[
  {
    "path": ".claude/commands/ship.md",
    "content": "---\ndescription: Create a new git branch, commit changes, and create a pull request\nallowed-tools: Bash(git:*), Bash(gh:*)\n---\n\n# Current Git State\n\n- Branch: !`git branch --show-current`\n- Status: !`git status --porcelain`\n- Recent commits: !`git log --oneline -5`\n\n# Arguments\n\n$ARGUMENTS - optional branch name\n\n# Workflow\n\n## 1. Validate\n\n- Confirm there are uncommitted changes (from status above). If none, stop.\n- Confirm current branch. If not on `main`, ask user before proceeding.\n- Warn if any .env or credential files would be staged.\n- Run `pnpm format:fix` in the root directory to fix formatting issues\n\n## 2. Create Branch\n\n- If `$ARGUMENTS` provided, use it as branch name\n- Otherwise, generate from task context using conventional prefixes: `feat/`, `fix/`, `chore/`, `refactor/`, `docs/` in kebab-case\n\n```bash\ngit checkout -b <branch-name>\n```\n\n## 3. Commit\n\n1. Run `git diff` to review all changes\n2. **Think about context**: What was the user trying to achieve? What problem does this solve?\n3. Draft commit message:\n   - Conventional commit format: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`\n   - **Write at the outcome level, not implementation level**\n   - Bad: \"change timeout from 5000 to 10000 in checker/monitor.go\"\n   - Good: \"increase monitor check timeout to handle slow API responses\"\n4. Stage relevant files (not .env/credentials)\n5. Commit with heredoc format:\n\n```bash\ngit commit -m \"$(cat <<'EOF'\n<message>\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n)\"\n```\n\n6. Run `git status` to verify success\n\n## 4. Push & Create PR\n\n```bash\ngit push -u origin <branch-name>\n```\n\nCreate PR with `gh`:\n\n- Title from commit message (same high-level framing)\n- Summary explains **outcomes**, not file changes\n  - Bad: \"Added `getMonitorStatus()` to monitor.ts, updated `Monitor` type\"\n  - Good: \"Status pages now reflect real-time monitor degradation states\"\n\n```bash\ngh pr create --title \"<title>\" --body \"$(cat <<'EOF'\n## Summary\n- <outcome 1>\n- <outcome 2>\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\nEOF\n)\"\n```\n\nReturn the PR URL.\n\n## Error Handling\n\nIf any step fails, report the error and stop. Don't proceed to the next step.\n"
  },
  {
    "path": ".dockerignore",
    "content": "# Dependencies\n**/node_modules\nnode_modules\n**/.pnpm-store\n.pnpm-store\n\n# Build outputs\n**/.next\n**/.turbo\n**/dist\n**/build\n**/.nuxt\n**/.output\n**/.cache\n\n# Environment files\n**/.env\n**/.env.*\n.env\n.env.*\n!.env.docker.example\n\n# Development files\n**/.vscode\n**/.idea\n**/.DS_Store\n.DS_Store\n**/*.log\n**/npm-debug.log\n**/yarn-debug.log\n**/yarn-error.log\n**/pnpm-debug.log\n\n# Testing\n**/coverage\n**/.nyc_output\n**/test-results\n**/__tests__\n**/tests\n**/*.test.ts\n**/*.test.tsx\n**/*.test.js\n**/*.spec.ts\n**/*.spec.tsx\n**/*.spec.js\n\n# Git\n**/.git\n**/.gitignore\n**/.gitattributes\n.git\n.gitignore\n\n# Documentation\n**/README.md\n**/CHANGELOG.md\n**/LICENSE\n**/*.md\n!packages/**/README.md\n\n# CI/CD\n**/.github\n.github\n**/.gitlab\n**/.circleci\n**/.drone.yml\n**/.travis.yml\n\n# Docker\n**/.dockerignore\n**/Dockerfile\n**/docker-compose*.yml\n**/docker-compose*.yaml\n.dockerignore\ndocker-compose*.yml\ndocker-compose*.yaml\n\n# Database files\nopenstatus.db\n**/*.db\n**/*.db-shm\n**/*.db-wal\n\n# Temporary files\n**/tmp\n**/temp\n**/.temp\n**/.tmp\n\n# IDE\n**/.vscode\n**/.idea\n**/.fleet\n\n# OS\n.DS_Store\n**/Thumbs.db\n\n# Other build artifacts\n**/.contentlayer\n**/public/build\n**/.svelte-kit"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: openstatusHQ\n"
  },
  {
    "path": ".github/workflows/api-preview.yml",
    "content": "name: Fly Preview API server\non:\n  workflow_dispatch:\n    inputs:\n      action:\n        description: \"Action to perform\"\n        required: true\n        type: choice\n        options:\n          - deploy\n          - destroy\n\npermissions:\n  contents: read\n  deployments: write\n\nenv:\n    FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}\n    FLY_REGION: ams\n    FLY_ORG: openstatus\n    APP_NAME: openstatus-api-preview-${{ github.ref_name }}\n\njobs:\n  deploy:\n    if: ${{ github.event.inputs.action == 'deploy' }}\n    runs-on: depot-ubuntu-24.04-4\n    timeout-minutes: 15\n    concurrency:\n      group: api-preview-${{ github.ref_name }}\n    environment:\n      name: api-preview-${{ github.ref_name }}\n      url: https://${{ env.APP_NAME }}.fly.dev\n    steps:\n      - name: Get code\n        uses: actions/checkout@v4\n\n      - name: Setup Fly.io CLI\n        uses: superfly/flyctl-actions/setup-flyctl@master\n\n      - name: Create app if not exists\n        run: flyctl apps create ${{ env.APP_NAME }} --org ${{ env.FLY_ORG }} || true\n\n      - name: Set secrets\n        run: |\n          flyctl secrets set \\\n            DATABASE_URL=\"${{ secrets.STAGING_DB_URL }}\" \\\n            DATABASE_AUTH_TOKEN=\"${{ secrets.STAGING_DB_AUTH_TOKEN }}\" \\\n            RESEND_API_KEY=\"${{ secrets.STAGING_RESEND_API_KEY }}\" \\\n            UPSTASH_REDIS_REST_URL=test \\\n            UPSTASH_REDIS_REST_TOKEN=test \\\n            GCP_PROJECT_ID=test \\\n            NEXT_PUBLIC_OPENPANEL_CLIENT_ID=test \\\n            OPENPANEL_CLIENT_SECRET=test \\\n            --app ${{ env.APP_NAME }}\n\n      - name: Deploy to Fly.io\n        run: |\n          flyctl deploy \\\n            --config apps/server/fly.toml \\\n            --app ${{ env.APP_NAME }} \\\n            --region ${{ env.FLY_REGION }} \\\n            --vm-size shared-cpu-1x \\\n            --yes\n\n  destroy:\n    if: ${{ github.event.inputs.action == 'destroy' }}\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    steps:\n      - name: Setup Fly.io CLI\n        uses: superfly/flyctl-actions/setup-flyctl@master\n\n      - name: Destroy app\n        run: flyctl apps destroy ${{ env.APP_NAME }} --yes || true\n\n      - name: Clean up GitHub environment\n        uses: strumwolf/delete-deployment-environment@v2\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          environment: api-preview-${{ github.ref_name }}\n"
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "content": "name: Claude Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize, ready_for_review, reopened]\n    # Optional: Only run on specific file changes\n    # paths:\n    #   - \"src/**/*.ts\"\n    #   - \"src/**/*.tsx\"\n    #   - \"src/**/*.js\"\n    #   - \"src/**/*.jsx\"\n\njobs:\n  claude-review:\n    # Optional: Filter by PR author\n    if: |\n      github.event.pull_request.author_association == 'MEMBER' ||\n      github.event.pull_request.author_association == 'OWNER'\n\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: write\n      issues: read\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Run Claude Code Review\n        id: claude-review\n        uses: anthropics/claude-code-action@v1\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n          plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'\n          plugins: 'code-review@claude-code-plugins'\n          prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'\n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://code.claude.com/docs/en/cli-reference for available options\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      (\n        github.event_name == 'issue_comment' &&\n        contains(github.event.comment.body, '@claude') &&\n        (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')\n      ) ||\n      (\n        github.event_name == 'pull_request_review_comment' &&\n        contains(github.event.comment.body, '@claude') &&\n        (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')\n      ) ||\n      (\n        github.event_name == 'pull_request_review' &&\n        contains(github.event.review.body, '@claude') &&\n        (github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'MEMBER' || github.event.review.author_association == 'COLLABORATOR')\n      ) ||\n      (\n        github.event_name == 'issues' &&\n        (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&\n        (github.event.issue.author_association == 'OWNER' || github.event.issue.author_association == 'MEMBER' || github.event.issue.author_association == 'COLLABORATOR')\n      )\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    permissions:\n      contents: write\n      pull-requests: write\n      issues: write\n      id-token: write\n      actions: read # Required for Claude to read CI results on PRs\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n\n          # This is an optional setting that allows Claude to read CI results on PRs\n          additional_permissions: |\n            actions: read\n\n          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.\n          # prompt: 'Update the pull request description to include a summary of changes.'\n\n          # Optional: Add claude_args to customize behavior and configuration\n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://code.claude.com/docs/en/cli-reference for available options\n          # claude_args: '--allowed-tools Bash(gh pr:*)'\n\n"
  },
  {
    "path": ".github/workflows/deploy-checker.yml",
    "content": "name: Fly Deploy Checker\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"apps/checker/**\"\njobs:\n  deploy-checker:\n    name: Deploy Checker\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@v4\n      - uses: superfly/flyctl-actions/setup-flyctl@master\n      - working-directory: apps/checker\n        name: Deploy Checker\n        run: |\n          flyctl deploy --remote-only --wait-timeout=500\n        env:\n          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/deploy-private-location.yml",
    "content": "name: Fly Deploy Private Location\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"apps/private-location/**\"\n      - \"apps/checker/**\"\njobs:\n  deploy-private-location:\n    name: Deploy Private Location\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    permissions:\n      contents: read\n    steps:\n      - uses: actions/checkout@v4\n      - uses: superfly/flyctl-actions/setup-flyctl@master\n      - working-directory: apps/private-location\n        name: Deploy Private Location\n        run: |\n          flyctl deploy --remote-only --wait-timeout=500\n        env:\n          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/deploy-workflows.yml",
    "content": "name: Fly Deploy Workflows\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"apps/workflows/**\"\n      - \"packages/db/**\"\n      - \"packages/emails/**\"\n      - \"packages/utils/**\"\n      - \"packages/tsconfig/**\"\n      - \"packages/notifications/**\"\njobs:\n  deploy-workflows:\n    name: Deploy Workflows\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@v4\n      - uses: superfly/flyctl-actions/setup-flyctl@master\n      - run: flyctl deploy --config apps/workflows/fly.toml\n          --dockerfile  apps/workflows/Dockerfile --remote-only --wait-timeout=500\n        env:\n          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Fly Deploy\non:\n  push:\n    branches:\n      - main\njobs:\n  deploy:\n    name: Deploy API\n    runs-on: depot-ubuntu-24.04-4\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@v4\n      - uses: superfly/flyctl-actions/setup-flyctl@master\n      - run:\n          flyctl deploy --config apps/server/fly.toml\n          --dockerfile  apps/server/Dockerfile --remote-only --wait-timeout=500\n        env:\n          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/docker-publish-dev.yml",
    "content": "name: Publish Docker Images (Development)\n\non:\n  workflow_dispatch:\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: openstatus\n\njobs:\n  build-and-push-dev:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write\n      packages: write\n    \n    strategy:\n      matrix:\n        service: [server, dashboard, workflows, private-location, status-page, checker]\n        include:\n          - service: server\n            context: .\n            dockerfile: apps/server/Dockerfile\n            port: 3001\n          - service: dashboard\n            context: .\n            dockerfile: apps/dashboard/Dockerfile\n            port: 3002\n          - service: workflows\n            context: .\n            dockerfile: apps/workflows/Dockerfile\n            port: 3000\n          - service: private-location\n            context: apps/private-location\n            dockerfile: apps/private-location/Dockerfile\n            port: 8081\n          - service: status-page\n            context: .\n            dockerfile: apps/status-page/Dockerfile\n            port: 3003\n          - service: checker\n            context: apps/checker\n            dockerfile: apps/checker/Dockerfile\n            port: 8082\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}\n          tags: |\n            type=ref,event=branch,suffix=-${{ matrix.service }}\n            type=ref,event=pr,suffix=-${{ matrix.service }}\n            type=sha,prefix=dev-,suffix=-${{ matrix.service }}\n            type=raw,value=dev-latest,suffix=-${{ matrix.service }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v5\n        with:\n          context: ${{ matrix.context }}\n          file: ${{ matrix.dockerfile }}\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          provenance: false\n\n  test-images:\n    runs-on: ubuntu-latest\n    needs: build-and-push-dev\n    if: github.event_name == 'pull_request'\n    \n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Compose\n        run: |\n          sudo curl -L \"https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose\n          sudo chmod +x /usr/local/bin/docker-compose\n\n      - name: Create test compose file\n        run: |\n          cp docker-compose.github-packages.yaml docker-compose.test.yaml\n          # Replace latest tags with dev-latest for testing\n          sed -i 's/:latest/:dev-latest/g' docker-compose.test.yaml\n\n      - name: Test service startup\n        run: |\n          # Start external services first\n          docker-compose -f docker-compose.test.yaml up -d libsql tinybird-local\n          \n          # Wait for external services to be healthy\n          timeout 120 bash -c 'until docker-compose -f docker-compose.test.yaml ps libsql | grep -q \"healthy\"; do sleep 5; done'\n          timeout 120 bash -c 'until docker-compose -f docker-compose.test.yaml ps tinybird-local | grep -q \"healthy\"; do sleep 5; done'\n          \n          # Start one service for basic testing\n          docker-compose -f docker-compose.test.yaml up -d workflows\n          \n          # Wait and check if it's running\n          sleep 30\n          if ! docker-compose -f docker-compose.test.yaml ps workflows | grep -q \"Up\"; then\n            echo \"Workflows service failed to start\"\n            docker-compose -f docker-compose.test.yaml logs workflows\n            exit 1\n          fi\n\n      - name: Cleanup\n        if: always()\n        run: |\n          docker-compose -f docker-compose.test.yaml down -v\n"
  },
  {
    "path": ".github/workflows/docker-publish.yml",
    "content": "name: Publish Docker Images\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"apps/server/**\"\n      - \"apps/dashboard/**\"\n      - \"apps/workflows/**\"\n      - \"apps/private-location/**\"\n      - \"apps/status-page/**\"\n      - \"apps/checker/**\"\n      - \"packages/**\"\n      - \"docker-compose.yaml\"\n  workflow_dispatch:\n    inputs:\n      services:\n        description: 'Services to build (comma-separated)'\n        required: false\n        default: 'server,dashboard,workflows,private-location,status-page,checker'\n        type: string\n\nconcurrency:\n  group: docker-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: openstatus\n\njobs:\n  prepare:\n    runs-on: ubuntu-latest\n    outputs:\n      services: ${{ steps.set-services.outputs.services }}\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 2\n\n      - name: Determine services to build\n        id: set-services\n        run: |\n          if [ \"${{ github.event_name }}\" = \"workflow_dispatch\" ]; then\n            # Convert comma-separated input to JSON array\n            SERVICES=$(echo \"${{ inputs.services }}\" | tr ',' '\\n' | jq -R . | jq -sc .)\n          else\n            # Detect changed files\n            CHANGED=$(git diff --name-only HEAD~1 HEAD)\n            SERVICES_LIST=()\n\n            # packages/** changes affect all services\n            if echo \"$CHANGED\" | grep -q '^packages/'; then\n              SERVICES_LIST=(\"server\" \"dashboard\" \"workflows\" \"private-location\" \"status-page\" \"checker\")\n            else\n              for svc in server dashboard workflows private-location status-page checker; do\n                if echo \"$CHANGED\" | grep -q \"^apps/$svc/\"; then\n                  SERVICES_LIST+=(\"$svc\")\n                fi\n              done\n            fi\n\n            SERVICES=$(printf '%s\\n' \"${SERVICES_LIST[@]}\" | jq -R . | jq -sc .)\n          fi\n\n          echo \"services=$SERVICES\" >> \"$GITHUB_OUTPUT\"\n          echo \"Building services: $SERVICES\"\n\n  build-and-push:\n    runs-on: ubuntu-latest\n    needs: [prepare]\n    if: needs.prepare.outputs.services != '[]'\n    timeout-minutes: 30\n    permissions:\n      contents: read\n      id-token: write\n      packages: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        service: ${{ fromJson(needs.prepare.outputs.services) }}\n        include:\n          - service: server\n            context: .\n            dockerfile: apps/server/Dockerfile\n          - service: dashboard\n            context: .\n            dockerfile: apps/dashboard/Dockerfile\n          - service: workflows\n            context: .\n            dockerfile: apps/workflows/Dockerfile\n          - service: private-location\n            context: apps/private-location\n            dockerfile: apps/private-location/Dockerfile\n          - service: status-page\n            context: .\n            dockerfile: apps/status-page/Dockerfile\n          - service: checker\n            context: apps/checker\n            dockerfile: apps/checker/Dockerfile\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Lowercase owner\n        run: echo \"OWNER_LC=${GITHUB_REPOSITORY_OWNER,,}\" >> $GITHUB_ENV\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.OWNER_LC }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}\n          tags: |\n            type=ref,event=branch\n            type=ref,event=pr\n            type=sha,prefix=\n            type=raw,value=latest,enable={{is_default_branch}}\n\n      - name: Build and push Docker image\n        id: build\n        uses: docker/build-push-action@v6\n        with:\n          context: ${{ matrix.context }}\n          file: ${{ matrix.dockerfile }}\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64,linux/arm64\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          provenance: false\n\n      - name: Generate SBOM\n        uses: anchore/sbom-action@v0\n        with:\n          image: ${{ env.REGISTRY }}/${{ env.OWNER_LC }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}@${{ steps.build.outputs.digest }}\n          format: spdx-json\n          output-file: sbom-${{ matrix.service }}.spdx.json\n\n      - name: Upload SBOM\n        uses: actions/upload-artifact@v4\n        with:\n          name: sbom-${{ matrix.service }}\n          path: sbom-${{ matrix.service }}.spdx.json\n          retention-days: 30\n"
  },
  {
    "path": ".github/workflows/dx.yml",
    "content": "# https://github.com/kentcdodds/kentcdodds.com/blob/main/.github/workflows/deployment.yml\nname: DX Check\non:\n  push:\n    branches:\n      - \"main\"\n  pull_request:\n    branches: [main]\n\njobs:\n  dx:\n    name: 🧑‍💻 DX checker\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    services:\n      sqld:\n        image: ghcr.io/tursodatabase/libsql-server:latest\n        ports:\n          - 8080:8080\n        # env:\n        #   SQLD_HTTP_AUTH: \"basic:token\"\n\n    env:\n      TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n      TURBO_TEAM: ${{ secrets.TURBO_TEAM }}\n      DATABASE_URL: http://127.0.0.1:8080\n      DATABASE_AUTH_TOKEN: \"basic:token\"\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v4\n\n      - name: Set up pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.26.0\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: 📥 Download deps\n        run: pnpm install\n\n      - name: 🔥 Install bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: 🔥  DX task\n        run: pnpm dx\n"
  },
  {
    "path": ".github/workflows/go-tests.yml",
    "content": "name: Go Tests\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - '*.*.*'\n  pull_request:\n    branches:\n      - '**'\n    paths:\n      - \"apps/checker/**\"\n      - \"apps/private-location/**\"\n\njobs:\n  ci:\n    name: Continuous Integration\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v5\n        with:\n          go-version: '>=1.25.0'\n      - name: Run test Checker\n        run: go test -timeout 30s -race -count=1 ./...\n        working-directory: apps/checker\n      - name: Run test Private Location\n        run: go test -timeout 30s -race -count=1 ./...\n        working-directory: apps/private-location\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "# https://github.com/kentcdodds/kentcdodds.com/blob/main/.github/workflows/deployment.yml\nname: autofix.ci # needed to securely identify the workflow\non:\n  pull_request:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.number || github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\n\njobs:\n  autofix:\n    name: autofix\n    runs-on: ubuntu-latest\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v4\n\n      - name: Set up pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.26.0\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: 📥 Download deps\n        run: pnpm install\n\n      - name: 🔬 Lint\n        run: pnpm format\n      - name: Apply fixes\n        uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a\n        with:\n          commit-message: 'ci: apply automated fixes'\n"
  },
  {
    "path": ".github/workflows/migrate.yml",
    "content": "name: Migrate DB\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"packages/db/drizzle/**\"\njobs:\n  migrate:\n    name: 🗃️ Migrate DB\n    runs-on: ubuntu-latest\n    env:\n      DATABASE_URL:  ${{ secrets.DATABASE_URL }}\n      DATABASE_AUTH_TOKEN:  ${{ secrets.DATABASE_AUTH_TOKEN }}\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v4\n\n      - name: Set up pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.26.0\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: 🔥 Install bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: 📥 Download deps\n        run: pnpm install\n\n      - name: 🗃️ Run migrations\n        run: pnpm migrate\n        working-directory: ./packages/db\n"
  },
  {
    "path": ".github/workflows/publish-checker.yml",
    "content": "name: Publish Checker\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"apps/checker/**\"\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: checker\n\njobs:\n  build-checker:\n    runs-on: ubuntu-latest\n    # Permissions to use OIDC token authentication\n    permissions:\n      contents: read\n      id-token: write\n      # Allows pushing to the GitHub Container Registry\n      packages: write\n    steps:\n      - uses: actions/checkout@v3\n      - uses: depot/setup-action@v1\n      - name: Log in to the Container registry\n        uses: docker/login-action@v3\n        with:\n            registry: ${{ env.REGISTRY }}\n            username: ${{ github.repository_owner }}\n            password: ${{ secrets.GITHUB_TOKEN }}\n      - name: Build and push\n        uses: depot/build-push-action@v1\n        with:\n          project: 9cknw183m8\n          context: apps/checker\n          tags: ghcr.io/openstatushq/checker:latest\n          platforms: linux/amd64,linux/arm64\n          push: true\n  build-private-location:\n    runs-on: ubuntu-latest\n    # Permissions to use OIDC token authentication\n    permissions:\n      contents: read\n      id-token: write\n      # Allows pushing to the GitHub Container Registry\n      packages: write\n    steps:\n      - uses: actions/checkout@v3\n      - uses: depot/setup-action@v1\n      - name: Log in to the Container registry\n        uses: docker/login-action@v3\n        with:\n            registry: ${{ env.REGISTRY }}\n            username: ${{ github.repository_owner }}\n            password: ${{ secrets.GITHUB_TOKEN }}\n      - name: Build and push\n        uses: depot/build-push-action@v1\n        with:\n          file: apps/checker/private-location.Dockerfile\n          project: 9cknw183m8\n          context: apps/checker\n          tags: ghcr.io/openstatushq/private-location:latest\n          platforms: linux/amd64,linux/arm64\n          push: true\n"
  },
  {
    "path": ".github/workflows/synthetic.yml",
    "content": "name: Run OpenStatus Synthetics CI\n\non:\n  workflow_run:\n      workflows: ['Fly Deploy']\n      types: [completed]\n      branches:\n        - main\n  repository_dispatch:\n      types:\n        - 'vercel.deployment.success'\n      branches:\n        - main\n\njobs:\n  synthetic_ci:\n    runs-on: ubuntu-latest\n    name: Run OpenStatus Synthetics CI\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Run OpenStatus Synthetics CI\n        uses: openstatushq/openstatus-github-action@v1\n        with:\n          api_key: ${{ secrets.OPENSTATUS_API_KEY }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "# https://github.com/kentcdodds/kentcdodds.com/blob/main/.github/workflows/deployment.yml\nname: Tests\non:\n  push:\n    branches:\n      - \"main\"\n  pull_request:\n    branches: [main]\n\njobs:\n  tests:\n    name: 🧪 Tests\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    services:\n      sqld:\n        image: ghcr.io/tursodatabase/libsql-server:latest\n        ports:\n          - 8080:8080\n        # env:\n        #   SQLD_HTTP_AUTH: \"basic:token\"\n\n    env:\n      TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n      TURBO_TEAM: ${{ secrets.TURBO_TEAM }}\n      DATABASE_URL: http://127.0.0.1:8080\n      DATABASE_AUTH_TOKEN: \"basic:token\"\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n\n      - name: Set up pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.26.0\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          cache: \"pnpm\"\n\n      - name: 🔥 Install bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: 📥 Download deps\n        run: pnpm install\n\n      - name: 🗃️ Run migrations\n        run: pnpm migrate\n        working-directory: ./packages/db\n\n      - name: 💽  Seed database\n        run: pnpm seed\n        working-directory: ./packages/db\n\n      - name: 🧪 Tests\n        run: pnpm test\n"
  },
  {
    "path": ".github/workflows/workflow-preview.yml",
    "content": "name: Fly Preview Workflows\non:\n  workflow_dispatch:\n    inputs:\n      action:\n        description: \"Action to perform\"\n        required: true\n        type: choice\n        options:\n          - deploy\n          - destroy\n\npermissions:\n  contents: read\n  deployments: write\n\nenv:\n    FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}\n    FLY_REGION: ams\n    FLY_ORG: openstatus\n    APP_NAME: openstatus-workflows-preview-${{ github.ref_name }}\n\njobs:\n  deploy:\n    if: ${{ github.event.inputs.action == 'deploy' }}\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    concurrency:\n      group: workflows-preview-${{ github.ref_name }}\n    environment:\n      name: workflows-preview-${{ github.ref_name }}\n      url: https://${{ env.APP_NAME }}.fly.dev\n    steps:\n      - name: Get code\n        uses: actions/checkout@v4\n\n      - name: Setup Fly.io CLI\n        uses: superfly/flyctl-actions/setup-flyctl@master\n\n      - name: Create app if not exists\n        run: flyctl apps create ${{ env.APP_NAME }} --org ${{ env.FLY_ORG }} || true\n\n      - name: Set secrets\n        run: |\n          flyctl secrets set \\\n            DATABASE_URL=\"${{ secrets.STAGING_DB_URL }}\" \\\n            DATABASE_AUTH_TOKEN=\"${{ secrets.STAGING_DB_AUTH_TOKEN }}\" \\\n            RESEND_API_KEY=\"${{ secrets.STAGING_RESEND_API_KEY }}\" \\\n            UPSTASH_REDIS_REST_URL=test \\\n            UPSTASH_REDIS_REST_TOKEN=test \\\n            GCP_PROJECT_ID=test \\\n            --app ${{ env.APP_NAME }}\n\n      - name: Deploy to Fly.io\n        run: |\n          flyctl deploy \\\n            --config apps/workflows/fly.toml \\\n            --app ${{ env.APP_NAME }} \\\n            --region ${{ env.FLY_REGION }} \\\n            --vm-size shared-cpu-1x \\\n            --yes\n\n  destroy:\n    if: ${{ github.event.inputs.action == 'destroy' }}\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    steps:\n      - name: Setup Fly.io CLI\n        uses: superfly/flyctl-actions/setup-flyctl@master\n\n      - name: Destroy app\n        run: flyctl apps destroy ${{ env.APP_NAME }} --yes || true\n\n      - name: Clean up GitHub environment\n        uses: strumwolf/delete-deployment-environment@v2\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          environment: workflows-preview-${{ github.ref_name }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\n.pnp\n.pnp.js\n\n# testing\ncoverage\n\n# next.js\n.next/\nout/\nbuild\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n.env*.local\n\n# turbo\n.turbo\n\n# vercel\n.vercel\n.venv\n# packages\ndist\npackages/emails/.react-email\npackages/db/database.db\npackages/db/openstatus.db\npackages/db/database.db-wal\npackages/db/database.db-shm\npackages/db/openstatus.db-shm\npackages/db/openstatus.db-wal\nopenstatus.db-wal\nopenstatus.db\nopenstatus.db-shm\n\npackages/tinybird/.tinyb\napps/web/tsconfig.tsbuildinfo\nopenstatus-test.db\nopenstatus-test.db-shm\nopenstatus-test.db-wal\n\n#webstorm\n.idea\n\n.wrangler\napps/web/.env.dev\npackages/db/.env.dev\nopenstatus-dev.db\nopenstatus-dev.db-wal\nopenstatus-dev.db-shm\napps/ingest-worker/.production.vars\napps/ingest-worker/.dev.vars\napps/alerting-engine/tmp/\n\n.env.docker\n\n# UI Registry build output\napps/web/public/r\npackages/ui/public/r\n# For vibing\nresearch.md\nplan.md\n\n.zed\n\n*.tsbuildinfo\n\n.claude/skills/**\n.agents/skills/**\nskills-lock.json\n"
  },
  {
    "path": ".koyebignore",
    "content": "*\n!.koyebignore\n\n!/apps/checker/*\n"
  },
  {
    "path": ".npmrc",
    "content": "auto-install-peers = true\n"
  },
  {
    "path": ".oxlintrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n  \"ignorePatterns\": [\"**/*.test.ts\", \"**/*_pb.ts\"]\n}\n"
  },
  {
    "path": ".prettierignore",
    "content": "**/.content-collections\n"
  },
  {
    "path": ".stacked.toml",
    "content": "mainBranch = \"main\"\ndraft = true\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.codeActionsOnSave\": {\n    \"source.organizeImports.biome\": \"explicit\"\n  },\n  \"editor.defaultFormatter\": \"biomejs.biome\",\n  \"editor.formatOnSave\": true,\n  \"typescript.tsdk\": \"node_modules/typescript/lib\",\n\n  \"typescript.enablePromptUseWorkspaceTsdk\": true,\n  \"[dotenv]\": {\n    \"editor.defaultFormatter\": \"foxundermoon.shell-format\"\n  },\n  \"[dockerfile]\": {\n    \"editor.defaultFormatter\": \"ms-azuretools.vscode-docker\"\n  },\n  \"[xml]\": {\n    \"editor.defaultFormatter\": \"redhat.vscode-xml\"\n  },\n  \"[go]\": {\n    \"editor.defaultFormatter\": \"golang.go\"\n  },\n  \"[ignore]\": {\n    \"editor.defaultFormatter\": \"foxundermoon.shell-format\"\n  },\n  \"go.lintTool\": \"golangci-lint\",\n  \"go.lintFlags\": [\"--fast\"],\n  \"gopls\": {\n    \"ui.diagnostic.analyses\": {\n      \"fieldalignment\": true\n    }\n  }\n}\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# Agent.md\n\nThis file provides a comprehensive overview of the OpenStatus project, its architecture, and development conventions to be used as instructional context for future interactions.\n\n## Project Overview\n\nOpenStatus is an open-source synthetic monitoring platform. It allows users to monitor their websites and APIs from multiple locations and receive notifications when they are down or slow.\n\nThe project is a monorepo managed with pnpm workspaces and Turborepo. It consists of several applications and packages that work together to provide a complete monitoring solution.\n\n### Core Technologies\n\n-   **Frontend:**\n    -   Next.js (with Turbopack)\n    -   React\n    -   Tailwind CSS\n    -   shadcn/ui\n    -   tRPC\n-   **Backend:**\n    -   Hono (Node.js framework)\n    -   Go\n-   **Database:**\n    -   Turso (libSQL)\n    -   Drizzle ORM\n-   **Data Analytics:**\n    -   Tinybird\n-   **Authentication:**\n    -   NextAuth.js\n-   **Build System:**\n    -   Turborepo\n\n### Architecture\n\nThe OpenStatus platform is composed of three main applications:\n\n-   **`apps/dashboard`**: A Next.js application that provides the main user interface for managing monitors, viewing status pages, and configuring notifications.\n-   **`apps/server`**: A Hono-based backend server that provides the API for the dashboard application.\n-   **`apps/checker`**: A Go application responsible for performing the actual monitoring checks from different locations.\n\nThese applications are supported by a collection of shared packages in the `packages/` directory, which provide common functionality such as database access, UI components, and utility functions.\n\n## Building and Running\n\nThe project can be run using Docker (recommended) or a manual setup.\n\n### With Docker\n\n1.  Copy the example environment file:\n    ```sh\n    cp .env.docker.example .env.docker\n    ```\n2.  Start all services:\n    ```sh\n    docker compose up -d\n    ```\n3.  Access the applications:\n    -   Dashboard: `http://localhost:3002`\n    -   Status Pages: `http://localhost:3003`\n\n### Manual Setup\n\n1.  Install dependencies:\n    ```sh\n    pnpm install\n    ```\n2.  Initialize the development environment:\n    ```sh\n    pnpm dx\n    ```\n3.  Run a specific application:\n    ```sh\n    pnpm dev:dashboard\n    pnpm dev:status-page\n    pnpm dev:web\n    ```\n\n### Running Tests\n\nTo run the test suite, use the following command:\n\nBefore running the test you should launch turso dev in a separate terminal:\n```sh\nturso dev\n```\n\nThen, seed the database with test data:\n\n```sh\ncd packages/db\npnpm migrate \npnpm seed\n```\n\nThen run the tests with:\n\n```sh\npnpm test\n```\n\n## Development Conventions\n\n-   **Monorepo:** The project is organized as a monorepo using pnpm workspaces. All applications and packages are located in the `apps/` and `packages/` directories, respectively.\n-   **Build System:** Turborepo is used to manage the build process. The `turbo.json` file defines the build pipeline and dependencies between tasks.\n-   **Linting and Formatting:** The project uses Biome for linting and formatting. The configuration can be found in the `biome.jsonc` file.\n-   **Code Generation:** The project uses `drizzle-kit` for database schema migrations.\n-   **API:** The backend API is built using Hono and tRPC. The API is documented using OpenAPI.\n"
  },
  {
    "path": "CONTRIBUTING.MD",
    "content": "# Contribution Guidelines\n\nThank you for considering contributing to this project! We appreciate your efforts to make it better.\n\nTo contribute to this project, please follow these guidelines:\n\n## Table of Contents\n\n- [Reporting Issues](#reporting-issues)\n- [Feature Requests](#feature-requests)\n- [Submitting Changes](#submitting-changes)\n- [Coding Conventions](#coding-conventions)\n- [Documentation](#documentation)\n\n\n## Reporting Issues\n\nIf you encounter any problems or bugs while using this project, please report them by opening a GitHub issue. Before creating a new issue, please check if a similar one already exists to avoid duplicates. When reporting issues, provide a clear and concise description of the problem, including steps to reproduce it, expected behavior, and any relevant screenshots or error messages.\n\n## Feature Requests\n\nIf you have ideas for new features or improvements, you can submit a GitHub issue as well. Clearly describe the feature or improvement you would like to see and provide any additional context or examples that might be helpful. Feature requests help us understand your needs and prioritize the project's development.\n\n## Submitting Changes\n\nTo contribute code changes, follow these steps:\n\n1. Fork the repository and create a new branch for your changes.\n2. Ensure that your code follows the project's coding conventions and style guide.\n3. Make commits with clear and descriptive messages. Each commit should have a single logical purpose.\n4. Push your branch to your forked repository.\n5. Open a pull request (PR) from your branch to the original repository's `main` branch.\n6. Provide a detailed description of your changes in the PR, including any related issues or feature requests.\n\nA project maintainer will review your PR, provide feedback if necessary, and merge it once it meets the project's standards.\n\n## Coding Conventions\n\nPlease adhere to the existing coding conventions and style guide used in this project. Consistent coding styles improve code readability and maintainability. If you're unsure about any aspect of the coding conventions, feel free to ask for clarification in your PR.\n\n## Documentation\n\nImprovements to documentation are always welcome. If you find any inaccuracies, missing information, or have suggestions for improving the documentation, you can contribute by submitting a PR with your changes. Make sure to clearly explain the purpose of the documentation update and provide relevant examples, if applicable.\n\n"
  },
  {
    "path": "COOLIFY_DEPLOYMENT.md",
    "content": "# Coolify Deployment Guide\n\nThis guide explains how to deploy OpenStatus using Coolify with pre-built Docker images from GitHub Container Registry.\n\n## Prerequisites\n\n- Coolify instance (self-hosted or cloud)\n- GitHub account with access to the repository\n- Environment variables configured\n\n## Available Docker Images\n\nAll images are published to `ghcr.io/openstatusHQ/openstatus-*`:\n\n- `ghcr.io/openstatusHQ/openstatus-server:latest` - Main API server\n- `ghcr.io/openstatusHQ/openstatus-dashboard:latest` - Web dashboard\n- `ghcr.io/openstatusHQ/openstatus-workflows:latest` - Workflow engine\n- `ghcr.io/openstatusHQ/openstatus-private-location:latest` - Private monitoring agent\n- `ghcr.io/openstatusHQ/openstatus-status-page:latest` - Public status page\n- `ghcr.io/openstatusHQ/openstatus-checker:latest` - Monitoring checker service\n\n## Coolify Setup\n\n### 1. Create a New Application\n\n1. In Coolify, click \"Add New\" → \"Application\"\n2. Choose \"Docker\" as the application type\n3. Select \"Public Repository\" for the image source\n\n### 2. Configure Each Service\n\n#### Server Service\n- **Image**: `ghcr.io/openstatusHQ/openstatus-server:latest`\n- **Port**: 3000\n- **Environment Variables**:\n  ```yaml\n  DATABASE_URL: http://libsql:8080\n  PORT: 3000\n  # Add other required variables from .env.docker\n  ```\n\n#### Dashboard Service\n- **Image**: `ghcr.io/openstatusHQ/openstatus-dashboard:latest`\n- **Port**: 3000\n- **Environment Variables**:\n  ```yaml\n  DATABASE_URL: http://libsql:8080\n  PORT: 3000\n  HOSTNAME: 0.0.0.0\n  AUTH_TRUST_HOST: true\n  ```\n\n#### Workflows Service\n- **Image**: `ghcr.io/openstatusHQ/openstatus-workflows:latest`\n- **Port**: 3000\n- **Environment Variables**:\n  ```yaml\n  DATABASE_URL: http://libsql:8080\n  PORT: 3000\n  ```\n\n#### Private Location Service\n- **Image**: `ghcr.io/openstatusHQ/openstatus-private-location:latest`\n- **Port**: 8080\n- **Environment Variables**:\n  ```yaml\n  DB_URL: http://libsql:8080\n  TINYBIRD_URL: http://tinybird-local:7181\n  GIN_MODE: release\n  PORT: 8080\n  ```\n\n#### Checker Service\n- **Image**: `ghcr.io/openstatusHQ/openstatus-checker:latest`\n- **Port**: 8080\n- **Environment Variables**:\n  ```yaml\n  DATABASE_URL: http://libsql:8080\n  PORT: 8080\n  ```\n\n#### Status Page Service\n- **Image**: `ghcr.io/openstatusHQ/openstatus-status-page:latest`\n- **Port**: 3000\n- **Environment Variables**:\n  ```yaml\n  DATABASE_URL: http://libsql:8080\n  PORT: 3000\n  HOSTNAME: 0.0.0.0\n  AUTH_TRUST_HOST: true\n  ```\n\n### 3. External Dependencies\n\n#### LibSQL Database\n- **Image**: `ghcr.io/tursodatabase/libsql-server:latest`\n- **Port**: 8080\n- **Environment Variables**:\n  ```yaml\n  SQLD_NODE: primary\n  ```\n\n#### TinyBird (Optional)\n- **Image**: `tinybirdco/tinybird-local:latest`\n- **Port**: 7181\n- **Environment Variables**:\n  ```yaml\n  COMPATIBILITY_MODE: 1\n  ```\n\n### 4. Network Configuration\n\n1. Create a shared network for all services\n2. Ensure services can communicate using container names\n3. Configure health checks for each service\n\n### 5. Deployment Order\n\nDeploy services in this order:\n1. LibSQL (database)\n2. TinyBird (if used)\n3. Workflows\n4. Server\n5. Private Location\n6. Checker\n7. Dashboard\n8. Status Page\n\n## Environment Variables\n\nCreate a `.env` file with all required variables. Refer to `.env.docker.example` in the repository for the complete list.\n\n## Health Checks\n\nAll images include built-in health checks:\n- **Server/Workflows**: `curl -f http://localhost:3000/ping`\n- **Dashboard/Status Page**: `curl -f http://localhost:3000/`\n- **Private Location**: `wget --spider -q http://localhost:8080/health`\n- **Checker**: `curl -f http://localhost:8080/health`\n\n## Version Management\n\n- `latest` tag points to the latest main branch build\n- Specific commits are tagged with their SHA\n- Use specific tags for production deployments\n\n## Troubleshooting\n\n### Image Pull Issues\nEnsure Coolify has access to GitHub Container Registry:\n1. Add GitHub token as a registry credential in Coolify\n2. Use `GITHUB_TOKEN` with `packages: read` permissions\n\n### Service Communication\n- Verify all services are on the same network\n- Check container names match the configuration\n- Ensure ports are correctly mapped\n\n### Database Connection\n- Wait for LibSQL to be fully healthy before starting other services\n- Verify the DATABASE_URL format: `http://libsql:8080`\n\n## Updates\n\nImages are automatically built and pushed when:\n- Code is pushed to the main branch\n- Manual workflow dispatch is triggered\n\nTo update in Coolify:\n1. Pull new images\n2. Redeploy services in the correct order\n3. Verify health checks pass\n\n## Support\n\n- Check the GitHub Actions workflows for build status\n- Review container logs in Coolify\n- Open an issue for deployment problems\n"
  },
  {
    "path": "COOLIFY_ENVIRONMENT_GUIDE.md",
    "content": "# Coolify Environment Variables Setup Guide\n\nThis guide explains how to configure environment variables for OpenStatus deployment in Coolify.\n\n## 🚀 Quick Setup\n\n### Step 1: Import the Stack\n1. In Coolify dashboard, click **\"New Service\"** → **\"Docker Compose\"**\n2. Choose **\"Import from URL\"** and enter:\n   ```\n   https://raw.githubusercontent.com/openstatusHQ/openstatus/main/coolify-deployment.yaml\n   ```\n3. Click **\"Deploy\"**\n\n### Step 2: Configure Environment Variables\n1. After deployment, click on the **OpenStatus stack**\n2. Go to **\"Settings\"** → **\"Environment Variables\"**\n3. Add the required variables below\n\n## 🔧 Required Environment Variables\n\n### **Core Database & Authentication**\n```bash\n# Database Connection\nDATABASE_URL=http://libsql:8080\nDATABASE_AUTH_TOKEN=\n\n# Authentication\nAUTH_SECRET=your-32-character-secret-here\nNEXT_PUBLIC_URL=https://your-domain.com\nSELF_HOST=true\n```\n\n### **Email Service (Required for Login)**\n```bash\nRESEND_API_KEY=re_your_resend_api_key_here\n```\n\n## 🔧 Optional Environment Variables\n\n### **Analytics (TinyBird)**\n```bash\nTINY_BIRD_API_KEY=your_tinybird_api_key\nTINYBIRD_URL=http://tinybird:7181\n```\n\n### **OAuth Providers**\n```bash\n# GitHub OAuth\nAUTH_GITHUB_ID=your_github_oauth_id\nAUTH_GITHUB_SECRET=your_github_oauth_secret\n\n# Google OAuth\nAUTH_GOOGLE_ID=your_google_oauth_id\nAUTH_GOOGLE_SECRET=your_google_oauth_secret\n```\n\n### **Redis & Queue (Optional)**\n```bash\nUPSTASH_REDIS_REST_URL=http://localhost:6379\nUPSTASH_REDIS_REST_TOKEN=your_redis_token\nQSTASH_CURRENT_SIGNING_KEY=your_qstash_key\nQSTASH_NEXT_SIGNING_KEY=your_qstash_key\nQSTASH_TOKEN=your_qstash_token\nQSTASH_URL=https://qstash.upstash.io/v1/publish/\n```\n\n### **Google Cloud (Optional)**\n```bash\nGCP_PROJECT_ID=your_gcp_project_id\nGCP_LOCATION=your_gcp_location\nGCP_CLIENT_EMAIL=your_gcp_service_account\nGCP_PRIVATE_KEY=your_gcp_private_key\nCRON_SECRET=your_cron_secret\n```\n\n### **API Keys (Optional)**\n```bash\nUNKEY_API_ID=your_unkey_api_id\nUNKEY_TOKEN=your_unkey_token\nSUPER_ADMIN_TOKEN=your_super_admin_token\nFLY_REGION=self-hosted\n```\n\n### **Stripe (Optional)**\n```bash\nSTRIPE_SECRET_KEY=sk_test_your_stripe_secret\nSTRIPE_WEBHOOK_SECRET_KEY=whsec_your_webhook_secret\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key\n```\n\n### **Vercel (Optional)**\n```bash\nPROJECT_ID_VERCEL=your_vercel_project_id\nTEAM_ID_VERCEL=your_vercel_team_id\nVERCEL_AUTH_BEARER_TOKEN=your_vercel_token\nBLOB_READ_WRITE_TOKEN=your_vercel_blob_token\n```\n\n### **Observability (Optional)**\n```bash\nNEXT_PUBLIC_SENTRY_DSN=your_sentry_dsn\nSENTRY_AUTH_TOKEN=your_sentry_auth_token\nNEXT_PUBLIC_OPENPANEL_CLIENT_ID=your_openpanel_client_id\nOPENPANEL_CLIENT_SECRET=your_openpanel_client_secret\nPAGERDUTY_APP_ID=your_pagerduty_app_id\nSLACK_SUPPORT_WEBHOOK_URL=your_slack_webhook_url\nTELEGRAM_BOT_TOKEN=your_telegram_bot_token\n```\n\n### **External Services (Optional)**\n```bash\nOPENSTATUS_INGEST_URL=https://openstatus-private-location.fly.dev\nSCREENSHOT_SERVICE_URL=your_screenshot_service_url\n```\n\n### **Development & Testing (Optional)**\n```bash\nTURBO_ENV_MODE=loose\nPLAYGROUND_UNKEY_API_KEY=your_playground_key\nWORKSPACES_LOOKBACK_30=true\nWORKSPACES_HIDE_URL=false\n```\n\n## 🔍 Environment Variable Visibility in Coolify\n\n### **Where to Find Environment Variables**\n1. **Stack Settings**: Click on your OpenStatus stack\n2. **Environment Tab**: Look for \"Environment Variables\" section\n3. **Add Variables**: Click \"Add Variable\" for each required variable\n4. **Apply Changes**: Save and redeploy the stack\n\n### **Variable Categories in Coolify UI**\n- **Database**: `DATABASE_URL`, `DATABASE_AUTH_TOKEN`\n- **Authentication**: `AUTH_SECRET`, `NEXT_PUBLIC_URL`, `SELF_HOST`\n- **Email**: `RESEND_API_KEY`\n- **Analytics**: `TINY_BIRD_API_KEY`, `TINYBIRD_URL`\n- **OAuth**: `AUTH_GITHUB_*`, `AUTH_GOOGLE_*`\n- **Queue**: `UPSTASH_*`, `QSTASH_*`\n- **Cloud**: `GCP_*`, `CRON_SECRET`\n- **API Keys**: `UNKEY_*`, `SUPER_ADMIN_TOKEN`, `FLY_REGION`\n- **Payments**: `STRIPE_*`, `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`\n- **Vercel**: `PROJECT_ID_VERCEL`, `TEAM_ID_VERCEL`, `VERCEL_*`\n- **Observability**: `NEXT_PUBLIC_SENTRY_DSN`, `SENTRY_*`, `OPENPANEL_*`\n- **External**: `OPENSTATUS_INGEST_URL`, `SCREENSHOT_SERVICE_URL`\n- **Development**: `TURBO_ENV_MODE`, `PLAYGROUND_*`, `WORKSPACES_*`\n\n## 🚨 Troubleshooting\n\n### **Environment Variables Not Working?**\n1. **Check Stack Logs**: Look for environment variable errors\n2. **Verify Variable Names**: Ensure exact match with this guide\n3. **Redeploy**: Sometimes requires full stack restart\n4. **Check Coolify Docs**: Some variables may need special handling\n\n### **Common Issues**\n- **DATABASE_URL**: Must be `http://libsql:8080` (internal container communication)\n- **AUTH_SECRET**: Must be at least 32 characters\n- **NEXT_PUBLIC_URL**: Must include protocol (http:// or https://)\n- **RESEND_API_KEY**: Required for email login functionality\n\n### **Getting Help**\n- **Coolify Documentation**: Check Coolify's environment variables guide\n- **OpenStatus Docs**: Visit [OpenStatus Documentation](https://github.com/openstatusHQ/openstatus)\n- **Community Support**: Open an issue in the GitHub repository\n\n## 🎯 Production Deployment\n\n### **Minimum Required Variables**\nFor a basic working deployment, you only need:\n```bash\nDATABASE_URL=http://libsql:8080\nDATABASE_AUTH_TOKEN=\nAUTH_SECRET=your-32-char-secret\nNEXT_PUBLIC_URL=https://your-domain.com\nRESEND_API_KEY=your-resend-key\n```\n\n### **Testing Your Setup**\n1. Deploy with minimum variables first\n2. Check if services start correctly\n3. Add optional variables incrementally\n4. Test each feature as you add variables\n\nThis ensures a smooth, working deployment! 🚀\n"
  },
  {
    "path": "COOLIFY_SETUP.md",
    "content": "# Coolify Setup Guide\n\nThis guide provides step-by-step instructions for deploying OpenStatus on Coolify using pre-built Docker images.\n\n## Quick Start\n\n### Option 1: Import Complete Stack\n\n1. In Coolify dashboard, click **\"New Service\"** → **\"Docker Compose\"**\n2. Choose **\"Import from URL\"** and enter:\n   ```\n   https://raw.githubusercontent.com/openstatusHQ/openstatus/main/coolify-deployment.yaml\n   ```\n3. Configure your environment variables\n4. Click **\"Deploy\"**\n\n### Option 2: Manual Service Setup\n\nCreate each service individually using the configurations below.\n\n## Environment Setup\n\n### Setup\n\n1. **Copy the example file**:\n   ```bash\n   cp .env.docker.example .env.docker\n   ```\n2. **Edit the file** with your values:\n   ```bash\n   nano .env.docker\n   ```\n3. **Required variables**:\n   - `DATABASE_URL=http://libsql:8080`\n   - `AUTH_SECRET=your-32-char-secret`\n   - `RESEND_API_KEY=your-resend-key`\n   - `NEXT_PUBLIC_URL=http://your-domain:3002`\n\n## Service Configurations\n\n### 1. Database Services\n\n#### LibSQL Database\n- **Image**: `ghcr.io/tursodatabase/libsql-server:latest`\n- **Name**: `openstatus-libsql`\n- **Port**: `8080`\n- **Environment Variables**:\n  ```\n  SQLD_NODE=primary\n  ```\n- **Volumes**: `openstatus-libsql-data:/var/lib/sqld`\n- **Health Check**: `curl -f http://localhost:8080`\n\n#### TinyBird (Optional)\n- **Image**: `tinybirdco/tinybird-local:latest`\n- **Name**: `openstatus-tinybird`\n- **Port**: `7181`\n- **Environment Variables**:\n  ```\n  COMPATIBILITY_MODE=1\n  ```\n\n### 2. Core Services\n\n#### Workflows Engine\n- **Image**: `ghcr.io/openstatusHQ/openstatus-workflows:latest`\n- **Name**: `openstatus-workflows`\n- **Port**: `3000`\n- **Environment Variables**:\n  ```\n  DATABASE_URL=http://libsql:8080\n  PORT=3000\n  NODE_ENV=production\n  ```\n- **Volumes**: `./data:/app/data`\n- **Depends On**: `openstatus-libsql`\n\n#### API Server\n- **Image**: `ghcr.io/openstatusHQ/openstatus-server:latest`\n- **Name**: `openstatus-server`\n- **Port**: `3001`\n- **Environment Variables**:\n  ```\n  DATABASE_URL=http://libsql:8080\n  PORT=3000\n  NODE_ENV=production\n  ```\n- **Depends On**: `openstatus-workflows`, `openstatus-libsql`\n\n#### Private Location Agent\n- **Image**: `ghcr.io/openstatusHQ/openstatus-private-location:latest`\n- **Name**: `openstatus-private-location`\n- **Port**: `8081`\n- **Environment Variables**:\n  ```\n  DB_URL=http://libsql:8080\n  TINYBIRD_URL=http://tinybird:7181\n  GIN_MODE=release\n  PORT=8080\n  NODE_ENV=production\n  ```\n- **Depends On**: `openstatus-server`\n\n#### Checker Service\n- **Image**: `ghcr.io/openstatusHQ/openstatus-checker:latest`\n- **Name**: `openstatus-checker`\n- **Port**: `8082`\n- **Environment Variables**:\n  ```\n  DATABASE_URL=http://libsql:8080\n  PORT=8080\n  NODE_ENV=production\n  ```\n- **Depends On**: `openstatus-server`\n\n#### Web Dashboard\n- **Image**: `ghcr.io/openstatusHQ/openstatus-dashboard:latest`\n- **Name**: `openstatus-dashboard`\n- **Port**: `3002`\n- **Environment Variables**:\n  ```\n  DATABASE_URL=http://libsql:8080\n  PORT=3000\n  HOSTNAME=0.0.0.0\n  AUTH_TRUST_HOST=true\n  NODE_ENV=production\n  ```\n- **Depends On**: `openstatus-workflows`, `openstatus-libsql`, `openstatus-server`\n\n#### Status Page\n- **Image**: `ghcr.io/openstatusHQ/openstatus-status-page:latest`\n- **Name**: `openstatus-status-page`\n- **Port**: `3003`\n- **Environment Variables**:\n  ```\n  DATABASE_URL=http://libsql:8080\n  PORT=3000\n  HOSTNAME=0.0.0.0\n  AUTH_TRUST_HOST=true\n  NODE_ENV=production\n  ```\n- **Depends On**: `openstatus-workflows`, `openstatus-libsql`, `openstatus-server`\n\n## Environment Variables\n\nCreate a `.env.docker` file with the following variables:\n\n```bash\n# Database\nDATABASE_URL=http://libsql:8080\n\n# Authentication\nNEXTAUTH_SECRET=your-secret-key-here\nNEXTAUTH_URL=http://localhost:3002\n\n# External Services\nRESEND_API_KEY=your-resend-api-key\nUPSTASH_REDIS_REST_URL=your-upstash-url\nUPSTASH_REDIS_REST_TOKEN=your-upstash-token\n\n# TinyBird\nTINYBIRD_TOKEN=your-tinybird-token\nTINYBIRD_URL=http://tinybird:7181\n\n# Other\nENCRYPTION_KEY=your-encryption-key\n```\n\n## Network Configuration\n\n1. **Create Network**: In Coolify, create a network named `openstatus`\n2. **Attach Services**: Ensure all services are attached to this network\n3. **Service Discovery**: Services can communicate using container names as hostnames\n\n## Deployment Order\n\nDeploy services in this sequence:\n\n1. **libsql** (Database)\n2. **tinybird** (Optional - Analytics)\n3. **workflows** (Workflow Engine)\n4. **server** (API Server)\n5. **private-location** (Monitoring Agent)\n6. **checker** (Health Checker)\n7. **dashboard** (Web Interface)\n8. **status-page** (Public Status)\n\n## Resource Allocation\n\nRecommended resource limits for each service:\n\n| Service | Memory Limit | Memory Reservation |\n|----------|---------------|-------------------|\n| libsql | 512MB | 256MB |\n| tinybird | 1GB | 512MB |\n| workflows | 512MB | 256MB |\n| server | 512MB | 256MB |\n| private-location | 256MB | 128MB |\n| checker | 256MB | 128MB |\n| dashboard | 512MB | 256MB |\n| status-page | 512MB | 256MB |\n\n## Health Checks\n\nAll services include built-in health checks:\n\n- **API Services**: `curl -f http://localhost:3000/ping`\n- **Web Services**: `curl -f http://localhost:3000/`\n- **Private Location**: `wget --spider -q http://localhost:8080/health`\n\n## Access URLs\n\nAfter deployment, access services at:\n\n- **Dashboard**: `http://your-domain:3002`\n- **API Server**: `http://your-domain:3001`\n- **Status Page**: `http://your-domain:3003`\n- **Workflows**: `http://your-domain:3000`\n\n## Troubleshooting\n\n### Common Issues\n\n1. **Service Not Starting**\n   - Check environment variables\n   - Verify network connectivity\n   - Review service logs\n\n2. **Database Connection Failed**\n   - Ensure libsql is healthy first\n   - Check DATABASE_URL format\n   - Verify network configuration\n\n3. **Health Check Failing**\n   - Wait for full startup (30-60 seconds)\n   - Check if required ports are available\n   - Review service dependencies\n\n### Log Locations\n\nIn Coolify, access logs via:\n1. Service → **Logs** tab\n2. **Real-time** streaming logs\n3. **Historical** log files\n\n### Updates\n\nTo update services:\n1. Pull new images in Coolify\n2. Redeploy affected services\n3. Verify health checks pass\n\n## Production Considerations\n\n1. **SSL/TLS**: Configure HTTPS in Coolify\n2. **Backups**: Enable automated backups for database\n3. **Monitoring**: Set up external monitoring\n4. **Scaling**: Configure resource limits based on usage\n5. **Security**: Update images regularly, use secrets management\n\n## Support\n\n- **Documentation**: [OpenStatus Docs](https://docs.openstatus.dev)\n- **Issues**: [GitHub Issues](https://github.com/openstatusHQ/openstatus/issues)\n- **Community**: [Discord](https://www.openstatus.dev/discord)\n"
  },
  {
    "path": "DOCKER.md",
    "content": "# Docker Setup Guide\n\nComplete guide for running OpenStatus with Docker\n\n## Quick Start\n\n```bash\n# 1. Copy environment file\ncp .env.docker.example .env.docker\n\n# 2. Configure required variables (see Configuration section)\nvim .env.docker\n\n# 3. Build and start services (migrations will run automatically)\nexport DOCKER_BUILDKIT=1\ndocker compose up -d\n\n# 4. Check service health\ndocker compose ps\n\n# 5. (Optional) Seed database with test data\ndocker run --rm --network openstatus \\\n  -e DATABASE_URL=http://libsql:8080 \\\n  $(docker build -q -f apps/workflows/Dockerfile --target build .) \\\n  sh -c \"cd /app/packages/db && bun src/seed.mts\"\n\n# 6. (Optional) Deploy Tinybird local - requires tb CLI\ncd packages/tinybird\ntb --local deploy\n\n# 7. Access the application\nopen http://localhost:3002  # Dashboard\nopen http://localhost:3003  # Status Page Theme Explorer\n# Note: Status pages are accessed via subdomain/slug (e.g., http://localhost:3003/status)\n```\n\n## Cleanup\n\n```bash\n# Remove stopped containers\ndocker compose down\n\n# Remove volumes\ndocker compose down -v\n\n# Clean build cache\ndocker builder prune\n```\n\n## Services\n\n| Service | Port | Purpose |\n|---------|------|---------|\n| workflows | 3000 | Background jobs |\n| server | 3001 | API backend (tRPC) |\n| dashboard | 3002 | Admin interface |\n| status-page | 3003 | Public status pages |\n| private-location | 8081 | Monitoring agent |\n| libsql | 8080 | Database (HTTP) |\n| libsql | 5001 | Database (gRPC) |\n| tinybird-local | 7181 | Analytics |\n\n\n## Architecture\n\n```\n┌─────────────┐     ┌─────────────┐\n│  Dashboard  │────▶│   Server    │\n│  (Next.js)  │     │   (Bun)     │\n└─────────────┘     └─────────────┘\n      │                    │\n      ▼                    ▼\n┌─────────────┐     ┌─────────────┐\n│ Status Page │     │  Workflows  │\n│  (Next.js)  │     │   (Bun)     │\n└─────────────┘     └─────────────┘\n      │                    │\n      └────────┬───────────┘\n               ▼\n        ┌─────────────┐\n        │   LibSQL    │\n        │  (Database) │\n        └─────────────┘\n```\n\n## Database Setup\n\n### Automatic Migrations\n\nMigrations run **automatically** when you start the stack with `docker compose up -d`.\n\n**Verifying migrations:**\n```bash\n# Check workflows logs for migration output\ndocker compose logs workflows | grep -A 5 \"Running database migrations\"\n\n# Should show:\n# openstatus-workflows  | Running database migrations...\n# openstatus-workflows  | Migrated successfully\n# openstatus-workflows  | Starting workflows service...\n```\n\n**Manual migration:**\n\nIf you need to re-run migrations or troubleshoot:\n\n```bash\n# Run migrations using workflows container\ndocker compose exec workflows sh -c \"cd /app/packages/db && bun src/migrate.mts\"\n\n# Or restart workflows to trigger migrations again\ndocker compose restart workflows\n```\n\n### Seeding Test Data (Optional)\n\n**Note:** Migrations run automatically, but seeding does **not**. You must manually seed the database if you want test data.\n\nAfter migrations complete, seed the database with sample data:\n\n```bash\ndocker run --rm --network openstatus \\\n  -e DATABASE_URL=http://libsql:8080 \\\n  $(docker build -q -f apps/workflows/Dockerfile --target build .) \\\n  sh -c \"cd /app/packages/db && bun src/seed.mts\"\n```\n\nThis creates:\n- 3 workspaces (`love-openstatus`, `test2`, `test3`)\n- 5 sample monitors and 1 status page with slug `status`\n- Test user account: `ping@openstatus.dev`\n- Sample incidents, status reports, and maintenance windows\n\n**Verifying seeded data:**\n```bash\n# Check table counts via libsql HTTP API\ncurl -s http://localhost:8080/ -H \"Content-Type: application/json\" \\\n  -d '{\"statements\":[\"SELECT COUNT(*) FROM page\"]}' | jq -r '.[0].results.rows[0][0]'\n\n# Should output: 1\n```\n\n**Accessing Seeded Data:**\n\nAfter seeding, you can access the test data:\n\n**Dashboard:**\n1. Navigate to http://localhost:3002/login\n2. Use magic link authentication with email: `ping@openstatus.dev`\n3. Check your console/logs for the magic link (with `SELF_HOST=true` in `.env.docker`)\n4. After logging in, you'll see the `love-openstatus` workspace with all seeded monitors and status page\n\n**Status Page:**\n- The seeded status page has slug `status`\n- Access it via subdomain routing: http://status.localhost:3003\n- Or view theme explorer at: http://localhost:3003\n\n**If you use a different email address**, the system will create a new empty workspace for you instead of showing the seeded data. To access seeded data with a different account, you must add your user to the seeded workspace using SQL:\n\n  ```bash\n  # First, find your user_id\n  curl -X POST http://localhost:8080/ -H \"Content-Type: application/json\" \\\n    -d '{\"statements\":[\"SELECT id, email FROM user\"]}'\n\n  # Then add association (replace USER_ID with your id)\n  curl -X POST http://localhost:8080/ -H \"Content-Type: application/json\" \\\n    -d '{\"statements\":[\"INSERT INTO users_to_workspaces (user_id, workspace_id, role) VALUES (USER_ID, 1, '\\''owner'\\'')\"]}'\n  ```\n\n\n## Tinybird Setup (Optional)\n\nTinybird is used for analytics and monitoring metrics. The application will work without it, but analytics features will be unavailable.\n\nIf you want to enable analytics, you can:\n1. Use Tinybird Cloud and configure `TINY_BIRD_API_KEY` in `.env.docker`\n2. Manually configure Tinybird Local (requires additional setup beyond this guide)\n\n## Configuration\n\n### Required Environment Variables\n\nEdit `.env.docker` and set:\n\n```bash\n# Authentication\nAUTH_SECRET=your-secret-here\n\n# Database\nDATABASE_URL=http://libsql:8080\nDATABASE_AUTH_TOKEN=basic:token\n\n# Email\nRESEND_API_KEY=test\n```\n\n### Optional Services\n\nConfigure these for full functionality:\n\n```bash\n# Redis\nUPSTASH_REDIS_REST_URL=\nUPSTASH_REDIS_REST_TOKEN=\n\n# Analytics\nTINY_BIRD_API_KEY=\n\n# OAuth providers\nAUTH_GITHUB_ID=\nAUTH_GITHUB_SECRET=\nAUTH_GOOGLE_ID=\nAUTH_GOOGLE_SECRET=\n```\n\nSee [.env.docker.example](.env.docker.example) for complete list.\n\n## Development Workflow\n\n### Common Commands\n\n```bash\n# View logs\ndocker compose logs -f [service-name]\n\n# Restart service\ndocker compose restart [service-name]\n\n# Rebuild after code changes\ndocker compose up -d --build [service-name]\n\n# Stop all services\ndocker compose down\n\n# Reset database (removes all data)\ndocker compose down -v\ndocker compose up -d\n# Migrations run automatically on startup\n```\n\n### Authentication\n\n**Magic Link**:\n\nSet `SELF_HOST=true` in `.env.docker` to enable email-based magic link authentication. This allows users to sign in without configuring OAuth providers.\n\n**OAuth Providers**:\n\nConfigure GitHub/Google OAuth credentials in `.env.docker` and set up callback URLs:\n  - GitHub: `http://localhost:3002/api/auth/callback/github`\n  - Google: `http://localhost:3002/api/auth/callback/google`\n\n### Creating Status Pages\n\n**Via Dashboard (Recommended)**:\n1. Login to http://localhost:3002\n2. Create a workspace\n3. Create a status page with a slug\n4. Access at http://localhost:3003/[slug]\n\n**Via Database (Testing)**:\n```bash\n# Insert test data\ncurl -s http://localhost:8080/v2/pipeline \\\n  -H 'Content-Type: application/json' \\\n  --data-raw '{\n    \"requests\":[{\n      \"type\":\"execute\",\n      \"stmt\":{\n        \"sql\":\"INSERT INTO workspace (id, slug, name) VALUES (1, '\\''test'\\'', '\\''Test Workspace'\\'');\"\n      }\n    }]\n  }'\n```\n\n### Resource Limits\n\nAdd to `docker-compose.yaml`:\n\n```yaml\nservices:\n  dashboard:\n    deploy:\n      resources:\n        limits:\n          cpus: '1.0'\n          memory: 1G\n        reservations:\n          cpus: '0.5'\n          memory: 512M\n```\n\n## Monitoring\n\n### Health Checks\n\nAll services have automated health checks:\n\n```bash\n# View health status\ndocker compose ps\n\n# Inspect specific service\ndocker inspect openstatus-dashboard --format='{{.State.Health.Status}}'\n```\n\n## Getting Help\n\n- **Documentation**: [docs.openstatus.dev](https://docs.openstatus.dev)\n- **Discord**: [openstatus.dev/discord](https://www.openstatus.dev/discord)\n- **GitHub Issues**: [github.com/openstatusHQ/openstatus/issues](https://github.com/openstatusHQ/openstatus/issues)\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\" style=\"margin-top: 120px\">\n\n  <h3 align=\"center\">openstatus</h3>\n\n  <p align=\"center\">The open-source status page and uptime monitoring platform.\n    <br />\n    <a href=\"https://www.openstatus.dev\"><strong>Learn more »</strong></a>\n    <br />\n    <br />\n    <a href=\"https://docs.openstatus.dev\">Documentation</a>\n    ·\n    <a href=\"https://www.openstatus.dev\">Website</a>\n    ·\n    <a href=\"https://www.openstatus.dev/discord\">Discord</a>\n  </p>\n\n  <p align=\"center\">\n  <a href=\"https://status.openstatus.dev\"><img src=\"https://status.openstatus.dev/badge/v2?variant=outline\" alt=\"openstatus status\"></a>\n\n  </p>\n  <p align=\"center\">\n      <a href=\"https://github.com/openstatushq/openstatus/blob/main/LICENSE\"><img src=\"https://img.shields.io/badge/license-AGPL--3.0-blue.svg\" alt=\"License\"></a>\n      <a href=\"https://github.com/openstatushq/openstatus/stargazers\"><img src=\"https://img.shields.io/github/stars/openstatushq/openstatus?style=social\" alt=\"GitHub stars\"></a>\n      <a href=\"https://www.openstatus.dev/discord\"><img src=\"https://img.shields.io/discord/1129008226264940625?color=7289da&logo=discord&logoColor=white\" alt=\"Discord\"></a>\n</p>\n\n## About openstatus\n\nopenstatus is an open-source platform that combines **status pages** and **uptime monitoring** in a single tool. Keep your users informed and your services reliable. Available as a managed service or self-hosted.\n\n### Status pages\n\nBeautiful, customizable status pages with custom domains, password protection, maintenance windows, and subscriber notifications via email and RSS. Build trust and keep your users informed during incidents.\n\n### Synthetic monitoring\n\nMonitor your servers, websites and APIs from 28 regions across multiple cloud providers globally. Get notified via Slack, Discord, PagerDuty, email, and more when your services are down or slow.\n\n## Recognitions\n\n<a href=\"https://trendshift.io/repositories/1780\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/1780\" alt=\"openstatus | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n<a href=\"https://news.ycombinator.com/item?id=37740870\"><img alt=\"Featured on Hacker News\" src=\"https://hackerbadge.now.sh/api?id=37740870\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\" /></a>\n<a href=\"https://www.producthunt.com/posts/openstatus-2?utm_source=badge-top-post-badge&utm_medium=badge\" target=\"_blank\"><img alt=\"openstatus - #2 Product of the Day on Product Hunt\" src=\"https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=openstatus-2&theme=light&period=daily\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\" /></a>\n\n## Getting Started\n\n### With Docker (Recommended)\n\nThe fastest way to get started for both development and self-hosting:\n\n```sh\n# 1. Copy environment file\ncp .env.docker.example .env.docker\n\n# 2. Start all services\ndocker compose up -d\n\n# 3. Access the application\nopen http://localhost:3002  # Dashboard\nopen http://localhost:3003  # Status Pages\n```\n\nFull guide: [DOCKER.md](DOCKER.md)\n\n### Self-Hosting with Coolify\n\nWe provide pre-built Docker images for easy deployment:\n\n```bash\nghcr.io/openstatushq/openstatus-server:latest\nghcr.io/openstatushq/openstatus-dashboard:latest\nghcr.io/openstatushq/openstatus-workflows:latest\nghcr.io/openstatushq/openstatus-private-location:latest\nghcr.io/openstatushq/openstatus-status-page:latest\nghcr.io/openstatushq/openstatus-checker:latest\n```\n\n[Complete Coolify Deployment Guide](./COOLIFY_DEPLOYMENT.md)\n\n### Manual Setup\n\n#### Requirements\n\n- [Node.js](https://nodejs.org/en/) >= 20.0.0\n- [pnpm](https://pnpm.io/) >= 8.6.2\n- [Bun](https://bun.sh/)\n- [Turso CLI](https://docs.turso.tech/quickstart)\n\n#### Setup\n\n1. Clone the repository\n\n```sh\ngit clone https://github.com/openstatushq/openstatus.git\n```\n\n2. Install dependencies\n\n```sh\npnpm install\n```\n\n3. Initialize the development environment\n\nLaunch the database in one terminal:\n\n```sh\nturso dev --db-file openstatus-dev.db\n```\n\nIn another terminal, run the following command:\n\n```sh\npnpm dx\n```\n\n4. Launch whatever app you wish to:\n\n```sh\npnpm dev:web\npnpm dev:status-page\npnpm dev:dashboard\n```\n\nThe above commands will automatically run the libSQL client on `8080` so you might want to kill the turso command from step 3.\n\n5. See the results:\n\n- open [http://localhost:3000](http://localhost:3000) (default port)\n\n## Tech Stack\n\n- [Next.js](https://nextjs.org/) - Dashboard\n- [Hono](https://hono.dev/) - API server\n- [Go](https://go.dev/) - Checker\n- [Turso](https://turso.tech/) - Database\n- [Drizzle](https://orm.drizzle.team/) - ORM\n- [Tinybird](https://tinybird.co/?ref=openstatus.dev) - Analytics\n- [Tailwind CSS](https://tailwindcss.com/) - Styling\n- [shadcn/ui](https://ui.shadcn.com/) - UI components\n\n## Contributing\n\nIf you want to help us build the best status page and monitoring platform, check our [contributing guidelines](https://github.com/openstatusHQ/openstatus/blob/main/CONTRIBUTING.MD).\n\n<a href=\"https://github.com/openstatushq/openstatus/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openstatushq/openstatus\" />\n</a>\n\n![openstatus repository activity](https://repobeats.axiom.co/api/embed/180eee159c0128f683a30f15f51ac35bdbd9fa44.svg \"Repobeats analytics image\")\n\n## Contact\n\nInterested in our enterprise plan or need special features? Email us at [ping@openstatus.dev](mailto:ping@openstatus.dev) or book a call.\n\n<a href=\"https://cal.com/team/openstatus/30min\"><img alt=\"Book us with Cal.com\" src=\"https://cal.com/book-with-cal-dark.svg\" /></a>\n\n## License\n\nDistributed under the [AGPL-3.0 License](LICENSE).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Reporting Security Issues\n\nThe openstatus team takes security bugs in opnestatus seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.\n\nTo report a security issue, please  reach out to us via email at [ping@openstatus](mailto:ping@openstatus.dev) or on Discord\n\n\n\n\n## Please do the following:\n\n- Do not run automated scanners on our infrastructure or dashboard. If you wish to do this, contact us and we will set up a sandbox for you.\n- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data,\n- Do not reveal the problem to others until it has been resolved,\n- Do not use attacks on physical security, social engineering, distributed denial of service, spam or applications of third parties,\n- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation.\n\n\n\n## What we promise:\n\n- We will respond to your report within 3 business days with our evaluation of the report and an expected resolution date,\n- If you have followed the instructions above, we will not take any legal action against you in regard to the report,\n- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission,\n- We will keep you informed of the progress towards resolving the problem,\n- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise), and\n- We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.\n"
  },
  {
    "path": "apps/README.md",
    "content": "# Apps\n\nAll openstatus apps\n"
  },
  {
    "path": "apps/checker/.gitignore",
    "content": "tmp\n"
  },
  {
    "path": "apps/checker/.golangci.yml",
    "content": "version: \"2\"\n\nlinters:\n  default: fast\n"
  },
  {
    "path": "apps/checker/.private.air.toml",
    "content": "root = \".\"\ntestdata_dir = \"testdata\"\ntmp_dir = \"tmp\"\n\n[build]\n    args_bin = []\n    bin = \"./tmp/main\"\n    cmd = \"go build -o ./tmp/main ./cmd/private/main.go\"\n    delay = 1000\n    exclude_dir = [\"assets\", \"tmp\", \"vendor\", \"testdata\"]\n    exclude_file = []\n    exclude_regex = [\"_test.go\"]\n    exclude_unchanged = false\n    follow_symlink = false\n    full_bin = \"\"\n    include_dir = []\n    include_ext = [\"go\", \"tpl\", \"tmpl\", \"html\"]\n    include_file = []\n    kill_delay = \"0s\"\n    log = \"build-errors.log\"\n    poll = false\n    poll_interval = 0\n    post_cmd = []\n    pre_cmd = []\n    rerun = false\n    rerun_delay = 500\n    send_interrupt = false\n    stop_on_error = false\n\n\n[color]\n  app = \"\"\n  build = \"yellow\"\n  main = \"magenta\"\n  runner = \"green\"\n  watcher = \"cyan\"\n\n[log]\n  main_only = false\n  time = false\n\n[misc]\n  clean_on_exit = false\n\n[screen]\n  clear_on_rebuild = false\n  keep_scroll = true\n"
  },
  {
    "path": "apps/checker/.probe.air.toml",
    "content": "root = \".\"\ntestdata_dir = \"testdata\"\ntmp_dir = \"tmp\"\n\n[build]\n    args_bin = []\n    bin = \"./tmp/main\"\n    cmd = \"go build -o ./tmp/main ./cmd/server/main.go\"\n    delay = 1000\n    exclude_dir = [\"assets\", \"tmp\", \"vendor\", \"testdata\"]\n    exclude_file = []\n    exclude_regex = [\"_test.go\"]\n    exclude_unchanged = false\n    follow_symlink = false\n    full_bin = \"\"\n    include_dir = []\n    include_ext = [\"go\", \"tpl\", \"tmpl\", \"html\"]\n    include_file = []\n    kill_delay = \"0s\"\n    log = \"build-errors.log\"\n    poll = false\n    poll_interval = 0\n    post_cmd = []\n    pre_cmd = []\n    rerun = false\n    rerun_delay = 500\n    send_interrupt = false\n    stop_on_error = false\n\n\n[color]\n  app = \"\"\n  build = \"yellow\"\n  main = \"magenta\"\n  runner = \"green\"\n  watcher = \"cyan\"\n\n[log]\n  main_only = false\n  time = false\n\n[misc]\n  clean_on_exit = false\n\n[screen]\n  clear_on_rebuild = false\n  keep_scroll = true\n"
  },
  {
    "path": "apps/checker/Dockerfile",
    "content": "FROM golang:1.26-alpine as builder\n\nWORKDIR /go/src/app\nCOPY ca/ca-bundle.crt /usr/local/share/ca-certificates/ca-bundle.crt\n\nRUN apk update \\\n&& apk upgrade --available \\\n&& update-ca-certificates\n\nRUN apk add --no-cache tzdata\nENV TZ=UTC\n\nENV CGO_ENABLED=0\nENV GOOS=linux\nENV GOARCH=amd64\n\nCOPY go.* .\nRUN go mod download\n\nCOPY . .\nRUN go build -trimpath -ldflags \"-s -w\" -o checker ./cmd/server/main.go\n\nFROM scratch\n\nWORKDIR /opt/bin\n\nCOPY --from=builder /usr/local/share/ca-certificates/ca-bundle.crt /etc/ssl/certs/ca-bundle.crt\nCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt\n\n\n\nCOPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo\n\nCOPY --from=builder /go/src/app/checker/main /opt/bin/checker\n\nENV TZ=UTC\nENV USER=1000\nENV GIN_MODE=release\n\nCMD [ \"/opt/bin/checker\" ]\n"
  },
  {
    "path": "apps/checker/README.md",
    "content": "# OpenStatus Checker\n\nThe checker service to ping external service.\n\nIt pings the service and save thedata to the tinybird\n\n## How to run\n\n```bash\ngo run cmd/main.go\n```\n\nyou can also set the env variable\n\n```fish\nset CRON_SECRET YOLO\nset CLOUD_PROVIDER local\nset TINYBIRD_TOKEN random\n```\n\n## How to build\n\n```bash\ngo build -o checker *.go\n```\n\n## How to run in docker\n\n```bash\ndocker build -t checker .\ndocker run -p 8080:8080 checker\n```\n\n## How to deploy\n\n```bash\nfly deploy\n```\n\n## Deploy to all region\n\n```bash\nfly scale count 35 --region   ams,arn,atl,bog,bom,bos,cdg,den,dfw,ewr,eze,fra,gdl,gig,gru,hkg,iad,jnb,lax,lhr,mad,mia,nrt,ord,otp,phx,qro,scl,sjc,sea,sin,syd,waw,yul,yyz\n```\n\n## Deploy to your own infra\n\nUse our docker image\n\n<https://github.com/openstatusHQ/openstatus/pkgs/container/checker>\n"
  },
  {
    "path": "apps/checker/ca/ca-bundle.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz\ncyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2\nMDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV\nBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt\nYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN\nADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE\nBarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is\nI19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G\nCSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do\nlbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc\nAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPTCCAaYCEQDNun9W8N/kvFT+IqyzcqpVMA0GCSqGSIb3DQEBAgUAMF8xCzAJ\nBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xh\nc3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05\nNjAxMjkwMDAwMDBaFw0yODA4MDEyMzU5NTlaMF8xCzAJBgNVBAYTAlVTMRcwFQYD\nVQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xhc3MgMSBQdWJsaWMgUHJp\nbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOB\njQAwgYkCgYEA5Rm/baNWYS2ZSHH2Z965jeu3noaACpEO+jglr0aIguVzqKCbJF0N\nH8xlbgyw0FaEGIeaBpsQoXPftFg5a27B9hXVqKg/qhIGjTGsf7A01480Z4gJzRQR\n4k5FVmkfeAKA2txHkSm7NsljXMXg1y2He6G3MrB7MLoqLzGq7qNn2tsCAwEAATAN\nBgkqhkiG9w0BAQIFAAOBgQBMP7iLxmjf7kMzDl3ppssHhE16M/+SG/Q2rdiVIjZo\nEWx8QszznC7EBz8UsA9P/5CSdvnivErpj82ggAr3xSnxgiJduLHdgSOjeyUVRjB5\nFvjqBUuUfx3CHMjjt/QQQDwTw18fU+hI5Ia0e6E1sHslurjTjqs/OJ0ANACY89Fx\nlA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz\ncyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2\nMDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV\nBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt\nYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN\nADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f\nzGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi\nTkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G\nCSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW\nNWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV\nGx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPDCCAaUCEC0b/EoXjaOR6+f/9YtFvgswDQYJKoZIhvcNAQECBQAwXzELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz\ncyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2\nMDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV\nBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAyIFB1YmxpYyBQcmlt\nYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN\nADCBiQKBgQC2WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh\nYGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7\nFYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHwIDAQABMA0G\nCSqGSIb3DQEBAgUAA4GBAIobK/o5wXTXXtgZZKJYSi034DNHD6zt96rbHuSLBlxg\nJ8pFUs4W7z8GZOeUaHxgMxURaa+dYo2jA1Rrpr7l7gUYYAS/QoD90KioHgE796Nc\nr6Pc5iaAIzy4RHT3Cq5Ji2F4zCS/iIqnDupzGUH9TQPwiNHleI2lKk/2lw0Xd8rY\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPDCCAaUCEAq6HgBiMui0NiZdH3zNiWYwDQYJKoZIhvcNAQEFBQAwXzELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz\ncyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2\nMDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV\nBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAyIFB1YmxpYyBQcmlt\nYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN\nADCBiQKBgQC2WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh\nYGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7\nFYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHwIDAQABMA0G\nCSqGSIb3DQEBBQUAA4GBAIDToA+IyeVoW4R7gB+nt+MjWBEc9RTwWBKMi99x2ZAk\nEXyge8N6GRm9cr0gvwA63/rVeszC42JFi8tJg5jBcGnQnl6CjDVHjk8btB9jAa3k\nltax7nosZm4XNq8afjgGhixrTcsnkm54vwDVAcCxB8MJqmSFKPKdc57PYDoKHUpI\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz\ncyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2\nMDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV\nBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt\nYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN\nADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE\nBarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is\nI19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G\nCSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i\n2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ\n2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ\nBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh\nc3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy\nMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp\nemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X\nDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw\nFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg\nUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo\nYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5\nMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB\nAQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4\npO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0\n13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID\nAQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk\nU01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i\nF6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY\noJ2daZH9\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDAjCCAmsCEDKIjprS9esTR/h/xCA3JfgwDQYJKoZIhvcNAQEFBQAwgcExCzAJ\nBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh\nc3MgNCBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy\nMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp\nemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X\nDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw\nFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgNCBQdWJsaWMg\nUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo\nYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5\nMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB\nAQUAA4GNADCBiQKBgQC68OTP+cSuhVS5B1f5j8V/aBH4xBewRNzjMHPVKmIquNDM\nHO0oW369atyzkSTKQWI8/AIBvxwWMZQFl3Zuoq29YRdsTjCG8FE3KlDHqGKB3FtK\nqsGgtG7rL+VXxbErQHDbWk2hjh+9Ax/YA9SPTJlxvOKCzFjomDqG04Y48wApHwID\nAQABMA0GCSqGSIb3DQEBBQUAA4GBAIWMEsGnuVAVess+rLhDityq3RS6iYF+ATwj\ncSGIL4LcY/oCRaxFWdcqWERbt5+BO5JoPeI3JPV7bI92NZYJqFmduc4jq3TWg/0y\ncyfYaT5DdPauxYma51N86Xv2S/PBZYPejYqcPIiNOVn8qj8ijaHBZlCBckztImRP\nT8qAkbYp\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ\nBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh\nc3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy\nMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp\nemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X\nDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw\nFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg\nUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo\nYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5\nMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB\nAQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK\nVdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm\nFc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID\nAQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J\nh9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul\nuIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68\nDzFc6PLZ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw\nCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns\nYXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH\nMjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y\naXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe\nFw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX\nMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj\nIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx\nKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s\neTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B\nAQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM\nHiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw\nDqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC\nAwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji\nnb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX\nrXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn\njBJ7xUS0rg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG\nA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv\nb3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw\nMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i\nYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT\naWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ\njc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp\nxy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp\n1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG\nsnUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ\nU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8\n9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\nBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B\nAQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz\nyj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE\n38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP\nAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad\nDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME\nHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw\nCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl\ncmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu\nLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT\naWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD\nVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT\naWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ\nbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu\nIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1\nGQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ\n+mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd\nU6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm\nNxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY\nufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/\nky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1\nCtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq\ng6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm\nfjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c\n2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/\nbLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw\nCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl\ncmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu\nLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT\naWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD\nVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT\naWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ\nbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu\nIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b\nN3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t\nKmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu\nkxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm\nCC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ\nXwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu\nimi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te\n2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe\nDGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC\n/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p\nF4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt\nTxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw\nCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl\ncmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu\nLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT\naWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD\nVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT\naWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ\nbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu\nIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4\nnN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO\n8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV\nojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb\nPG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2\n6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr\nn5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a\nqtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4\nwTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3\nns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs\npSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4\nE1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ\nBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy\naVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s\nIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp\nZ24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0\neSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV\nBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp\nZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu\nYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g\nQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt\nIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU\nJ92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO\nJxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY\nwZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o\nkoqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN\nqWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E\nSrg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe\nxbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u\n7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU\nsQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI\nsH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP\ncjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML\nRW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp\nbmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5\nIEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp\nZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3\nMjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3\nLmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp\nYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG\nA1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq\nK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe\nsYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX\nMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT\nXTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/\nHoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH\n4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub\nj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo\nU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf\nzX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b\nu/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+\nbYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er\nfF6adulZkMV8gzURZVE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIIBhDCeat3PfIwDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UE\nBhMCQ0gxEjAQBgNVBAoTCVN3aXNzU2lnbjEyMDAGA1UEAxMpU3dpc3NTaWduIENB\nIChSU0EgSUsgTWF5IDYgMTk5OSAxODowMDo1OCkxHzAdBgkqhkiG9w0BCQEWEGNh\nQFN3aXNzU2lnbi5jb20wHhcNMDAxMTI2MjMyNzQxWhcNMzExMTI2MjMyNzQxWjB2\nMQswCQYDVQQGEwJDSDESMBAGA1UEChMJU3dpc3NTaWduMTIwMAYDVQQDEylTd2lz\nc1NpZ24gQ0EgKFJTQSBJSyBNYXkgNiAxOTk5IDE4OjAwOjU4KTEfMB0GCSqGSIb3\nDQEJARYQY2FAU3dpc3NTaWduLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAKw5fjnmNneLQlUCQG8jQLwwfbrOZoUwNX8cbNqhxK03/xUloFVgAt+S\nTe2RxNXaCAXLBPn5ZST35TLV57aLmbHCtifv3YZqaaQGvjedltIBMJihJhZ+h3LY\nSKsUb+xEJ3x5ZUf8jP+Q1g57y1s8SnBFWN/ni5NkF1Y1y31VwOi9wiOf/VISL+uu\nSC4i1CP1Kbz3BDs6Hht1GpRYCbJ/K0bc9oJSpWpT5PGONsGIawqMbJuyoDghsXQ1\npbn2e8K64BSscGZVZTNooSGgNiHmACNJBYXiWVWrwXPF4l6SddmC3Rj0aKXjgECc\nFkHLDQcsM5JsK2ZLryTDUsQFbxVP2ikCAwEAAaNHMEUwCwYDVR0PBAQDAgEGMAwG\nA1UdEwQFMAMBAf8wHQYDVR0OBBYEFJbXcc05KtT8iLGKq1N4ae+PR34WMAkGA1Ud\nIwQCMAAwDQYJKoZIhvcNAQEFBQADggEBAKMy6W8HvZdS1fBpEUzl6Lvw50bgE1Xc\nHU1JypSBG9mhdcXZo5AlPB4sCvx9Dmfwhyrdsshc0TP2V3Vh6eQqnEF5qB4lVziT\nBko9mW6Ot+pPnwsy4SHpx3rw6jCYnOqfUcZjWqqqRrq/3P1waz+Mn4cLMVEg3Xaz\nqYov/khvSqS0JniwjRlo2H6f/1oVUKZvP+dUhpQepfZrOqMAWZW4otp6FolyQyeU\nNN6UCRNiUKl5vTijbKwUUwfER/1Vci3M1/O1QCfttQ4vRN4Buc0xqYtGL3cd5WiO\nvWzyhlTzAI6VUdNkQhhHJSAyTpj6dmXDRzrryoFGa2PjgESxz7XBaSI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6\nMRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp\ndHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX\nBgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy\nMDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp\neafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg\n/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl\nwSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh\nAMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2\nPcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu\nAWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR\nMKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc\nHnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/\nZb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+\nf00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO\nrSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch\n6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3\n7CAFYd4=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc\nMBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP\nbmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2\nMDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft\nZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg\nQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk\nhsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym\n1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW\nOqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb\n2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko\nO3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw\nAwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU\nAK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB\nBQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF\nZu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb\nLjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir\noQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C\nMMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds\nsPmuujz9dLQR6FgNgLzTqIA6me11zEZ7\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc\nMBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP\nbmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2\nMDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft\nZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg\nQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP\nADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC\n206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci\nKtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2\nJxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9\nBoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e\nXz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B\nPeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67\nXnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq\nZ8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ\no2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3\n+L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj\nYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj\nFNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE\nAwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn\nxPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2\nLHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc\nobGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8\nCNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe\nIjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA\nDjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F\nAjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX\nOm/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb\nAZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl\nZvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw\nRY8mkaKO/qk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMCVVMx\nHTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNBbWVyaWNh\nIE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIgUm9vdCBDZXJ0\naWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyOTA2MDAwMFoXDTM3MTEyMDE1\nMDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRBT0wgVGltZSBXYXJuZXIg\nSW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUgSW5jLjE3MDUGA1UEAxMuQU9M\nIFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnej8Mlo2k06AX3dLm/WpcZuS+U\n0pPlLYnKhHw/EEMbjIt8hFj4JHxIzyr9wBXZGH6EGhfT257XyuTZ16pYUYfw8ItI\nTuLCxFlpMGK2MKKMCxGZYTVtfu/FsRkGIBKOQuHfD5YQUqjPnF+VFNivO3ULMSAf\nRC+iYkGzuxgh28pxPIzstrkNn+9R7017EvILDOGsQI93f7DKeHEMXRZxcKLXwjqF\nzQ6axOAAsNUl6twr5JQtOJyJQVdkKGUZHLZEtMgxa44Be3ZZJX8VHIQIfHNlIAqh\nBC4aMqiaILGcLCFZ5/vP7nAtCMpjPiybkxlqpMKX/7eGV4iFbJ4VFitNLLMCAwEA\nAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUoTYwFsuGkABFgFOxj8jY\nPXy+XxIwHwYDVR0jBBgwFoAUoTYwFsuGkABFgFOxj8jYPXy+XxIwDgYDVR0PAQH/\nBAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQCKIBilvrMvtKaEAEAwKfq0FHNMeUWn\n9nDg6H5kHgqVfGphwu9OH77/yZkfB2FK4V1Mza3u0FIy2VkyvNp5ctZ7CegCgTXT\nCt8RHcl5oIBN/lrXVtbtDyqvpxh1MwzqwWEFT2qaifKNuZ8u77BfWgDrvq2g+EQF\nZ7zLBO+eZMXpyD8Fv8YvBxzDNnGGyjhmSs3WuEvGbKeXO/oTLW4jYYehY0KswsuX\nn2Fozy1MBJ3XJU8KDk2QixhWqJNIV9xvrr2eZ1d3iVCzvhGbRWeDhhmH05i9CBoW\nH1iCC+GWaQVLjuyDUTEH1dSf/1l7qG6Fz9NLqUmwX7A5KGgOc90lmt4S\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF5jCCA86gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMCVVMx\nHTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNBbWVyaWNh\nIE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIgUm9vdCBDZXJ0\naWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyOTA2MDAwMFoXDTM3MDkyODIz\nNDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRBT0wgVGltZSBXYXJuZXIg\nSW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUgSW5jLjE3MDUGA1UEAxMuQU9M\nIFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIw\nDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQ3WggWmRToVbEbJGv8x4vmh6mJ\n7ouZzU9AhqS2TcnZsdw8TQ2FTBVsRotSeJ/4I/1n9SQ6aF3Q92RhQVSji6UI0ilb\nm2BPJoPRYxJWSXakFsKlnUWsi4SVqBax7J/qJBrvuVdcmiQhLE0OcR+mrF1FdAOY\nxFSMFkpBd4aVdQxHAWZg/BXxD+r1FHjHDtdugRxev17nOirYlxcwfACtCJ0zr7iZ\nYYCLqJV+FNwSbKTQ2O9ASQI2+W6p1h2WVgSysy0WVoaP2SBXgM1nEG2wTPDaRrbq\nJS5Gr42whTg0ixQmgiusrpkLjhTXUr2eacOGAgvqdnUxCc4zGSGFQ+aJLZ8lN2fx\nI2rSAG2X+Z/nKcrdH9cG6rjJuQkhn8g/BsXS6RJGAE57COtCPStIbp1n3UsC5ETz\nkxmlJ85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86QNZ8MUhy/XCFh\nEVsVS6kkUfykXPcXnbDS+gfpj1bkGoxoigTTfFrjnqKhynFbotSg5ymFXQNoKk/S\nBtc9+cMDLz9l+WceR0DTYw/j1Y75hauXTLPXJuuWCpTehTacyH+BCQJJKg71ZDIM\ngtG6aoIbs0t0EfOMd9afv9w3pKdVBC/UMejTRrkDfNoSTllkt1ExMVCgyhwn2RAu\nrda9EGYrw7AiShJbAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE\nFE9pbQN+nZ8HGEO8txBO1b+pxCAoMB8GA1UdIwQYMBaAFE9pbQN+nZ8HGEO8txBO\n1b+pxCAoMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAO/Ouyugu\nh4X7ZVnnrREUpVe8WJ8kEle7+z802u6teio0cnAxa8cZmIDJgt43d15Ui47y6mdP\nyXSEkVYJ1eV6moG2gcKtNuTxVBFT8zRFASbI5Rq8NEQh3q0l/HYWdyGQgJhXnU7q\n7C+qPBR7V8F+GBRn7iTGvboVsNIYvbdVgaxTwOjdaRITQrcCtQVBynlQboIOcXKT\nRuidDV29rs4prWPVVRaAMCf/drr3uNZK49m1+VLQTkCpx+XCMseqdiThawVQ68W/\nClTluUI8JPu3B5wwn3la5uBAUhX0/Kr0VvlEl4ftDmVyXr4m+02kLQgH3thcoNyB\nM5kYJRF3p+v9WAksmWsbivNSPxpNSGDxoPYzAlOL7SUJuA0t7Zdz7NeWH45gDtoQ\nmy8YJPamTQr5O8t1wswvziRpyQoijlmn94IM19drNZxDAGrElWe6nEXLuA4399xO\nAU++CrYD062KRffaJ00psUjf5BHklka9bAI+1lHIlRcBFanyqqryvy9lG2/QuRqT\n9Y41xICHPpQvZuTpqP9BnHAqTyo5GJUefvthATxRCC4oGKQWDzH9OmwjkyB24f0H\nhdFbP9IcczLd+rn4jM8Ch3qaluTtT4mNU0OrDhPAARW0eTjb/G49nlG2uBOLZ8/5\nfNkiHfZdxRwBL5joeiQYvITX+txyW/fBOmg=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM\nMRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD\nQTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM\nMRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD\nQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E\njG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo\nePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI\nULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu\nOb7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg\nAKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7\nHVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA\nuI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa\nTOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg\nxSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q\nCjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x\nO/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs\n6GAqm4VKQPNriiTsBhYscw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/\nMQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj\nYXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow\nPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp\nY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\nAJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR\nIePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q\ngQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy\nyhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts\nF/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2\njWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx\nls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC\nVGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK\nYS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH\nEgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN\nXo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud\nDgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE\nMTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK\nUWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ\nTulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf\nqzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK\nZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE\nJPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7\nhUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1\nEqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm\nnD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX\nudpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz\nssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe\nLMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl\npYYsfPQS\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB\n8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy\ndGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1\nYmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3\ndy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh\nIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD\nLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG\nEwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g\nKE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD\nZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu\nbmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg\nZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R\n85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm\n4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV\nHMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd\nQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t\nlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB\no4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E\nBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4\nopvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo\ndHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW\nZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN\nAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y\n/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k\nSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy\nRp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS\nAgu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl\nnJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFGTCCBAGgAwIBAgIEPki9xDANBgkqhkiG9w0BAQUFADAxMQswCQYDVQQGEwJE\nSzEMMAoGA1UEChMDVERDMRQwEgYDVQQDEwtUREMgT0NFUyBDQTAeFw0wMzAyMTEw\nODM5MzBaFw0zNzAyMTEwOTA5MzBaMDExCzAJBgNVBAYTAkRLMQwwCgYDVQQKEwNU\nREMxFDASBgNVBAMTC1REQyBPQ0VTIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEArGL2YSCyz8DGhdfjeebM7fI5kqSXLmSjhFuHnEz9pPPEXyG9VhDr\n2y5h7JNp46PMvZnDBfwGuMo2HP6QjklMxFaaL1a8z3sM8W9Hpg1DTeLpHTk0zY0s\n2RKY+ePhwUp8hjjEqcRhiNJerxomTdXkoCJHhNlktxmW/OwZ5LKXJk5KTMuPJItU\nGBxIYXvViGjaXbXqzRowwYCDdlCqT9HU3Tjw7xb04QxQBr/q+3pJoSgrHPb8FTKj\ndGqPqcNiKXEx5TukYBdedObaE+3pHx8b0bJoc8YQNHVGEBDjkAB2QMuLt0MJIf+r\nTpPGWOmlgtt3xDqZsXKVSQTwtyv6e1mO3QIDAQABo4ICNzCCAjMwDwYDVR0TAQH/\nBAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgewGA1UdIASB5DCB4TCB3gYIKoFQgSkB\nAQEwgdEwLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuY2VydGlmaWthdC5kay9yZXBv\nc2l0b3J5MIGdBggrBgEFBQcCAjCBkDAKFgNUREMwAwIBARqBgUNlcnRpZmlrYXRl\nciBmcmEgZGVubmUgQ0EgdWRzdGVkZXMgdW5kZXIgT0lEIDEuMi4yMDguMTY5LjEu\nMS4xLiBDZXJ0aWZpY2F0ZXMgZnJvbSB0aGlzIENBIGFyZSBpc3N1ZWQgdW5kZXIg\nT0lEIDEuMi4yMDguMTY5LjEuMS4xLjARBglghkgBhvhCAQEEBAMCAAcwgYEGA1Ud\nHwR6MHgwSKBGoESkQjBAMQswCQYDVQQGEwJESzEMMAoGA1UEChMDVERDMRQwEgYD\nVQQDEwtUREMgT0NFUyBDQTENMAsGA1UEAxMEQ1JMMTAsoCqgKIYmaHR0cDovL2Ny\nbC5vY2VzLmNlcnRpZmlrYXQuZGsvb2Nlcy5jcmwwKwYDVR0QBCQwIoAPMjAwMzAy\nMTEwODM5MzBagQ8yMDM3MDIxMTA5MDkzMFowHwYDVR0jBBgwFoAUYLWF7FZkfhIZ\nJ2cdUBVLc647+RIwHQYDVR0OBBYEFGC1hexWZH4SGSdnHVAVS3OuO/kSMB0GCSqG\nSIb2fQdBAAQQMA4bCFY2LjA6NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEACrom\nJkbTc6gJ82sLMJn9iuFXehHTuJTXCRBuo7E4A9G28kNBKWKnctj7fAXmMXAnVBhO\ninxO5dHKjHiIzxvTkIvmI/gLDjNDfZziChmPyQE+dF10yYscA+UYyAFMP8uXBV2Y\ncaaYb7Z8vTd/vuGTJW1v8AqtFxjhA7wHKcitJuj4YfD9IQl+mo6paH1IYnK9AOoB\nmbgGglGBTvH1tJFUuSN6AJqfXY3gPGS5GhKSKseCRHI53OI8xthV9RVOyAUO28bQ\nYqbsFbS1AoLbrIyigfCbmTH1ICCoiGEKB5+U/NDXG8wuF/MEJ3Zn61SD/aSQfgY9\nBKNDLdr8C2LqL19iUw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn\nMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL\nExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg\nb2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa\nMH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB\nODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw\nIAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B\nAQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb\nunXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d\nBmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq\n7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3\n0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX\nroDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG\nA1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j\naGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p\n26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA\nBzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud\nEgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN\nBgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz\naWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB\nAAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd\np0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi\n1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc\nXCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0\neDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu\ntGWaIZDgqtCYvDi1czyL+Nw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn\nMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL\nExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo\nYW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9\nMQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy\nNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G\nA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA\nA4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0\nMi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s\nQJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV\neAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795\nB9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh\nz0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T\nAQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i\nZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w\nTcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH\nMCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD\nVR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE\nVDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh\nbWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B\nAQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM\nbKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi\nryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG\nVwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c\necQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/\nAYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb\nMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\nGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj\nYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL\nMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\nBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM\nGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua\nBtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe\n3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4\nYgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR\nrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm\nez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU\noBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF\nMAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v\nQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t\nb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF\nAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q\nGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz\nRt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2\nG9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi\nl2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3\nsmPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb\nMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\nGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp\nZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow\nfjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV\nBAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM\ncm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S\nHpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996\nCF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk\n3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz\n6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV\nHQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud\nEwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv\nY2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw\nOi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww\nDQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0\n5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj\nZ55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI\ngKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ\naD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl\nizeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb\nMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\nGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0\naWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla\nMH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO\nBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD\nVQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW\nfnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt\nTGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL\nfhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW\n1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7\nkUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G\nA1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD\nVR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v\nZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo\ndHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu\nY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/\nHrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32\npSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS\njBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+\nxqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn\ndBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDhDCCAmygAwIBAgIBCTANBgkqhkiG9w0BAQUFADAzMQswCQYDVQQGEwJDTjER\nMA8GA1UEChMIVW5pVHJ1c3QxETAPBgNVBAMTCFVDQSBSb290MB4XDTA0MDEwMTAw\nMDAwMFoXDTI5MTIzMTAwMDAwMFowMzELMAkGA1UEBhMCQ04xETAPBgNVBAoTCFVu\naVRydXN0MREwDwYDVQQDEwhVQ0EgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBALNdB8qGJn1r4vs4CQ7MgsJqGgCiFV/W6dQBt1YDAVmP9ThpJHbC\nXivF9iu/r/tB/Q9a/KvXg3BNMJjRnrJ2u5LWu+kQKGkoNkTo8SzXWHwk1n8COvCB\na2FgP/Qz3m3l6ihST/ypHWN8C7rqrsRoRuTej8GnsrZYWm0dLNmMOreIy4XU9+gD\nXv2yTVDo1h//rgI/i0+WITyb1yXJHT/7mLFZ5PCpO6+zzYUs4mBGzG+OoOvwNMXx\nQhhgrhLtRnUc5dipllq+3lrWeGeWW5N3UPJuG96WUUqm1ktDdSFmjXfsAoR2XEQQ\nth1hbOSjIH23jboPkXXHjd+8AmCoKai9PUMCAwEAAaOBojCBnzALBgNVHQ8EBAMC\nAQYwDAYDVR0TBAUwAwEB/zBjBgNVHSUEXDBaBggrBgEFBQcDAQYIKwYBBQUHAwIG\nCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEFBQcD\nBwYIKwYBBQUHAwgGCCsGAQUFBwMJMB0GA1UdDgQWBBTbHzXza0z/QjFkm827Wh4d\nSBC37jANBgkqhkiG9w0BAQUFAAOCAQEAOGy3iPGt+lg3dNHocN6cJ1nL5BXXoMNg\n14iABMUwTD3UGusGXllH5rxmy+AI/Og17GJ9ysDawXiv5UZv+4mCI4/211NmVaDe\nJRI7cTYWVRJ2+z34VFsxugAG+H1V5ad2g6pcSpemKijfvcZsCyOVjjN/Hl5AHxNU\nLJzltQ7dFyiuawHTUin1Ih+QOfTcYmjwPIZH7LgFRbu3DJaUxmfLI3HQjnQi1kHr\nA6i26r7EARK1s11AdgYg1GS4KUYGis4fk5oQ7vuqWrTcL9Ury/bXBYSYBZELhPc9\n+tb5evosFeo2gkO3t7jj83EB7UNDogVFwygFBzXjAaU4HoDU18PZ3g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW\nMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy\nc2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE\nBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0\nIFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV\nVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8\ncQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT\nQjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh\nF7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v\nc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w\nmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd\nVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX\nteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ\nf9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe\nBi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+\nnhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB\n/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY\nMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG\n9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc\naanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX\nIwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn\nANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z\nuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN\nPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja\nQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW\nkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9\nER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt\nDF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm\nbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW\nMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy\nc2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD\nVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1\nc3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC\nAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81\nWzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG\nFF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq\nXbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL\nse4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb\nKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd\nIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73\ny/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt\nhAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc\nQIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4\nLt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV\nHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ\nKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z\ndXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ\nL1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr\nFg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo\nag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY\nT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz\nGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m\n1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV\nOCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH\n6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX\nQMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDkzCCAnugAwIBAgIQFBOWgxRVjOp7Y+X8NId3RDANBgkqhkiG9w0BAQUFADA0\nMRMwEQYDVQQDEwpDb21TaWduIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQG\nEwJJTDAeFw0wNDAzMjQxMTMyMThaFw0yOTAzMTkxNTAyMThaMDQxEzARBgNVBAMT\nCkNvbVNpZ24gQ0ExEDAOBgNVBAoTB0NvbVNpZ24xCzAJBgNVBAYTAklMMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8ORUaSvTx49qROR+WCf4C9DklBKK\n8Rs4OC8fMZwG1Cyn3gsqrhqg455qv588x26i+YtkbDqthVVRVKU4VbirgwTyP2Q2\n98CNQ0NqZtH3FyrV7zb6MBBC11PN+fozc0yz6YQgitZBJzXkOPqUm7h65HkfM/sb\n2CEJKHxNGGleZIp6GZPKfuzzcuc3B1hZKKxC+cX/zT/npfo4sdAMx9lSGlPWgcxC\nejVb7Us6eva1jsz/D3zkYDaHL63woSV9/9JLEYhwVKZBqGdTUkJe5DSe5L6j7Kpi\nXd3DTKaCQeQzC6zJMw9kglcq/QytNuEMrkvF7zuZ2SOzW120V+x0cAwqTwIDAQAB\no4GgMIGdMAwGA1UdEwQFMAMBAf8wPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2Zl\nZGlyLmNvbXNpZ24uY28uaWwvY3JsL0NvbVNpZ25DQS5jcmwwDgYDVR0PAQH/BAQD\nAgGGMB8GA1UdIwQYMBaAFEsBmz5WGmU2dst7l6qSBe4y5ygxMB0GA1UdDgQWBBRL\nAZs+VhplNnbLe5eqkgXuMucoMTANBgkqhkiG9w0BAQUFAAOCAQEA0Nmlfv4pYEWd\nfoPPbrxHbvUanlR2QnG0PFg/LUAlQvaBnPGJEMgOqnhPOAlXsDzACPw1jvFIUY0M\ncXS6hMTXcpuEfDhOZAYnKuGntewImbQKDdSFc8gS4TXt8QUxHXOZDOuWyt3T5oWq\n8Ir7dcHyCTxlZWTzTNity4hp8+SDtwy9F1qWF8pb/627HOkthIDYIb6FUtnUdLlp\nhbpN7Sgy6/lhSuTENh4Z3G+EER+V9YMoGKgzkkMn3V0TBEVPh9VGzT2ouvDzuFYk\nRes3x+F2T3I5GN9+dHLHcy056mDmrRGiVod7w2ia/viMcKjfZTL0pECMocJEAw6U\nAGegcQCCSA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAw\nPDEbMBkGA1UEAxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWdu\nMQswCQYDVQQGEwJJTDAeFw0wNDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwx\nGzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBDQTEQMA4GA1UEChMHQ29tU2lnbjEL\nMAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGtWhf\nHZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs49oh\ngHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sW\nv+bznkqH7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ue\nMv5WJDmyVIRD9YTC2LxBkMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr\n9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d19guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt\n6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUwAwEB/zBEBgNVHR8EPTA7\nMDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29tU2lnblNl\nY3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58\nADsAj8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkq\nhkiG9w0BAQUFAAOCAQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7p\niL1DRYHjZiM/EoZNGeQFsOY3wo3aBijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtC\ndsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtpFhpFfTMDZflScZAmlaxMDPWL\nkz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP51qJThRv4zdL\nhfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz\nOjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDQzCCAiugAwIBAgIQX/h7KCtU3I1CoxW1aMmt/zANBgkqhkiG9w0BAQUFADA1\nMRYwFAYDVQQKEw1DaXNjbyBTeXN0ZW1zMRswGQYDVQQDExJDaXNjbyBSb290IENB\nIDIwNDgwHhcNMDQwNTE0MjAxNzEyWhcNMjkwNTE0MjAyNTQyWjA1MRYwFAYDVQQK\nEw1DaXNjbyBTeXN0ZW1zMRswGQYDVQQDExJDaXNjbyBSb290IENBIDIwNDgwggEg\nMA0GCSqGSIb3DQEBAQUAA4IBDQAwggEIAoIBAQCwmrmrp68Kd6ficba0ZmKUeIhH\nxmJVhEAyv8CrLqUccda8bnuoqrpu0hWISEWdovyD0My5jOAmaHBKeN8hF570YQXJ\nFcjPFto1YYmUQ6iEqDGYeJu5Tm8sUxJszR2tKyS7McQr/4NEb7Y9JHcJ6r8qqB9q\nVvYgDxFUl4F1pyXOWWqCZe+36ufijXWLbvLdT6ZeYpzPEApk0E5tzivMW/VgpSdH\njWn0f84bcN5wGyDWbs2mAag8EtKpP6BrXruOIIt6keO1aO6g58QBdKhTCytKmg9l\nEg6CTY5j/e/rmxrbU6YTYK/CfdfHbBcl1HP7R2RQgYCUTOG/rksc35LtLgXfAgED\no1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJ/PI\nFR5umgIJFq0roIlgX9p7L6owEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEF\nBQADggEBAJ2dhISjQal8dwy3U8pORFBi71R803UXHOjgxkhLtv5MOhmBVrBW7hmW\nYqpao2TB9k5UM8Z3/sUcuuVdJcr18JOagxEu5sv4dEX+5wW4q+ffy0vhN4TauYuX\ncB7w4ovXsNgOnbFp1iqRe6lJT37mjpXYgyc81WhJDtSd9i7rp77rMKSsH0T8lasz\nBvt9YAretIpjsJyp8qS5UwGH0GikJ3+r/+n6yUA4iGe0OcaEb1fJU9u6ju7AQ7L4\nCYNu/2bPPu8Xs1gYJQk0XuPL1hS27PKSb3TkL4Eq1ZKR4OCXPDJoBYVL0fdX4lId\nkxpUnwVwwEpxYB5DC2Ae/qPOgRnhCzU=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICmDCCAgGgAwIBAgIBDjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJVUzEY\nMBYGA1UEChMPVS5TLiBHb3Zlcm5tZW50MQwwCgYDVQQLEwNFQ0ExFDASBgNVBAMT\nC0VDQSBSb290IENBMB4XDTA0MDYxNDEwMjAwOVoXDTQwMDYxNDEwMjAwOVowSzEL\nMAkGA1UEBhMCVVMxGDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDEMMAoGA1UECxMD\nRUNBMRQwEgYDVQQDEwtFQ0EgUm9vdCBDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw\ngYkCgYEArkr2eXIS6oAKIpDkOlcQZdMGdncoygCEIU+ktqY3of5SVVXU7/it7kJ1\nEUzR4ii2vthQtbww9aAnpQxcEmXZk8eEyiGEPy+cCQMllBY+efOtKgjbQNDZ3lB9\n19qzUJwBl2BMxslU1XsJQw9SK10lPbQm4asa8E8e5zTUknZBWnECAwEAAaOBizCB\niDAfBgNVHSMEGDAWgBT2uAQnDlYW2blj2f2hVGVBoAhILzAdBgNVHQ4EFgQU9rgE\nJw5WFtm5Y9n9oVRlQaAISC8wDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB\nAf8wJQYDVR0gBB4wHDAMBgpghkgBZQMCAQwBMAwGCmCGSAFlAwIBDAIwDQYJKoZI\nhvcNAQEFBQADgYEAHh0EQY2cZ209aBb5q0wW1ER0dc4OGzsLyqjHfaQ4TEaMmUwL\nAJRta/c4KVWLiwbODsvgJk+CaWmSL03gRW/ciVb/qDV7qh9Pyd1cOlanZTAnPog2\ni82yL3i2fK9DCC84uoxEQbgqK2jx9bIjFTwlAqITk9fGAm5mdT84IEwq1Gw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh\nMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE\nYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3\nMDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo\nZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg\nMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN\nADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA\nPVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w\nwdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi\nEqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY\navx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+\nYihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE\nsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h\n/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5\nIEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj\nYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD\nggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy\nOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P\nTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ\nHmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER\ndEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf\nReYNnyicsbkqWletNw+vHX/bvZ8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl\nMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp\nU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw\nNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE\nChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp\nZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3\nDQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf\n8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN\n+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0\nX9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa\nK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA\n1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G\nA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR\nzt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0\nYXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD\nbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w\nDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3\nL7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D\neruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl\nxy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp\nVSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY\nWQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB\ngjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk\nMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY\nUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx\nNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3\ndy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy\ndmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB\ndXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6\n38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP\nKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q\nDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4\nqEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa\nJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi\nPvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P\nBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs\njVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0\neS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD\nggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR\nvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt\nqZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa\nIR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy\ni6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ\nO+7ETPTsJ3xCwnR8gooJybQDJbw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDcDCCAligAwIBAgIBBTANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQGEwJVUzEY\nMBYGA1UEChMPVS5TLiBHb3Zlcm5tZW50MQwwCgYDVQQLEwNEb0QxDDAKBgNVBAsT\nA1BLSTEWMBQGA1UEAxMNRG9EIFJvb3QgQ0EgMjAeFw0wNDEyMTMxNTAwMTBaFw0y\nOTEyMDUxNTAwMTBaMFsxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9VLlMuIEdvdmVy\nbm1lbnQxDDAKBgNVBAsTA0RvRDEMMAoGA1UECxMDUEtJMRYwFAYDVQQDEw1Eb0Qg\nUm9vdCBDQSAyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwCzB9o07\nrP8/PNZxvrh0IgfscEEV/KtA4weqwcPYn/7aTDq/P8jYKHtLNgHArEUlw9IOCo+F\nGGQQPRoTcCpvjtfcjZOzQQ84Ic2tq8I9KgXTVxE3Dc2MUfmT48xGSSGOFLTNyxQ+\nOM1yMe6rEvJl6jQuVl3/7mN1y226kTT8nvP0LRy+UMRC31mI/2qz+qhsPctWcXEF\nlrufgOWARVlnQbDrw61gpIB1BhecDvRD4JkOG/t/9bPMsoGCsf0ywbi+QaRktWA6\nWlEwjM7eQSwZR1xJEGS5dKmHQa99brrBuKG/ZTE6BGf5tbuOkooAY7ix5ow4X4P/\nUNU7ol1rshDMYwIDAQABoz8wPTAdBgNVHQ4EFgQUSXS7DF66ev4CVO97oMaVxgmA\ncJYwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD\nggEBAJiRjT+JyLv1wGlzKTs1rLqzCHY9cAmS6YREIQF9FHYb7lFsHY0VNy17MWn0\nmkS4r0bMNPojywMnGdKDIXUr5+AbmSbchECV6KjSzPZYXGbvP0qXEIIdugqi3VsG\nK52nZE7rLgE1pLQ/E61V5NVzqGmbEfGY8jEeb0DU+HifjpGgb3AEkGaqBivO4XqS\ntX3h4NGW56E6LcyxnR8FRO2HmdNNGnA5wQQM5X7Z8a/XIA7xInolpHOZzD+kByeW\nqKKV7YK5FtOeC4fCwfKI9WLfaN/HvGlR7bFc3FRUKQ8JOZqsA8HbDE2ubwp6Fknx\nv5HSOJTT9pUst2zJQraNypCNhdk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe\nMQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0\nZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe\nFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw\nIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL\nSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH\nSyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh\nijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X\nDZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1\nTBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ\nfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA\nsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU\nWH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS\nnT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH\ndmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip\nNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC\nAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF\nMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH\nClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB\nuvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl\nPwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP\nJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/\ngpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2\nj6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6\n5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB\no2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS\n/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z\nGp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE\nW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D\nhNQ+IIX3Sj0rnP0qCglN6oH4EZw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEvDCCA6SgAwIBAgIQAJCLMk/BkBrOtMM4Cc3P5DANBgkqhkiG9w0BAQUFADB5\nMQswCQYDVQQGEwJFUzE2MDQGA1UEChMtQ29uc2VqbyBHZW5lcmFsIGRlIGxhIEFi\nb2dhY2lhIE5JRjpRLTI4NjMwMDZJMTIwMAYDVQQDEylBdXRvcmlkYWQgZGUgQ2Vy\ndGlmaWNhY2lvbiBkZSBsYSBBYm9nYWNpYTAeFw0wNTA2MTMyMjAwMDBaFw0zMDA2\nMTMyMjAwMDBaMHkxCzAJBgNVBAYTAkVTMTYwNAYDVQQKEy1Db25zZWpvIEdlbmVy\nYWwgZGUgbGEgQWJvZ2FjaWEgTklGOlEtMjg2MzAwNkkxMjAwBgNVBAMTKUF1dG9y\naWRhZCBkZSBDZXJ0aWZpY2FjaW9uIGRlIGxhIEFib2dhY2lhMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtLJX7oXwI+gN+7KAhPEQZ6uy+UnfXN5b5I8p\nGVPJ1egcUGthAoyH8I88wUWSC6yZocYahdY9rX4mph24PbKzPorFCjLTS5HvSXV+\nVvf+oAhiRivO6vJRn2DeMsjtGqfPdVzrPcC9mkilhpTOWFAU6mrhmvSMZZXhYBUl\nlRL2uniLssDt5myXJFod5HRDyjjENZRYjvWKsGg8KCxElgm/CVtyCudnPJC5VDh0\nVLttLWpDyLzvCawfI+hSVl41F18ru17NZVKlFHw7sqrp3Se1NyM7Bg0se4262m9m\nF4anttceB10ebBmXyOUjc3jRrvkeuqGuSSLtZXEff/dadESNQwIDAQABo4IBPjCC\nATowNwYDVR0RBDAwLoERYWNAYWNhYm9nYWNpYS5vcmeGGWh0dHA6Ly93d3cuYWNh\nYm9nYWNpYS5vcmcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwEQYJ\nYIZIAYb4QgEBBAQDAgAHMB0GA1UdDgQWBBT8iEyObQShIJDT+Byas2cEX3mAxjCB\nqwYDVR0gBIGjMIGgMIGdBgsrBgEEAYGBFQoBATCBjTApBggrBgEFBQcCARYdaHR0\ncDovL3d3dy5hY2Fib2dhY2lhLm9yZy9kb2MwYAYIKwYBBQUHAgIwVBpSQ29uc3Vs\ndGUgbGEgZGVjbGFyYWNpb24gZGUgcHJhY3RpY2FzIGRlIGNlcnRpZmljYWNpb24g\nZW4gaHR0cDovL3d3dy5hY2Fib2dhY2lhLm9yZzANBgkqhkiG9w0BAQUFAAOCAQEA\nmKf6ObVzESZ/vIk/tGslMzEKhjhryR4VlxTg0kwthfQ8dJuNKBH7zA4muYCDFtH5\nRpi2RgeOZoVtcMC6TIDzpPDVN1Qrr2aEcnP5SC8JzuGFAcqP4IfeoJfQlLQNtU0O\nZyzIYMQylMBBgQeNur+p6AxAmkJ4BV2B62Ic5E8UCj0LPh/p9M197kW7vN5d85iX\nJnvGEyn4K38a1Or6sm4gntoX6qGSvTfpDru7kdUl9mBdhSFQW/9UXfVLO7TDKRFY\nAvYl5OGCgruijeeRJF5AkZ5HB4wzV9RiMVF2dYVDbwmrEaUlKbnY/1+l9z/rZTsd\n74blFiLVHsoyaX1+BdcwJw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEezCCA2OgAwIBAgIQNxkY5lNUfBq1uMtZWts1tzANBgkqhkiG9w0BAQUFADCB\nrjELMAkGA1UEBhMCREUxIDAeBgNVBAgTF0JhZGVuLVd1ZXJ0dGVtYmVyZyAoQlcp\nMRIwEAYDVQQHEwlTdHV0dGdhcnQxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fz\nc2VuIFZlcmxhZyBHbWJIMT4wPAYDVQQDEzVTLVRSVVNUIEF1dGhlbnRpY2F0aW9u\nIGFuZCBFbmNyeXB0aW9uIFJvb3QgQ0EgMjAwNTpQTjAeFw0wNTA2MjIwMDAwMDBa\nFw0zMDA2MjEyMzU5NTlaMIGuMQswCQYDVQQGEwJERTEgMB4GA1UECBMXQmFkZW4t\nV3VlcnR0ZW1iZXJnIChCVykxEjAQBgNVBAcTCVN0dXR0Z2FydDEpMCcGA1UEChMg\nRGV1dHNjaGVyIFNwYXJrYXNzZW4gVmVybGFnIEdtYkgxPjA8BgNVBAMTNVMtVFJV\nU1QgQXV0aGVudGljYXRpb24gYW5kIEVuY3J5cHRpb24gUm9vdCBDQSAyMDA1OlBO\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2bVKwdMz6tNGs9HiTNL1\ntoPQb9UY6ZOvJ44TzbUlNlA0EmQpoVXhOmCTnijJ4/Ob4QSwI7+Vio5bG0F/WsPo\nTUzVJBY+h0jUJ67m91MduwwA7z5hca2/OnpYH5Q9XIHV1W/fuJvS9eXLg3KSwlOy\nggLrra1fFi2SU3bxibYs9cEv4KdKb6AwajLrmnQDaHgTncovmwsdvs91DSaXm8f1\nXgqfeN+zvOyauu9VjxuapgdjKRdZYgkqeQd3peDRF2npW932kKvimAoA0SVtnteF\nhy+S8dF2g08LOlk3KC8zpxdQ1iALCvQm+Z845y2kuJuJja2tyWp9iRe79n+Ag3rm\n7QIDAQABo4GSMIGPMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG\nMCkGA1UdEQQiMCCkHjAcMRowGAYDVQQDExFTVFJvbmxpbmUxLTIwNDgtNTAdBgNV\nHQ4EFgQUD8oeXHngovMpttKFswtKtWXsa1IwHwYDVR0jBBgwFoAUD8oeXHngovMp\nttKFswtKtWXsa1IwDQYJKoZIhvcNAQEFBQADggEBAK8B8O0ZPCjoTVy7pWMciDMD\npwCHpB8gq9Yc4wYfl35UvbfRssnV2oDsF9eK9XvCAPbpEW+EoFolMeKJ+aQAPzFo\nLtU96G7m1R08P7K9n3frndOMusDXtk3sU5wPBG7qNWdX4wple5A64U8+wwCSersF\niXOMy6ZNwPv2AtawB6MDwidAnwzkhYItr5pCHdDHjfhA7p0GVxzZotiAFP7hYy0y\nh9WUUpY6RsZxlj33mA6ykaqP2vROJAA5VeitF7nTNCtKqUDMFypVZUF0Qn71wK/I\nk63yGFs9iQzbRzkk+OBM8h+wPQrKBU6JIRrjKpms/H+h8Q8bHz2eBIPdltkdOpQ=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIE5zCCA8+gAwIBAgIBADANBgkqhkiG9w0BAQUFADCBjTELMAkGA1UEBhMCQ0Ex\nEDAOBgNVBAgTB09udGFyaW8xEDAOBgNVBAcTB1Rvcm9udG8xHTAbBgNVBAoTFEVj\naG93b3J4IENvcnBvcmF0aW9uMR8wHQYDVQQLExZDZXJ0aWZpY2F0aW9uIFNlcnZp\nY2VzMRowGAYDVQQDExFFY2hvd29yeCBSb290IENBMjAeFw0wNTEwMDYxMDQ5MTNa\nFw0zMDEwMDcxMDQ5MTNaMIGNMQswCQYDVQQGEwJDQTEQMA4GA1UECBMHT250YXJp\nbzEQMA4GA1UEBxMHVG9yb250bzEdMBsGA1UEChMURWNob3dvcnggQ29ycG9yYXRp\nb24xHzAdBgNVBAsTFkNlcnRpZmljYXRpb24gU2VydmljZXMxGjAYBgNVBAMTEUVj\naG93b3J4IFJvb3QgQ0EyMIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEA\nutU/5BkV15UBf+s+JQruKQxr77s3rjp/RpOtmhHILIiO5gsEWP8MMrfrVEiidjI6\nQh6ans0KAWc2Dw0/j4qKAQzOSyAZgjcdypNTBZ7muv212DA2Pu41rXqwMrlBrVi/\nKTghfdLlNRu6JrC5y8HarrnRFSKF1Thbzz921kLDRoCi+FVs5eVuK5LvIfkhNAqA\nbyrTgO3T9zfZgk8upmEkANPDL1+8y7dGPB/d6lk0I5mv8PESKX02TlvwgRSIiTHR\nk8++iOPLBWlGp7ZfqTEXkPUZhgrQQvxcrwCUo6mk8TqgxCDP5FgPoHFiPLef5szP\nZLBJDWp7GLyE1PmkQI6WiwIBA6OCAVAwggFMMA8GA1UdEwEB/wQFMAMBAf8wCwYD\nVR0PBAQDAgEGMB0GA1UdDgQWBBQ74YEboKs/OyGC1eISrq5QqxSlEzCBugYDVR0j\nBIGyMIGvgBQ74YEboKs/OyGC1eISrq5QqxSlE6GBk6SBkDCBjTELMAkGA1UEBhMC\nQ0ExEDAOBgNVBAgTB09udGFyaW8xEDAOBgNVBAcTB1Rvcm9udG8xHTAbBgNVBAoT\nFEVjaG93b3J4IENvcnBvcmF0aW9uMR8wHQYDVQQLExZDZXJ0aWZpY2F0aW9uIFNl\ncnZpY2VzMRowGAYDVQQDExFFY2hvd29yeCBSb290IENBMoIBADBQBgNVHSAESTBH\nMEUGCysGAQQB+REKAQMBMDYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuZWNob3dv\ncnguY29tL2NhL3Jvb3QyL2Nwcy5wZGYwDQYJKoZIhvcNAQEFBQADggEBAG+nrPi/\n0RpfEzrj02C6JGPUar4nbjIhcY6N7DWNeqBoUulBSIH/PYGNHYx7/lnJefiixPGE\n7TQ5xPgElxb9bK8zoAApO7U33OubqZ7M7DlHnFeCoOoIAZnG1kuwKwD5CXKB2a74\nHzcqNnFW0IsBFCYqrVh/rQgJOzDA8POGbH0DeD0xjwBBooAolkKT+7ZItJF1Pb56\nQpDL9G+16F7GkmnKlAIYT3QTS3yFGYChnJcd+6txUPhKi9sSOOmAIaKHnkH9Scz+\nA2cSi4A3wUYXVatuVNHpRb2lygfH3SuCX9MU8Ure3zBlSU1LALtMqI4JmcQmQpIq\nzIzvO2jHyu9PQqo=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB\nijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly\naWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl\nZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w\nNTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G\nA1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD\nVQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX\nSVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR\nVVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2\nw93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF\nmQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg\n4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9\n4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw\nDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw\nEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx\nSPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2\nftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8\nvPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa\nhNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi\nFj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ\n/L7fCg0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjEL\nMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV\nBAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0\nQ2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYwMTEyMTQzODQzWhcNMjUxMjMxMjI1\nOTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i\nSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UEAxMc\nVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jf\ntMjWQ+nEdVl//OEd+DFwIxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKg\nuNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2J\nXjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQXa7pIXSSTYtZgo+U4+lK\n8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7uSNQZu+99\n5OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1Ud\nEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3\nkUrL84J6E1wIqzCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy\ndXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6\nLy93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz\nJTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290\nY2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u\nTGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iS\nGNn3Bzn1LL4GdXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprt\nZjluS5TmVfwLG4t3wVMTZonZKNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8\nau0WOB9/WIFaGusyiC2y8zl3gK9etmF1KdsjTYjKUCjLhdLTEKJZbtOTVAB6okaV\nhgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kPJOzHdiEoZa5X6AeI\ndUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfkvQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjEL\nMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV\nBAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0\nQ2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYwMTEyMTQ0MTU3WhcNMjUxMjMxMjI1\nOTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i\nSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UEAxMc\nVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJW\nHt4bNwcwIi9v8Qbxq63WyKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+Q\nVl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo6SI7dYnWRBpl8huXJh0obazovVkdKyT2\n1oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZuV3bOx4a+9P/FRQI2Alq\nukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk2ZyqBwi1\nRb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1Ud\nEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NX\nXAek0CSnwPIA1DCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy\ndXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6\nLy93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz\nJTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290\nY2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u\nTGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlN\nirTzwppVMXzEO2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8\nTtXqluJucsG7Kv5sbviRmEb8yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6\ng0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9IJqDnxrcOfHFcqMRA/07QlIp2+gB\n95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal092Y+tTmBvTwtiBj\nS+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc5A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFvzCCA6egAwIBAgIQANKFcP2up9ZfEYQVxjG1yzANBgkqhkiG9w0BAQUFADBd\nMQswCQYDVQQGEwJFUzEoMCYGA1UECgwfRElSRUNDSU9OIEdFTkVSQUwgREUgTEEg\nUE9MSUNJQTENMAsGA1UECwwERE5JRTEVMBMGA1UEAwwMQUMgUkFJWiBETklFMB4X\nDTA2MDIxNjEwMzcyNVoXDTM2MDIwODIyNTk1OVowXTELMAkGA1UEBhMCRVMxKDAm\nBgNVBAoMH0RJUkVDQ0lPTiBHRU5FUkFMIERFIExBIFBPTElDSUExDTALBgNVBAsM\nBEROSUUxFTATBgNVBAMMDEFDIFJBSVogRE5JRTCCAiIwDQYJKoZIhvcNAQEBBQAD\nggIPADCCAgoCggIBAIAArQzDoyAHo2P/9zSgze5qVAgXXbEBFafmuV+Kcf8Mwh3q\nN/Pek3/WBU2EstXXHAz0xJFwQA5ayJikgOgNM8AH87f1rKE4esBmVCT8UswwKvLD\nxKEsdr/BwL+C8ZvwaHoTQMiXvBwlBwgKt5bvzClU4OZlLeqyLrEJaRJOMNXY+LwA\ngC9Nkw/NLlcbM7ufME7Epct5p/viNBi2IJ4bn12nyTqtRWSzGM4REpxtHlVFKISc\nV2dN+cvii49YCdQ5/8g20jjiDGV/FQ59wQfdqSLfkQDEbHE0dNw56upPRGl/WNtY\nClJxK+ypHVB0M/kpavr+mfTnzEVFbcpaJaIS487XOAU58BoJ9XZZzmJvejQNLNG8\nBBLsPVPI+tACy849IbXF4DkzZc85U8mbRvmdM/NZgAhBvm9LoPpKzqR2HIXir68U\nnWWs93+X5DNJpq++zis38S7BcwWcnGBMnTANl1SegWK75+Av9xQHFKl3kenckZWO\n04iQM0dvccMUafqmLQEeG+rTLuJ/C9zP5yLw8UGjAZLlgNO+qWKoVYgLNDTs3CEV\nqu/WIl6J9VGSEypvgBbZsQ3ZLvgQuML+UkUznB04fNwVaTRzv6AsuxF7lM34Ny1v\nPe+DWsYem3RJj9nCjb4WdlDIWtElFvb2zIycWjCeZb7QmkiT1/poDXUxh/n3AgMB\nAAGjezB5MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQW\nBBSORfSfc8X/LxsF2wFHYBsDioG3ujA3BgNVHSAEMDAuMCwGBFUdIAAwJDAiBggr\nBgEFBQcCARYWaHR0cDovL3d3dy5kbmllLmVzL2RwYzANBgkqhkiG9w0BAQUFAAOC\nAgEAdeVzyVFRL4sZoIfp/642Nqb8QR/jHtdxYBnGb5oCML1ica1z/pEtTuQmQESp\nrngmIzFp3Jpzlh5JUQvg78G4Q+9xnO5Bt8VQHzKEniKG8fcfj9mtK07alyiXu5aa\nGvix2XoE81SZEhmWFYBnOf8CX3r8VUJQWua5ov+4qGIeFM3ZP76jZUjFO9c3zg36\nKJDav/njUUclfUrTZ02HqmK8Xux6gER8958KvWVXlMryEWbWUn/kOnB1BM07l9Q2\ncvdRVr809dJB4bTaqEP+axJJErRdzyJClowIIyaMshBOXapT7gEvdeW5ohEzxNdq\n/fgOym6C2ee7WSNOtfkRHS9rI/V7ESDqQRKQMkbbMTupwVtzaDpGG4z+l7dWuWGZ\nzE7wg/o38d4cnRxxiwOTw8Rzgi6omB1kopqM91QITc/qgcv1WwmZY691jJb4eTXV\n3OtBgXk4hF5v8W9idtuRzlqFYDkdW+IqL0Ml28J6JNMVsKLxjKB9a0gJE/+iTGaK\n7HBSCVOMMMy41bok3DCZPqFet9+BrOw3vk6bJ1jefqGbVH8Gti/kMlD95xC7qM3a\nGBvUY2Y96lFxOfScPt9a9NrHTCbti7UhujR5AnNhENqYMahgy34Hp9C3BUOJW82F\nJtmwUa/3jFKqEqdY35KbZ/Kd8ub0aTH0Fufed1se3ZoFAa0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHxDCCBaygAwIBAgIIGq+SbI+Tr2AwDQYJKoZIhvcNAQEFBQAwgZUxgZIwCQYD\nVQQGDAJCRzAVBgNVBAoMDkluZm9Ob3RhcnkgUExDMBUGCgmSJomT8ixkARkWB3Jv\nb3QtY2EwGgYDVQQDDBNJbmZvTm90YXJ5IENTUCBSb290MBoGA1UECwwTSW5mb05v\ndGFyeSBDU1AgUm9vdDAfBgkqhkiG9w0BCQEWEmNzcEBpbmZvbm90YXJ5LmNvbTAi\nGA8yMDA2MDMwNjE3MzMwNVoYDzIwMjYwMzA2MTczMzA1WjCBlTGBkjAJBgNVBAYM\nAkJHMBUGA1UECgwOSW5mb05vdGFyeSBQTEMwFQYKCZImiZPyLGQBGRYHcm9vdC1j\nYTAaBgNVBAMME0luZm9Ob3RhcnkgQ1NQIFJvb3QwGgYDVQQLDBNJbmZvTm90YXJ5\nIENTUCBSb290MB8GCSqGSIb3DQEJARYSY3NwQGluZm9ub3RhcnkuY29tMIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnM2kXh+kfgiCT4B2wSeMRxZgn3Os\nyZF/LRBsO5RJnEIzX5TxgyEJkfeStw84RYuUB0qr/j82NVvR1VI4QkboR8dKUDPI\n5OBztpCauqOIONMrQsBu36ITF/JPuefyId1l+qb9UsJgsstPN72vJ45pazJAU1n1\n8jF2iAZtaJ8wvLeBHI5nY8MFZfcz9lmpq7OKRwsUE8c23SL7+EQ0NUEowIG6TrVx\nE68DqEkKLqSbYXdrHSTSpfEt0Udegv9Ig7AVmEfedvtPTsC/VElmvE8B0Xw6Zf7F\nrHBbxRUbcqV9pls7F8O+wz9dEn9nLXZHDIu/FiO1g3LNgM3ouq9eaDM41JDtbtLp\nVgQkofm+bF7KniMgkiuaDUtN46JIj77tqg6Mvqk4cLkQfzJdgCdaA+DURzMlWnap\ng9mYQO1/dQLv7mGRrDE+gNM0S/DCTjUwSV63Keh9IbD/KnmcNDEZZjMgYRovsILV\nsWdTkA2dKpcobrdmPxp/psSaaJbmZlt6N99adxXg2eN8s7LQ1WjbPh1YH47KmHRj\nv9R9Nmju0C+kKvKIL3fDEj+NqCUsBGlYpsu0OfGI0Kbels5zQkbTo/nx2I6KnW6K\nwFJPRbdw/hx7PQ2W3PmQlm3GGKFCwrbc2SMfl5ObDxn5sLc/JXS9glPsDS9t1j9S\nL9h20CW90Ln1vQ8CAwEAAaOCAhAwggIMMA4GA1UdDwEB/wQEAwIBBjBEBggrBgEF\nBQcBAQQ4MDYwNAYIKwYBBQUHMAGGKGh0dHA6Ly9vY3NwLmluZm9ub3RhcnkuY29t\nL3Jlc3BvbmRlci5jZ2kwVgYIKwYBBQUHAQsESjBIMEYGCCsGAQUFBzAFhjpsZGFw\nOi8vbGRhcC5pbmZvbm90YXJ5LmNvbS9kYz1yb290LWNhLGRjPWluZm9ub3Rhcnks\nZGM9Y29tMIGqBgNVHSAEgaIwgZ8wbwYJKwYBBAGBrQABMGIwOgYIKwYBBQUHAgEW\nLmh0dHA6Ly9yZXBvc2l0b3J5LmluZm9ub3RhcnkuY29tL2Nwcy9xY3BzLmh0bWww\nJAYIKwYBBQUHAgIwGBoWSW5mb05vdGFyeSBDU1AgUm9vdCBDQTAsBgkrBgEEAYGt\nAAAwHzAdBggrBgEFBQcCARYRaHR0cDovL3d3dy5jcmMuYmcwDwYDVR0TAQH/BAUw\nAwEB/zB/BgNVHREEeDB2pHQwcjFwMAsGA1UEEQwEMTAwMDAMBgNVBAcMBVNvZmlh\nMBMGA1UEFAwMKzM1OTI5ODc1NzE3MBsGBlUECmQBAQwRMTMxMjc2ODI3OkJVTFNU\nQVQwIQYJKoZIhvcNAQkIDBQxNiBJdmFuIFZhc292IFN0cmVldDAdBgNVHQ4EFgQU\n3dROZ0M/0+pi6NqJbo47bgu7lZ8wDQYJKoZIhvcNAQEFBQADggIBABib/A3B+HGs\n1MwUtScJwVhKNEDmm2XK4PGLUj2Wfoke3qgV+t2ULoPGNl0bIak2Dlw9SYgMUyFd\nH21JNm+cUOvbZM+Juq9erRREh2LvMHzAlt9wOcs7Ue4r/AgFh1bNMyXggBrgpucN\nQ0wAI0NWog4ZVOKN0Q0WuVpvm3flHKmDiyjx4TJ2X0ewmjbqsm/dhjFY/gZpsMNg\npvvYKNQI3fFuThq9zbesviKHFkmxOADVjEMp5ylrKxJiWapD8LRyiDjWAl8f2iSS\njY18/B99OJBYCx8ctxy7NictWhzHMd20K529R6ExtwkR4s1vp270uKMpj/Ngv6cd\n4E+XJSUKMEnH/no5RNTTZe+IXf/Z7lM5vDpEqDE7JZd7mr/R3Wvi1xJq+zK70diO\n85azj5DqeFjBCtulJb6KAx/lktK8f6Ry5OtWeqn6fLjxoL8m/ko0zyWqZMS7e+0Y\n5Uwsb0Fl7eC7PSx1VW/kFQbFS2yT9g9VzJN1hWEmA9LMlct6ECAnVnq/dVjc9Q2W\n2UoHhNgimxDR1gZuFgcDs69546AXurhDCEKdPDsnzHwR1H4x82ZRAhFyOqPpcr2V\nXaEVf0cTOZWyta728WF0MHjHZgHTsnCXCMkNJPREKnCmnS8SZAZuio0g437a8VS/\nDRSsKb59f3+5a0UCUWcVWaIOISfJgmcx\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTEL\nMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV\nBAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1\nc3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcNMDYwMzIyMTU1NDI4WhcNMjUxMjMx\nMjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIg\nR21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYwJAYD\nVQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSR\nJJZ4Hgmgm5qVSkr1YnwCqMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3T\nfCZdzHd55yx4Oagmcw6iXSVphU9VDprvxrlE4Vc93x9UIuVvZaozhDrzznq+VZeu\njRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtwag+1m7Z3W0hZneTvWq3z\nwZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9OgdwZu5GQ\nfezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYD\nVR0jBBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAO\nBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0G\nCSqGSIb3DQEBBQUAA4IBAQAo0uCG1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X1\n7caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/CyvwbZ71q+s2IhtNerNXxTPqYn\n8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3ghUJGooWMNjs\nydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT\nujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/\n2TYcuiUaUj0a7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF3zCCA8egAwIBAgIOGTMAAQACKBqaBLzyVUUwDQYJKoZIhvcNAQEFBQAwejEL\nMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV\nBAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEnMCUGA1UEAxMeVEMgVHJ1\nc3RDZW50ZXIgVW5pdmVyc2FsIENBIElJMB4XDTA2MDMyMjE1NTgzNFoXDTMwMTIz\nMTIyNTk1OVowejELMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVy\nIEdtYkgxJDAiBgNVBAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEnMCUG\nA1UEAxMeVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBIElJMIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEAi9R3azRs5TbYalxeOO781R15Azt7g2JEgk6I\n7d6D/+7MUGIFBZWZdpj2ufJf2AaRksL2LWYXH/1TA+iojWOpbuHWG4y8mLOLO9Tk\nLsp9hUkmW3m4GotAnn+7yT9jLM/RWny6KCJBElpN+Rd3/IX9wkngKhh/6aAsnPlE\n/AxoOUL1JwW+jhV6YJ3wO8c85j4WvK923mq3ouGrRkXrjGV90ZfzlxElq1nroCLZ\ngt2Y7X7i+qBhCkoy3iwX921E6oFHWZdXNwM53V6CItQzuPomCba8OYgvURVOm8M7\n3xOCiN1LNPIz1pDp81PcNXzAw9l8eLPNcD+NauCjgUjkKa1juPD8KGQ7mbN9/pqd\niPaZIgiRRxaJNXhdd6HPv0nh/SSUK2k2e+gc5iqQilvVOzRZQtxtz7sPQRxVzfUN\nWy4WIibvYR6X/OJTyM9bo8ep8boOhhLLE8oVx+zkNo3aXBM9ZdIOXXB03L+PemrB\nLg/Txl4PK1lszGFs/sBhTtnmT0ayWuIZFHCE+CAA7QGnl37DvRJckiMXoKUdRRcV\nI5qSCLUiiI3cKyTr4LEXaNOvYb3ZhXj2jbp4yjeNY77nrB/fpUcJucglMVRGURFV\nDYlcjdrSGC1z8rjVJ/VIIjfRYvd7Dcg4i6FKsPzQ8eu3hmPn4A5zf/1yUbXpfeJV\nBWR4Z38CAwEAAaNjMGEwHwYDVR0jBBgwFoAUzdeQoW6jv9sw1toyJZAM5jkegGUw\nDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFM3XkKFu\no7/bMNbaMiWQDOY5HoBlMA0GCSqGSIb3DQEBBQUAA4ICAQB+FojoEw42zG4qhQc4\nxlaJeuNHIWZMUAgxWlHQ/KZeFHXeTDvs8e3MfhEHSmHu6rOOOqQzxu2KQmZP8Tx7\nyaUFQZmx7Cxb7tyW0ohTS3g0uW7muw/FeqZ8Dhjfbw90TNGp8aHp2FRkzF6WeKJW\nGsFzshXGVwXf2vdIJIqOf2qp+U3pPmrOYCx9LZAI9mOPFdAtnIz/8f38DBZQVhT7\nupeG7rRJA1TuG1l/MDoCgoYhrv7wFfLfToPmmcW6NfcgkIw47XXP4S73BDD7Ua2O\ngiRAyn0pXdXZ92Vk/KqfdLh9kl3ShCngE+qK99CrxK7vFcXCifJ7tjtJmGHzTnKR\nN4xJkunI7Cqg90lufA0kxmts8jgvynAF5X/fxisrgIDV2m/LQLvYG/AkyRDIRAJ+\nLtOYqqIN8SvQ2vqOHP9U6OFKbt2o1ni1N6WsZNUUI8cOpevhCTjXwHxgpV2Yj4wC\n1dxWqPNNWKkL1HxkdAEy8t8PSoqpAqKiHYR3wvHMl700GXRd4nQ+dSf3r7/ufA5t\nVIimVuImrTESPB5BeW0X6hNeH/Vcn0lZo7Ivo0LD+qh+v6WfSMlgYmIK371F3uNC\ntVGW/cT1Gpm4UqJEzS1hjBWPgdVdotSQPYxuQGHDWV3Y2eH2dEcieXR92sqjbzcV\nNvAsGnE8EXbfXRo+VGN4a2V+Hw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDtjCCAp6gAwIBAgIOBcAAAQACQdAGCk3OdRAwDQYJKoZIhvcNAQEFBQAwdjEL\nMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV\nBAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDQgQ0ExJTAjBgNVBAMTHFRDIFRydXN0\nQ2VudGVyIENsYXNzIDQgQ0EgSUkwHhcNMDYwMzIzMTQxMDIzWhcNMjUxMjMxMjI1\nOTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i\nSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgNCBDQTElMCMGA1UEAxMc\nVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgNCBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALXNTJytrlG7fEjFDSmGehSt2VA9CXIgDRS2Y8b+WJ7gIV7z\njyIZ3E6RIM1viCmis8GsKnK6i1S4QF/yqvhDhsIwXMynXX/GCEnkDjkvjhjWkd0j\nFnmA22xIHbzB3ygQY9GB493fL3l1oht48pQB5hBiecugfQLANIJ7x8CtHUzXapZ2\nW78mhEj9h/aECqqSB5lIPGG8ToVYx5ct/YFKocabEvVCUNFkPologiJw3fX64yhC\nL04y87OjNopq1mJcrPoBbbTgci6VaLTxkwzGioLSHVPqfOA/QrcSWrjN2qUGZ8uh\nd32llvCSHmcOHUJG5vnt+0dTf1cERh9GX8eu4I8CAwEAAaNCMEAwDwYDVR0TAQH/\nBAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFB/quz4lGwa9pd1iBX7G\nTFq/6A9DMA0GCSqGSIb3DQEBBQUAA4IBAQBYpCubTPfkpJKknGWYGWIi/HIy6QRd\nxMRwLVpG3kxHiiW5ot3u6hKvSI3vK2fbO8w0mCr3CEf/Iq978fTr4jgCMxh1KBue\ndmWsiANy8jhHHYz1nwqIUxAUu4DlDLNdjRfuHhkcho0UZ3iMksseIUn3f9MYv5x5\n+F0IebWqak2SNmy8eesOPXmK2PajVnBd3ttPedJ60pVchidlvqDTB4FAVd0Qy+BL\niILAkH0457+W4Ze6mqtCD9Of2J4VMxHL94J59bXAQVaS4d9VA61Iz9PyLrHHLVZM\nZHQqMc7cdalUR6SnQnIJ5+ECpkeyBM1CE+FhDOB4OiIgohxgQoaH96Xm\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET\nMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv\nbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0\nMDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw\nbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx\nFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+\n+FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1\nXQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w\ntj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW\nq6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM\naLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E\nBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3\nR01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE\nggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93\nd3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl\nIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0\nYW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj\nb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp\nY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc\nNplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP\ny3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7\nR6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg\nxhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP\nIQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX\nUKqK1drk/NAJBzewdXUh\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFbjCCA1agAwIBAgIPQupbClERJnzYJ3S3339xMA0GCSqGSIb3DQEBBQUAMDMx\nCzAJBgNVBAYTAlBUMQ0wCwYDVQQKDARTQ0VFMRUwEwYDVQQDDAxFQ1JhaXpFc3Rh\nZG8wHhcNMDYwNjIzMTM0MTI3WhcNMzAwNjIzMTM0MTI3WjAzMQswCQYDVQQGEwJQ\nVDENMAsGA1UECgwEU0NFRTEVMBMGA1UEAwwMRUNSYWl6RXN0YWRvMIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2++iQ27Iqf1u19+sopKEochZoAyaU/7v\nrswZDXKKpMIzI+/nBnLqbUs6QVIPyUgOLee6ZO6iOkxjXGYpi9+piMW96PH3jkv8\nATxEEjkqcKLA28Wi31/HS8ao3D1hfEpYwUQyk95wmaEjJlY/o+HqXzBG2Hj1MKOW\nCYmwPfGGkwW2EmoYjfClZDsrh2RePReOC27mmMyXODggjHBaaSu9ZY3NN1lcbNFy\ndFkGTsi3Add3v/BIhqizGl1B1DcXERBfSm6NdcUDQH0hrgDw2/yfbDpmpN/3yt+A\nZlrZ2H8UoiYZ9K4LIeDKPgXdFth+WdqhsGnDnTQT+mVJOYfudi+NvTwnGQNOrQ4L\nKyzGLnETNSlX6XDcG1HqzZfxlY2yhvomBi+AGpXxmDvu9uWGpc4bAeX06TPKD1VE\nX2iKLMdbZijdlkuDnV4dfhjV/rJg+5pRaMOWjB9oS1BSCzbmMSfk1ykMG9obL+EE\nU7jUeUmwO4FeCIgid+IpwK5yqqu0clK9bLv1unjZnLggbzCNSp0y+fQB5mJ5mEJA\nBXpvHCo/tfvfzRhAjuUQxDlbVvE8VwWr0jlNP/iLI8druUCx4v7/sxwKaR+bjA+0\nH+AK3kj9jV+PmfUBdgU2XY7cM45RbhHiQf3Mt40qXz6S5fKx4KQj4qK3xo0YmylK\n0UZ/9GQgGN0CAwEAAaN/MH0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAQYwHQYDVR0OBBYEFHF/Nd71d3FtHRKc4ZCkuvCpg4+AMDsGA1UdIAQ0MDIwMAYE\nVR0gADAoMCYGCCsGAQUFBwIBFhpodHRwOi8vd3d3LmVjZWUuZ292LnB0L2RwYzAN\nBgkqhkiG9w0BAQUFAAOCAgEAjK2ccqW1Z3ZnOIfpOoz+nVk1vpDxAwCgWNiY0b/8\n/PNQ3LRl1dq68IwufA3mCZFfTaP2XXicWF1qcJSjr9svAMkDQGvfUQMWGYwrvJk2\n9sCtkhgTjKftHdLfA5AF7LCTmJv3TVoT+Oeb9zZ23nwm+BE4T0lOs3MfXydb4Z4y\nHvbAmBvZICxclo2GyQtF15Ktir3qV6KjVrYgPOyyxzl+sID+vVErKrTDcmnD+Ucu\nbv+ch+3cdcsQiOC0zi4OUx0L6G4eQkzQvjl4dckU3ieRc6rsaoDw8BeWYk++BMvi\np+VdD5NFy1lIJhPe3bH1CtoWsagdj35YG7fVCd6Ia86EPqi+UmLK0qGhx8s8FuB2\nVjA/5g9rBnf+ZJ1aanN87t4h6ZpJlze2hH+ikT5F+9daBsWHNdy6SEyGAQhHNrY4\nUJURmXPRN0kK+kJPLxBU00GQ+sjcuxHcDcx9fJvcDpFxhk248hWaKzgXEaHynqhs\nnOPOruLmS4vyigY7B3cCEe6D6p1mhsrwYqnVV4OkFfFFFP4adX+lD9xSdFl1Cvj7\nVUGpXI0xRN3NlE4z0RtBqtvXoTzwxUhtRUE1tXmD5vlN8VY4179AIvsggOMcwllG\nB2MCYQA7m1C7Q8Ow6QqauHb0R2FVZHBPN9mcEaMTsuHdQEK7mNegBovmaFdLDjho\nf7o=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT\nAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD\nQTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP\nMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do\n0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ\nUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d\nRdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ\nOA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv\nJoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C\nAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O\nBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ\nLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY\nMnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ\n44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I\nJd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw\ni/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN\n9u6wWk5JRFRYX0KD\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIH/jCCBeagAwIBAgIBADANBgkqhkiG9w0BAQUFADCB1DELMAkGA1UEBhMCQVQx\nDzANBgNVBAcTBlZpZW5uYTEQMA4GA1UECBMHQXVzdHJpYTE6MDgGA1UEChMxQVJH\nRSBEQVRFTiAtIEF1c3RyaWFuIFNvY2lldHkgZm9yIERhdGEgUHJvdGVjdGlvbjEq\nMCgGA1UECxMhR0xPQkFMVFJVU1QgQ2VydGlmaWNhdGlvbiBTZXJ2aWNlMRQwEgYD\nVQQDEwtHTE9CQUxUUlVTVDEkMCIGCSqGSIb3DQEJARYVaW5mb0BnbG9iYWx0cnVz\ndC5pbmZvMB4XDTA2MDgwNzE0MTIzNVoXDTM2MDkxODE0MTIzNVowgdQxCzAJBgNV\nBAYTAkFUMQ8wDQYDVQQHEwZWaWVubmExEDAOBgNVBAgTB0F1c3RyaWExOjA4BgNV\nBAoTMUFSR0UgREFURU4gLSBBdXN0cmlhbiBTb2NpZXR5IGZvciBEYXRhIFByb3Rl\nY3Rpb24xKjAoBgNVBAsTIUdMT0JBTFRSVVNUIENlcnRpZmljYXRpb24gU2Vydmlj\nZTEUMBIGA1UEAxMLR0xPQkFMVFJVU1QxJDAiBgkqhkiG9w0BCQEWFWluZm9AZ2xv\nYmFsdHJ1c3QuaW5mbzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANIS\nR+xfmOgNhhVJxN3snvFszVG2+5VPi8SQPVMzsdMTxUjipb/19AOED5x4cfaSl/Fb\nWXUYPycLUS9caMeh6wDz9pU9acN+wqzECjZyelum0PcBeyjHKscyYO5ZuNcLJ92z\nRQUre2Snc1zokwKXaOz8hNue1NWBR8acwKyXyxnqh6UKo7h1JOdQJw2rFvlWXbGB\nARZ98+nhJPMIIbm6rF2ex0h5f2rK3zl3BG0bbjrNf85cSKwSPFnyas+ASOH2AGd4\nIOD9tWR7F5ez5SfdRWubYZkGvvLnnqRtiztrDIHutG+hvhoSQUuerQ75RrRa0QMA\nlBbAwPOs+3y8lsAp2PkzFomjDh2V2QPUIQzdVghJZciNqyEfVLuZvPFEW3sAGP0q\nGVjSBcnZKTYl/nfua1lUTwgUopkJRVetB94i/IccoO+ged0KfcB/NegMZk3jtWoW\nWXFb85CwUl6RAseoucIEb55PtAAt7AjsrkBu8CknIjm2zaCGELoLNex7Wg22ecP6\nx63B++vtK4QN6t7565pZM2zBKxKMuD7FNiM4GtZ3k5DWd3VqWBkXoRWObnYOo3Ph\nXJVJ28EPlBTF1WIbmas41Wdu0qkZ4Vo6h2pIP5GW48bFJ2tXdDGY9j5xce1+3rBN\nLPPuj9t7aNcQRCmt7KtQWVKabGpyFE0WFFH3134fAgMBAAGjggHXMIIB0zAdBgNV\nHQ4EFgQUwAHV4HgfL3Q64+vAIVKmBO4my6QwggEBBgNVHSMEgfkwgfaAFMAB1eB4\nHy90OuPrwCFSpgTuJsukoYHapIHXMIHUMQswCQYDVQQGEwJBVDEPMA0GA1UEBxMG\nVmllbm5hMRAwDgYDVQQIEwdBdXN0cmlhMTowOAYDVQQKEzFBUkdFIERBVEVOIC0g\nQXVzdHJpYW4gU29jaWV0eSBmb3IgRGF0YSBQcm90ZWN0aW9uMSowKAYDVQQLEyFH\nTE9CQUxUUlVTVCBDZXJ0aWZpY2F0aW9uIFNlcnZpY2UxFDASBgNVBAMTC0dMT0JB\nTFRSVVNUMSQwIgYJKoZIhvcNAQkBFhVpbmZvQGdsb2JhbHRydXN0LmluZm+CAQAw\nDwYDVR0TAQH/BAUwAwEB/zALBgNVHQ8EBAMCAcYwEQYDVR0gBAowCDAGBgRVHSAA\nMD0GA1UdEQQ2MDSBFWluZm9AZ2xvYmFsdHJ1c3QuaW5mb4YbaHR0cDovL3d3dy5n\nbG9iYWx0cnVzdC5pbmZvMD0GA1UdEgQ2MDSBFWluZm9AZ2xvYmFsdHJ1c3QuaW5m\nb4YbaHR0cDovL3d3dy5nbG9iYWx0cnVzdC5pbmZvMA0GCSqGSIb3DQEBBQUAA4IC\nAQAVO4iDXg7ePvA+XdwtoUr6KKXWB6UkSM6eeeh5mlwkjlhyFEGFx0XuPChpOEmu\nIo27jAVtrmW7h7l+djsoY2rWbzMwiH5VBbq5FQOYHWLSzsAPbhyaNO7krx9i0ey0\nec/PaZKKWP3Bx3YLXM1SNEhr5Qt/yTIS35gKFtkzVhaP30M/170/xR7FrSGshyya\n5BwfhQOsi8e3M2JJwfiqK05dhz52Uq5ZfjHhfLpSi1iQ14BGCzQ23u8RyVwiRsI8\np39iBG/fPkiO6gs+CKwYGlLW8fbUYi8DuZrWPFN/VSbGNSshdLCJkFTkAYhcnIUq\nmmVeS1fygBzsZzSaRtwCdv5yN3IJsfAjj1izAn3ueA65PXMSLVWfF2Ovrtiuc7bH\nUGqFwdt9+5RZcMbDB2xWxbAH/E59kx25J8CwldXnfAW89w8Ks/RuFVdJG7UUAKQw\nK1r0Vli/djSiPf4BJvDduG3wpOe8IPZRCPbjN4lXNvb3L/7NuGS96tem0P94737h\nHB5Ufg80GYEQc9LjeAYXttJR+zV4dtp3gzdBPi1GqH6G3lb0ypCetK2wHkUYPDSI\nAofo8DaR6/LntdIEuS64XY0dmi4LFhnNdqSr+9Hio6LchH176lDq9bIEO4lSOrLD\nGU+5JrG8vCyy4YGms2G19EVgLyx1xcgtiEsmu3DuO38BLQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW\nMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg\nQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh\ndGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9\nMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi\nU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh\ncnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA\nA4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk\npMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf\nOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C\nJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT\nKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi\nHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM\nAv+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w\n+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+\nGkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3\nZzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B\n26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID\nAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE\nFE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j\nZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js\nLnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM\nBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0\nY29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy\ndGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh\ncnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh\nYmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg\ndGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp\nbGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ\nYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT\nTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ\n9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8\njhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW\nFjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz\newT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1\nny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L\nEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu\nL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq\nyvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC\nO3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V\num0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh\nNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW\nMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg\nQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh\ndGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9\nMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi\nU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh\ncnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA\nA4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk\npMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf\nOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C\nJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT\nKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi\nHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM\nAv+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w\n+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+\nGkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3\nZzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B\n26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID\nAQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\nVR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul\nF2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC\nATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w\nZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk\naWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0\nYXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg\nc2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0\naWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93\nd3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG\nCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1\ndGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF\nwWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS\nTa0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst\n0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc\npRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl\nCcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF\nP0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK\n1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm\nKhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE\nJnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ\n8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm\nfyWl8kgAwKQB2j8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV\nBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln\nbiBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF\nMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT\nd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC\nCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8\n76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+\nbbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c\n6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE\nemA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd\nMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt\nMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y\nMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y\nFGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi\naG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM\ngI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB\nqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7\nlqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn\n8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov\nL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6\n45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO\nUYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5\nO1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC\nbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv\nGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a\n77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC\nhdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3\n92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp\nLd6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w\nZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt\nQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE\nBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu\nIFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow\nRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY\nU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A\nMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv\nFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br\nYT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF\nnbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH\n6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt\neJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/\nc8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ\nMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH\nHTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf\njNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6\n5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB\nrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU\nF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c\nwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0\ncDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB\nAHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp\nWJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9\nxCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ\n2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ\nIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8\naRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X\nem1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR\ndAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/\nOMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+\nhAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy\ntGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE\nBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWdu\nIFBsYXRpbnVtIENBIC0gRzIwHhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAw\nWjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMSMwIQYDVQQD\nExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQAD\nggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu669y\nIIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2Htn\nIuJpX+UFeNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+\n6ixuEFGSzH7VozPY1kneWCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5ob\njM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIoj5+saCB9bzuohTEJfwvH6GXp43gOCWcw\nizSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/68++QHkwFix7qepF6w9fl\n+zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34TaNhxKFrY\nzt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaP\npZjydomyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtF\nKwH3HBqi7Ri6Cr2D+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuW\nae5ogObnmLo2t/5u7Su9IPhlGdpVCX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMB\nAAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCvzAeHFUdvOMW0\nZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW\nIGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUA\nA4ICAQAIhab1Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0\nuMoI3LQwnkAHFmtllXcBrqS3NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+\nFHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4U99REJNi54Av4tHgvI42Rncz7Lj7\njposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8KV2LwUvJ4ooTHbG/\nu0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl9x8D\nYSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1\npuEa+S1BaYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXa\nicYwu+uPyyIIoK6q8QNsOktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbG\nDI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSYMdp08YSTcU1f+2BY0fvEwW2JorsgH51x\nkcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAciIfNAChs0B0QTwoRqjt8Z\nWr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI\nMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\nFzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz\nMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv\ncnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz\nZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO\n0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao\nwW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj\n7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS\n8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT\nBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB\n/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg\nJYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC\nNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3\n6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/\n3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm\nD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS\nCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR\n3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK\nMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\nGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx\nMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg\nQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ\niQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa\n/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ\njnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI\nHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7\nsFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w\ngZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF\nMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw\nKaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG\nAQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L\nURYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO\nH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm\nI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY\niNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc\nf8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB\nyjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\nExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp\nU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW\nZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0\naG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL\nMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW\nZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln\nbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp\nU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y\naXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1\nnmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex\nt0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz\nSdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG\nBO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+\nrCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/\nNIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E\nBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH\nBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy\naXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv\nMzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE\np6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y\n5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK\nWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ\n4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N\nhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\nb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG\nEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\ncnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c\nJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP\nmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+\nwRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4\nVYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/\nAUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB\nAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\nBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun\npyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC\ndWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf\nfwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm\nNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx\nH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe\n+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\nQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\nCSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\nnh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\nT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\ngdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\nBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\nTLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\nDQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\nhMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\nPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\nYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\nCAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\nZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\nMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\nLmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\nRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\nPNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\nxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\nIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\nhzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\nEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\nMAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\nFLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\nnzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\neM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\nhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\nYzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\nvEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n+OkuE6N36B9K\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB\nqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf\nQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw\nMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV\nBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw\nNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j\nLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG\nA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl\nIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs\nW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta\n3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk\n6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6\nSk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J\nNqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA\nMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP\nr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU\nDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz\nYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX\nxPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2\n/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/\nLHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7\njVaMaA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x\nGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv\nb3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV\nBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W\nYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa\nGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg\nFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J\nWpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB\nrrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp\n+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1\nksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i\nUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz\nPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og\n/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH\noycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI\nyV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud\nEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2\nA8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL\nMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT\nElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f\nBluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn\ng/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl\nfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K\nWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha\nB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc\nhLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR\nTUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD\nmbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z\nohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y\n4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza\n8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x\nGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv\nb3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV\nBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W\nYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM\nV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB\n4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr\nH556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd\n8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv\nvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT\nmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe\nbtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc\nT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt\nWAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ\nc6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A\n4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD\nVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG\nCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0\naXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0\naWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu\ndC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw\nczALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G\nA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC\nTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg\nUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0\n7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem\nd1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd\n+LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B\n4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN\nt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x\nDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57\nk8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s\nzHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j\nWy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT\nmJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK\n4SVhM7JZG+Ju1zdXtg2pEto=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY\nMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo\nR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx\nMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK\nEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp\nZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9\nAWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA\nZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0\n7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W\nkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI\nmO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G\nA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ\nKoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1\n6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl\n4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K\noKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj\nUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU\nAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC\nVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0\nLm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW\nKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl\ncnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw\nNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw\nNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy\nZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV\nBAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo\nNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4\n4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9\nKlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI\nrb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi\n94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB\nsDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi\ngA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo\nkORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE\nvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA\nA4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t\nO1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua\nAGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP\n9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/\neu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m\n0vdXcDazv/wor3ElhVsT/h5/WrQ8\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsx\nCzAJBgNVBAYTAkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRp\nZmljYWNpw7NuIERpZ2l0YWwgLSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwa\nQUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4wHhcNMDYxMTI3MjA0NjI5WhcNMzAw\nNDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+U29jaWVkYWQgQ2Ft\nZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJhIFMu\nQS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeG\nqentLhM0R7LQcNzJPNCNyu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzL\nfDe3fezTf3MZsGqy2IiKLUV0qPezuMDU2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQ\nY5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU34ojC2I+GdV75LaeHM/J4\nNy+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP2yYe68yQ\n54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+b\nMMCm8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48j\nilSH5L887uvDdUhfHjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++Ej\nYfDIJss2yKHzMI+ko6Kh3VOz3vCaMh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/zt\nA/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK5lw1omdMEWux+IBkAC1vImHF\nrEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1bczwmPS9KvqfJ\npxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE\nAwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCB\nlTCBkgYEVR0gADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFy\nYS5jb20vZHBjLzBaBggrBgEFBQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW50\n7WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2UgcHVlZGVuIGVuY29udHJhciBlbiBs\nYSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEfAygPU3zmpFmps4p6\nxbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuXEpBc\nunvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/\nJre7Ir5v/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dp\nezy4ydV/NgIlqmjCMRW3MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42\ngzmRkBDI8ck1fj+404HGIGQatlDCIaR43NAvO2STdPCWkPHv+wlaNECW8DYSwaN0\njJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wkeZBWN7PGKX6jD/EpOe9+\nXCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f/RWmnkJD\nW2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/\nRL5hRqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35r\nMDOhYil/SrnhLecUIw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxk\nBYn8eNZcLCZDqQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB\ngTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV\nBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw\nMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl\nYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P\nRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0\naG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3\nUcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI\n2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8\nQ5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp\n+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+\nDT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O\nnKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW\n/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g\nPKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u\nQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY\nSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv\nIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/\nRxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4\nzJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd\nBA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB\nZQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi\nMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu\nMTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp\ndHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV\nUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO\nZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz\nc7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP\nOCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl\nmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF\nBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4\nqY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw\ngZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB\nBjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu\nbmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp\ndHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8\n6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/\nh1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH\n/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv\nwKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN\npGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGKjCCBBKgAwIBAgIQZgej0p0pVhgO4V5ZmLGEVTANBgkqhkiG9w0BAQUFADB0\nMQswCQYDVQQGEwJMVDErMCkGA1UEChMiU2thaXRtZW5pbmlvIHNlcnRpZmlrYXZp\nbW8gY2VudHJhczEgMB4GA1UECxMXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAU\nBgNVBAMTDVNTQyBSb290IENBIEEwHhcNMDYxMjI3MTIxODUyWhcNMjYxMjI4MTIw\nNTA0WjB0MQswCQYDVQQGEwJMVDErMCkGA1UEChMiU2thaXRtZW5pbmlvIHNlcnRp\nZmlrYXZpbW8gY2VudHJhczEgMB4GA1UECxMXQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkxFjAUBgNVBAMTDVNTQyBSb290IENBIEEwggIiMA0GCSqGSIb3DQEBAQUAA4IC\nDwAwggIKAoICAQC66k++hMAZJIohqUyZffcM1aVRkqhl44mjC2bnQvh50g+DI3u3\npsEk1jXW2OUBynCxFtZHbr4QbH7pUG529+Xkgw941aBz9Y3RmR+URCOWxu5yWvna\nXTyRr2zol+iGXfeei/rErGZP5HI/O92eTjXSEx99u0RL9FOs1hTXQDm6wD/8hSDT\nxADQ59hHmQR5h4ZAsqxeyXUgwwkUrwSOpqKtKleIZaHMKL42yR8lD8NrIoQ5d046\nA8Bq2z66tome5NcumrdDAT/52qyprOR3M4ftCzndx8GtDVmDMNE2BFi0ZE7m/wjo\nQrGAq/iY//MphhYRJE4Joc8wf7xesApqoXFr9ZoSayVtdwKiRl75aS/7OxiVX45c\nl5RgXh1xqEG0Xc9aemfj1Eo1HzfgdhYDO/RRnJgUKUmIDELQLW2pp0AmOnkAMDvA\nu0SYrSTO0ZbciXiB9lpbQrx04YfTZchH5jayzMFvwMfcgCVSPDGQ3cnIUKh6u3bg\n7xOUzgR+arZOd/mD0G/4OtAKQ8q6ELb/PB2UYJSEbfWlyX1MCn4vj2/93S17Sunv\nNNu7fv8Mbzf6+cPMyS/R6Sw9KqxsJjvQCV7EgCeL3WHw55VRQ8QN5jHQeNbBxsJm\nAdHjzMfTHhUFNtuUmuxSw5HHL7H0A/cHrNNLkatWPNCu/V9tLdMAEc+TvQIDAQAB\no4G3MIG0MA8GA1UdEwEB/wQFMAMBAf8wPQYDVR0gBDYwNDAyBgsrBgEEAYGvZQEC\nADAjMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNzYy5sdC9jcHMwMwYDVR0fBCww\nKjAooCagJIYiaHR0cDovL2NybC5zc2MubHQvcm9vdC1hL2NhY3JsLmNybDAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMy/3qeQd2JqHXhpLgo4m3dRUwPwMA0GCSqG\nSIb3DQEBBQUAA4ICAQA+r8ioxzNP8G6aQ+HysFdS4ZyeBl9C1vH9yotRP+HHZWlP\ndBlQis8Yk0mNoBywOz2OSJPZ6AV+xAmxD1KKa5dv1448gADQQXOtPcNEB3Fqj2J+\nBdhTYHKxAekAYqoN2NhJwrR9DVuzlyk2mbmn0UuYa0S8shKOdmR1TA3Nwi6zWPx6\nT1WzWX9d4C8wM8+IG2npTYqQnpC5MTrzogW8/vndUI0OlBmdfo2qFX4PUpMl5IEO\nli0cAxwwgxGWQqmYpJ1fyalcO0lowoRtmdr2/qLy3DdejXrlpVfKI0uTXZIqVYSz\nlrMemJRJfGw83J4dtqvDrAnFnd4311TEnK0/sNZpAeUQhn25gYNunGZOlQWSkDGH\nJrLakXS9hORxaOR2AOB2czRHhpVluluQom0FKXhg64b5Ek3oCFakzIyiVkrOgPQU\nYSLlqx06QTuE14J4BS+sHSNoq3J5hc1G5nqngloo0BU9HduMmFDO+69YO9OproA7\nFgB2J9Vw6QmNNpQJf+PvYBBRysZVcGarUW/zUU8SVq7719kN4PqrEN5qgayFdy2s\nemN7RuE32ldurWX8IQSZhQHPIzoyxe1am9WhggR3EUWOpER9wsvLpw/oErrybrqP\nMzAb3Sn48EKjbkKlbvpWpalQg9EFZhaLLfvmktHmbAvVWiltK89519naT/Botg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGKzCCBBOgAwIBAgIRAL6SgxjzVYp4o2dZHGkkCT8wDQYJKoZIhvcNAQEFBQAw\ndDELMAkGA1UEBhMCTFQxKzApBgNVBAoTIlNrYWl0bWVuaW5pbyBzZXJ0aWZpa2F2\naW1vIGNlbnRyYXMxIDAeBgNVBAsTF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYw\nFAYDVQQDEw1TU0MgUm9vdCBDQSBCMB4XDTA2MTIyNzEyMjI1MFoXDTI2MTIyNTEy\nMDgyNlowdDELMAkGA1UEBhMCTFQxKzApBgNVBAoTIlNrYWl0bWVuaW5pbyBzZXJ0\naWZpa2F2aW1vIGNlbnRyYXMxIDAeBgNVBAsTF0NlcnRpZmljYXRpb24gQXV0aG9y\naXR5MRYwFAYDVQQDEw1TU0MgUm9vdCBDQSBCMIICIjANBgkqhkiG9w0BAQEFAAOC\nAg8AMIICCgKCAgEAwfNV9UdRTlUXZY2wskEooUrRn0v2c/8+0slNWT/kt8efBl3Y\nPKOIhOBzXf0F6seO16QEauufvUP9FJJGuMW6qu1g7OzKkI0KcqlBm9SdvLBsohEf\nZMvnHdRFZw4Ja+V47PE/BFTzmpnHWdHSeaekGrB8Sfwch1ReeAbV3R3MhaBCeNXQ\nsIrq6PGhnlbv08F9h6zn2mhPGdZv4JOtSVxzFMFGap33WEDZV1hObDf0ciME+NtK\nsN7xQZYSQKEVi2e4XnhWy3/kvsBJaJG4RwiTgcG1GzEG04B70UWhzww9YfOS+PGw\nFQ74LjBbAKNJ923+7ty/iM/wfVc+r8DRiut80m0xVfqEjXNq2nCAxPTCz5COMJrh\nxjVyAQjmP+ZmAKPy+JIdvFLsj/bc9wrvvBCH+YQYjF4fA7j/NS8BauXwW2J847N/\nM6qU105RgbXoV3iPIpapDIlUPrbu2XNfZPRE4fFqGP9SlsQcv4mXpMOnyn4Ybhbc\nE4y71bUlCYav9i9FlCowwRSUNfZdyiWVnLFYibi1YIXJxr4UGaM++VaFq8ps1pl5\nokoUb8M62OdmUQrpHP7MaeY0bPSB232iEfhMxIcFFj3rl3Q/buycubYnjCTfLbOv\n3RNhdo//8kzgCBkwMiQyXDaAF+6Gyd8vUeJWroOS8LO92Ic6LJ7E3GmZ+csCAwEA\nAaOBtzCBtDAPBgNVHRMBAf8EBTADAQH/MD0GA1UdIAQ2MDQwMgYLKwYBBAGBr2UB\nAgAwIzAhBggrBgEFBQcCARYVaHR0cDovL3d3dy5zc2MubHQvY3BzMDMGA1UdHwQs\nMCowKKAmoCSGImh0dHA6Ly9jcmwuc3NjLmx0L3Jvb3QtYi9jYWNybC5jcmwwDgYD\nVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBScA/Co0phyaK7y7eBP4oUOsiVOzzANBgkq\nhkiG9w0BAQUFAAOCAgEArFy8L/yuASSmED6sqOGnJ5mNyojBHT2R9qJ+pfGYQf+q\nYfgJvs0aJWF0tMOvQloJD5EBvkiV9Mp3XguDzoSdz0D9gCy942Y1Crix+mDa5dhU\ntUuXuqIawyBpjbRGc1yqv717/xowNFhA+StgC3lE+feilgtrUnvwK0s70ouga5M9\nyVdjimvMUBOPd6hRvhpMLUxdDJBbjvPvUCBtgeZRSavE59ddCCtR/D1GEufRpXbF\nUyQFyarTjljF84p0kjLt8C/dq63p0jWPdCPjmQDiizDkw0Ku8Lvp4ggbSnAtffjS\nmieRQnB1egh+vi8cfzc9qIvcRnL16G82aPpujSCd1PUHcb+9J0K5cyjW7Em0BYVP\naEj2q5TfDqNGFGDCMSA76y5b3tWhLG3lUvqBX5eIyWO9AezjzWsKNcLJOOMO81gb\nfdqQbbf1yFhWna4B35GdrVWCAwwRdASRhsd8k4zzJ/vFJFdui9kbmJ2IMfCvd7gN\ntMzP9gpvEpvsCStTiexE4KFpi6h0hnQYUuDSv6ChZSG5CIN686T1+F43JUeZpl3X\nIlrbk2cX2xDjjNESkUeKlaVHoQP4Sy4hxZBisH8no9sVfzh/bH9OBcUDtC3fRV91\nLB3xX6a19hc5Qen4ZcIeWBHKfI7itbqSD2e3j+uZ1DH7cntamF+SlMcE6jD2uxo=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGKjCCBBKgAwIBAgIQNLkSn6zHklVCXN5X/+PABTANBgkqhkiG9w0BAQUFADB0\nMQswCQYDVQQGEwJMVDErMCkGA1UEChMiU2thaXRtZW5pbmlvIHNlcnRpZmlrYXZp\nbW8gY2VudHJhczEgMB4GA1UECxMXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAU\nBgNVBAMTDVNTQyBSb290IENBIEMwHhcNMDYxMjI3MTIyNjMwWhcNMjYxMjIyMTIx\nMTMwWjB0MQswCQYDVQQGEwJMVDErMCkGA1UEChMiU2thaXRtZW5pbmlvIHNlcnRp\nZmlrYXZpbW8gY2VudHJhczEgMB4GA1UECxMXQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkxFjAUBgNVBAMTDVNTQyBSb290IENBIEMwggIiMA0GCSqGSIb3DQEBAQUAA4IC\nDwAwggIKAoICAQChRSL6jMypbwSz9GgyFmkRT3nfQ71RYHAamN14eJaYYvdwS4Go\n4B0EifSP627p8P+B2C59rxcg8SLv8D9FR0C0y7K4ID8+SmhQ/5oG15fFt4oWLnHS\nR3NdGGUv7zkz6LZVryatAoDpY9chcAc+zL5ficD4zh0lbsP8f1Y5YdGOwiZ653gC\nClndVSOw+DWn4qvzqy/XtYsKKnJUK215vPLZ6UP5z/GOZhL3l1kq2deU3PiUs0Wj\nrxYts4DKPc7opscKlHT8N5rpPww3FiBDyUdwu4yF/JiJKcuHGX4ZUxCJgHWuE/G/\npF0wBSl8qPe2XgcwFYiuTRWgys3X/6ujBlcPp+OJaRzWGtHUJ9+Wxjhcr3f+FatE\nQX3TmLuoIBivi23UWsLYlo1I9QcxfmH0YZtSgUCOSicEsgfTAhCU8/vdsXtwuLTI\ngfUAB6aNiAVNxI+WztS2wMFmjCqsaErJRtwN5i6oeSh9d0NwFn4cGjqmeU8TQImx\nMrsJRhENdLwn5djtLfpQKdwlypcQ56miYS46iaZEYb5PXpIJ7dwupu9Tu2El2Cel\nFEYphSYA2Pn5BdV7FjFCQwUXkZxKYEAkbbVtenn7nJpjw5hp5XdiIypRiQ9ssv3D\nytj0GkOU0H0L4Vg+Gsh0hJv3rIKuUUWS0gZZ4bPB3qUfkyJ52M3EeWAjlQIDAQAB\no4G3MIG0MA8GA1UdEwEB/wQFMAMBAf8wPQYDVR0gBDYwNDAyBgsrBgEEAYGvZQEC\nADAjMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNzYy5sdC9jcHMwMwYDVR0fBCww\nKjAooCagJIYiaHR0cDovL2NybC5zc2MubHQvcm9vdC1jL2NhY3JsLmNybDAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFIgHc/bxvFIaWh09kWLtXaydC+W3MA0GCSqG\nSIb3DQEBBQUAA4ICAQAfkHFQmNXZNNKWhBjCrEYCIBzLObG3rwWk11jzkVF9joEn\nnOiSseccnzqLEFJzTMLHQh3Q694qyiJRfYx0ehr8vKTzc8hmI8QuQxBH4IppV+4v\n8gBSsDCSqtbUFcVXy2B69A6N/h4JY3SP4P6+UNkBOVa6UEz240Wau1J23n6d+43C\nVDE+x7E8Pt/jT/3dmyRpfO3ocbZCBscfxV/7IHXbwf3pbKIqkNSG/c0N/+AFilhh\nPZ/EmS/t23zEDZiYVZx0ohde26oR5DcMJP8gZ9El25qJoGWIMZEEcV8glFgzNh0y\n3m/XZwipoDv926RQJZYeqV+JF6WXmVGVadvE8Y/0bzArWfOsdYczfQbd4cFr1sTJ\nXnBEemrHnHc7Fv7+db6fLNHAA+4ReXXsqVsceoW1KFAgqRod5nuMMxj/we3IdmUf\nHfBMO6fb6s1W2JRXP+BIqX+MM0u99AxlFICC9DV32AQQcM4PbMFZy5mtge7ePUjQ\neogvQJPXnLp5hBiAdd/QWt9Rdz5YiWl1RzHkahZwVATsvVx5U2PS4l69TSXaEbYP\nquksrvXRqY0CVsv8sCTqjLpw/zLQt8YEKmPVykaR1ZlyCQdeKAOrEhwls2w6WWW0\ndG0tLRlyb/3nmBGHHnMjvzXxm7bD2cw7UHxy6M9ewJjMLgP9Hy/KdFyxHNHsaQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIQJAZiXxG2TMzanpxs6gQCaDANBgkqhkiG9w0BAQUFADBF\nMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL\nExNUcnVzdGlzIEVWUyBSb290IENBMB4XDTA3MDEwOTEyMDAwMVoXDTI3MDEwOTEx\nNTYwMFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc\nMBoGA1UECxMTVHJ1c3RpcyBFVlMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBANJclMPP3EkLfN8EXAvqebvg0DdbjA7XzTkY3oyXuLQ0EG1n\nrcSzDu031KW/+kPl2nAnhkef4umygbm+LVu5sSiAMEHfuJSpIKZ+qqzKcEyjEgzP\nYGKBUnpJgwvIIiBzoJ01PJZv7c49JsJPSMqigqeVBqn2JjL+8sH9I1hfcTv4x9fd\nTDMfoNbBz3M3q5X+nZ3s7IXD4gm7iVf5lFYqVGKJzARobmSKT7GFe2zUpL0mCvUn\n/BM+XWozCmc5Yoz+FP2qJNpP3rK18neQoZHvKXcdcZVZtsDjgbj0/4nueqf7Wf7J\n1I1u39apn/wLNbXcbqSZefOIMGv6SqlrOMhhS0MCAwEAAaNjMGEwDwYDVR0TAQH/\nBAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHktc5bOsn1acw6HVikx\nxSjy7aVfMB8GA1UdIwQYMBaAFHktc5bOsn1acw6HVikxxSjy7aVfMA0GCSqGSIb3\nDQEBBQUAA4IBAQBDyG/fm8wJr9w4Jh1I+9aWc6Bm3xriwOad0i+JHOg08iI0kfQV\nny69tHKq1+08n61Rn38ehWYlakJpgGA3Vka6PFaraoCAmLXKeljbBQjH9geBv6A1\nNck3kr8R8hdBTz9B7RPTrxk9cYp+sGTIO+f6kxNk/lbD3ivZ61fNgtCMnVJSvGET\nNT9vn2j2afduGs8LOEpDtUAFM9KmKxo2D9OPsfd7Ph3SoG3YQ6orP2kLAH4a5E5v\nym0xkf90gfz74kiTdlyLjMPcoQggC1tlNvx2+yM/0dqXwdNakKO/RZvXW4EIpFL8\neH4YLi1x/drvDYFGifBS8gxQaffO1QF8sn8o\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHEzCCBPugAwIBAgIPLeQK4ZvRwqpM9ACsgTX5MA0GCSqGSIb3DQEBBQUAMIGk\nMQswCQYDVQQGEwJFUzFKMEgGA1UECgxBQ29sZWdpbyBkZSBSZWdpc3RyYWRvcmVz\nIGRlIGxhIFByb3BpZWRhZCB5IE1lcmNhbnRpbGVzIGRlIEVzcGHDsWExGzAZBgNV\nBAsMEkNlcnRpZmljYWRvIFByb3BpbzEsMCoGA1UEAwwjUmVnaXN0cmFkb3JlcyBk\nZSBFc3Bhw7FhIC0gQ0EgUmHDrXowHhcNMDcwMTA5MTcwMDM5WhcNMzEwMTA5MTcw\nMDM5WjCBpDELMAkGA1UEBhMCRVMxSjBIBgNVBAoMQUNvbGVnaW8gZGUgUmVnaXN0\ncmFkb3JlcyBkZSBsYSBQcm9waWVkYWQgeSBNZXJjYW50aWxlcyBkZSBFc3Bhw7Fh\nMRswGQYDVQQLDBJDZXJ0aWZpY2FkbyBQcm9waW8xLDAqBgNVBAMMI1JlZ2lzdHJh\nZG9yZXMgZGUgRXNwYcOxYSAtIENBIFJhw616MIICIjANBgkqhkiG9w0BAQEFAAOC\nAg8AMIICCgKCAgEArFAbDpLOuHwVavjkD518fHx25AsmOlEGzSiz7Q8+2ZF7zPyH\ng0L3e7BduHpn/jQhYr+5KcPeWvED8uvy4hLCZWR2p/XmyzGjaPJ5651UxVL/nz2D\nYw7mvx0oAn38I/REk6OpQ5zY6CUaIDX1tbDO61Ur+tlesKFEK+UALCQPN38yNISy\nyBVvivXy6C73Q44CuDKbgBpTHQGZSGt081pwSqTo9wLRupGja4e+EF5+VLlYsgr2\nOwrjDjjzgF33QY74jza5g5sRTOELscWTijOyv5u2nkS3H/4qgSg5fM/UrzVlrmde\njSHfAGARK9Q85CdQn5O3BfHSDhTcKYKW8SqiG0MFcLPQXB4DQVX+FjjFUk2TtbQ8\ndiJNqSusFcSpS3S5pSPYzStIweLvzd74SrDfoOPuhjW/W3KUb7JGSupKU64x5pG1\ndJhFmqR97HEq5ZBRNkP5SdTXKAYDsf15h9YG+Kyh+b8UeA3LI0vNuy4y9H28abu2\nNX55z71Lcn5hqyp+QMcM5bKQtUwM1lcHfJfM+dl323vnjBN+zH4YT0xLI46uGsfq\nXx+mF904tk/eCm5SUFmsbc3WMRm9JOmgWM/Z1LJDeT9f1m+qZchG8tLVfvkuQxjC\nmORo38HTX0UvadEd7pEkSNLrAA7CEEvSnb2jTRejN5qv75cxgdqJsWF6Y6cCAwEA\nAaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud\nDgQWBBQbjVkcs7dYYmRmrOLkpPaiGRL25TCB9wYDVR0gBIHvMIHsMIHpBgRVHSAA\nMIHgMDwGCCsGAQUFBwIBFjBodHRwOi8vcGtpLnJlZ2lzdHJhZG9yZXMub3JnL25v\ncm1hdGl2YS9pbmRleC5odG0wgZ8GCCsGAQUFBwICMIGSGoGPQ2VydGlmaWNhZG8g\nc3VqZXRvIGEgbGEgRGVjbGFyYWNp824gZGUgUHLhY3RpY2FzIGRlIENlcnRpZmlj\nYWNp824gZGVsIENvbGVnaW8gZGUgUmVnaXN0cmFkb3JlcyBkZSBsYSBQcm9waWVk\nYWQgeSBNZXJjYW50aWxlcyBkZSBFc3Bh8WEgKKkgMjAwNikwDQYJKoZIhvcNAQEF\nBQADggIBAD8f1iwZdkCSnCbmnlgGEj0Swis63uXYiXdAH8ZRqnSJlsXGw53x+rxp\nE6AGdRcmifxlOY1zeevPd6e71UgmeTGRMCeYQaUX4F9cG1oqfLqtFmUAUX2H3rq6\nY9ZjtDXg104ZRX6/UWlIbz6IblJVg/CLxEz0CtQRIa4pYOhbi5/4wuy3dj+AwnQu\nR3hiUZ7bjPWtX4UF6P2ae71waAuTwjB+EvRLT3TiiY+5Q3QP1oReet5wVKQTNl9k\nftMEDv7dGW8kU5Xt6ckO1Kbxk6FbCeOi0ldOPhrOfazE91PQzaiS7aTJlyJm+Mai\n8nXlEX4vdRKW949vzwflyswHPvU8i+28fDJgPuMP1BGDNA12hmS9M5dOcO32IDhf\nmmnHwE8WyoWCjwG2uhNe0PHt6SjdKr0ljtD6EwwWD3efdik0cGzreUud70408EW7\nJSx1kkRfp5vEqtKzby68YeuGAUzZerl1Z4sDS8czUnieBcDtj3R4HRIjtjL8UVBe\nLd5QvhA8ju8IhfU6+vLe59hMOuUS6/Q2dJhaUoqUGmapbkU+FCuNNAiq7wUTYRKQ\nhGgNEVosr3mecJSfxWTLzHj2U1zg1w2xPuMWC/Om7DRCPnUQhKXYvbHj6mHmJJzC\ngdoe2G/8eC0W40QtwNI9Xn2g0lbUYDdx/kyOZZzWO9o23NgzZ9AB\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIJmzCCB4OgAwIBAgIBATANBgkqhkiG9w0BAQUFADCCAR4xPjA8BgNVBAMTNUF1\ndG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s\nYW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz\ndHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0\naWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh\nIGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ\nKoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTA3MDIxNjE1MzU1\nMVoXDTI3MDIxMTIzNTk1OVowggEeMT4wPAYDVQQDEzVBdXRvcmlkYWQgZGUgQ2Vy\ndGlmaWNhY2lvbiBSYWl6IGRlbCBFc3RhZG8gVmVuZXpvbGFubzELMAkGA1UEBhMC\nVkUxEDAOBgNVBAcTB0NhcmFjYXMxGTAXBgNVBAgTEERpc3RyaXRvIENhcGl0YWwx\nNjA0BgNVBAoTLVNpc3RlbWEgTmFjaW9uYWwgZGUgQ2VydGlmaWNhY2lvbiBFbGVj\ndHJvbmljYTFDMEEGA1UECxM6U3VwZXJpbnRlbmRlbmNpYSBkZSBTZXJ2aWNpb3Mg\nZGUgQ2VydGlmaWNhY2lvbiBFbGVjdHJvbmljYTElMCMGCSqGSIb3DQEJARYWYWNy\nYWl6QHN1c2NlcnRlLmdvYi52ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC\nggIBALcok9KOeQsz+FEa+MXGdAJVJN63wozmjcrg6uCuKguU9VhnC1UzxQjFsUze\nrnpGVwX2QYVnA0NJxyzm9fWMSkimcynnpO85uHeFyk8M1DT7WBR8REn50eK9MqVo\n8tNXAS80lUxxGdm7dbKY4iL9TL8megLnfNBNSUUaLeq11d1NL47W/uW9+hAzWlu6\naPt3cc/Fpd01XMlGL/K0w9NB5Tv9KQWDerAH6QWIKjMkmxmeQ5USojV55hztS1gP\nsnlcPWk+5oPC9H/MkZxTPn8JK9ATXcOpFMAwNn9jgJL7BMljYzV/cZFHS03aurrz\nfnb+hI3leMTpCzlnbFAR/eUSN2JIyu/blsHu3S5aXQiDVxNb+q7NCMqACeza38Zd\n6ONTyaD8gvAV6JR9rY6wB3SqKWr5Nef0wMn9/EJoGhfTli5SIjYmfjYKWj5gzrDU\n+vM3gHnlFix6hiskajdswgLEoK+PG7onW2ar6CQpay/U68FcDsn2jIDHhxAIaZIS\nK6FoecIYvZX6P8SlemDBMxuMaepXR9dFHM9hpyCaqzXbume4bscS8paLWQwMduil\noQjOEP0Ocl7Fnuk4w2Kvek+aL69s0ykp6yPoGs0y03S83FmLfwtIt4rT5LfUYQv9\n3dDBluLOt++Elw3A3HbajirVPI4lzsLFlirwUXqm/Wf7Gy6PAgMBAAGjggLeMIIC\n2jASBgNVHRMBAf8ECDAGAQH/AgECMDcGA1UdEgQwMC6CD3N1c2NlcnRlLmdvYi52\nZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0wMB0GA1UdDgQWBBRmDZwMrrrR\nSkMD7hObbfHS1HLVmjCCAVAGA1UdIwSCAUcwggFDgBRmDZwMrrrRSkMD7hObbfHS\n1HLVmqGCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0b3JpZGFkIGRlIENlcnRpZmlj\nYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xhbm8xCzAJBgNVBAYTAlZFMRAw\nDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0cml0byBDYXBpdGFsMTYwNAYD\nVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25p\nY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEgZGUgU2VydmljaW9zIGRlIENl\ncnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkqhkiG9w0BCQEWFmFjcmFpekBz\ndXNjZXJ0ZS5nb2IudmWCAQEwDgYDVR0PAQH/BAQDAgEGMDcGA1UdEQQwMC6CD3N1\nc2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0wMFQGA1Ud\nHwRNMEswJKAioCCGHmh0dHA6Ly93d3cuc3VzY2VydGUuZ29iLnZlL2xjcjAjoCGg\nH4YdbGRhcDovL2FjcmFpei5zdXNjZXJ0ZS5nb2IudmUwNwYIKwYBBQUHAQEEKzAp\nMCcGCCsGAQUFBzABhhtodHRwOi8vb2NzcC5zdXNjZXJ0ZS5nb2IudmUwQAYDVR0g\nBDkwNzA1BgVghl4BAjAsMCoGCCsGAQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRl\nLmdvYi52ZS9kcGMwDQYJKoZIhvcNAQEFBQADggIBAIIZ7DHkEaEoHIGrJR44YAjG\n9wyGXUMOpagwfBUyBmrhUc2sARNuBhmQJkhYGUUnLwDuDZFx7Y3FwjcZoEYzls1n\nKJM689/pTskFl4gk6xZnRVl8imf2j8P1jWBVzQ+B2AFuuIE0VVHxkya577LkieqR\n5AcTbV+93DRdvy/tsgpNaEUdKQmIgZTb+HbzEUxJHNLJSyqctDuTAZi66gQGG/im\nkSu4raQHHdvcK8XmUoMwwzdhG/vKv6sAfvKTS+lAlZA73lZx8n/0A9wGz8fpEd0A\ndhhUDH3SAxyETKkrtNp2dsv0E2jbEvC6piAUoYvaJcGhZMMxq4dmAxzzwGFhilxR\nxDwv4RYJjxV9xHlRmHzViwVI1/NB7Ob8d5bIDc7w417eSIuel//xAIC8ufVzPsoM\n/12n3mheMLinbec52N0/Wi/gZKbVANl0e/1vWbPd6okO/ou7QE/PGk4aHwq8rA+U\n72NM6WATAicV+rZkR0/qlDVkgfWeIg/Spl5/kqrzAHHwT3YQCNEFZGnPy6sVqPbX\nDQnG50JaARYKLm8z3akalf8gjY5UIJ3PHb39JIqpIKRwU84Q/1RIsqJo9HELd3zM\nrtcHFBfTfa7dx3DPYo30r4mE7LNT9gZ5f9+Ct8eOAvbQ3WoubQGG5r55+c7FZAU2\nEHgFy96xE/FAndEXR872\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIG/zCCBOegAwIBAgICcRkwDQYJKoZIhvcNAQEFBQAwWjELMAkGA1UEBhMCRlIx\nEzARBgNVBAoTCkNlcnRldXJvcGUxFzAVBgNVBAsTDjAwMDIgNDM0MjAyMTgwMR0w\nGwYDVQQDExRDZXJ0ZXVyb3BlIFJvb3QgQ0EgMjAeFw0wNzAzMjcyMjAwMDBaFw0z\nNzAzMjcyMzAwMDBaMFoxCzAJBgNVBAYTAkZSMRMwEQYDVQQKEwpDZXJ0ZXVyb3Bl\nMRcwFQYDVQQLEw4wMDAyIDQzNDIwMjE4MDEdMBsGA1UEAxMUQ2VydGV1cm9wZSBS\nb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDD/Fa1KwaL\n7Z5Gz8MAeRyAOaLKyhsQbSH5xx8KrPOteYKXnsaxaIhScTjEqkxHb3f95x/3lPZy\nV59EPGtf0NnOIijNcCMBFCJQEA4ae0sb9IZXj+ovaUC6RXoCQFpfNduguZ4/8D91\nzTpFkRNVw0gp87fXFPIDhqPJsFd7PdkqrF7h35U6hcYFTDGi2i2xAI6vUVeewYtF\nTSkHi6Dl5d8xDH8GbGFPa+IjMsHljCsN2JYGcLMmJ8rPs6gjAMASJIG/rEQ9F5iD\niM4JkDcuooAZSdmgCBeGmWrdHkCf0gLns5hWR3YXqk6h19vqpLrVUmdpcy6gJ1Rz\nrIvQu/BhWCaoankYwQznfFbMz83XBoYiB15zuNDmDCU1YroExPEALM6dSJ1btPbR\nYphDd1ercv4zgBAqMRvbGVApkqyB4AhpX+ZOPl6tXEh5nsVdsJeRF54W3wf6auGr\nvCV8OADh1th6nPzc1yIAUmeol7tsDWeZlxC4eThnaGGIKW6Uv1IHiDbC8i/GRmoh\nHvGa6Luf7bYms4anMEqbMGO85OhCVkQnPFqhDn3OqsMbXmjscz8/s/vEhSwEFfus\nCjhmMxmVA0vKtAR9534PDZhWPthXX7eZvnoUrcWn25QOBZ4lq7Kr+QmVeKoHi2wF\nHO5agGHo3742+7PjI9w9jHVm76PkVdCa7wIDAQABo4IBzTCCAckwDwYDVR0TAQH/\nBAUwAwEB/zARBgNVHQ4ECgQIS8lOuWexmDUwUwYDVR0gBEwwSjBIBgcqgXoBaQQB\nMD0wOwYIKwYBBQUHAgEWL2h0dHA6Ly93d3cuY2VydGV1cm9wZS5mci9yZWZlcmVu\nY2UvcGMtcm9vdDIucGRmMAsGA1UdDwQEAwIBBjCCAT8GA1UdHwSCATYwggEyMDKg\nMKAuhixodHRwOi8vd3d3LmNlcnRldXJvcGUuZnIvcmVmZXJlbmNlL3Jvb3QyLmNy\nbDB9oHugeYZ3bGRhcDovL2xjcjEuY2VydGV1cm9wZS5mci9jbj1DZXJ0ZXVyb3Bl\nJTIwUm9vdCUyMENBJTIwMixvdT0wMDAyJTIwNDM0MjAyMTgwLG89Q2VydGV1cm9w\nZSxjPUZSP2NlcnRpZmljYXRlUmV2b2NhdGlvbkxpc3QwfaB7oHmGd2xkYXA6Ly9s\nY3IyLmNlcnRldXJvcGUuZnIvY249Q2VydGV1cm9wZSUyMFJvb3QlMjBDQSUyMDIs\nb3U9MDAwMiUyMDQzNDIwMjE4MCxvPUNlcnRldXJvcGUsYz1GUj9jZXJ0aWZpY2F0\nZVJldm9jYXRpb25MaXN0MA0GCSqGSIb3DQEBBQUAA4ICAQAbRJZgJFo+a6rezdPY\nW1LAS/pRJePuzbyMPtO1Hfb8QIOsfuXXBkMtbCdz/r/apIIiUW7+jAymEVJgaAZe\nM0z6SPhbSCHWDJu+OLnhwEwToVPvIjlu7kZQZQsaHwV+d9nOJc30r8Z8nYyXbGod\n9mTtlOHOXe9AHZbLcdVKrXlYOUVNq28HuzN8rj6l6cco2mignlcnZu99l+5pqELr\nc6pLsVnGjTecqcBGUG+MSVPV5S3hok3L51u/pbs8rFLOGZNkwxCaeUKrqPuEg8JG\nX7sozA5pT3xfuzxn5g2WHoRMXiAVWzlD5YsrgiSJo6D3EGXTyYnapMFFfYlZkOtB\nno7QxAlgX5ctIW0EphGBMEyTwlhguGvWeqDlsRGfYrgwcUand2RmOkJZH1VjR9cd\noDSOgXJiSNmXrqHxvkDioDF/awDZxwLQaQIO8c4eLaSd78yBO2Oe91Qbzr7ECleb\nzbFr4qfgqx4eg9jAUhyqOlFGktCf2yHfaagLFU1e5In8W1NIeWutYZ8e5bixMrLb\nfehHatii4GX1zlYXoBKQuvBLLQEaqWnSp+fHrDSbbaKQwYYmSrIvvftvaGtVu8Vj\nOMF3YGMtrQycPKqYskOj1EbcDdw2HzIuaLp8ZSFBl5aQZxTWpC/9IT9//CJ7KjVY\n9Ubxkw7Z7eA6Jn9uLo+YuE/UmQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJD\nTjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2\nMDcwOTE0WhcNMjcwNDE2MDcwOTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMF\nQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzDo+/hn7E7SIX1mlwh\nIhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tizVHa6\ndLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZO\nV/kbZKKTVrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrC\nGHn2emU1z5DrvTOTn1OrczvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gN\nv7Sg2Ca+I19zN38m5pIEo3/PIKe38zrKy5nLAgMBAAGjczBxMBEGCWCGSAGG+EIB\nAQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscCwQ7vptU7ETAPBgNVHRMB\nAf8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991SlgrHAsEO\n76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnK\nOOK5Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvH\nugDnuL8BV8F3RTIMO/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7Hgvi\nyJA/qIYM/PmLXoXLT1tLYhFHxUV8BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fL\nbuXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2G8kS1sHNzYDzAgE8yGnLRUhj\n2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5mmxE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDEl\nMCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMh\nU2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIz\nMloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09N\nIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNlY3VyaXR5IENvbW11\nbmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSE\nRMqm4miO/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gO\nzXppFodEtZDkBp2uoQSXWHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5\nbmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4zZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDF\nMxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4bepJz11sS6/vmsJWXMY1\nVkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK9U2vP9eC\nOKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G\nCSqGSIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HW\ntWS3irO4G8za+6xmiEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZ\nq51ihPZRwSzJIxXYKLerJRO1RuGGAv8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDb\nEJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnWmHyojf6GPgcWkuF75x3sM3Z+\nQi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEWT1MKZPlO9L9O\nVL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV\nBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X\nDTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ\nBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4\nQCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny\ngQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw\nzBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q\n130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2\nJsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw\nDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw\nZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT\nAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj\nAQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG\n9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h\nbV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc\nfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu\nHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w\nt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw\nWyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDoTCCAomgAwIBAgIQKTZHquOKrIZKI1byyrdhrzANBgkqhkiG9w0BAQUFADBO\nMQswCQYDVQQGEwJ1czEYMBYGA1UEChMPVS5TLiBHb3Zlcm5tZW50MQ0wCwYDVQQL\nEwRGQkNBMRYwFAYDVQQDEw1Db21tb24gUG9saWN5MB4XDTA3MTAxNTE1NTgwMFoX\nDTI3MTAxNTE2MDgwMFowTjELMAkGA1UEBhMCdXMxGDAWBgNVBAoTD1UuUy4gR292\nZXJubWVudDENMAsGA1UECxMERkJDQTEWMBQGA1UEAxMNQ29tbW9uIFBvbGljeTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJeNvTMn5K1b+3i9L0dHbsd4\n6ZOcpN7JHP0vGzk4rEcXwH53KQA7Ax9oD81Npe53uCxiazH2+nIJfTApBnznfKM9\nhBiKHa4skqgf6F5PjY7rPxr4nApnnbBnTfAu0DDew5SwoM8uCjR/VAnTNr2kSVdS\nc+md/uRIeUYbW40y5KVIZPMiDZKdCBW/YDyD90ciJSKtKXG3d+8XyaK2lF7IMJCk\nFEhcVlcLQUwF1CpMP64Sm1kRdXAHImktLNMxzJJ+zM2kfpRHqpwJCPZLr1LoakCR\nxVW9QLHIbVeGlRfmH3O+Ry4+i0wXubklHKVSFzYIWcBCvgortFZRPBtVyYyQd+sC\nAwEAAaN7MHkwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFC9Yl9ipBZilVh/72at17wI8NjTHMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJ\nKwYBBAGCNxUCBBYEFHa3YJbdFFYprHWF03BjwbxHhhyLMA0GCSqGSIb3DQEBBQUA\nA4IBAQBgrvNIFkBypgiIybxHLCRLXaCRc+1leJDwZ5B6pb8KrbYq+Zln34PFdx80\nCTj5fp5B4Ehg/uKqXYeI6oj9XEWyyWrafaStsU+/HA2fHprA1RRzOCuKeEBuMPdi\n4c2Z/FFpZ2wR3bgQo2jeJqVW/TZsN5hs++58PGxrcD/3SDcJjwtCga1GRrgLgwb0\nGzigf0/NC++DiYeXHIowZ9z9VKEDfgHLhUyxCynDvux84T8PCVI8L6eaSP436REG\nWOE2QYrEtr+O3c5Ks7wawM36GpnScZv6z7zyxFSjiDV2zBssRm8MtNHDYXaSdBHq\nS4CNHIkRi+xb/xfJSPzn4AYR4oRe\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw\nNzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv\nb3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD\nVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F\nVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1\n7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X\nZ75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+\n/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs\n81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm\ndtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe\nOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu\nsDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4\npgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs\nslESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ\narMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD\nVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG\n9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl\ndxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx\n0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj\nTQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed\nY2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7\nQ4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI\nOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7\nvVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW\nt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn\nHL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx\nSK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL\nMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW\nZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln\nbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp\nU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y\naXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp\nU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg\nSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln\nbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5\nIC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm\nGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve\nfLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw\nAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ\naW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj\naHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW\nkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC\n4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga\nFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL\nMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj\nKSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2\nMDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0\neSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV\nBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw\nNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV\nBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH\nMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL\nSo17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal\ntJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO\nBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG\nCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT\nqQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz\nrD6ogRLQy7rQkgu2npaqBA+K\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL\nMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp\nIDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi\nBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw\nMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh\nd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig\nYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v\ndCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/\nBebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6\npapu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E\nBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K\nDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3\nKMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox\nXZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF8DCCA9igAwIBAgIPBuhGJy8fCo/RhFzjafbVMA0GCSqGSIb3DQEBBQUAMDgx\nCzAJBgNVBAYTAkVTMRQwEgYDVQQKDAtJWkVOUEUgUy5BLjETMBEGA1UEAwwKSXpl\nbnBlLmNvbTAeFw0wNzEyMTMxMzA4MjdaFw0zNzEyMTMwODI3MjVaMDgxCzAJBgNV\nBAYTAkVTMRQwEgYDVQQKDAtJWkVOUEUgUy5BLjETMBEGA1UEAwwKSXplbnBlLmNv\nbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMnTesoPHqynhugWZWqx\nwhtFMnGV2f4QW8yv56V5AY+Jw8ryVXH3d753lPNypCxE2J6SmxQ6oeckkAoKVo7F\n2CaU4dlI4S0+2gpy3aOZFdqBoof0e24md4lYrdbrDLJBenNubdt6eEHpCIgSfocu\nZhFjbFT7PJ1ywLwu/8K33Q124zrX97RovqL144FuwUZvXY3gTcZUVYkaMzEKsVe5\no4qYw+w7NMWVQWl+dcI8IMVhulFHoCCQk6GQS/NOfIVFVJrRBSZBsLVNHTO+xAPI\nJXzBcNs79AktVCdIrC/hxKw+yMuSTFM5NyPs0wH54AlETU1kwOENWocivK0bo/4m\ntRXzp/yEGensoYi0RGmEg/OJ0XQGqcwL1sLeJ4VQJsoXuMl6h1YsGgEebL4TrRCs\ntST1OJGh1kva8bvS3ke18byB9llrzxlT6Y0Vy0rLqW9E5RtBz+GGp8rQap+8TI0G\nM1qiheWQNaBiXBZO8OOi+gMatCxxs1gs3nsL2xoP694hHwZ3BgOwye+Z/MC5TwuG\nKP7Suerj2qXDR2kS4Nvw9hmL7Xtw1wLW7YcYKCwEJEx35EiKGsY7mtQPyvp10gFA\nWo15v4vPS8+qFsGV5K1Mij4XkdSxYuWC5YAEpAN+jb/af6IPl08M0w3719Hlcn4c\nyHf/W5oPt64FRuXxqBbsR6QXAgMBAAGjgfYwgfMwgbAGA1UdEQSBqDCBpYEPaW5m\nb0BpemVucGUuY29tpIGRMIGOMUcwRQYDVQQKDD5JWkVOUEUgUy5BLiAtIENJRiBB\nMDEzMzcyNjAtUk1lcmMuVml0b3JpYS1HYXN0ZWl6IFQxMDU1IEY2MiBTODFDMEEG\nA1UECQw6QXZkYSBkZWwgTWVkaXRlcnJhbmVvIEV0b3JiaWRlYSAxNCAtIDAxMDEw\nIFZpdG9yaWEtR2FzdGVpejAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQUHRxlDqjyJXu0kc/ksbHmvVV0bAUwDQYJKoZIhvcNAQEFBQAD\nggIBAMeBRm8hGE+gBe/n1bqXUKJg7aWSFBpSm/nxiEqg3Hh10dUflU7F57dp5iL0\n+CmoKom+z892j+Mxc50m0xwbRxYpB2iEitL7sRskPtKYGCwkjq/2e+pEFhsqxPqg\nl+nqbFik73WrAGLRne0TNtsiC7bw0fRue0aHwp28vb5CO7dz0JoqPLRbEhYArxk5\nja2DUBzIgU+9Ag89njWW7u/kwgN8KRwCfr00J16vU9adF79XbOnQgxCvv11N75B7\nXSus7Op9ACYXzAJcY9cZGKfsK8eKPlgOiofmg59OsjQerFQJTx0CCzl+gQgVuaBp\nE8gyK+OtbBPWg50jLbJtooiGfqgNASYJQNntKE6MkyQP2/EeTXp6WuKlWPHcj1+Z\nggwuz7LdmMySlD/5CbOlliVbN/UShUHiGUzGigjB3Bh6Dx4/glmimj4/+eAJn/3B\nkUtdyXvWton83x18hqrNA/ILUpLxYm9/h+qrdslsUMIZgq+qHfUgKGgu1fxkN0/P\npUTEvnK0jHS0bKf68r10OEMr3q/53NjgnZ/cPcqlY0S/kqJPTIAcuxrDmkoEVU3K\n7iYLHL8CxWTTnn7S05EcS6L1HOUXHA0MUqORH5zwIe0ClG+poEnK6EOMxPQ02nwi\no8ZmPrgbBYhdurz3vOXcFD2nhqi2WVIhA16L4wTtSyoeo09Q\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4\nMQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6\nZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD\nVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j\nb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq\nscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO\nxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H\nLmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX\nuaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD\nyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+\nJrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q\nrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN\nBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L\nhij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB\nQFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+\nHMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu\nZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg\nQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB\nBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx\nMCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA\nA4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb\nlaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56\nawmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo\nJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw\nLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT\nVyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk\nLhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb\nUjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/\nQnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+\nnaM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls\nQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFkjCCA3qgAwIBAgIBCDANBgkqhkiG9w0BAQUFADA6MQswCQYDVQQGEwJDTjER\nMA8GA1UEChMIVW5pVHJ1c3QxGDAWBgNVBAMTD1VDQSBHbG9iYWwgUm9vdDAeFw0w\nODAxMDEwMDAwMDBaFw0zNzEyMzEwMDAwMDBaMDoxCzAJBgNVBAYTAkNOMREwDwYD\nVQQKEwhVbmlUcnVzdDEYMBYGA1UEAxMPVUNBIEdsb2JhbCBSb290MIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2rPlBlA/9nP3xDK/RqUlYjOHsGj+p9+I\nA2N9Apb964fJ7uIIu527u+RBj8cwiQ9tJMAEbBSUgU2gDXRm8/CFr/hkGd656YGT\n0CiFmUdCSiw8OCdKzP/5bBnXtfPvm65bNAbXj6ITBpyKhELVs6OQaG2BkO5NhOxM\ncE4t3iQ5zhkAQ5N4+QiGHUPR9HK8BcBn+sBR0smFBySuOR56zUHSNqth6iur8CBV\nmTxtLRwuLnWW2HKX4AzKaXPudSsVCeCObbvaE/9GqOgADKwHLx25urnRoPeZnnRc\nGQVmMc8+KlL+b5/zub35wYH1N9ouTIElXfbZlJrTNYsgKDdfUet9Ysepk9H50DTL\nqScmLCiQkjtVY7cXDlRzq6987DqrcDOsIfsiJrOGrCOp139tywgg8q9A9f9ER3Hd\nJ90TKKHqdjn5EKCgTUCkJ7JZFStsLSS3JGN490MYeg9NEePorIdCjedYcaSrbqLA\nl3y74xNLytu7awj5abQEctXDRrl36v+6++nwOgw19o8PrgaEFt2UVdTvyie3AzzF\nHCYq9TyopZWbhvGKiWf4xwxmse1Bv4KmAGg6IjTuHuvlb4l0T2qqaqhXZ1LUIGHB\nzlPL/SR/XybfoQhplqCe/klD4tPq2sTxiDEhbhzhzfN1DiBEFsx9c3Q1RSw7gdQg\n7LYJjD5IskkCAwEAAaOBojCBnzALBgNVHQ8EBAMCAQYwDAYDVR0TBAUwAwEB/zBj\nBgNVHSUEXDBaBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcD\nBAYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEFBQcDBwYIKwYBBQUHAwgGCCsGAQUF\nBwMJMB0GA1UdDgQWBBTZw9P4gJJnzF3SOqLXcaK0xDiALTANBgkqhkiG9w0BAQUF\nAAOCAgEA0Ih5ygiq9ws0oE4Jwul+NUiJcIQjL1HDKy9e21NrW3UIKlS6Mg7VxnGF\nsZdJgPaE0PC6t3GUyHlrpsVE6EKirSUtVy/m1jEp+hmJVCl+t35HNmktbjK81HXa\nQnO4TuWDQHOyXd/URHOmYgvbqm4FjMh/Rk85hZCdvBtUKayl1/7lWFZXbSyZoUkh\n1WHGjGHhdSTBAd0tGzbDLxLMC9Z4i3WA6UG5iLHKPKkWxk4V43I29tSgQYWvimVw\nTbVEEFDs7d9t5tnGwBLxSzovc+k8qe4bqi81pZufTcU0hF8mFGmzI7GJchT46U1R\nIgP/SobEHOh7eQrbRyWBfvw0hKxZuFhD5D1DCVR0wtD92e9uWfdyYJl2b/Unp7uD\npEqB7CmB9HdL4UISVdSGKhK28FWbAS7d9qjjGcPORy/AeGEYWsdl/J1GW1fcfA67\nloMQfFUYCQSu0feLKj6g5lDWMDbX54s4U+xJRODPpN/xU3uLWrb2EZBL1nXz/gLz\nKa/wI3J9FO2pXd96gZ6bkiL8HvgBRUGXx2sBYb4zaPKgZYRmvOAqpGjTcezHCN6j\nw8k2SjTxF+KAryAhk5Qe5hXTVGLxtTgv48y5ZwSpuuXu+RBuyy5+E6+SFP7zJ3N7\nOPxzbbm5iPZujAv1/P8JDrMtXnt145Ik4ubhWD5LKAN1axibRww=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL\nMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\nBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT\nIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw\nMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy\nZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N\nT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv\nbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR\nFtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J\ncfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW\nBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/\nBAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm\nfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv\nGDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB\nrjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf\nQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw\nMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV\nBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa\nFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl\nLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u\nMTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl\nZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm\ngcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8\nYZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf\nb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9\n9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S\nzhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk\nOQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV\nHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA\n2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW\noCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu\nt8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c\nKUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM\nm7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu\nMdRAGmI0Nj81Aa6sY6A=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB\nvTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\nExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp\nU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W\nZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe\nFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX\nMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0\nIE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y\nIGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh\nbCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF\n9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH\nH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H\nLL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN\n/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT\nrJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud\nEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw\nWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs\nexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud\nDgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4\nsAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+\nseQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz\n4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+\nBxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR\nlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3\n7M2CYfE45k+XmCpajQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB\nmDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT\nMChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s\neTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv\ncml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ\nBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg\nMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0\nBgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz\n+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm\nhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn\n5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W\nJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL\nDmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC\nhuOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw\nHQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB\nAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB\nzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN\nkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD\nAWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH\nSJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G\nspki4cErx5z481+oghLrGREt\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE\nAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x\nCzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW\nMBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF\nRElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC\nAgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7\n09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7\nXBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P\nGrjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK\nt0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb\nX79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28\nMHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU\nfecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI\n2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH\nK9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae\nZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP\nBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ\nMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw\nRAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv\nbS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm\nfQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3\ngvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe\nI6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i\n5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi\nipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn\nMCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ\no5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6\nzqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN\nGHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt\nr0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK\nZ05phkOTOPu220+DkdRgfks+KzgHVZhepA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEfjCCA2agAwIBAgIBADANBgkqhkiG9w0BAQUFADCBzzELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\nHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOjA4BgNVBAsTMWh0dHA6Ly9j\nZXJ0aWZpY2F0ZXMuc3RhcmZpZWxkdGVjaC5jb20vcmVwb3NpdG9yeS8xNjA0BgNV\nBAMTLVN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0\neTAeFw0wODA2MDIwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIHPMQswCQYDVQQGEwJV\nUzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTElMCMGA1UE\nChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjE6MDgGA1UECxMxaHR0cDov\nL2NlcnRpZmljYXRlcy5zdGFyZmllbGR0ZWNoLmNvbS9yZXBvc2l0b3J5LzE2MDQG\nA1UEAxMtU3RhcmZpZWxkIFNlcnZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9y\naXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8sxWKk3mFjdal+pt\nNTjREJvbuNypBAmVMy4JxQB7GnhCj8j0BY7+0miDHk6ZzRfbRz5Q84nS59yY+wX4\nqtZj9FRNwXEDsB8bdrMaNDBz8SgyYIP9tJzXttIiN3wZqjveExBpblwG02+j8mZa\ndkJIr4DRVFk91LnU2+25qzmZ9O5iq+F4cnvYOI1AtszcEgBwQ4Vp2Bjjyldyn7Tf\nP/wiqEJS9XdbmfBWLSZwFjYSwieeV6Z80CPxedyjk1goOD2frTZD7jf7+PlDrchW\n8pQSXkLrc7gTDcum1Ya5qihqVAOhPw8p6wkA6D9eon8XPaEr+L7QdR2khOOrF2UG\nUgCvsQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd\nBgNVHQ4EFgQUtMZ/GkPMm3VdL8RL8ouYEOnxURAwHwYDVR0jBBgwFoAUtMZ/GkPM\nm3VdL8RL8ouYEOnxURAwDQYJKoZIhvcNAQEFBQADggEBAKyAu8QlBQtYpOR+KX6v\nvDvsLcBELvmR4NI7MieQLfaACVzCq2Uk2jgQRsRJ0v2aqyhId4jG6W/RR5HVNU8U\nCahbQAcdfHFWy4lC1L9hwCL3Lt+r83JDi0DolOuwJtrRE9Or0DYtLjqVs3cuFTkY\nDGm6qoDt8VNOM5toBOKgMC7X0V3UpmadhObnuzyJuzad/BepPVUrivubxEyE/9/S\nvmkbdLCo9uqwnLIpdIFMaDqaf3MlOfUT4GaRadRXS7furUXgLMOI076USYkf/3DV\nW205E7Ady5jmZ2MNY/b7w9dhcoOIP3B+U8meiVTWT399cbmu8WCLd2Ds+L/6aqOc\nASI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIKbzCCCFegAwIBAgIQAldiBmp1YIdPkAS/ocgoQTANBgkqhkiG9w0BAQUFADCB\ngzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1OMRQwEgYDVQQHEwtNaW5uZWFwb2xp\nczExMC8GA1UEChMoT3BlbiBBY2Nlc3MgVGVjaG5vbG9neSBJbnRlcm5hdGlvbmFs\nIEluYzEeMBwGA1UEAxMVT0FUSSBXZWJDQVJFUyBSb290IENBMB4XDTA4MDYwMzE5\nMjgzMVoXDTM4MDYwMzE5MzYwMFowgYMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJN\nTjEUMBIGA1UEBxMLTWlubmVhcG9saXMxMTAvBgNVBAoTKE9wZW4gQWNjZXNzIFRl\nY2hub2xvZ3kgSW50ZXJuYXRpb25hbCBJbmMxHjAcBgNVBAMTFU9BVEkgV2ViQ0FS\nRVMgUm9vdCBDQTCCAiAwDQYJKoZIhvcNAQEBBQADggINADCCAggCggIBAN54mUOu\nXmEeLdJ1ePU+LDZCisx8tt8Xd2FWp8zjOoAhgbJu0Ge1z6Whdr4oDRJWg6qWuySB\nO2v5wQOwi7QHBPmZ0D+0iv7A5RIqlb8VLwreFwFrVcq06LOyk+bjTLwHEXg9//sz\ndry4MryeFgPc0f1q3VTLJ+BL1DlpkPC6giIPZ3Ula8NiNveYkQTK/xJ0Xsuptndj\n8RvkRE6GNtpraC+QXaE1mFylUopwukNeXN8t8TL4rPP27ZLDYmO3VkjHYR4StyGr\nuN1rZJDQR3AAt2jOlr1PQuULm3pNWbkcpK7vZ7WUtkibP4sESeb8KeP28TmdWkog\nFOAbwVhDGW26nSJshsu6Gf9YoFZE8W9RW1gL93t3f/ss0Qi6FX506OpnNCm4W5O7\npjDphJGXsCoHqduptYia3JPZZeYbcMzNRY5WkdVbG/PfajXiyIY+reWNegsodA/A\nfBJoyP2UtohJrFZXAOsMP+VRo5zqNhH9StbyCiDRYBM4w2CsuGdxJeHdBHn2EL9E\nxfJt0DyV2r3ju40JnaMgdpS1DxGORjM6XpW3hsTj5MgD25yy2ET73j6wZqFADYJJ\nCRa7eAPmnWeRLOOA6yv3dC+BSPvKJEsEEasZUGYFIsjynOxaWyQyK4ntp6FxtlMO\nOfv0rt4Z8+XfAr2k9Ta35j8aCTKtHeMg2ACPAgEDo4IE3TCCBNkwCwYDVR0PBAQD\nAgFGMBMGCSsGAQQBgjcUAgQGHgQAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFOUNZHGdyVLpwJsqaTPAk3zzgfXfMHAGA1UdHwRpMGcwZaBjoGGGMWh0dHA6\nLy9jZXJ0cy5vYXRpY2VydHMuY29tL3JlcG9zaXRvcnkvT0FUSUNBMi5jcmyGLGh0\ndHA6Ly9jZXJ0cy5vYXRpLm5ldC9yZXBvc2l0b3J5L09BVElDQTIuY3JsMBAGCSsG\nAQQBgjcVAQQDAgEAMIIDdQYDVR0gBIIDbDCCA2gwggNkBggqhkiG/GYLATCCA1Yw\nggNSBggrBgEFBQcCAjCCA0QeggNAAEYAbwByACAAbQBvAHIAZQAgAGkAbgBmAG8A\ncgBtAGEAdABpAG8AbgAgAHIAZQBnAGEAcgBkAGkAbgBnACAATwBBAFQASQAgAGMA\nZQByAHQAaQBmAGkAYwBhAHQAZQBzACAAYQBuAGQAIAB0AGgAZQAgAE8AQQBUAEkA\nIAB3AGUAYgBDAEEAUgBFAFMAIABTAHkAcwB0AGUAbQAsACAAcABsAGUAYQBzAGUA\nIABzAGUAZQAgAHQAaABlACAATwBBAFQASQAgAEMAZQByAHQAaQBmAGkAYwBhAHQA\naQBvAG4AIABQAHIAYQBjAHQAaQBjAGUAIABTAHQAYQB0AGUAbQBlAG4AdAAgACgA\nQwBQAFMAKQAgAGEAdAAgAHQAaABlACAAZgBvAGwAbABvAHcAaQBuAGcAIABsAG8A\nYwBhAHQAaQBvAG4AOgAgAGgAdAB0AHAAOgAvAC8AdwB3AHcALgBvAGEAdABpAGMA\nZQByAHQAcwAuAGMAbwBtAC8AcgBlAHAAbwBzAGkAdABvAHIAeQAuACAAIABJAGYA\nIAB5AG8AdQAgAGgAYQB2AGUAIABzAHAAZQBjAGkAZgBpAGMAIABxAHUAZQBzAHQA\naQBvAG4AcwAgAHQAaABhAHQAIABjAGEAbgBuAG8AdAAgAGIAZQAgAGEAbgBzAHcA\nZQByAGUAZAAgAGIAeQAgAHQAaABlACAATwBBAFQASQAgAEMAUABTACAAbwByACAA\ndwBvAHUAbABkACAAbABpAGsAZQAgAE8AQQBUAEkAIAB3AGUAYgBDAEEAUgBFAFMA\nIABwAHIAbwBkAHUAYwB0ACAAaQBuAGYAbwByAG0AYQB0AGkAbwBuACwAIABwAGwA\nZQBhAHMAZQAgAGUALQBtAGEAaQBsACAAeQBvAHUAcgAgAHIAZQBxAHUAZQBzAHQA\ncwAgAHQAbwAgAE8AQQBUAEkAIABhAHQAIAB0AGgAZQAgAGYAbwBsAGwAbwB3AGkA\nbgBnACAAYQBkAGQAcgBlAHMAcwA6ACAAQwB1AHMAdABvAG0AZQByAF8AUwBlAHIA\ndgBpAGMAZQBAAG8AYQB0AGkAYwBlAHIAdABzAC4AYwBvAG0ALjCBhwYIKwYBBQUH\nAQEEezB5MD0GCCsGAQUFBzAChjFodHRwOi8vY2VydHMub2F0aWNlcnRzLmNvbS9y\nZXBvc2l0b3J5L09BVElDQTIuY3J0MDgGCCsGAQUFBzAChixodHRwOi8vY2VydHMu\nb2F0aS5uZXQvcmVwb3NpdG9yeS9PQVRJQ0EyLmNydDANBgkqhkiG9w0BAQUFAAOC\nAgEAsFcVBnu/4QCC+58H4Fb0rIQ1nIF1aHhRUNpweD+7Ndc8dmlPRQFtHS2vQrAz\nbv+cCvup0fyp2o+lS0qHLSKksuD0Fw4EuOsOQnMH79S6j0IS0w4tu21UyQHJP03W\n7gxCVonaYjcLoUh9bMSxx6tEYsumPPRloH3f82BixYr4ifXbIYZTnefIME/bJXE5\nLYTxKXghVpnWX0hJuzO4yc884ysVakReOglgPsDSIBZ2vGbyWwMZP0q2np7dohpY\nPnPvt2l7e5AHOZpnM7tWkrr+rp1iS1VhLpYfxlSVLWW+SRgR9/f9tsYGoTIPdW8W\n4SRiyA5vOvKVgPGp+6B9TdWiQx+FYNZceSvMNM+hd+/m085zhbTYZ4mZvG/LDgcn\nLnVRiX/BO98NA7+IF+a8+pQMqBmww9GqgKgZ2bZE0pUrVyJbyC2uDtAIraJ7NADg\nlv+SyjnNwMPSzLn0N8NWpNemGoAebDNyzVb7X+Xd3DBb7rhMs99asJEk4o0cMQ8p\nswcghdZ2yj66d4v49VCFDU82cWtVEglAOwMVOP7ll3hLKB24gLuOsvrgsh3CeIkp\ns44M7ABfTke1ncvcTcLIdcg+UEbYfN+GyvVxKpQKbVdveOry1+XjV1R3W2KX1+yR\nzkJz3pBKv4IcldkZSND8mycZ+4nz5hATRNkCu8VfY29lmzE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGGjCCBAKgAwIBAgIQAMoieQgjKvD1griF02Pd8TANBgkqhkiG9w0BAQUFADB/\nMQswCQYDVQQGEwJVWTErMCkGA1UECgwiQURNSU5JU1RSQUNJT04gTkFDSU9OQUwg\nREUgQ09SUkVPUzEfMB0GA1UECwwWU0VSVklDSU9TIEVMRUNUUk9OSUNPUzEiMCAG\nA1UEAwwZQ29ycmVvIFVydWd1YXlvIC0gUm9vdCBDQTAeFw0wODA3MTQxNjUyMTVa\nFw0zMDEyMzEwMjU5NTlaMH8xCzAJBgNVBAYTAlVZMSswKQYDVQQKDCJBRE1JTklT\nVFJBQ0lPTiBOQUNJT05BTCBERSBDT1JSRU9TMR8wHQYDVQQLDBZTRVJWSUNJT1Mg\nRUxFQ1RST05JQ09TMSIwIAYDVQQDDBlDb3JyZW8gVXJ1Z3VheW8gLSBSb290IENB\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsT3SpyVwl4N4DtcyyWYO\neCPkKhvsw+9ThYO7ys9+6lOZbSdVyNE4IUBuSU3DPfYJKwYZQ6mYyQFO9KqAMAdV\n8/W3fZm3c4XVHGVWbA0ymwgONGEqQAmEN8Nm7Q1MnAx4QDrs7avMpITydTGVQKiq\nu5O1d5hs8sjgIVoj5EKnk8ioHTjOpBpAQL88k5CbX9aUwSJbRtfFABXVj8b33guv\nbosFj1uAlQ6jvZPMkPJ940h+ss0HPRvtFJB08900H3zkA1nxLc3go6A7IS5crqwI\nBlAVMTXuX/kfDTSlgG5ick/jIbo4QF1f22gqXDTGCDv2fC6ojcS3pq3Zm78ZQQ5I\nOQlmbg00AcW7BxEjpNr+YJYoR9yPZ5sTr315DnjNwIwvuyEs/HQWHt7AMp36eDqG\nuj7JeAoA0eTgyRLiW9zru4CaMjWr8DDDDkiEL40ICvYsjE0ygEVVCNvNDai/CHq4\n52hdmpSJlbz8mo64fzrYbNX0GKxp4qTBC7Mfo4Kf84o8hUA4CfrCBT7hnIn6wwVs\nCI9dUfR/u8TzbAG9PU/EGYs52crM6XmIBFWrbbjaFkVlORUFGPsLLHMB7ZRS5X0M\nATsJoE3xPQiBZjQ2F0TwZ/Nb8gW2IZhY2fShN9lv5u9WxPu/VmICrDAwtgLW0hb8\nTuqHQ5poXYijkUYoK785FRUCAwEAAaOBkTCBjjAPBgNVHRMBAf8EBTADAQH/MA4G\nA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUfbtp64hh4UDPRyNkAIaiZmvchJUwTAYD\nVR0gBEUwQzBBBgRVHSAAMDkwNwYIKwYBBQUHAgEWK2h0dHA6Ly93d3cuY29ycmVv\nLmNvbS51eS9jb3JyZW9jZXJ0L2Nwcy5wZGYwDQYJKoZIhvcNAQEFBQADggIBAFbf\nE4m+YrcOgSFzpNQ3yu23L5V014n4S0eB7mftuCnfIaD8VGdnyFcsW6EKdXghIcqg\nqN9rnNk2Ao24AcFvjntsyaSyxUapykwCgfqje509SObKQGbSRJ124FW5ppyn0UPY\n9aC0nfj35aamQvMCMllGcisU7F5l1VGBeM6qL42WiXlq+w/IW8+0rpC2X+N8Ymy3\npv+QgbWYkXMSMK/H6IECaHMpu1h1PbfWQ9WuTfJCufDf2jEAE9rhs7YGi1v9yZi4\nohPRuo/BihqeD/+CvgSC5SuTPh61ogwbxhqwc4l2g7yOO7sXbRTDi759FSa1qZwX\nelB6LevpmZSumBC97ipdXdaONFusHodga5jHh4/TnLJoBUkH+akxZpz+v6dZ6Czw\nNtTyqBmCwJ6nOfmxmDSjH/rNyRkteN63/WLwk6P+AFvWCuTzfnyXKOEF7AU0RRP/\nKRNhiidP27jSkiEntYh3Z6h+zyQ8hwgEM3OPC7aG+M/vsqYkHguRkQBQFjIS2Akl\n2mNO3dst1+cEa+NjH6n+qQFjxMpMFGiDvAWsWRb7bqEHb7tLvm2YSHYle0oRllQI\nrKnzN6uDw9HNgZjA5UA1uJ+R52/mSyAWilN7rDrRmDVU0NS/rn6aSx7pdaMlsDvn\nZb9PlfQdvcS6yU2BUcI/WtkS9CEb1pXqPZD+qZPi\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD\nVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0\nIHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3\nMRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz\nIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz\nMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj\ndXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw\nEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp\nMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G\nCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9\n28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq\nVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q\nDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR\n5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL\nZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a\nSd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl\nUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s\n+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5\nWk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj\nya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx\nhduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV\nHQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1\n+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN\nYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t\nL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy\nZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt\nIDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV\nHSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w\nDQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW\nPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF\n5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1\nglanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH\nFoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2\npSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD\nxvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG\ntjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq\njktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De\nfhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg\nOGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ\nd0jQ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD\nVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0\nIHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3\nMRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD\naGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx\nMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy\ncmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG\nA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl\nBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI\nhvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed\nKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7\nG706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2\nzxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4\nddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG\nHoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2\nId3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V\nyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e\nbeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r\n6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh\nwZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog\nzCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW\nBBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr\nru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp\nZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk\ncmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt\nYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC\nCQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow\nKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI\nhvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ\nUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz\nX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x\nfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz\na2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd\nYhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd\nSqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O\nAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso\nM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge\nv8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z\n09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES\nMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU\nV0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz\nWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO\nLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\nAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE\nAcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH\nK3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX\nRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z\nrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx\n3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq\nhkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC\nMErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls\nXebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D\nlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn\naspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ\nYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFezCCA2OgAwIBAgIBATANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJUVzES\nMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU\nV0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDc0NzEz\nWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO\nLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC\nAQDLAQxkgbtQSJnzHNkgsIukhxL3rk/OXnPLqIYV4hz5mWMG2hzWCrz+lLreYIFo\nnLLyS3cB2rcbxvn1YJAgePwxSj41d8AaHoxZVXhVcEGSCZsCSsNvrAuY6bJSfZp/\nv10fA4dC/cMc6mMwZdG1jpxpWmvuM42vuvpjJFCJLckFHmzrjl6OAUihgMM4RStO\nH/QiwvguSnZ+6s6c0RiHV/a2+u3MkFWOgMo1udpkZRbM6WRRWT77is6AsKWSRWP9\nm5YAvFxeho7FSd8UqMmRm3j3HIwmhmia+YHDgXs9M9sQXj0EadZm4K453Ini5ib7\nUX97qAlrhyY4zdmNLZ49yrHzK5v9Ru2B28+FIb7ARcnlid12l7+0gUQpO7eYFzTy\nuKqasHtBVSbBPLQkl5atG482cbcr87aDAjD6sgoTvEu2D/mjnWNuIlDTKNxfNgc8\nKaxFaOoiQF0/CccKMo/KtOXo19fKi2T/b2Ul7A10qLUcGibmKLJyzs36xCRKNxLi\n2LcJzqwuJz3CFOvqMIw3ynMZhYmzu/s4Qx15paWLGSSgphJSGv7RV8GdEnudldZs\ne0odwa4VAk0sY6B1Jz/+8gAgMkrlsawuE+BIpvROkVQM2XRYPhF17fqcwqq7SH/L\n9l9cJrAJh3rE/Zx+rzNnQlcWU/7xPUNAUoq2NXFP/AE85QIDAQABo0IwQDAOBgNV\nHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUyERa/n/9qZuG\nNb7ipfYZ+16/b1kwDQYJKoZIhvcNAQELBQADggIBABKL3dUInShvCPGJVm1AB6v7\n4QioBKS+132FrlpYtmdMAWnNrIs2jlz9OzYB+dWheyh6TISTzcQLGfASBIlNj+2g\nmSWFuyyVmWww7W0Eb+Yr+4kQu0yTDL6T1dYJhJMAcM/OxXga7VVU3y8OWVj4NYWu\nT/gh0pLeIbqZ0ZfxYJ6sREOnYK+J4lHSOYkQlBXPBNIhgu/K7WkVTxmrvJNqmQHg\nAtEcOp0xyvX7xPZNmrsgjD9vcTJ/J0tEf4FR6ZMbYg6kh1893VRAuSSXYleVjscQ\nkaeYxVhCUKmWHOVKtray3E6R8oDSkehQCNMWQIkPaBOuw1xXFxMX7TMdsuqR9qou\ncURAibLgZr2xvRc9TT0PVzklZQRqKoPbOLXOv+QA100oN5CoqabksVXwys/jDGov\nR5a31OTvyDcDPH/rgZKKKmmYHQE7SIota6/lz7K03dZneoABCwRXJbZlQg9J4SPK\nQKrCLPyFr2UYqgcx2y2u68Nx5mjfN5f5mj/xJV7w31fp/BLgOQaHdN1jk2uBxPPg\n2wkU2L+/QG9xgSZo96WFF0BSA75ckxTlQVIVd7w1oUdzKgyXXIzeMTxjjPCbX1RP\n0uJbbDwcw+c0ZnOmQaMgMkR7zeq8aZf9Q3AxvDKComWYo0Avakb0AFAuVeDbek7g\nbcho4VTolYMYvvrNjx1m\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIICTCCBfGgAwIBAgIQZ9a23dRLHqpL7JR4cjWXZjANBgkqhkiG9w0BAQUFADCB\nsTELMAkGA1UEBhMCQ0wxHTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREw\nDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UEChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsT\nF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMR8wHQYJKoZIhvcNAQkBFhBzY2xpZW50\nZXNAY2NzLmNsMRcwFQYDVQQDEw5FLUNFUlQgUk9PVCBDQTAeFw0wODA5MDUxOTM0\nMDhaFw0yODA5MDUxOTM5NDFaMIGxMQswCQYDVQQGEwJDTDEdMBsGA1UECBMUUmVn\naW9uIE1ldHJvcG9saXRhbmExETAPBgNVBAcTCFNhbnRpYWdvMRQwEgYDVQQKEwtF\nLUNFUlRDSElMRTEgMB4GA1UECxMXQXV0b3JpZGFkIENlcnRpZmljYWRvcmExHzAd\nBgkqhkiG9w0BCQEWEHNjbGllbnRlc0BjY3MuY2wxFzAVBgNVBAMTDkUtQ0VSVCBS\nT09UIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApICHJduVUuwY\nPz4hL5SBnacjGsNqTkZemtgnf/Sk8RiwvZRM1Ir7RIhFxF4nmX5jaoQSVHHXWTu4\nqBJOsact4OJ8RxTNtXC0YpTHYqP7bZU6ZlzqfsqM/uofY/XJW+tAnYTYM6jdgax+\n++i4mbuSeWT/Cm+DN43eK60PDbvnw2H9sEqzGuAQ5h0VDDddi9+8PSvy7YoBpEBL\n/UJoGRRjNDVbxURrzFCYnJ7ta93rB0M7HWwLBH/O4rxiutM+y0sUImw7tFb0uifb\nIUUPvhaVklS18fkXp+fth8Gd1pK3rGGNF3ZQdh5RM85vfiQrjhRvzXzErWNwntLO\n7jxXxhqUQAVdY44l15Fj4sZiWh/Q7PfU3w9NVs1axY0nqnaDjmYaHJXj+YKWnqIT\nVE3W1lIO7EOGQf9URp6UNaRgELYEy5jZ9nx967OhA+nYuULfb2AmbkfwiRPlgSwH\n+/1W2PvYae4+h6n5jPLyldF8yIqdw8TBNNMj5Qs4rHIW2sP05nKblJstP1O3XbHc\nZBAjiTnGx46Fe398NJiFNvG6x4mHIuLyPqngHecrMGOsdFX34DLJ4y5uiRW+LFHY\nEAp4qc86DTfpgpIKlYfnAzxOe9vo5nIICNn/nTvZo/fy8hUQnTlO9y5zJcvtv1Mw\n1X//GfcXXVQqNkhWzzXrdKzUosa6OFECAwEAAaOCAhkwggIVMA4GA1UdDwEB/wQE\nAwIBBjATBgkrBgEEAYI3FAIEBh4EAEMAQTAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBRQ3YLRiJItjzzpul3HYmbib3urIDCCARMGA1UdHwSCAQowggEGMIIBAqCB\n/6CB/IaBumxkYXA6Ly8vQ049RS1DRVJUJTIwUk9PVCUyMENBLENOPXBraS1yb290\nLENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxD\nTj1Db25maWd1cmF0aW9uLERDPWVjZXJ0cGtpLERDPWNsP2NlcnRpZmljYXRlUmV2\nb2NhdGlvbkxpc3Q/YmFzZT9vYmplY3RDbGFzcz1jUkxEaXN0cmlidXRpb25Qb2lu\ndIY9aHR0cDovL3BraS1yb290LmVjZXJ0cGtpLmNsL0NlcnRFbnJvbGwvRS1DRVJU\nJTIwUk9PVCUyMENBLmNybDAQBgkrBgEEAYI3FQEEAwIBADBiBgNVHSAEWzBZMFcG\nCCsGAQQBw1IFMEswSQYIKwYBBQUHAgEWPWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu\nY2wvaHRtbC9wcm9kdWN0b3MvZG93bmxvYWQvQ1BTdjEuNy5wZGYwMQYDVR0lBCow\nKAYIKwYBBQUHAwEGCCsGAQUFBwMCBggrBgEFBQcDBAYIKwYBBQUHAwgwDQYJKoZI\nhvcNAQEFBQADggIBAHWDlT8h7XR5Lt7bHnbyIcLblmC+m7DFTgtXLkkoorbDpi5Q\nBIsATKmzcoWk5nRvQm/Sm9Q/+NToMWtXPoHLsYbOsH7Z2OPt7uAE13RwrVUXMbG8\nFAVXmVKi53vD7ttKs7P67KdDtMITVl8KMtENWFXgMD9kBwSqz28LKa/tiR0cwOaV\nAxmBt7Fw7OF6fTg6U8Xc0rnqginxTa5d2ejFnahGJGkLaikpGqnNL9zi6YMZ4nNp\n6+ry1tdoFhleymLrSguPBFb17pVdEhHjDVCgzfTrYX6oIKLEUjcx56aFMGPftkop\njliIz7V1WGhcm9I/EoasppONh5P3MRipV9LON6UXRV9nPoLb5TpvCpiSBr9gXTqE\npEUSQ1BAHFaUJf2nUgpsLcTooM1xQuQ1C6hRiaT5hwj4HQj6rg6dAr0tf1luHQGC\no4lwY7RhrmMkNXQgTcDXGHLprCyvmVGDbqN7F9j8chXzxxHES0G0csRw/1hRIaHh\nK2Nl5XSQom3C/5F9rU8HNyUYqp+cLRwodk7fgq7OQm4Gkjy7h3Fxoe40H+wNDhf1\np/ha+mbkfefR6IIxZUe7cp9UleRHmiBM+vgaRcloy0SYbuyZLRBdy9js+wpUSaWP\nMNPfhhPQVD/uJLOhJ/wg73jjkbnYlvJEum5fiQnKeidO+/mMrMEXt1iJ/3Y+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjET\nMBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAk\nBgNVBAMMHUNlcnRpbm9taXMgLSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4\nMjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNl\ncnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYwJAYDVQQDDB1DZXJ0\naW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQADggIP\nADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jY\nF1AMnmHawE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N\n8y4oH3DfVS9O7cdxbwlyLu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWe\nrP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K\n/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92NjMD2AR5vpTESOH2VwnHu\n7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9qc1pkIuVC\n28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6\nlSTClrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1E\nnn1So2+WLhl+HPNbxxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB\n0iSVL1N6aaLwD4ZFjliCK0wi1F6g530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql09\n5gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna4NH4+ej9Uji29YnfAgMBAAGj\nWzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQN\njLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ\nKoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9s\nov3/4gbIOZ/xWqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZM\nOH8oMDX/nyNTt7buFHAAQCvaR6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q\n619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40nJ+U8/aGH88bc62UeYdocMMzpXDn\n2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1BCxMjidPJC+iKunqj\no3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjvJL1v\nnxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG\n5ERQL1TEqkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWq\npdEdnV1j6CTmNhTih60bWfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZb\ndsLLO7XSAPCjDuGtbkD326C00EauFddEwk01+dIL8hf2rGbVJLJP0RyZwG71fet0\nBLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/vgt2Fl43N+bYdJeimUV5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx\nKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd\nBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl\nYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1\nOTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy\naXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50\nZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN\n8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/\nRLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4\nhqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5\nZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM\nEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1\nA/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy\nWL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ\n1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30\n6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT\n91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml\ne9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p\nTpPDpFQUWw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx\nKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd\nBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl\nYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1\nOTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy\naXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50\nZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd\nAqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC\nFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi\n1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq\njnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ\nwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/\nWSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy\nNsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC\nuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw\nIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6\ng1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN\n9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP\nBSeOE6Fuwg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHHzCCBgegAwIBAgIESPx+9TANBgkqhkiG9w0BAQUFADCBrjESMBAGCgmSJomT\n8ixkARkWAnJzMRUwEwYKCZImiZPyLGQBGRYFcG9zdGExEjAQBgoJkiaJk/IsZAEZ\nFgJjYTEWMBQGA1UEAxMNQ29uZmlndXJhdGlvbjERMA8GA1UEAxMIU2VydmljZXMx\nHDAaBgNVBAMTE1B1YmxpYyBLZXkgU2VydmljZXMxDDAKBgNVBAMTA0FJQTEWMBQG\nA1UEAxMNUG9zdGEgQ0EgUm9vdDAeFw0wODEwMjAxMjIyMDhaFw0yODEwMjAxMjUy\nMDhaMIGuMRIwEAYKCZImiZPyLGQBGRYCcnMxFTATBgoJkiaJk/IsZAEZFgVwb3N0\nYTESMBAGCgmSJomT8ixkARkWAmNhMRYwFAYDVQQDEw1Db25maWd1cmF0aW9uMREw\nDwYDVQQDEwhTZXJ2aWNlczEcMBoGA1UEAxMTUHVibGljIEtleSBTZXJ2aWNlczEM\nMAoGA1UEAxMDQUlBMRYwFAYDVQQDEw1Qb3N0YSBDQSBSb290MIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqPK9iL7Ar0S+m0qiYxzWVqsdKbIcqhUeRdGs\nnaBh1TX55FqDNmND3jhXFfzwlGL0B4BXg1eosxW8+00jeF/a9seBFr6r3+fcg1Nz\nK7bdY4iNRfMN3X2/6IiwZsFDXTfSbaGcmkbDsz/QwqCKlC6DpjzDYL0szB6LY4J2\nQSjkFWtcDGE5VThByshm6Me4l1IQJnC3B7cJHqYTXq6ZWiZvZD3sxNOluVx2ZK1j\nfYiD4kvMDd7UxtMIQvVbF/Vx4ZEtA5+eHNyLcqToR2QQh2Qwc9jytPFXJpNXy7bH\nDYiLHc8FMF0E1nY36CAyV78PnDPGCIz2tMKpBrBbMKEeLRK6PwIDAQABo4IDQTCC\nAz0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgboGA1UdIASBsjCB\nrzCBrAYLKwYBBAH6OAoKAQEwgZwwMAYIKwYBBQUHAgEWJGh0dHA6Ly93d3cuY2Eu\ncG9zdGEucnMvZG9rdW1lbnRhY2lqYTBoBggrBgEFBQcCAjBcGlpPdm8gamUgZWxl\na3Ryb25za2kgc2VydGlmaWthdCBST09UIENBIHNlcnZlcmEgU2VydGlmaWthY2lv\nbm9nIHRlbGEgUG9zdGU6ICJQb3N0YSBDQSBSb290Ii4wEQYJYIZIAYb4QgEBBAQD\nAgAHMIIBvAYDVR0fBIIBszCCAa8wgcmggcaggcOkgcAwgb0xEjAQBgoJkiaJk/Is\nZAEZFgJyczEVMBMGCgmSJomT8ixkARkWBXBvc3RhMRIwEAYKCZImiZPyLGQBGRYC\nY2ExFjAUBgNVBAMTDUNvbmZpZ3VyYXRpb24xETAPBgNVBAMTCFNlcnZpY2VzMRww\nGgYDVQQDExNQdWJsaWMgS2V5IFNlcnZpY2VzMQwwCgYDVQQDEwNBSUExFjAUBgNV\nBAMTDVBvc3RhIENBIFJvb3QxDTALBgNVBAMTBENSTDEwgeCggd2ggdqGgaNsZGFw\nOi8vbGRhcC5jYS5wb3N0YS5ycy9jbj1Qb3N0YSUyMENBJTIwUm9vdCxjbj1BSUEs\nY249UHVibGljJTIwS2V5JTIwU2VydmljZXMsY249U2VydmljZXMsY249Q29uZmln\ndXJhdGlvbixkYz1jYSxkYz1wb3N0YSxkYz1ycz9jZXJ0aWZpY2F0ZVJldm9jYXRp\nb25MaXN0JTNCYmluYXJ5hjJodHRwOi8vc2VydGlmaWthdGkuY2EucG9zdGEucnMv\nY3JsL1Bvc3RhQ0FSb290LmNybDArBgNVHRAEJDAigA8yMDA4MTAyMDEyMjIwOFqB\nDzIwMjgxMDIwMTI1MjA4WjAfBgNVHSMEGDAWgBTyy43iNe8QQ8Tae8r664kDoSKv\nuDAdBgNVHQ4EFgQU8suN4jXvEEPE2nvK+uuJA6Eir7gwHQYJKoZIhvZ9B0EABBAw\nDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4IBAQBwRqHI5BcFZg+d4kMx\nSB2SkBnEhQGFFm74ks57rlIWxJeNCih91cts49XlDjJPyGgtNAg9c6iTQikzRgxE\nZ/HQmpxpAeWR8Q3JaTwzS04Zk2MzBSkhodj/PlSrnvahegLX3P+lPlR4+dPByhKV\n+YmeFOLyoUSyy+ktdTXMllW7OAuIJtrWrO/TUqILSzpT2ksiU8zKKiSaYqrEMpp+\n3MzBsmzNj9m0wM/1AsCMK4RbG0C8ENBQ4WHWZlaaBJGl49W9oC4igbHZONrkqIdf\nPEYElt7Jmju/rXhsHUlJtGm5cA8Fkla2/a+u+CAtRyPPthzNxJuATvm/McBUvrsx\nf/M+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM\nMSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D\nZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU\ncnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3\nWjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg\nUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw\nIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH\nUV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM\nTXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU\nBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM\nkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x\nAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV\nHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y\nsHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL\nI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8\nJ9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY\nVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI\n03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFhDCCA2ygAwIBAgIQAIG73WskH9q0vo8b2ghVxDANBgkqhkiG9w0BAQUFADA7\nMQswCQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xGTAXBgNVBAsMEEFDIFJB\nSVogRk5NVC1SQ00wHhcNMDgxMDI5MTU1OTU1WhcNMzAwMTAxMDAwMDAwWjA7MQsw\nCQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xGTAXBgNVBAsMEEFDIFJBSVog\nRk5NVC1SQ00wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6cYB6TIZu\nf8gTbcDGfRwAl48sDCO7EJpAqRq3h4j4m1Zq++Z7jouSjqclXVkR2zYut1EXH6kI\nHwQXJFiqN0oY3+U51Ff918EskQGR4iLUA8BY/HdH7I8+dEO6rDSNTTh2Z46wyG8w\nM1hxXLT1a27UAVC4E35sSqNJ0SAZ7rzAKRhlp97+790KkCHnGmeSQhCYX08wvD4c\nRbQQ12hAFMBA+ud3F3rmC49lWzzZmlLbtb2eRs8965EFAsCWsnZMTRCWO5L6nH8P\nmd++IzVFHgJc/rWom5kl2l7zIsM59eQqLtPGH8RsqsUcagEFSi/SxcGoNCZdZqXS\nAiH5GLcG9U6Zb6irTFHoz1AYxXfIOQksSZIymai7Fxd5sFrF5qPEWWVHNYNeqeg1\nC5m75M0gxptKBjm1aPwiuu5VjCtO6vOx4/y2mZrVQvpxTQjPhx5qcX3507TppXGB\ne8JOR5al9naFoyiP6YBugVOlbV+4SPnC+TamLkn/uJbCjAezm4hY/OsbHN4tcOKX\nkjChieO8Vagn1kvtkK2L+mMlWS2oNd3KlzO85c3HndHs714OSpAGJmOtudk1LQe6\ndmUsrFePffQHlNeBApZdowdJ1XrQV/kb51NGdaqweULLaHEI6WC9OWnO9K/DVkDH\nrVKiCeRvhkeKH+soJ12DIK8EyWxWmotG9QIDAQABo4GDMIGAMA8GA1UdEwEB/wQF\nMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBT3fcX9xOiaG3dkp/UdoMy/\nh2CabTA+BgNVHSAENzA1MDMGBFUdIAAwKzApBggrBgEFBQcCARYdaHR0cDovL3d3\ndy5jZXJ0LmZubXQuZXMvZHBjcy8wDQYJKoZIhvcNAQEFBQADggIBAHa5Jte8YHw7\nw8eEUZJZebaNv1PgvIigtdlM6a31Zk7voGDIC7iR7TOOgvGGlf7G0xqJq0872TMf\n0AvHsfVPpEu7AwwjXGyw3qxy+mneABDN8dbPNlK+f/wmQfPy/DDiMcbbED6pdLpP\n7O0gmcmw4qKjqUKZM8t/96oC6SSWKvjkzl1BoAYJVVra3xpP6zn8X+CpqUTXGOqV\nsUR72uo4CXQeZyg/4Is5LFP6DOA59ysaDjEB1GZ5iHSdSEiOtJNh5r8pCe++Bqka\nbAhwBAq/bgl2pGRDzh9XnZeebPh0FxxRA/pgU9RWRpbQUJ/GnTPzQ7Go16LJsMmD\nsX3H3KyBdteJ7UMm1v+iXKItoCRHqkaaaTEJwf0QebCF7HAg5j1BVKJKYi/W3kzD\nnI+9y6ZVlBzdvUHPKGWN0E3Xh9FM00NzIezXLhdnMoe20Bt0qmnH5GyH130Zmuw9\nRPGqgllyzUXb2mZC4ThsNl9U3SZWV6LZPqQK8u/8GYAf27qqgLzYUc1UatV/2G+1\n3Bb7QOJVVJDD3Ycz0f8epWKLNkSsqL/A1sSUd7O9xHUkaen/OZSr/FFnJOpAHuuJ\nLRMGfa4HocMM9dRask63IR0XxeW58h/jhgFdCwZ5XcnKPxZ+gR5NfvCaPCXFznR5\nnkrh8en1JUb2xN7kRGRzHcY5PnrmhXsY\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx\nCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ\nWiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ\nBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG\nTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/\nyBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf\nBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz\nWHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF\ntBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z\n374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC\nIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL\nmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7\nwk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS\nMKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2\nZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet\nUqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw\nAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H\nYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3\nLmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD\nnFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1\nRXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM\nLVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf\n77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N\nJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm\nfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp\n6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp\n1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B\n9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok\nRqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv\nuu8wd+RU4riEmViAqhOLUTpPSPaLtrM=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG\nEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3\nMDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl\ncnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR\ndGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB\npzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM\nb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm\naWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz\nIEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT\nlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz\nAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5\nVA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG\nILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2\nBJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG\nAQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M\nU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh\nbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C\n+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC\nbLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F\nuLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2\nXjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGITCCBAmgAwIBAgIGSUEt7AAQMA0GCSqGSIb3DQEBCwUAMIGtMQswCQYDVQQG\nEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3\nMDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl\ncnZpY2VzKTE7MDkGA1UEAwwyTmV0TG9jayBQbGF0aW5hIChDbGFzcyBQbGF0aW51\nbSkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUxMjQ0WhcNMjgxMjA2MTUx\nMjQ0WjCBrTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQK\nDAxOZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAo\nQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcykxOzA5BgNVBAMMMk5ldExvY2sgUGxhdGlu\nYSAoQ2xhc3MgUGxhdGludW0pIEbFkXRhbsO6c8OtdHbDoW55MIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEAzfLuxBp663QpTLa95NYKF2xl4mY9xNG8DLZa\n1itwXy3MIdFZEOSxE732zCKV1mxGTpEys+v1rMsEAU923VM+eJ/5Xry1ghNGyhDj\nHS1pK5QyEHMhq6k4xeNuE2TVY6ntCWbsim+JjRGG0PW/MpYLdXD1KFhCXqxptPX8\nkTkuopFA0TxUQYcjZFBIeWhLaJNLcuuAabNKHJC+skGjpc0XwNEaaX8CGEq1Yocm\nVy1sqCwhOfWXXpuapvjnTHnEeztW3Hr4tFjOdgquIlXrj8eEZHu9a8qVT9i+MRO/\njaEKK9V5t/V2rdpRXIFHYqiq/89T4DRxzw0lU6meY0evhZH4zxkR5U75z+3jNQUB\nIgPPmnzqHVFay/1zPTkLMevEO8qFKhEUAKAbgaIJiEjzfKJkoexntFiH8BTqqb6l\nIkFN7L2kDug9h/cvqs41hk8wV5KNNq541v0Y/NclHs96/Bn9oD9yFzYIQT+XNpUM\niZVxRfqE1tQgYLNFCvK3lT0L5aTDuBLykWzpbWCD9kURBbrmR4PZkeJu4btGa0gb\nvMb7z37eLLuQhO62JznnjaIxD9+BtyxsAOKx2CoXXBseR4lLF1EUQEBPxDkYMsKA\nYDblekdn9qgFVMFdlqAftohSDAK+jVV+FEvDogHunIpBXflflpEJjrTktcUE39Y2\nrVm0stcCAwDzkaNFMEMwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8EBAMC\nAQYwHQYDVR0OBBYEFOahE4P+l0vy2P8xoad0M4nOXn+kMA0GCSqGSIb3DQEBCwUA\nA4ICAQBphELA414TYZcgSfH0FoWln6QRCCXEY4aP8Euvsyn1B1caYscbRW6vXRa3\nwdBkgzuX9UO2RZDxZiqDJCr/iOl6C/nCW3qvY/cJeIZIWTRem2oQTvFulYk2SmjQ\nb5vgfk+3NQ/jebEFryd8qokKQ976DO/ZVy8occ1pa1JCyYowRVmhzPpZSo/31t1E\npbMuWxEY4rK15xFTOP6CTNNzvmWSGjqo0tKqvNS+bTZS/2vU0rUbN/MXQvEup9WQ\nbHSddPX6XyIb09x1qLX/8hrRvCsAXDzFuIYIVEminCP776aNcPRCUk0bIACB+KC4\n9HQjnL70uQ7sHmrYZUoVdfF3W27YseYPtJa4HfqGyJJui+l936IO1fHxfK5K42a/\nXfxb70iynmnHfZCgVbaUcIG5Cr2JdVPshKkDpd9RmQjQdAwC1nNyBnuLu12qTvxn\nZ9iOEAMZLTc61HepOhydwHl7bCl3Mk1KizCIwuc2zmijmpiG+YkVnr+qUX3xUEZU\nDwIuXJ/j3lczFf4YkmGo0ikFXWVEHpvj7/vcBd8Vq6bYC6Rzskw64J7Us2rlOg4K\n8E7PeIEfvqmYb7FHUX1CMzazpqkCUgV0fips1KqSVrA+OyNYsY01pxOPZx5xFaaz\ntQOGuCBmwEhvuazUSgNVsjffBN0iDFOGKkoqocE4PjzlPN91lw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G\nA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp\nZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4\nMTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG\nA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8\nRgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT\ngHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm\nKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd\nQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ\nXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o\nLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU\nRUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp\njjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK\n6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX\nmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs\nMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH\nWD9f\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr\nMCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG\nA1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0\nMDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp\nY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD\nQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz\ni1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8\nh9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV\nMdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9\nUK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni\n8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC\nh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD\nVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB\nAKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm\nKbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ\nX5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr\nQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5\npPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN\nQSdJQO7e5iNEOdyhIta6A/I=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF4DCCA8igAwIBAgIQLu/b+9iJPY9JHJNy/kXf7TANBgkqhkiG9w0BAQUFADBQ\nMQswCQYDVQQGEwJMVjEoMCYGA1UECxMfU2VydGlmaWthY2lqYXMgcGFrYWxwb2p1\nbXUgZGFsYTEXMBUGA1UEAxMORS1NRSBTU0kgKFJDQSkwHhcNMDkwNTE5MDg0NTU2\nWhcNMjcwNTE5MDg0ODE1WjBQMQswCQYDVQQGEwJMVjEoMCYGA1UECxMfU2VydGlm\naWthY2lqYXMgcGFrYWxwb2p1bXUgZGFsYTEXMBUGA1UEAxMORS1NRSBTU0kgKFJD\nQSkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDEBGsAw7DgLjvoUwUF\nCL7IhCdV1h2KEuIXIlps/7PdtpnDysHq+dgltd86nZ0/UsXp8qy/iXSKVK5Oz27y\nXq7avRIHmZXPZKv+mZFkWYzJvkRqMZuY6rrq0SEOKAs5m+PWiqb3Aro/PdlZ9HmZ\n3tMkm4twGyqE1uUJDyYmJFiPJV7zxZ10iaU2xeVSsuvohpNHbqcph6R+3LSjyzJW\n90WA2lzHL6Cn1+/1/LWozYSVYvipKyM7bdO3ksjqwbwUTehrnBZ60+wH+wclEE8U\nh3uSNs5WgmVLEyYG2KOjpt/Cevt7NQWiEz0+drwcV4MDUcc03lr1PL02JZwWD03O\n6A0ay11DohRvunxg1AKFdsVrKrhFsVx3RxGtoCWpZpGMURdtYVUKGT+bAv/E9dbS\ns+klU+EEPY8i0KJl5a6ntOAdkWrChpL3Ol0Tp3pMQt9as0qIRCzvR7qpr9bPYnOK\nBiIWLMLsHwao00dQWTIS5bmdYjWeyl4KtJ0jiMLTTywsyZPofrgJ7KbZ3WPhyahq\naNyEUaxaEuc7prUHCrGqTrO0olffN2wWTquZMnrwnCMli8qaqIzgOCG0zvdsYcji\nDBJZBoEmNloPNXPUFkX93pXe1ktcn3PZvhm957/kVWrIa0T3x7gziHkZDQZk6K8L\noXUMUmW6CiOVcfdj/H7ljI/M0QIDAQABo4G1MIGyMA4GA1UdDwEB/wQEAwIBBjAY\nBggrBgEFBQcBAwQMMAowCAYGBACORgEBMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFDsmA7rP41lGQlCtFJy/Azvv1j4xMBAGCSsGAQQBgjcVAQQDAgEAMEQGA1Ud\nIAQ9MDswOQYLKwYBBAGB+j0BAQEwKjAoBggrBgEFBQcCARYcaHR0cDovL3d3dy5l\nbWUubHYvcmVwb3NpdG9yeTANBgkqhkiG9w0BAQUFAAOCAgEAheamlOTZRl+dv5O7\n+Wt2ZCiuvzxFKoqTeWzTS4iGIGsiJjg9HBOq62GXbC4+V5xsQ6LebUDEMfJtukYW\nsy3Gu6bc5S+x2MHVkR4Rf/tfodwdYfhtm2Hw4j8rcdUNy97fZT+gb5WbesvbNTcp\nXV6duVSxrGAS5WPZza9SGwWWE3zaJHUBrdSepcvBEkVPV68jvym86o6tePiHI+hI\ny0Covl0z1uzGBkPCZyro44UuYJ5ELytPMbEHnZUh1SqSr4CR08cpvc3xFQyfAe74\nLTukB3BJeSTtvKHTllGCn8LIvN4jmsdQK5q2eFKqzpX2YDuimfkmZvRHLEElvEH6\n1ot/vV+CfNNFhbRM2OyzF+9EOvUoZe/1nnHMId7o1lEcEPtA/EnlXIQXr6oZXqLt\nTh6i+8pHHBxkPhSRojkZNIh/kcs7nRlw6ij7/FAPzL09XgIDa3k1REF27rYtdITh\ngnHTJbDTw5lEqz/iDKXuvab8pBEA7py9N9HWYsQwFC0QCpeKiPUlPJa+RkAaisCF\ndsSgSeBJpecZtQnzzE3tFl6a1NPIadDYijeFa07kqgeSXNRxcYFI03j1VmD+zALU\nAJMfTJJAl75yU3kuJlK+pqN0sZTZFGM6blvRPJInUpAyWpLSD05bCwY6YuXWJwwB\n9iUCuIsQKUKp92nK3OsKkksoMYY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE\nBhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h\ncHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy\nMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg\nQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9\nthDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM\ncas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG\nL9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i\nNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h\nX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b\nm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy\nZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja\nEbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T\nKI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF\n6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh\nOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD\nVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD\nVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp\ncm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv\nACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl\nAGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF\n661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9\nam58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1\nILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481\nPyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS\n3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k\nSeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF\n3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM\nZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g\nStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz\nQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB\njLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl\nMCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe\nU2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX\nDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy\ndXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj\nYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV\nOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr\nzbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM\nVAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ\nhNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO\nojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw\nawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs\nOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\nDQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF\ncoJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc\nokgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8\nt/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy\n1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/\nSjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD\nVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0\nZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G\nCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y\nOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx\nFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp\nZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o\ndTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP\nkd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc\ncbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U\nfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7\nN4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC\nxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1\n+rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\nA1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM\nPcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG\nSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h\nmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk\nddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775\ntyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c\n2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t\nHMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC\nVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50\ncnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs\nIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz\ndCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy\nNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu\ndHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt\ndGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0\naG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj\nYXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T\nRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN\ncCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW\nwcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1\nU1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0\njaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP\nBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN\nBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/\njTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ\nRkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v\n1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R\nnAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH\nVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDdTCCAl2gAwIBAgIBATANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJVUzEZ\nMBcGA1UECgwQVmVyaXpvbiBCdXNpbmVzczERMA8GA1UECwwIT21uaVJvb3QxHzAd\nBgNVBAMMFlZlcml6b24gR2xvYmFsIFJvb3QgQ0EwHhcNMDkwNzMwMTQyNzA0WhcN\nMzQwNzMwMTQyNzA0WjBcMQswCQYDVQQGEwJVUzEZMBcGA1UECgwQVmVyaXpvbiBC\ndXNpbmVzczERMA8GA1UECwwIT21uaVJvb3QxHzAdBgNVBAMMFlZlcml6b24gR2xv\nYmFsIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCKAAxw\nHb/rNIbDmUU1Hn9D96tvJC3NGcIQu7DKKVupIKurcizE4gI5bYK4xRHq+PuznmL4\nMx6wH8nj9jfbBMg7Y0824oWkJR3HaR8EvWhFE5YHH5RQ9T7FJ1SewElXRI4HY9Sm\nru0imcxNlmkEE252iZ90FpT5HVS9ornSgwEiDE1EgKr+NYknJaeGicbVGpLjj8WV\noBRymuhWxQJVHJf5IC7Q9TwTGVr24fkLA4Jpp4y31m+cVj6d6CoJYG1L5vuLmRT3\nNE9lWYCNuVfIojUh2IhxVl3uglctJpAYn5qcnI/v1MVjp1R9R5GHfRoSqBsYb6lv\nsSe65AR0zjcef2bFAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\nBAQDAgEGMB0GA1UdDgQWBBRMOBG4mABbWitwPqp45NVnZ2enfjANBgkqhkiG9w0B\nAQsFAAOCAQEAAV+gsQYB9HnXZRhgPs95oLrCI08j34eWX4EOOBUuXMgCaCkg/Ivu\npYoYgWRcmDV+OTCCpIKKULW6w+ha1qie4sMX29vE67AKIA3pnuP/YFRH8Tud1Cg8\noq6j+6qLgiIqNYeQuBxZR5DVnS76SeNlqDbrx+QcaNyzMWyrTs4kgBXIEFkQEXJN\nepyYnMT8YeCzsp1OoMbCWasY1qJVRewpqiU31k5KPQtAweST5PzNkQv45qvMs3bE\nYr8Z7Ya2ecMpVFS8mX1GV8+mz/RUKpoDZUcBoUIqyyVHbnxeAEuR2fkbEAZw+UIV\npl+q10Ae/clInZeB6lxowqDniaFTTb/H4w==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFfjCCA2agAwIBAgIJAKqIsFoLsXabMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV\nBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxJjAkBgNVBAMTHVN3aXNzU2ln\nbiBTaWx2ZXIgUm9vdCBDQSAtIEczMB4XDTA5MDgwNDEzMTkxNFoXDTM3MDgwNDEz\nMTkxNFowTDELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEmMCQG\nA1UEAxMdU3dpc3NTaWduIFNpbHZlciBSb290IENBIC0gRzMwggIiMA0GCSqGSIb3\nDQEBAQUAA4ICDwAwggIKAoICAQC+h5sF5nF8Um9t7Dep6bPczF9/01DqIZsE8D2/\nvo7JpRQWMhDPmfzscK1INmckDBcy1inlSjmxN+umeAxsbxnKTvdR2hro+iE4bJWc\nL9aLzDsCm78mmxFFtrg0Wh2mVEhSyJ14cc5ISsyneIPcaKtmHncH0zYYCNfUbWD4\n8HnTMzYJkmO3BJr1p5baRa90GvyC46hbDjo/UleYfrycjMHAslrfxH7+DKZUdoN+\nut3nKvRKNk+HZS6lujmNWWEp89OOJHCMU5sRpUcHsnUFXA2E2UTZzckmRFduAn2V\nAdSrJIbuPXD7V/qwKRTQnfLFl8sJyvHyPefYS5bpiC+eR1GKVGWYSNIS5FR3DAfm\nvluc8d0Dfo2E/L7JYtX8yTroibVfwgVSYfCcPuwuTYxykY7IQ8GiKF71gCTc4i+H\nO1MA5cvwsnyNeRmgiM14+MWKWnflBqzdSt7mcG6+r771sasOCLDboD+Uxb4Subx7\nJ3m1MildrsUgI5IDe1Q5sIkiVG0S48N46jpA/aSTrOktiDzbpkdmTN/YF+0W3hrW\n10Fmvx2A8aTgZBEpXgwnBWLr5cQEYtHEnwxqVdZYOJxmD537q1SAmZzsSdaCn9pF\n1j9TBgO3/R/shn104KS06DK2qgcj+O8kQZ5jMHj0VN2O8Fo4jhJ/eMdvAlYhM864\nuK1pVQIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd\nBgNVHQ4EFgQUoYxFkwoSYwunV18ySn3hIee3PmYwHwYDVR0jBBgwFoAUoYxFkwoS\nYwunV18ySn3hIee3PmYwDQYJKoZIhvcNAQELBQADggIBAIeuYW1IOCrGHNxKLoR4\nScAjKkW4NU3RBfq5BTPEZL3brVQWKrA+DVoo2qYagHMMxEFvr7g0tnfUW44dC4tG\nkES1s+5JGInBSzSzhzV0op5FZ+1FcWa2uaElc9fCrIj70h2na9rAWubYWWQ0l2Ug\nMTMDT86tCZ6u6cI+GHW0MyUSuwXsULpxQOK93ohGBSGEi6MrHuswMIm/EfVcRPiR\ni0tZRQswDcoMT29jvgT+we3gh/7IzVa/5dyOetTWKU6A26ubP45lByL3RM2WHy3H\n9Qm2mHD/ONxQFRGEO3+p8NgkVMgXjCsTSdaZf0XRD46/aXI3Uwf05q79Wz55uQbN\nuIF4tE2g0DW65K7/00m8Ne1jxrP846thWgW2C+T/qSq+31ROwktcaNqjMqLJTVcY\nUzRZPGaZ1zwCeKdMcdC/2/HEPOcB5gTyRPZIJjAzybEBGesC8cwh+joCMBedyF+A\nP90lrAKb4xfevcqSFNJSgVPm6vwwZzKpYvaTFxUHMV4PG2n19Km3fC2z7YREMkco\nBzuGaUWpxzaWkHJ02BKmcyPRTrm2ejrEKaFQBhG52fQmbmIIEiAW8AFXF9QFNmeX\n61H5/zMkDAUPVr/vPRxSjoreaQ9aH/DVAzFEs5LG6nWorrvHYAOImP/HBIRSkIbh\ntJOpUC/o69I2rDBgp9ADE7UK\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFejCCA2KgAwIBAgIJAN7E8kTzHab8MA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNV\nBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxJDAiBgNVBAMTG1N3aXNzU2ln\nbiBHb2xkIFJvb3QgQ0EgLSBHMzAeFw0wOTA4MDQxMzMxNDdaFw0zNzA4MDQxMzMx\nNDdaMEoxCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxJDAiBgNV\nBAMTG1N3aXNzU2lnbiBHb2xkIFJvb3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEB\nBQADggIPADCCAgoCggIBAMPon8hlWp1nG8FFl7S0h0NbYWCAnvJ/XvlnRN1E+qu1\nq3f/KhlMzm/Ej0Gf4OLNcuDR1FJhQQkKvwpw++CDaWEpytsimlul5t0XlbBvhI46\nPmRaQfsbWPz9Kz6ypOasyYK8zvaV+Jd37Sb2WK6eJ+IPg+zFNljIe8/Vh6GphxoT\nZ2EBbaZpnOKQ8StoZfPosHz8gj3erdgKAAlEeROc8P5udXvCvLNZAQt8xdUt8L//\nbVfSSYHrtLNQrFv5CxUVjGn/ozkB7fzc3CeXjnuL1Wqm1uAdX80Bkeb1Ipi6LgkY\nOG8TqIHS+yE35y20YueBkLDGeVm3Z3X+vo87+jbsr63ST3Q2AeVXqyMEzEpel89+\nxu+MzJUjaY3LOMcZ9taKABQeND1v2gwLw7qX/BFLUmE+vzNnUxC/eBsJwke6Hq9Y\n9XWBf71W8etW19lpDAfpNzGwEhwy71bZvnorfL3TPbxqM006PFAQhyfHegpnU9t/\ngJvoniP6+Qg6i6GONFpIM19k05eGBxl9iJTOKnzFat+vvKmfzTqmurtU+X+P388O\nWsStmryzOndzg0yTPJBotXxQlRHIgl6UcdBBGPvJxmXszom2ziKzEVs/4J0+Gxho\nDaoDoWdZv2udvPjyZS+aQTpF2F7QNmxvOx5jtI6YTBPbIQ6fe+3qoKpxw+ujoNIl\nAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBRclwZGNKvfMMV8xQ1VcWYwtWCPnjAfBgNVHSMEGDAWgBRclwZGNKvfMMV8\nxQ1VcWYwtWCPnjANBgkqhkiG9w0BAQsFAAOCAgEAd0tN3uqFSqssJ9ZFx/FfIMFb\nYO0Hy6Iz3DbPx5TxBsfV2s/NrYQ+/xJIf0HopWZXMMQd5KcaLy1Cwe9Gc7LV9Vr9\nDnpr0sgxow1IlldlY1UYwPzkisyYhlurDIonN/ojaFlcJtehwcK5Tiz/KV7mlAu+\nzXJPleiP9ve4Pl7Oz54RyawDKUiKqbamNLmsQP/EtnM3scd/qVHbSypHX0AkB4gG\ntySz+3/3sIsz+r8jdaNc/qplGsK+8X2BdwOBsY3XlQ16PEKYt4+pfVDh31IGmqBS\nVHiDB2FSCTdeipynxlHRXGPRhNzC29L6Wxg2fWa81CiXL3WWHIQHrIuOUxG+JCGq\nZ/LBrYic07B4Z3j101gDIApdIPG152XMDiDj1d/mLxkrhWjBBCbPj+0FU6HdBw7r\nQSbHtKksW+NpPWbAYhvAqobAN8MxBIZwOb5rXyFAQaB/5dkPOEtwX0n4hbgrLqof\nk0FD+PuydDwfS1dbt9RRoZJKzr4Qou7YFCJ7uUG9jemIqdGPAxpg/z+HiaCZJyJm\nsD5onnKIUTidEz5FbQXlRrVz7UOGsRQKHrzaDb8eJFxmjw6+of3G62m8Q3nXA3b5\n3IeZuJjEzX9tEPkQvixC/pwpTYNrCr21jsRIiv0hB6aAfR+b6au9gmFECnEnX22b\nkJ6u/zYks2gD1pWMa3M=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFgTCCA2mgAwIBAgIIIj+pFyDegZQwDQYJKoZIhvcNAQELBQAwTjELMAkGA1UE\nBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEoMCYGA1UEAxMfU3dpc3NTaWdu\nIFBsYXRpbnVtIFJvb3QgQ0EgLSBHMzAeFw0wOTA4MDQxMzM0MDRaFw0zNzA4MDQx\nMzM0MDRaME4xCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxKDAm\nBgNVBAMTH1N3aXNzU2lnbiBQbGF0aW51bSBSb290IENBIC0gRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCUoO8TG59EIBvNxaoiu9nyUj56Wlh35o2h\nK8ncpPPksxOUAGKbHPJDUEOBfq8wNkmsGIkMGEW4PsdUbePYmllriholqba1Dbd9\nI/BffagHqfc+hi7IAU3c5jbtHeU3B2kSS+OD0QQcJPAfcHHnGe1zSG6VKxW2VuYC\n31bpm/rqpu7gwsO64MzGyHvXbzqVmzqPvlss0qmgOD7WiOGxYhOO3KswZ82oaqZj\nK4Kwy8c9Tu1y9n2rMk5lAusPmXT4HBoojA5FAJMsFJ9txxue9orce3jjtJRHHU0F\nbYR6kFSynot1woDfhzk/n/tIVAeNoCn1+WBfWnLou5ugQuAIADSjFTwT49YaawKy\nlCGjnUG8KmtOMzumlDj8PccrM7MuKwZ0rJsQb8VORfddoVYDLA1fer0e3h13kGva\npS2KTOnfQfTnS+x9lUKfTKkJD0OIPz2T5yv0ekjaaMTdEoAxGl0kVCamJCGzTK3a\nFwg2AlfGnIZwyXXJnnxh2HjmuegUafkcECgSXUt1ULo80GdwVVVWS/s9HNjbeU2X\n37ie2xcs1TUHuFCp9473Vv96Z0NPINnKZtY4YEvulDHWDaJIm/80aZTGNfWWiO+q\nZsyBputMU/8ydKe2nZhXtLomqfEzM2J+OrADEVf/3G8RI60+xgrQzFS3LcKTHeXC\npozH2O9T9wIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB\n/zAdBgNVHQ4EFgQUVio/kFj0F1oUstcIG4VbVGpUGigwHwYDVR0jBBgwFoAUVio/\nkFj0F1oUstcIG4VbVGpUGigwDQYJKoZIhvcNAQELBQADggIBAGztiudDqHknm7jP\nhz5kOBiMEUKShjfgWMMb7gQu94TsgxBoDH94LZzCl442ThbYDuprSK1Pnl0NzA2p\nPhiFfsxomTk11tifhsEy+01lsyIUS8iFZtoX/3GRrJxWV95xLFZCv/jNDvCi0//S\nIhX70HgKfuGwWs6ON9upnueVz2PyLA3S+m/zyNX7ALf3NWcQ03tS7BAy+L/dXsmm\ngqTxsL8dLt0l5L1N8DWpkQFH+BAClFvrPusNutUdYyylLqvn4x6j7kuqX7FmAbSC\nWvlGS8fx+N8svv113ZY4mjc6bqXmMhVus5DAOYp0pZWgvg0uiXnNKVaOw15XUcQF\nbwRVj4HpTL1ZRssqvE3JHfLGTwXkyAQN925P2sM6nNLC9enGJHoUPhxCMKgCRTGp\n/FCp3NyGOA9bkz9/CE5qDSc6EHlWwxW4PgaG9tlwZ691eoviWMzGdU8yVcVsFAko\nO/KV5GreLCgHraB9Byjd1Fqj6aZ8E4yZC1J429nR3z5aQ3Z/RmBTws3ndkd8Vc20\nOWQQW5VLNV1EgyTV4C4kDMGAbmkAgAZ3CmaCEAxRbzeJV9vzTOW4ue4jZpdgt1Ld\n2Zb7uoo7oE3OXvBETJDMIU8bOphrjjGD+YMIUssZwTVr7qEVW4g/bazyNJJTpjAq\nE9fmhqhd2ULSx52peovL3+6iMcLl\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBV\nMQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV\nBAMTIUNlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgw\nMTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFX\nb1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvcqN\nrLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1U\nfcIiePyOCbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcScc\nf+Hb0v1naMQFXQoOXXDX2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2\nZjC1vt7tj/id07sBMOby8w7gLJKA84X5KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4M\nx1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR+ScPewavVIMYe+HdVHpR\naG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ezEC8wQjch\nzDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDar\nuHqklWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221K\nmYo0SLwX3OSACCK28jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvA\nSh0JWzko/amrzgD5LkhLJuYwTKVYyrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWv\nHYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0CAwEAAaNCMEAwDgYDVR0PAQH/\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R8bNLtwYgFP6H\nEtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1\nLOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJ\nMuYhOZO9sxXqT2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2e\nJXLOC62qx1ViC777Y7NhRCOjy+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VN\ng64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC2nz4SNAzqfkHx5Xh9T71XXG68pWp\ndIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes5cVAWubXbHssw1ab\nR80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/EaEQ\nPkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGce\nxGATVdVhmVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+\nJ7x6v+Db9NpSvd4MVHAxkUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMl\nOtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGikpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWT\nee5Ehr7XHuQe+w==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBG\nMQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNV\nBAMMEkNBIOayg+mAmuagueivgeS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgw\nMTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRl\nZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k8H/r\nD195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld1\n9AXbbQs5uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExf\nv5RxadmWPgxDT74wwJ85dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnk\nUkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+L\nNVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFyb7Ao65vh4YOhn0pdr8yb\n+gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc76DbT52V\nqyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6K\nyX2m+Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0G\nAbQOXDBGVWCvOGU6yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaK\nJ/kR8slC/k7e3x9cxKSGhxYzoacXGKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwEC\nAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUAA4ICAQBqinA4\nWbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6\nyAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj\n/feTZU7n85iYr83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6\njBAyvd0zaziGfjk9DgNyp115j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2\nltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0AkLppRQjbbpCBhqcqBT/mhDn4t/lX\nX0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97qA4bLJyuQHCH2u2n\nFoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Yjj4D\nu9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10l\nO1Hm13ZBONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Le\nie2uPAmvylezkolwQOQvT8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR1\n2KvxAmLBsX5VYc8T1yaw15zLKYs4SgsOkI26oQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\nHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs\nZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5\nMDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD\nVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy\nZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy\ndmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p\nOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2\n8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K\nTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe\nhRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk\n6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw\nDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q\nAdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI\nbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB\nve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z\nqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd\niEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn\n0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN\nsSi6\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT\nEUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp\nZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz\nNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH\nEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE\nAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD\nE6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH\n/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy\nDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh\nGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR\ntDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA\nAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\nFDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX\nWWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu\n9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr\ngIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo\n2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO\nLPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI\n4uJEvlz36hz1\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\nHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs\nZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw\nMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6\nb25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj\naG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp\nY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg\nnLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1\nHOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N\nHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN\ndloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0\nHZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO\nBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G\nCSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU\nsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3\n4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg\n8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K\npL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1\nmMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID4TCCAsmgAwIBAgIOYyUAAQACFI0zFQLkbPQwDQYJKoZIhvcNAQEFBQAwezEL\nMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV\nBAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEoMCYGA1UEAxMfVEMgVHJ1\nc3RDZW50ZXIgVW5pdmVyc2FsIENBIElJSTAeFw0wOTA5MDkwODE1MjdaFw0yOTEy\nMzEyMzU5NTlaMHsxCzAJBgNVBAYTAkRFMRwwGgYDVQQKExNUQyBUcnVzdENlbnRl\nciBHbWJIMSQwIgYDVQQLExtUQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0ExKDAm\nBgNVBAMTH1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQSBJSUkwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDC2pxisLlxErALyBpXsq6DFJmzNEubkKLF\n5+cvAqBNLaT6hdqbJYUtQCggbergvbFIgyIpRJ9Og+41URNzdNW88jBmlFPAQDYv\nDIRlzg9uwliT6CwLOunBjvvya8o84pxOjuT5fdMnnxvVZ3iHLX8LR7PH6MlIfK8v\nzArZQe+f/prhsq75U7Xl6UafYOPfjdN/+5Z+s7Vy+EutCHnNaYlAJ/Uqwa1D7KRT\nyGG299J5KmcYdkhtWyUB0SbFt1dpIxVbYYqt8Bst2a9c8SaQaanVDED1M4BDj5yj\ndipFtK+/fz6HP3bFzSreIMUWWMv5G/UPyw0RUmS40nZid4PxWJ//AgMBAAGjYzBh\nMB8GA1UdIwQYMBaAFFbn4VslQ4Dg9ozhcbyO5YAvxEjiMA8GA1UdEwEB/wQFMAMB\nAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRW5+FbJUOA4PaM4XG8juWAL8RI\n4jANBgkqhkiG9w0BAQUFAAOCAQEAg8ev6n9NCjw5sWi+e22JLumzCecYV42Fmhfz\ndkJQEw/HkG8zrcVJYCtsSVgZ1OK+t7+rSbyUyKu+KGwWaODIl0YgoGhnYIg5IFHY\naAERzqf2EQf27OysGh+yZm5WZ2B6dF7AbZc2rrUNXWZzwCUyRdhKBgePxLcHsU0G\nDeGl6/R1yrqc0L2z0zIkTO5+4nYES0lT2PLpVDP85XEfPRRclkvxOvIAu2y0+pZV\nCIgJwcyRGSmwIC3/yzikQOEXvnlhgP8HA4ZMTnsGnxGGjYnuJ8Tb4rwZjgvDwxPH\nLQNjO9Po5KIqwoIIlBZU8O8fJ5AluA0OKBtHd0e9HKgl8ZS0Zg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF\nMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD\nbGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha\nME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM\nHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03\nUAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42\ntSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R\nySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM\nlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp\n/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G\nA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G\nA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj\ndG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy\nMENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl\ncmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js\nL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL\nBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni\nacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0\no3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K\nzCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8\nPIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y\nJohw1+qRzT65ysCQblrGXnRl11z+o+I=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF\nMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD\nbGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw\nNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV\nBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn\nljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0\n3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z\nqQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR\np75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8\nHgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw\nggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea\nHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw\nOi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh\nc3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E\nRT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt\ndHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku\nY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp\n3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05\nnsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF\nCSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na\nxpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX\nKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW\nMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1\nOTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG\nA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G\nCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ\nJZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD\nvfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo\nD/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/\nQ0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW\nRST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK\nHDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN\nnw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM\n0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i\nUUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9\nHa90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg\nTuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE\nAwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL\nBQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K\n2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX\nUfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl\n6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK\n9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ\nHgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI\nwpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY\nXzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l\nIxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo\nhdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr\nso8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\nhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV\nBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5\nMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT\nEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR\nQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\ndGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR\n6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X\npz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC\n9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV\n/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf\nZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z\n+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w\nqP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah\nSL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC\nu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf\nFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq\ncrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E\nFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\n/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl\nwFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM\n4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV\n2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna\nFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ\nCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK\nboHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke\njkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL\nS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb\nQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl\n0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\nNVOFBkpdn627G190\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz\ndCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL\nMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp\ncm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP\nHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr\nba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL\nMeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1\nyHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr\nVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/\nnx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ\nKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG\nXUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj\nvbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt\nZ8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g\nN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC\nnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz\ndCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL\nMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp\ncm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y\nYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua\nkCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL\nQESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp\n6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG\nyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i\nQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ\nKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO\ntDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu\nQY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ\nLgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u\nolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48\nx3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz\ndCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG\nA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U\ncnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf\nqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ\nJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ\n+jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS\ns8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5\nHMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7\n70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG\nV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S\nqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S\n5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia\nC1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX\nOwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE\nFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\nBAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2\nKI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg\nNt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B\n8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ\nMKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc\n0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ\nu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF\nu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH\nYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8\nGKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO\nRtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e\nKeC2uAloGRwYQw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC\nVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ\ncmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ\nBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt\nVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D\n0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9\nss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G\nA1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G\nA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs\naobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I\nflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB\niDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\ncnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\nBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw\nMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\nBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU\naGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy\ndGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B\n3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY\ntJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/\nFp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2\nVN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT\n79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6\nc0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT\nYo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l\nc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee\nUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE\nHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd\nBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G\nA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF\nUp/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO\nVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3\nATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs\n8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR\niQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze\nSf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ\nXHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/\nqS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB\nVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB\nL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG\njjxDah2nGN59PRbxYvnKkKj9\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl\neSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT\nJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx\nMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\nCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg\nVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo\nI+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng\no4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G\nA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD\nVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB\nzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW\nRNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGHDCCBASgAwIBAgIES45gAzANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJE\nSzESMBAGA1UEChMJVFJVU1QyNDA4MSIwIAYDVQQDExlUUlVTVDI0MDggT0NFUyBQ\ncmltYXJ5IENBMB4XDTEwMDMwMzEyNDEzNFoXDTM3MTIwMzEzMTEzNFowRTELMAkG\nA1UEBhMCREsxEjAQBgNVBAoTCVRSVVNUMjQwODEiMCAGA1UEAxMZVFJVU1QyNDA4\nIE9DRVMgUHJpbWFyeSBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\nAJlJodr3U1Fa+v8HnyACHV81/wLevLS0KUk58VIABl6Wfs3LLNoj5soVAZv4LBi5\ngs7E8CZ9w0F2CopW8vzM8i5HLKE4eedPdnaFqHiBZ0q5aaaQArW+qKJx1rT/AaXt\nalMB63/yvJcYlXS2lpexk5H/zDBUXeEQyvfmK+slAySWT6wKxIPDwVapauFY9QaG\n+VBhCa5jBstWS7A5gQfEvYqn6csZ3jW472kW6OFNz6ftBcTwufomGJBMkonf4ZLr\n6t0AdRi9jflBPz3MNNRGxyjIuAmFqGocYFA/OODBRjvSHB2DygqQ8k+9tlpvzMRr\nkU7jq3RKL+83G1dJ3/LTjCLz4ryEMIC/OJ/gNZfE0qXddpPtzflIPtUFVffXdbFV\n1t6XZFhJ+wBHQCpJobq/BjqLWUA86upsDbfwnePtmIPRCemeXkY0qabC+2Qmd2Fe\nxyZphwTyMnbqy6FG1tB65dYf3mOqStmLa3RcHn9+2dwNfUkh0tjO2FXD7drWcU0O\nI9DW8oAypiPhm/QCjMU6j6t+0pzqJ/S0tdAo+BeiXK5hwk6aR+sRb608QfBbRAs3\nU/q8jSPByenggac2BtTN6cl+AA1Mfcgl8iXWNFVGegzd/VS9vINClJCe3FNVoUnR\nYCKkj+x0fqxvBLopOkJkmuZw/yhgMxljUi2qYYGn90OzAgMBAAGjggESMIIBDjAP\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjARBgNVHSAECjAIMAYGBFUd\nIAAwgZcGA1UdHwSBjzCBjDAsoCqgKIYmaHR0cDovL2NybC5vY2VzLnRydXN0MjQw\nOC5jb20vb2Nlcy5jcmwwXKBaoFikVjBUMQswCQYDVQQGEwJESzESMBAGA1UEChMJ\nVFJVU1QyNDA4MSIwIAYDVQQDExlUUlVTVDI0MDggT0NFUyBQcmltYXJ5IENBMQ0w\nCwYDVQQDEwRDUkwxMB8GA1UdIwQYMBaAFPZt+LFIs0FDAduGROUYBbdezAY3MB0G\nA1UdDgQWBBT2bfixSLNBQwHbhkTlGAW3XswGNzANBgkqhkiG9w0BAQsFAAOCAgEA\nVPAQGrT7dIjD3/sIbQW86f9CBPu0c7JKN6oUoRUtKqgJ2KCdcB5ANhCoyznHpu3m\n/dUfVUI5hc31CaPgZyY37hch1q4/c9INcELGZVE/FWfehkH+acpdNr7j8UoRZlkN\n15b/0UUBfGeiiJG/ugo4llfoPrp8bUmXEGggK3wyqIPcJatPtHwlb6ympfC2b/Ld\nv/0IdIOzIOm+A89Q0utx+1cOBq72OHy8gpGb6MfncVFMoL2fjP652Ypgtr8qN9Ka\n/XOazktiIf+2Pzp7hLi92hRc9QMYexrV/nnFSQoWdU8TqULFUoZ3zTEC3F/g2yj+\nFhbrgXHGo5/A4O74X+lpbY2XV47aSuw+DzcPt/EhMj2of7SA55WSgbjPMbmNX0rb\noenSIte2HRFW5Tr2W+qqkc/StixgkKdyzGLoFx/xeTWdJkZKwyjqge2wJqws2upY\nEiThhC497+/mTiSuXd69eVUwKyqYp9SD2rTtNmF6TCghRM/dNsJOl+osxDVGcwvt\nWIVFF/Onlu5fu1NHXdqNEfzldKDUvCfii3L2iATTZyHwU9CALE+2eIA+PIaLgnM1\n1oCfUnYBkQurTrihvzz9PryCVkLxiqRmBVvUz+D4N5G/wvvKDS6t6cPCS+hqM482\ncbBsn0R9fFLO4El62S9eH1tqOzO20OAOK65yJIsOpSE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDrTCCApWgAwIBAgIQPr1DlqNlQqhJtpGoElEmzzANBgkqhkiG9w0BAQUFADBm\nMQswCQYDVQQGEwJTRTEoMCYGA1UEChMfU3dlZGlzaCBTb2NpYWwgSW5zdXJhbmNl\nIEFnZW5jeTEtMCsGA1UEAxMkU3dlZGlzaCBHb3Zlcm5tZW50IFJvb3QgQXV0aG9y\naXR5IHYxMB4XDTEwMDQxNDEzMzMyM1oXDTMwMDQxNDEzNDMxOVowZjELMAkGA1UE\nBhMCU0UxKDAmBgNVBAoTH1N3ZWRpc2ggU29jaWFsIEluc3VyYW5jZSBBZ2VuY3kx\nLTArBgNVBAMTJFN3ZWRpc2ggR292ZXJubWVudCBSb290IEF1dGhvcml0eSB2MTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMocT3QV99VeycBYhDLn3PxR\nkZDOESLhJXCDQnD7cNwbC2/CLdCK211WKaq3XoW1d+fBWiBiyJdCYcJDcNtYbHXg\nC7YyiuCkLUW+51s7i3qx/QXMM3+f8Fvtco7NM9PJEovAk0Cjj4Zu342I8+ZqTG+l\nRvpoplsQloMuV6BPjxVZNqVuaIRFHhCHtuJV1bi/q7euZb9XR4zE4+QCjfPcM9vv\nJ0f47MvyPOcJ8/nl+YvH6VrgLZOrqik2L37GyPbw5oBFMRY0avDoSTlEYQDzm+a+\nCmUbMiN5YnesIn8bpF16sOes2OB2Ay9972v3N++jomQczNd92oKizrUBTfgJLDUC\nAwEAAaNXMFUwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYD\nVR0OBBYEFF0HuveNXUnwXJWQCfNe6qzQ9kO2MBAGCSsGAQQBgjcVAQQDAgEAMA0G\nCSqGSIb3DQEBBQUAA4IBAQAw36Sc6JRzKWuuHwxwukz+ZH2sT69JD3KLpLIASGlO\n1fKEMwuNc9vOaQ63Yr+xkeRq4RJJfEZHm4TYmCksB4Mk8B7rcZ2RaHr3W0OtggQP\nQ98mvYJdtpG8Yr5DWqNUb35RMS35N5y2H6j75xGbFgvuYRJU1aWE2f+fkiedU/e2\nCzMXxuuy6oLy9kN2HLwxkzCd6UhiDj5DO6wpzJTgK2yavxfe8crw5h2F0g+rvYed\ngk2GMDW0CdNAqLu3Iv54qNvRUGmAM2AtfY+EueyH0pjpC8zbeRhxE1ig+WvWzuRG\nTAB3ii9cna6JBevqHOCaY/m7RqO5Sr2dGn3KhM3+kPG6\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHBDCCBOygAwIBAgIQDziMcP6mlV3pXZyHr3kEmTANBgkqhkiG9w0BAQsFADB1\nMQswCQYDVQQGEwJFUzFBMD8GA1UEChM4QWdlbmNpYSBOb3RhcmlhbCBkZSBDZXJ0\naWZpY2FjaW9uIFMuTC5VLiAtIENJRiBCODMzOTU5ODgxIzAhBgNVBAMTGkFOQ0VS\nVCBDZXJ0aWZpY2Fkb3MgQ0dOIFYyMB4XDTEwMDUyNTE2MzEyMloXDTMwMDUyNTE2\nMzEyM1owdTELMAkGA1UEBhMCRVMxQTA/BgNVBAoTOEFnZW5jaWEgTm90YXJpYWwg\nZGUgQ2VydGlmaWNhY2lvbiBTLkwuVS4gLSBDSUYgQjgzMzk1OTg4MSMwIQYDVQQD\nExpBTkNFUlQgQ2VydGlmaWNhZG9zIENHTiBWMjCCAiIwDQYJKoZIhvcNAQEBBQAD\nggIPADCCAgoCggIBAJ1ScOknGIPK6sSZ2KbhLhSvbh4OZMqBN1UnHBd3WGcfjMn5\nwopiZSh0m+LRvlUHdnbufG1OY1seSiV14Aeh0NKCp84PM+u6FMBlskou5WW8ItKv\nGg7Ky/NkZSssmaOXi4t1MP5m+sFPSzdQjD/z3pl6ToecIEZyl/5WG2ZOoIJTo1zY\nKEYMBRdvONZcnw4lIsGG41waVNuunWV9AJLfqCEhxVsQJnThsXNXZHx9FwMM6vcU\nlw/5xe5ddbDFxgoLtD5J4xnGm0ST/FoVZAqyg/+AXogJ0Mogo1v7283hGncjGHAa\ni+1EP9YaqDY44Z0vp3fEerPAcrJyzR4/EF4aiHSN8BLF969J3JWvK020kMr57u8M\n478WNyNT4yn69HRpaD1XbRRgimRpKGRN+jZH/bgSzsOGqlzcZjkHTzvj48Vors7g\nOVwggz8SCjizAMFcE5ciXjpLNZn4xB7e+YgRjoTJizLy0te/Igc/YHgudRyiuiMS\n0/BPUDnsyXcnx1oqjtO5tXQEmRUvLoZfjwbByuriqB9NfTOEkaSSw9CmSF1mGneE\nIFCc6gQLDCOWz7Gc/Lm6H5eo06sDZS99rlTHeeIcNt1t0gaYAf3O/D9Lw9Ku/4nY\nOTED2LFkdwPG+KON/Cp55xC9uW2RHD6dy7xVfyL+YYT42NSnIXo5XnIy60x1AgMB\nAAGjggGOMIIBijAPBgNVHRMBAf8EBTADAQH/MIIBJQYDVR0gBIIBHDCCARgwggEU\nBgkrBgEEAYGTaAQwggEFMCUGCCsGAQUFBwIBFhlodHRwOi8vd3d3LmFuY2VydC5j\nb20vY3BzMIHbBggrBgEFBQcCAjCBzjANFgZBTkNFUlQwAwIBAR6BvABBAGcAZQBu\nAGMAaQBhACAATgBvAHQAYQByAGkAYQBsACAAZABlACAAQwBlAHIAdABpAGYAaQBj\nAGEAYwBpAG8AbgAuACAAUABhAHMAZQBvACAAZABlAGwAoABHAGUAbgBlAHIAYQBs\nACAATQBhAHIAdABpAG4AZQB6ACAAQwBhAG0AcABvAHMAIAA0ADYAIAA2AGEAIABw\nAGwAYQBuAHQAYQAgADIAOAAwADEAMAAgAE0AYQBkAHIAaQBkMA4GA1UdDwEB/wQE\nAwIBhjAdBgNVHQ4EFgQUBW7hoZruB6/O9bTTZT0EUOLQm0QwHwYDVR0jBBgwFoAU\nBW7hoZruB6/O9bTTZT0EUOLQm0QwDQYJKoZIhvcNAQELBQADggIBAH9UQBkkykwT\n9hP5XGKVMNW44JOAbNQVRtQnPpJSqtyBY4ZA29Ulr5+TbAr1TaH+VJZdh68Rkw+L\n8uPwH0qf/KnRyVB3X5gICC16i4EQzDsCVFjlxqf098ro9jcGfucR12yFY/eoow7i\nJWIEpPJiU5xHtKdku4Hl1l5WEb5FEWHCZun0DXSoq/lbv4KykaZQ+4d+b7vI6wWi\nuRDXG0IHVc+J5r/7ufBqOVdTcIy9S6Npvx+LplxNZYq5AAnoaL8JJwdNXtpSCYzl\ncZOKzIWO0jdeU9yCbQtWSoR5CvQQJUT1b10aZrXN1RBLh1pO1H/kcazuaJ+8+i5Y\nwcSef6RZheBSDvLHR3UVLSx2jA9FBTVg+Hs7dzJ/KIAJ2jG8cX3hrJHNYAp5IOxu\nO7eE4HLzqUrQL+Rb49Ia1Eq89Xb5fyoZSOvdDs+ZVkW4fdYJjg7Os4RoSYRUNUvk\nmRuv86gU81SYCoB+T7zyZi0m/zCNp/a925qP5eHfu7cyDvmSb2nj5HbTADbxLV7H\nE1/V2Wot6NEba3bLGG4OBRD1WvJJG1m0herKGXTMu1LiN4zCagIlwtJxpJLbjsnW\nqW7QhShtXG0IeAKweQxXbwtaAeOEhAL2z/KrY+sCarnLShjVOSI8VkqqlYjmMAAf\njSEhyVfuubdEKYhPtiunFO6O7m++FtAT\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHEjCCBPqgAwIBAgIQCb1WBSoTFvRoT3QOqX0cSDANBgkqhkiG9w0BAQsFADB8\nMQswCQYDVQQGEwJFUzFBMD8GA1UEChM4QWdlbmNpYSBOb3RhcmlhbCBkZSBDZXJ0\naWZpY2FjaW9uIFMuTC5VLiAtIENJRiBCODMzOTU5ODgxKjAoBgNVBAMTIUFOQ0VS\nVCBDZXJ0aWZpY2Fkb3MgTm90YXJpYWxlcyBWMjAeFw0xMDA1MjUxNjU2MTRaFw0z\nMDA1MjUxNjU2MTRaMHwxCzAJBgNVBAYTAkVTMUEwPwYDVQQKEzhBZ2VuY2lhIE5v\ndGFyaWFsIGRlIENlcnRpZmljYWNpb24gUy5MLlUuIC0gQ0lGIEI4MzM5NTk4ODEq\nMCgGA1UEAxMhQU5DRVJUIENlcnRpZmljYWRvcyBOb3RhcmlhbGVzIFYyMIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsua5xh1qKi1Jxfz81GRA0OAULveg\nwv+S80GmtD/avhkUkZR20xXMXn94UHrb2sVFqsscI3lzkKi7ZwFzjs5A+Rqpqofk\nk5IPXGhcXvAGYCtY3DxtPMd6MGsFqpKGcyrS8hqIxNvlWmaOdclCP5uIKEAe9alc\nHvrIQaEwqwuc7haiwS2lhfrtoAzof5ZKe72PmqIYdtKv3bc9EKtSEIiuHeu4MnSW\n9LeqJ/elBw3jlFdqVCB3zR28eS3knLTeUYj+VtY9i6HP+lIejAVzd9YFz2MAUYdh\n41C+mZfh/B4ReWtOas+chQoclirAIDYUxQkXYjv0rerV1/3QOSp409Ciz8hzMAlH\nxU4Z/bgw1A+AmIiGwUxBeiPFQ/1eErg+D7G3gWIMfm/je5rCwkcRIR/PntEwzoPB\nEE1Ad9e1wksyQEL6m7Csz+sh2BnrZMVr3VUtgIdEfEw8qw3YEr80goyxqsS4a+gO\nRnfSiwYdQvusvcnnM7Mib37VLgPFXwUWhnzt457RFncaRtjJ0IzkXFwhBZHxZOSs\nxTeutb1nE64p5bNCxHAJo11M6zcg4/D1czM7wvyOUYU2KsuB2w6JI9ni4Wi6LER3\nPhxAuvBnjhiH8D3X6T9HWzVCzacEzkhyKQUatNGi5w15ipZtZ1ItOyPm+YKc1rN5\nXhTeZUgz/B1C6C0CAwEAAaOCAY4wggGKMA8GA1UdEwEB/wQFMAMBAf8wggElBgNV\nHSAEggEcMIIBGDCCARQGCSsGAQQBgZNoATCCAQUwJQYIKwYBBQUHAgEWGWh0dHA6\nLy93d3cuYW5jZXJ0LmNvbS9jcHMwgdsGCCsGAQUFBwICMIHOMA0WBkFOQ0VSVDAD\nAgEBHoG8AEEAZwBlAG4AYwBpAGEAIABOAG8AdABhAHIAaQBhAGwAIABkAGUAIABD\nAGUAcgB0AGkAZgBpAGMAYQBjAGkAbwBuAC4AIABQAGEAcwBlAG8AIABkAGUAbACg\nAEcAZQBuAGUAcgBhAGwAIABNAGEAcgB0AGkAbgBlAHoAIABDAGEAbQBwAG8AcwAg\nADQANgAgADYAYQAgAHAAbABhAG4AdABhACAAMgA4ADAAMQAwACAATQBhAGQAcgBp\nAGQwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBT2Ejqre1jBjUNvdoHS8rjT7xfq\nCzAfBgNVHSMEGDAWgBT2Ejqre1jBjUNvdoHS8rjT7xfqCzANBgkqhkiG9w0BAQsF\nAAOCAgEAVDXTomXJ2TbFU9G0jXI0ibqnCJ/pNRC5uAwG+WSqlZYoqMijgNxWwL9y\nTVa/f10E1a0oW02988MPFbBx2laNQFVXpn1ioq0TaVGqlFC6vQAwUPXdpE4JepQx\na9tzA73z2hoPjC+yyTe8VNULIzf15Fs3ZolPtMcFpGXcWTCmEyt+Fe3sEBeJUsmd\n36JM7fYPHqZJsA1RszGxUZnLtNEjeNJLqLQdFqag0D4HfmU/Jc5kThsuS02ChRpl\n2+7iA/BZJAWPme95gt/uKjdow2pQAVlfn2jcLFFgK13gUjw7cLgA0zeoPlsedgha\n1Lt2MK75yPKOpI8KdX0amOG/0DaULzzBUtNp6hpgN4yA201txppdjaBhUbs9DeYS\noJ9vWVZ0MmcK/DcGwTrkK46EH9ohDEmIQ9Ol9YINdobDLMyQu7O4q8bLrsAXUZ7T\ngPck2hzszhKDzk42MDl1+HR2kIKePkBMDBS5Gh5IarAx6oh/gEFAU3s4S4eQYHpL\nzmdGaHV3jgBdILDkkzdtA99YOeiaxaTr7GEzCIUka08G6a2QpTZibOPdfQkfM7+3\nu/fJdQX3W6v6h1mvGmcQfoTcjHDWROkQwdibLtHGQGrq5loPEH1s+1WHuk21cQOe\nF4942lU9V14iCmqY8I0Izd2WQlobzbpvJ7h0J6g/5aDWc8deLyE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF7TCCA9WgAwIBAgIQKMw6Jb+6RKxEmptYa0M5qjANBgkqhkiG9w0BAQsFADCB\niDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl\nZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp\nTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAw\nNjIzMjE1NzI0WhcNMzUwNjIzMjIwNDAxWjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\nBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv\nc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm\naWNhdGUgQXV0aG9yaXR5IDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQC5CJ4o5OTsBk5QaLNBxXvrrraOr4G6IkQfZTRpTL5wQBfyFnvief2G7Q05\n9BuorZKQHss9do9a2bWREC48BY2KbSRU5x/tVq2DtFCcFaUXdIhZIPwIxYR202jU\nbyh4zly481CQRP/jY1++oZoslhUE1gf+HoQh4EIxEcQoNpTPUKRinsnWq3EAslsM\n5pbUCiSW9f/G1bcb18u3IWKvEtyhXTfjGvsaRpjAm8DnYx8qCJMCfh5qjvKfGInk\nIoWisYRXQP/1DthvnO3iRTEBzRfpf7CBReOqIUAmoXKqp088AQV+7oNYsV4GY5li\nkXiCtw2TDCRqtBvbJ+xflQQ/k0ow9ZcYs6f5GaeTMx0ByNsiUlzXJclG+aL7h1lD\nvptisY0thkQaRqx4YX4wCfquicRBKiJmA5E5RZzHiwyoyg0v+1LqDPdjMyOd/rAf\nrWfWp1ADxgRwY7UssYZaQ7f7rvluKW4hIUEmBozJw+6wwoWTobmF2eYybEtMP9Zd\no+W1nXfDnMBVt3QA47g4q4OXUOGaQiQdxsCjMNEaWshSNPdz8ccYHzOteuzLQWDz\nI5QgwkhFrFxRxi6AwuJ3Fb2Fh+02nZaR7gC1o3Dsn+ONgGiDdrqvXXBSIhbiZvu6\ns8XC9z4vd6bK3sGmxkhMwzdRI9Mn17hOcJbwoUR2r3jPmuFmEwIDAQABo1EwTzAL\nBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU1fZWy4/oolxi\naNE9lJBb186aGMQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIB\nAKylloy/u66m9tdxh0MxVoj9HDJxWzW31PCR8q834hTx8wImBT4WFH8UurhP+4my\nsufUCcxtuVs7ZGVwZrfysVrfGgLz9VG4Z215879We+SEuSsem0CcJjT5RxiYadgc\n17bRv49hwmfEte9gQ44QGzZJ5CDKrafBsSdlCfjN9Vsq0IQz8+8f8vWcC1iTN6B1\noN5y3mx1KmYi9YwGMFafQLkwqkB3FYLXi+zA07K9g8V3DB6urxlToE15cZ8PrzDO\nZ/nWLMwiQXoH8pdCGM5ZeRBV3m8Q5Ljag2ZAFgloI1uXLiaaArtXjMW4umliMoCJ\nnqH9wJJ8eyszGYQqY8UAaGL6n0eNmXpFOqfp7e5pQrXzgZtHVhB7/HA2hBhz6u/5\nl02eMyPdJgu6Krc/RNyDJ/+9YVkrEbfKT9vFiwwcMa4y+Pi5Qvd/3GGadrFaBOER\nPWZFtxhxvskkhdbz1LpBNF0SLSW5jaYTSG1LsAd9mZMJYYF0VyaKq2nj5NnHiMwk\n2OxSJFwevJEU4pbe6wrant1fs1vb1ILsxiBQhyVAOvvH7s3+M+Vuw4QJVQMlOcDp\nNV1lMaj2v6AJzSnHszYyLtyV84PBWs+LjfbqsyH4pO0eMQ62TBGrYAukEiMiF6M2\nZIKRBBLgq28ey1AFYbRA/1mGcdHVM2l8qXOKONdkDPFp\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC\nQ04xMjAwBgNVBAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24g\nQ2VudGVyMUcwRQYDVQQDDD5DaGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0\naW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMgUm9vdDAeFw0xMDA4MzEwNzExMjVa\nFw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAGA1UECgwpQ2hpbmEg\nSW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMMPkNo\naW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRp\nZmljYXRlcyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z\n7r07eKpkQ0H1UN+U8i6yjUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA//\nDdmEEbK40ctb3B75aDFk4Zv6dOtouSCV98YPjUesWgbdYavi7NifFy2cyjw1l1Vx\nzUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2HklY0bBoQCxfVWhyXWIQ8\nhBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23KzhmBsUs\n4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54u\ngQEC7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oY\nNJKiyoOCWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E\nFgQUfHJLOcfA22KlT5uqGDSSosqDglkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3\nj92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd50XPFtQO3WKwMVC/GVhMPMdoG\n52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM7+czV0I664zB\nechNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws\nZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrI\nzo9uoV1/A3U05K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATy\nwy39FCqQmbkHzJ8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGGTCCBAGgAwIBAgIIPtVRGeZNzn4wDQYJKoZIhvcNAQELBQAwajEhMB8GA1UE\nAxMYU0cgVFJVU1QgU0VSVklDRVMgUkFDSU5FMRwwGgYDVQQLExMwMDAyIDQzNTI1\nMjg5NTAwMDIyMRowGAYDVQQKExFTRyBUUlVTVCBTRVJWSUNFUzELMAkGA1UEBhMC\nRlIwHhcNMTAwOTA2MTI1MzQyWhcNMzAwOTA1MTI1MzQyWjBqMSEwHwYDVQQDExhT\nRyBUUlVTVCBTRVJWSUNFUyBSQUNJTkUxHDAaBgNVBAsTEzAwMDIgNDM1MjUyODk1\nMDAwMjIxGjAYBgNVBAoTEVNHIFRSVVNUIFNFUlZJQ0VTMQswCQYDVQQGEwJGUjCC\nAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANqoVgLsfJXwTukK0rcHoyKL\nULO5Lhk9V9sZqtIr5M5C4myh5F0lHjMdtkXRtPpZilZwyW0IdmlwmubHnAgwE/7m\n0ZJoYT5MEfJu8rF7V1ZLCb3cD9lxDOiaN94iEByZXtaxFwfTpDktwhpz/cpLKQfC\neSnIyCauLMT8I8hL4oZWDyj9tocbaF85ZEX9aINsdSQePHWZYfrSFPipS7HYfad4\n0hNiZbXWvn5qA7y1svxkMMPQwpk9maTTzdGxxFOHe0wTE2Z/v9VlU2j5XB7ltP82\nmUWjn2LAfxGCAVTeD2WlOa6dSEyJoxA74OaD9bDaLB56HFwfAKzMq6dgZLPGxXvH\nVUZ0PJCBDkqOWZ1UsEixUkw7mO6r2jS3U81J2i/rlb4MVxH2lkwEeVyZ1eXkvm/q\nR+5RS+8iJq612BGqQ7t4vwt+tN3PdB0lqYljseI0gcSINTjiAg0PE8nVKoIV8IrE\nQzJW5FMdHay2z32bll0eZOl0c8RW5BZKUm2SOdPhTQ4/YrnerbUdZbldUv5dCamc\ntKQM2S9FdqXPjmqanqqwEaHrYcbrPx78ZrQSnUZ/MhaJvnFFr5Eh2f2Tv7QCkUL/\nSR/tixVo3R+OrJvdggWcRGkWZBdWX0EPSk8ED2VQhpOX7EW/XcIc3M/E2DrmeAXQ\nxVVVqV7+qzohu+VyFPcLAgMBAAGjgcIwgb8wHQYDVR0OBBYEFCkgy/HDD9oGjhOT\nh/5fYBopu/O2MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUKSDL8cMP2gaO\nE5OH/l9gGim787YwEQYDVR0gBAowCDAGBgRVHSAAMEkGA1UdHwRCMEAwPqA8oDqG\nOGh0dHA6Ly9jcmwuc2d0cnVzdHNlcnZpY2VzLmNvbS9yYWNpbmUtR3JvdXBlU0cv\nTGF0ZXN0Q1JMMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEATEZn\n4ERQ9cW2urJRCiUTHbfHiC4fuStkoMuTiFJZqmD1zClSF/8E5ze0MRFGfisebKeL\nPEeaXvSqXZA7RT2fSsmKe47A7j55i5KjyJRKuCgRa6YlX129x8j7g09VMeZc8BN8\n471/Kiw3N5RJr4QfFCeiWBCPCjk3GhIgQY8Z9qkfGe2yNLKtfTNEi18KB0PydkVF\nLa3kjQ4A/QQIqudr+xe9sAhWDjUqcvCz5006Tw3c82ASszhkjNv54SaNL+9O6CRH\nPjY0imkPKGuLh8a9hSb50+tpIVZgkdb34GLCqHGuLt5mI7VSRqakSDcsfwEWVxH3\nJw0O5Q/WkEXhHj8h3NL8FhgTPk1qsiZqQF4leP049KxYejcbmEAEx47J1MRnYbGY\nrvDNDty5r2WDewoEij9hqvddQYbmxkzCTzpcVuooO6dEz8hKZPVyYC3jQ7hK4HU8\nMuSqFtcRucFF2ZtmY2blIrc07rrVdC8lZPOBVMt33lfUk+OsBzE6PlwDg1dTx/D+\naNglUE0SyObhlY1nqzyTPxcCujjXnvcwpT09RAEzGpqfjtCf8e4wiHPvriQZupdz\nFcHscQyEZLV77LxpPqRtCRY2yko5isune8YdfucziMm+MG2chZUh6Uc7Bn6B4upG\n5nBYgOao8p0LadEziVkw82TTC/bOKwn7fRB2LhA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGOTCCBCGgAwIBAgIBAzANBgkqhkiG9w0BAQUFADCBzjELMAkGA1UEBhMCWkEx\nFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEWMBQGA1UEBxMNU29tZXJzZXQgV2VzdDEq\nMCgGA1UEChMhU291dGggQWZyaWNhbiBQb3N0IE9mZmljZSBMaW1pdGVkMRowGAYD\nVQQLExFTQVBPIFRydXN0IENlbnRyZTEdMBsGA1UEAxMUU0FQTyBDbGFzcyA0IFJv\nb3QgQ0ExKTAnBgkqhkiG9w0BCQEWGnBraWFkbWluQHRydXN0Y2VudHJlLmNvLnph\nMB4XDTEwMDkxNTAwMDAwMFoXDTMwMDkxNDAwMDAwMFowgc4xCzAJBgNVBAYTAlpB\nMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxFjAUBgNVBAcTDVNvbWVyc2V0IFdlc3Qx\nKjAoBgNVBAoTIVNvdXRoIEFmcmljYW4gUG9zdCBPZmZpY2UgTGltaXRlZDEaMBgG\nA1UECxMRU0FQTyBUcnVzdCBDZW50cmUxHTAbBgNVBAMTFFNBUE8gQ2xhc3MgNCBS\nb290IENBMSkwJwYJKoZIhvcNAQkBFhpwa2lhZG1pbkB0cnVzdGNlbnRyZS5jby56\nYTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvc7UiaoKOf4BGO2ciS\ndTpVwVEiygt6pDUNxeZXLYPwKm8iODcxbXyFJKIGL0OCPUUwQCUc7lhHQebwngAe\n+PQvEbuSsphFLdMfgMl2FBPDzEDmres5YPzPyN8q/YwSUe/PDGTGV+gjUV3nZlLq\nZr2Tf516KPEZcG6EnzBHt7A5axMs60tNLq8/v/0CE0o55z4zxRCRUb4PR51NUvws\n8+MTogCC4RQMzdKes/Lggdq+mZJT432Zd0Ph4UgpgZ7WBVc6cdw+mK1YcG9Gu34y\nA+KDm1lX9/izzVQW7LatoRwaktHUKZ6PzbPofVDxwoKsur20dVag9UVdGH0sjPF7\nQcyGsZqESwoqXZuW4c36qxYnQeeVNabLiqeW86XMUfktfR5D+9xttbk4vQX7WPou\n0+xeZC2vWAFKfCJG00HLPeSWXklDOLuJ6/ScaTkSA1yEu+WMHurgZrvAv4z+ngpN\nPWg/QHbWMqnqRbhqB1KOzVHxXShjDNNZOPzJ/YLJRSC85ujMogzLe2Q5SUZF9XMc\napcg6yFC97QgUrdK/XW8yw8MZxFXH/cw8auQzF08lgVi08pVAUtGxYCHHHLQc1Qh\n6tejnNOuf9RT2Sj8V97lP1JKu8gmJEdTHHO6z8a0MM1eccdWvEk4JebFEAl42dQd\nXM1u7duRXKFTFFaqjSeppo4bAgMBAAGjIDAeMA4GA1UdDwEB/wQEAwIBBjAMBgNV\nHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQDRC4XUXqaW8JCJEtkBUwjs8u1D\nOIk+wSlk7lP/T/6LCg6S9P19KIDOAQn8MdFrWwOJsi5gA+XvY5aRKV/gP5j4aJhV\nY8pPa1NyurNiGhmewRMdmYL6q6ozgLn3Gvx5TALQ4YOLLHgsL1wwYTJPu9E+lV9g\ndlXRDm95Tg/6uHWxH/4Ni1uLKELKTUVjUgJJzJ0PX7s5P2GbsVx0Q5pWw5l/n8RN\nwNOP18tUl/X7SH4ngv66Y+3ob+OE62k+CPLKJKo0jmJAhw3HUdQBddxmev/pujJv\nT49yNWwJ7Vt4sKlI+nyRQbKsjjE3JZUMRaVVShlRjFWTCRXJ9EABnLV1fKoB/rJp\nTRiaAut0APt7aPTww3+mnfTs6EK664N5l/yjdhlbcX8mZ+lPKnuzy37z/PWnv/s/\nl3V10MvreOMDa46C0BFh+zc9p5gLHv47XtnPAKUX6ez9DLXduMa8vfPSMO6FDoX5\nUjNIhufGr7+/DNBLcED1XshVP1AcTwfZZ3YUfWYNwrXKooI0A4b40lq2EpYmeXoX\nbZipzPngyxpF//PADMVi/hPGLb9qF+pjDX4+JH5iOU4nORtBS8OxYHFR+gmCrA+N\n19fv3h2rK1G9+ADz1IHHDZgcPewvowbI9VUApz/9l6uwtUqZWQiIkGwcsP/o7XyZ\nPy1q7W19ZhLu2OAx7A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGOTCCBCGgAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBzjELMAkGA1UEBhMCWkEx\nFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEWMBQGA1UEBxMNU29tZXJzZXQgV2VzdDEq\nMCgGA1UEChMhU291dGggQWZyaWNhbiBQb3N0IE9mZmljZSBMaW1pdGVkMRowGAYD\nVQQLExFTQVBPIFRydXN0IENlbnRyZTEdMBsGA1UEAxMUU0FQTyBDbGFzcyAzIFJv\nb3QgQ0ExKTAnBgkqhkiG9w0BCQEWGnBraWFkbWluQHRydXN0Y2VudHJlLmNvLnph\nMB4XDTEwMDkxNTAwMDAwMFoXDTMwMDkxNDAwMDAwMFowgc4xCzAJBgNVBAYTAlpB\nMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxFjAUBgNVBAcTDVNvbWVyc2V0IFdlc3Qx\nKjAoBgNVBAoTIVNvdXRoIEFmcmljYW4gUG9zdCBPZmZpY2UgTGltaXRlZDEaMBgG\nA1UECxMRU0FQTyBUcnVzdCBDZW50cmUxHTAbBgNVBAMTFFNBUE8gQ2xhc3MgMyBS\nb290IENBMSkwJwYJKoZIhvcNAQkBFhpwa2lhZG1pbkB0cnVzdGNlbnRyZS5jby56\nYTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMp4Gge89vu0t4m80BlW\nOCpZnQfqGvn4+GhnXo/vyvf1aonmo5V/qdspJBw10DiWbD5WJP9eYlGQLofonMfa\nvDPxnqFvC44KJPT4TZCmss1eEdPCl0z1X0AdJiRNjQkQC/+7IBuTJhkMQz/pjrwx\nNxBukcpIglZGx7y5Op5GgWbP2ehcEM85nmXDnsVa9EvMRJlmhvRyG6NTSequR80y\nDXDmoKB2B53/WO/kPJHAteTcuAEM0/6zQqA7YQLUN1vXTEWV0nVd9W4wX1dRi7L/\nfsiLnKqjQTcMEJGopoVcucePBVGy0HjS4ktJ6dQapzusqjPmmioDQJhvdFITMZTR\nEsG0yzD5/0S4kltS1jDZM9F14xmlFhW3VFfxVlDOTr4DOy/stjDuFGBeX3o19E5k\nBxHqpQdmG26T4rBPXtbgROCz3K7vuP2os+zs5TmIRLShuxRgZI/WkpPL88xQ3ekH\nyGdn+fCHhJGyAGLpv0oVdMW/BEwFRl0Ky+XqYQDhb0GxNI6mAKJ8pqWm+mxMQ+Wo\nJpo0mB6HmOdMeNGPnwVVXYpLyc+gC30GkJwYkrLEstfjRdlrc8OXOb8pHgYJVUC6\nvNpIdUPt/kR+PSzmYpED/T2J7370XSSPpQsrsz56KSi8uz+/63eFBCaLlLKQ9euN\nT6JEIlConCpESAB4GaudCJYVAgMBAAGjIDAeMA4GA1UdDwEB/wQEAwIBBjAMBgNV\nHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQBFyZ6kYCWJ/peZuMLxrtc1EzZ5\n0N2CIlHxTPZjNqENWyDyG4XWdo95D6urosg3uClj7OWetniWpv+KEJ4Ubhpt316b\nuSyFcgnyNxqbebMo7+WW3Uabh4yN+EGMFOGtRV/LpyQMwTe6LALEq4w1OAnpkPFm\ncqWRQnkJChROmnSWRaEvIKSXdCrLAbPLVireLFhfF2diviu6ERMtEEBFYfDDxe+P\nGdA6wmUK2WjonAYgN7qfSxY5YHjgdWJVwNnLNyEJEJA5z1yZ7N+s1lpHQSOruKch\nB5IUrIzaiiQW6xSISLzvgc6OFt890lpvn8BBcSWJBizmvE/tpJHzxu1U3dmTAyKq\nhAeoc9unWolN9u1ygOuDeESpIiRomLE/qUHy7OkEpCIzX+Z13L2eJfXjZGUewfNX\nJy7JwDJ4RNvYOBN24R1/4BeHmn9NSwduuFc4hbnpU06XOg0fU7mBckVG88h+pgnu\nGDR1fofn6CDu3BbU5seEqtpvX5zM61gGQZOM5cxZDGhlOTwpFmHxaftHucLYZ4Ek\nC/T8SWIArwej/56gDsMBiyFn1jsbPOCht23cVUvj0C6d1p7KbrqzuvBgfn8FUONB\n8b5AOpBX4C1pbAvBvHrBjvsJ3uqVfzmbw2OfSfV4r35JgqyfbowSGlC2wOPchILY\n69K7Dl02hgJJKwU7Vw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEOTCCAyGgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBzjELMAkGA1UEBhMCWkEx\nFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEWMBQGA1UEBxMNU29tZXJzZXQgV2VzdDEq\nMCgGA1UEChMhU291dGggQWZyaWNhbiBQb3N0IE9mZmljZSBMaW1pdGVkMRowGAYD\nVQQLExFTQVBPIFRydXN0IENlbnRyZTEdMBsGA1UEAxMUU0FQTyBDbGFzcyAyIFJv\nb3QgQ0ExKTAnBgkqhkiG9w0BCQEWGnBraWFkbWluQHRydXN0Y2VudHJlLmNvLnph\nMB4XDTEwMDkxNTAwMDAwMFoXDTMwMDkxNDAwMDAwMFowgc4xCzAJBgNVBAYTAlpB\nMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxFjAUBgNVBAcTDVNvbWVyc2V0IFdlc3Qx\nKjAoBgNVBAoTIVNvdXRoIEFmcmljYW4gUG9zdCBPZmZpY2UgTGltaXRlZDEaMBgG\nA1UECxMRU0FQTyBUcnVzdCBDZW50cmUxHTAbBgNVBAMTFFNBUE8gQ2xhc3MgMiBS\nb290IENBMSkwJwYJKoZIhvcNAQkBFhpwa2lhZG1pbkB0cnVzdGNlbnRyZS5jby56\nYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALd8aXxg9Wwm9KocF39d\n1BFL5/Pa53On+qRCSWg/2qVAXAZoX07Mvb6BOCQtzCagRG0DyyPgu96FU0uUX197\nqsgal/7XI5PtsGq92PwAPrOSBOBLvk87mKed7c1j8IHnbJjUbGBVAOW5POY0lV3g\n/XGH6f+B7uV3bxj/88l8pZXdgtwU2aLhvs0nc7tFWz90sWJ4ZhAiLPVo8xeIFjua\nGx37FK4NuvKQVaLVMNYrlTLHOW57ZdJ3OM5uVqXZI6s4sjtRhcAdG7cRLwVpR9gC\nypKo4TPehQib7ZDV2CGZcb+29XPvZwiYZNLyKnpLIRbhH1hh3pFHHyGfH/6MI4aD\nGCcCAwEAAaMgMB4wDgYDVR0PAQH/BAQDAgEGMAwGA1UdEwQFMAMBAf8wDQYJKoZI\nhvcNAQEFBQADggEBACPByWyDecjPhX88XrtWrP9gR1GnnErxh8RNh9/mTA3kM+l+\nCFMQoutCPq9I8ccdFZd0dhy9dCJD6FlZPg3Kccbnl6h+91uf3nToG1FCSWPAo+iU\nj9aets0F1s6g6rGHsLsuCrroXTs8AP9vFl1lZFBQNf8XuHYYx/FrXw3Z6OoTI2F/\nYc5rSQeBMFIh8qHBmO/GQvMv4w5oaUXzkdFkUabaSnmaJFvDTLGHEcfh91z4Il43\n1nZHe79pn1XVMCUsSqtMhOQlWqTSYcah4JBzLH+pvjac/m4hV0WRQaoCbVO4MLvc\nwucgMw5Ve/tCkwcaSF4t/kS3H2S+G8NNnerWMmA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGWDCCBECgAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBzjELMAkGA1UEBhMCWkEx\nFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEWMBQGA1UEBxMNU29tZXJzZXQgV2VzdDEq\nMCgGA1UEChMhU291dGggQWZyaWNhbiBQb3N0IE9mZmljZSBMaW1pdGVkMRowGAYD\nVQQLExFTQVBPIFRydXN0IENlbnRyZTEdMBsGA1UEAxMUU0FQTyBDbGFzcyAzIFJv\nb3QgQ0ExKTAnBgkqhkiG9w0BCQEWGnBraWFkbWluQHRydXN0Y2VudHJlLmNvLnph\nMB4XDTEwMDkxNTAwMDAwMFoXDTMwMDkxNDAwMDAwMFowgc4xCzAJBgNVBAYTAlpB\nMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxFjAUBgNVBAcTDVNvbWVyc2V0IFdlc3Qx\nKjAoBgNVBAoTIVNvdXRoIEFmcmljYW4gUG9zdCBPZmZpY2UgTGltaXRlZDEaMBgG\nA1UECxMRU0FQTyBUcnVzdCBDZW50cmUxHTAbBgNVBAMTFFNBUE8gQ2xhc3MgMyBS\nb290IENBMSkwJwYJKoZIhvcNAQkBFhpwa2lhZG1pbkB0cnVzdGNlbnRyZS5jby56\nYTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMp4Gge89vu0t4m80BlW\nOCpZnQfqGvn4+GhnXo/vyvf1aonmo5V/qdspJBw10DiWbD5WJP9eYlGQLofonMfa\nvDPxnqFvC44KJPT4TZCmss1eEdPCl0z1X0AdJiRNjQkQC/+7IBuTJhkMQz/pjrwx\nNxBukcpIglZGx7y5Op5GgWbP2ehcEM85nmXDnsVa9EvMRJlmhvRyG6NTSequR80y\nDXDmoKB2B53/WO/kPJHAteTcuAEM0/6zQqA7YQLUN1vXTEWV0nVd9W4wX1dRi7L/\nfsiLnKqjQTcMEJGopoVcucePBVGy0HjS4ktJ6dQapzusqjPmmioDQJhvdFITMZTR\nEsG0yzD5/0S4kltS1jDZM9F14xmlFhW3VFfxVlDOTr4DOy/stjDuFGBeX3o19E5k\nBxHqpQdmG26T4rBPXtbgROCz3K7vuP2os+zs5TmIRLShuxRgZI/WkpPL88xQ3ekH\nyGdn+fCHhJGyAGLpv0oVdMW/BEwFRl0Ky+XqYQDhb0GxNI6mAKJ8pqWm+mxMQ+Wo\nJpo0mB6HmOdMeNGPnwVVXYpLyc+gC30GkJwYkrLEstfjRdlrc8OXOb8pHgYJVUC6\nvNpIdUPt/kR+PSzmYpED/T2J7370XSSPpQsrsz56KSi8uz+/63eFBCaLlLKQ9euN\nT6JEIlConCpESAB4GaudCJYVAgMBAAGjPzA9MA4GA1UdDwEB/wQEAwIBBjAMBgNV\nHRMEBTADAQH/MB0GA1UdDgQWBBRhs3lSnUqVklGOgiRw045AyMVm0DANBgkqhkiG\n9w0BAQUFAAOCAgEAf8azJIRQN/nEsMUwPBbpUA16urQ70iPl6Yl4auXjGwUekRzO\nBpeNZhYHRO+BuQh+o8c5NLi/mm2NsMEgQi4N9wsGA09uy7y3sC8ZcY2OrwpNWDGL\nRJkqKGaFx4AmZrBHwjmy+k8+Vb3ciSdLczME/ntHkMkFwC0z+LcIgilBQ/0mU+b6\nHzdWjU8Xutj9OoRw2D7wM67EBUhUobnVIT/qPsepMUf3m65KYpjRZyBl3nnhsTIe\na9/7gGtHXDnHDgiqx6PuKek04pv5dbgm64idtDkRLnD9UQQyuw95hFAhRXwv5Nn/\nJTgGI6tOsQ7cOzEKrdpLAGlrLuLDDMkFAUVm4aWJYRxkmY0LmJCzfmY7C9ir6HUO\n2X+abn3JgyfJvOg0OMJahzJyBwz+1ZTR8MB48oCoRvVrmuzi2RaOivqE9tFSyZyy\nIVZgQ6YQ939Jv74H01BkbQK6KlUsz9nCbq98C0jQ8eGnwq10j4bk7ar6XIN9Quh9\nBx0HVcwraTK5d4JoxnfyImmmyQpdh5nlcZ59LxMe0vT9CXknWCsKh4Eq+2ojLUsk\nhXQWRxgPCcX+qUgk46zQaT1fU5gyvezgUcFTSrH2O/A0SPWa3tzR4OO9JbNE6Dpz\nyXnQrNHt4gAKX6EdZllKc2jUBXIzOKdrr5HbDceMQOiekIjJ+/4k14Gs894=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGWDCCBECgAwIBAgIBAzANBgkqhkiG9w0BAQUFADCBzjELMAkGA1UEBhMCWkEx\nFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEWMBQGA1UEBxMNU29tZXJzZXQgV2VzdDEq\nMCgGA1UEChMhU291dGggQWZyaWNhbiBQb3N0IE9mZmljZSBMaW1pdGVkMRowGAYD\nVQQLExFTQVBPIFRydXN0IENlbnRyZTEdMBsGA1UEAxMUU0FQTyBDbGFzcyA0IFJv\nb3QgQ0ExKTAnBgkqhkiG9w0BCQEWGnBraWFkbWluQHRydXN0Y2VudHJlLmNvLnph\nMB4XDTEwMDkxNTAwMDAwMFoXDTMwMDkxNDAwMDAwMFowgc4xCzAJBgNVBAYTAlpB\nMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxFjAUBgNVBAcTDVNvbWVyc2V0IFdlc3Qx\nKjAoBgNVBAoTIVNvdXRoIEFmcmljYW4gUG9zdCBPZmZpY2UgTGltaXRlZDEaMBgG\nA1UECxMRU0FQTyBUcnVzdCBDZW50cmUxHTAbBgNVBAMTFFNBUE8gQ2xhc3MgNCBS\nb290IENBMSkwJwYJKoZIhvcNAQkBFhpwa2lhZG1pbkB0cnVzdGNlbnRyZS5jby56\nYTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvc7UiaoKOf4BGO2ciS\ndTpVwVEiygt6pDUNxeZXLYPwKm8iODcxbXyFJKIGL0OCPUUwQCUc7lhHQebwngAe\n+PQvEbuSsphFLdMfgMl2FBPDzEDmres5YPzPyN8q/YwSUe/PDGTGV+gjUV3nZlLq\nZr2Tf516KPEZcG6EnzBHt7A5axMs60tNLq8/v/0CE0o55z4zxRCRUb4PR51NUvws\n8+MTogCC4RQMzdKes/Lggdq+mZJT432Zd0Ph4UgpgZ7WBVc6cdw+mK1YcG9Gu34y\nA+KDm1lX9/izzVQW7LatoRwaktHUKZ6PzbPofVDxwoKsur20dVag9UVdGH0sjPF7\nQcyGsZqESwoqXZuW4c36qxYnQeeVNabLiqeW86XMUfktfR5D+9xttbk4vQX7WPou\n0+xeZC2vWAFKfCJG00HLPeSWXklDOLuJ6/ScaTkSA1yEu+WMHurgZrvAv4z+ngpN\nPWg/QHbWMqnqRbhqB1KOzVHxXShjDNNZOPzJ/YLJRSC85ujMogzLe2Q5SUZF9XMc\napcg6yFC97QgUrdK/XW8yw8MZxFXH/cw8auQzF08lgVi08pVAUtGxYCHHHLQc1Qh\n6tejnNOuf9RT2Sj8V97lP1JKu8gmJEdTHHO6z8a0MM1eccdWvEk4JebFEAl42dQd\nXM1u7duRXKFTFFaqjSeppo4bAgMBAAGjPzA9MA4GA1UdDwEB/wQEAwIBBjAMBgNV\nHRMEBTADAQH/MB0GA1UdDgQWBBQWhC37G+e0HmiY00IgGm5+T5FXAjANBgkqhkiG\n9w0BAQUFAAOCAgEAe+MNYzpkIG3M/Cy46dar29erJilHogxW7XXMlZlSNssg+xE0\nF0JOdQWw2OS4sIQvmBm5+9A5bHIGGMlcinp0CDdIaf0ioV3F13gT8ChCQcPJwzkJ\nB9Sh+DciaeTfMlVvwny5k/GyN3XMrtIzlow29wHt42TpC2hbEKoBNpl8z5qUXf0a\nWWGiZRV9nhdk1J9TmAH9cVfQXUARFj8/RNKvyfwIMn12+NVD6Nw2aAfDTsOWl1fG\nfTZe23Ct/q7UiJ21pGDWo2K+fPk0Hvy79EpyxYMeRmjDDpeDGD3TDgoRNXxplcWr\nKvXIORBNDIkwKYlJG0SXkfTqZSEbPwpDcoIcbRFd4CJFX2FMoqb636NGuuGBYGwy\ntPzk3DYF5DP36493SaqNCu9IiuZBl347q0OH8ghgC2/XWWb9K7svzjNPcuC217NT\nV4nwO7xu4hC/cz5ij6UI6VNnwU7BLkJDp7Kk+RaLQu7cNH9Is5DbJOLI5FM1U5zq\nN4XPv5gGNUcm165t3YSpY1gmQfV1Mi5hnk+TUlL2WiPrwaBzJiUiQpGRkYBP/4jO\nXnPnlsLtCRL3dpapeWKQSYGDnwwyMuJbyt1INKyHjnGVrkzkfHgdp1HDvRH6AtGV\niXMIRiKJaQDPT4DBTVuUxMqZUZgvDb19VGTUCtonWac3u1YM0AaicrkSuVs=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEWDCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBzjELMAkGA1UEBhMCWkEx\nFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEWMBQGA1UEBxMNU29tZXJzZXQgV2VzdDEq\nMCgGA1UEChMhU291dGggQWZyaWNhbiBQb3N0IE9mZmljZSBMaW1pdGVkMRowGAYD\nVQQLExFTQVBPIFRydXN0IENlbnRyZTEdMBsGA1UEAxMUU0FQTyBDbGFzcyAyIFJv\nb3QgQ0ExKTAnBgkqhkiG9w0BCQEWGnBraWFkbWluQHRydXN0Y2VudHJlLmNvLnph\nMB4XDTEwMDkxNTAwMDAwMFoXDTMwMDkxNDAwMDAwMFowgc4xCzAJBgNVBAYTAlpB\nMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxFjAUBgNVBAcTDVNvbWVyc2V0IFdlc3Qx\nKjAoBgNVBAoTIVNvdXRoIEFmcmljYW4gUG9zdCBPZmZpY2UgTGltaXRlZDEaMBgG\nA1UECxMRU0FQTyBUcnVzdCBDZW50cmUxHTAbBgNVBAMTFFNBUE8gQ2xhc3MgMiBS\nb290IENBMSkwJwYJKoZIhvcNAQkBFhpwa2lhZG1pbkB0cnVzdGNlbnRyZS5jby56\nYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALd8aXxg9Wwm9KocF39d\n1BFL5/Pa53On+qRCSWg/2qVAXAZoX07Mvb6BOCQtzCagRG0DyyPgu96FU0uUX197\nqsgal/7XI5PtsGq92PwAPrOSBOBLvk87mKed7c1j8IHnbJjUbGBVAOW5POY0lV3g\n/XGH6f+B7uV3bxj/88l8pZXdgtwU2aLhvs0nc7tFWz90sWJ4ZhAiLPVo8xeIFjua\nGx37FK4NuvKQVaLVMNYrlTLHOW57ZdJ3OM5uVqXZI6s4sjtRhcAdG7cRLwVpR9gC\nypKo4TPehQib7ZDV2CGZcb+29XPvZwiYZNLyKnpLIRbhH1hh3pFHHyGfH/6MI4aD\nGCcCAwEAAaM/MD0wDgYDVR0PAQH/BAQDAgEGMAwGA1UdEwQFMAMBAf8wHQYDVR0O\nBBYEFKudI5P9HzNKMi2qJFryLWSpAZpBMA0GCSqGSIb3DQEBBQUAA4IBAQBWUlG5\nDwLh9i6csTFapvjOvO4ChBUJ8ShSX+fhLL3beQp6v+tintWGRynudDDsTHW1HuOq\nM++t4WpMvzcBvlWDTKlS2DeYUG9o3UdBtywwyG5MByzG00m5tVzSy8zUNsYHDRhP\nP2MAxOy2iPsBZGOt0fd3fGRUKxI9NBWF8KC6eSlfmJtC6q7BqJ8TiYpt6bg4yWHt\nYOz3KlgFm6FgeIMX4X5f6P144GtWKoZ2rlvCXutF5DC4Me1ksV0uwD2ADccnE9N2\n4ob73NuACoHh/Qj5C8QxtGNb54wz5Qa2Umqz1+lr4zJ4MmaUTt2Nd23TJChbVGF3\nAmd1lEtXS+ZsxTlv\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDNzCCAh+gAwIBAgICJxwwDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCR1Ix\nHTAbBgNVBAoTFEF0aGVucyBFeGNoYW5nZSBTLkEuMRYwFAYDVQQDEw1BVEhFWCBS\nb290IENBMB4XDTEwMTAxODE1NTYwM1oXDTMwMTAxNzIxMDAwMFowRDELMAkGA1UE\nBhMCR1IxHTAbBgNVBAoTFEF0aGVucyBFeGNoYW5nZSBTLkEuMRYwFAYDVQQDEw1B\nVEhFWCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzRo9\nMLWzOLK/eruuodbXhfAiOqSJacThYgTJcNM8MxLi5jjld6QkRGQNt65MWt3hGAY+\n7ZtaBfXh3hLtNircR9mRUZntsb9qc6EKCCSoio0cC1nTv3AjVUSgjDDFzm1PsOy+\n84wx3wpa3NNXXAWgM5U7l49UC7j1a33Hxay1eY4GOPGoKVU9mjbQJ180ahJ4FyjZ\nmEns2VpS2iY6+u5MpiaOqD5VH7If4bWb+To19u2RHP0LECT9H/nT4wAlsQslwLd9\nmjwHOoAL1qj+kUXowdLFIm/T5XEftiw2tFig7c1KaORqV/ShdezXAJnV9plc607J\nu9cao0VZAA+MO9t0NQIDAQABozMwMTAPBgNVHRMBAf8EBTADAQH/MBEGA1UdDgQK\nBAhD4oDou9K3wTALBgNVHQ8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAD8BY3UH\nMitfdf92jtOpuG/MUD2EV08og+h8o26ivPfCuq46q07QD5IouN1bLNvl1h86k+GR\nDteqXwFhLD5hT96VFU3MPeoy4qP++Bap8rwp/CmefXKlXaFrAtVfSPSgO8sYRvA9\nF1WD0ClhkbuaQUnRE75BlPI+wySrn8drQpBCeX5aUfs8XgshH8vZSBMVsWp/A8TR\nulHScImqCEqHHPZ6mLHUUQVVxpAXb8PgBMB69C8YolZCcy62spvROb4JwgJKJBf5\n96y9cQe/leKX5aGECI2y4kSh3IkwO6gMBXpddgBPHm9xfys52kVCOTHSqTJA1Dhj\nE5Y3mkld2cf9uEw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg\nQ2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow\nTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw\nHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB\nBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y\nZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E\nN3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9\ntznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX\n0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c\n/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X\nKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY\nzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS\nO1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D\n34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP\nK9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3\nAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv\nTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj\nQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV\ncSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS\nIGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2\nHJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa\nO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv\n033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u\ndmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE\nkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41\n3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD\nu79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq\n4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg\nQ2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow\nTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw\nHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB\nBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr\n6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV\nL4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91\n1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx\nMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ\nQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB\narcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr\nUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi\nFRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS\nP/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN\n9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP\nAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz\nuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h\n9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s\nA20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t\nOluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo\n+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7\nKcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2\nDISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us\nH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ\nI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7\n5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h\n3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz\nY11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy\nMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl\nZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS\nb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy\neuuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO\nbntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw\nWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d\nMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE\n1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD\nVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/\nzQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB\nBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF\nBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV\nv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG\nE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u\nuSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW\niAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v\nGVCJYMzpJJUPwssd8m92kMfMdcGWxZ0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEYDCCA0igAwIBAgICATAwDQYJKoZIhvcNAQELBQAwWTELMAkGA1UEBhMCVVMx\nGDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDENMAsGA1UECxMERlBLSTEhMB8GA1UE\nAxMYRmVkZXJhbCBDb21tb24gUG9saWN5IENBMB4XDTEwMTIwMTE2NDUyN1oXDTMw\nMTIwMTE2NDUyN1owWTELMAkGA1UEBhMCVVMxGDAWBgNVBAoTD1UuUy4gR292ZXJu\nbWVudDENMAsGA1UECxMERlBLSTEhMB8GA1UEAxMYRmVkZXJhbCBDb21tb24gUG9s\naWN5IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2HX7NRY0WkG/\nWq9cMAQUHK14RLXqJup1YcfNNnn4fNi9KVFmWSHjeavUeL6wLbCh1bI1FiPQzB6+\nDuir3MPJ1hLXp3JoGDG4FyKyPn66CG3G/dFYLGmgA/Aqo/Y/ISU937cyxY4nsyOl\n4FKzXZbpsLjFxZ+7xaBugkC7xScFNknWJidpDDSPzyd6KgqjQV+NHQOGgxXgVcHF\nmCye7Bpy3EjBPvmE0oSCwRvDdDa3ucc2Mnr4MrbQNq4iGDGMUHMhnv6DOzCIJOPp\nwX7e7ZjHH5IQip9bYi+dpLzVhW86/clTpyBLqtsgqyFOHQ1O5piF5asRR12dP8Qj\nwOMUBm7+nQIDAQABo4IBMDCCASwwDwYDVR0TAQH/BAUwAwEB/zCB6QYIKwYBBQUH\nAQsEgdwwgdkwPwYIKwYBBQUHMAWGM2h0dHA6Ly9odHRwLmZwa2kuZ292L2ZjcGNh\nL2NhQ2VydHNJc3N1ZWRCeWZjcGNhLnA3YzCBlQYIKwYBBQUHMAWGgYhsZGFwOi8v\nbGRhcC5mcGtpLmdvdi9jbj1GZWRlcmFsJTIwQ29tbW9uJTIwUG9saWN5JTIwQ0Es\nb3U9RlBLSSxvPVUuUy4lMjBHb3Zlcm5tZW50LGM9VVM/Y0FDZXJ0aWZpY2F0ZTti\naW5hcnksY3Jvc3NDZXJ0aWZpY2F0ZVBhaXI7YmluYXJ5MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQUrQx6dVzl85jEeZgOrCj9l/TnAvwwDQYJKoZIhvcNAQELBQAD\nggEBAI9z2uF/gLGH9uwsz9GEYx728Yi3mvIRte9UrYpuGDco71wb5O9Qt2wmGCMi\nTR0mRyDpCZzicGJxqxHPkYnos/UqoEfAFMtOQsHdDA4b8Idb7OV316rgVNdF9IU+\n7LQd3nyKf1tNnJaK0KIyn9psMQz4pO9+c+iR3Ah6cFqgr2KBWfgAdKLI3VTKQVZH\nvenAT+0g3eOlCd+uKML80cgX2BLHb94u6b2akfI8WpQukSKAiaGMWMyDeiYZdQKl\nDn0KJnNR6obLB6jI/WNaNZvSr79PMUjBhHDbNXuaGQ/lj/RqDG8z2esccKIN47lQ\nA2EC/0rskqTcLe4qNJMHtyznGI8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIJmDCCB4CgAwIBAgIBCjANBgkqhkiG9w0BAQwFADCCAR4xPjA8BgNVBAMTNUF1\ndG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s\nYW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz\ndHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0\naWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh\nIGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ\nKoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NDEz\nNloXDTMwMTIyMzIzNTk1OVowggEeMT4wPAYDVQQDEzVBdXRvcmlkYWQgZGUgQ2Vy\ndGlmaWNhY2lvbiBSYWl6IGRlbCBFc3RhZG8gVmVuZXpvbGFubzELMAkGA1UEBhMC\nVkUxEDAOBgNVBAcTB0NhcmFjYXMxGTAXBgNVBAgTEERpc3RyaXRvIENhcGl0YWwx\nNjA0BgNVBAoTLVNpc3RlbWEgTmFjaW9uYWwgZGUgQ2VydGlmaWNhY2lvbiBFbGVj\ndHJvbmljYTFDMEEGA1UECxM6U3VwZXJpbnRlbmRlbmNpYSBkZSBTZXJ2aWNpb3Mg\nZGUgQ2VydGlmaWNhY2lvbiBFbGVjdHJvbmljYTElMCMGCSqGSIb3DQEJARYWYWNy\nYWl6QHN1c2NlcnRlLmdvYi52ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC\nggIBAME77xNS8ZlW47RsBeEaaRZhJoZ4rw785UAFCuPZOAVMqNS1wMYqzy95q6Gk\nUO81ER/ugiQX/KMcq/4HBn83fwdYWxPZfwBfK7BP2p/JsFgzYeFP0BXOLmvoJIzl\nJb6FW+1MPwGBjuaZGFImWZsSmGUclb51mRYMZETh9/J5CLThR1exStxHQptwSzra\nzNFpkQY/zmj7+YZNA9yDoroVFv6sybYOZ7OxNDo7zkSLo45I7gMwtxqWZ8VkJZkC\n8+p0dX6mkhUT0QAV64Zc9HsZiH/oLhEkXjhrgZ28cF73MXIqLx1fyM4kPH1yOJi/\nR72nMwL7D+Sd6mZgI035TxuHXc2/uOwXfKrrTjaJDz8Jp6DdessOkxIgkKXRjP+F\nK3ze3n4NUIRGhGRtyvEjK95/2g02t6PeYiYVGur6ruS49n0RAaSS0/LJb6XzaAAe\n0mmO2evnEqxIKwy2mZRNPfAVW1l3wCnWiUwryBU6OsbFcFFrQm+00wOicXvOTHBM\naiCVAVZTb9RSLyi+LJ1llzJZO3pq3IRiiBj38Nooo+2ZNbMEciSgmig7YXaUcmud\nSVQvLSL+Yw+SqawyezwZuASbp7d/0rutQ59d81zlbMt3J7yB567rT2IqIydQ8qBW\nk+fmXzghX+/FidYsh/aK+zZ7Wy68kKHuzEw1Vqkat5DGs+VzAgMBAAGjggLbMIIC\n1zASBgNVHRMBAf8ECDAGAQH/AgECMDcGA1UdEgQwMC6CD3N1c2NlcnRlLmdvYi52\nZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0wMB0GA1UdDgQWBBStuyIdxuDS\nAaj9dlBSk+2YwU2u0zCCAVAGA1UdIwSCAUcwggFDgBStuyIdxuDSAaj9dlBSk+2Y\nwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0b3JpZGFkIGRlIENlcnRpZmlj\nYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xhbm8xCzAJBgNVBAYTAlZFMRAw\nDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0cml0byBDYXBpdGFsMTYwNAYD\nVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25p\nY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEgZGUgU2VydmljaW9zIGRlIENl\ncnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkqhkiG9w0BCQEWFmFjcmFpekBz\ndXNjZXJ0ZS5nb2IudmWCAQowCwYDVR0PBAQDAgEGMDcGA1UdEQQwMC6CD3N1c2Nl\ncnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0wMFQGA1UdHwRN\nMEswJKAioCCGHmh0dHA6Ly93d3cuc3VzY2VydGUuZ29iLnZlL2xjcjAjoCGgH4Yd\nbGRhcDovL2FjcmFpei5zdXNjZXJ0ZS5nb2IudmUwNwYIKwYBBQUHAQEEKzApMCcG\nCCsGAQUFBzABhhtodHRwOi8vb2NzcC5zdXNjZXJ0ZS5nb2IudmUwQAYDVR0gBDkw\nNzA1BgVghl4BAjAsMCoGCCsGAQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRlLmdv\nYi52ZS9kcGMwDQYJKoZIhvcNAQEMBQADggIBABxZEOVepFHBR7tlsgtV4i+poye8\n4TyKx2wDVqOpKaKbipXYH/e2EmAWvnr0/QOBT/2BgapPgXAeLu/AkhJ7uw+FiMT5\nHUG1uiQqwygmE8r5APvXw1z5aOkbwRgiyaJsZMP4OcNOId3Wwt7ltizJXDjw3l5q\n5Cf0uDPEy2GSM1OozPydzVP7KAvv7X+wj3QitjVXgKiuBa4pCjuypP0949TBkPY/\nzrzkRP7RwX4oL/0AJDIgiMRvGHuRDkiQvJZiYIFtFAAaUbq1XWmNYUccLKxORSCp\nSEWjh0mjeJDdNkJ/2HZv/W2DAcb5f5ggf5YuImCroifAsDUk0Mm/M5kiUw5uH2JM\nJvwkM8rBA8ypF2FjMyTMaEDvr6LihcOIMNNFG+5W6lYKDwpHmzBZ2EnRMJAMJyom\nCChcMh8n160LSeUXUWPP5g07YFEavUMJUOaRtWPmZJeqC5cTAQaGXKUflb5Qjguy\n0mR/26tM5kPG5IWNav6N/ruUVR6RUycI07pnPTqhycHFFLr5Q1zFjiGMgqL9KjIl\n1RaMFVbAmPwuso4ZpBZxw0vdcf5x7CId8MGMmIGHtL8CuMQwMUfCwLCvezNjCt2s\nRZvBzICH9NmYXpyG/poE/2ZK/HthVL5XYwUHxqcBdVnkbjk7APSqnfOfiL/P0SUr\n339z7RaGqZBlD3Ap\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDkDCCAnigAwIBAgIQHKAtwVI7am2LXB+VSu2sMDANBgkqhkiG9w0BAQUFADBi\nMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu\nMTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp\ndHkwHhcNMTEwMTAxMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV\nUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO\nZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz\nc7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP\nOCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl\nmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF\nBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4\nqY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjQjBA\nMB0GA1UdDgQWBBQhMMn7ANdOmNqHqirQpy6xQDGnTDAOBgNVHQ8BAf8EBAMCAQYw\nDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAwomEoOiMZv3/EwUb\nwzqOmEmK+KoAXCb9cmqjfhIblK5U+CGPp5NP9xbvubmzMsAlITFmNywJsP4ysDfs\nPLjOjwiqCJAHXHXV4U4sywIk6aJe6fV4NSIGHPIfiLHhXMyWVPpvScyN8VYD7c8s\nnyfe5cqDRL5GQPlXLtJ/MS3Og9z+cGuE0KOf/5fQqNcC7LEs8O9zOD2ZrMRPAb/V\naurGLjIpFwrL5mme0Uq19t+OGfiV6UWpDs1tQVkgnnPGbHEcnNRNMKhzCaAV86BF\nJsNb/bu52C3XH/UFMBn2rg+OYo/fyE+G2R1hFrPJ8Lv7x/WvASJH7NjazxzzU2a6\nUwkB+Q==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID0DCCArigAwIBAgIQIKTEf93f4cdTYwcTiHdgEjANBgkqhkiG9w0BAQUFADCB\ngTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV\nBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xMTAxMDEwMDAw\nMDBaFw0zMDEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl\nYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P\nRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0\naG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3\nUcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI\n2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8\nQ5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp\n+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+\nDT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O\nnKVIrLsm9wIDAQABo0IwQDAdBgNVHQ4EFgQUC1jli8ZMFTekQKkwqSG+RzZaVv8w\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD\nggEBAC/JxBwHO89hAgCx2SFRdXIDMLDEFh9sAIsQrK/xR9SuEDwMGvjUk2ysEDd8\nt6aDZK3N3w6HM503sMZ7OHKx8xoOo/lVem0DZgMXlUrxsXrfViEGQo+x06iF3u6X\nHWLrp+cxEmbDD6ZLLkGC9/3JG6gbr+48zuOcrigHoSybJMIPIyaDMouGDx8rEkYl\nFo92kANr3ryqImhrjKGsKxE5pttwwn1y6TPn/CbxdFqR5p2ErPioBhlG5qfpqjQi\npKGfeq23sqSaM4hxAjwu1nqyH6LKwN0vEJT9s4yEIHlG1QXUEOTS22RPuFvuG8Ug\nR1uUq27UlTMdphVx8fiUylQ5PsE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIINDCCBhygAwIBAgIRAP11BI16YIaTaUyqADxl0z0wDQYJKoZIhvcNAQELBQAw\ngaYxCzAJBgNVBAYTAkNIMTswOQYDVQQKEzJUaGUgRmVkZXJhbCBBdXRob3JpdGll\ncyBvZiB0aGUgU3dpc3MgQ29uZmVkZXJhdGlvbjERMA8GA1UECxMIU2VydmljZXMx\nIjAgBgNVBAsTGUNlcnRpZmljYXRpb24gQXV0aG9yaXRpZXMxIzAhBgNVBAMTGlN3\naXNzIEdvdmVybm1lbnQgUm9vdCBDQSBJMB4XDTExMDIxNTA5MDAwMFoXDTM1MDIx\nNTA4NTk1OVowgaYxCzAJBgNVBAYTAkNIMTswOQYDVQQKEzJUaGUgRmVkZXJhbCBB\ndXRob3JpdGllcyBvZiB0aGUgU3dpc3MgQ29uZmVkZXJhdGlvbjERMA8GA1UECxMI\nU2VydmljZXMxIjAgBgNVBAsTGUNlcnRpZmljYXRpb24gQXV0aG9yaXRpZXMxIzAh\nBgNVBAMTGlN3aXNzIEdvdmVybm1lbnQgUm9vdCBDQSBJMIICIjANBgkqhkiG9w0B\nAQEFAAOCAg8AMIICCgKCAgEAyA5y9AEvhnsLwmOwjWjtHz3euYObXKFdug82JxEE\nrQZUILceoObOvvCZaXIZNWRmMY0svY5CCp/GyqmQLNq8hTAD2TKWlvC+oCINJGzU\nxn9aTFEkLVRyCHwz6cwox2ZlI2lrlbTrvuOH52PX5PsHrRKS6+fkCkOyqd/HkLwm\nW5H5o7eHnJS5EI2IxVhcMrwW7A5XT/6nk3iP4MU5uweIYMFUZeuHvp8xl3E8+ovI\ng2xSluCswO/LaQiVW+Dgu68npMIX8VGfhHZh2CTi/mFtZDVJ6jnEIWK9zOIC/0hr\nOK9px7mSLYIRjb0LiYUq6re0ss1L69H6qvDgTAk8Td/2MR2GMKhBiFdwLCdR3s+L\nTj8C8lClF+BnG3IMQTEfAaKWPjzbAradlOYCTvPwGYKyCCMT65HNUdOqRsJzmJg/\nusPumvz6za9yCjcTj/mgULPq+z8svPpjVTX00ry4cdKR6+nKylzsUWaonlkFIi+j\nGttP4EViIzxdVfswlSs0os+ntEvAM8k0UZ3TsyvfxeosLMffRB+2jbn+81zNNy+w\nbJxKCL3o9db6cOVpMjdcXwvLP+SIAszKs3gvfb9IsyGwH4h5m1qKcdghhCkPSgQx\nKr0NIUTOdJ0m00kd+Iao5RJ3xcBzDFCDapBrocr40JXZNYbHEaM7FMfLhlhWDfuD\n9wECAwEAAaOCAlkwggJVMA8GA1UdEwEB/wQFMAMBAf8wgZsGA1UdIASBkzCBkDCB\njQYIYIV0AREDAQAwgYAwQwYIKwYBBQUHAgEWN2h0dHA6Ly93d3cucGtpLmFkbWlu\nLmNoL2Nwcy9DUFNfMl8xNl83NTZfMV8xN18zXzFfMC5wZGYwOQYIKwYBBQUHAgIw\nLRorVGhpcyBpcyB0aGUgU3dpc3MgR292ZXJubWVudCBSb290IENBIEkgQ1BTLjCB\njgYDVR0fBIGGMIGDMIGAoH6gfIZ6bGRhcDovL2FkbWluZGlyLmFkbWluLmNoOjM4\nOS9jbj1Td2lzcyUyMEdvdmVybm1lbnQlMjBSb290JTIwQ0ElMjBJLG91PUNlcnRp\nZmljYXRpb24lMjBBdXRob3JpdGllcyxvdT1TZXJ2aWNlcyxvPUFkbWluLGM9Q0gw\nHQYDVR0OBBYEFLUbg7s7T7LS++UDjtRhXdEajrCiMA4GA1UdDwEB/wQEAwIBBjCB\n4wYDVR0jBIHbMIHYgBS1G4O7O0+y0vvlA47UYV3RGo6woqGBrKSBqTCBpjELMAkG\nA1UEBhMCQ0gxOzA5BgNVBAoTMlRoZSBGZWRlcmFsIEF1dGhvcml0aWVzIG9mIHRo\nZSBTd2lzcyBDb25mZWRlcmF0aW9uMREwDwYDVQQLEwhTZXJ2aWNlczEiMCAGA1UE\nCxMZQ2VydGlmaWNhdGlvbiBBdXRob3JpdGllczEjMCEGA1UEAxMaU3dpc3MgR292\nZXJubWVudCBSb290IENBIEmCEQD9dQSNemCGk2lMqgA8ZdM9MA0GCSqGSIb3DQEB\nCwUAA4ICAQAl2t94sCbcn5nrM5zJRbpcY1KNbgNzqnRIxQ0L0hcMLAvSxiWD1FTN\nB4FUL2d2Jafp13+WR3ekHZtF//HY9p5HDnSME8TyvtYHKBg8mHXB2+uSiCbmBmSO\n+dL94pk1gdHYdRe1c+rd6BgilRYZClkqItyGWkNPJWg2qdiTAI9excNhhvDSFAmV\nUcR+2FLusI2KiHGl1yin9NwGWCVexFUYCJV0fLgB507Y1vZ8IENIDaPg3lTEqF8A\nSUPTRTuCZW7ui6MBIlaa8c4p5QzEa+3nTvixVYGtcf+E+whX5kfKrYf4Rvj68DWE\n7bTYiJcid6SPFsg8Z9HhbgSse482zd6lCKwqjfWnHZ/Hw5EhQqOGgbkq2LHpOB1U\nCJg5ChHKMg4zzfRM6qhKBukYPkHGz6D24CtrII6nIALrMEGBsOjkrqQYiSvfFPAS\nKW14+k1E+7I05a/zjjX3w84sCxi00HmPE78Di2a4tWHUrA79eD0JrbXSLE9WQZmI\nRAx+Z+Nkn/paKlh3UWmxzSyapzQQBXT6bkVjy4tSrUeRohLIoiYExdAiHgOzspI3\nVFf9iYN1A20tO7PxpKIQfJyTjaNQhDmLlVlB9gJ2Boq8DpDn2TrrrSZeV1PRb8h1\n4KuRe2uhf/kbUKjc/k0G4RWKpBDrHgbPVEgVlii2Ix8a43ylj/o3Vw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIIODCCBiCgAwIBAgIQDp8XmaWxPZzL7Abro/AOaTANBgkqhkiG9w0BAQsFADCB\npzELMAkGA1UEBhMCQ0gxOzA5BgNVBAoTMlRoZSBGZWRlcmFsIEF1dGhvcml0aWVz\nIG9mIHRoZSBTd2lzcyBDb25mZWRlcmF0aW9uMREwDwYDVQQLEwhTZXJ2aWNlczEi\nMCAGA1UECxMZQ2VydGlmaWNhdGlvbiBBdXRob3JpdGllczEkMCIGA1UEAxMbU3dp\nc3MgR292ZXJubWVudCBSb290IENBIElJMB4XDTExMDIxNjA5MDAwMFoXDTM1MDIx\nNjA4NTk1OVowgacxCzAJBgNVBAYTAkNIMTswOQYDVQQKEzJUaGUgRmVkZXJhbCBB\ndXRob3JpdGllcyBvZiB0aGUgU3dpc3MgQ29uZmVkZXJhdGlvbjERMA8GA1UECxMI\nU2VydmljZXMxIjAgBgNVBAsTGUNlcnRpZmljYXRpb24gQXV0aG9yaXRpZXMxJDAi\nBgNVBAMTG1N3aXNzIEdvdmVybm1lbnQgUm9vdCBDQSBJSTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAKksEu2/wCLphugcN4KDm2gFbxbjiKgBD8txnn9H\nkEvMJXfI8NdpLpFoVyGysgchM+5MpDclmEy0RjJO1vlri1GK7yw38pjV9dS0t+cA\nyu/BE16Uq267nL36a4+r+B42Vmk4ZjrQ9DMNADkCqMUcCyG3XCAMYdCtrs6OXtk6\n6d7/R3x4Vw4ccfRgHN3bmhgpr9mAo5+FhGMzke+9dO7dA3rI+uCE5tm9Tn76bk92\n0V0+qOiHRZB5862u9cJdEU0p94gTydWTcwGr3e39r3f7aU7vj1Icz/UsWmzs/oKb\n23w5q3UjfjiQT5SOLWJYnvfncvyUW3JWxZ2jrqu1tsDXdlAAPD9HiJJaYNS/Mhum\nlEANdnnpPM7ksx3HjPXohjG52CtQSoASidcsUIDmZy+2k5ytrAVSIlMgmQ69l8bh\n2nOpHYnyxFnmh+ZWKw6VAhqHxnn+mWrpdOzwEvkUKCCVljovXVe1b/+TvLYoaiyk\nKHhGYa9BJKTz+gSO8YoZopFz4nePtKf5nP9uUey9H5YT6GORXodob+vYfC4QT1AY\nkMe3dO8zwIHfM+MakytVBCx80iu3Ywz+rXu9tjqXuT0DI3RzA6YsWQBs1dXo7K9C\nzNN/cItgYOeyoLaKUkz+CpbLzzqwWAjuHELJhndCbj+0rJAAWEIcQMRuuEXIvDM2\n370nAgMBAAGjggJcMIICWDAPBgNVHRMBAf8EBTADAQH/MIGdBgNVHSAEgZUwgZIw\ngY8GCGCFdAERAxUBMIGCMEQGCCsGAQUFBwIBFjhodHRwOi8vd3d3LnBraS5hZG1p\nbi5jaC9jcHMvQ1BTXzJfMTZfNzU2XzFfMTdfM18yMV8xLnBkZjA6BggrBgEFBQcC\nAjAuGixUaGlzIGlzIHRoZSBTd2lzcyBHb3Zlcm5tZW50IFJvb3QgQ0EgSUkgQ1BT\nLjCBjwYDVR0fBIGHMIGEMIGBoH+gfYZ7bGRhcDovL2FkbWluZGlyLmFkbWluLmNo\nOjM4OS9jbj1Td2lzcyUyMEdvdmVybm1lbnQlMjBSb290JTIwQ0ElMjBJSSxvdT1D\nZXJ0aWZpY2F0aW9uJTIwQXV0aG9yaXRpZXMsb3U9U2VydmljZXMsbz1BZG1pbixj\nPUNIMB0GA1UdDgQWBBTlhG+JaT12ABd/wau9rl/BfbrhYjAOBgNVHQ8BAf8EBAMC\nAQYwgeMGA1UdIwSB2zCB2IAU5YRviWk9dgAXf8Grva5fwX264WKhga2kgaowgacx\nCzAJBgNVBAYTAkNIMTswOQYDVQQKEzJUaGUgRmVkZXJhbCBBdXRob3JpdGllcyBv\nZiB0aGUgU3dpc3MgQ29uZmVkZXJhdGlvbjERMA8GA1UECxMIU2VydmljZXMxIjAg\nBgNVBAsTGUNlcnRpZmljYXRpb24gQXV0aG9yaXRpZXMxJDAiBgNVBAMTG1N3aXNz\nIEdvdmVybm1lbnQgUm9vdCBDQSBJSYIQDp8XmaWxPZzL7Abro/AOaTANBgkqhkiG\n9w0BAQsFAAOCAgEAgzdXdck4UL9BBpZwwtnH17BaAM2jQE/T0vmKh5GyictdpLxv\nTz5U9so8s8RMi8c+9NnEYt3HVZ7R+dJE5x5Pz+juKxyoAfAzB/vhOxTTz1CRXtjq\nQsZ5WIWq+9zbcMqV+fQOYgJwaUQtaE/RcOooUma3cd4l6KGnb7ChJsfXyiBk3MBz\nPBCiFB70rcE+FJA5NmOIbyjgYKWR92Lkms/StXGeXTv2mSztkToInLSEhUnj4bqm\ntmiztrZPS1xTCldsoQeS9mKeqPqK1vNrpw+yK2a9r0JHCE/o13yfhg/6WoO+LW8A\nBLV2hxav3U86lrQ0V7fi/0H/3kIcZsWF68JyH7gcTu4X8mLvCgSsm6uh8u7uokAk\nHEfeQosYtKlXs088YjIcrWxErbzVHGM4Pckzpvu8KDdERuN6YvqASDXinhuIGUyz\nQf3ud+BZgBphHjWkQXqzwY1E6cUhWems00TKdoU2FEYKHhY0psQ0d8OCOEghAv4S\nbNrX6rDs9s0szPObCmOA0/ULfQQthA3C2Uwrl/HVVPePswrivVg8mfKvORuQ+Tvn\nt0XnWmp9wZ8UbzBXmBmgB0Pr7tEIhtdJnBIKADsPp0GxSquQs9S9CeeID54kDiv7\nYT1VmdNY5LjHffQVTWUOGHlBybvpmsFZGEQ0YtXoOHvKhRiYhnnNfbpH25U=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF7TCCA9WgAwIBAgIQP4vItfyfspZDtWnWbELhRDANBgkqhkiG9w0BAQsFADCB\niDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl\nZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp\nTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEw\nMzIyMjIwNTI4WhcNMzYwMzIyMjIxMzA0WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\nBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv\nc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm\naWNhdGUgQXV0aG9yaXR5IDIwMTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCygEGqNThNE3IyaCJNuLLx/9VSvGzH9dJKjDbu0cJcfoyKrq8TKG/Ac+M6\nztAlqFo6be+ouFmrEyNozQwph9FvgFyPRH9dkAFSWKxRxV8qh9zc2AodwQO5e7BW\n6KPeZGHCnvjzfLnsDbVU/ky2ZU+I8JxImQxCCwl8MVkXeQZ4KI2JOkwDJb5xalwL\n54RgpJki49KvhKSn+9GY7Qyp3pSJ4Q6g3MDOmT3qCFK7VnnkH4S6Hri0xElcTzFL\nh93dBWcmmYDgcRGjuKVB4qRTufcyKYMME782XgSzS0NHL2vikR7TmE/dQgfI6B0S\n/Jmpaz6SfsjWaTr8ZL22CZ3K/QwLopt3YEsDlKQwaRLWQi3BQUzK3Kr9j1uDRprZ\n/LHR47PJf0h6zSTwQY9cdNCssBAgBkm3xy0hyFfj0IbzA2j70M5xwYmZSmQBbP3s\nMJHPQTySx+W6hh1hhMdfgzlirrSSL0fzC/hV66AfWdC7dJse0Hbm8ukG1xDo+mTe\nacY1logC8Ea4PyeZb8txiSk190gWAjWP1Xl8TQLPX+uKg09FcYj5qQ1OcunCnAfP\nSRtOBA5jUYxe2ADBVSy2xuDCZU7JNDn1nLPEfuhhbhNfFcRf2X7tHc7uROzLLoax\n7Dj2cO2rXBPB2Q8Nx4CyVe0096yb5MPa50c8prWPMd/FS6/r8QIDAQABo1EwTzAL\nBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUci06AjGQQ7kU\nBU7h6qfHMdEjiTQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIB\nAH9yzw+3xRXbm8BJyiZb/p4T5tPw0tuXX/JLP02zrhmu7deXoKzvqTqjwkGw5biR\nnhOBJAPmCf0/V0A5ISRW0RAvS0CpNoZLtFNXmvvxfomPEf4YbFGq6O0JlbXlccmh\n6Yd1phV/yX43VF50k8XDZ8wNT2uoFwxtCJJ+i92Bqi1wIcM9BhS7vyRep4TXPw8h\nIr1LAAbblxzYXtTFC1yHblCk6MM4pPvLLMWSZpuFXst6bJN8gClYW1e1QGm6CHmm\nZGIVnYeWRbVmIyADixxzoNOieTPgUFmG2y/lAiXqcyqfABTINseSO+lOAOzYVgm5\nM0kS0lQLAausR7aRKX1MtHWAUgHoyoL2n8ysnI8X6i8msKtyrAv+nlEex0NVZ09R\ns1fWtuzuUrc66U7h14GIvE+OdbtLqPA1qibUZ2dJsnBMO5PcHd94kIZysjik0dyS\nTclY6ysSXNQ7roxrsIPlAT/4CTL2kzU0Iq/dNw13CYArzUgA8YyZGUcFAenRv9FO\n0OYoQzeZpApKCNmacXPSqs0xE2N2oTdvkjgefRI8ZjLny23h/FKJ3crWZgWalmG+\noijHHKOnNlA8OqTfSm7mhzvO6/DggTedEzxSjr25HTTGHdUKaj2YKXCMiSrRq4IQ\nSB/c9O+lxbtVGjhjhE63bK2VVOxlIhBJF7jAHscPrFRH\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFoTCCA4mgAwIBAgIBATANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTDEZ\nMBcGA1UEChMQRGlnaWRlbnRpdHkgQi5WLjEkMCIGA1UEAxMbRGlnaWRlbnRpdHkg\nTDMgUm9vdCBDQSAtIEcyMB4XDTExMDQyOTEwNDQxOVoXDTMxMTExMDEwNDQxOVow\nTjELMAkGA1UEBhMCTkwxGTAXBgNVBAoTEERpZ2lkZW50aXR5IEIuVi4xJDAiBgNV\nBAMTG0RpZ2lkZW50aXR5IEwzIFJvb3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEB\nBQADggIPADCCAgoCggIBALgRo0XeAUdWDWK4jrpYZlz6MsZrgG64f/hT337fYqjB\nV0+aRSXISkUtUzgksyCsT+qt/5oQr3/iDsq0DiQlkc52jhCpL5lTp5BLBItterlB\nG9MBeYyfQWu5kNeBEhoHltAJr+nkaiFTgLiGnmJoQ62zahX69m0DMmo1sVATSMd6\ntSETnASc2pP5aivBpxj99sB+Wfb75w4Rtdwj6hzvZwVXzhfp8Xux0TIkjM9l59S8\nNhlwfKInIdaA0i0VT0q14FWQlVGTIYDznEQf/x1VVeTiEBGUFlPQ/q/z75e6RuJ3\nW8vWolkRiKbnVUHDkmUdIxRiFH8lciD2pIcpbwf8/uDQGNKX+RSONsboDBiX8XYc\n9CTa40r5t0wSGWfz8OFT+13kwHRjXyWRCtk+9DOs5At1X87mmLxUDZ2iMcUVVF0i\nHIs6VKYN0dcjOqw+qkoXZHYtDftU5euCPDlBQ53hrnlgz2bux3GDewxrCdueok1O\nRpNot/pn4dq/35GA2qOiia1ebMxLd3Vkb40k44iIC+M/6b+n5VZiDYN/vWphyJCJ\neFsMrxIq4pOtZOfZRS72sMirRe5wOG+7NT4W/quew2Yv874JYNVvgL1N26+N/gxg\nM2sP6J1rxDB3nyxQONCYaew36J4P5GLq+v8RRFTZ782TdZFM4YllppS5U/n5SWPF\nAgMBAAGjgYkwgYYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\nVR0OBBYEFCsjIGC9LYqIR6ytK74CObqY1OyYMEQGA1UdIAQ9MDswOQYEVR0gADAx\nMC8GCCsGAQUFBwIBFiNodHRwOi8vcGtpLmRpZ2lkZW50aXR5LmV1L3ZhbGlkYXRp\nZTANBgkqhkiG9w0BAQsFAAOCAgEAqf3vuo8bfjISZx1BDS2mi8/y9K1WeH4KmNib\nqNG0SywmrOTSf2c3vQmN5blzETpuCcdXZAchNPgOXSrYkXzxVFG8nPAMakL0PAFO\nk0VBPazzmEsecR4zWTL/fDDwXOThvi0uterdYiEOPbQNlfzJuNm6oPdip+3DA64I\nLEHV70NxOLcUcq4/9BR0R9jejFF5zu+xVKxwR5Z+LS7dm+6hAS4Z775YYHEtrZdb\nWmAwyzKCYk5W5WdqtNIxVHI/AtC8MDmPt0MJKh8mOwzHfB2bgGCEDuku0vkVu1vg\niqQA6eMp+yhbvTZFYCFDMf9woV9cg1uXfA23U1nsmLVO4imx1HxG4+jjQ+o6ljUf\nU/EEFiXjLPNooaaR3xX7vZ/mTp7CVGt+IlfjpJxcIiUfga+ZyN8RFUhD+LMzqSN/\nDjOPvEYdQ7Q7YPWXhRmiFrBV3BpwKWXa2X4JFzTribrpYZLY3jRjPEpVar/ahu3O\nM967U2/PHNqUT3ZUrGVVEFOayLhr3AbmuuVR1UF/H8TAQaFgkTTzE4LRoXfT90zk\nGf/XRJqwtbzcyl6P3M7xoGk24ESSLpn6vK+zx3g6VWbHa6XkaSbpNB0fKpcK6Xep\nd1tzSDKBv//R7IPFcINpnpgbw1ffkZUcgPyN6JaDBdOfeoh7+uhX8cGEKL3N1hzM\npeJJCnM=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE\nAwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw\nCQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ\nBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND\nVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb\nqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY\nHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo\nG2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA\nlHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr\nIA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/\n0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH\nk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47\n4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO\nm3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa\ncXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl\nuUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI\nKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls\nZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG\nAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2\nVuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT\nVfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG\nCCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA\ncgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA\nQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA\n7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA\ncgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA\nQwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA\nczAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu\naHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt\naW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud\nDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF\nBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp\nD70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU\nJyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m\nAM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD\nvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms\ntn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH\n7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h\nI6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA\nh1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF\nd3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H\npPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDHzCCAgegAwIBAgIEGZk8PzANBgkqhkiG9w0BAQUFADAiMQswCQYDVQQGEwJD\nTjETMBEGA1UEChMKQ0ZDQSBHVCBDQTAeFw0xMTA2MTMwODE1MDlaFw0yNjA2MDkw\nODE1MDlaMCIxCzAJBgNVBAYTAkNOMRMwEQYDVQQKEwpDRkNBIEdUIENBMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv3PGWiuMePZYt/zSF5ClK3TsgSyT\nzVLMbuQqyyShMeStMG7jmCIx1yGbn9UPNy9auziit3kmZ9YNxRcqnLlUBOENdYZu\n2MzFgGcbyIwtACaGPHp5Prapwk4gsDeXxoV2EoIK51S7i/49ruPsa1hD9qU361ii\nvZDE5fvKa8owbLd7ifYx0oz/T8KWJUOpcTUlCxjhrMijJLZxk4zxXfycEAV7/8Bb\n4LGXrR/Y/kX1wB+dW0c5HAb622aF2yQj6nvSOSD46yqyGlHzlFooAk6nXEduz/zZ\n6OZhWhYnxxUNmNno0wM1kCnfsi+NEHcjyLh60xFhavP/gZKl7EJLaE6A1wIDAQAB\no10wWzAfBgNVHSMEGDAWgBSMdlDOJdN5Kzz0bZ2a4Z4FT+g9JTAMBgNVHRMEBTAD\nAQH/MAsGA1UdDwQEAwIBxjAdBgNVHQ4EFgQUjHZQziXTeSs89G2dmuGeBU/oPSUw\nDQYJKoZIhvcNAQEFBQADggEBAL67lljU3YmJDyzN+mNFdg05gJqN+qhFYT0hVejO\naMcZ6cKxB8KLOy/PYYWQp1IXMjqvCgUVyMbO3Y6UJgb40GDus27UDbpa3augfFBy\nptWQk1bXWTnb6H+zlXhTgVJSX/SSgQLB+yK50QNXp37L+8BGvBN0TCgrdpJpH8FQ\nkRHFTN4LlIwXg4yvN4e06mtvolo1QWGFL5wXwPu5DqJhBkd2vJAJmHQN0ggvveQN\ncvGmX8N8wH3qvNOrIJHLXAWMnag1+jZWuwnzhF3W8eIsntl+8YKg4bcvfu35e6AA\nuLLeHXnhgfNSWZoUXefCEfOawzp4I75OZt6kOWnymDosCgA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBk\nMQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0\nYWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg\nQ0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2MjUwNzM4MTRaMGQxCzAJBgNVBAYT\nAmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp\nY2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvEr\njw0DzpPMLgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r\n0rk0X2s682Q2zsKwzxNoysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f\n2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJwDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVP\nACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpHWrumnf2U5NGKpV+GY3aF\ny6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1aSgJA/MTA\ntukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL\n6yxSNLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0\nuPoTXGiTOmekl9AbmbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrAL\nacywlKinh/LTSlDcX3KwFnUey7QYYpqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velh\nk6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3qPyZ7iVNTA6z00yPhOgpD/0Q\nVAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw\nFDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O\nBBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqh\nb97iEoHF8TwuMA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4R\nfbgZPnm3qKhyN2abGu2sEzsOv2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv\n/2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ82YqZh6NM4OKb3xuqFp1mrjX2lhI\nREeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLzo9v/tdhZsnPdTSpx\nsrpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcsa0vv\naGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciAT\nwoCqISxxOQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99n\nBjx8Oto0QuFmtEYE3saWmA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5W\nt6NlUe07qxS/TFED6F+KBZvuim6c779o+sjaC+NCydAXFJy3SuCvkychVSa1ZC+N\n8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TCrvJcwhbtkj6EPnNgiLx2\n9CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX5OfNeOI5\nwSsSnqaeG8XmDtkx2Q==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAw\nZzELMAkGA1UEBhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdp\ndGFsIENlcnRpZmljYXRlIFNlcnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290\nIEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcNMzEwNjI1MDg0NTA4WjBnMQswCQYD\nVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2Vy\ndGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYgQ0Eg\nMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7Bx\nUglgRCgzo3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD\n1ycfMQ4jFrclyxy0uYAyXhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPH\noCE2G3pXKSinLr9xJZDzRINpUKTk4RtiGZQJo/PDvO/0vezbE53PnUgJUmfANykR\nHvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8LiqG12W0OfvrSdsyaGOx9/\n5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaHZa0zKcQv\nidm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHL\nOdAGalNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaC\nNYGu+HuB5ur+rPQam3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f\n46Fq9mDU5zXNysRojddxyNMkM3OxbPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCB\nUWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDixzgHcgplwLa7JSnaFp6LNYth\n7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGGMB0G\nA1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED\nMB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWB\nbj2ITY1x0kbBbkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6x\nXCX5145v9Ydkn+0UjrgEjihLj6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98T\nPLr+flaYC/NUn81ETm484T4VvwYmneTwkLbUwp4wLh/vx3rEUMfqe9pQy3omywC0\nWqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7XwgiG/W9mR4U9s70\nWBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH59yL\nGn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm\n7JFe3VE/23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4S\nnr8PyQUQ3nqjsTzyP6WqJ3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VN\nvBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyAHmBR3NdUIR7KYndP+tiPsys6DXhyyWhB\nWkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/giuMod89a2GQ+fYWVq6nTI\nfI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuWl8PVP3wb\nI+2ksx0WckNLIOFZfsLorSa/ovc=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE\nAwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG\nEwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM\nFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC\nREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp\nNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM\nVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+\nSZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ\n4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L\ncp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi\neowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV\nHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG\nA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3\nDQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j\nvZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP\nDpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc\nmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D\nlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv\nKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFwzCCA6ugAwIBAgISESGFDLOcajL6vmcbgT+khhWPMA0GCSqGSIb3DQEBCwUA\nMF4xCzAJBgNVBAYTAkZSMQ4wDAYDVQQKEwVBTlNTSTEXMBUGA1UECxMOMDAwMiAx\nMzAwMDc2NjkxJjAkBgNVBAMTHUlHQy9BIEFDIHJhY2luZSBFdGF0IGZyYW5jYWlz\nMB4XDTExMDcwODA5MDAwMFoXDTI4MDQxNTA5MDAwMFowXjELMAkGA1UEBhMCRlIx\nDjAMBgNVBAoTBUFOU1NJMRcwFQYDVQQLEw4wMDAyIDEzMDAwNzY2OTEmMCQGA1UE\nAxMdSUdDL0EgQUMgcmFjaW5lIEV0YXQgZnJhbmNhaXMwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQCqfCifETCYzW9uLIUSJjsIBspB/VJPQ73AJidxdhpZ\nltgJ6weqJk5PPkuh45eHhWaBccm5FXZvd1AYkxAtN4hNF7fzRb0iLrcnmFvHBf29\nM+2i9VMdKCNlv0A1bs5qC8Op9SUMqyLwuMDEfTcMo2J87rTbPSE5p5yJ45uiEPiK\ntkovLphpK2qghtrxCOW+TGcWLSVh89UNCxdERwnURgWdD8CITWHkJMTHaAmvrNKv\nuZUmb4AE/HasqscjtuQGkVVE7GTbmYEc0lZ0/dYyKLvLyTcN+2lsb7qjawaMakAu\nFzo56tAM31ocum+kMrC4zD53G9OLH4b6/z4+b1yIRufjD/qrHfN9S/hUbk7M3DJa\nY3iiMq8zeOpD4Ux6TdeUBi3mT6VCkq8oik/DFeypa6nf4N0TArzMff8t5gepvnWW\n6kJeWxreojOzY72rBfmL5r1N0W1WmuuJPJ/AeOS+JXAGxRFzoMjKFMs61PKcKjza\nXxcz2XYUN6pJh2XZ9NkuGV/5oM2ouUEybXGmpMv3YyLQKeS6gRpqKR2apaRcRlQk\nRdTI7Xp5heyEd25nTWQPQ956g6Sn2Nu1U0z+YsgTw2I2pSgxMpu0lofimcYfVr9G\no6lkMeXVsUuoZsxbof8W/Ao4KmiPdyUmrZF0hWjIfxrlWhS4fQ63IzHAZLcFL0FY\nVQIDAQABo3sweTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAWBgNV\nHSAEDzANMAsGCSqBegGBXwEBAjAdBgNVHQ4EFgQUn6rTKZbfAOVD4PFjrN4SjsIn\nePowHwYDVR0jBBgwFoAUn6rTKZbfAOVD4PFjrN4SjsInePowDQYJKoZIhvcNAQEL\nBQADggIBAHW1ddGONmacSPeFDU4Fu02anLQOKKIEvFAwu/SUTJiQhavgUmRP0tIu\nYpOQsIUNiFT7xlRsnuuVeYBeopcWH/JndEGcVfS3aptKFoa9BR9mgHB+ydH1LSFx\nUDmlrYimJhyL1yUcOtbj9MIMn1fBZMhXUSMWI40PI2pWS//6xp81k8YiwGXxr96p\nbBi+V2VZzfQjVWQh2O2VYWkzcmpR9p/llW2O3mtzJxOUXn6XSMAyFr49N+3W3I68\nXC38YqjP9pD3sYsJ6zokYw3IlkXUL3dIQvUtYucnC+ARhhndpxD3YwaRMGladfSs\n+aGNl8ag7zofkyVIVjoaiCEZk8OVIEkIVUlNolOcmZxzaS6n9cq3DiXvNyNfkNhD\nfu6EF2onXn/SLT+sPq8wp42RxPSPCR3z95EO4xi63ETJfQVTA7duoPN519EaT9C4\nbIh2wYCYVYVTYc9EV0zeTg0WUfE9iYGufQutirXuVsTGzBELGNT8/Xn7/gQRnCPv\ndnLHjb65Hnh28pocrWNCx9jtbWGQwiEqDwgULSBDJXwYtbegpH25pQwZ/smrPedb\n3q/6VxknhecjDvTNDRkwPorkxhEe8LR9aWObDpaGkOD7A29bWT4dIfVXZ1Ym8ocZ\nB4S6LJA6wyikBVogzalblXU5fyJQCk5/F/ezrNMHpr4tUgowTHgQ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGATCCA+mgAwIBAgIRAI9hcRW6eVgXjH0ROqzW264wDQYJKoZIhvcNAQELBQAw\nRTEfMB0GA1UEAxMWQ29tU2lnbiBHbG9iYWwgUm9vdCBDQTEVMBMGA1UEChMMQ29t\nU2lnbiBMdGQuMQswCQYDVQQGEwJJTDAeFw0xMTA3MTgxMDI0NTRaFw0zNjA3MTYx\nMDI0NTVaMEUxHzAdBgNVBAMTFkNvbVNpZ24gR2xvYmFsIFJvb3QgQ0ExFTATBgNV\nBAoTDENvbVNpZ24gTHRkLjELMAkGA1UEBhMCSUwwggIiMA0GCSqGSIb3DQEBAQUA\nA4ICDwAwggIKAoICAQCyKClzKh3rm6n1nvigmV/VU1D4hSwYW2ro3VqpzpPo0Ph3\n3LguqjXd5juDwN4mpxTpD99d7Xu5X6KGTlMVtfN+bTbA4t3x7DU0Zqn0BE5XuOgs\n3GLH41Vmr5wox1bShVpM+IsjcN4E/hMnDtt/Bkb5s33xCG+ohz5dlq0gA9qfr/g4\nO9lkHZXTCeYrmVzd/il4x79CqNvGkdL3um+OKYl8rg1dPtD8UsytMaDgBAopKR+W\nigc16QJzCbvcinlETlrzP/Ny76BWPnAQgaYBULax/Q5thVU+N3sEOKp6uviTdD+X\nO6i96gARU4H0xxPFI75PK/YdHrHjfjQevXl4J37FJfPMSHAbgPBhHC+qn/014DOx\n46fEGXcdw2BFeIIIwbj2GH70VyJWmuk/xLMCHHpJ/nIF8w25BQtkPpkwESL6esaU\nb1CyB4Vgjyf16/0nRiCAKAyC/DY/Yh+rDWtXK8c6QkXD2XamrVJo43DVNFqGZzbf\n5bsUXqiVDOz71AxqqK+p4ek9374xPNMJ2rB5MLPAPycwI0bUuLHhLy6nAIFHLhut\nTNI+6Y/soYpi5JSaEjcY7pxI8WIkUAzr2r+6UoT0vAdyOt7nt1y8844a7szo/aKf\nwoziHl2O1w6ZXUC30K+ptXVaOiW79pBDcbLZ9ZdbONhS7Ea3iH4HJNwktrBJLQID\nAQABo4HrMIHoMA8GA1UdEwEB/wQFMAMBAf8wgYQGA1UdHwR9MHswPKA6oDiGNmh0\ndHA6Ly9mZWRpci5jb21zaWduLmNvLmlsL2NybC9jb21zaWduZ2xvYmFscm9vdGNh\nLmNybDA7oDmgN4Y1aHR0cDovL2NybDEuY29tc2lnbi5jby5pbC9jcmwvY29tc2ln\nbmdsb2JhbHJvb3RjYS5jcmwwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBQCRZPY\nDUhirGm6rgZbPvuqJpFQsTAfBgNVHSMEGDAWgBQCRZPYDUhirGm6rgZbPvuqJpFQ\nsTANBgkqhkiG9w0BAQsFAAOCAgEAk1V5V9701xsfy4mfX+tP9Ln5e9h3N+QMwUfj\nkr+k3e8iXOqADjTpUHeBkEee5tJq09ZLp/43F5tZ2eHdYq2ZEX7iWHCnOQet6Yw9\nSU1TahsrGDA6JJD9sdPFnNZooGsU1520e0zNB0dNWwxrWAmu4RsBxvEpWCJbvzQL\ndOfyX85RWwli81OiVMBc5XvJ1mxsIIqli45oRynKtsWP7E+b0ISJ1n+XFLdQo/Nm\nWA/5sDfT0F5YPzWdZymudMbXitimxC+n4oQE4mbQ4Zm718Iwg3pP9gMMcSc7Qc1J\nkJHPH9O7gVubkKHuSYj9T3Ym6c6egL1pb4pz/uT7cT26Fiopc/jdqbe2EAfoJZkv\nhlp/zdzOoXTWjiKNA5zmgWnZn943FuE9KMRyKtyi/ezJXCh8ypnqLIKxeFfZl69C\nBwJsPXUTuqj8Fic0s3aZmmr7C4jXycP+Q8V+akMEIoHAxcd960b4wVWKqOcI/kZS\nQ0cYqWOY1LNjznRt9lweWEfwDBL3FhrHOmD4++1N3FkkM4W+Q1b2WOL24clDMj+i\n2n9Iw0lc1llHMSMvA5D0vpsXZpOgcCVahfXczQKi9wQ3oZyonJeWx4/rXdMtagAB\nVBYGFuMEUEQtybI+eIbnp5peO2WAAblQI4eTy/jMVowe5tfMEXovV3sz9ULgmGb3\nDscLP1I=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIBATANBgkqhkiG9w0BAQsFADBQMQswCQYDVQQGEwJLUjEc\nMBoGA1UECgwTR292ZXJubWVudCBvZiBLb3JlYTENMAsGA1UECwwER1BLSTEUMBIG\nA1UEAwwLR1BLSVJvb3RDQTEwHhcNMTEwODAzMDY1MjMwWhcNMzEwODAzMDY1MjMw\nWjBQMQswCQYDVQQGEwJLUjEcMBoGA1UECgwTR292ZXJubWVudCBvZiBLb3JlYTEN\nMAsGA1UECwwER1BLSTEUMBIGA1UEAwwLR1BLSVJvb3RDQTEwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQCh/m8EBbDJhGQyN2+g5dTlsgjtaRKqhgj3gkYK\nBgtuXsXkaTVxbf99AvbN3QE8+WCIaPJUd0091UGmLzaBVyW4ct+iUNrX/FXyzjaf\nbNbbl1nfHhaZhkiOTVQhmY5zuj96evEtJMevnxe6iRADOPWnqp+CxT2IzcSFkQCq\n7L2qn8hU2/LpXUvnAYglJZi8t6Ef+r03P1r8dA5OzZ8yV3qhD1R1wsNQtCzMgwcE\nrFRZhFZYuxpfmS5y0fZW0seeTjcdxHiR3whYI5U6AI7DjdWIrT9Cd9ByV4aevkBh\nqkePPIYGmUPXnnqCkdHdnzkMH0WP9TBhD2jTXZKdcFtTyEJrAgMBAAGjQjBAMB0G\nA1UdDgQWBBR4A+sMjKbTVXWkh7Tr0ZpmD0xzizAOBgNVHQ8BAf8EBAMCAQYwDwYD\nVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEARGJWATwo81x7UEQugNbi\ncL8IWXoV51SZVH3kz49fNUjVoq1n2yzfaMddlblbflDNObp/68DxTlSXCeqFHkgi\n/WvyVHERRECXnF0WeeelI+Q8XdF3IJZLT3u5Ss0VAB2loCuC+4hBWSRQu2WZu2Yk\ns9eBN0x6NmtopRmnf2d6VrcFA+WOgUeTjXiDkG52IaPw0w1uTfmRw5epky5idyY2\nbfJ1JeVUINMJnOWpgLkOH3xxakoD8F1Fbi6C3t7MmKupojUq/toUDms6zTk3DIkc\nwd7PALNWL5U8TxNLoroTHSf/lzaOv3o9KDRa0FQo58bPI7MdbRWE4F3mS/ZIrnv7\njQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGMjCCBBqgAwIBAgIQWMv5ZJZxdJVA9K0IrGTk4zANBgkqhkiG9w0BAQsFADBz\nMQswCQYDVQQGEwJJTDEYMBYGA1UECgwPUGVyc29uYWxJRCBMdGQuMR0wGwYDVQQL\nDBRDZXJ0aWZpY2F0ZSBTZXJ2aWNlczErMCkGA1UEAwwiUGVyc29uYWxJRCBUcnVz\ndHdvcnRoeSBSb290Q0EgMjAxMTAeFw0xMTA5MDEwODM1MjFaFw00MTA5MDEwODQ1\nMTZaMHMxCzAJBgNVBAYTAklMMRgwFgYDVQQKDA9QZXJzb25hbElEIEx0ZC4xHTAb\nBgNVBAsMFENlcnRpZmljYXRlIFNlcnZpY2VzMSswKQYDVQQDDCJQZXJzb25hbElE\nIFRydXN0d29ydGh5IFJvb3RDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A\nMIICCgKCAgEAsJWMYP4FDmoz7feL4/LV8nzTVkJU9yvyiKX157dshwErab4FSUTY\n2yF6KteKMaEhEJ7T4m5jgoVUhE0oJhviE/dR+y/rEtU9OYxkn6QTh8PYyfopI44J\nj0lGxNTJV1hpnxfPc3Sl7soYucfBMM1POjUIU/jsGvtvMO32nwnw8NDEjjt5Ti6F\nIlzUfXDR/5K6H9RVU2e6KFgt9xOM/KULnDimRhwO6Kp4K/UKMNM7YIbIf6WbomMB\nL9DTEiWFfpbNMbHkm47qLJOkYqg31faP3yGa0z4d4VARcFSbBBedTathzo8qLO95\n5ndFWdZo1bZLmquRSw5hF7lYwp5moY+JwUMgQrB/gJxKKrd6IEHGTcSSb3p+XVu5\no8lOyuVQZbwAAHlH8EUEsCL7DpiqYR1PYGNyj7WwBJR/EKwZPydiadYcV905Tzjq\nAJr9KJ1AJsBAncSgSchBtWc9oEuUKRKpWCdZBH+P0Yx+DLMIFzSsj7lcvelwoX7C\npWVh6bYQUI/c5HRh8V9ye39cLy18q9ZDMRAcWXfKSEoYomQLAFlnx9TKw5saCFIV\nvtfFxrcv5mKcpsfY3vAV+645VS1vUHUu/aAHtF96fgSL9pmide3JO9U9z2dSPT7v\nH3CaGDynIAZJDLFlrDO71H9HaYj2ioHundS0xy8D6K4ayVYFZ2moyIECAwEAAaOB\nwTCBvjALBgNVHQ8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU\nx38LyC9Xjiny9gGL6yelVo79pB4wEAYJKwYBBAGCNxUBBAMCAQAwagYDVR0gBGMw\nYTBfBggrBgEEAeEYATBTMFEGCCsGAQUFBwIBFkVodHRwOi8vd3d3LmljYS5jby5p\nbC9yZXBvc2l0b3J5L2Nwcy9QZXJzb25hbElEX1ByYWN0aWNlX1N0YXRlbWVudC5w\nZGYwDQYJKoZIhvcNAQELBQADggIBAEJliyT6khU0Ghz6yM5Nei9739ADQRzUpOH7\n6MytCd0dpAjZqCB9l58MSfGlwubVd0aXfqSQonnpvRpeNIJmCVL8UNGP0Kscov//\nPe7+I/i/I7PNvuH3z+TYEuOUyE7M13uwN5t36u1cgcjMj8454+RlXd6C2I8jaeFR\nr1+3T5BppJllU7rm/a94Z5RKyMN/jAJPSuaHmPY4t0j4bSh/98ZsJVT9Ltbq2gbi\nsf0HaPCvgIy0wul0FaQav7nKQ1sS54VHXlID8JHg6VBx1CECLHuGkXA2xpy2dPkq\nVfch+2+gBl3XMBLyUfHJODaPyGZhQdnHS4JoUqP1iQwVvE4qlawxaacb4tTXSPSR\n9QN8eRY+LA1p4Yo3Hp98GFVBL1/npHKbVfPjAbACpYQSakCmq+ShrOsD2bxfJFYn\nrSDgZjVFPUcJ8AWxb3F+QLDQFV4rrFKBqPuD9SxXRIY05BRq4899mnfYbEhcy5rh\npvu/EaIG5R9xvTS1z73EQhbFKfjUwEyKst7FlIKGm8zgqQZEMSQkTfrt4UIlZqLB\n14AX73qVZUM+ZtMF8QHkQlZEAHhrnTYg+2X/QFzoaDUf4SagggN2A8twRhEkrt8v\nYP3xJwADvUsn27yclzdRK+V4tME2kBCM/z0A1LpIn0jKhzGa7cSaU9LdcxQ/CYKh\nXWVOTSbi\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE\nBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w\nMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290\nIENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC\nSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1\nODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv\nUTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX\n4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9\nKK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/\ngCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb\nrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ\n51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F\nbe8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe\nKF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F\nv6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn\nfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7\njPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz\nezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt\nifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL\ne3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70\njsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz\nWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V\nSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j\npwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX\nX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok\nfcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R\nK4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU\nZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU\nLysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT\nLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICqDCCAi2gAwIBAgIQIW4zpcvTiKRvKQe0JzzE2DAKBggqhkjOPQQDAzCBlDEL\nMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYD\nVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRlYyBD\nbGFzcyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0g\nRzQwHhcNMTExMDA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBlDELMAkGA1UEBhMC\nVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZTeW1h\nbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRlYyBDbGFzcyAxIFB1\nYmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcq\nhkjOPQIBBgUrgQQAIgNiAATXZrUb266zYO5G6ohjdTsqlG3zXxL24w+etgoUU0hS\nyNw6s8tIICYSTvqJhNTfkeQpfSgB2dsYQ2mhH7XThhbcx39nI9/fMTGDAzVwsUu3\nyBe7UcvclBfb6gk7dhLeqrWjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\nBTADAQH/MB0GA1UdDgQWBBRlwI0l9Qy6l3eQP54u4Fr1ztXh5DAKBggqhkjOPQQD\nAwNpADBmAjEApa7jRlP4mDbjIvouKEkN7jB+M/PsP3FezFWJeJmssv3cHFwzjim5\naxfIEWi13IMHAjEAnMhE2mnCNsNUGRCFAtqdR+9B52wmnQk9922Q0QVEL7C8g5No\n8gxFSTm/mQQc0xCg\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICqDCCAi2gAwIBAgIQNBdlEkA7t1aALYDLeVWmHjAKBggqhkjOPQQDAzCBlDEL\nMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYD\nVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRlYyBD\nbGFzcyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0g\nRzQwHhcNMTExMDA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBlDELMAkGA1UEBhMC\nVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZTeW1h\nbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRlYyBDbGFzcyAyIFB1\nYmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcq\nhkjOPQIBBgUrgQQAIgNiAATR2UqOTA2ESlG6fO/TzPo6mrWnYxM9AeBJPvrBR8mS\nszrX/m+c95o6D/UOCgrDP8jnEhSO1dVtmCyzcTIK6yq99tdqIAtnRZzSsr9TImYJ\nXdsR8/EFM1ij4rjPfM2Cm72jQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\nBTADAQH/MB0GA1UdDgQWBBQ9MvM6qQyQhPmijGkGYVQvh3L+BTAKBggqhkjOPQQD\nAwNpADBmAjEAyKapr0F/tckRQhZoaUxcuCcYtpjxwH+QbYfTjEYX8D5P/OqwCMR6\nS7wIL8fip29lAjEA1lnehs5fDspU1cbQFQ78i5Ry1I4AWFPPfrFLDeVQhuuea9//\nKabYR9mglhjb8kWz\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF0zCCA7ugAwIBAgIVALhZFHE/V9+PMcAzPdLWGXojF7TrMA0GCSqGSIb3DQEB\nDQUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dp\nZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5\nMSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBIDIwHhcNMTExMDA2\nMDgzOTU2WhcNNDYxMDA2MDgzOTU2WjCBgDELMAkGA1UEBhMCUEwxIjAgBgNVBAoT\nGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0\naWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0\nd29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvfl4+ObV\ngAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC5SBLm9qbe7mZXdmbgEvXhEAr\nJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Vaqp3cKcniNQfrcE1K1sGzVrih\nQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ0WeNG0a+RzDVLnLRxWPa52N5\nRH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet/5oCl8HGUJKbAiy9qbk0WQq/\nhEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNeGWJoRVfkkIJCu0LW8GHgwaM9\nZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcVWiNoidQ+3k4nsPBADLxNF8tN\norMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbls+V1e9dBkQXcXWnjlQ1DufyD\nljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZbMI28iQzV13D4h1L92u+sUS4H\ns07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+NqcnoYsylfzGuXIkosagpZ6w7xQ\nEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbljnX98sy50IdbzAYQYLuDNbde\nZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEAAaNCMEAwDwYDVR0TAQH/BAUw\nAwEB/zAdBgNVHQ4EFgQUtqFUOQLDoD+Oirz61PgcptE6Dv0wDgYDVR0PAQH/BAQD\nAgEGMA0GCSqGSIb3DQEBDQUAA4ICAQCdU8KBJdw1LK4K3VqbRjBWu9S0bEuG5gql\n0pKKmo3cj7TudvQDy+ubAXirKmu1uiNOMXy1LN0taWczbmNdORgS+KAoU0SHq2rE\nkpYfKqIcup3dJ/tSTbCPWujtjcNo45KgJgyHkLAD6mplKAjERnjgW7oO8DPcJ7Z+\niD29kqSWfkGogAh71jYSvBAVmyS8q619EYkvMe340s9Tjuu0U6fnBMovpiLEEdzr\nmMkiXUFq3ApSBFu8LqB9x7aSuySg8zfRK0OozPFoeBp+b2OQe590yGvZC1X2eQM9\ng8dBQJL7dgs3JRc8rz76PFwbhvlKDD+KxF4OmPGt7s/g/SE1xzNhzKI3GEN8M+mu\ndoKCB0VIO8lnbq2jheiWVs+8u/qry7dXJ40aL5nzIzM0jspTY9NXNFBPz0nBBbrF\nqId744aP+0OiEumsUewEdkzw+o+5MRPpCLckCfmgtwc2WFfPxLt+SWaVNQS2dzW4\nqVMpX5KF+FLEWk79BmE5+33QdkeSzOwrvYRu5ptFwX1isVMtnnWg58koUNflvKiq\nB3hquXS0YPOEjQPcrpHadEQNe0Kpd9YrfKHGbBNTIqkSmqX5TyhFNbCXT0ZlhcX0\n/WKiomr8NDAGft8M4HOBlslEKt4fguxscletKWSk8cYpjjVgU85r2QK+OTB14Pdc\nY2rwQMEsjQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB\ngDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu\nQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG\nA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz\nOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ\nVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp\nZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3\nb3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA\nDGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn\n0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB\nOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE\nfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E\nSv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m\no130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i\nsx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW\nOZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez\nTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS\nadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n\n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD\nAQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC\nAQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ\nF/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf\nCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29\nXN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm\ndjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/\nWjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb\nAoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq\nP/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko\nb7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj\nXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P\n5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi\nDrW5viSP\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIE5DCCA8ygAwIBAgIBATANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJteTEL\nMAkGA1UECgwCVE0xNDAyBgNVBAsMK1RNIEFwcGxpZWQgQnVzaW5lc3MgQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkxLTArBgNVBAMMJFRNIEFwcGxpZWQgQnVzaW5lc3Mg\nUm9vdCBDZXJ0aWZpY2F0ZTAeFw0xMTEwMTAwNjIzMzlaFw0zMTEwMTAwNjUzMzla\nMH8xCzAJBgNVBAYTAm15MQswCQYDVQQKDAJUTTE0MDIGA1UECwwrVE0gQXBwbGll\nZCBCdXNpbmVzcyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEtMCsGA1UEAwwkVE0g\nQXBwbGllZCBCdXNpbmVzcyBSb290IENlcnRpZmljYXRlMIIBIDANBgkqhkiG9w0B\nAQEFAAOCAQ0AMIIBCAKCAQEAxbd1GV7r9EIJjbFqbG4ydqQFBw+PK2Q672vHtxtX\nWiUzwGEYo4IdgHft7RxkskC6yMJVtV+Owt2RbvPF56M5m0wvfqPm948VXH0bWrqW\nlpOgYXIgRIgnq0FHdz5eMKWLNegwRqBY6k4CbT1iDTnzZK5m7twSfhlL0b/CgkT6\n+deZSOyzDPRiZzWbnUZoR5emIl4TVgALUfX7ZF9b4L/yb+9F1K7Gr9ycH+0UHbKm\n7wc45wh3Nqq5qDw5GuWRaKqQjsGYGeTqbYWTGwbm3FELoQDsxK5ypxxpEXI+3M7z\nOFfXGhpXFE2LUHZFVXMwI29Lr0pIQpNCX/nx2jlcBtUPyQIBA6OCAWswggFnMIGr\nBgNVHSMEgaMwgaCAFEAa+7SWN5aD3yw7FO0cxsveIG0IoYGEpIGBMH8xCzAJBgNV\nBAYTAm15MQswCQYDVQQKDAJUTTE0MDIGA1UECwwrVE0gQXBwbGllZCBCdXNpbmVz\ncyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEtMCsGA1UEAwwkVE0gQXBwbGllZCBC\ndXNpbmVzcyBSb290IENlcnRpZmljYXRlggEBMB0GA1UdDgQWBBRAGvu0ljeWg98s\nOxTtHMbL3iBtCDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zB3BgNV\nHR8EcDBuMGygaqBohmZsZGFwOi8vbGRhcC50bWNhLmNvbS5teTozODkvY249YXJs\nMWRwMSxvdT1BUkwsb3U9VE0gQXBwbGllZCBCdXNpbmVzcyBDZXJ0aWZpY2F0aW9u\nIEF1dGhvcml0eSxvPVRNLGM9bXkwDQYJKoZIhvcNAQELBQADggEBAECJXpdECqtm\nMStt3E6m5y2xR/9SefPt26eB6To8VWf1RdHuGXn9N+CupCiiGDjez9KXkqQ5vFSD\n7x2hgWfIjCZlhrrKbwBCWE26GWa3G0BRJZLQghWIbGIy4vFAEt2+wO8Q8iaEJfX0\nag9ZPyMZHb0NvDk6vNrcbj8OjCaRJDPM/TM5jF2iu0eX5xAqhCZUsSt+X/mqf+3H\n/sojplW/38pe4Ps+p1LWKjqle2PyhfwhNCvBrvBBkBg/RcQjjbw7ht2qRmdphyGi\nVxamp3w7/okgRxj61XL9XDpotTvhPMIrS3hTVVqy9oa+wD3bSP/wwHoQ1B7f5LYu\nwhrUDnpqoHY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID9jCCAt6gAwIBAgIQJDJ18h0v0gkz97RqytDzmDANBgkqhkiG9w0BAQsFADCB\nlDELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8w\nHQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRl\nYyBDbGFzcyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5\nIC0gRzYwHhcNMTExMDE4MDAwMDAwWhcNMzcxMjAxMjM1OTU5WjCBlDELMAkGA1UE\nBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZT\neW1hbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRlYyBDbGFzcyAx\nIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzYwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHOddJZKmZgiJM6kXZBxbje/SD\n6Jlz+muxNuCad6BAwoGNAcfMjL2Pffd543pMA03Z+/2HOCgs3ZqLVAjbZ/sbjP4o\nki++t7JIp4Gh2F6Iw8w5QEFa0dzl2hCfL9oBTf0uRnz5LicKaTfukaMbasxEvxvH\nw9QRslBglwm9LiL1QYRmn81ApqkAgMEflZKf3vNI79sdd2H8f9/ulqRy0LY+/3gn\nr8uSFWkI22MQ4uaXrG7crPaizh5HmbmJtxLmodTNWRFnw2+F2EJOKL5ZVVkElauP\nN4C/DfD8HzpkMViBeNfiNfYgPym4jxZuPkjctUwH4fIa6n4KedaovetdhitNAgMB\nAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\nBBQzQejIORIVk0jyljIuWvXalF9TYDANBgkqhkiG9w0BAQsFAAOCAQEAFeNzV7EX\ntl9JaUSm9l56Z6zS3nVJq/4lVcc6yUQVEG6/MWvL2QeTfxyFYwDjMhLgzMv7OWyP\n4lPiPEAz2aSMR+atWPuJr+PehilWNCxFuBL6RIluLRQlKCQBZdbqUqwFblYSCT3Q\ndPTXvQbKqDqNVkL6jXI+dPEDct+HG14OelWWLDi3mIXNTTNEyZSPWjEwN0ujOhKz\n5zbRIWhLLTjmU64cJVYIVgNnhJ3Gw84kYsdMNs+wBkS39V8C3dlU6S+QTnrIToNA\nDJqXPDe/v+z28LSFdyjBC8hnghAXOKK3Buqbvzr46SMHv3TgmDgVVXjucgBcGaP0\n0jPg/73RVDkpDw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID9jCCAt6gAwIBAgIQZIKe/DcedF38l/+XyLH/QTANBgkqhkiG9w0BAQsFADCB\nlDELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8w\nHQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRl\nYyBDbGFzcyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5\nIC0gRzYwHhcNMTExMDE4MDAwMDAwWhcNMzcxMjAxMjM1OTU5WjCBlDELMAkGA1UE\nBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZT\neW1hbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRlYyBDbGFzcyAy\nIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzYwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNzOkFyGOFyz9AYxe9GPo15gRn\nV2WYKaRPyVyPDzTS+NqoE2KquB5QZ3iwFkygOakVeq7t0qLA8JA3KRgmXOgNPLZs\nST/B4NzZS7YUGQum05bh1gnjGSYc+R9lS/kaQxwAg9bQqkmi1NvmYji6UBRDbfkx\n+FYW2TgCkc/rbN27OU6Z4TBnRfHU8I3D3/7yOAchfQBeVkSz5GC9kSucq1sEcg+y\nKNlyqwUgQiWpWwNqIBDMMfAr2jUs0Pual07wgksr2F82owstr2MNHSV/oW5cYqGN\nKD6h/Bwg+AEvulWaEbAZ0shQeWsOagXXqgQ2sqPy4V93p3ec5R7c6d9qwWVdAgMB\nAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\nBBSHjCCVyJhK0daABkqQNETfHE2/sDANBgkqhkiG9w0BAQsFAAOCAQEAgY6ypWaW\ntyGltu9vI1pf24HFQqV4wWn99DzX+VxrcHIa/FqXTQCAiIiCisNxDY7FiZss7Y0L\n0nJU9X3UXENX6fOupQIR9nYrgVfdfdp0MP1UR/bgFm6mtApI5ud1Bw8pGTnOefS2\nbMVfmdUfS/rfbSw8DVSAcPCIC4DPxmiiuB1w2XaM/O6lyc+tHc+ZJVdaYkXLFmu9\nSc2lo4xpeSWuuExsi0BmSxY/zwIa3eFsawdhanYVKZl/G92IgMG/tY9zxaaWI4Sm\nKIYkM2oBLldzJbZev4/mHWGoQClnHYebHX+bn5nNMdZUvmK7OaxoEkiRIKXLsd3+\nb/xa5IJVWa8xqQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDcTCCAlmgAwIBAgIVAOYJ/nrqAGiM4CS07SAbH+9StETRMA0GCSqGSIb3DQEB\nBQUAMFAxCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGlj\nemVuaW93YSBTLkEuMRcwFQYDVQQDDA5TWkFGSVIgUk9PVCBDQTAeFw0xMTEyMDYx\nMTEwNTdaFw0zMTEyMDYxMTEwNTdaMFAxCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L\ncmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRcwFQYDVQQDDA5TWkFGSVIg\nUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKxHL49ZMTml\n6g3wpYwrvQKkvc0Kc6oJ5sxfgmp1qZfluwbv88BdocHSiXlY8NzrVYzuWBp7J/9K\nULMAoWoTIzOQ6C9TNm4YbA9A1jdX1wYNL5Akylf8W5L/I4BXhT9KnlI6x+a7BVAm\nnr/Ttl+utT/Asms2fRfEsF2vZPMxH4UFqOAhFjxTkmJWf2Cu4nvRQJHcttB+cEAo\nag/hERt/+tzo4URz6x6r19toYmxx4FjjBkUhWQw1X21re//Hof2+0YgiwYT84zLb\neqDqCOMOXxvH480yGDkh/QoazWX3U75HQExT/iJlwnu7I1V6HXztKIwCBjsxffbH\n3jOshCJtywcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAQYwHQYDVR0OBBYEFFOSo33/gnbwM9TrkmdHYTMbaDsqMA0GCSqGSIb3DQEBBQUA\nA4IBAQA5UFWd5EL/pBviIMm1zD2JLUCpp0mJG7JkwznIOzawhGmFFaxGoxAhQBEg\nhaP+E0KR66oAwVC6xe32QUVSHfWqWndzbODzLB8yj7WAR0cDM45ZngSBPBuFE3Wu\nGLJX9g100ETfIX+4YBR/4NR/uvTnpnd9ete7Whl0ZfY94yuu4xQqB5QFv+P7IXXV\nlTOjkjuGXEcyQAjQzbFaT9vIABSbeCXWBbjvOXukJy6WgAiclzGNSYprre8Ryydd\nfmjW9HIGwsIO03EldivvqEYL1Hv1w/Pur+6FUEOaL68PEIUovfgwIB2BAw+vZDuw\ncH0mX548PojGyg434cDjkSXa3mHF\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix\nRDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1\ndGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p\nYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw\nNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK\nEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl\ncnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl\nc2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz\ndYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ\nfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns\nbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD\n75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP\nFEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV\nHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp\n5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu\nb3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA\nA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p\n6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8\nTqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7\ndIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys\nNnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI\nl7WdmplNsDz4SgCbZN2fOUvRJ9e4\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00\nMjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV\nwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe\nrNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341\n68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh\n4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp\nUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o\nabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc\n3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G\nKubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt\nhfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO\nTk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt\nzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD\nggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC\nMTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2\ncDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN\nqXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5\nYCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv\nb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2\n8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k\nNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj\nZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp\nq1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt\nnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00\nMjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf\nqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW\nn4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym\nc5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+\nO7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1\no9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j\nIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq\nIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz\n8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh\nvNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l\n7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG\ncC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD\nggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66\nAarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC\nroijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga\nW/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n\nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE\n+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV\ncsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd\ndbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg\nKCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM\nHVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4\nWSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00\nMjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR\n/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu\nFoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR\nU7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c\nra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR\nFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k\nA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw\neyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl\nsSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp\nVzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q\nA4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+\nydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD\nggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px\nKGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI\nFUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv\noxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg\nu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP\n0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf\n3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl\n8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+\nDhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN\nPlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/\nywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEBDCCAuygAwIBAgIIGHqpqMKWIQwwDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE\nBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxlIENlcnRp\nZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENBMB4XDTEy\nMDIwMTIyMTIxNVoXDTI3MDIwMTIyMTIxNVoweTEtMCsGA1UEAwwkRGV2ZWxvcGVy\nIElEIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSYwJAYDVQQLDB1BcHBsZSBDZXJ0\naWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UE\nBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCJdk8GW5pB7qUj\nKwKjX9dzP8A1sIuECj8GJH+nlT/rTw6Tr7QO0Mg+5W0Ysx/oiUe/1wkI5P9WmCkV\n55SduTWjCs20wOHiYPTK7Cl4RWlpYGtfipL8niPmOsIiszFPHLrytjRZQu6wqQID\nGJEEtrN4LjMfgEUNRW+7Dlpbfzrn2AjXCw4ybfuGNuRsq8QRinCEJqqfRNHxuMZ7\nlBebSPcLWBa6I8WfFTl+yl3DMl8P4FJ/QOq+rAhklVvJGpzlgMofakQcbD7EsCYf\nHex7r16gaj1HqVgSMT8gdihtHRywwk4RaSaLy9bQEYLJTg/xVnTQ2QhLZniiq6yn\n4tJMh1nJAgMBAAGjgaYwgaMwHQYDVR0OBBYEFFcX7aLP3HyYoRDg/L6HLSzy4xdU\nMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/\nCF4wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDovL2NybC5hcHBsZS5jb20vcm9vdC5j\ncmwwDgYDVR0PAQH/BAQDAgGGMBAGCiqGSIb3Y2QGAgYEAgUAMA0GCSqGSIb3DQEB\nCwUAA4IBAQBCOXRrodzGpI83KoyzHQpEvJUsf7xZuKxh+weQkjK51L87wVA5akR0\nouxbH3Dlqt1LbBwjcS1f0cWTvu6binBlgp0W4xoQF4ktqM39DHhYSQwofzPuAHob\ntHastrW7T9+oG53IGZdKC1ZnL8I+trPEgzrwd210xC4jUe6apQNvYPSlSKcGwrta\n4h8fRkV+5Jf1JxC3ICJyb3LaxlB1xT0lj12jAOmfNoxIOY+zO+qQgC6VmmD0eM70\nDgpTPqL6T9geroSVjTK8Vk2J6XgY4KyaQrp6RhuEoonOFOiI0ViL9q5WxCwFKkWv\nC9lLqQIPNKyIx2FViUTJJ3MH7oLlTvVw\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDHjCCAgagAwIBAgIDB7HTMA0GCSqGSIb3DQEBDQUAMDcxCzAJBgNVBAYTAlNJ\nMQ8wDQYDVQQKEwZIYWxjb20xFzAVBgNVBAMTDkhhbGNvbSBSb290IENBMB4XDTEy\nMDIwODA5NTU0MVoXDTMyMDIwODA5NTU0MVowNzELMAkGA1UEBhMCU0kxDzANBgNV\nBAoTBkhhbGNvbTEXMBUGA1UEAxMOSGFsY29tIFJvb3QgQ0EwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQCJuYXK/vR1fX/snUI3urqNvOw9FwP92UVl1s3J\nTl+MSFyXCFcUiy2cPJBJmc9pr0mN2xwBsG7p9OqRZ13Ks2lP2MzBDT3uqgN24Mlw\nop/+65vQtsmW0/D7W9DwB6tMXk2k4kdeBWh0po4iR+5+02eEVDeSRw7zo+wVGvNt\ne78ZNSGPgkusVJwJzW62wVe90Ek9b59zjrFsfr3+1rs9A+jmTBq07q+0g04ykFT2\nThvhL86lNBqOoyD52T4ia29u4/rZM1wIoPcVAD2cEJJKVc2Asgaq/dePt1qSJyQP\nMzwouvEfaLV3KV6uwtqNNnDiejIbI6bexWENmqUSILXzllm1AgMBAAGjMzAxMA8G\nA1UdEwEB/wQFMAMBAf8wEQYDVR0OBAoECE6U2Ipjws95MAsGA1UdDwQEAwIBBjAN\nBgkqhkiG9w0BAQ0FAAOCAQEAKb7nseT6A6IPr3ZZnfhOU008BIOfoeKM9pTZtK5o\nKlZrMlMogwdyTLBOqB2BgyFnAzfRjMbBToTpNDvT9fUnto0jBVK4TDLyLtrRKn0+\ngwMq0rHjmumKg0LwLAqhUw/AK+KPGk6VuUW8S2c6vTLzraWPj8Mu6vb0e2LQbm7F\nYTETZuZnSZk7L4BPenxzigMNX/WzMigKisDh+bijJu7cG1fPdhpPU772SotXFysv\nmYaq3ozatqhs32g21mGLbsBzTrc5RfR9zknE8x35qXds7++SFRMnmUbon6mKG58p\nL6IdPtYrx+RVEDoY97N7Ty7HACLt5DHQ57jkVE/BgEUlbg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDvTCCAqWgAwIBAgIQD2tVL56/kHsPZimpvfTYzjANBgkqhkiG9w0BAQsFADBk\nMQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xNjA0\nBgNVBAMTLVN5bWFudGVjIEVudGVycHJpc2UgTW9iaWxlIFJvb3QgZm9yIE1pY3Jv\nc29mdDAeFw0xMjAzMTUwMDAwMDBaFw0zMjAzMTQyMzU5NTlaMGQxCzAJBgNVBAYT\nAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtU3lt\nYW50ZWMgRW50ZXJwcmlzZSBNb2JpbGUgUm9vdCBmb3IgTWljcm9zb2Z0MIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtT2wcu6R6aVpnBFNevmz+j3ylJsj\nt6YD7GIY/IUSIv7BcX1Uk7mRfWL2yqg4FWX4dz3lgiA61LXRbo0GSb3fgg4khefv\neC0Y8uALaEY+JBDIV+4ObXGm07FWHNcp1bLqVAUKqDyhuCVSBwWg3+fc7lw7QbWr\nXDMy0s7r6Zb4QPQKujMd+FYDCYL1ZwfEwDTBXfxFu+o8mtV0cW3VhtPC/IW8VOuj\n1fJP1UWvV7zwIsCPokXIdTR33qFtN3Kzc40Ma1O6WeGoPoBX0l9Z7mh1z4Gco8pF\njDfbBXI0HDIC+NX5LA3aWJ7EF7SbyZDEiFk/cZGQRBi+Iot5ki5CsIuXWwIDAQAB\no2swaTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAnBgNVHREEIDAe\npBwwGjEYMBYGA1UEAxMPTVBLSS0yMDQ4LTEtMTExMB0GA1UdDgQWBBRN7N8mBtwk\nEMC2mfTXOcdvGfgmKDANBgkqhkiG9w0BAQsFAAOCAQEAqVdZ0AFUFEavx3lUDGoq\nW9g6HYHkiKMxtPHzNfFGc1xDyf68omoZwL0vX8s4o21u6BRe8nh+RXrhu/Qum0Xr\n4B1QHDRbf5iKhg+H2uRkJnf8Cd8jQU8On/oO+kSF8CmXpJTi9EAtkRx29Khg3nGm\nsAXiT2nZGQuJOuD6qyv68bMy7fx8cGVe0HsRe53oWxpKdqR7UTmsfakMdDjou1Xf\nxM7ApyFauBufAcWnEP59+WoImQHR9jVQOOT2Q+QY2IBM7McE4mGMfUntz7Sl8fKQ\nkgkINXOgIzLK6ZyeHL4LByx3XhdM2pyC4YAbpfPa94i/vzkn+CT+sUvIl+3kEhQl\niA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFkzCCA3ugAwIBAgIRAJBmYahiPWVEdwQ/cZrDlwwwDQYJKoZIhvcNAQEFBQAw\nOzELMAkGA1UEBhMCU0UxETAPBgNVBAoMCEluZXJhIEFCMRkwFwYDVQQDDBBTSVRI\nUyBSb290IENBIHYxMB4XDTEyMDMyOTA3NTQ0OVoXDTMyMDMyOTA3NTQ0OVowOzEL\nMAkGA1UEBhMCU0UxETAPBgNVBAoMCEluZXJhIEFCMRkwFwYDVQQDDBBTSVRIUyBS\nb290IENBIHYxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwO3mnqis\nqP/YNbn8+/CVTz89RyPRksnJ+PDiH4atiD/gAM9PEZVhPaXWIBnRiNLCVglFIKEq\n6iLD6rrMQmmeuIWfcMBsp75vo1zdQ4gHzcop32l6Hy2fVmobYiAhYcZQS2V1SUa/\nXNcpHsIehULhDjhNwzZxQkRROtFYzMm0qmxAx4PxxwmfSvNr8wcWNfSCjl6LhNxx\nebn7bldFt8VwOv9CAtE0v4VwbU+P5x8ZIffVNLzuWeYuIvNxgmIZnwVkfDsicRil\nLcF4WJnRr96UQAYZdhNQhyPLR1eubMUT6pqFUsPKVyYf3hZtrXF+8thh/eY2TnEa\nndMgNa0SIVh1NouJFqQ3KM+ggzpAo8oR77TlkBvjZZJnmG8OKeVnGNeI+o22x3ql\noH+RHqu2+XSYdlJgL1o3majb0T7WhGpvUtO02hrHuLLRlBEfxYiJ6Vupo5Tmon1N\npzKJod4ma83Vo/IyG9o1E4kRSU2/RjG76S0T+A4Apf4D9VZGPI8TK+Dlxx4D34rq\nRoVFhtntXgu4ZJP00FguKY1FV02JdZBlzGo7wZyAubSANQOO324qk76mvgoBRG9A\nc6oqghyEdn9p3bG7kljoQFFyXPc+OUT6pZmgf42LsEFYd60ixaDAuv0xmTVq2ckg\nGl7zvbwIf91JLS+dkRANW6g/z7RXcztb4GcCAwEAAaOBkTCBjjAPBgNVHRMBAf8E\nBTADAQH/MEwGA1UdIARFMEMwQQYJKoVwSggBAgEBMDQwMgYIKwYBBQUHAgEWJmh0\ndHA6Ly9jcHMuc2l0aHMuc2Uvc2l0aHNyb290Y2F2MS5odG1sMA4GA1UdDwEB/wQE\nAwIBBjAdBgNVHQ4EFgQUMvmdT2npmI2g1ox9+R3Oozy6dhUwDQYJKoZIhvcNAQEF\nBQADggIBAB8/43hYyArKNCIJ2LIFi9FlnOHX130KwByYpSRSODPaZCIjgK7+PYC+\nT4/dg/YNTDNa1aM7UIpSWiYUc1GU5FKXY9u3Bqjvj63i7d6jvyDRRtsteOgsJ0Sc\nPOy3F/yJl/Ojol7CWVPgz+S1ATtjUyjTr2ZLNDmvYQ4+m+6zidaToDsBxLMjVBA8\nTdeqsNrZbMowRC3dsihiikFg8kATbLB8PkHgi6Y08eeuUYcDjpl/2Wii9pwNeYKy\nn98kyGZg6LZIRCfIa1a3RIXOArfTinFcV1FXIYzqwlEPUD+AqwRNyVLd5KXyLh9t\ndbqHHZAL7hiEgHO7i5WEimENTl1in+NmDPs2DifTSPgGiAalX+5+XN2tCh09HKpA\neZh5uFCMNo0LCjYL1T7nXYHdbNxtsW8NdJ4sL8IF8kQRsjP6gcVKbT5F1izia18u\n5EOVURuZMQXfJRtz0XucxHNJ+2Jg2Wlj3dE+ZW1H+mRMA1hQ2aa+5Spo6z+LEPHm\nuyIGKJqgpJhpbza01A0ODH3AKTG7LAMn4WenvdGLLraHxArgCQuCoeZPWJ372Phh\n4cqXxLi3UDnMMU79LRwa9kfjbOwbBeh/FzUQhNoz5zTmtaTrxCIHSvabWNgPnED7\nsYtfov2Z6qJ7WWLRXq7RSnIYK0s2OXIHmlrwYzrPG/nP3UhzWXDk\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIExTCCA62gAwIBAgIESbY1GDANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJT\nQTEyMDAGA1UECgwpTmF0aW9uYWwgQ2VudGVyIGZvciBEaWdpdGFsIENlcnRpZmlj\nYXRpb24xHzAdBgNVBAsMFlNhdWRpIE5hdGlvbmFsIFJvb3QgQ0EwHhcNMTIwNDI5\nMDY1NTIwWhcNMjkxMTI5MDcyNTIwWjBiMQswCQYDVQQGEwJTQTEyMDAGA1UECgwp\nTmF0aW9uYWwgQ2VudGVyIGZvciBEaWdpdGFsIENlcnRpZmljYXRpb24xHzAdBgNV\nBAsMFlNhdWRpIE5hdGlvbmFsIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDj5CziDK+WOay1n4cRF/Ojv4FFDfMaDLoy4kzop4bbXNK52zVK\nLs1+cYIk+twf8uS8zrfG4sreKWjP7yRbv6YVz57jaUuUufz7nNhjpblp383u3Mhc\nwKD+KRWTvz2Gg1W1lhy9p3DatwXkOZO/pXnk9ZNGGPLbDecqd2YMgCdKPjzdT5A1\nxmuBqj1vCaWMLiFXC7AKkOqhHvpYDUmnzyuyqMA46RPalFhAki/lOL22iSZzhIGN\n60pZNDB4KuqLFkjBN5J1mI0KSi5/2xKO1ik5MCvuvYC2KOlXcBSCfYST/gk1vGD1\nGHVQlBQkWkwYlxNCogT8mb2oWpvRZ7McG/KfAgMBAAGjggGBMIIBfTAOBgNVHQ8B\nAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAzBggrBgEFBQcBAQQnMCUwIwYIKwYB\nBQUHMAGGF2h0dHA6Ly9vY3NwLm5jZGMuZ292LnNhMIHkBgNVHR8Egdwwgdkwgaag\ngaOggaCGKWh0dHA6Ly93ZWIubmNkYy5nb3Yuc2EvY3JsL25yY2FwYXJ0YTEuY3Js\npHMwcTELMAkGA1UEBhMCU0ExMjAwBgNVBAoMKU5hdGlvbmFsIENlbnRlciBmb3Ig\nRGlnaXRhbCBDZXJ0aWZpY2F0aW9uMR8wHQYDVQQLDBZTYXVkaSBOYXRpb25hbCBS\nb290IENBMQ0wCwYDVQQDDARDUkwxMC6gLKAqhihodHRwOi8vd2ViLm5jZGMuZ292\nLnNhL2NybC9ucmNhY29tYjEuY3JsMB8GA1UdIwQYMBaAFPyZmEEX4/M9Hv23cqm/\noxbkKumqMB0GA1UdDgQWBBT8mZhBF+PzPR79t3Kpv6MW5CrpqjANBgkqhkiG9w0B\nAQsFAAOCAQEALpUOix3h+/qcQm1Ai7/f7DMESwUOXCI2H6QClDh1/AhZm52FvznN\nm86ATFaGmU1zZvW2Asm0JEiPC2Pzjn8xgZt8WXeRtSMIeXptPsXVD0eCsO+XLic0\nuYfR1AV8Xz0hN6R/yavRmJD3S5EYrsTpI4nou2DGS88L2PcrfSWM4DZk5KuqeD02\n+qL0SZIDtRnu13JgsP7JB2q4YAWZP31WBHBI3TPGSOkB88LqRXGaQ1r9vhkzM4ne\nPFjJEodWE2EmHpEQQ3y8Hgw+0Fp8SX523G4BHUuSqdlm5Xod9LiLYC7slSz/TWTI\n7CUAD9jzEqpL1/PSBmXeLdniE6YHskWu6g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEHjCCAwagAwIBAgIET7PQ7jANBgkqhkiG9w0BAQUFADCBiTELMAkGA1UEBhMC\nWkExETAPBgNVBAoTCExBV3RydXN0MTIwMAYDVQQLEylMQVcgVHJ1c3RlZCBUaGly\nZCBQYXJ0eSBTZXJ2aWNlcyBQVFkgTHRkLjEzMDEGA1UEAxMqTEFXdHJ1c3QgUm9v\ndCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMDQ4MB4XDTEyMDUxNjE1NDAxOFoX\nDTMyMDUxNjE2MTAxOFowgYkxCzAJBgNVBAYTAlpBMREwDwYDVQQKEwhMQVd0cnVz\ndDEyMDAGA1UECxMpTEFXIFRydXN0ZWQgVGhpcmQgUGFydHkgU2VydmljZXMgUFRZ\nIEx0ZC4xMzAxBgNVBAMTKkxBV3RydXN0IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRo\nb3JpdHkgMjA0ODCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTckbEK\nFR42rhFERZfVJTWHixsK0c9w+iZBsfxKDahatWan3B9uHQjppoYLZkRcuFCiMJYC\nC4jIFVQXr/rX5GoPgMfO5eimmbJLf5JNNmVU7iEwI+QPx0LnXcwvGz5rCqc+0Y8H\nLti3+s8YVTWZs9BSuw3nqUsb+/tG/wEJsjdPsf15Ovg27GMq3Ps48bfoYeCR0rt4\nFTZ0vR21Xtm9tm4I/Hn2un/kHC1AvR22A6QCyOtqGNt3ZWe1k2o64N0kV6uB4v1x\n19de7Y78YMXnufwjprlr99zTJgKabuADhfvFp8ZR7MlpE/QWC+00ASIje90rQZap\nOkzqald1KwsPFD8CAwEAAaOBizCBiDArBgNVHRAEJDAigA8yMDEyMDUxNjE1NDAx\nOFqBDzIwMzIwNTE2MTYxMDE4WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUXN46\nMzRJZMSSMXxVXvXyO0/uwx0wHQYDVR0OBBYEFFzeOjM0SWTEkjF8VV718jtP7sMd\nMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJYl5BxGneuWSlaE5zbA\nr7IxxqtnyTv3X3GZZK5U4w1KccxcfNI1u0cSx7PEkW1UCTbFREaCF1InNnmLukSU\ntIJxZdM1Vf7Drj8j9vpFho1VjvbHmc/PP+RHepzwqVQIuqQ/lIxALIQkAyJFx3Ep\nGFxV/O9dh/2nmoMD3L++jESN6/FiWlNpjYADYLMP53hDTKnZsXJAy1hEx3Xo1oni\nSv73kKyE9ybEQOGUuFPcsgPyJiQXZc2yxtOTncJhG1GfzSQbALNltD5qs98Gha2c\nh3bc08fCFrHFult+FUU9Nnuc8yanErD2np40mrN3C6pHDoXsFWENtjplBI59Oz+I\nc88=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx\nEjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT\nVFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5\nNTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT\nB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF\n10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz\n0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh\nMBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH\nzIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc\n46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2\nyKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi\nlaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP\noA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA\nBDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE\nqYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm\n4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB\n/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL\n1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn\nLhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF\nH6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo\nRI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+\nnile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh\n15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW\n6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW\nnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j\nwa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz\naGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy\nKwbQBM0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNV\nBAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu\nMRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQy\nMDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx\nEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjEw\nggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy3QRk\nD2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/o\nOI7bm+V8u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3A\nfQ+lekLZWnDZv6fXARz2m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJe\nIgpFy4QxTaz+29FHuvlglzmxZcfe+5nkCiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8n\noc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTaYVKvJrT1cU/J19IG32PK\n/yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6vpmumwKj\nrckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD\n3AjLLhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE\n7cderVC6xkGbrPAXZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkC\nyC2fg69naQanMVXVz0tv/wQFx1isXxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLd\nqvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud\nDwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ04IwDQYJKoZI\nhvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR\nxVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaA\nSfX8MPWbTx9BLxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXo\nHqJPYNcHKfyyo6SdbhWSVhlMCrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpB\nemOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5GfbVSUZP/3oNn6z4eGBrxEWi1CXYBmC\nAMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85YmLLW1AL14FABZyb\n7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKSds+x\nDzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvk\nF7mGnjixlAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqF\na3qdnom2piiZk4hA9z7NUaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsT\nQ6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJa7+h89n07eLw4+1knj0vllJPgFOL\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV\nBAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu\nMRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy\nMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx\nEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw\nggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe\nNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH\nPWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I\nx2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe\nQTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR\nyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO\nQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912\nH9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ\nQfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD\ni/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs\nnLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1\nrqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud\nDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI\nhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM\ntCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf\nGopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb\nlvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka\n+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal\nTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i\nnSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3\ngzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr\nG5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os\nzMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x\nL4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD\nTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y\naXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx\nMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j\naWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP\nT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03\nsQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL\nTIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5\n/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp\n7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz\nEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt\nhxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP\na931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot\naK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg\nTnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV\nPKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv\ncWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL\ntbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd\nBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB\nACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT\nej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL\njOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS\nESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy\nP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19\nxIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d\nCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN\n5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe\n/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z\nAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ\n5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID6DCCAtCgAwIBAgIQXCwtpvvXVopPgEL9qP3rJjANBgkqhkiG9w0BAQsFADCB\njTELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJl\nMRwwGgYDVQQLExNXZWxscyBGYXJnbyBCYW5rIE5BMT4wPAYDVQQDEzVXZWxsc1Nl\nY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAwMSBHMjAe\nFw0xMjA4MDkxNjM2NDVaFw0zMDEyMjYxNjQ2MzNaMIGNMQswCQYDVQQGEwJVUzEg\nMB4GA1UEChMXV2VsbHMgRmFyZ28gV2VsbHNTZWN1cmUxHDAaBgNVBAsTE1dlbGxz\nIEZhcmdvIEJhbmsgTkExPjA8BgNVBAMTNVdlbGxzU2VjdXJlIFB1YmxpYyBSb290\nIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IDAxIEcyMIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAwyWEb3XDCHkxFHAv0geLGyu5Ao0guOzJ+S4atX73DJUc\nx9qNric9AxqppOpwiThrd45xOhlAjKmzFPwB7Fti2IpnvXT2eFdJ53SATQrXDLM3\nRntUn1xBYvIuDifZT4hcuBUa2VvZMJ3Aqag2tdeULyptO19o6yN3Vs4IP/6Y3nuX\n8diTmRFCCYUGWwdb9Pkkk9Z6vc//AT6P0/8cvVI7MREroqyvwJHslOf0VAmMB2gN\nicZ1ub42Zm0jmiZDjbX/gikaU/X3IIJQJBX8t1JGdUdpAwtL1QPvsTx+Hjxk0i2B\no2j0VFrXtkFn4u6TTgcoAyVfvFhqGhGHg1+EZd3TFQIDAQABo0IwQDAOBgNVHQ8B\nAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUIckl02r+pBL/5t1X\nV8+PPnKECM0wDQYJKoZIhvcNAQELBQADggEBAArE2ntHUGAvYnFg+yWyyCGyui4a\nYxW+/SR6HI7lr2AZD29yHWLwh8GuPufUesF30mluNZ46GSWZWMrGtvViK4UJAgnf\nEwvFUo8rEjug+o9EqIormGCHwgbAzNsaXjGi+L5oKbdB64E96EOY1zOH7zUWidaL\nmWbECjNWzFmuPMeLwAEVkhy7pNyfmvgEdRnquBkSInGX8OAmrWDERyEsQfwWI+sE\nHC0F6n7eE4QtaItigOdnwzGafqicKT/xQIFJJ0dP8tb4lqjhlqo3Q+j7oore26FH\n4K1RoQNutEB2TJM7IbQRx+lKSWDwXResA3FLrQjVeGjNZxomzljPjzjRG2c=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFSzCCAzOgAwIBAgIRALZLiAfiI+7IXBKtpg4GofIwDQYJKoZIhvcNAQELBQAw\nPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp\nY2F0aW9uIEF1dGhvcml0eTAeFw0xMjA5MjgwODU4NTFaFw0zNzEyMzExNTU5NTla\nMD8xCzAJBgNVBAYTAlRXMTAwLgYDVQQKDCdHb3Zlcm5tZW50IFJvb3QgQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC\nAQC2/5c8gb4BWCQnr44BK9ZykjAyG1+bfNTUf+ihYHMwVxAA+lCWJP5Q5ow6ldFX\neYTVZ1MMKoI+GFy4MCYa1l7GLbIEUQ7v3wxjR+vEEghRK5lxXtVpe+FdyXcdIOxW\njuVhYC386RyA3/pqg7sFtR4jEpyCygrzFB0g5AaPQySZn7YKk1pzGxY5vgW28Yyl\nZJKPBeRcdvc5w88tvQ7Yy6gOMZvJRg9nU0MEj8iyyIOAX7ryD6uBNaIgIZfOD4k0\neA/PH07p+4woPN405+2f0mb1xcoxeNLOUNFggmOd4Ez3B66DNJ1JSUPUfr0t4urH\ncWWACOQ2nnlwCjyHKenkkpTqBpIpJ3jmrdc96QoLXvTg1oadLXLLi2RW5vSueKWg\nOTNYPNyoj420ai39iHPplVBzBN8RiD5C1gJ0+yzEb7xs1uCAb9GGpTJXA9ZN9E4K\nmSJ2fkpAgvjJ5E7LUy3Hsbbi08J1J265DnGyNPy/HE7CPfg26QrMWJqhGIZO4uGq\ns3NZbl6dtMIIr69c/aQCb/+4DbvVq9dunxpPkUDwH0ZVbaCSw4nNt7H/HLPLo5wK\n4/7NqrwB7N1UypHdTxOHpPaY7/1J1lcqPKZc9mA3v9g+fk5oKiMyOr5u5CI9ByTP\nisubXVGzMNJxbc5Gim18SjNE2hIvNkvy6fFRCW3bapcOFwIDAQABo0IwQDAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTVZx3gnHosnMvFmOcdByYqhux0zTAOBgNV\nHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAJA75cJTQijq9TFOjj2Rnk0J\n89ixUuZPrAwxIbvx6pnMg/y2KOTshAcOD06Xu29oRo8OURWV+Do7H1+CDgxxDryR\nT64zLiNB9CZrTxOH+nj2LsIPkQWXqmrBap+8hJ4IKifd2ocXhuGzyl3tOKkpboTe\nRmv8JxlQpRJ6jH1i/NrnzLyfSa8GuCcn8on3Fj0Y5r3e9YwSkZ/jBI3+BxQaWqw5\nghvxOBnhY+OvbLamURfr+kvriyL2l/4QOl+UoEtTcT9a4RD4co+WgN2NApgAYT2N\nvC2xR8zaXeEgp4wxXPHj2rkKhkfIoT0Hozymc26Uke1uJDr5yTDRB6iBfSZ9fYTf\nhsmL5a4NHr6JSFEVg5iWL0rrczTXdM3Jb9DCuiv2mv6Z3WAUjhv5nDk8f0OJU+jl\nwqu+Iq0nOJt3KLejY2OngeepaUXrjnhWzAWEx/uttjB8YwWfLYwkf0uLkvw4Hp+g\npVezbp3YZLhwmmBScMip0P/GnO0QYV7Ngw5u6E0CQUridgR51lQ/ipgyFKDdLZzn\nuoJxo4ZVKZnSKdt1OvfbQ/+2W/u3fjWAjg1srnm3Ni2XUqGwB5wH5Ss2zQOXlL0t\nDjQG/MAWifw3VOTWzz0TBPKR2ck2Lj7FWtClTILD/y58Jnb38/1FoqVuVa4uzM8s\niTTa9g3nkagQ6hed8vbs\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFojCCA4qgAwIBAgIGC4LclDN2MA0GCSqGSIb3DQEBCwUAMHAxCzAJBgNVBAYT\nAkNBMSswKQYDVQQKEyJDYXJpbGxvbiBJbmZvcm1hdGlvbiBTZWN1cml0eSBJbmMu\nMSIwIAYDVQQLExlDZXJ0aWZpY2F0aW9uIEF1dGhvcml0aWVzMRAwDgYDVQQDEwdD\nSVNSQ0ExMB4XDTEyMTAxNjE4MjgzM1oXDTMyMTAxNjE4MjgzM1owcDELMAkGA1UE\nBhMCQ0ExKzApBgNVBAoTIkNhcmlsbG9uIEluZm9ybWF0aW9uIFNlY3VyaXR5IElu\nYy4xIjAgBgNVBAsTGUNlcnRpZmljYXRpb24gQXV0aG9yaXRpZXMxEDAOBgNVBAMT\nB0NJU1JDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDEdvFial/N\nKc0ENn9uYX5z9J1m3yJamoNEgWb9ThGwPqzoiLJTOf/jur7U/9OF2L1br2hPM6y4\nFH0SW3qVa8c2/iuP9IhgiTqqWThMwV1VgaXf2B8xetOjTvBRy8Mxh64L3speG6F0\nOPCSd3E8yxN+oMEKmL3YuPhUNJhOZxaaV0smhl8bZnKqwfJogp1YQXxxIuLPATH+\n4uBWqWjgrTOvNTkunG4GTPMjdi9pJugFOWm39Uga99/ZOTcyVREnBIEfnTyLjINS\nd8GuLM0rKkrlLfEZabqHXoud4HHIdNLN7m44N2pdGQDSdt2i6247qh31NgZPX15s\nwhDz3W+12nla/tVGRDRIr4YANHwkhN1FkPkWgqyokdTpRjNvfrpHH+Hvr+VQ1sb5\np+1sl6orKU5dxfge9nTJqyT4DVPHaBW+/FyrPXIL0nAEtxbjaanxZ7rGAEx7gDQ1\nLl7tH6Al96WCahB/v49Zb8NGpspCTkIjhQY5NYy18dfBI0JF/S8lcfjzB9MHaL7b\nmGwq9qVH97BlYK2ufOYRHSdUCGWw2ILAYWvpfo8i1nEda0EgZdhXmh98DlpU4JSw\nbXXvKDI1PFXDbWf4JL37QPNanTbZNUy74mvZsTYP5G8gGsVvesOROa+vzPP2vSCG\nutMkITwfNynmn/wav5jfPLogIRKpwjoqkwIDAQABo0IwQDAdBgNVHQ4EFgQU6pUV\n2lw5AOKa28S6LWf6ofd1NO0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAcYwDQYJKoZIhvcNAQELBQADggIBADXQ2Lie8gn48J+ybkiy1+qhmyiJOc3+Fmod\n6ZyCX1FHOvWe0byuH5/iXErI7O1GQvF8QwcV326X9u2G/J/FCF6CDqMuqAouvI4b\nMRIo9nkowSK20ZVpQOhZCSeikWR26tATjXD8ZcNvEZ8qSMqnYvWDFOUaFseRi7QJ\nxc574+QdbZei6csmHmu03D6Ddi9eTahoiVT9TtJGqED22Mp4zzYaPVlljJv1Kx9M\ngt94eE0mSkdprW8zHwMeIk7ZBlmeRvxQNV/GhRvkG/gAyeDTOqsmQ81H+lr4hQvH\nMtq1DS0wKTp5sxTppQ9wJdGNCVCU7U2SnjA3QNtaeEmPDzkvvS7XqwiUySmK992M\nvYJ8MFti6DVGVjhdkfYOb4zulZ/9dJ3t7RCrzouPt61/TWlJ8McRVZuagvei+jPy\nRBH6FUtGqZtrl0LWtLcJERR5U6bnfy0nOgo0JETOVYx6gHVzAkvi+kaUfTMUDUJW\nuaDmL4VIkZ9EuqEoqbEfiXomClNchbl8hJiMKGCltnqNPaAAPdx/qkjpqC6sX96H\nLVykaxbqveiVtc54CfhxNuWQaNIHlrq8AIsOmG1NcFPAw8wbE5xImpk9EsAnjmGS\nTGhSb40DHIn104bA/3FJTyBr/dFvkST18UcjTVnf0L1JQv1AOD7i8QVcJegQ5FoC\nA+O7fCUq\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICpzCCAi2gAwIBAgIQTHm1miicdjFk9YlE0JEC3jAKBggqhkjOPQQDAzCBlDEL\nMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYD\nVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRlYyBD\nbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0g\nRzQwHhcNMTIxMDE4MDAwMDAwWhcNMzcxMjAxMjM1OTU5WjCBlDELMAkGA1UEBhMC\nVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZTeW1h\nbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRlYyBDbGFzcyAzIFB1\nYmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcq\nhkjOPQIBBgUrgQQAIgNiAARXz+qzOU0/oSHgbi84csaHl/OFC0fnD1HI0fSZm8pZ\nZf9M+eoLtyXV0vbsMS0yYhLXdoan+jjJZdT+c+KEOfhMSWIT3brViKBfPchPsD+P\noVAR5JNGrcNfy/GkapVW6MCjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\nBTADAQH/MB0GA1UdDgQWBBQknbzScfcdwiW+IvGJpSwVOzQeXjAKBggqhkjOPQQD\nAwNoADBlAjEAuWZoZdsF0Dh9DvPIdWG40CjEsUozUVj78jwQyK5HeHbKZiQXhj5Q\nVm6lLZmIuL0kAjAD6qfnqDzqnWLGX1TamPR3vU+PGJyRXEdrQE0QHbPhicoLIsga\nxcX+i93B3294n5E=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF9jCCA96gAwIBAgIQZWNxhdNvRcaPfzH5CYeSgjANBgkqhkiG9w0BAQwFADCB\nlDELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8w\nHQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRl\nYyBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5\nIC0gRzYwHhcNMTIxMDE4MDAwMDAwWhcNMzcxMjAxMjM1OTU5WjCBlDELMAkGA1UE\nBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZT\neW1hbnRlYyBUcnVzdCBOZXR3b3JrMUUwQwYDVQQDEzxTeW1hbnRlYyBDbGFzcyAz\nIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzYwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC3DrL6TbyachX7d1vb/UMPywv3\nYC6zK34Mu1PyzE5l8xm7/zUd99Opu0Attd141Kb5N+qFBXttt+YTSwZ8+3ZjjyAd\nLTgrBIXy6LDRX01KIclq2JTqHgJQpqqQB6BHIepm+QSg5oPwxPVeluInTWHDs8GM\nIrZmoQDRVin77cF/JMo9+lqUsITDx7pDHP1kDvEo+0dZ8ibhMblE+avd+76+LDfj\nrAsY0/wBovGkCjWCR0yrvYpe3xOF/CDMSFmvr0FvyyPNypOn3dVfyGQ7/wEDoApP\nLW49hL6vyDKyUymQFfewBZoKPPa5BpDJpeFdoDuw/qi2v/WJKFckOiGGceTciotB\nVeweMCRZ0cBZuHivqlp03iWAMJjtMERvIXAc2xJTDtamKGaTLB/MTzwbgcW59nhv\n0DI6CHLbaw5GF4WU87zvvPekXo7p6bVk5bdLRRIsTDe3YEMKTXEGAJQmNXQfu3o5\nXE475rgD4seTi4QsJUlF3X8jlGAfy+nN9quX92Hn+39igcjcCjBcGHzmzu/Hbh6H\nfLPpysh7avRo/IOlDFa0urKNSgrHl5fFiDAVPRAIVBVycmczM/R8t84AJ1NlziTx\nWmTnNi/yLgLCl99y6AIeoPc9tftoYAP6M6nmEm0G4amoXU48/tnnAGWsthlNe4N/\nNEfq4RhtsYsceavnnQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/\nBAUwAwEB/zAdBgNVHQ4EFgQUOXEIAD7eyIbnkP/k/SEPziQZFvYwDQYJKoZIhvcN\nAQEMBQADggIBAFBriE1gSM5a4yLOZ3yEp80c/ekMA4w2rwqHDmquV64B0Da78v25\nc8FftaiuTKL6ScsHRhY2vePIVzh+OOS/JTNgxtw3nGO7XpgeGrKC8K6mdxGAREeh\nKcXwszrOmPC47NMOgAZ3IzBM/3lkYyJbd5NDS3Wz2ztuO0rd8ciutTeKlYg6EGhw\nOLlbcH7VQ8n8X0/l5ns27vAg7UdXEyYQXhQGDXt2B8LGLRb0rqdsD7yID08sAraj\n1yLmmUc12I2lT4ESOhF9s8wLdfMecKMbA+r6mujmLjY5zJnOOj8Mt674Q5mwk25v\nqtkPajGRu5zTtCj7g0x6c4JQZ9IOrO1gxbJdNZjPh34eWR0kvFa62qRa2MzmvB4Q\njxuMjvPB27e+1LBbZY8WaPNWxSoZFk0PuGWHbSSDuGLc4EdhGoh7zk5//dzGDVqa\npPO1TPbdMaboHREhMzAEYX0c4D5PjT+1ixIAWn2poQDUg+twuxj4pNIcgS23CBHI\nJnu21OUPA0Zy1CVAHr5JXW2T8VyyO3VUaTqg7kwiuqya4gitRWMFSlI1dsQ09V4H\nMq3cfCbRW4+t5OaqG3Wf61206MCpFXxOSgdy30bJ1JGSdVaw4e43NmUoxRXIK3bM\nbW8Zg/T92hXiQeczeUaDV/nxpbZt07zXU+fucW14qZen7iCcGRVyFT0E\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk\nMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH\nbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX\nDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD\nQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ\nFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F\nuOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX\nkPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs\newv4n4Q=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk\nMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH\nbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX\nDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD\nQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc\n8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke\nhOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD\nVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI\nKoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg\n515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO\nxwy8p2Fp8fc74SrL+SvzZpA3\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG\nA1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3\nd3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu\ndHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq\nRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy\nMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD\nVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0\nL2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g\nZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD\nZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi\nA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt\nByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH\nBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC\nR98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX\nhTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID9zCCAt+gAwIBAgILMTI1MzcyODI4MjgwDQYJKoZIhvcNAQELBQAwWDELMAkG\nA1UEBhMCSlAxHDAaBgNVBAoTE0phcGFuZXNlIEdvdmVybm1lbnQxDTALBgNVBAsT\nBEdQS0kxHDAaBgNVBAMTE0FwcGxpY2F0aW9uQ0EyIFJvb3QwHhcNMTMwMzEyMTUw\nMDAwWhcNMzMwMzEyMTUwMDAwWjBYMQswCQYDVQQGEwJKUDEcMBoGA1UEChMTSmFw\nYW5lc2UgR292ZXJubWVudDENMAsGA1UECxMER1BLSTEcMBoGA1UEAxMTQXBwbGlj\nYXRpb25DQTIgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKaq\nrSVl1gAR1uh6dqr05rRL88zDUrSNrKZPtZJxb0a11a2LEiIXJc5F6BR6hZrkIxCo\n+rFnUOVtR+BqiRPjrq418fRCxQX3TZd+PCj8sCaRHoweOBqW3FhEl2LjMsjRFUFN\ndZh4vqtoqV7tR76kuo6hApfek3SZbWe0BSXulMjtqqS6MmxCEeu+yxcGkOGThchk\nKM4fR8fAXWDudjbcMztR63vPctgPeKgZggiQPhqYjY60zxU2pm7dt+JNQCBT2XYq\n0HisifBPizJtROouurCp64ndt295D6uBbrjmiykLWa+2SQ1RLKn9nShjZrhwlXOa\n2Po7M7xCQhsyrLEy+z0CAwEAAaOBwTCBvjAdBgNVHQ4EFgQUVqesqgIdsqw9kA6g\nby5Bxnbne9owDgYDVR0PAQH/BAQDAgEGMHwGA1UdEQR1MHOkcTBvMQswCQYDVQQG\nEwJKUDEYMBYGA1UECgwP5pel5pys5Zu95pS/5bqcMRswGQYDVQQLDBLmlL/lupzo\nqo3oqLzln7rnm6QxKTAnBgNVBAMMIOOCouODl+ODquOCseODvOOCt+ODp+ODs0NB\nMiBSb290MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH+aCXWs\nB9FydC53VzDCBJzUgKaD56WgG5/+q/OAvdVKo6GPtkxgEefK4WCB10jBIFmlYTKL\nnZ6X02aD2mUuWD7b5S+lzYxzplG+WCigeVxpL0PfY7KJR8q73rk0EWOgDiUX5Yf0\nHbCwpc9BqHTG6FPVQvSCLVMJEWgmcZR1E02qdog8dLHW40xPYsNJTE5t8XB+w3+m\nBcx4m+mB26jIx1ye/JKSLaaX8ji1bnOVDMA/zqaUMLX6BbfeniCq/BNkyYq6ZO/i\nY+TYmK5rtT6mVbgzPixy+ywRAPtbFi+E0hOe+gXFwctyTiLdhMpLvNIthhoEdlkf\nSUJiOxMfFui61/0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGgTCCBGmgAwIBAgIEUVLFjDANBgkqhkiG9w0BAQ0FADCBzzELMAkGA1UEBhMC\nVEgxSTBHBgNVBAoMQEVsZWN0cm9uaWMgVHJhbnNhY3Rpb25zIERldmVsb3BtZW50\nIEFnZW5jeSAoUHVibGljIE9yZ2FuaXphdGlvbikxNzA1BgNVBAsMLlRoYWlsYW5k\nIE5hdGlvbmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxPDA6BgNVBAMM\nM1RoYWlsYW5kIE5hdGlvbmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMTAeFw0xMzAzMjcwOTQwMjJaFw0zNjAzMjcxMDEwMjJaMIHPMQswCQYDVQQG\nEwJUSDFJMEcGA1UECgxARWxlY3Ryb25pYyBUcmFuc2FjdGlvbnMgRGV2ZWxvcG1l\nbnQgQWdlbmN5IChQdWJsaWMgT3JnYW5pemF0aW9uKTE3MDUGA1UECwwuVGhhaWxh\nbmQgTmF0aW9uYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTE8MDoGA1UE\nAwwzVGhhaWxhbmQgTmF0aW9uYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0\neSAtIEcxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1rpK5Izxmi6J\nF8JA84IAMf4TAnplygjYqyKxAppxNEpkWYLlQkbrI/aLWiKxzzbnc20UbfdJlF7v\n5zRZZ/aoz1ZZI4RV4vsaEcqj+YqrZx6CE9CLOZq/D8IPPNZh2OqbzxUOvtTwzD9z\nnAT0onFzfYCwnTHxBvmwE+WISTD2Fn2IVyk6LKKMkJjOERbOTVEP/MeyzPJmGCGA\nBYitudDFC3gB/k7SCIs28VbPbrpzJgvW96VGamlOlranBlbM5i4xn26L7ZwAVUf0\ne6Z6tt8BHUgEC6tCwnBKlL38rFHyqz/W62DfCP/1ErKJKnq5RZklfXzvzxXQSCwQ\n1tS8CCe1hinU49PEKpAS9qIq+YuvFv8C83puz6LLarTgcgv7PoV/4ofgL0Mj+IXJ\nmerWQN6g++fedv+PgDnrZxITpvvlo/wmgFlj8tIj6x/GSHNRnbezoFuraoj5v/tx\nUdxutnbvsFvyy4gwugbbG0HTVbSttOogIfzUd7Y9W6EMLSUhUiNS1zRTbRYEUmb4\n1erxLFjyO7HxfkO13IK4XuOH4aOkX+eJDryc6Sk6JafYT2qH1JZElxgWh8JxUoXO\neoytHme+ui2/oyEnxecw6QaZG7AM475SZZNNYUvyOOaPGPECUpgupg4dBc8m7AEj\nBzb24BM3qUeIA4dHy92yAR9fZBsEm8UCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB\n/zAOBgNVHQ8BAf8EBAMCAQYwHwYDVR0jBBgwFoAUfyN2t4Mqcfcs0YgP3UxfMgpo\nu38wHQYDVR0OBBYEFH8jdreDKnH3LNGID91MXzIKaLt/MA0GCSqGSIb3DQEBDQUA\nA4ICAQANZRxaB6merEzJX0/dMWzZ4lMdP5GNWrOMvTSeLk3KWNOvWWJJNnOwYXYR\nvos2x5Sq+DZpByDfXC8L9o4CFu9SBjjd7TRgqodeF844bVBN5d/lUb4dBJb03Orl\n2eqO3p90y4KUU4Fs+14s1aF1lk37MFzNYaCeocyCuVJyC4djYXthNHS2Lt3i4Ye1\nSRRhFUdKSz53uQjSNk9YZ0KJgHhaEiPtRTvdvyAmVPxbP2ABGEHjZ3UTtyoVcMzL\nedIU+PPC4CoQ9/lC2NzaCtMBBdtXmMm26wyZCsqMfe87FijA91/hR1HT+AZFB/AL\nusKcmOzSf01+/Qb8c8LCVRJi0CNE3yLk+HnnpRBOPsmOqoPpNuqrecYFhM2WaHx0\nrD8y/67JQOyPUL9QqLdO1a02atcnM/rn2C3ZN5iFG6YM6nsQE3AenojF3D6OuQ1V\n3wHO0El2UdsQYnhBrWljpZUJtxgGb/0EZ9QQD07bO18MY3zrZL1uSwCogfqSMoYw\njAm/fVg/ZQ2pN9FF42ZpxGj0YqmoHjfZLplJoLAGjEB/hbH18UxLOKAIzCrZlsDs\nwA08LkVXw++V2rbL7ltlqCsyr8kn+RVTN3VYH0vql6IiXGdW4qDMNcSswzFAuZwD\ner3JSA7qahXanLx4b8kV52QD2UkTZkVLLfSEmbPqpxKV5ZMu/A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFyDCCA7CgAwIBAgIBATANBgkqhkiG9w0BAQsFADB1MQswCQYDVQQGEwJQTDEk\nMCIGA1UECgwbVGVsZWtvbXVuaWthY2phIFBvbHNrYSBTLkEuMScwJQYDVQQLDB5T\naWduZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFzAVBgNVBAMMDlNpZ25ldCBS\nb290IENBMB4XDTEzMDUwNjExMzgwNFoXDTM4MDUwNjExMzgwNFowdTELMAkGA1UE\nBhMCUEwxJDAiBgNVBAoMG1RlbGVrb211bmlrYWNqYSBQb2xza2EgUy5BLjEnMCUG\nA1UECwweU2lnbmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRcwFQYDVQQDDA5T\naWduZXQgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKlk\ny7gx4rUPgCcGzEVe6g1f13dql2i2XaB4BUSrMLB6h+9i7ghYVVwX+iuADhx1p2d2\nSpbDKGt4+Vrf+mp5p4pUHSqWhvG1F9VdGlb3QBC3DuEH3GcLmaIACNQEInemQ46f\n1TCq+p2BRvI9zl7CfsF8nzOvJtod3mD3gqc2zPXIwAKPks9uTv/7/mE/rr+9lmf+\n0K8d1iP3MOZ7iF3p9TNEyoq7pztZjnAXaSgXuxBWpcK0Cw37tHeJEERVbYmr1U0y\nudf3aZz9ta8DsiG2LGD1X9HCVIgvYO+cVIa1QQczLGwLHBLaR5lmNK6g7G3QY5d/\nxAWAk/hCLFTY/tqVGGuF8lz5doc2HrGAH0DgCwqT1K5acVcNOu/h7Htd+BCaN3yp\nFqLEjlc7EBt2rahxQDOFAz9t2B495zBTx+Pq19AwVcSaZ0J8t0Br3KlEUPLjLkVi\ncby5bigFOXb1WeqhAzB04N+yCiMVTuNYOqJPeMiIW1GSzjoqNg/O37MCTy78hapD\n1ga1eLfIuyMbRY+nNTTKqhQ31Z97MFaP6VcKRqcBl5ssp03/WT3unjMsLPMgu1j4\ncx8B0EMiwygXtiQAElW4WxO8v9fZvVn7wlNp9a5SJs2sUrfIHVOaoQSgAkNQnRKp\nwG5Rwe0RTt/vxBQhurqhDpWDNVLQ559S1ZL5IsOHAgMBAAGjYzBhMA8GA1UdEwEB\n/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB8GA1UdIwQYMBaAFFB7Ca9pLS+14JGv\njZITK7Ey6EnQMB0GA1UdDgQWBBRQewmvaS0vteCRr42SEyuxMuhJ0DANBgkqhkiG\n9w0BAQsFAAOCAgEAECWnACU9/o1G1kDHL9laJIVImKPg0UCh06PABJU0IXYW6daL\nKqbRNiY+w+VIjmv4BtPJbSCLwfl4hyztdUEoPnD5wnFtMQw34BXi217wwK5QFeyI\nUVODyaXyz6zC5swQx2wYd1ZYtSSahwNhdk8eWPPblTJ4ESuxIBOxftLl5Hu0MGUD\nixvi6N7qEt6Xal4ARdbgWyqQodAr6NF2SWkW79uCtFMySCVsdPDK987d4UmPUtVU\nFfQIrwZnU5jnrOw1ipsT9B39gegbMc7z4IWS64NazrQXibBO4WFwX+ixMs6bHgp7\nGS3IaDYzpFb1ukm9L/yzCrJrml4++0wYr1zwX9mx2wkdRlLHcNu4mCnUOWpePGKH\neoqPdr/cp2i6i8U5xglPb3ZCTM8AUwq0H1jGShX9+uG8t3xUhk+8d3kkEk1kXbR6\n22k2dGbofeRbKfIw/bXd3qEhYWZgJTtIb86rj02iTMsM+8E29FDBbCxpXEpEHcRc\nJ00k907hP6tlA9O4kXzwhTjWikdELLAOCaWy0vfq7PR1tmtVS8EpO6ZEm8IQ7HqO\nTB2joiHcZcaAHtSXT/SAUwq6XY07doAnOllbH/VWhuHoili3mvdC71qoSu5U+iSe\nn7jM7KII4qyCjdIzI8Ju4+T/mfVcZ8WydiIbbSz2BveONFEi6PYZar9QmoI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFrjCCA5agAwIBAgIQUJZucr0Q1oxPa8diP5xwODANBgkqhkiG9w0BAQsFADBx\nMQswCQYDVQQGEwJMVDErMCkGA1UEChMiU2thaXRtZW5pbmlvIHNlcnRpZmlrYXZp\nbW8gY2VudHJhczEZMBcGA1UECxMQQ0EgUk9PVCBTZXJ2aWNlczEaMBgGA1UEAxMR\nU1NDIEdETCBDQSBSb290IEEwHhcNMTMwNjA0MTMwMDQ3WhcNMzMwNjA0MTMwMDQ3\nWjBxMQswCQYDVQQGEwJMVDErMCkGA1UEChMiU2thaXRtZW5pbmlvIHNlcnRpZmlr\nYXZpbW8gY2VudHJhczEZMBcGA1UECxMQQ0EgUk9PVCBTZXJ2aWNlczEaMBgGA1UE\nAxMRU1NDIEdETCBDQSBSb290IEEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCfXEr8HGu3GZfZATc+CukYhtMF6qLa3wmCV+5tK42aFj1VPonXyb7hAaOA\nNaNG7OER7ag8leU6UoHKTpgIKg+E3LvppPl5tknCFZ6glegPSPdQ1/mmQ9QHCzBB\nyTYSYrdseAsGPy6znuow/UFjT4QsN84Hpjlke3EVWysB8td9mA0YPtuFmuABUCEk\nuBujY0PTgVtNDIFOOGvOYMXqB+In4uv2w1SayMmz0SsyNwK8bXuekHcjjZMTJjuH\nV6NlTyZYFGpjJZrlYfocV/0NLGkPxgrwJjkXAqPWc4FCw0Ixg4vg+ktOWGExKJI8\nxskQCMkMW0SsY8LXYhnyce4gt0mDGZ5H2lbFHKykOWgXXxEabKqlko+9G8vF4AKA\nVdNwU+WLKv5C6r07XONSAH14PybMEa400TIM+Hug0X0944q8vh4ekj84sl8yXjXE\nfsKSDZ22y1nV6xJq3XIhURGwc+Uy6dbMDt2zOVoi7+T16QZphip8c68YInMsNiXc\nValSMbOKjhV9sk4Qe1CKAEy6h+JFU3d+TWUCa4yTtmt17e+Wt0iOqOC6uYKyUm0h\n/5K60T6wXLGrGQ4Zc0Yr01JIZTTaBDXSeD7PYzWkU+ZL41CDvfObh7Ih2kihekvs\nsuLx1CUFlFMWTCtmJBDI4NecEqSUwgEjk6EApuBuuzni9XpoqQIDAQABo0IwQDAO\nBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUoBF+H+SU\nZFE7Ejl6bN1Jk/n9wFwwDQYJKoZIhvcNAQELBQADggIBAAzxS4zhTxYW0upikrat\n1FKOCxlkSznwmDlzSlLqTs2OZEewMI88Dy3aImXzGVgyPH+DjwoM5VTmqb64rpdW\n5rcNGXy9lyxqKqVWc4LeTpiLPRzE0Csru8UM+E7+La6/qWd/V7Nv7f+L01YM7zCM\nwV6m6VmKPC7cR8/MlF6DrBR2+n68DKMOXBuI7CsbNWiIsfV7xfOzxRq8+++1Xt/w\nOR51aO1EwksicD5ca5TJEKzw/cgvfiPigacbzgy6RTInUEU5rOD+ALQqdQcMZxu7\nccCC45dWl9Dkd1m5/3xnXIRluwg2qEtOkcJp/h3smhMfdTMsKcbpsGiQI/8jX3/G\nO6coELgfoojNZBYlT+OAt8BKgFfwkNs6sgIyINVryNgUQMnZOBlUOOvoZTtvXNVF\neq/b2diVnranlc0cCR0CHgHpBJVdhZc4Fb2ox5ne00RCXYaDQSR8UYmqQwknNOjx\nCrWWS7TzoP7yAI1qO3S5Q7lmuc/q6zfO/5vpI/hs0yP96Ongbvj7DVJAiqyAayAQ\nXdCo/ao9ORErL/9SkTqg3IrHdjYRWYW7MIqkSDCcYUOr1K927cC/F5R4NdtINwjU\njmoA6SLdyvDTEjg8mJ9gTG0/Qv3vjJq3HnF6GknUYMnrj/Tpxr9wVIjSx1c7Vs3X\nbtztDXR+5XVBkVeTNH2p9b2H\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFrjCCA5agAwIBAgIQPoxPvOQpg4JNhFWO1TWAzzANBgkqhkiG9w0BAQsFADBx\nMQswCQYDVQQGEwJMVDErMCkGA1UEChMiU2thaXRtZW5pbmlvIHNlcnRpZmlrYXZp\nbW8gY2VudHJhczEZMBcGA1UECxMQQ0EgUk9PVCBTZXJ2aWNlczEaMBgGA1UEAxMR\nU1NDIEdETCBDQSBSb290IEIwHhcNMTMwNjA0MTQyMDE1WhcNMzMwNjA0MTQyMTU1\nWjBxMQswCQYDVQQGEwJMVDErMCkGA1UEChMiU2thaXRtZW5pbmlvIHNlcnRpZmlr\nYXZpbW8gY2VudHJhczEZMBcGA1UECxMQQ0EgUk9PVCBTZXJ2aWNlczEaMBgGA1UE\nAxMRU1NDIEdETCBDQSBSb290IEIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCPlenS68FzJcc4Z/CDjlO8tsvOunPbTyf2IpA/Qr8h1t5igrRvBAVJCTt3\nAddLX1LS2RnHbXwMqToJYuQqGGmMoN3rrBO2DjkRgGlOY1/cPA362YxivmSFMjJZ\nl1CTid/7/9TYZXHHRlWiG5lhH9xQAMgXeehQsAxe5v52pgFOCchwbPqQs17cPQfN\nSaNOVl4ST2RBf34MFcOg3rOjKQZJRKFfbz+BoERN8HsKOCjtEu5jl8N7XYxPcd2V\nOtouqAFGCvNs6LXxHwgA8UCSGyYAMXU5RkkmuaTUcXcRpE8zzAnb2dEhS5JErM54\nYoIX+/oStH3V8obt9H6WFOaNA1KvzRei1Ryl/oGmmu195NkOMmYQj9vZMzGBfilX\n78yyoWDuilu5Zdt/G5osjycxiYoota+xVtQDIu4lT9iavdJsV7yDpkgfLFUHCTQr\nuXksAqWgX3x2nyQyPC2S3+tIV4eh9v4j+jSrifVoG44fqm4OpdIh0u+50bFJVzVa\nhNMe4gJtUhB/4oxNIdsyMhx9zJYiAy1qpwZCbW6Qh/ocXLBP0ANBE/oLU+bBEAJI\nC3dj9KWcUXuYZtfFdjLlb10UYX0Mu22VQNqpJsf3qcvS/ifBK/axaIb+42JSmVCO\nK95BIQcbh/VAHXCtz/3CQ6g1VhFCxcteZqHIqGj3/kxXYTZSgQIDAQABo0IwQDAO\nBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUcgNGh2H2\nHbPUlWO5UHduDauY/i8wDQYJKoZIhvcNAQELBQADggIBAAjbijKBdDNxFuwhhVNI\nCm8fcuPjBPgutz/zJJVPnO0T4YiCAvZm97exLYAnra64bf4jBxEIq3RhjCgS+fYQ\nNPDPtnyjdS0S1JTfdO6xmKux7iJiS1kff/4aZa1N4qQRPxMhtNg1i3ZApl+9MxHf\nmOMhXh2ju3g2AjvY/WSE2jfNWe38DNB0pGtxPDYSRJ5+bk8KIRxlH0sSbL+Octbd\nPgBwmAFFK+yVkOPTaTjnK51+ZVlb4duFymP+q7/k0P3kUroa5v7GkLp7zvGkYsVH\nviTHoHrlIeHGCOAMiYOPgGn97qDfekw600gqFr+uppW13Wgf+w61BYzRskR8YDBW\ndhe1NU+o1QrrwrVuAu6cXw6jsQGo5VNvfoNBHxXY/+HCthrxRpxkoBrgSsq4prSJ\nJO57lZli1OJAu86jmn0dcvMbgUF3AF7sPKIwBTzNfEg2E8gysGtvnzgoOGlce+bi\nrYO7bRPRLrfRdm9dMF65UEVI1kiAk1HJFqkQXWfGy35nfQVP9CDvJCVe7WdDxvtu\nefuy8sjJzkF8BeCti80KRS7iYp+XkfT5Y+zywmCK3Bv/Iaj/I4eMc42wOswfjzFy\nCv2Wod8aU9M2trB3Rt4D9sKALm+XI+ERzFGYP+5A//Q9m4h/jLvhWYa9CTQnXJ4K\nkzI7VSqpXgsND6mmUQTimyoR\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIIGDCCBgCgAwIBAgIGAT8vMVNvMA0GCSqGSIb3DQEBBQUAMIIBCjELMAkGA1UE\nBhMCRVMxEjAQBgNVBAgMCUJhcmNlbG9uYTFYMFYGA1UEBwxPQmFyY2Vsb25hIChz\nZWUgY3VycmVudCBhZGRyZXNzIGF0IGh0dHA6Ly93d3cuYW5mLmVzL2VzL2FkZHJl\nc3MtZGlyZWNjaW9uLmh0bWwgKTEnMCUGA1UECgweQU5GIEF1dG9yaWRhZCBkZSBD\nZXJ0aWZpY2FjaW9uMRcwFQYDVQQLDA5BTkYgQ2xhc2UgMSBDQTEaMBgGCSqGSIb3\nDQEJARYLaW5mb0BhbmYuZXMxEjAQBgNVBAUTCUc2MzI4NzUxMDEbMBkGA1UEAwwS\nQU5GIEdsb2JhbCBSb290IENBMB4XDTEzMDYxMDE3NDUyOVoXDTMzMDYwNTE3NDUy\nOVowggEKMQswCQYDVQQGEwJFUzESMBAGA1UECAwJQmFyY2Vsb25hMVgwVgYDVQQH\nDE9CYXJjZWxvbmEgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgaHR0cDovL3d3dy5h\nbmYuZXMvZXMvYWRkcmVzcy1kaXJlY2Npb24uaHRtbCApMScwJQYDVQQKDB5BTkYg\nQXV0b3JpZGFkIGRlIENlcnRpZmljYWNpb24xFzAVBgNVBAsMDkFORiBDbGFzZSAx\nIENBMRowGAYJKoZIhvcNAQkBFgtpbmZvQGFuZi5lczESMBAGA1UEBRMJRzYzMjg3\nNTEwMRswGQYDVQQDDBJBTkYgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQDHPi9xy4wynbcUbWjorVUgQKeUAVh937J7P37XmsfH\nZLOBZKIIlhhCtRwnDlg7x+BUvtJOTkIbEGMujDygUQ2s3HDYr5I41hTyM2Pl0cq2\nEuSGEbPIHb3dEX8NAguFexM0jqNjrreN3hM2/+TOkAxSdDJP2aMurlySC5zwl47K\nZLHtcVrkZnkDa0o5iN24hJT4vBDT4t2q9khQ+qb1D8KgCOb02r1PxWXu3vfd6Ha2\nmkdB97iGuEh5gO2n4yOmFS5goFlVA2UdPbbhJsb8oKVKDd+YdCKGQDCkQyG4AjmC\nYiNm3UPG/qtftTH5cWri67DlLtm6fyUFOMmO6NSh0RtR745pL8GyWJUanyq/Q4bF\nHQB21E+WtTsCaqjGaoFcrBunMypmCd+jUZXl27TYENRFbrwNdAh7m2UztcIyb+Sg\nVJFyfvVsBQNvnp7GPimVxXZNc4VpxEXObRuPWQN1oZN/90PcZVqTia/SHzEyTryL\nckhiLG3jZiaFZ7pTZ5I9wti9Pn+4kOHvE3Y/4nEnUo4mTxPX9pOlinF+VCiybtV2\nu1KSlc+YaIM7VmuyndDZCJRXm3v0/qTE7t5A5fArZl9lvibigMbWB8fpD+c1GpGH\nEo8NRY0lkaM+DkIqQoaziIsz3IKJrfdKaq9bQMSlIfameKBZ8fNYTBZrH9KZAIhz\nYwIDAQABo4IBfjCCAXowHQYDVR0OBBYEFIf6nt9SdnXsSUogb1twlo+d77sXMB8G\nA1UdIwQYMBaAFIf6nt9SdnXsSUogb1twlo+d77sXMA8GA1UdEwEB/wQFMAMBAf8w\nDgYDVR0PAQH/BAQDAgEGMIIBFQYDVR0RBIIBDDCCAQiCEWh0dHA6Ly93d3cuYW5m\nLmVzgQtpbmZvQGFuZi5lc6SB5TCB4jE0MDIGA1UECQwrR3JhbiBWaWEgZGUgbGVz\nIENvcnRzIENhdGFsYW5lcy4gOTk2LiAwODAxODESMBAGA1UEBwwJQmFyY2Vsb25h\nMScwJQYDVQQKDB5BTkYgQXV0b3JpZGFkIGRlIENlcnRpZmljYWNpb24xEjAQBgNV\nBAUTCUc2MzI4NzUxMDFZMFcGA1UECwxQSW5zY3JpdGEgZW4gZWwgTWluaXN0ZXJp\nbyBkZWwgSW50ZXJpb3IgZGUgRXNwYcOxYSBjb24gZWwgbnVtZXJvIG5hY2lvbmFs\nIDE3MS40NDMwDQYJKoZIhvcNAQEFBQADggIBADGB3clTJTMcaGs8j/NktDs2c7HI\nS3GApxTxog5JuUUUuOmA6Ju0BxXe+f4ZTi/Pb5IZSsBAoM4Gbfn8mkQyfh5BY7iS\nK3Fnzbl9GGF613eC3T+5Q4DI1lc6n8V+jVRIej9H4nMjH/wzbWmHZcKWA3L/fJXr\ns8iUrvRacyXx2FyCRUmqHgnca0VNOGt+obz1WUaOCmgWO8Ga06sylddooNLtOIHO\nvut26a583SDjFbstMWZfz+UD54Jmqr2KnTNmOHHWo/LzbtkErsZNMMlfNn7ri5ek\n1NHVrXOB8KaDszxQXxacwSMaXqpUU/X2Tx1DQK+Nb0mEBss9HQu0nfr2OeAxxxrc\nzt3fLv1Fsy2moQWCAQISMpIF149+VQAOoC5/u06yROCbBtMQniG8Ru8u2f+h5B2+\nIT3kJICXTanWfJST0WM3IOJ/efahqPaAMxkc669Zo3+Un9Zb9QfRmLkc/R3LHSFb\nQngpIwh04MnLhUaOMs4Y38uFUz8XHxJsW7pDxtMZdfGgEx94oNklvzrBP3rxeJxQ\n8FknN+Zaf2Lz2T4Q7srTH8ShMddMoiOCRFR5n3DbmamoCeyu5LxbZBud0M99RCoF\nf4Bov9yNQL8QqnP/ZtcwM2NjbfzYSPqDyt2l5e1oNGdbFewP7N+eaAHpltM7IdHE\nxJhqqSqPzE7W6RT9\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\nb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG\nEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\ncnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA\nn61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc\nbiJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp\nEgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA\nbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu\nYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB\nAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW\nBBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI\nQW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I\n0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni\nlmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9\nB5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv\nON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo\nIhNzbM8m9Yop5w==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\nZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg\nRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV\nUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\nY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq\nhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf\nZn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q\nRSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\nBAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD\nAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY\nJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv\n6pZjamVFkpUBtA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\nq2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\ntCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\nvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\nNeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\nFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\npLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\nMrY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\nZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe\nFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw\nEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x\nIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF\nK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG\nfp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO\nZ9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd\nBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx\nAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/\noAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8\nsycX\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg\nRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV\nUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\nY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y\nithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If\nxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV\nySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO\nDCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ\njdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/\nCNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi\nEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM\nfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY\nuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK\nchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t\n9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD\nggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2\nSV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd\n+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc\nfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa\nsjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N\ncCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N\n0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie\n4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI\nr/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1\n/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm\ngKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGQDCCBCigAwIBAgIIdPhg8eijj0EwDQYJKoZIhvcNAQELBQAwgasxCzAJBgNV\nBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDE8MDoGA1UECgwzTklTWiBOZW16ZXRp\nIEluZm9rb21tdW5pa8OhY2nDs3MgU3pvbGfDoWx0YXTDsyBacnQuMUswSQYDVQQD\nDEJGxZF0YW7DunPDrXR2w6FueWtpYWTDsyAtIEtvcm3DoW55emF0aSBIaXRlbGVz\nw610w6lzIFN6b2xnw6FsdGF0w7MwHhcNMTMwOTEzMTAyNzA0WhcNMzMwOTEzMTAy\nNzA0WjCBqzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MTwwOgYDVQQK\nDDNOSVNaIE5lbXpldGkgSW5mb2tvbW11bmlrw6FjacOzcyBTem9sZ8OhbHRhdMOz\nIFpydC4xSzBJBgNVBAMMQkbFkXRhbsO6c8OtdHbDoW55a2lhZMOzIC0gS29ybcOh\nbnl6YXRpIEhpdGVsZXPDrXTDqXMgU3pvbGfDoWx0YXTDszCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBALVCpWRI22SlN/gsuJkCUbmiKMT7cATE2WyKhvcP\niRekhMIubE71/0TvW0MOiX83NaVbyOQjW68ZnFvtrNYALttjysNU2K9n1MtgRKJP\nz6Te/B8xZla34d04ilP8zyMVi4qH/Qkw5ZhHBA4Waa8JBbzH1JBFj2hjvoJYN/vY\nTG+lrBV3daWIZDhc0mUPUwXOlDCXb3qB6WSYEtEeSp/B8xfbGTYQObgBs7d4TbUM\ne16qTp25zV04/39J/rdIrwNCbL5kG2H5zmt6m1BxAPNXl8UBdBurySZZbHq/Cpdn\nlrWARUgBRpxAFORhOCFbiWTiBTYToCrO24gEhkQ13JM0WVdq7VNj+ovCGBY89HHH\nPgwaEeTODyDDFyOro38TVay0/5bYwC96CZvbHJaNpoz8oWqma9EMnTGsmjH6UvmJ\nOfovU/PpkS5Qjqq4pCWvG4vZalKIVwrDC5pxn7zKRYrpudWVwbbCztENaUo2PK6N\nrMt19pAhwwmXzi0SdmJe6w6Pcl8rm7DJChXz/s/3RIRGAf3PZuzQMJd8bazROMFG\ncgcXDj77MObLNNW1cxNFIQ4dGWtIFtrokakG0Og9b/qM0bj1mQPx69i1abu4iU9S\nAqd+PtvsxZcGlftT6+DT58iPiJn/LreXmX2E81H9joND3vOv4DN0xBUcKRenSXPc\nwE7dAgMBAAGjZjBkMB0GA1UdDgQWBBTVqFEOeTByXrSsFg3TtevqwUvcOjASBgNV\nHRMBAf8ECDAGAQH/AgEDMB8GA1UdIwQYMBaAFNWoUQ55MHJetKwWDdO16+rBS9w6\nMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAYfZkTup7l8LCAtlZ\nMoBtgpKi+k2Cc2ZanYLSVWIw+CDNp9OJwcZzxNhdST3Ovgx6HchpbD367wD2gZqN\nO1VPDJ1W2afmTeZrsKK1oP7fXYNbqxHyaxivq2bbG8lLGvdE3fGcgqyaXqioqDGe\n3pzBQiKMxBOE5SxDBhspaTPX4AcCH6vuSZ7Xw4iuWRuXy/gbZWABzG3hQCAtSyEB\n7B4ssYFr3saM9TSwjMOb3lg+EU3oSEyHlu5aR0tCb57og0iCuZrpPET5UZNUq5RF\n+aiVrqaIefXmkqhYIi7UlEwYuq39p4VaghNqva5bwCwZXdiTwN11QDNp2U4mCjaH\npAEM4d+tDBkYX4jKNbEKe4EHZvl/Dy1tGYrk5IO7Qx1eT9LhKTjBH/Vco1Rg6/hD\n3uaVBJmH4cupJDp5LRpwZZ8RJ104LkUNW/gRWS4ONRNq16dUBP5S+EwV5gOZXLKH\n/KpGCPjTaAdgHC8nUnWTAtjd07GH1P2ZdnzB/AOq78eCSXr6+kvah9sFn1jib75j\n+hqjNMHPukwiAAcFgF8F5gFzV9SR4dBh74Yo433MyjKX47NtvL/wCaAtxABUM20F\nh/SHJB2Fzd7DOzeg5Qiv44sBHbgdNmOiEOElK2xS4B3Gx/ZtneDHIuTdsIYupqOY\nZTMgdlbbZ/DGXkOCwgptZNXejGw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEDjCCAvagAwIBAgIDD92sMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkRF\nMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxHzAdBgNVBAMMFkQtVFJVU1QgUm9vdCBD\nQSAzIDIwMTMwHhcNMTMwOTIwMDgyNTUxWhcNMjgwOTIwMDgyNTUxWjBFMQswCQYD\nVQQGEwJERTEVMBMGA1UECgwMRC1UcnVzdCBHbWJIMR8wHQYDVQQDDBZELVRSVVNU\nIFJvb3QgQ0EgMyAyMDEzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\nxHtCkoIf7O1UmI4SwMoJ35NuOpNcG+QQd55OaYhs9uFp8vabomGxvQcgdJhl8Ywm\nCM2oNcqANtFjbehEeoLDbF7eu+g20sRoNoyfMr2EIuDcwu4QRjltr5M5rofmw7wJ\nySxrZ1vZm3Z1TAvgu8XXvD558l++0ZBX+a72Zl8xv9Ntj6e6SvMjZbu376Ml1wrq\nWLbviPr6ebJSWNXwrIyhUXQplapRO5AyA58ccnSQ3j3tYdLl4/1kR+W5t0qp9x+u\nloYErC/jpIF3t1oW/9gPP/a3eMykr/pbPBJbqFKJcu+I89VEgYaVI5973bzZNO98\nlDyqwEHC451QGsDkGSL8swIDAQABo4IBBTCCAQEwDwYDVR0TAQH/BAUwAwEB/zAd\nBgNVHQ4EFgQUP5DIfccVb/Mkj6nDL0uiDyGyL+cwDgYDVR0PAQH/BAQDAgEGMIG+\nBgNVHR8EgbYwgbMwdKByoHCGbmxkYXA6Ly9kaXJlY3RvcnkuZC10cnVzdC5uZXQv\nQ049RC1UUlVTVCUyMFJvb3QlMjBDQSUyMDMlMjAyMDEzLE89RC1UcnVzdCUyMEdt\nYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MDugOaA3hjVodHRwOi8v\nY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2FfM18yMDEzLmNybDAN\nBgkqhkiG9w0BAQsFAAOCAQEADlkOWOR0SCNEzzQhtZwUGq2aS7eziG1cqRdw8Cqf\njXv5e4X6xznoEAiwNStfzwLS05zICx7uBVSuN5MECX1sj8J0vPgclL4xAUAt8yQg\nt4RVLFzI9XRKEBmLo8ftNdYJSNMOwLo5qLBGArDbxohZwr78e7Erz35ih1WWzAFv\nm2chlTWL+BD8cRu3SzdppjvW7IvuwbDzJcmPkn2h6sPKRL8mpXSSnON065102ctN\nh9j8tGlsi6BDB2B4l+nZk3zCRrybN1Kj7Yo8E6l7U0tJmhEFLAtuVqwfLoJs4Gln\ntQ5tLdnkwBXxP/oYcuEVbSdbLTAoK59ImmQrme/ydUlfXA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw\nWjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw\nMiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x\nMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD\nVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX\nBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\nggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO\nty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M\nCiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu\nI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm\nTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh\nC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf\nePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz\nIoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT\nCo/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k\nJWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5\nhwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB\nGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\nFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of\n1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov\nL3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo\ndHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr\naHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq\nhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L\n6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG\nHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6\n0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB\nlA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi\no2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1\ngPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v\nfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63\nNwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh\njWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw\n3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET\nMBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb\nBgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz\nMTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx\nFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g\nUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2\nfxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl\nLieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV\nWZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF\nTKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb\n5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc\nCbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri\nwsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ\nwx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG\nm/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4\nF2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng\nWVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB\nBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0\n2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF\nAAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/\n0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw\nF6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS\ng081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj\nqh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN\nh4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/\nql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V\nbtaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj\nY/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ\n8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW\ngQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID2DCCAsCgAwIBAgIQYFbFSyNAW2TU7SXa2dYeHjANBgkqhkiG9w0BAQsFADCB\nhTELMAkGA1UEBhMCREUxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fzc2VuIFZl\ncmxhZyBHbWJIMScwJQYDVQQLEx5TLVRSVVNUIENlcnRpZmljYXRpb24gU2Vydmlj\nZXMxIjAgBgNVBAMTGVMtVFJVU1QgVW5pdmVyc2FsIFJvb3QgQ0EwHhcNMTMxMDIy\nMDAwMDAwWhcNMzgxMDIxMjM1OTU5WjCBhTELMAkGA1UEBhMCREUxKTAnBgNVBAoT\nIERldXRzY2hlciBTcGFya2Fzc2VuIFZlcmxhZyBHbWJIMScwJQYDVQQLEx5TLVRS\nVVNUIENlcnRpZmljYXRpb24gU2VydmljZXMxIjAgBgNVBAMTGVMtVFJVU1QgVW5p\ndmVyc2FsIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo\n4wvfETeFgpq1bGZ8YT/ARxodRuOwVWTluII5KAd+F//0m4rwkYHqOD8heGxI7Gsv\notOKcrKn19nqf7TASWswJYmM67fVQGGY4tw8IJLNZUpynxqOjPolFb/zIYMoDYuv\nWRGCQ1ybTSVRf1gYY2A7s7WKi1hjN0hIkETCQN1d90NpKZhcEmVeq5CSS2bf1XUS\nU1QYpt6K1rtXAzlZmRgFDPn9FcaQZEYXgtfCSkE9/QC+V3IYlHcbU1qJAfYzcg6T\nOtzoHv0FBda8c+CI3KtP7LUYhk95hA5IKmYq3TLIeGXIC51YAQVx7YH1aBduyw20\nS9ih7K446xxYL6FlAzQvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P\nAQH/BAQDAgEGMB0GA1UdDgQWBBSafdfr639UmEUptCCrbQuWIxmkwjANBgkqhkiG\n9w0BAQsFAAOCAQEATpYS2353XpInniEXGIJ22D+8pQkEZoiJrdtVszNqxmXEj03z\nMjbceQSWqXcy0Zf1GGuMuu3OEdBEx5LxtESO7YhSSJ7V/Vn4ox5R+wFS5V/let2q\nJE8ii912RvaloA812MoPmLkwXSBvwoEevb3A/hXTOCoJk5gnG5N70Cs0XmilFU/R\nUsOgyqCDRR319bdZc11ZAY+qwkcvFHHVKeMQtUeTJcwjKdq3ctiR1OwbSIoi5MEq\n9zpok59FGW5Dt8z+uJGaYRo2aWNkkijzb2GShROfyQcsi1fc65551cLeCNVUsldO\nKjKNoeI60RAgIjl9NEVvcTvDHfz/sk+o4vYwHg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO\nTDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh\ndCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX\nDTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl\nciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv\nb3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP\ncPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW\nIkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX\nxz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy\nKJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR\n9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az\n5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8\n6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7\nNgzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP\nbMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt\nBznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt\nXUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF\nMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd\nINyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD\nU5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp\nLiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8\nIpf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp\ngZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh\n/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw\n0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A\nfsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq\n4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR\n1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/\nQFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM\n94B7IWcnMFk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx\nGDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp\nbXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w\nKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0\nBgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy\ndW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG\nEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll\nIEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU\nQUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT\nTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg\nLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7\na9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr\nLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr\nN3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X\nYacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/\niSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f\nAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH\nV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL\nBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh\nAHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf\nIPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4\nlzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c\n8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf\nlo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFbDCCA1SgAwIBAgIQDLMPcPKGpDPguQmJ3gHttzANBgkqhkiG9w0BAQsFADBQ\nMQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhvdHNwb3QgMi4wMScwJQYDVQQD\nEx5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0gMDMwHhcNMTMxMjA4MTIwMDAw\nWhcNNDMxMjA4MTIwMDAwWjBQMQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhv\ndHNwb3QgMi4wMScwJQYDVQQDEx5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0g\nMDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsdEtReIUbMlO+hR6b\nyQk4nGVITv3meYTaDeVwZnQVal8EjHuu4Kd89g8yRYVTv3J1kq9ukE7CDrDehrXK\nym+8VlR7ro0lB/lwRyNk3W7yNccg3AknQ0x5fKVwcFznwD/FYg37owGmhGFtpMTB\ncxzreQaLXvLta8YNlJU10ZkfputBpzi9bLPWsLOkIrQw7KH1Wc+Oiy4hUMUbTlSi\ncjqacKPR188mVIoxxUoICHyVV1KvMmYZrVdc/b5dbmd0haMHxC0VSqbydXxxS7vv\n/lCrC2d5qbKE66PiuBPkhzyU7SI9C8GU/S7akYm1MMSTn5W7lSp2AWRDnf9LQg51\ndLvDxJ7t2fruXtSkkqG/cwY1yQI8O+WZYPDThKPcDmNbaxVE9lOizAHXFVsfYrXA\nPbbMOkzKehYwaIikmNgcpxtQNw+wikJiZb9N8VwwtwHK71XEFi+n5DGlPa9VDYgB\nYkBcxvVo2rbE3i3teQgHm+pWZNP08aFNWwMk9yQkm/SOGdLq1jLbQA9yd7fyR1Ct\nW1GLzKi1Ojr/6XiB9/noL3oxP/+gb8OSgcqVfkZp4QLvrGdlKiOI2fE7Bslmzn6l\nB3UTpApjab7BQ99rCXzDwt3Xd7IrCtAJNkxi302J7k6hnGlW8S4oPQBElkOtoH9y\nXEhp9rNS0lZiuwtFmWW2q50fkQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G\nA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUZw5JLGEXnuvt4FTnhNmbrWRgc2UwDQYJ\nKoZIhvcNAQELBQADggIBAFPoGFDyzFg9B9+jJUPGW32omftBhChVcgjllI07RCie\nKTMBi47+auuLgiMox3xRyP7/dX7YaUeMXEQ1BMv6nlrsXWv1lH4yu+RNuehPlqRs\nfY351mAfPtQ654SBUi0Wg++9iyTOfgF5a9IWEDt4lnSZMvA4vlw8pUCz6zpKXHnA\nRXKrpY3bU+2dnrFDKR0XQhmAQdo7UvdsT1elVoFIxHhLpwfzx+kpEhtrXw3nGgt+\nM4jNp684XoWpxVGaQ4Vvv00Sm2DQ8jq2sf9F+kRWszZpQOTiMGKZr0lX2CI5cww1\ndfmd1BkAjI9cIWLkD8YSeaggZzvYe1o9d7e7lKfdJmjDlSQ0uBiG77keUK4tF2fi\nxFTxibtPux56p3GYQ2GdRsBaKjH3A3HMJSKXwIGR+wb1sgz/bBdlyJSylG8hYD//\n0Hyo+UrMUszAdszoPhMY+4Ol3QE3QRWzXi+W/NtKeYD2K8xUzjZM10wMdxCfoFOa\n8bzzWnxZQlnu880ULUSHIxDPeE+DDZYYOaN1hV2Rh/hrFKvvV+gJj2eXHF5G7y9u\nYg7nHYCCf7Hy8UTIXDtAAeDCQNon1ReN8G+XOqhLQ9TalmnJ5U5ARtC0MdQDht7T\nDZpWeEVv+pQHARX9GDV/T85MV2RPJWKqfZ6kK0gvQDkunADdg8IhZAjwMMx3k6B/\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu\nVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN\nMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0\nMSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7\nekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy\nRBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS\nbdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF\n/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R\n3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw\nEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy\n9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V\nGxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ\n2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV\nWaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD\nW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/\nBAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN\nAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj\nt2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV\nDRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9\nTaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G\nlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW\nmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df\nWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5\n+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ\ntshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA\nGaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv\n8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK\nMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu\nVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw\nMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw\nJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT\n3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU\n+ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp\nS0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1\nbVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi\nT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL\nvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK\nVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK\ndHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT\nc+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv\nl7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N\niGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB\n/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD\nggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH\n6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt\nLRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93\nnAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3\n+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK\nW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT\nAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq\nl1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG\n4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ\nmUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A\n7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF8TCCA9mgAwIBAgIIVE2lvEA1VlowDQYJKoZIhvcNAQELBQAwgYUxCzAJBgNV\nBAYTAlBUMUIwQAYDVQQKDDlNVUxUSUNFUlQgLSBTZXJ2acOnb3MgZGUgQ2VydGlm\naWNhw6fDo28gRWxlY3Ryw7NuaWNhIFMuQS4xMjAwBgNVBAMMKU1VTFRJQ0VSVCBS\nb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IDAxMB4XDTE0MDQwNDA4NTk0N1oX\nDTM5MDQwNDA4NTk0N1owgYUxCzAJBgNVBAYTAlBUMUIwQAYDVQQKDDlNVUxUSUNF\nUlQgLSBTZXJ2acOnb3MgZGUgQ2VydGlmaWNhw6fDo28gRWxlY3Ryw7NuaWNhIFMu\nQS4xMjAwBgNVBAMMKU1VTFRJQ0VSVCBSb290IENlcnRpZmljYXRpb24gQXV0aG9y\naXR5IDAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAztw/9BluuxVp\nhvTkzec6cDvHmos7gwCBW/sgFlq+v1gAXynmV29+iiwVB1waY4xCXxbd2omERVcX\nlqCcoXUiQRo6/cUXkRP2vmIKvG4lLVvAjBBm9+LW+9xIMaMaqOVNSMmiHHP+j2ZA\nY3dZBzw9FJ/U94WR0MNC9Rths3eAgCptEgKWi1HZwW8nCxoHNAD/0llMKejXGWPY\nkbQ//I4OJfKhEgdlyjXeq/4WowiMr39+EvRZFgUf6K10eTL3eAK2tMyr2x44YQQZ\nekFA2loRZHUC/WTR1pRCDyLnZc2vkA4MWzEBmVHvRYx9pTjannxL5Kbos6SC1gM0\nLk+3Uat3OAn1Bv7cZhsPP/p974xVvuANhpWh3L3EwwjRRR7yvb5w8eYmxrsIsSil\nwqXtiNahwPsj8Sc5zOGEBxm8fvbMOP9uELtG6SOJJIH/AOJRANxSUH0TUH0WPUCN\n07/5imXYYhIpd8K6wkk0T4p5aclLFfM03s+vhuLlyKlWYUwGVFrFbBnq88hEzSQa\ndtFxAFlr2XWbzv0Q/rGDoqW3koZ2m0r3HdyMhaZYrYqmaGkXyW0bps8nSyks3XFC\nGokQ5dWbEl9Ji4S82Ahc+884Qq++0W57kapmQMUFfivQZrbH31L+9EVtI5IhnhIB\nkHOD4qUJDdfA+IWVHmPRPzXalNE32fUCAwEAAaNjMGEwHQYDVR0OBBYEFNU5HJxb\nbwSqopVM7yDdKXSkxUVxMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1Tkc\nnFtvBKqilUzvIN0pdKTFRXEwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUA\nA4ICAQA/51/zIhbeg54g5ILn5Z53yfsrsHQN3xt0Ig9zEKGwF+xMDNQocGpmckRp\nEJN2Nc8v+I88qxl8cZKVcRs3FcIbKHrvbng43/uPmwEg3K/21o0JZtrERqn8lapE\nIxLfR8CwFey1sZ5sD5GqpjrlwQ1gbFBAcFxcyM6zzOvtqogZVqWkyAx65XZAZzO0\nPZbcd8sjePlTW8+N3rGnjlp6ojJjo4jXJWFaXUk6cubPqpSGbG73guCOZ5MoxagN\nTe84rXlKZo2EAQgEefNSxkHnmmIGs/USHuzZAEPT65Z3dOF5+RSUhG26VIIFjN8B\n8jCIgax6L4tDLHY0zjXnh45OCwqlGlexU1q/a9i+AH7G+e5mMQix35QzhJx3T3tk\nL++OD1koIsvwXD4r/TXWlf8D7GVSfr7yGfh71VIsUneakWZBcI3VSecLSH+Krt5F\nPd3+5tLkksN7zjCgSW43rajTLLY9niHbBlfi8K4G+9nFETehe9sdEXxodiA+9byl\n2Wa1Ia1FJsZdHgKjQcTUfYEZyxeXBg/m7HQARsR13T3wQzSvprz89oL7z8X6sw8l\npT9mENaegqXbOhN53o2p16aNhtIv2WkN4nV4fklfIquGcChRs3q2oHn61OWDp7B3\nytsBgu/ivk0v08BN0ONpbnwmm+um+0XvsQSKL6ohBvbm1LxBIw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFkjCCA3qgAwIBAgIIAeDltYNno+AwDQYJKoZIhvcNAQEMBQAwZzEbMBkGA1UE\nAwwSQXBwbGUgUm9vdCBDQSAtIEcyMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0\naW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMw\nHhcNMTQwNDMwMTgxMDA5WhcNMzkwNDMwMTgxMDA5WjBnMRswGQYDVQQDDBJBcHBs\nZSBSb290IENBIC0gRzIxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0\naG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzCCAiIwDQYJ\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgREkhI2imKScUcx+xuM23+TfvgHN6s\nXuI2pyT5f1BrTM65MFQn5bPW7SXmMLYFN14UIhHF6Kob0vuy0gmVOKTvKkmMXT5x\nZgM4+xb1hYjkWpIMBDLyyED7Ul+f9sDx47pFoFDVEovy3d6RhiPw9bZyLgHaC/Yu\nOQhfGaFjQQscp5TBhsRTL3b2CtcM0YM/GlMZ81fVJ3/8E7j4ko380yhDPLVoACVd\nJ2LT3VXdRCCQgzWTxb+4Gftr49wIQuavbfqeQMpOhYV4SbHXw8EwOTKrfl+q04tv\nny0aIWhwZ7Oj8ZhBbZF8+NfbqOdfIRqMM78xdLe40fTgIvS/cjTf94FNcX1RoeKz\n8NMoFnNvzcytN31O661A4T+B/fc9Cj6i8b0xlilZ3MIZgIxbdMYs0xBTJh0UT8TU\ngWY8h2czJxQI6bR3hDRSj4n4aJgXv8O7qhOTH11UL6jHfPsNFL4VPSQ08prcdUFm\nIrQB1guvkJ4M6mL4m1k8COKWNORj3rw31OsMiANDC1CvoDTdUE0V+1ok2Az6DGOe\nHwOx4e7hqkP0ZmUoNwIx7wHHHtHMn23KVDpA287PT0aLSmWaasZobNfMmRtHsHLD\nd4/E92GcdB/O/WuhwpyUgquUoue9G7q5cDmVF8Up8zlYNPXEpMZ7YLlmQ1A/bmH8\nDvmGqmAMQ0uVAgMBAAGjQjBAMB0GA1UdDgQWBBTEmRNsGAPCe8CjoA1/coB6HHcm\njTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwF\nAAOCAgEAUabz4vS4PZO/Lc4Pu1vhVRROTtHlznldgX/+tvCHM/jvlOV+3Gp5pxy+\n8JS3ptEwnMgNCnWefZKVfhidfsJxaXwU6s+DDuQUQp50DhDNqxq6EWGBeNjxtUVA\neKuowM77fWM3aPbn+6/Gw0vsHzYmE1SGlHKy6gLti23kDKaQwFd1z4xCfVzmMX3z\nybKSaUYOiPjjLUKyOKimGY3xn83uamW8GrAlvacp/fQ+onVJv57byfenHmOZ4VxG\n/5IFjPoeIPmGlFYl5bRXOJ3riGQUIUkhOb9iZqmxospvPyFgxYnURTbImHy99v6Z\nSYA7LNKmp4gDBDEZt7Y6YUX6yfIjyGNzv1aJMbDZfGKnexWoiIqrOEDCzBL/FePw\nN983csvMmOa/orz6JopxVtfnJBtIRD6e/J/JzBrsQzwBvDR4yGn1xuZW7AYJNpDr\nFEobXsmII9oDMJELuDY++ee1KG++P+w8j2Ud5cAeh6Squpj9kuNsJnfdBrRkBof0\nTta6SqoWqPQFZ2aWuuJVecMsXUmPgEkrihLHdoBR37q9ZV0+N0djMenl9MU/S60E\ninpxLK8JQzcPqOMyT/RFtm2XNuyE9QoB6he7hY1Ck3DDUOUUi78/w0EP3SIEIwiK\num1xRKtzCTrJ+VKACd+66eYWyi4uTLLT3OUEVLLUNIAytbwPF+E=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS\nQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u\nIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN\nMTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS\nb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9y\naXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtf\nTjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517\nIDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySr\nMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gA\nMGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4\nat+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM\n6BgD56KyKA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFUTCCAzmgAwIBAgIIAPtxJlitmeUwDQYJKoZIhvcNAQELBQAwNjEWMBQGA1UE\nAwwNQ0FFRElDT00gUm9vdDEPMA0GA1UECgwGRURJQ09NMQswCQYDVQQGEwJFUzAe\nFw0xNDA1MjExMTA2MzVaFw0zNDA1MjExMDIwMDBaMDYxFjAUBgNVBAMMDUNBRURJ\nQ09NIFJvb3QxDzANBgNVBAoMBkVESUNPTTELMAkGA1UEBhMCRVMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQDbgMroSXTH0zgu8cUjYvw2jC8efjkL6Qb0\nVZulmCmU7YZHMoPzxZJ6BdcpAj4Wwyh/NWQpenm7oeIeYRSN5wDQ3KJUZYrfablx\nR384OBZGp2kxETVM4Sp//21PlT3jXUhNGVMIWmsh1RIwaZeQry3B9X9BX0k2j024\nHhqVX9oPb1wVNcQRvF+Fm72tO1Veu9/Ou69cmWDdH2kaSUgh+QkKz3Kn8PLe5XgZ\nvhLdzYd5Qc4vRdcLkRARBB4SnfI4A18Waa6gCtrA+eugDRgPeV6RneQfFJw0ExkC\nRLpRw+55smAUo6+8SC0oOGgBQ2TKDoaDYtCKGaYn8St7SykhW5rMaEIQyEtPDyOy\niHzEXG4XcMV3r5XAJaQiCtN8+dhyyNAtvafo0i2LTKFuCvy0QDO7mmv8pOrJ/uA0\niEPMxrw/ddKlqa/6l7k+t85UoE3AXS7BKNhjVHK4rFr1OvsgYQY69KArOKvMgwxJ\n1G4+bQ8+cy825vNPs8AA0UVJW4z2o5gdhH+ZCsPqCjzD0yR4SGf1GzsOHQ5DsQR1\nwaA5dov22QKlHeGeWwe7NldKIU35iWm0bA/Xr6AVJJnn+NdTlOwSv6Sl1+3ujjV3\nd9ymfyBUktZj1nKeTSq2j3PzGaHEsB/mNKMLAD6XSSdhqqoEQTM4tVBRzDYV2x//\nvcpIg0inswIDAQABo2MwYTAdBgNVHQ4EFgQUFM0qWXhjq2EZ6Lg9oeBawHXn+csw\nDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQUzSpZeGOrYRnouD2h4FrAdef5\nyzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAD0JGQC2kQJs7A73\n4eJisL8zDf1VEvQImvcrLa73nEfYHwYBE7WO57KCCz2EWUPUB9grXBB6JCzKjejV\nozmcMczr4Drh1b/Px4d7YP9HOdejRNYIJlvPWlTsiNOOD3k8yKNPpsKOJ/DeEq5e\nGa3nIlaKWDLg+QbQqSq0NZsMhiZRAJRHUPylxCVh+VjwRXAuSXZ/EdZvtfkpBeEN\nw05YH68d7DfQSvkGBoHT26CWuA6RMHnmUN+IuAupXNQH9MmozH2Pk2MJZAAFKmhm\nQ7uiu/6VrvnEpQqIYkh4JXwqPxFkptMiIEedMtby48ikYXTngsJEuqDRXV+88UQO\ng08cUIXE6eds/Oa4VeGiGoC3kESnhCKXRyLeqzg3z7XyHD5CcLt1tmUoa8t/gjWq\n9vMgeChzB5YwcKUqcVyheaQWuUY9XrQASYWJ0w7fga5YjVjW4cVEeC4cILuiR5e/\ndhQ7qSiPnwt10qE87SvHjpCheqKZMGL8hR01czvztVkiG80IsQyddWrbhTsOh58y\nT5IAAQFMSWiCgEFs+f1xvYv0eApgo56xUh3AiuOexb8rGWqYp7HeFVCfqpQlj6mA\ngqdyuklkCSdhK268IygzXZ5u8Lm9IDKM3aALmbu0hAQkdSmW96elF7hRBet0rVF5\nlvy7+98JLQiSRM7A0rMYxxQivyHx\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFazCCA1OgAwIBAgISESBVg+QtPlRWhS2DN7cs3EYRMA0GCSqGSIb3DQEBDQUA\nMD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy\ndHBsdXMgUm9vdCBDQSBHMTAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBa\nMD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy\ndHBsdXMgUm9vdCBDQSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\nANpQh7bauKk+nWT6VjOaVj0W5QOVsjQcmm1iBdTYj+eJZJ+622SLZOZ5KmHNr49a\niZFluVj8tANfkT8tEBXgfs+8/H9DZ6itXjYj2JizTfNDnjl8KvzsiNWI7nC9hRYt\n6kuJPKNxQv4c/dMcLRC4hlTqQ7jbxofaqK6AJc96Jh2qkbBIb6613p7Y1/oA/caP\n0FG7Yn2ksYyy/yARujVjBYZHYEMzkPZHogNPlk2dT8Hq6pyi/jQu3rfKG3akt62f\n6ajUeD94/vI4CTYd0hYCyOwqaK/1jpTvLRN6HkJKHRUxrgwEV/xhc/MxVoYxgKDE\nEW4wduOU8F8ExKyHcomYxZ3MVwia9Az8fXoFOvpHgDm2z4QTd28n6v+WZxcIbekN\n1iNQMLAVdBM+5S//Ds3EC0pd8NgAM0lm66EYfFkuPSi5YXHLtaW6uOrc4nBvCGrc\nh2c0798wct3zyT8j/zXhviEpIDCB5BmlIOklynMxdCm+4kLV87ImZsdo/Rmz5yCT\nmehd4F6H50boJZwKKSTUzViGUkAksnsPmBIgJPaQbEfIDbsYIC7Z/fyL8inqh3SV\n4EJQeIQEQWGw9CEjjy3LKCHyamz0GqbFFLQ3ZU+V/YDI+HLlJWvEYLF7bY5KinPO\nWftwenMGE9nTdDckQQoRb5fc5+R+ob0V8rqHDz1oihYHAgMBAAGjYzBhMA4GA1Ud\nDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSowcCbkahDFXxd\nBie0KlHYlwuBsTAfBgNVHSMEGDAWgBSowcCbkahDFXxdBie0KlHYlwuBsTANBgkq\nhkiG9w0BAQ0FAAOCAgEAnFZvAX7RvUz1isbwJh/k4DgYzDLDKTudQSk0YcbX8ACh\n66Ryj5QXvBMsdbRX7gp8CXrc1cqh0DQT+Hern+X+2B50ioUHj3/MeXrKls3N/U/7\n/SMNkPX0XtPGYX2eEeAC7gkE2Qfdpoq3DIMku4NQkv5gdRE+2J2winq14J2by5BS\nS7CTKtQ+FjPlnsZlFT5kOwQ/2wyPX1wdaR+v8+khjPPvl/aatxm2hHSco1S1cE5j\n2FddUyGbQJJD+tZ3VTNPZNX70Cxqjm0lpu+F6ALEUz65noe8zDUa3qHpimOHZR4R\nKttjd5cUvpoUmRGywO6wT/gUITJDT5+rosuoD6o7BlXGEilXCNQ314cnrUlZp5Gr\nRHpejXDbl85IULFzk/bwg2D5zfHhMf1bfHEhYxQUqq/F3pN+aLHsIqKqkHWetUNy\n6mSjhEv9DKgma3GX7lZjZuhCVPnHHd/Qj1vfyDBviP4NxDMcU6ij/UgQ8uQKTuEV\nV/xuZDDCVRHc6qnNSlSsKWNEz0pAoNZoWRsz+e86i9sgktxChL8Bq4fA1SCC28a5\ng4VCXA9DO2pJNdWY9BW/+mGBDAkgGNLQFwzLSABQ6XaCjGTXOqAHVcweMcDvOrRl\n++O/QmueD6i9a5jc2NvLi6Td11n0bt3+qsOR0C5CB8AMTVPNJLFMWx5R9N/pkvo=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFbzCCA1egAwIBAgISESChaRu/vbm9UpaPI+hIvyYRMA0GCSqGSIb3DQEBDQUA\nMEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w\nZW5UcnVzdCBSb290IENBIEcyMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAw\nMFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU\nT3BlblRydXN0IFJvb3QgQ0EgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQDMtlelM5QQgTJT32F+D3Y5z1zCU3UdSXqWON2ic2rxb95eolq5cSG+Ntmh\n/LzubKh8NBpxGuga2F8ORAbtp+Dz0mEL4DKiltE48MLaARf85KxP6O6JHnSrT78e\nCbY2albz4e6WiWYkBuTNQjpK3eCasMSCRbP+yatcfD7J6xcvDH1urqWPyKwlCm/6\n1UWY0jUJ9gNDlP7ZvyCVeYCYitmJNbtRG6Q3ffyZO6v/v6wNj0OxmXsWEH4db0fE\nFY8ElggGQgT4hNYdvJGmQr5J1WqIP7wtUdGejeBSzFfdNTVY27SPJIjki9/ca1TS\ngSuyzpJLHB9G+h3Ykst2Z7UJmQnlrBcUVXDGPKBWCgOz3GIZ38i1MH/1PCZ1Eb3X\nG7OHngevZXHloM8apwkQHZOJZlvoPGIytbU6bumFAYueQ4xncyhZW+vj3CzMpSZy\nYhK05pyDRPZRpOLAeiRXyg6lPzq1O4vldu5w5pLeFlwoW5cZJ5L+epJUzpM5ChaH\nvGOz9bGTXOBut9Dq+WIyiET7vycotjCVXRIouZW+j1MY5aIYFuJWpLIsEPUdN6b4\nt/bQWVyJ98LVtZR00dX+G7bw5tYee9I8y6jj9RjzIR9u701oBnstXW5DiabA+aC/\ngh7PU3+06yzbXfZqfUAkBXKJOAGTy3HCOV0GEfZvePg3DTmEJwIDAQABo2MwYTAO\nBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUajn6QiL3\n5okATV59M4PLuG53hq8wHwYDVR0jBBgwFoAUajn6QiL35okATV59M4PLuG53hq8w\nDQYJKoZIhvcNAQENBQADggIBAJjLq0A85TMCl38th6aP1F5Kr7ge57tx+4BkJamz\nGj5oXScmp7oq4fBXgwpkTx4idBvpkF/wrM//T2h6OKQQbA2xx6R3gBi2oihEdqc0\nnXGEL8pZ0keImUEiyTCYYW49qKgFbdEfwFFEVn8nNQLdXpgKQuswv42hm1GqO+qT\nRmTFAHneIWv2V6CG1wZy7HBGS4tz3aAhdT7cHcCP009zHIXZ/n9iyJVvttN7jLpT\nwm+bREx50B1ws9efAvSyB7DH5fitIw6mVskpEndI2S9G/Tvw/HRwkqWOOAgfZDC2\nt0v7NqwQjqBSM2OdAzVWxWm9xiNaJ5T2pBL4LTM8oValX9YZ6e18CL13zSdkzJTa\nTkZQh+D5wVOAHrut+0dSixv9ovneDiK3PTNZbNTe9ZUGMg1RGUFcPk8G97krgCf2\no6p6fAbhQ8MTOWIaNr3gKC6UAuQpLmBVrkA9sHSSXvAgZJY/X0VdiLWK2gKgW0VU\n3jg9CcCoSmVGFvyqv1ROTVu+OEO3KMqLM6oaJbolXCkvW0pujOotnCr2BXbgd5eA\niN1nE28daCSLT7d0geX0YJ96Vdc+N9oWaz53rK4YcJUIeSkDiv7BO7M/Gg+kO14f\nWKGVyasvc0rQLW6aWQ9VGHgtPFGml4vmu7JwqkwR3v98KzfUetF3NI/n+UL3PIEM\nS1IK\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICHDCCAaKgAwIBAgISESDZkc6uo+jF5//pAq/Pc7xVMAoGCCqGSM49BAMDMD4x\nCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs\ndXMgUm9vdCBDQSBHMjAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBaMD4x\nCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs\ndXMgUm9vdCBDQSBHMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABM0PW1aC3/BFGtat\n93nwHcmsltaeTpwftEIRyoa/bfuFo8XlGVzX7qY/aWfYeOKmycTbLXku54uNAm8x\nIk0G42ByRZ0OQneezs/lf4WbGOT8zC5y0xaTTsqZY1yhBSpsBqNjMGEwDgYDVR0P\nAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNqDYwJ5jtpMxjwj\nFNiPwyCrKGBZMB8GA1UdIwQYMBaAFNqDYwJ5jtpMxjwjFNiPwyCrKGBZMAoGCCqG\nSM49BAMDA2gAMGUCMHD+sAvZ94OX7PNVHdTcswYO/jOYnYs5kGuUIe22113WTNch\np+e/IQ8rzfcq3IUHnQIxAIYUFuXcsGXCwI4Un78kFmjlvPl5adytRSv3tjFzzAal\nU5ORGpOucGpnutee5WEaXw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICITCCAaagAwIBAgISESDm+Ez8JLC+BUCs2oMbNGA/MAoGCCqGSM49BAMDMEAx\nCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9wZW5U\ncnVzdCBSb290IENBIEczMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAwMFow\nQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwUT3Bl\nblRydXN0IFJvb3QgQ0EgRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARK7liuTcpm\n3gY6oxH84Bjwbhy6LTAMidnW7ptzg6kjFYwvWYpa3RTqnVkrQ7cG7DK2uu5Bta1d\noYXM6h0UZqNnfkbilPPntlahFVmhTzeXuSIevRHr9LIfXsMUmuXZl5mjYzBhMA4G\nA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRHd8MUi2I5\nDMlv4VBN0BBY3JWIbTAfBgNVHSMEGDAWgBRHd8MUi2I5DMlv4VBN0BBY3JWIbTAK\nBggqhkjOPQQDAwNpADBmAjEAj6jcnboMBBf6Fek9LykBl7+BFjNAk2z8+e2AcG+q\nj9uEwov1NcoG3GRvaBbhj5G5AjEA2Euly8LQCGzpGPta3U1fJAuwACEl74+nBCZx\n4nxp5V2a+EEfOzmTk51V6s2N8fvB\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDWjCCAkKgAwIBAgIBMTANBgkqhkiG9w0BAQsFADA+MQswCQYDVQQGEwJKUDEO\nMAwGA1UEChMFTEdQS0kxHzAdBgNVBAMTFkFwcGxpY2F0aW9uIENBIEczIFJvb3Qw\nHhcNMTQwNjAzMTUwMDAwWhcNMzQwNjAzMTQ1OTU5WjA+MQswCQYDVQQGEwJKUDEO\nMAwGA1UEChMFTEdQS0kxHzAdBgNVBAMTFkFwcGxpY2F0aW9uIENBIEczIFJvb3Qw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNRT730ZYiXJEvPgoAA8y2\n92xU/Cg31AQY7K2Yya/Tpbnn2b9O5qOZPJluoSAeRhvidVW80uz2iBrsNEVLg53T\nsubdB4nBCNn4O4uSZHJdmjvMrTeJx9xgeQjgcKz3K+2fA0kfjj6DqG7iklxU0Xnf\n7Bg6fbhtj9ajJU2tH0CmX9SqTrFwGFmZ8gtUaT55KESI93GXzX8F3MrcdkqQTGtg\n6PomMdi1+Of8bYskarbvQtcjVMUaY4o7x/yqbTyPy2zaILDyvGUcAUwilQ0cIx+s\n1fnOdVvqML1MASQfddRhScMbmWWOCFw5OM0pwzhFzWR5t5tNR+pYMvqm9pLwwbdf\nAgMBAAGjYzBhMB0GA1UdDgQWBBSpNSpIviw37YbbfFWHACa+GC1cLjAOBgNVHQ8B\nAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSpNSpIviw37Ybb\nfFWHACa+GC1cLjANBgkqhkiG9w0BAQsFAAOCAQEAtoK9xUbQcYulkT1+LVr5nIR9\nByeVHedNyHzs5pPoVhp6MEg7DPpO9Qmyr4itlOz9sq0v5gV0IRuEizgqw+3vRmi1\n3VL6cMJ1T/+jQS48F5RMCSK0jsF/xKas7YNoz2Ve7Hq9xWbu0KN/8lexCMJ5cOty\nf0FZCXl18byxIf6Ds0Q9iaO+sXrYncMf5sRU4Y3l2FDc5FY3e74oAPMsd9ojf2CY\nPQUW8nhprZnDOnRsPpqylO2PqvZTa+fIt+g8jPvHfE8ZXaRmFel/h6DQ1a0gpEYJ\nRazlyGWHuwbf/NdoVkNzogCZMpLCDqAcDpG9lVi8k5+EwqVm52XNKeJi8gWSYA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDNTCCAh2gAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MQswCQYDVQQGEwJVUzEW\nMBQGA1UEChMNQ2lzY28gU3lzdGVtczEVMBMGA1UEAxMMQ2lzY28gUlhDLVIyMB4X\nDTE0MDcwOTIxNDY1NloXDTM0MDcwOTIxNDY1NlowPDELMAkGA1UEBhMCVVMxFjAU\nBgNVBAoTDUNpc2NvIFN5c3RlbXMxFTATBgNVBAMTDENpc2NvIFJYQy1SMjCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANtCMXsK05wqTce60mQGZLAIL8wT\n6i02PnfuPth2FAGDwUtPL4jLHBJW8uVJJEBLom3pyhPpc/jaqd1g6dddKxwK4Y2L\nvHW/c1j86IMqjXLeE9//u58xND+hiOhBx1QQpO+BFe4jpQW6NRKYqWlz7G5aPO+M\nfk3zDWEnEWRpoisf2jNOnNYVqRQdEY4+xZ9NHTsATS3NbAGFADRi7Vx0C6dSieI+\nCtNsTRG6dMU8x8/IX40VzREyPtIqMSWtGwuz0xk6KayB1ADYuBW8mH5jfufIOLn1\n/XSgVz7flasyfJ8iKbW1eoIgpGNyXJGBI39iPWTYZswh+Ok7swZskj0mPzECAwEA\nAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE\nFJByBGD93fqE7I5aBFj3z/vDcgkWMA0GCSqGSIb3DQEBCwUAA4IBAQCBDfRhZWOb\nblcaSjp0A8tREiYjHaDW9oR6Pk3xd5SMYE2axpy45nFjbfXCr9HTBz+mi8SrunUw\nP4lzgv+P+EyyT/Kmt6KRrm2z+CPr6JUaexYgsennNi/TRmiqdWRXY4gyrYSsCgJB\njw3A7srAUvZSma6JEiP2E4skx3KVHmliwyBaK04KSkKKwY4b+oQIZVq2cgySm2bB\n1q2+SMI5jMk9pRUh0anImbDyZPCARsIQuhUD5MOSYh+GiG7oTurvsf70H1RxuZrQ\n/RwhDKseClSVWzBiLtiDW3LOAo5UNjqyQAZgZcS1yhAsGcsPXB7eel783IZDbq7Q\nkK4RSUNGApEO\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFmTCCA4GgAwIBAgIIcYwvOXxAdEAwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE\nBhMCR1IxGzAZBgNVBAoMEkJZVEUgQ29tcHV0ZXIgUy5BLjEuMCwGA1UEAwwlQllU\nRSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IDAwMTAeFw0xNDA5MTAyMjAx\nNTRaFw0zOTA5MTAyMjAxNTRaMFoxCzAJBgNVBAYTAkdSMRswGQYDVQQKDBJCWVRF\nIENvbXB1dGVyIFMuQS4xLjAsBgNVBAMMJUJZVEUgUm9vdCBDZXJ0aWZpY2F0aW9u\nIEF1dGhvcml0eSAwMDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDt\nEi4Xc55v9POZ6J4IVwk0JBFAH4whfhuvOMPRx+YU5fobul5m9SVp9+3NboJwr7pC\n8LEZXCv8RYQYLHoXT2GFRhl8zsGNn1SedyVmD+D2+JLKKc4nVxUqbII4bSfmvk1z\nDnOv43E9vAlCD9UNoe19a673wfBszcKXoVj9NRWWF0yfv/XxOUtwt+dKbBw/wXBb\nz9aL6+9vMOhfyEZ3IWIWXsZURTn1dLpnJGilcVs+wfsJk+simfjS9XsCbI9Y4qvv\n3XQh5CRplEDWwQQYDthC8P3XigXAXxuK6y7ADQcGcwGFjh/BwIqhWKZRuViRQg9u\n4bwK6LsogxV15Q3+STApKULCwjb/pDx9Lvfa8qIvFrxhqJlYGKRJxmoHEusbfLTO\n5/shgCtwpsjOrVUeHx2E0P1UakxWY8jdfqD5OdvvfFr3jDWlbipW+v7jX5NUcg5o\n40krk001IpcUlWZPp3c6LiVM9gmLEhtxxXKnm7m86xygpclUg2HcV1WttebaeCt2\np/742/6MM6SKo0ZcrbIKEg6K5FCe8LjLmVNMZCFrijgq4IiGANQXrGay574tOynl\n+KeU24xY+NJLMJ/yxGJlUEdygM+kcEC2vUT+2b8oKy43x7NRDoIptbFvrX4sk8Cp\nf5H6xx818LuXyU9hKJCEQeh9IUDFyYY87ZqthZyiUwIDAQABo2MwYTAdBgNVHQ4E\nFgQUtE1mt9OzyJl8ATLQkTr31qgSMd4wDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME\nGDAWgBS0TWa307PImXwBMtCROvfWqBIx3jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI\nhvcNAQELBQADggIBABwa6wauVb07PzYsYZ7qx1P8cKoyb+RCquu9hewbilrylZYp\noQQGks4kV/9AI3hOyfgwTUJVRE43on1rjmj+Dv5/37CfY1Hz4cWllJ+KIyhI80GL\n0v547dnQCA9tfdWdlazV/hJmGuS+dVTz0U2cThPUnnA0bai6CjOIja0FN/5LeX99\nA0F5Ew2fPfc4nDVaRE8+PKLlgcV/X3ZPGztub5ptt+0PyzIfiLRFDJwR0vgEWhM3\nWZiBzkz05ZQoBMS1U8lUjXA/aAHbzBMK5CWjbJntELN6IKlJvAX0+Bto1rogHYJn\nZuCwn1zKNdJFrtWIGdt6BpuMoDeHUSO+Rdpcs39rz8aoHDOKex2R+p687H07RRVP\nG6c7NbR581uCUOCcp+0WddtjgGKh2hgCaoDegqpETUQ4KKpu+hhjOWD3QylJWrok\nwL+zCpcdZ0laIrJnBJxYqfgMNFxAlrSHtUVhGeWO7wbekRXAuIrKlMkKdX1xO1iB\nM8j3B0FVmClDtcuaQ+ly+s/wizG85++5auNBnSE+DRWohb0bToeOR7IQ/jcYaoTl\niRwUY+i5g6m1u+hjmnoZjMt09/gXCPGLGdi07B5uSXM/XCDdNSqWd+lGbxY7y6nv\nmwohEcjDpMkjRW0/YpWd0yjHnQ+z/jeNHUiyUOYluU4zYTbWFhzKMjcgdhws\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGAzCCA+ugAwIBAgIQL9Z6QyKTMpBF6VM0PuJ0ZjANBgkqhkiG9w0BAQsFADCB\nkzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl\nZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE9MDsGA1UEAxM0\nTWljcm9zb2Z0IFRpbWUgU3RhbXAgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkg\nMjAxNDAeFw0xNDEwMjIyMjA4NTdaFw0zOTEwMjIyMjE1MTlaMIGTMQswCQYDVQQG\nEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG\nA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMT0wOwYDVQQDEzRNaWNyb3NvZnQg\nVGltZSBTdGFtcCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArgHUXaBYyu3ozOE3RkYQW3rEUrgL\nWI8FPV2prVHiA4ngBhL4AnhrXgRQz7bWBAxUHnk3IDzjfmMdRXeYFB2+diLcWqo/\n5G9AYiRjyDzATIcoPWt4a5g5lRpBf3NR/gf8FHzzj4QJ4fjCL6FOvTl9zGNniQyA\nBM2wgskAiz4JhwOdwnlCxFwhkSuVGmw1R2zIvzwKTur2hXDVxV/BnkfbXMIyYVoI\n1nGdLIGffri+baHYZkNpCuTzcvCRSyhgqNXj3YSuKGVVn4QrSnXtJKYsdTHUhXd0\n8oBVAmNB8nAI9MjCU5HbFAdlIAmB5orXmw/KDNcbX/3R5XSFXBD7msmmK55Dlsxb\ncnPQD1WZhxgbPfgpeLBv0XS85SC6Q4sUOGlkoXMPwRYpeU+bhSlosT6ZKo+y3EcG\nzd/Q6yLcHlccflmQJaMDgr6Myx2buY0quKEQ5/qtFv7s5VPGrcCXfESbgfN6pvn/\nrvqsF6mmYL1nPHlshQtVrzHEw1mQDqHVfEg5i63juw7k5frf/dqdnltvGzIOpjfT\nqqosBBdl08ZORyStglCZQSvWs+cmWrE1m+ZxVeHIb6JEHchchPz5eAF2wT53k/Ki\nlOHacDDsZAquoqEdP4NDc0DS4IlwWa+NLtTUIQphpPT3I4ZDgCiyHEMMRdr8Bvgl\nQAd1aXjjphOD15cCAwEAAaNRME8wCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMB\nAf8wHQYDVR0OBBYEFMvR8s5I/QGf6laqV9F+mVj4P//gMBAGCSsGAQQBgjcVAQQD\nAgEAMA0GCSqGSIb3DQEBCwUAA4ICAQAT2NPko9gmzv07R++AvvujloXafZWcKQBi\nRr5ICsoU7B1Osh71zkavWQEQOP3iZQu/+A/mddncM8qD764gV30n+shcID4Lhiy8\nEnYDXNMhU6DPPvdFGSIPbiE3xmiHxJwpVaOQ6KkevrNB549H6R00xWQkW0wy7Laa\nDLhW4AbkQIvyEAf6jo5mIOYcS+Slo7suBulFe57J/5SKV8Fpo11lWN20wmNKpt1j\nMRiv7RYY2sFqPx/Sqpa2YW/Vgym0eWbBwVADHNDqLsa6z8aYbdYbxs4QsMnxQxor\n1/8VNIY72Uo8bT4juwI9zlTDSiXvRjx5W46zwiqCEkVSlsIJ1Ep4nt1vn/mfcEqa\no03vLfqqlvq0fdY2l87w2HzSL1ZUCgBg0DyOaOLNKao9LiCDy7JVRqDfuJF5KJJB\nDv4mOEN103el3YdS8U2dv9yjLfIeD0kspRGwijYTObD1G5J3tIPdmJ4Fr6CjCdDf\nHXaYQkQBc7CyqTtS5bZvq4zy1Rcpf2/45aM0625FkkhNAlW2N6ECsTTfx7KSPQK9\nNxoG4aGAjpIlMc72geeu5ZIXrFnEkqzfyCwnUkIeJh14h7lOi/dHescBcNWhyQui\nIgg4/MqowjtT3As2O+Gjyq33tgjDE1WvAzpptOmk0S3NZ9TDQspjX56ApOxjbHLE\nWOUH+pb4jQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE\nBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ\nIENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0\nMTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV\nBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w\nHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj\nDp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj\nTnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u\nKU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj\nqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm\nMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12\nZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP\nzgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk\nL30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC\njGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA\nHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC\nAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB\n/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg\np8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm\nDRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5\nCOmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry\nL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf\nJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg\nIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io\n2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV\n09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ\nXR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq\nT8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe\nMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt\nMQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg\nRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i\nYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x\nCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG\nb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh\nbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3\nHEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx\nWuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX\n1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk\nu7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P\n99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r\nM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw\nAwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB\nBAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh\ncViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5\ngSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO\nZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf\naPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic\nNc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg\nMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh\nbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx\nMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET\nMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI\nxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k\nZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD\naNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw\nLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw\n1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX\nk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2\nSXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h\nbguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n\nWUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY\nrZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce\nMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD\nAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu\nbAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN\nnsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt\nIxg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61\n55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj\nvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf\ncDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz\noHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp\nnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs\npA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v\nJJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R\n8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4\n5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFrjCCA5agAwIBAgIEVJGosDANBgkqhkiG9w0BAQsFADBSMQswCQYDVQQGEwJD\nQTEVMBMGA1UEChMMTm90YXJpdXMgSW5jMSwwKgYDVQQDEyNOb3Rhcml1cyBSb290\nIENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0xNDEyMTcxNTMwNTFaFw0zNDEyMTcx\nNjAwNTFaMFIxCzAJBgNVBAYTAkNBMRUwEwYDVQQKEwxOb3Rhcml1cyBJbmMxLDAq\nBgNVBAMTI05vdGFyaXVzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArVK5kig4XFE/X2tRUy/8uc3z573P\naPUWc28qWqN+IsxfJjK/0x/HXuexkOvGIvXpXhkSohLzvCL5iNX3O2HFGcjiY2uM\n8ds/IqD73Fn6ZvB+dMZKsQ3JUh2Lt05M3ZbLmOYOQPu9Oh6kLJBe3oTWYednACoz\nDjOD8jeivk6oqkZtGEhGdyY0v2aBbyCT/PEy8WDyFi2fTkQdnes4LW2lWE0B++Jd\nxB6K/9VC3AwFp/bkhONn7NGpT5nen8YLlB/lMLcHqHnwYOqzoZzCZTea6LnBPFms\nYAvmBu04B1gBTKV+15zzbDNPIDZrVcpOVm/4OO7PlGXlSC9NPlDMqU45tv6KCBF1\nxv17Srqj95O0nXjkoYuo7HeCKPebkSQe8fzPkUR76AZeKm/Kd4mAXRBgubZxolux\nZifq92R8d+gKCi+PSFPitC+oNB/y5Mn1S74bcxH2HJlbsRHRRd6uGuGxxUN4Ob3J\n6sDcg/sL4sLVyT9KQcWdPuHwJgKaU223hg9yTwxDC67EVGA2SoNOyVCmbQf68A/E\n9AXz1WYd6+S/HKX9uOcYNzq7BBobhw3Sknt0joirijo+14CjSFeQKM/UQ1yUNy6L\nrxISTqo4pg21iRz5eWRtZfcRlD6h3D4ix4MpqWbEmY/NGk35xyWszPer86vmuP3j\n6e3PKzkoir8wFJMCAwEAAaOBizCBiDArBgNVHRAEJDAigA8yMDE0MTIxNzE1MzA1\nMVqBDzIwMzQxMjE3MTYwMDUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUmckQ\nSn14uolWMU717DVzPaQb7W4wHQYDVR0OBBYEFJnJEEp9eLqJVjFO9ew1cz2kG+1u\nMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAII/6ndKCHTpbRuOrXnd\n2bEQ8Z13TBfrLRjoL5TGU2ZeoKWRUrKs/MhQlA7FeaoNJs0VRr2bs7y1eIDfUM1b\n3lk/+6a6APlysUPloJvbZJGpvgXXYvrbEr06hvB6YzX82lA0POZvtEIGKoErUh0e\nT/e1srxsYJrUpyjOpG4Ef+/eRStyMl3mzw1Sjy9AuNPfyYbMCQ5TYAfATzrK9iYG\nXkacvw2+HVphJzp9YZO1p1QT3rGgm0lmm7M3vaC6SmXIIuDE7/CVzuifACmk+TIS\nnHA8ENfrpjx/VVDVQjH7uwnqhErNa3PWjKWUb4Q1mmVaeAgDAvxHs3q+jD4zZy3U\nAKpqnzgb9U540IvFby8qPYI+W1CAcEG1qGDA/vtYabnYwgwXoBhOBhr/P3KxN+6J\nb3rcpy+cyVfIgwtLgfHXNi8e7Pe4IGT6iwrmUbgFrFR77DIK484SHVFy+N59201K\nf5qEsAq4EHHYc3oWrvzF1G3kx58KF2tz4wExbfg6/BySZKXA2KSQwOP5jhkxrTZ2\n7Lf7ZTz04PiUm+cYlB8qAnhxnkJdCm29O3vKcEr2xOedos5LmOKW87HWrcAhOKJ5\nRkDH30jAB64volYYepq5wxhQFh+j40zDnmAuYC/pDOFZoRszKSuREjx9hTaieBIR\n4sBFY8WLdJMuwrRbEWjHccjm\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH\nMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF\neHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx\nMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV\nBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog\nD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS\nsPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop\nO2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk\nsHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi\nc0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj\nVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz\nKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/\nTuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G\nsx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs\n1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD\nfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T\nAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN\nl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR\nap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ\nVBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5\nc6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp\n4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s\nt2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj\n2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO\nvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C\nxR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx\ncmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM\nfjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFvDCCA6SgAwIBAgIQIWYVBQUnBQW8irAdrwq+xDANBgkqhkiG9w0BAQsFADB4\nMQswCQYDVQQGEwJUTjE5MDcGA1UEAxMwVHVuaXNpYW4gUm9vdCBDZXJ0aWZpY2F0\nZSBBdXRob3JpdHkgLSBUdW5Sb290Q0EyMS4wLAYDVQQKEyVOYXRpb25hbCBEaWdp\ndGFsIENlcnRpZmljYXRpb24gQWdlbmN5MB4XDTE1MDUwNTA4NTcwMVoXDTI3MDUw\nNTA4NTcwMVoweDELMAkGA1UEBhMCVE4xOTA3BgNVBAMTMFR1bmlzaWFuIFJvb3Qg\nQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gVHVuUm9vdENBMjEuMCwGA1UEChMlTmF0\naW9uYWwgRGlnaXRhbCBDZXJ0aWZpY2F0aW9uIEFnZW5jeTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBALAH52L70A1Vzme3V41uDKknVB7rqSSrZ4+PnGEP\n2ygyLzv4LGWSLa66M5LAK57yH15tI12zWB+NocBtdYUKsBNOW1ZGizm9C4K7OkOb\nCLpG7vkX683I1+N1E96uUUgKziCVRp8C7FWMdKpa/PzqCTM1bqNHBsfdfoRoDscS\nypTD7eZsAm3eAok1swTLRfh8R6TTH9/lXCPi8yJ7uUui/Rc1XUjpv/WzJWOL53jr\n/HUnvYhcpoU/Qd+VfN16Ro/+Htqxq9jTjs0GjMnYUkIRUqKDj1yDe+Qnto8foF49\n0nV9eVOTBpfjA8eWLNoBPHnFO1DosNOhpOLTg31E+BDPoBoq8mWAvXfBmGV2rhIh\nYso6vr61mcNbxNG/m8AKylgeFabXIV6xTQrlcHiaaOZ0ZjIUKh4Rvoj3BvZVo8Mf\nbheQVdGKQIlWQ9VP5qLJiGQABVE/V7Q8tr5qkXFA8aJc8dftnLZX9lnUKhHl1OW/\nux7RyNdfRAWbu4k6radDd34VYHyIXZvspVzSRq0Mi1RF1JRRVUVSqlzYEaz4ViJs\n2dIU6bdOQoVURvgBxj0mBnfosjUb8J1CyX/+gCcBUMt/xaxU+mttloxBpKHS57WR\nSG93HIvCK3T+PFzEXZTOq/EglmvBDFpf+eU1uWyjEGfvkapIDu9It3ZYYtm+nkKz\npL01AgMBAAGjQjBAMB0GA1UdDgQWBBTMc8Wjaikxl6eNoNhUwQp1tiM/pjAPBgNV\nHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEA\nLvKHSO2Znp8BDDzJCnhTfBg13rblbPQAgOGbi9n6+6r7ZbfSyfXXp8t+ybGicVht\nWTfW4DMQyrXZcttOJTeqpt0dGL31yYqceojuHwLELZJUfVfiXBkYIwJ6XEmVtpjn\nwmBBZUC77Fq3cZxQ8nN2+18N7zXPtGmNhehMkBcDC8mzLiA3YxFipk/jNOD7eVXn\nxsKuQv6wNGxJIw5yB3tmBVVI+xIPoMD6TtH7Pcz+/RZLVlDNESynm/exCs+m6+/d\njriuQgh8pIyU6obHQ+P3PIrfR9IwQMgtU/VvEUnMIYyWQ08QoEehVo0fHFvYVlvr\nNHbhNTpx1MwhL541KPJa3p7k7kdqEOg4vUq0fQR/Ba5ICrQDvy6zChufy63dTdCH\nIbdHdoKDLcdXvpoVoxswGGyjOnFvZEcoktsRYSCad2Ut+axWE2xLo1//m6To7+dY\n6HueO39qp745ChOUyUhOZmTYU0zsQWv9/DYu1w7fYQt7tUCs3UJJbZ6Av2CV8OnA\nP3u7GOk4tVZOp36KYu+YHvh4QKm72OnltLT542ec7FPPuEK0L5OBNaBs9rogimg9\n923/f9NM93qUaAN3Qzs1UapTEj5HExQ5rNZlj6hG/zwh9NK/0EikfqdRm5cS9Zk0\nFyNWhBNjyzTKH8q6qAcp80MkCkl//Q7UkPCrQyFinI8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGCzCCA/OgAwIBAgIQVBIVn6uKdZNElMp3QFYu7zANBgkqhkiG9w0BAQsFADBm\nMQswCQYDVQQGEwJTRTEoMCYGA1UEChMfU3dlZGlzaCBTb2NpYWwgSW5zdXJhbmNl\nIEFnZW5jeTEtMCsGA1UEAxMkU3dlZGlzaCBHb3Zlcm5tZW50IFJvb3QgQXV0aG9y\naXR5IHYyMB4XDTE1MDUwNTExMTUyM1oXDTQwMDUwNTExMjQxOVowZjELMAkGA1UE\nBhMCU0UxKDAmBgNVBAoTH1N3ZWRpc2ggU29jaWFsIEluc3VyYW5jZSBBZ2VuY3kx\nLTArBgNVBAMTJFN3ZWRpc2ggR292ZXJubWVudCBSb290IEF1dGhvcml0eSB2MjCC\nAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMiutUO8QkVLNWM/AkvF/9s2\n1yfFwq5FZqNxhxZNiU9hlOBWRrjQRBPmc5DwYXhBiuAafjesAim+6P8CJsYafAqx\nj2QpotoHitUkhWgZkjLfnylgWG0qhYARNsm2wtOehAy6URHMVOmrBjASjyB3BcDG\njZqbWci2hehwBwKxHv/Xac8WRothL0LNUqbYDnovhy3GLzwiQ7GTfsMWdtnM14vs\nERvQyXEUwolJfvGkEKo1PKgbu//sMkDlvSrzpgETyIyXGZDOY/mwa333+YrObuCF\n59uU1XogJaA18Kn3r1ooWgzI83Q5izE7IsxJJclvuFx6LiyW4y+jPsp5d2mRWvjw\nxVM3TlNtSSdWYsrl+XNgqRc7W6Ilry17ybfbzxkROjNxOVlaA+nnLAz/bZxyY2OA\nBVhThtwodRbC5fATWaGB/wUMmai2PGwuxQ4AmIHpg3dmQztajoVFTLLPuT3knDaT\nQHpTFSnUEZC6oWCKnav0Skpq3Yeqwe0F2p5bVuGITyprlSiGZlCh79pKspAKNjdJ\nhZdCeAdn5psgoQxsyc/P/neVhFp6Oxew70z3LZGqzxlvxvkSKOceCqaWzSGwA2JQ\ngwYg5uje30MWFrmBoPCBNFvLwYn28+giuM64Uj5RHrEFuLcDKwusdHVTJOF2uE8l\ndl3v0Zrzbkq4fEv4isAZAgMBAAGjgbQwgbEwDgYDVR0PAQH/BAQDAgEGMBIGA1Ud\nEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFGNrQmBTVxG7yCSJJQJmRHShDSVuMBAG\nCSsGAQQBgjcVAQQDAgEAMBEGA1UdIAQKMAgwBgYEVR0gADBHBgNVHSUEQDA+Bggr\nBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMEBgorBgEEAYI3CgMMBggrBgEFBQcD\nAwYIKwYBBQUHAwgwDQYJKoZIhvcNAQELBQADggIBACMuqoWXS6RcEK/a+D29k1gv\nePsZdwM5FkdJclXMh+i9pH/SqySs59RQ7p3Yg8aZIPsWL4jGFzfKix6r0OJsB1i4\nZJGhEKFpN3Ve/tpzFOaKa77CYCEvwPmjBEg2Wze+2mz96ZaOnvFTfI9lRKdVfQuU\nTlT2/zK9L32cpV5CxEwp4xBkL+bPWjs0VShh0ScSu25Um4FYrNVenVcDoE3R/zd0\npo3z+ZX9Kol1enk3/SZ5Lydzf6kZIOXQX5jolgWPmHnpeRBBKQFD9Wk3zFAQaLXY\nRE4O8pnjJyxqjl+7fbtrcUsGit0q2Ao/W8hyLlhhCg+BaB5Hx+ktuu+N3A6jI8Oy\nLbVHsYu0PidI59wIYgxU/kPXlUq/By9KQH4GpVGHJokF3TzKT/4cJ+nbiB7Asv7j\n7x9+sehZlaBPqwqJAOBzsuccwRdQgIdM0kMZWZXSWxRbClvAfIlxerUKwIpFL+7E\nwP5ULeeVJHcFLu50xqCQsXPcQtagdclYWQWi3hG/WekNpybCbsBGisYe0/XqD309\ncs0ZlUy64GiXjVjAau9597JoarhyNsMkDOgy7b3xn8jv3nXS23aplCc49AFhv2Y4\nj2o93ABbs/xE3wNL+fF2JTX/Uh8IHdClFOmLBit4gyxxXE+Rh2PWDA4FiDyUoLFa\nVBbf3VHDqDYuLIJ8uZqw\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5\nMQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\nUm9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\nA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\nQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl\nui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr\nttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr\nBqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM\nYyRIHN8wfdVoOw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK\ngXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ\nW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg\n1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K\n8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r\n2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me\nz/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR\n8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj\nmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz\n7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6\n+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI\n0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB\nAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm\nUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2\nLIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY\n+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS\nk5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl\n7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm\nbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl\nurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+\nfUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63\nn749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE\n76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H\n9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT\n4PsJYGw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5\nMQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\nUm9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\nA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\nQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi\n9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk\nM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB\n/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB\nMAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw\nCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW\n1KyLa2tJElMzrdfkviT8tQp21KW8EA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw\ngb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL\nEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg\nMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw\nBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0\nMB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT\nMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1\nc3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ\nbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg\nUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B\nAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ\n2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E\nT+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j\n5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM\nC1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T\nDtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX\nwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A\n2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm\nnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8\ndWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl\nN4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj\nc0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\nVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS\n5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS\nGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr\nhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/\nB7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI\nAeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw\nH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+\nb7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk\n2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol\nIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk\n5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY\nn/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGfjCCBGagAwIBAgIEBfXhADANBgkqhkiG9w0BAQ0FADBwMQswCQYDVQQGEwJD\nWjEtMCsGA1UECgwkUHJ2bsOtIGNlcnRpZmlrYcSNbsOtIGF1dG9yaXRhLCBhLnMu\nMRkwFwYDVQQDDBBJLkNBIFJvb3QgQ0EvUlNBMRcwFQYDVQQFEw5OVFJDWi0yNjQz\nOTM5NTAeFw0xNTA1MjcxMjIwMDBaFw00MDA1MjcxMjIwMDBaMHAxCzAJBgNVBAYT\nAkNaMS0wKwYDVQQKDCRQcnZuw60gY2VydGlmaWthxI1uw60gYXV0b3JpdGEsIGEu\ncy4xGTAXBgNVBAMMEEkuQ0EgUm9vdCBDQS9SU0ExFzAVBgNVBAUTDk5UUkNaLTI2\nNDM5Mzk1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqj9VtYmvdhQG\nKmQmlDgPX/bcBG8xRfUK/Tt/m3Jv+EB8/l39NJkFOJRJurHwvfiZXMBR+qoN++Zx\nFYVjESp3BpGSBoiz/BThmUa0KYKuhIPutSaHbviLVUSdQNj/Klqq6H/SZeEUR8J8\nMf11YQobjIBKnrTiLhRHMe68BVGupn7PEbjFSL0FVMKE5Kdoa/i4+n4oybnP5CFP\nZcmIaKA42XWlETtMHG5LHtSGbMGtBUfTLJQNzIctGi3D1szehP7sa8DhIxOh05wY\nfuBy11xVvEyzQDEbnEDNmuuADnGu12JuWhZPH/ZlRdGfeoVBGcJ6Os4hkuSUcEy7\nqEHGxLs1zfU6nmOpjaBq0SBEqiq2SKVyw86e5FhIRwl/AkHzDRxtCXjw1xTRoFX8\nEdZaGgB55TvmCMtSnqQJq2vnbJwqLyJ9+7lQst5Q0y8McrnWs7ezCObre6z0tMX2\nwTIfpxkh9dxeN6rHH1ObQz7mnp/aDddWog9TaS1Vv+uGeBG/ptdaTfMOk3Pq/w7Q\n54/xyLPw2BhzbKVyiPFwTEdUtpta0bwmN40Y35trLtsLJbOKsuOtBlxtu30XAwcB\nijCXiXRtSpR3Luvuz7Aetep29LUUOJXX1dkvP7KkJsxNo1yNCfNeDIUyzlZsAgjx\nS6Orv8hUoAWFdOR1HXq8nDtgPWr9GZECAwEAAaOCAR4wggEaMA4GA1UdDwEB/wQE\nAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2uQNI+9UYoaE3oO3MaIJM\nUjQ2DTCB1wYDVR0gBIHPMIHMMIHJBgRVHSAAMIHAMIG9BggrBgEFBQcCAjCBsBqB\nrVRlbnRvIGt2YWxpZmlrb3Zhbnkgc3lzdGVtb3Z5IGNlcnRpZmlrYXQgYnlsIHZ5\nZGFuIHBvZGxlIHpha29uYSAyMjcvMjAwMCBTYi4gdiBwbGF0bmVtIHpuZW5pL1Ro\naXMgcXVhbGlmaWVkIHN5c3RlbSBjZXJ0aWZpY2F0ZSB3YXMgaXNzdWVkIGFjY29y\nZGluZyB0byBBY3QgTm8uIDIyNy8yMDAwIENvbGwuMA0GCSqGSIb3DQEBDQUAA4IC\nAQAZVAIlg9silosdlZ6Z2zTOk9AfLntcYCRqDNeFRHgfHEnyFPiDVBmmnTJmuCOm\nO4Yqnzb8F/xQD2DGN/0kqPd5p46/2AcVVF5SDL74ptjIQUTx9hPcgxlbr91k9zMW\nhw8VWvFkvNTnVT8yOIma88xIxWwxcZKaJhfCfEcCbTUnn/Ma4aodDXQRqZN8Qahv\nu46cxQHkc/a6UC7mENS8bxOaOLlpRqUG1vJMbDerPPjbGsZV8Mj4HSFuLwBqseJt\nWgQtfd0JT/bvFC/AEuoJGSsayqBxm7E6Mrz/QxjzfS/1LojpUbbxSZBM/ybHw1nd\ndF/BUF04XJ1oVWlqtEB3yV8yKUhUk8GzISN2oVUwaSM/MUnEoc07dlmVWoK0rXG1\nvqaRzIAVSi/OlK4YVUl1IES48wGbwXgsjhBMp2StrTrrTB1WLn+U1B7QCtXJVIEO\nHv73lPlhOj817tNgyftIsm7C2b56bpgFcACj0RfHxjSvbPVNj11SDN2Am3pt55jj\nOYVcP4vMRKJANjKTElaQAp4+WWgCH1aIHq/B/g97VY2X2bumk0e6fPhHtjnXjPJA\nbIecDP4t3dxx/A6RCKRDPYpX3d0H66eXUdC6hJmti3n+yQSQgxMr6ZcNZYnyES03\njku4u9J6OSrF3NBdDd0EJ5ifWP2OhrsFf/DtN5KQ3Zy9/A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\nWhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\nZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\nh77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\nA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\nT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\nB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\nB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\nKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\nOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\njh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\nqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\nrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\nhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\nubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\nNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\nORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\nTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\njNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\noyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\nmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\nemyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGDDCCA/SgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBlzELMAkGA1UEBhMCQVQx\nDTALBgNVBAgTBFdpZW4xDTALBgNVBAcTBFdpZW4xIzAhBgNVBAoTGmUtY29tbWVy\nY2UgbW9uaXRvcmluZyBHbWJIMSowKAYDVQQLEyFHTE9CQUxUUlVTVCBDZXJ0aWZp\nY2F0aW9uIFNlcnZpY2UxGTAXBgNVBAMTEEdMT0JBTFRSVVNUIDIwMTUwHhcNMTUw\nNjExMDAwMDAwWhcNNDAwNjEwMDAwMDAwWjCBlzELMAkGA1UEBhMCQVQxDTALBgNV\nBAgTBFdpZW4xDTALBgNVBAcTBFdpZW4xIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u\naXRvcmluZyBHbWJIMSowKAYDVQQLEyFHTE9CQUxUUlVTVCBDZXJ0aWZpY2F0aW9u\nIFNlcnZpY2UxGTAXBgNVBAMTEEdMT0JBTFRSVVNUIDIwMTUwggIgMA0GCSqGSIb3\nDQEBAQUAA4ICDQAwggIIAoICAQDUppeo8vSQEUOttIJGQfEvkW9jos0NINy9DDiK\nZUoKKzqodKl3oYuO8i+B94QYza3rYraSfeBB5U5UODeC78vg7c+7ysyjS/db/rh8\npwhty0PETCIUZuOdA7l3IatEayFHI8gg+irLkXYddWz4m+kPJulDL5ogBWgYx46Z\nhS1BB6ZkjljhjZWApE1f9QLYgXnb1effoiL9FKdnFuzZWEzKqd3qGo6pCGRPUSG2\ncqJO/1BxvTtl5L1/UxGu3xA5e132R3AX90ORA3phJV8s/PiJETzsOVQWScQhmnHg\neYt2HXY9B1m4B7GM3MfNTuH7rUNNP0DvIWIvMUROacdvIsurVEvowvoRaKzIbg7e\nbMUnlglRAk0Btle/MijVCUOW98SItflU/ho6arcstSRk+0p4csP82U/ITiO5KdgN\noUhBkwJtvxKFm8bFYC3wkfyZ/SCUnnFjq9VJq5DshzmFf42FzAvo20s7DvzCdn1G\n5zkmnt9V3x6E+UE2JmwCWSuO+7zpHyckYgRnhOE/2J0YTpagJe7KKANPAlHP9zU3\naaS01tbVHhlDJxYfR1HuSglMEVq2Wz1h6DsQvtZG5vQc/bhFvXz6dVrs4VIjDY4f\nhpdTkVybmyjWjuVuJ60gjKfBQamXN4ss6m4YBZf2zgNS8b15NJtAxyOSdPNv7aPp\nWfBVSwIBA6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\nVR0OBBYEFMuw3T2MPN9iLCtmPJ486RVtcbTXMB8GA1UdIwQYMBaAFMuw3T2MPN9i\nLCtmPJ486RVtcbTXMA0GCSqGSIb3DQEBCwUAA4ICAQCifVUEZu4WFLyCgYclGTli\n9P47H+HAcwBxynWp4nPxxQ1Bo12OwS3ZZVvZieLwjsWgfb3LzEZTH1/tILYCKtYT\n8p19UUpAVXGtnux26kUgjqr6ekOacGd+E96Y6MuN3R+sNNKhte3+uOcWz/jRODCN\nNInSzn2B0h7/URhTNpPcCcsIFrgI11owkIoK+S+1z8TNVHIqxr0B51gLbgZAtAnO\ntI6zmumJkZSselTh++OELIOgT/7r6MH067Ym0zjELa2sRYA0bSE9XYU64nv+VLfd\n6IVUy6TxqylQeNcktaMvnq8RZq4YuP1dKM9A11XgLOtSMWhDZgWXkrvF8SEs/RJk\nMZlDb4udS2D+FF5SsyOo4Zh67hTJoeLMP3YhYv1rDdm0SpXmblt6JMPTxtYfous3\na06j32Lr6w5KCL/rGIj7RxqtwlHD1Xz3HyuzyEpQDmlYIGIBSlvKY5YmIq726ZxA\nrGcDnZ1pFcLA+F2nJLEnPL8F4quiysmwLX6jwTEgRiFlkt3K3t+TG7xtL1+pFqRX\nhyxymlqCZ9FE4j0JCoGMHhD9xjRo7P93YXZ/Jvfb/BJGEqrA0fh5haICzIuqpK1s\nFMC9/GiuRH0i+QpFXewE5vrjpMXm+bIZw9mMqJN7OoppO1ITPB0zAk6WQJ+5lf2T\nFzPByQv2/b1pEPWtKfvj2g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFoTCCA4mgAwIBAgIQLHA+VOkP2ZggzMbZ9UY/NTANBgkqhkiG9w0BAQsFADBa\nMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmlj\nYXRpb24gQXV0aG9yaXR5MRkwFwYDVQQDDBBDRkNBIElkZW50aXR5IENBMB4XDTE1\nMDYzMDAxMjExMloXDTQwMDYzMDAxMjExMlowWjELMAkGA1UEBhMCQ04xMDAuBgNV\nBAoMJ0NoaW5hIEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEZMBcG\nA1UEAwwQQ0ZDQSBJZGVudGl0eSBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC\nAgoCggIBAOFRJSx4u/rui1XDTiVkGS2UQqTm3oQPITUo3DKJPvs0c3tX6awSKUoM\nmCzOyb+kT6VtDs7CzhgJMRBwcg0ia5798whuktLAJc+1s+thfxeE/HVrtaxXF0EZ\nDDTVL1Fu1fdRa3FvMrHi066g1jsUUEgZdPztr7UgqJLgP64H0VC81d2v1tD5zs6S\nuMaBjMX5OY2+9hsumjhkv7fNcuf/7YlauKR1WuH+rzIMbSJukzWoYuLArgqX0bCq\nPvY6UB6bUCoH25eVYAM/o6RdGVUhJzpJnsvI7CzMmxdI0wgQsqlvIQH0WmHd096J\nXbUK8+AV1wZ3C17YaFjfoHe+XxQKRL0tHxo+8aosXQyFDOej24s4BqVbd0zUyt1X\nleSj6LJkd9k0r2gdKm0/MkcmmTOfCmBoEVZb1gLxhyrYadhRKZej3vchJozd8yyM\nBY+ZNkqQsVhpOf2U0xfWpinDUAvVu6MhQE+xBxwAZFfjUVRz4+sZdAKIdw/RflWD\nAszZzHSlAWyvlbC52RindZoeTo9rXkNHKjGEA6yIETDos7F4x0PhrQWHnGhLI597\nND/M/e+cQsvxNhELNdqaeqGvhU4uWmwneQtFgSV2ZG9k52jKluUEMQVYnqi0j/h9\nVsTtKDHNbYnikHh78ZAalERJ04PvGCPHamW+n+q0e7VjBONc4Xf7AgMBAAGjYzBh\nMB8GA1UdIwQYMBaAFMCsdqLTXf/2zRYAWzinf1V9hVlsMA8GA1UdEwEB/wQFMAMB\nAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTArHai013/9s0WAFs4p39VfYVZ\nbDANBgkqhkiG9w0BAQsFAAOCAgEAfNPmbXRLuY9du1uVIvxlr0psXoETrLoUCE2v\n8Hnx0iVCwPjZZCoCNcHhKg86fWoaOhZhG0FGqHVDv9881e1MO0O8LJA5/kyOeetQ\nvsDNWFihMB46a5GR4TRxlSEUoCASy4MqzIGuRuAebbIMytOCiPpua3i2XK28QSva\nfkMLgjP9MqwF/KmKfE5YrTcWCfRgdMVT3JNtZYC9cSCF8RCFOGQj0yGCgeu3bSZl\nTqvQ1hB1huroHTWf6HdWsZO6qfl3BdQeIg1LuIflM58K4QG8kSQurL+hAzASN06V\n3rziYz6cM+bYWP5twY+2cwrBGkrB4IsqxzdCZfbFyHXe+UxlqDb/2+ldPczGY/A2\nC3sCT89pvcLvpZ4hTl616jBEo4MtMYYJJKRWwYTz63w2czJtF6HnpTCT01q6h2aM\nBmjJbhNI75kpUd3FBDdj3lY7jKX3XIVAHPDULuM43ojnpoiKkmo7gSehjl/9LIJY\nlq/asEdwPg4kUwymUeqCo8ttc66xcAeNM4A2P6ywPl8eBrtuVfYZK+xq/ZuaMnqR\nortgZGH57BRmxsE3vrrcsNSvGhpdd66EVqGxzGO8kzfDRDi0hDFjuKX4wrGIoNnm\nRdlHESm7na7pbEGyTl2VwHLlAnbv0NtBPu/gL/ukgvx60RunN4pJo8d/DG9CNhx9\ngMl9JH4=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix\nDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k\nIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT\nN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v\ndENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG\nA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh\nZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx\nQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1\ndGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC\nAQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA\n4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0\nAoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10\n4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C\nojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV\n9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD\ngfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6\nY5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq\nNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko\nLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc\nBw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV\nHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd\nctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I\nXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI\nM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot\n9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V\nZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea\nj8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh\nX9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ\nl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf\nbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4\npcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK\ne7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0\nvm9qp/UsQu0yrbYhnr68\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN\nBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl\nc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl\nbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv\nb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ\nBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj\nYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5\nMUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0\ndXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg\nQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa\njq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC\nMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi\nC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep\nlSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof\nTUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFzzCCA7egAwIBAgIUaKX9ptAcXj/P5PmZ33psbzmpf/wwDQYJKoZIhvcNAQEL\nBQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ\nSG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n\na29uZyBQb3N0IFJvb3QgQ0EgMjAeFw0xNTA5MDUwMjM0MzZaFw00MDA5MDUwMjM0\nMzZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT\nCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u\nZ2tvbmcgUG9zdCBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQDicuzwRxAiw0TKBXlbWdg6KyQhJOqviPh9WgVlwxhIAQTBfloDNXlKw8Fi\nW9JegvcM+7n+0iEdXIe8JuPEpnuZU5cE2N8SSj5lRefOG2WpcDmWFmKBzOngG4K7\n7ajgQmvpuskbS0j9nUYSQOUo00xH+mKIZ4QNV0wPcamFf1blFuijQrpHtt3o42r3\nCmnl8xTjXFXdh/9+PFxN+ckbDptO7n6s7E3ToiO3iJt5oIpjRx50V73Hrv2Urh1K\nRcPH9qVTB9Vp+HPlZje2pTB3qsy68AnFKFeD8KIZ8n5FtGzrSSK6jjojHB2Jso9p\nRBMoumJVEYKOWX58TbqHt+4z3s3ZwvGULVM7pNAWVA8RIQp+WMOugsHE1SV3D3bb\nDV73YjO1p/zKHvOGilOI3cIyHz523p+PDIpKUC3IUFEGBUFXm6R20BzGbhZIJs8y\nR1kWk0tK1J+6fu0f8wV3Q8ctYvFg1Ywo8f4WI4LPWmufbmn81KhJV/c+kglEwl0o\nvSpUM4ianpdNLK+9C31KO1NEvcLBLdU0zwKgFAlRSorCqgARbprRHdc82fHBftgZ\nUBLEkSthBW37Mo+HrHrAlbaNB/Uo7r1oi+/+TQZDzRcloP7iVCa05fiQ+w3Yogwx\nc5RNe/tSFKtlUQoD2vJmA5LffEWXG049exBRp+mDjbk/tJLRHwIDAQABo2MwYTAP\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBxjAfBgNVHSMEGDAWgBRhteBC\n3ravpyDq9iTIp52FoFhT2DAdBgNVHQ4EFgQUYbXgQt62r6cg6vYkyKedhaBYU9gw\nDQYJKoZIhvcNAQELBQADggIBAHqni9ztZvbdyRDfUPHRkDI9j9qRssdTrnH5p+zE\naOIO+o4aXyqS44PR/Nry5XrIrKQXLea43ewqF1GidWkoObpYPx9Qs+3DGbcW9cao\nWj2g0Hc/UQdFrG+flMu/bC4PiQmSNBk57XqyWWWwdu0nRh1Dz9Q2vGiKm9Tbwis/\nzl1UmcoiwXmEmP+6QVi/RUmZuwkblo5YTPrISEKUG4nJ+VJmy51txA3pvF831boI\nYf/VS4xj6P734NwZE+lSaraBLBhkbN7YMFf/ixnHv7dyXlauw/YZ0v2u6balMbgy\nTsm8OhspH4lhsPvH+4gGKcNWpk1iEPCrUbdk9CRTkIM6p66pEQLgglQjvQS+NLTO\n2ao+VJpIAoshGBL4mOCqqvmrriu/tWuDnyLQWFgFFqfdx5Ppe4Qo4tXuqDX5zM62\n8CdQUTOHMtRkcojYNUC3rZvuWhSpfoCYPV3Rd3TK+JGG10Lp3KDvMCWfyDpgaA8t\nUfjxlrBF9ICotJGHKUMpkTmDNWJtuOn8+P6aTihkfg2QaQPyq00+TtGOJwNEl2Da\neIpljRZ1/A4scpt4imdisa4sRgWQEThX13YpI6jAfQnfh6vaWx96EzOBsvf+HO/C\nnmVf5Bnpcq/INRy++9P43eTYwzlO2UNgq3U2VxvLBVrYQ/w3JVcaPbVW6/Lv2yYw\nsaDu\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFqjCCA5KgAwIBAgIQMmmiv0BrjbRHg2Q8iw3JQzANBgkqhkiG9w0BAQsFADBm\nMQswCQYDVQQGEwJTRTEoMCYGA1UEChMfU3dlZGlzaCBTb2NpYWwgSW5zdXJhbmNl\nIEFnZW5jeTEtMCsGA1UEAxMkU3dlZGlzaCBHb3Zlcm5tZW50IFJvb3QgQXV0aG9y\naXR5IHYzMB4XDTE1MDkyOTExMzIzMloXDTQwMDkyOTExNDIwOVowZjELMAkGA1UE\nBhMCU0UxKDAmBgNVBAoTH1N3ZWRpc2ggU29jaWFsIEluc3VyYW5jZSBBZ2VuY3kx\nLTArBgNVBAMTJFN3ZWRpc2ggR292ZXJubWVudCBSb290IEF1dGhvcml0eSB2MzCC\nAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALoeomkbaE9cj7r0I8deZgV4\nhTZYo6J/Z++iDBaSpEqL4KCSh1U3C8TRxNBAQ5cyUE/slUe3P69DBeWElwnvVlTn\nQzNH/a3xOpuYpOHkUaO5rIwL7iUGCfLTujVnYYzCvSbL12PM14Mz2Uzi7/kbn6jL\nDXYBLXLJIrtokd6QDzs9tEK9GX2fhFw8fkI3hrFgwkiHUk5cV/7Okq7KPla3s56V\nmpT4L6HQoi7CVFpszMzWrUtH0C6HgjOoe1A5pyossVsnCp+t9RTr/I1TsnMrVCP0\njJeZl/s13My1+jMUJo11pySm6BQuLaaAKIOaP7jKO8f1GOD97I55+6pCbEpLFn7z\nggNuuucRBqWfhCvSYG3pRu5BWpa5FP0cP4YS8VQmJv1ngC/lqC0oLkO3ZMLv5Ld+\nltyEiyfZdj2YgVMU3EJFoVRn+doYZpAKtEeQPAHlK6Nm72/7MoPxM30yIWylRRU/\nL/NVkUiTnyXPLTw5O1INGq/H36tvgNiQy55xcmpCaZPqkgA9SQTZo1y6RfsCEP+t\naXRSpThjmmaIBLIRuhOqOdWDX+1lW1PInVyyhaB4cDVNXCQQpPYxKpJVQdnzF2yZ\nE1j63SjQbBO9W4eNk4OtWClWFbRYJ0qbEWygpmdFOs7Q2M7/kDPsWjFND1IS+632\nYV/kL28NZjDloE/Pz/1fAgMBAAGjVDBSMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBScvAHnq2Q19TGbjbX7F5mIAsub8jAQBgkrBgEE\nAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAh6tq5OdrJFI99iKDT1MERTKc\nYVoWXJxEtaPRltBA/s9mFV5+QAAgFf2nqmTap2FmaMLdUnEloGq53cDNzoYI1Dw1\nES999G/S2gyXA2WXg7Q+OssJdI3rBcp66YCwt1EtIpPjmhnu7ZcIIYOtxwqRX8TK\n216vuOeMujpJ0lUDNRkZUErihqe7eD2V/bEfRvJPZvL7v4VktgojGJIJnklFMbbW\nFFee/IlFdH85zMBqaMjPR9DhHsfTLy35LCQ7/Gq6lBPezHLyoh3LH5/Vg3cmXn6b\noK9pn3jbpcFucVxIQk4r2Hi41Q+lP2zLj5DNR9iQGUmF1mz84quqQr/LE5e/aUR1\nYzUt2qDH/WH3ykE9VJz0NsDkbiFIn11xYoHT8iXmWYxZQSZIp+PrZ2rT7DS3mPfM\nyqM2BpXnyDBZ9//JodHkebzfEx8u2bN10QS3IwkhzB0hHCecDiv6wYcYyfr5SYOM\nEhb7xRLOOw9C+vAFZX6ox+tSSvmYXnGjrBLHKHEaWnXPh8ofNygcFJ2QUG/Gv0rM\nxyXPMd1bkU52qBHVdmbZv4BzYrDsw/5EvM1ZEwsMLdihzKpiTVRFXqRSo4xXPBQx\nk1TOpRZUXi1Cs+5lqbadP2zOYdlWy97qoFbebYYD+reBaozS2PPXtsCsKYRZIw6b\nl2rmoM7VKlQY71CYeSA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL\nBQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6\nZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw\nNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L\ncmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg\nUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN\nQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT\n3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw\n3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6\n3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5\nBSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN\nXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD\nAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF\nAAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw\n8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG\nnXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP\noky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy\nd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg\nLvWpCz/UXeHPhJ/iGcJfitYgHuNztw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFkzCCA3ugAwIBAgIRANaWLsEKFZMSr49jvNREyVswDQYJKoZIhvcNAQELBQAw\nYzELMAkGA1UEBhMCVFcxIzAhBgNVBAoMGkNodW5naHdhIFRlbGVjb20gQ28uLCBM\ndGQuMS8wLQYDVQQDDCZlUEtJIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMjAeFw0xNTExMTcwODIzNDJaFw0zNzEyMzExNTU5NTlaMGMxCzAJBgNVBAYT\nAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEvMC0GA1UE\nAwwmZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggIiMA0G\nCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCkWR+gL9++4Pvp3LWJ/lqXA8k6d6eO\nXK/y4xg59ardD0bSaA9XnKdjYNNYzjXCp/aIwk9/Gyjp0KcAxBdNbeIPxQ4mIyCr\n9zoookwKC8yOzuYAmlpADdRQGpvRDZyU+dvuXNDxigfNmitALEmkXWJfp2vf7lYI\nUPNCGGwxsF7lnHOSvA7SDH3FOFe8u1jbJhkC7eNDhIpOVmvbraEx2cwiZ5Z4/3ed\nzGTFMiBq704w1SQl/Yh5r3Ea/tVLGxWIvBhwqr2tOApmMEbliYXVdiSpqbPmWWAP\ntKlTwjqdRRrWruN3XsRiNjMvMMS/lfEtOKV16NFqky5Fh0tKot+/WCeaymIZql7U\nsYBJlt0r7F+Pm+Cdl4j1hAOjr7Olcy1BuuUHt29rcff3yVqvaZmzL8hPQutsa3Fn\neN8KrE/XSoUARhrVzbif6pWdD3zRxgWF5gjeiBeB9tW1buqhHNdhquNZQomcWX6x\nfGQ03WEjKjm1EKv8hqlTGsXrauKATlmRwDiJ/rNd1vuR6dewfdl4CMz1K8wr4aHW\nlHPB/lH0jH0KtZqKufXa4Mmz2I+qgoONaVMt/QAEGEqg2lTheYyJ63/1gueguXdN\nrvm6AjuIdut8XbNaE9t8KRZrmdEd5Eghog1eAYjovvGYTT7HFlccX+EIbxxMWENW\n94BljHEOogRnTwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRy\nW7qqcjjuJZAktZQi+gmIyosK+zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEL\nBQADggIBAAaj8bZzVcZnZiHlnVvWlC5KImDyVAGQof21v8CVvxhfLPZrNQ78Mcjt\nRA6Sl9yv3VbPtR+6cpwwyJuxkcB2d9QPGpUa6U0UiKHPawKmautkRU1kjd7862zy\nUwmhhVEV0E+eYvoRuc7IJA5yZIh1NCMwKj+8PDnMzN0LNru9BoKPEgHFcQXRJKGZ\nbMrk96rtitenCq2v8OCAu6GyP1qHZHCGM3sNHtcAhoNDl3X1O8FI/bYOQ6gCbrg+\nf49O4l20fZ4wNC+o8esnh2gNWgpNAdvJFFiV8ppNFqpz2/QliBc4t69ZCQm0Hy0P\nq/W4J1XuRTAzuO0bjryIbK/4Wipr4KyxBSShCfyjD/OwLXuWuraUBxVFjincWA6p\nBdg7OqB7zYrHZoKXz9Yz4Gf8pttALwXlxYt6KnrwsDabDBj2N+lBof2xKPlva73r\nH0xjcXtQ3Sny/+73x0Vf6DYK6GxbIsPowOcm3OOolYDluToT2wBLGv2uM0d+eJTj\nsV0rtVa1QoufgcX8k0wQtboKvH434/pUbfUExXCzqQTSUdeFzX1vQ49ZaOUxVhFx\n+WQpCRP+0B+8iwA4stDKNFZ2EDlWc2bD0UnZvldPPxZ9ani3qIK4W86uhYoKQgwD\n0RfEGPfYV4jGgrgHuT79pOku3G+6kJLuZbBQNNMH2gGXD7znc4J7\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIClDCCAhmgAwIBAgIQeThLtBkajXQizP+FMvLkujAKBggqhkjOPQQDAzCBijEL\nMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkZMMRUwEwYDVQQHEwxKYWNrc29udmlsbGUx\nITAfBgNVBAoTGE5ldHdvcmsgU29sdXRpb25zIEwuTC5DLjE0MDIGA1UEAxMrTmV0\nd29yayBTb2x1dGlvbnMgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0xNTEx\nMTgwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGKMQswCQYDVQQGEwJVUzELMAkGA1UE\nCBMCRkwxFTATBgNVBAcTDEphY2tzb252aWxsZTEhMB8GA1UEChMYTmV0d29yayBT\nb2x1dGlvbnMgTC5MLkMuMTQwMgYDVQQDEytOZXR3b3JrIFNvbHV0aW9ucyBFQ0Mg\nQ2VydGlmaWNhdGUgQXV0aG9yaXR5MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFOED\nC2VvrVnWHu7Jv7RMxcZcLzDHn1LbaGHAaRDiknoaw7+SqIk5ivvnoLtxpKDD33fW\nlDcTX35TXVC640wIx2XiQbDmWfKc+MCyd8EKkSZ38mm2u9BBPCqIGpSRFsY+o0Iw\nQDAdBgNVHQ4EFgQUm3vryP+D8lKYRzAKVvg4vuPrAM4wDgYDVR0PAQH/BAQDAgGG\nMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwMDaQAwZgIxAKlkWPecuRNmIkl/\nstEC6RP8HPukNJLkygcNt7FSeCg0y/IhVpGGhsiKC68yhFRliQIxAOx5DZ2J8AwY\n6ntXUq0L5tR5W8ub4gZFdRi90Pyn3cfhxyK240EkXSPmqJ8AalAyJQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF4jCCA8qgAwIBAgIQTANLrGcYTH+vRAhNgpbHsjANBgkqhkiG9w0BAQwFADCB\nijELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkZMMRUwEwYDVQQHEwxKYWNrc29udmls\nbGUxITAfBgNVBAoTGE5ldHdvcmsgU29sdXRpb25zIEwuTC5DLjE0MDIGA1UEAxMr\nTmV0d29yayBTb2x1dGlvbnMgUlNBIENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0x\nNTExMTgwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGKMQswCQYDVQQGEwJVUzELMAkG\nA1UECBMCRkwxFTATBgNVBAcTDEphY2tzb252aWxsZTEhMB8GA1UEChMYTmV0d29y\nayBTb2x1dGlvbnMgTC5MLkMuMTQwMgYDVQQDEytOZXR3b3JrIFNvbHV0aW9ucyBS\nU0EgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A\nMIICCgKCAgEAhN+opqOMC3geyE0Zld0pkJIgNZAqlI2CMy1wElilCIqewQjzk9Zo\nwC8Uvnmk/H3M1bw+j+2cSgJhWT2qw290ANL4GjTUVJ5qdEeaL+DS9w/3w90/pb/B\n+n1CaWAAgOw85ruBN6QeBhQ9V4+QpDVKNHOHthrDXZDvBk1wdjY8gontz2QZgyVD\nThzi8WpShv5R5H443xWNTGxgQUpPsEBVRjl1yYE5AHOKYuoPZbePT5dAzs/uwWoo\noHGpmSfRPck1c3qAmfh9hrmdeTrt0yr6fqa4/1cqc7Kmv9qJugYb2mWg5r5glIj2\n32bhJ2ob/tBeqY0giwrEH36IQS+ywdDztmjtyDvx76oH3n7XIuCB9qXqexb0QlSd\nln72YhZTzf0Kq7JCoU4qiEJ1g72M5U165x3jTLje46tgOC1nKf7kX67CqOi/rmz5\n67NS8X/p7MIv2Z3KF55C+jtYwT6IYk9fk8GXbWaPHCLzmsH07blrGn42hMgxuPBe\nK36V5HnPdUzC2AS/OI4os91btthPI26S6DeVroOu1vw5KkYGH/GEdSHWuE6mKpdY\nZfWaGAHX9cN/KckQ7nNKQ3Z70aYwUf/WKx0eYoS++b5pl5nHDed8JFB1F/2kIOc1\naANglKfZDcYaLOXiTtXMDsB6MFbvYJK+2S71x/DoRc/ahq7v2HepEicCAwEAAaNC\nMEAwHQYDVR0OBBYEFA/xSkp1dAURDB3YW5nrv/6qfV7XMA4GA1UdDwEB/wQEAwIB\nhjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQA9y9JGePX2Ohfo\nw3tk0cW7kHiN9U+5xC2X+wvmxbjxturoWEs0rXd5LDUfcn0CPu610BaKBjeWte9D\n0AkQLJdmx4EfHuYnxYKRWF7zyFtBaICDkbmcgfgn+kXf7nnyXG1wAlTuwFPYQ+sF\nesz0Ud2p1CJ9ajvy/ojUUkk6hZJkU/hqU2CIj/Jb1K4rUuDq/1R+oeTvhhungwsG\nZl4wgIxVoEcz/2seREhLYaoePuhMZMfYbX0Orjw8Qj3KJBpw8WEUnDoY1fAGKZEi\nsjo6oRZUYxr5M5VEnySjIWQECOKb1d4IUhxiHFMWRzVCJsenDP3zWxN3Aoxc4hbw\nGB/ZffXfAiSIevNe/xcOs2JnoauxF449Okaw9UaMq4TY9Q6hIOvC8Jl0PY6zA9gk\nxWzrawxTv2Bp3YwoxW/Pu9KBdyvGfLHESmwVEDcpXa74sREFxBSN7BOjRP1Ni2i4\nwf+d1TcuSPgofNz5c1PZtgF1Qnq/C99RULhTsuHudJDLvKrQcYOiq07JELY9HO9A\n109DkDO5AZZUXSrVBluShrgGEIEGyJHbKSCyU73zS1tM22kfiW5UP9eJXee1zQy+\nP314OAHStmemz1hIlBpF/ZBzScq1Q6AhYo1JBCaq+B8uP/IuofKr9AYesC3EwXBC\nPf3DUUmIAA7Kgg2beQLiwC6T3+Ty8Q==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFcDCCA1igAwIBAgINAJgzyagAAAAAVlS8bjANBgkqhkiG9w0BAQsFADBDMQsw\nCQYDVQQGEwJIUjEdMBsGA1UEChMURmluYW5jaWpza2EgYWdlbmNpamExFTATBgNV\nBAMTDEZpbmEgUm9vdCBDQTAeFw0xNTExMjQxOTA3MzBaFw0zNTExMjQxOTM3MzBa\nMEMxCzAJBgNVBAYTAkhSMR0wGwYDVQQKExRGaW5hbmNpanNrYSBhZ2VuY2lqYTEV\nMBMGA1UEAxMMRmluYSBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC\nCgKCAgEAtHPS1mXTAu9YOvSCgtOn4Ipgsjr1sWU4pyQOIWt96aCdM6J0za6RupS1\nzMaAtXHfSHHKdUunv/8m64T+uXIWyMJ+htS/r+5jNbnA5NoFT7hIniIo/1UFI2uB\nTrMXESwqJR/k4d9hyDzyVmnQVX2WELKoe1aQW6ZeU4tB48eHxzG9NDnsGSHZgMTo\nDdvaAwwA9Kq1ggYlDMXZGmKd/QpJBfwcvpNG/M6Jkf/NzF9IX9w40HVv0i2rzCIS\neIgSH+DVTne8LIlNdnqIm10H2rNnmNE5znpGq8/2fVclE/qExANwrwx2DNJAJHxZ\n33c3WVCxJUZOQh0IIglyVcRC6m9vZVnUTuA9o6twfOYJMFV2Yonzb9IKprNuGT2W\nhnpmlM3yzHrwBwizaa4b/xxxGKJE+dvWDYQQgXRJYWLXEPABpkXAtdBS9FGGPeL3\nFila+kqeJ0uORvFyPqf1pAzgCxeaIv/5fqs1jgGE1XWTf+Z1qHpk3mI6AkcaoCPE\nTD/Q3E4z52y7+vYYECs0MF/HM1CZAumxWUZVZaa6pIMYi83h8coY4tkg5reEhx8L\nVnxNMVQm8plWyKZZ1oUz8pDMKFrIbKTLpkdGxJpVOYRkjXfnCj3D0BL3dqjMHLMf\nWIU6xDaN7JrsDuccyZ9P+9B6BwzGBbCrjbpyXU4j2W8MXPimctECAwEAAaNjMGEw\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU/hGi\nbBDu3uIDuFWCTiI8huQ+a1QwHQYDVR0OBBYEFP4RomwQ7t7iA7hVgk4iPIbkPmtU\nMA0GCSqGSIb3DQEBCwUAA4ICAQCkgIYu56adjf+CV2Ny5xpg36uyubIBEmc4QOZA\nfFi8zEhxWwGXnHkcHnHSO6PY6KLiGAGlRajj9O+ru4p4/MeIffIFYJrbcMN41av4\nLTOMa6L2yQPAijxm3Z3o7qdOJQ8U3/gPFi4eF6dYyNkF05iivGRCU/4kyXWqJu5u\nMjMIYaA2fcq7nbu1cV4GgWr/Z+6miD+2P9MXTM4EzrMLdTnRwOOcs5qiGVYoi5ak\ns58WSdyEICLt73JMXxCqHwkBO1XIxmyvp9Iunu2wzJFtZMPsGL46akuuAS4/ec00\nHDiuuQ1hBHP3nik7p7aQOrgsIzTDuAwGUcI+IZmfPBSQyqkm9UDjIul9zgMX7P+8\n0ZkuxGSPPyxZYCQ8sNvDlQiqAHWynQsgGbT3bqmjvWDwMw/iZr1H9giKkDV9RYZK\nyZ7Ez1/fcd7MyW45iE25Ss8DdAdZK+386+7V0tU5bXcN2NF/L353vmGYjSxScTCE\nvqDmsLAHCMW0dLeLsti62ADyGcf4oSIKZkSoFgh1XllESEU0NQhK8HslC6ZLUX93\nzQ0zOKsAkWZMiMFOKtQ6wLSG3oSAylBvgPlNZYAJFXUtIlbltZEjne4l2BgwKHLb\nf8MxTo7YvkP6246aBZn999yUiad42J1r6f71JMe60ulED4NLXZ//JBif0dWE6CFJ\nt9sg5w==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD\nVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk\nMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U\ncnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y\nIFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB\npDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h\nIENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG\nA1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU\ncnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid\nRtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V\nseq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme\n9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV\nEY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW\nhnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/\nDeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw\nDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD\nggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I\n/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf\nke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ\nyonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts\nL1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN\nzl/HHk484IkzlQsPpTLWPFp5LBk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV\nBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw\nIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy\ndXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig\nUm9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk\nMQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg\nQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD\nVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy\ndXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+\nQVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq\n1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp\n2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK\nDOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape\naz6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF\n3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88\noWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM\ng9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3\nmjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh\n8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd\nBgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U\nnrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw\nDQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX\ndKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+\nMWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL\n/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX\nCI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa\nZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW\n2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7\nN6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3\nSewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB\nAs8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp\n5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu\n1uwJ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD\nVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk\nMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U\ncnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y\nIEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV\nBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw\nIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy\ndXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig\nRUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb\n3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA\nBoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5\n3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou\nowReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/\nwZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF\nZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf\nBgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/\nMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv\ncivUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2\nAHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F\nhcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50\nsoIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI\nWJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi\ntJ/X5g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE\nBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK\nDA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp\nY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz\nOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv\ndXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv\nbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R\nxFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX\nqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC\nC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3\n6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh\n/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF\nYD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E\nJNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc\nUS4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8\nZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm\n+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi\nM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV\nHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G\nA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV\ncpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc\nHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs\nPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/\nq5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0\ncuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr\na6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I\nH37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y\nK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu\nnLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf\noYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY\nIc2wBlX7Jz9TkHCpBB5XJ7k=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC\nVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T\nU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0\naW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz\nWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0\nb24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS\nb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB\nBAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI\n7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg\nCemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud\nEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD\nVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T\nkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+\ngA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC\nVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T\nU0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp\nY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx\nNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv\ndXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv\nbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA\nVIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku\nWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP\nMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX\n5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ\nytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg\nh5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGoTCCBImgAwIBAgIBATANBgkqhkiG9w0BAQ0FADCBlzELMAkGA1UEBhMCQlIx\nEzARBgNVBAoMCklDUC1CcmFzaWwxPTA7BgNVBAsMNEluc3RpdHV0byBOYWNpb25h\nbCBkZSBUZWNub2xvZ2lhIGRhIEluZm9ybWFjYW8gLSBJVEkxNDAyBgNVBAMMK0F1\ndG9yaWRhZGUgQ2VydGlmaWNhZG9yYSBSYWl6IEJyYXNpbGVpcmEgdjUwHhcNMTYw\nMzAyMTMwMTM4WhcNMjkwMzAyMjM1OTM4WjCBlzELMAkGA1UEBhMCQlIxEzARBgNV\nBAoMCklDUC1CcmFzaWwxPTA7BgNVBAsMNEluc3RpdHV0byBOYWNpb25hbCBkZSBU\nZWNub2xvZ2lhIGRhIEluZm9ybWFjYW8gLSBJVEkxNDAyBgNVBAMMK0F1dG9yaWRh\nZGUgQ2VydGlmaWNhZG9yYSBSYWl6IEJyYXNpbGVpcmEgdjUwggIiMA0GCSqGSIb3\nDQEBAQUAA4ICDwAwggIKAoICAQD3LXgabUWsF+gUXw/6YODeF2XkqEyfk3VehdsI\nx+3/ERgdjCS/ouxYR0Epi2hdoMUVJDNf3XQfjAWXJyCoTneHYAl2McMdvoqtLB2i\nleQlJiis0fTtYTJayee9BAIdIrCor1Lc0vozXCpDtq5nTwhjIocaZtcuFsdrkl+n\nbfYxl5m7vjTkTMS6j8ffjmFzbNPDlJuV3Vy7AzapPVJrMl6UHPXCHMYMzl0KxR/4\n7S5XGgmLYkYt8bNCHA3fg07y+Gtvgu+SNhMPwWKIgwhYw+9vErOnavRhOimYo4M2\nAwNpNK0OKLI7Im5V094jFp4Ty+mlmfQH00k8nkSUEN+1TGGkhv16c2hukbx9iCfb\nmk7im2hGKjQA8eH64VPYoS2qdKbPbd3xDDHN2croYKpy2U2oQTVBSf9hC3o6fKo3\nzp0U3dNiw7ZgWKS9UwP31Q0gwgB1orZgLuF+LIppHYwxcTG/AovNWa4sTPukMiX2\nL+p7uIHExTZJJU4YoDacQh/mfbPIz3261He4YFmQ35sfw3eKHQSOLyiVfev/n0l/\nr308PijEd+d+Hz5RmqIzS8jYXZIeJxym4mEjE1fKpeP56Ea52LlIJ8ZqsJ3xzHWu\n3WkAVz4hMqrX6BPMGW2IxOuEUQyIaCBg1lI6QLiPMHvo2/J7gu4YfqRcH6i27W3H\nyzamEQIDAQABo4H1MIHyME4GA1UdIARHMEUwQwYFYEwBAQAwOjA4BggrBgEFBQcC\nARYsaHR0cDovL2FjcmFpei5pY3BicmFzaWwuZ292LmJyL0RQQ2FjcmFpei5wZGYw\nPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2FjcmFpei5pY3BicmFzaWwuZ292LmJy\nL0xDUmFjcmFpenY1LmNybDAfBgNVHSMEGDAWgBRpqL512cTvbOcTReRhbuVo+LZA\nXjAdBgNVHQ4EFgQUaai+ddnE72znE0XkYW7laPi2QF4wDwYDVR0TAQH/BAUwAwEB\n/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIBABRt2/JiWapef7o/\nplhR4PxymlMIp/JeZ5F0BZ1XafmYpl5g6pRokFrIRMFXLyEhlgo51I05InyCc9Td\n6UXjlsOASTc/LRavyjB/8NcQjlRYDh6xf7OdP05mFcT/0+6bYRtNgsnUbr10pfsK\n/UzyUvQWbumGS57hCZrAZOyd9MzukiF/azAa6JfoZk2nDkEudKOY8tRyTpMmDzN5\nfufPSC3v7tSJUqTqo5z7roN/FmckRzGAYyz5XulbOc5/UsAT/tk+KP/clbbqd/hh\nevmmdJclLr9qWZZcOgzuFU2YsgProtVu0fFNXGr6KK9fu44pOHajmMsTXK3X7r/P\nwh19kFRow5F3RQMUZC6Re0YLfXh+ypnUSCzA+uL4JPtHIGyvkbWiulkustpOKUSV\nwBPzvA2sQUOvqdbAR7C8jcHYFJMuK2HZFji7pxcWWab/NKsFcJ3sluDjmhizpQax\nbYTfAVXu3q8yd0su/BHHhBpteyHvYyyz0Eb9LUysR2cMtWvfPU6vnoPgYvOGO1Cz\niyGEsgKULkCH4o2Vgl1gQuKWO4V68rFW8a/jvq28sbY+y/Ao0I5ohpnBcQOAawiF\nbz6yJtObajYMuztDDP8oY656EuuJXBJhuKAJPI/7WDtgfV8ffOh/iQGQATVMtgDN\n0gv8bn5NdUX8UMNX1sHhU3H1UpoW\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9\nMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH\nbG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x\nCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds\nb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr\nb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9\nkmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm\nVHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R\nVogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc\nC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj\ntm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY\nD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv\nj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl\nNaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6\niIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP\nO6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV\nZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj\nL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5\n1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl\n1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU\nb3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV\nPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj\ny88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb\nEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg\nDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI\n+Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy\nYiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX\nUB+K+wb1whnw0A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFPzCCAyegAwIBAgICPs8wDQYJKoZIhvcNAQEMBQAwSDELMAkGA1UEBhMCR1Ix\nHjAcBgNVBAoTFUFUSEVOUyBTVE9DSyBFWENIQU5HRTEZMBcGA1UEAxMQQVRIRVgg\nUm9vdCBDQSBHMjAeFw0xNjAzMTUxMTE0MzJaFw0zNjAzMTQyMjAwMDBaMEgxCzAJ\nBgNVBAYTAkdSMR4wHAYDVQQKExVBVEhFTlMgU1RPQ0sgRVhDSEFOR0UxGTAXBgNV\nBAMTEEFUSEVYIFJvb3QgQ0EgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCv8F+SyvwcsJAt1CaLvyqZeTbHdIwB76G9cvg0hMwtTdfrk5HLCYO2+tRl\nM12cmBtew+bgQENlZ2OlcKvlqxZgtqsUezqjbvUZbyrEdKBZdJT2ntf8Mn8M+a8U\nUbiPWrjVdg6n/XEKPgv8EFJL78LEH1Kh8eXpsRAyrKluW68rt4DJUStKA+w//fBT\nLO++WqbEAfCcBO3g+n1GvxE36w+BrDoZhwed+F5YqP9jvHB1puCrMdGzgoY2aaOx\natU2RdWf8IWKCkUOC0GxEZqx7MAmbUuIN1/sFIOF570+ZZ1K0geHbYaDWLplGcww\nldusUvq2zH5uHbmwgFV5U1wNCFZTUrfkl4NjarnSH7xqIREiVhzoPRmEzlmGKtEG\nJxLbRyukp7DD+B68/qw/sp7csCLFT3Bh0/4o4RUZLHg8P8N9mWA2eW5byThmoaXp\nLYHGUqyezxteyybZ7dQF7VcmdqQC4zbkTkV+NGcY//wUKPX2vANOvIjLegkorQHj\ncOi5O1WNEMiUJAduG5pyxAsY+21rZXlv6L2MFaDkoBUU6TvJXfph4nnDCzNKBQ9B\nUQm8YoB3V+C0uxiSBe2OVCHd9YcYHGqosgJqQoxD1R4fZ+HV3QBjj+ALf0GUYQaW\nfACPoN9TGUe8VDLZGwu+jp89TNygUzyV2FHZp7idkbyDyPHkgQIDAQABozMwMTAP\nBgNVHRMBAf8EBTADAQH/MBEGA1UdDgQKBAhHo6YEnS2W5TALBgNVHQ8EBAMCAQYw\nDQYJKoZIhvcNAQEMBQADggIBAIbX9Rko9qewUKpuPSM+Bu/nNHusyYUusKmiwn0k\nRT+tyNaTJ7XKjyygBDiD2ZrP7lcs7LEJE7LOfCQbZ+BEgszipWRLSzVsZ0Jvc7w4\nuX7ARMh1/AVxp/udBcLlJdkssXVntDH3uiUMjp3JfGxK/HUFYKTNz7ufjl+dsiBA\nS2tuHacQHu+/YA/LN/1MI/pi431dgM2ubMfmp6STGHcfU9Z9qf914yTgT8uiYedm\nPtS0Ch0MFY46hQbG72xy/dRD0/2MqEOBWTjBhnwgh46oJIpGxAWtbaDVWBBTmZTy\nrIosVqZSSkw3OVW8wviueay5NoVuYVI+/TTqYWhlgYFM2xT5YI0EdQ8Q30PTJcdA\nX5vk0DB92gZB9O1m/jgRcyBZ2YB7FeFC1zqebGVfMXahE2XaJzuwEuisSLaZEQd+\nLspikapRYfRnyit50o8hWl8WcI5UmJ/281kBba61pBJzn4KfF5/a7YOPI/1izjbe\nA8HRMKbTou+rXXV699ccLPfZ6WY6l5QpUNv8AgNf8jDXUTKcxC+dStkx8TUPfoOq\nHeK1xlFBa1ctIhmPO6cjuwN1nrv8+SCHzHBfjiBwLzo+Yg1f0uE2nUbWVbKYCi6c\nwFXS+x56a0p2KSYS9q+kp7ztMqFw0/mNiweBpX0GwI3xNb62YLJvHiOikcr5YI3m\n6JPv\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIG4DCCBMigAwIBAgIINJotoYIGsrMwDQYJKoZIhvcNAQELBQAwggEMMQswCQYD\nVQQGEwJFUzEPMA0GA1UECAwGTUFEUklEMQ8wDQYDVQQHDAZNQURSSUQxOjA4BgNV\nBAsMMXNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2Fk\nZHJlc3MxKTAnBgNVBAsMIENIQU1CRVJTIE9GIENPTU1FUkNFIFJPT1QgLSAyMDE2\nMRIwEAYDVQQFEwlBODI3NDMyODcxGDAWBgNVBGEMD1ZBVEVTLUE4Mjc0MzI4NzEb\nMBkGA1UECgwSQUMgQ0FNRVJGSVJNQSBTLkEuMSkwJwYDVQQDDCBDSEFNQkVSUyBP\nRiBDT01NRVJDRSBST09UIC0gMjAxNjAeFw0xNjA0MTQwNzM1NDhaFw00MDA0MDgw\nNzM1NDhaMIIBDDELMAkGA1UEBhMCRVMxDzANBgNVBAgMBk1BRFJJRDEPMA0GA1UE\nBwwGTUFEUklEMTowOAYDVQQLDDFzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5j\nYW1lcmZpcm1hLmNvbS9hZGRyZXNzMSkwJwYDVQQLDCBDSEFNQkVSUyBPRiBDT01N\nRVJDRSBST09UIC0gMjAxNjESMBAGA1UEBRMJQTgyNzQzMjg3MRgwFgYDVQRhDA9W\nQVRFUy1BODI3NDMyODcxGzAZBgNVBAoMEkFDIENBTUVSRklSTUEgUy5BLjEpMCcG\nA1UEAwwgQ0hBTUJFUlMgT0YgQ09NTUVSQ0UgUk9PVCAtIDIwMTYwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQDqxqSh1K2Zlsmf9bxQAPQsz/J46PIsAifW\ng4wEq9MOe1cgydSvZfSH3TAI185Bo3YK24pG5Kb97QjOcD/6EGB5TGuBVIBV5Od6\nIbZ1mtxe9g6Z/PjC30GOL6vHW20cUFnA7eisgkL+ua8vDEFRnL0AbmRRsjvlNquV\nkRL7McdzrBzYZXY7zhtMTrAfIAb7ULT7m6F5jhaV45/rGEuEqzmTzTeD0Ol8CyeP\n7UII6YZGMqyaJmlwYS0YvT9Q8J72aFBOaZVwwe2TqZdOKaK63cKfbkkIK6P6I/Ep\nXrB9MVmb7YzNpm74+PfYGOjaVulI8kB0fp7NIK8UJFnudzWFv0qZSql13bMm4wbO\nfW9LZKN2NBk+FG+FVDjiiy1AtWRmH1czHHDNw7QoWhQjXPy4vbP+OxJf9rmMHciU\nClbbcn7vJwcNALS/fZk/TUWzm/cdGdBPBPrHc5SIfYsUKpng6ZmSCcbWAWu38NtD\nV2Ibx0RS4pdjus/qzmDmCuUYaC0zgHWgMAdo9tX3Eyw6sJ7oWFVujFZETUMXQQLM\nd9xfRQVZz81g07/S9uL01dyHcTMHGvVvtH89l/tfZPRODgBECenr7D5xGQQXOUhg\nuEv/XshlmSumMvJbhqid6CN0EHjvyyedMbpgi04GUOJQHQdgwkGMFbRbNxwK5QkZ\ncgSKPOMB2wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSeLmVP\nPlf1q32WxovfszVtSuieizAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQAD\nggIBAAVpKoWXJlC6QjkckyzST1vRXUQm2m9pK7V7ntD0Si5Ix+x/n8pZerlE9z69\n91BrUZ90/5AaQNCTeZIPiiNei6+BC9CLrWbgKtyaKb012GxAFElCPYkvupsrOLwa\nowu3iNetxhQM7nxJrK7s8j0YT4xtFF0Oqrffd6s7j2JOiwxlxhmOzcAMoXeqtN16\npxMF5jkYx5VkfgO2i5DB5V8AI5jmc9oR0hD/HlMiJ8fTAckvxTsybvDDOMoSZ7y6\nIym7xJVJWgbd1FqQ1BNt59XCfOJYBMDsxL2iPH7GI4F1fKtwXzSElfez1UeWT3HK\neDIIILRCpEJr1SWcsifrwQ5HRAnhKw/QIzZuHLm6TqzM8AyUzkEPa90P1cjgF4ve\nOl1Svul1JR26BQfaVhk8jdHX8VE22ZLvonhRBVi9UswKXm+v2tDlDNtswSPvOTF3\nFwcAjPa6D3D5vL7h5H3hzER6pCHsRz+o1hWl7AGpyHDomGcdvVlUfqFXFTUHxXLJ\nPrcpho2f2jJ5MtzbqOUJ/+9WKv6TsY4qE+2toitrLwTezS+SktY+YLV4AZUHCKls\n4xza++WbI1YgW+nQXMZKJDu847YiFiqEkv+o/pe/o53bYV7uGSos1+sNdlY4dX5J\nAJNXyfwjWvz08d8qnbCMafQQo1WdcDwi/wfWK7aZwJfQ9Cqg\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIG2DCCBMCgAwIBAgIILdIuUDCmXhMwDQYJKoZIhvcNAQELBQAwggEIMQswCQYD\nVQQGEwJFUzEPMA0GA1UECAwGTUFEUklEMQ8wDQYDVQQHDAZNQURSSUQxOjA4BgNV\nBAsMMXNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2Fk\nZHJlc3MxJzAlBgNVBAsMHkdMT0JBTCBDSEFNQkVSU0lHTiBST09UIC0gMjAxNjES\nMBAGA1UEBRMJQTgyNzQzMjg3MRgwFgYDVQRhDA9WQVRFUy1BODI3NDMyODcxGzAZ\nBgNVBAoMEkFDIENBTUVSRklSTUEgUy5BLjEnMCUGA1UEAwweR0xPQkFMIENIQU1C\nRVJTSUdOIFJPT1QgLSAyMDE2MB4XDTE2MDQxNDA3NTAwNloXDTQwMDQwODA3NTAw\nNlowggEIMQswCQYDVQQGEwJFUzEPMA0GA1UECAwGTUFEUklEMQ8wDQYDVQQHDAZN\nQURSSUQxOjA4BgNVBAsMMXNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVy\nZmlybWEuY29tL2FkZHJlc3MxJzAlBgNVBAsMHkdMT0JBTCBDSEFNQkVSU0lHTiBS\nT09UIC0gMjAxNjESMBAGA1UEBRMJQTgyNzQzMjg3MRgwFgYDVQRhDA9WQVRFUy1B\nODI3NDMyODcxGzAZBgNVBAoMEkFDIENBTUVSRklSTUEgUy5BLjEnMCUGA1UEAwwe\nR0xPQkFMIENIQU1CRVJTSUdOIFJPT1QgLSAyMDE2MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA0GvnniIrU3YVVa9MSsBta/v5hEQFoX1gzgXsnphz+luE\nBzH3/z1rx35WBmKlXJaW0/FeWX7rMRy/d1cwVO8exczEsurb5orQ9CiEyLBILSyW\nbfsiqDWOvt5wFRD5ZkFGFqBDZD+NSvOAMc+TgH6a26Wvj2ws/Q7vHHncD6JuhFwi\niQ5ELkiolHPsOTKRHOIUvX1l5nL+W+dUdS99DuLGymkuXqIO1eiF3j9rf6WCsEZ9\nXZ5xuhS06+3HwhRkDFhuT5U2YTZFYDZmGEuVGj5YrIsmHiXm+pUA+60SnvoSYb4a\n3qZ86av/15SJckL8u0UR7D9w/BnEmuqXbqzkOAQ74T8BKHGj4q5DZHgWmQJav9fE\n77W31cNYgUGG5LKMAKWImJjrCedYMWgx3u3iSTXz0rNX3MRCn/0879D1KzluYa56\n4cd6PW0XMGwCrInWWoScKcCeEI64IDYzyoAraH82dWUV+MPa/3Gi/O2bd9wZ+vHI\ntgX05XCSqcjduLAaVVuR3LjlmrUDwK22rvGZe0u1iQ7eZAtkflTup8OKmBnF/DwT\nCEU+35/7x32xoII2FD3AYwABZsTk8Jk7HlF4XbkXPFiTFa+o9SUgGY0jPRI8Qusv\nXUKO8jCoJVrm+vdPbb4mWPWPf/eK+LNuwxvyMYU2cY79O9bmMDXLJY1liVeoM5UC\nAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU6JvNfoZim3pNjACX\nOYXPHHiQcDowDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQB7/SrD\nyxspAhAJresusytt2Uug2zWY9Y8Cp9NCC0Org7V3R4hGhd3Rth59mBuMcb6qyPDv\nxmotVphS6JaJ+9XqAN/+5iLKp7k+ZUR1w4q/i3eJw2pX+rzI4RDe8dqFJ/HtB//V\nwkLUomEv34hx4zTmZ2SbxnoZ6znv8+oEqHRpTIC1/K29DQj0yO8oJ4LK3ejzuldn\nouopwZnhdmb59nhdnD7w9s+hGTTT8TwzocyCMrZI44M+D79nlcGimXhCQ/cDTRNX\nb91x3Rbz+3k4G2KapM1eUN4RIJCKIpir2kZ6TDTRSN3ZZmViVAXZdJlndFexOi4Y\nsK6snz8u6x+ynM2O+Nt4jtQGz6OTMWt/7VJyt4vPKG/J+VRPAdQ6hugu+uHQJYTj\nFvyMjSTjZMwqjLJgU59ZkkUJlFuoEIUyy3fyjpWKRHLPbhfeRL0Krv0mtj15Zj1N\nvH4yQ13b4GW1KGm6fJ4ySo/qerA9Fl39PvobBPgQNXjM7cHZLb9r0u/pn8Bbj+q+\netEx5wY9rYSr7DvxEsd/8fhGLwl4l8AnPbE/cSOLGqdc5hYlDiZNuQ5Wp1KkOAmv\nSQX+f84/wvzm5EqUJ+VTxIg06wJXvM6OK613U3JAu4UWVRkvg3aVo3Y5qLL0faTb\nAEJ6oHuOGQbkl81bPTq0XMBpHzJmvwifhJsiZQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEijCCAvKgAwIBAgINAJCud3YAAAAAVx3QbzANBgkqhkiG9w0BAQsFADBcMQsw\nCQYDVQQGEwJTSTEcMBoGA1UEChMTUmVwdWJsaWthIFNsb3ZlbmlqYTEXMBUGA1UE\nYRMOVkFUU0ktMTc2NTk5NTcxFjAUBgNVBAMTDVNJLVRSVVNUIFJvb3QwHhcNMTYw\nNDI1MDczODE3WhcNMzcxMjI1MDgwODE3WjBcMQswCQYDVQQGEwJTSTEcMBoGA1UE\nChMTUmVwdWJsaWthIFNsb3ZlbmlqYTEXMBUGA1UEYRMOVkFUU0ktMTc2NTk5NTcx\nFjAUBgNVBAMTDVNJLVRSVVNUIFJvb3QwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAw\nggGKAoIBgQDTy5wtwuAwQ2UxJP9LsDjZqVPXNdHbt0uTtHKN8cuV0lMrdJsymqQv\nPgIG3a9wFaGqzxGHimZ7y8wdcERcj6zK5sNbJ7SNo44Qv25UdAhwiiPoysd0xGaR\nIN1L6KWEdaWYlYKLG+EgJAdGqwxlNkBni3XuqdmRKRvtby1FwtbiYAGx8045Kztv\nP4W+CPZTK3uiyUWhRIGAZppgOhvEvgzMMBB/ETY4SuaboZZTnJTMEcYETKJVS/+A\n4a+MHDX8uZM33/ldPdzrDSdsRMlZZitWb/8EG/f1acNdwxj+vafZZC+in2DZcmw9\nPHXyJSeYLjq4yd1Ndb2rsCJhWAE3KKYgnS5gXPuQvEZDuP5t2MBmIiRrNHgi5bni\nWOlIOO5MvQF7bj5A6tHCCkKTZ8MmLz8HW8+v4x3oOuJl4YSRP/VmAP2qM0ZC7BY+\n0hNlLw4JU/bkKnUUnBkzFppF4dtXz8841Kf37VhD5A6YXMTgMT+UpG9LSqLVSo0m\nqR1kJQg1DecCAwEAAaNLMEkwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAQYwEwYDVR0jBAwwCoAITKPDaF4IAmMwEQYDVR0OBAoECEyjw2heCAJjMA0GCSqG\nSIb3DQEBCwUAA4IBgQAmI4W7XUEZbpKDiu4BiwQ1GX+rj9zWM8H5eZZeI/Xwzt3q\n22E7Wq/dWOlCiUDv+dlnEX9N8e3pEXuxQQ/tpNIWtu/B/Yv2ESss7/wHBkYMzwIL\n7Tvejwm5M6smgFREQmXX56/NUA7KyIihEpwqlTs+VDxIc/Z8eNSb/5P3ReQphGP8\n+n4a51zgclewL3gdMMYT/YhfsWWI2l6XE4F7/h7Pe79XMMFwkkOmmfBVn5jFI0K9\ndBwxjhKl2UVqKlrIWM291t0+NQsZfwMczgcPh0WTFaFrvTQc4N711LjlkRxLBbUn\nJrzP0QmYFsbh8VVLOntt3sZntsE3LZ+ojlnHt6bF798W4u3esrfzojakKDI6CpTL\nP17+blntujayk9bGwxn+9Zl460dH5a1Ceuy8e8kuQU5NDwQOikszh9zxdnxaGIyc\nChLXorPChYeubTFQYjIhoGgWX5Q1dFUp0nGBCErh112qVAGzG3xZrr6sDMq4QGRn\nW53qBgYR1tAwcx7jvCs=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF7DCCA9SgAwIBAgIIAlQaqVDXzh8wDQYJKoZIhvcNAQELBQAwYTELMAkGA1UE\nBhMCQkUxJDAiBgNVBAoMG1pFVEVTIFNBIChWQVRCRS0wNDA4NDI1NjI2KTEMMAoG\nA1UEBRMDMDAxMR4wHAYDVQQDDBVaRVRFUyBUU1AgUk9PVCBDQSAwMDEwHhcNMTYw\nNTIwMTMyMzM4WhcNMzYwNTIwMTMyMzM4WjBhMQswCQYDVQQGEwJCRTEkMCIGA1UE\nCgwbWkVURVMgU0EgKFZBVEJFLTA0MDg0MjU2MjYpMQwwCgYDVQQFEwMwMDExHjAc\nBgNVBAMMFVpFVEVTIFRTUCBST09UIENBIDAwMTCCAiIwDQYJKoZIhvcNAQEBBQAD\nggIPADCCAgoCggIBAKv5lg6EKHY1gSpWPwLt1fFwkQ5AlyJcu5bmmh4OPCuZPC9r\nNGGrB8xKJhVlngsozAA4D1v2rEZMxVwiiI4j1lYoXnXixE9S4zkEczk55k/386my\nIOoMJ9LH9HRzO+wkzmFsGpXb3FVCsRaUMfmmfIwU+DiifaC1OZzX1l+VL4VzUb+s\nqYgcHMkybDgAw6KwK9aPsobKujk4bGeDykeHV4udVqR/dk1IFRazwJeKwgz6ZLAg\nQ1aMaofDLSEXPl7gCKoat6qEPVYjK4Mx49MC2RIDBcI5r29TVhcDqyMcevC8CheV\nlyaB73ggPebf9Nq+jl9f0R79mXz3IW1ctwSWYsPTbh3K9++mRZNT3yZ75NRE121/\nsFSZfrYn4sO+SmdCBa5qSvLulwZdZ56Bvl/oAFpUSrZM2RUuCPZCGiUZPiuBe1rc\nGfRqJwLdj5QCl+zilge0VubkLu/dLBaFCPoc9wCWfg7koPopgJC2RFN9O3UV71lG\n4crc2JcbkElDly5YBXK0XTEGfTnhdP8aTE2VMuiNpa/0PHv/IBzL8LD3MvPmEsWh\n1+SSGelJZ8A8f5u4gt4E8RVX1rAJHjk6a6bi+KafIXCZqLBZeRK6SEbm9XLMzNQP\ns7dMw6PfLpd4yF97KyEitT6yHNlrQ1GL2yBJjtpqEzQLO071a46HG07GSgArAgMB\nAAGjgacwgaQwHQYDVR0OBBYEFDi8XDBU3OK7IO/ub0GgMW5c/Yt1MA8GA1UdEwEB\n/wQFMAMBAf8wHwYDVR0jBBgwFoAUOLxcMFTc4rsg7+5vQaAxblz9i3UwQQYDVR0g\nBDowODA2BgRVHSAAMC4wLAYIKwYBBQUHAgEWIGh0dHBzOi8vcmVwb3NpdG9yeS50\nc3AuemV0ZXMuY29tMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEA\nnDEKHd7KpKBSsJYq4Pws5aF23BQ4ZYazLtWll/NzYK5GaHWHsTPIEo3ZKaPqH71u\n/ronUIHhcWzOqzCcJppRcXBnH9FEpxQ0zUbdK+MOZb3GTkNoU7K4sT3wZD0Hh7H5\nhzIEepbkQrswKMeaXStrx1AKIbaGIvYSrS4V8LtTqTDKLesCoZRnYxHYt+bzpwsG\nH5J5ofKrU3s/o0gITPtEAAP/yQDCbMJKxYbEs+pZXA595T+2qU+S4xEEXbd3xjXD\nsjFz2nfXP38QGa0AIt1DyOASfkSYOFHSOMi2QxpMUV2cOovIPHm43LAe693l5p5E\nm+lQPcsRvFX+x3RlZQgNpKp3PRwTtpyfFSr5TuE0gnA2c9I0GYRV8w3AT43/Vhaa\nW2US8DJBnBtYv72vMhB21y0PxTdx5hr9Mea0Nhhs+0v1qjWwbFAt51siSuD6nTkg\nQcYuACXkkd+bONMFm5z9BGiRuA6CXNg192LcyWAFi5XMP3zrj8b9mp+pbzIBVJpk\npN3lxUVe6lXt4UPLreIebgqejjLk4668AdBTBA6dQk02+5nlGukH1FPwRQdCE8dr\nIT6Et/fFiVdTH/jzTlFb/mcyw1n2kRmIDYBs4d5FCkaZej/MPvAgbPi8z653LPtu\n9QsRdouZzq6OM5F4CqUMJLNTD2sR6bOwHWQBLpQdIdU=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGbzCCBFegAwIBAgIQQxwoxnQP7SVXRJ/y/Q5eFDANBgkqhkiG9w0BAQsFADB7\nMQswCQYDVQQGEwJDTzFHMEUGA1UECgw+U29jaWVkYWQgQ2FtZXJhbCBkZSBDZXJ0\naWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJhIFMuQS4xIzAhBgNVBAMM\nGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMB4XDTE2MDUyNDE4Mzk0NloXDTMx\nMDUyNDE4Mzk0NlowezELMAkGA1UEBhMCQ08xRzBFBgNVBAoMPlNvY2llZGFkIENh\nbWVyYWwgZGUgQ2VydGlmaWNhY2nDs24gRGlnaXRhbCAtIENlcnRpY8OhbWFyYSBT\nLkEuMSMwIQYDVQQDDBpBQyBSYcOteiBDZXJ0aWPDoW1hcmEgUy5BLjCCAiIwDQYJ\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKtriaNTzEgjCPvDz1GWCC64CHptPJAX\nhqnp7S4TNEey0HDcyTzQjcruSxer0IWwpyMEy6ii/OV120DKYomPUJ4BPSZbGIQc\ny3w3t33s039zGbBqstiIii1FdKj3s7jA1NrNIol0TVoVOXMYdE+165mnwR6ItMKT\nkGOX86enErIJIgcz2ZHNDpwfDiDH7rszjY/C0linX/1lN+KIwtiPhnVe+S2nhzPy\neDcvi7wdhjc5sZTy2LxKnIMYWgb889TUuowVCSXw+baNBH4XEjNrV0hMT9smHuvM\nkOeL+Wh8cA+jKtA6ON83l+Jb3oBh04DYkYNCWkwEiWgRPKxfaIBBzGBCzg1aKgwP\nmzDApvCG26tJ15dtSIv5A8BSZ5sS98LyLphlQtnWmuPQGTEMrYfVVwJ6MOiGJvuP\nI4pUh+S/PO7rw3VIXx45b4FibMUtxBdUGbc3jZw3kcj2C9XqY2+DrDjC8z/emvvh\nI2HwyCbLNsih8zCPpKOiod1Ts97wmjIfg5F5MMGpH1ObU6IVUz/dnbMQO0h9iQ/8\n7QP1+yVkdQ4XGQ2PABZneXpA/C1ZB9mQ+pqtPdyAiuZcNaJnTBFrsfiAZAAtbyJh\nxaxLJuVaEIKbpIN7NPeeiZEgl463Qsdmw9DppNb1II3Ew5WsRAqdW3M8Jj0vSr6n\nyacQHvufUGnzAgMBAAGjge4wgeswHQYDVR0OBBYEFNEJ0OnXznl0VPk6MLP0bSwD\nAxtoMIGoBgNVHSAEgaAwgZ0wgZoGBFUdIAAwgZEwMwYIKwYBBQUHAgEWJ2h0dHBz\nOi8vd2ViLmNlcnRpY2FtYXJhLmNvbS9tYXJjby1sZWdhbDBaBggrBgEFBQcCAjBO\nGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW507WFzIGRlIGVzdGUgY2VydGlmaWNhZG8g\nc2UgcHVlZGVuIGVuY29udHJhciBlbiBsYSBEUEMuMA8GA1UdEwEB/wQFMAMBAf8w\nDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQBRG5A+g1oa6Gpwpa1w\n/hCCYTCtjMO1xtjTMRLJH3lTFeJDx+EO8T1IzmgLFXlt4XaHf3Bm7fUPgqS8ce+x\nK8yxuusqqWAfQ4C+knJCLp+h/xgIsV5d9WzOKNvAbJrNh16W6cjvNZe6ZKq3fkVA\nibBDg0574bT0dglLzFY+IUmyxp9j293wtg8X9bpMcI3VJwDJQ1QPZqq6rrHZdu4D\nke2YtxobopZQblV/zV4Y0Gcbv/T6ctm72vvemqpRLgW6ztpqbRhoJmiChTTtTXna\nmnYN9PHUw/uxKnTskFLjDV31SVhUJwAwl6AjAWyJvx0A8f38GayfOymow4HNknH4\n1+Wx2hs6F49T2qauAc6ynhrNCWLPddTXZ1Cin1n2hPPHJzGeqh4mS7oOiqzp9eNc\nHaEqNzm7NG4zltVxpUM+NjSH/5Kiq+kl4NlRd1Sqe0E0hljxquU+kt7INBCThD8m\nRb1Sxjx29yEcruDhxaNT8gmffROeqfOyWYIUlE7fdqqD6SjaiohU+xRxqlA7viT9\nxD5E+Jhk82qPYnWwrEdl9psiOiHhtVdBVsUk1hmSd3CwrBf0LpUQThIwmahURWEv\nN2/6iVdGMvRb6ZvtCSkvla6U4oeqHmpx6W8bOe38fNQNpk4jIjb5Zc9C8ByxM500\n1YkkaeYXaKOZ73pcL/0gvXeZYA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFyDCCA7CgAwIBAgIQR0ORJD/Oww1XSChr7oBdqzANBgkqhkiG9w0BAQsFADBd\nMQswCQYDVQQGEwJFUzESMBAGA1UEBRMJUTI4NjMwMDZJMScwJQYDVQQKDB5DT05T\nRUpPIEdFTkVSQUwgREUgTEEgQUJPR0FDSUExETAPBgNVBAMMCEFDQSBST09UMB4X\nDTE2MDUyNzEwNTg1MVoXDTQxMDUyNzEwNTg1MVowXTELMAkGA1UEBhMCRVMxEjAQ\nBgNVBAUTCVEyODYzMDA2STEnMCUGA1UECgweQ09OU0VKTyBHRU5FUkFMIERFIExB\nIEFCT0dBQ0lBMREwDwYDVQQDDAhBQ0EgUk9PVDCCAiIwDQYJKoZIhvcNAQEBBQAD\nggIPADCCAgoCggIBALkrXvU+uokenfXRE8+7o1666d85cmSYUodppbbe4b+URb7F\n+KRTZxVQ3FJPKnYsLo0gaozmXbnZaL6RG7ppAxitGE42oqxGqyD4A2qqrXnV3x3B\n7kVvIXT5TbGxPZA7PDKA7f8Vz1HK16SHLqrlDrbRelrHufhRu9mU3T7Ghk4K/juJ\n8vhuJM6RA1gFEkrdUKtBes7tqR8RUx6lE9th8PWqgN50eR2k4ynW++D8l9qiuKsi\nPmWwIcTlxRBEh7Lj4CqCLn3m9LikEyXzd2BfY1OuLrGdimt2ezpxvZKBNrCcgvH3\nxYkoXf+8QgazCGpPYc2kLZDTObh3/8jHo3m7A7mRAwE0Etgwi7aMAsrkSOw4KjJM\nbcp2KFqGCrrUII6voF8gLWKciPnxFW1bvbEDUMA/NteuP1HRyuNYZkTmo5t3LjH6\n2X8ixAVM63QbXGN6pgKTfkMOdhQPTW8ylYiAklKXFPU8/JQH02wpBZVGD+Rx4X/4\nbRQSgpK181M+mRGXR3ZKCXLu1MOWCaza//FLS7bXJc8eTJcmCzS7tpTxLGRxX4ny\nFTs3pwLkDU9IiTOjjGh4MVFnChnbtOJ0Lz1683cAn3ESY/9zKmRpVOysOq7a8lhj\nNH74PF7AQjql27Oo1FrBTli4abasgmLb0fsaQyEi/B31nE9OO+WN/3ZaI15bAgMB\nAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O\nBBYEFBpV5BUx4jGbEdSIcXoAPXAoBb/NMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkG\nCCsGAQUFBwIBFh1odHRwOi8vd3d3LmFjYWJvZ2FjaWEub3JnL2RvYzANBgkqhkiG\n9w0BAQsFAAOCAgEAezDKVYbTr+4a17iVmOz5O92QE6OckkWgkolpoXGRvHGFh6At\nMAnkwlM99Km3aC1Nmc2kz547kJ2aCikNKkLBPVtrQILFixOxQWePvqR34MB25PO2\nKVYs73FPwmTx2rQLytA5X1OygwH7sn3Zg3R6NdDBXY+b917nUt/uqjeTq9k9fR7x\nvRzb6HXduFtM4xaj9nWIDo88wwts22BZ5AWrKEb3Zmkld97KSjPYWF57j5rPUo49\nbf3Rsr0+eVeGHkQcB030whCqeMvzURcNdj2NbmhJ6e8HSdG4Fsl5ncyuCwVHev2Y\nrDGhkFqHYvn4q2Ja4CF20GhC6By+coHwxmd9fnQ81VVvj6VolhHxytMwF71GtjGv\ncOmkhDdXugk8LtkLE1YHPpXEtXAvk8Kur4FdRhQw+67F85r3QXqx3ksW2UV1RwJ8\nFB7VsTugLEG1m0t7o4PwuczOHpS3Xi4jBpWRHDhHHO3EeA6kD/wbfNbya9CKW+qW\n8zHUXmrElLgwn5XhB4m4iNInhaRhdOWoRDF6IHXo+Njrs0+q/1M/lu3qu/xRQKYr\n7CSh+/lEjSPnppcAD8ukar9QoMpxomyub9/Zg4Jm3FNdr/pU94P/qz+Jlae0bfMP\nCg1IMy+BKcdLBcTGV3SEw5g2/++FMqtinBPRIoexvpjbdJqP6sLWk3lFIMM=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDgDCCAmigAwIBAgIDDN+bMA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNVBAYTAlNJ\nMRQwEgYDVQQKEwtIYWxjb20gZC5kLjEXMBUGA1UEYRMOVkFUU0ktNDMzNTMxMjYx\nKjAoBgNVBAMTIUhhbGNvbSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0x\nNjA2MTAwNzA3NTBaFw0zNjA2MTAwNzA3NTBaMGgxCzAJBgNVBAYTAlNJMRQwEgYD\nVQQKEwtIYWxjb20gZC5kLjEXMBUGA1UEYRMOVkFUU0ktNDMzNTMxMjYxKjAoBgNV\nBAMTIUhhbGNvbSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAOlSpsYa72O7rYH0kLJajw3VFjO0HBj7y4kq\nMLtlgcTh+wKplAd25dcV5HpkEIDqPNCzoq2uHB/qu4FhmNT5jWmVxEUuAwnKhvpc\nWhEXQDA+8MZjCcnxjUGlVg0FZGlLWKwqKZa7QDMWNEtnbNfxtEal6lmoQ2gPjDgq\nqjz2RAOG+IrbRSErKR4St/qlZUHeBghYcJU+9EzZ6w8pqZGKnq3KEvXlleY42Rqm\ni5xPpkgTEKV5RL1qOyn1FndAy36bXN++i+vnoBlvnxU/J54psfUN/F9HojzdLgsC\n+/SN6uwMsfm0Baz5j6k9biwdOZ/QTp9OyGqegANh3M/4bZTLD88CAwEAAaMzMDEw\nDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIQq6mQ8eYKLAwCwYDVR0PBAQDAgEG\nMA0GCSqGSIb3DQEBCwUAA4IBAQBSuXnQ22P+GYH7DPnB5VBZyp2y+1wz0Dioq7Ua\nTlMldSLTSb/Kgc/T4XujkUZ1yhrr2fVdvHuGNf2Bl5yE1yaYIvyxNdCplbZ8/+SX\ntEB+SV1oyOLUOXUnTwORsjFXv4bXbcpxACI30DtYJFCgnIyaiY71KEZs5xbtsIGr\n9EYmr6boGkV3cBaSsntxcdz330lnwDMIDi5TwXerx0qRTBLv5w4J5XUxIK5u/FqK\ngJwQsNuoSszzK9w2NKb3qQtnnZDLPSafdc1MyR0GCnWLUsCB8NEmrMySphScXDwW\nQvuTzAKoE/PargrDuBX0sNDU4BYgT6xQmHgmlB5o65Ry/veL\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT\nAkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD\nVQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx\nNjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT\nHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5\nIENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi\nAASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl\ndB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK\nULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E\nBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu\n9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O\nbe0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFfzCCA2egAwIBAgIJAOF8N0D9G/5nMA0GCSqGSIb3DQEBDAUAMF0xCzAJBgNV\nBAYTAkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMScw\nJQYDVQQDEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTMwHhcNMTYwNjE2\nMDYxNzE2WhcNMzgwMTE4MDYxNzE2WjBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc\nU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UEAxMeU2VjdXJpdHkg\nQ29tbXVuaWNhdGlvbiBSb290Q0EzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC\nCgKCAgEA48lySfcw3gl8qUCBWNO0Ot26YQ+TUG5pPDXC7ltzkBtnTCHsXzW7OT4r\nCmDvu20rhvtxosis5FaU+cmvsXLUIKx00rgVrVH+hXShuRD+BYD5UpOzQD11EKzA\nlrenfna84xtSGc4RHwsENPXY9Wk8d/Nk9A2qhd7gCVAEF5aEt8iKvE1y/By7z/MG\nTfmfZPd+pmaGNXHIEYBMwXFAWB6+oHP2/D5Q4eAvJj1+XCO1eXDe+uDRpdYMQXF7\n9+qMHIjH7Iv10S9VlkZ8WjtYO/u62C21Jdp6Ts9EriGmnpjKIG58u4iFW/vAEGK7\n8vknR+/RiTlDxN/e4UG/VHMgly1s2vPUB6PmudhvrvyMGS7TZ2crldtYXLVqAvO4\ng160a75BflcJdURQVc1aEWEhCmHCqYj9E7wtiS/NYeCVvsq1e+F7NGcLH7YMx3we\nGVPKp7FKFSBWFHA9K4IsD50VHUeAR/94mQ4xr28+j+2GaR57GIgUssL8gjMunEst\n+3A7caoreyYn8xrC3PsXuKHqy6C0rtOUfnrQq8PsOC0RLoi/1D+tEjtCrI8Cbn3M\n0V9hvqG8OmpI6iZVIhZdXw3/JzOfGAN0iltSIEdrRU0id4xVJ/CvHozJgyJUt5rQ\nT9nO/NkuHJYosQLTA70lUhw0Zk8jq/R3gpYd0VcwCBEF/VfR2ccCAwEAAaNCMEAw\nHQYDVR0OBBYEFGQUfPxYchamCik0FW8qy7z8r6irMA4GA1UdDwEB/wQEAwIBBjAP\nBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQDcAiMI4u8hOscNtybS\nYpOnpSNyByCCYN8Y11StaSWSntkUz5m5UoHPrmyKO1o5yGwBQ8IibQLwYs1OY0PA\nFNr0Y/Dq9HHuTofjcan0yVflLl8cebsjqodEV+m9NU1Bu0soo5iyG9kLFwfl9+qd\n9XbXv8S2gVj/yP9kaWJ5rW4OH3/uHWnlt3Jxs/6lATWUVCvAUm2PVcTJ0rjLyjQI\nUYWg9by0F1jqClx6vWPGOi//lkkZhOpn2ASxYfQAW0q3nHE3GYV5v4GwxxMOdnE+\nOoAGrgYWp421wsTL/0ClXI2lyTrtcoHKXJg80jQDdwj98ClZXSEIx2C/pHF7uNke\ngr4Jr2VvKKu/S7XuPghHJ6APbw+LP6yVGPO5DtxnVW5inkYO0QR4ynKudtml+LLf\niAlhi+8kTtFZP1rUPcmTPCtk9YENFpb3ksP+MW/oKjJ0DvRMmEoYDjBU1cXrvMUV\nnuiZIesnKwkK2/HmcBhWuwzkvvnoEKQTkrgc4NtnHVMDpCKn3F2SEDzq//wbEBrD\n2NCcnWXL0CsnMQMeNuE9dnUM/0Umud1RvCPHX9jYhxBAEg09ODfnRDwYwFMJZI//\n1ZqmfHAuc1Uh6N//g7kdPjIe1qZ9LPFm6Vwdp6POXiUyK+OVrCoHzrQoeIY8Laad\nTdJ0MN1kURXbg4NR16/9M51NZg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH\nMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM\nQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy\nMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl\ncnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM\nf/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX\nmX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7\nzUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P\nfyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc\nvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4\nZor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp\nzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO\nRc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW\nk70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+\nDVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF\nlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW\nCu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1\nd5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z\nXPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR\ngyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3\nd8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv\nJ4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg\nDdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM\n+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy\nF62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9\nSQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws\nE3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQsw\nCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\nMBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\nMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\nY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu\nhXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/l\nxKvRHYqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0\nCMRw3J5QdCHojXohw0+WbhXRIjVhLfoIN+4Zba3bssx9BzT1YBkstTTZbyACMANx\nsbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11xzPKwTdb+mciUqXWi4w==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBH\nMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM\nQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy\nMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl\ncnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv\nCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3Kg\nGjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9Bu\nXvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOd\nre7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXu\nPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1\nmKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K\n8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqj\nx5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsR\nnTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0\nkzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9Ok\ntwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBALZp\n8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT\nvhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiT\nz9D2PGcDFWEJ+YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiA\npJiS4wGWAqoC7o87xdFtCjMwc3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvb\npxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3DaWsYDQvTtN6LwG1BUSw7YhN4ZKJmB\nR64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5rn/WkhLx3+WuXrD5R\nRaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56GtmwfuNmsk\n0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC\n5AwiWVIQ7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiF\nizoHCBy69Y9Vmhh1fuXsgWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLn\nyOd/xCxgXS/Dr55FBcOEArf9LAhST4Ldo/DUhgkC\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQsw\nCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\nMBIGA1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\nMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\nY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout\n736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2A\nDDL24CejQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFuk\nfCPAlaUs3L6JbyO5o91lAFJekazInXJ0glMLfalAvWhgxeG4VDvBNhcl2MG9AjEA\nnjWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOaKaqW04MjyaR7YbPMAuhd\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF4zCCA8ugAwIBAgIEV8fs9DANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJT\nRzEYMBYGA1UEChMPTmV0cnVzdCBQdGUgTHRkMSYwJAYDVQQLEx1OZXRydXN0IENl\ncnRpZmljYXRlIEF1dGhvcml0eTEaMBgGA1UEAxMRTmV0cnVzdCBSb290IENBIDIw\nHhcNMTYwOTAxMDgyNTE3WhcNNDEwOTAxMDg1NTE3WjBrMQswCQYDVQQGEwJTRzEY\nMBYGA1UEChMPTmV0cnVzdCBQdGUgTHRkMSYwJAYDVQQLEx1OZXRydXN0IENlcnRp\nZmljYXRlIEF1dGhvcml0eTEaMBgGA1UEAxMRTmV0cnVzdCBSb290IENBIDIwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDV39ONmRdqmz3gsGnbtAXMvqUg\n+E8NB7MZPJeDPey8uVwMrKIDZKN/DHcT5siHq1IYTzDv6g7dgveVDzCKwBlQvGBl\nodwRxn8W8RuY5CJXUUKMynCWXG4NuY9naloFm98ePzjjqiVGwZwrkn/0grEjPN1s\nZ2ABVPLkqhD9o4p3JyGe1j3dRlwFPxgIFgplyAxNT2Y9XhZfFw8O/8EXC+cid18a\nC3hpp8oGj17F30CzDvjg12g+cUHJn41h60uZ4K8zAHetxBZZZgg2p0rkUixZP3t8\nOEPkC6PT5Yl4U+ZrvPUnMOggNg6xDI4OFMhUNwd6rujTtsBGTMe1MS51/FHyqmz4\nGKsmhWC/ELnDQRNf9HnBCfaRrPeOxY9INakW3R7gX4XzGrM/gVvRfkLu5BtnRGy5\nwen7kHQ/lE6TybTpfUJLHfCnlptIfaKQXLQUcCCpCASL0nyy0glMI2ypMZPWKYFF\nLsPkqqbvvZvxy64Ct2RdgD1BTYlLi5qct4FvX9xoU4aKcXTSVxcyg77V9Hrbmu4N\nCtVjq9QR5cxdbT7Bj/SPTl0SJkTPLX1XekED2c0eOC8Q1JShNXI6Yd7uQ4tIKdJ2\n4S1RLtS+vIDb/02LXw0wraMwpTDr1SRnljz6gW249RiBzMW2QgfzvITmHF6D1Gka\nuELq29THck1NpZm/owIDAQABo4GOMIGLMA8GA1UdEwEB/wQFMAMBAf8wKwYDVR0Q\nBCQwIoAPMjAxNjA5MDEwODI1MTdagQ8yMDQxMDkwMTA4NTUxN1owCwYDVR0PBAQD\nAgEGMB8GA1UdIwQYMBaAFDofR9lvhhjpKfr+Oc7L7YrJVlUrMB0GA1UdDgQWBBQ6\nH0fZb4YY6Sn6/jnOy+2KyVZVKzANBgkqhkiG9w0BAQsFAAOCAgEARbJm3IEyIRyA\nmmkJ9aaUVVkB93asquqINx6sVfVKH26JV6OiBuudmCkasa0EVtruWDtoKm7j+QSP\nKlKbW+wQ/kwors+qFCzeFgJAU/3XXGAZ5UWWkuzjHhDf+RtK1aS/opcp20BBb9qu\n7AmBukLwJDN+wFVssEd2Yo1Y6oG5FpkTBxou/xUqrWW7u9JNjCNVuxYo9SkZnsn8\navw+o+4XAgwTNJkvreeu4kA8dgxKsYQ5Ke3DPbiox5ZA/rK8t3LsoU++Pnf4fY7o\nDqa5IsPkt5FkD/2RjaWoL4POYf1Z3mNpo4YwbsXubM+272ZcXvZ1Uf2YSCM4yb/p\ndQb9cWwhf/zJGceoAMYqXACd+vLkc0i1eIteq+l07Cvjph38Kdbhd1GXikEwzNHM\nk+rJT8V+caOm2Whsbn9Duxa9RbwBQp4O5x/Zn9q+GDfH1COy7jIMy2/owbhGasW4\nBzI5zUq+w757LqLd8qtL2qbOkF49c35RlNLeL8dxFDaRV/VdpMvtxgIxaML7RfVa\nc/p7oT+o+W3NN9/APyjxvZKAuaCZo5JXcuXrsgXOzEYbobD3w4j1CCR1ZIc/K9MB\nZ1KPSTADjsdBUW2EmR4blEU+HkRHxSnM+gZp+Usn3GSkFkFrZuPN+c1+9a8nLZ3P\n7naLqfk3x/LtOfB6wiMDtoXZPJRBvNM=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEIjCCAwqgAwIBAgIUKeuSM0ZPMkH/gxkAqa3E2fjj4n8wDQYJKoZIhvcNAQEL\nBQAwcTELMAkGA1UEBhMCQVUxDDAKBgNVBAoTA0dPVjEMMAoGA1UECxMDRG9EMQww\nCgYDVQQLEwNQS0kxDDAKBgNVBAsTA0NBczEqMCgGA1UEAxMhQXVzdHJhbGlhbiBE\nZWZlbmNlIFB1YmxpYyBSb290IENBMB4XDTE2MTEyODIyMjUyOFoXDTM2MTEyODIy\nMTM0OFowcTELMAkGA1UEBhMCQVUxDDAKBgNVBAoTA0dPVjEMMAoGA1UECxMDRG9E\nMQwwCgYDVQQLEwNQS0kxDDAKBgNVBAsTA0NBczEqMCgGA1UEAxMhQXVzdHJhbGlh\nbiBEZWZlbmNlIFB1YmxpYyBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEA005UBBvQ9JuduCOH4CDHnpixcXoGkC7irUj+kwVs7Ia/KECFs0x5\n70dTmBAeVO59eLgYEwxEUv3QgaqTCCM5vl8Pa90ll/MBQt/UgQDEUL56iS0Zr3NK\nP8w6wL+iqMUV9z58QXSCay53ZuJqpZGIbgYxp68L5lrgrn1ary9H0PL7hHOcRqEe\nhERRxF8u2pACX4HfEQ7S+7s6F3Oj8o1jqk//cnplYoNaKjzyzSwjjc/rIR+/1ANX\n9TcWDF7lVxHCqPr/bDnyPVLmtXnAW+Ky6mMgDA6lKl4S4eavX4t8oK05NTWYX/Gv\nONAm0029Ynd1Pa9rFIZ7WvYhj9bq4qcOrQIDAQABo4GxMIGuMA8GA1UdEwEB/wQF\nMAMBAf8wSwYDVR0gBEQwQjAGBgRVHSAAMDgGCSokAYJOAQEBBzArMCkGCCsGAQUF\nBwIBFh1odHRwOi8vY3JsLmRlZmVuY2UuZ292LmF1L3BraTAOBgNVHQ8BAf8EBAMC\nAcYwHwYDVR0jBBgwFoAUrJnhAi/oXEtBtzS4HumbgzYNlLQwHQYDVR0OBBYEFKyZ\n4QIv6FxLQbc0uB7pm4M2DZS0MA0GCSqGSIb3DQEBCwUAA4IBAQB4vIFK2DpXu70m\nv+oqKPCIivJQTJBn2kv1uBQIutt/cqiaWbzxHImo9DoDEFQTel3G2ro+D4jVatMb\nly1iYTpv+QCvcgZz7BDAYR7MXE8ZMkY4wd0/0jcapY6GoPAJzDXWGQJ8zTn89/kf\n55R5Tj23+JdOO0RqzZSwufd+4uP5mX/F06ZQtEn7Fn5OQSzPPsd5QLqBGCYI+cWd\n49jxbxxoP2pbdxdSowbeGcJLbqKV/NUIvyy1aTVR4+PfTxopbYN4PTgkygI/VBDh\ns2Th1Zre8zf2MxC1drOr18kfUzqtVUEcSMk2nof/ddxp0K/ZelfGyrFD/DmB/Nx6\no5qlmFBU\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICvjCCAiCgAwIBAgIEBfXhATAKBggqhkjOPQQDBDB4MQswCQYDVQQGEwJDWjEt\nMCsGA1UECgwkUHJ2bsOtIGNlcnRpZmlrYcSNbsOtIGF1dG9yaXRhLCBhLnMuMRcw\nFQYDVQRhDA5OVFJDWi0yNjQzOTM5NTEhMB8GA1UEAwwYSS5DQSBSb290IENBL0VD\nQyAxMi8yMDE2MB4XDTE2MTIwNzExMDAwMFoXDTQxMTIwNzExMDAwMFoweDELMAkG\nA1UEBhMCQ1oxLTArBgNVBAoMJFBydm7DrSBjZXJ0aWZpa2HEjW7DrSBhdXRvcml0\nYSwgYS5zLjEXMBUGA1UEYQwOTlRSQ1otMjY0MzkzOTUxITAfBgNVBAMMGEkuQ0Eg\nUm9vdCBDQS9FQ0MgMTIvMjAxNjCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAftR\nBb2dghxXs6Ux+c+wN9n65c7jLZWUzLty376ONIGEtyRBKRZ6cJRb0nPN7MahIa1r\np+62J9aNMH5pabDyMw/aAagmk+jmrpgBSfOx97Rn4Ykjru9oJMYpeC2IoDlPQ9vB\n3/JU/EF6lzO/10wdL1vKoOR1BmkYFu6f6wziidk9tmfQo1UwUzAOBgNVHQ8BAf8E\nBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUddg3MwTzndDrHQqP5+Ym\nzNBNKyowEQYDVR0gBAowCDAGBgRVHSAAMAoGCCqGSM49BAMEA4GLADCBhwJBGieo\noGlHxjtDibWSwrV99tHrZTmU4EsvGb4vctlUlmnhRwEBp4tsf8PF8Ra2TbowhgS0\ny/N0XUH9Dn0I7ein2l0CQgGGuyiX8t/fYzue3h+GvevqS0lw2n4E8ea5yLUKNM0A\nB2eYVTxHkwWvbgOgl8nwCtsTSq1HleJIspSWOPt9F3Mf0g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV\nBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g\nUk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ\nBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ\nR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF\ndRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw\nvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ\nuIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp\nn+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs\ncpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW\nxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P\nrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF\nDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx\nDTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy\nLcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C\neWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB\n/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ\nd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq\nkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC\nb6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl\nqiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0\nOJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c\nNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk\nltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO\npwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj\n03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk\nPuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE\n1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX\nQRBdJ3NghVdJIgc=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDZDCCAkygAwIBAgILMaXzypDqI6zSnr0wDQYJKoZIhvcNAQELBQAwPjELMAkG\nA1UEBhMCSlAxDjAMBgNVBAoTBUxHUEtJMR8wHQYDVQQDExZBcHBsaWNhdGlvbiBD\nQSBHNCBSb290MB4XDTE3MDIxNTE1MDAwMFoXDTM3MDIxNTE0NTk1OVowPjELMAkG\nA1UEBhMCSlAxDjAMBgNVBAoTBUxHUEtJMR8wHQYDVQQDExZBcHBsaWNhdGlvbiBD\nQSBHNCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr6vH5Yym\nWJ4v1gXJkXcwvt4a1A5jYtHMLbHRhjiNHYVmU5+qQWXgWNLlKb6UqJWTPF9qxZuf\nNOhtwcbTp+VDoBIwwDk0YAyL9Gj1SN/pjhyuSKe7qj14t+JJu8EjBFobkAHFfatK\nAaHCk2rShbO253bra2846yBSMJUI9fks7sjAdbkB7cE3VjBcnX9kwspAILmVhbyl\nB30Mvi6h3cYm6SopbJ8omClR6HYTG+8uCzdaM57AJWeqDy2o1JImOAGn0GIYLiI4\nOHgLulKZoXwmArHixeLezooCRISio+mLiGMxyS84AOnEAk0eIycSSNwRsfDS4g4w\nGa8DoQezNZQipQIDAQABo2MwYTAdBgNVHQ4EFgQUbtwKNR8gwuih030FTk9MYOWk\nxGcwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU\nbtwKNR8gwuih030FTk9MYOWkxGcwDQYJKoZIhvcNAQELBQADggEBAFUz1UC3Gn5P\n3HSDDkS6P71SlciTliPyAbkU68oSdM1hiDSvTV70WYqrHtjjWcEe+DC1QMa7uK/R\n7T9sqnOYguSYNK6SQQ5ZNhq6UBwW9Bc6LBvil2+yr9Ha3hRS34A8x089h566lb14\nvFU8ifYuJtUV5dBAEsWzcT9sZh+j/Eu1TuJu3IAHw/koFHv3XhZqQ6eukQEfT2Wp\nSLPObhoGIaTTMYiIpUkRgmvruZ1g/p/+xff4f6s37q/nWEa6CeRdOadLBNgDAslg\nKl5VaRELYHiBevRx9Y9Gro8EqJccgIkjY9v+66YXDlm2LrmG619ebN2B56swgSOQ\nJ7H3K5A5C7g=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw\nCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91\nbmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg\nUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ\nBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu\nZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS\nb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni\neUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W\np2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E\nBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T\nrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV\n57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg\nMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV\nBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE\nCgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy\ndGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy\nMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G\nA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD\nDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq\nM0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf\nOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa\n4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9\nHSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR\naZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA\nb9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ\nGp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV\nPWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO\npgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu\nUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY\nMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV\nHSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4\n9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW\ns47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5\nSm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg\ncLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM\n79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz\n/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt\nll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm\nKf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK\nQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ\nw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi\nS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07\nmKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL\nBQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ\nSG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n\na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5\nNDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT\nCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u\nZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO\ndem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI\nVoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV\n9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY\n2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY\nvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt\nbNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb\nx39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+\nl2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK\nTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj\nHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e\ni9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw\nDQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG\n7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk\nMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr\ngZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk\nGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS\n3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm\nOzj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+\nl6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c\nJfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP\nL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa\nLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG\nmpv0\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM\nBQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG\nT1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0\naW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx\nCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD\nb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB\ndXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA\niQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH\n38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE\nHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz\nkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP\nszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq\nvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf\nnZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG\nYQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo\n0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a\nCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K\nAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I\n36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB\nAf8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN\nqo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj\ncu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm\n+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL\nhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe\nlHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7\np/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8\npiKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR\nLBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX\n5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO\ndh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul\n9XXeifdy\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV\nBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk\nLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv\nb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ\nBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg\nTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v\nIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv\nxie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H\nWyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\nA1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB\neAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo\njbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ\n+efcMQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw\nCQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x\nITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1\nc3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx\nOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI\nSWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI\nb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp\nY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\nALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn\nswuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu\n7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8\n1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW\n80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP\nJqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l\nRtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw\nhI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10\ncoos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc\nBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n\ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud\nEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud\nDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W\n0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe\nuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q\nlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB\naCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE\nsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT\nMaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe\nqu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh\nVicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8\nh6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9\nEEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK\nyeC2nOnOcXHebD8WpHk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD\nVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf\nBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\nYXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x\nNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G\nA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\nd2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF\nQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG\nSM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN\nFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w\nDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw\nCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh\nDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD\nVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf\nBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\nYXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x\nNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G\nA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\nd2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF\nQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB\nBAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ\nj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF\n1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G\nA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3\nAZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC\nMGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu\nSw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGUTCCBDmgAwIBAgIQaF3MJjngI2bkSp1k044ENTANBgkqhkiG9w0BAQsFADCB\nsTELMAkGA1UEBhMCWkExEDAOBgNVBAgMB0dhdXRlbmcxFTATBgNVBAcMDEpvaGFu\nbmVzYnVyZzEdMBsGA1UECgwUVHJ1c3RGYWN0b3J5KFB0eSlMdGQxJDAiBgNVBAsM\nG1RydXN0RmFjdG9yeSBQS0kgT3BlcmF0aW9uczE0MDIGA1UEAwwrVHJ1c3RGYWN0\nb3J5IFNTTCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0xNzEyMDUxMDU5\nMjlaFw00NzExMjgxMDU5MjlaMIGxMQswCQYDVQQGEwJaQTEQMA4GA1UECAwHR2F1\ndGVuZzEVMBMGA1UEBwwMSm9oYW5uZXNidXJnMR0wGwYDVQQKDBRUcnVzdEZhY3Rv\ncnkoUHR5KUx0ZDEkMCIGA1UECwwbVHJ1c3RGYWN0b3J5IFBLSSBPcGVyYXRpb25z\nMTQwMgYDVQQDDCtUcnVzdEZhY3RvcnkgU1NMIFJvb3QgQ2VydGlmaWNhdGUgQXV0\naG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAldFHKO7wVLzr\nvdWrBn4lpAOq/WB6zje5adopeXdsPX+CNMJd/kKkDUFaANKDpGptweXIUWL6a9XG\nR9w4bhGQjGgVz+m6WOaai4WBEC3P51NJ6aM3Igy8dLK2JVIRz6IhPImg16QdIxBr\nHVk7N/RdNjhAtXVCry0aB7yNYxTYSvgime/AWklvq5I/S+ykahg/US7TIOdPLoMG\nOl5/FYvP+jUuU7lqGs+n+Dy5yXMXOv2tDVjNknXqP/+5hvP+1aD1Zepj1vqGEbR0\n1bVYhKotXUoXvuymJNegvbcYOBZnbhGFW19gUovRz+VC0Jxe9Y6FvfKGbKhV3Osd\nev2sKPDE0sepB9ddPhdWlEbum8rEsIwaatfPm86mTC2A+J3xI0CaQCs4VR41A911\n2zHUToonb5eOnMx2mR1WrjJMF9kZr6ikzAvKAnUBTj28FPSqO5vQT7fn/lrEztYM\nczOsqc0six0NIflh5qF24q7wdEkB/DnfqBOSyGOJXrUQ8R0h9tMY+3dMaeJqzOB5\nrE6bZM/o4vMiooeenhskDHFm5el25GRUm80N9lF9u58AWh50tNCrjR2rCO8rwtu9\ng2HXyWS8D24XxjLfDPOmXu7sIAwqz3pFUHsY1vsSduGvWR+B2jSCNkW/kslVpdZ1\nBlmHm6SD3q14eWw8qI+d7lzsPOOJoisCAwEAAaNjMGEwHQYDVR0OBBYEFEI6XjZa\n3Buq0KLq9fFEf3Qlc+m9MB8GA1UdIwQYMBaAFEI6XjZa3Buq0KLq9fFEf3Qlc+m9\nMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUA\nA4ICAQAGOiJqHPwbet1ov9VKqL4LYthqZ0k0YBxbs+0lvjYOIFd1A4foZnesd9V3\nYZRt6HRxVGv0/Lbyi4pnXx0ECD/+gSDtjzzXR3ZYQtFqxzF0fjRNpntFUXAT+EZE\nR88N2pYUxoJWPoUa6LKln3/ND2yDguIYB9xmXIrKXaiEL1SMg/DFPEAgMuJP6Fbr\nlcLkxlD+IuivAVIrla6GVpWnex7GN+419vf7NtDgKt0wMsNtFCXHVdJrI2+QKgpj\nlnpm6N2Asnn/k2htD7EUU+XOe0zQwSMLOoPkzI773C7ZdFLgUL26Sfh2NBYfaSv0\nKIYdTDQVF9p0qHCWXT/CHccEh1Wia7Gy9TVWYru79UfsgrRmahNIeFRjz1+A7JhG\nxEnJ9KQrlSXHwKPbVly9qva5N+LaROUNS4d5naadH60P/c7pZq3xBJRVSNerJ5Zh\nVfk23TXfiFY19mqxk1hYZSq0pd0PTYsHGb2CqnW0QsxVWd6nciiBfqyrG+yAHJhX\nEhnftyYpMdL6kA1cHjAvKoYuRWPVnuV8cH8CZS4Z9AFG3ty4V52+eT5Ufy6DTnLF\nzVlhPfegtpOUa10JMCZzOFb8V3iH7+04wg1WMISJmxaOegi1fyYSw1D1Gyqyb5A4\nNuA1EUzZHh774biMRaxg4fm1uey/wQl6KSXD6SHL0O+DrCI8aA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGWDCCBECgAwIBAgIRAI5ZQFi3WJ+9F4SSs8w6x5MwDQYJKoZIhvcNAQELBQAw\ngbQxCzAJBgNVBAYTAlpBMRAwDgYDVQQIDAdHYXV0ZW5nMRUwEwYDVQQHDAxKb2hh\nbm5lc2J1cmcxHTAbBgNVBAoMFFRydXN0RmFjdG9yeShQdHkpTHRkMSQwIgYDVQQL\nDBtUcnVzdEZhY3RvcnkgUEtJIE9wZXJhdGlvbnMxNzA1BgNVBAMMLlRydXN0RmFj\ndG9yeSBDbGllbnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTcxMjA1\nMTE0ODM2WhcNNDcxMTI4MTE0ODM2WjCBtDELMAkGA1UEBhMCWkExEDAOBgNVBAgM\nB0dhdXRlbmcxFTATBgNVBAcMDEpvaGFubmVzYnVyZzEdMBsGA1UECgwUVHJ1c3RG\nYWN0b3J5KFB0eSlMdGQxJDAiBgNVBAsMG1RydXN0RmFjdG9yeSBQS0kgT3BlcmF0\naW9uczE3MDUGA1UEAwwuVHJ1c3RGYWN0b3J5IENsaWVudCBSb290IENlcnRpZmlj\nYXRlIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOrA\nZChzgke2wM6tiNzS4e5IUvMQ504IhuAv7zgmShfwe0MbqlFNIjIHU3YKt2Cxqj9H\nGkv+mMrz1KhbeN6Tnvw0JXSQ6BbmnWNVPn9Vc6YSb/eoc82WkjGutMQBSF0Rf/Z9\ngr5dDemjK+sxLjnmWkqe3AZsKJj2cfzwWkL2u8BBJub5z0Gg+H5swZPF42Pn9pRC\nJNhrZ9HndRsAjgoEJ8fgGze7XuAuyaUEcw369dY4pKTWBpYWK4AQd9D3afFpkqmq\n/MMhtv0TMQk4/8P1b+NHsyHo9mXUuNNbLnzdCk+6Sd9qj7BCbLZHaa6zaWuYKGLz\n/Hf3H3Y0Rji3Ixe51C3aVxgDCaVVnaHyDAC8JTlih9FAB8AOy87UC3pQke+QJw7Y\nVwCIkuIXyWnBNR6kb8CphjQ3RFK8Q7J9iY+lo1nA0DiMp8tW/RlbwZW15UC9+YLE\nySLUMp2Fo+9KdKcVBj5wIkgrDCOs0GJcuXz3hdmN+MXTl49e6vAM0LGaCE+ZBoHk\nGil8pPoWJ5tzUanFJPYlGKizMtdK59Na2ZvCMjsEho1Yc1WQLmhISVQ6O+4loJni\nXANmU8xu1A0RHXmq1PFlC4/NT1QBEAw/XY0AZDQfBiDsodaSC8m+tmKHVAn8/hpz\neSERZVye1bOQxaSWviOrfYFZ8TqbV69dgW760UuxAgMBAAGjYzBhMB0GA1UdDgQW\nBBQ8tpw4Wuy11CILQL5jDwiLKO4MGTAfBgNVHSMEGDAWgBQ8tpw4Wuy11CILQL5j\nDwiLKO4MGTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG\n9w0BAQsFAAOCAgEABTcWLooTAcR8JmnoMwVS/QhaghKNzwoTWXg3usVEFzriFT/z\nj8zcVy0Toz7leLsrkZ0+4UJsVXVuaCyUP5uCN/w8L34cZvFVyYPSiMCbrJP+2WAv\nOlMkv7UvVV9hs1NPBNtuNqsdLyjD1SK7GKQnHiun0XxRfoIrd/91dZuJgefQwdvz\nGb9LbAcSBA7iBgspSGY6NSbUveEFdCGK9cbPFlArFMVk6hb8TSFVjCjvHMzqEJtN\nGKqOTdwBxkVN8cdu+0eApzDHJ/ytCoGb91ZV2rsflfdfEHgji6OgZVAEY/M+QXOH\nFNxagyc40CMPpegsjhYmmevld5V+6Y+Fj0EUkP88icflXIrXYwxpc6U4HW2pYxyV\nf/filBDQ7VagR6FAJR+5sry6as1eNoAOslWLPEvmgcHKJ2nfsy44/L+zqh2ybSBS\n3Iw/G4N6rBt506ToKTAU73iM6T5Y4tnP9XvTYbkcATaw7DCIW5+zGDpG+hbly4S4\nOQSXTiQAR10g84zxpG8yA+BKZeWMuhXUVFi8sVB6cC6sQwoN5qbwIi5fShoAbHGT\n2xpk7hlxfQW2mIzfgN2KqDooNUMU/vMEOo8hOA9OE4OO39v72drg5fdGPO/a6G5M\nngH6MmW7UhMgaTubG3+TzzAzjrOKI/wH02lgEvdEvQMvqPBHFXcn2GG3kLU=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGDjCCA/agAwIBAgIDAw1AMA0GCSqGSIb3DQEBDQUAMIGWMQswCQYDVQQGEwJG\nSTEhMB8GA1UECgwYVmFlc3RvcmVraXN0ZXJpa2Vza3VzIENBMSkwJwYDVQQLDCBD\nZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBTZXJ2aWNlczEZMBcGA1UECwwQVmFybWVu\nbmVwYWx2ZWx1dDEeMBwGA1UEAwwVVlJLIEdvdi4gUm9vdCBDQSAtIEcyMB4XDTE3\nMTIxNDA4NTAzMVoXDTM4MTIxMzA4NTAzMVowgZYxCzAJBgNVBAYTAkZJMSEwHwYD\nVQQKDBhWYWVzdG9yZWtpc3RlcmlrZXNrdXMgQ0ExKTAnBgNVBAsMIENlcnRpZmlj\nYXRpb24gQXV0aG9yaXR5IFNlcnZpY2VzMRkwFwYDVQQLDBBWYXJtZW5uZXBhbHZl\nbHV0MR4wHAYDVQQDDBVWUksgR292LiBSb290IENBIC0gRzIwggIiMA0GCSqGSIb3\nDQEBAQUAA4ICDwAwggIKAoICAQC/1gBKiQ4vIztyf3MgZaBfFsV7XlwG+WZzIIL1\nYpYXlFH+mzXo8g5ffyGVHGLA5PmCeFzvVcDH/A1587ZMgjYKsEv8LWGmC4i4T7kF\nrgbMCdN7Sg1oiRNFAKOdXOZ+pR7nBi/wa0WkotSbh8qYZWDrWsyileyTW0qldn1f\nddItlUd6abFziKxlJHkgf4iGRWQS6BTHOJCXHPFB97jgN/+2tcwxWswo/4SoU1ZY\nct1jwDtHHYxWQ95UxwjMP3rowgPKNLyFlefD0SDS9Eor8envfXpbtQRgUgR4nejn\nKUNuOwEA2CrMBiYCaoQ/8wiqPhT99/eOuYAwQqUFfM3zoYQieBFBCdWMgAtOWI2Y\n1HM9FfdtmT3khPNHPC9rmRSEITucVmVS9Y+rDaljgsw5UrHqp1njo8APeT7olT5G\nrLnduFeF9pf/nrMI5jdW3vymMziNvw1rlqaL6XBKt2dEqIkukOaXi+5vnKxzRftp\nOP1W+AXroxHMyPLyxLD41xn4BuaWYH3U5Lbz1JsZX98xg8644HWWKW08L+hZwEqf\nuuz6k/aRby0kFJIrvq2dCFg14WEqE9/Y0HzxVvNrdC3E4+6AYSyrCl1VSUthr5VO\nsbdS1pnT7yTQHAZImhvCF5yy5ov9LXKxlzwYSVFWfFXkEr5QiR1pKBlIw9oigang\n4AWqvQIDAQABo2MwYTAfBgNVHSMEGDAWgBTRpwgWB57pvU7T1yBTllkGJ9eITTAd\nBgNVHQ4EFgQU0acIFgee6b1O09cgU5ZZBifXiE0wDgYDVR0PAQH/BAQDAgEGMA8G\nA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQENBQADggIBAC1Qj8Fm74llE8N41MzM\nWpdv7I9gVN5zZLcN6OE7pazPhbaWOUxEpDtZNwyAQBYzcnRI4IQloxstDQDhM2DC\nwV92D7OiS3DFJkDNEPpY9IFTj67cJ0iFlaaizkpCGb+VNSBk30JqZnUNVltLdZY1\nU4McUKDlx5Sdy9ayPZNKy5SQcchvb2GbbvHQiOvEbz6DNEBUmEf9TMzKHI2D4DFt\nMDWz3yTEjTbdwNT8WYaso/BQvhhKQHhXoI3cDZK1yZZspzldPryuK9pxVj3RJ1Sq\ntAZ82MA8bcWd8jxVvvFhDtgc0ah9b9izF0K31RJlJs77lIXGbG1a5W58gD07m84v\no/i98pIiXG4NeggKPlzd0//2F9YlZ8H7hnxUV2pzUr0HpUkF2RGLlUby3GIGiqyB\nBFfJuFRGGInEaB8VHpUCWKrEYZ8uD0TbTAGCaJX7Mf/QwgROfUex95nN5Q7CjBcS\nRJaCPZGYGpe2Z0Fw0o680WIgdoAS7Q65+Z8miUzXT2upbqXB+rsEE11mR46JqCqx\n9l8XFtz9WRJuJ23dvej9xxF98vVWz6p+0P8TIoVi+UfqaO0Pk9hYYcrPdeMUZSfg\nEn8jHtbtDz69AVvmFCYjXeAER3QlrMGVM6gzYCmdnYZj9dC9LxYRJtOZKY+Clnpc\nr/xS7vOO+Qq8VUHSmfQbp31m\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG\nA1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg\nSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw\nMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln\nbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v\ndCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ\nBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ\nHdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH\n3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH\nGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c\nxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1\naylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq\nTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL\nBQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87\n/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4\nkqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG\nYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT\n+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo\nWXzhriKi4gp6D/piq1JM4fHfyr6DDUI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFlTCCA32gAwIBAgILAIZNvw/jXtd9jtgwDQYJKoZIhvcNAQEMBQAwZzELMAkG\nA1UEBhMCSU4xEzARBgNVBAsTCmVtU2lnbiBQS0kxJTAjBgNVBAoTHGVNdWRocmEg\nVGVjaG5vbG9naWVzIExpbWl0ZWQxHDAaBgNVBAMTE2VtU2lnbiBSb290IENBIC0g\nRzIwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBnMQswCQYDVQQGEwJJ\nTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s\nb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMjCCAiIw\nDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMNwGIWW2kHfHK+sXTNwxF07K+IV\nySTuyFM2r1v002wUfcdT+zs5OM5QbMYFFnedXQI6gCFLsjKrcaej48Zt37OyEb3i\naPs7CsP4kAyTwzKH9aZe6gXYHrJq40/ZVMNcQVI2PcIp40B/SAN2gUZ+ZaUtIOvV\njEx26/ebNaXRIsthlkOG/caB+QRwDw1tl7338Zlv0M2oTBUy4B3e7dGP5pgXH71M\njqHPCoNo+xv9f0NTBT+hUDa8h8wUtcGQq9CDeJTpjWcD2bP2AMdVG6oVpMAUeUzo\ncCyglvtFdUMjggxBbw4qhau1HXPG8Ot9hwL7ZMi8tkTzrvUIxxb8G9LF/7kKeCE7\ntGZaVzDTnXuifl3msR4ErHsQ4P7lVu2AIjIAhrAXoedDidb7pMcf7TABdrYUT1Jo\nG/AiK+J9jO6GTjeADD4LMDSBZhHMuBK/PJ/g0kGBt+/C1L+/HURzQhJkMlRnM6Rv\nXoCtfKopSlns5trZmTi971Wjbn88QXP61lGpBCUPwCjs7rpOYvSUJtI+lcbF+37q\nkIqOXYkVT3cupDSpw+H89kFtj5GKY+Xny4LxY+3IvDIRiyd6ky1DPj713DI0yqve\nEpsIr3A0PdwuyUI7CS1jg0NnGFT6Xxyr0xB+VDt83FJYW8v16k2pbaQ4kVxA3aXd\nX9dZYyVR1S59KM75AgMBAAGjQjBAMB0GA1UdDgQWBBTt7E1FYRgo57MjKBEcTaUn\nDV7s9DAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B\nAQwFAAOCAgEACFC/ilQg8KTCVBxFJW/sazomkS0kNYbEIZg4B3obqwsJ7SX98z8Z\ngfzBpz0nYClwwJjWbFN1R2zY8pCEot6/dgmA8Vbq0GxhwPM5YN/SZquNyRIxO3cU\ndlAcwf+vSezdVCf9wOzvSAF3q0a5ljvbdbNJNpfScQVp7UUd5sBsZk8jXO1KQ/go\n/Vf/GDPnrIFmxpAIGE3sgnO8lAv9FzUaAeuv7HWe47xN9J7+bQzF93yHuIXACPTL\npQHhg2zMv5C7BAbuDHfbj1Cu294Z832yhSfBcziWGskOvl3es2EcHytbS9c9P+0z\nMpka7zGC1FHrvLb/FoduH86TeZt0QjZ6pcplNzoaxDnDvzTJ6CC2Eny+qH/APFCu\nVUv5/wjwF+HPm8Pup2ARj9cEp92+0qcerfHacNq5hMeGZdbA/dzdUR/5z5zXdxAk\nnl8mcfGb0eMNSTXQmmB/i4AecNnr72uYjzlaXUGYN7Nrb6XouG0pnh0/BBtWWp0U\nShIPpWEAqs7RJBj6+1ZUYXZ4ObrCw962DxhN2p19Hxw9LtuUUcLqqTPrFXYvwO4t\nouj7KJnAkaTUfXGdEaFVtFig1EA30WzJY2X1vAQ7hVnniCjgaXAGqjsU6sklNM9n\nxDx5rFCCCEtj9Kh8UHjGK2QqgP5kwgttjOApQMaCoezMfK4KD7WpOXU=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG\nEwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx\nIDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw\nMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln\nbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND\nIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci\nMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti\nsIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O\nBBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c\n3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J\n0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD\nVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU\nZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH\nMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO\nMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv\nZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz\nf2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO\n8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq\nd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM\ntTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt\nOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB\no0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD\nAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x\nPaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM\nwiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d\nGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH\n6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby\nRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx\niN66zB+Afko=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG\nEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo\nbm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g\nRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ\nTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s\nb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw\ndjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0\nWXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS\nfvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB\nzhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq\nhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB\nCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD\n+JbNR6iC8hZVdyR+EhCVBCyj\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFcjCCA1qgAwIBAgIKLwq3aw3LSq8nWDANBgkqhkiG9w0BAQwFADBWMQswCQYD\nVQQGEwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJ\nbmMxHDAaBgNVBAMTE2VtU2lnbiBSb290IENBIC0gQzIwHhcNMTgwMjE4MTgzMDAw\nWhcNNDMwMjE4MTgzMDAwWjBWMQswCQYDVQQGEwJVUzETMBEGA1UECxMKZW1TaWdu\nIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxHDAaBgNVBAMTE2VtU2lnbiBSb290\nIENBIC0gQzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCMfX1lA+Tb\nmh9YInmRgOW97IVx4LUJf2DRZfs837Jrml+py64aVnYgWO4t6C78fgjfS7jX+c4T\ninIzEquWcI+zi0fd4Sc8NDf7JONp27VWX0qwUYqzLDRCt+s7zpLcfx1ky0zVIJj6\nL06uPyK3kIr9+YAsrVj+39utm6e2MBQsRNstSI3fCQYAGvoQTQ8fULauTqNWaYAk\nNYFe6HUHHQPp2u1Ua00odMXiD5oRFxLcDnGAcE1I/9E9mLCdkggXijYUmico7+Xw\nZeFoPhva6eIJ5p03Lt3Du5W3EcHR0cJmmY1pyeA36JaXKWRNM9IRjYMVNCcp4jhB\n2tIYiZ+LVk8bwQ9/1c23txmv3u97taZlV22NF4ttS1qq3J+MOp0oGULBzpKfRx0q\nGVqbPukQNGAjOLIN8KDNQNzbR1iAl2d8H+MSoicBo4Aid8TjLWcNv48oCWL53ZrF\nBMTDjaIA6frG1t4IpbnHadA7qCJJe2qpJN6n2eQKAUn6UiQDHPsSqNBlcUhQ4Y/0\nY0mU5rghm2OB9rXQS1Fb1JRCfJMNnJIm5AUB2+2RWzq5Tgz7SbSho8NsZk0UbQnF\nxciqQ9uoVTAsK14Sk9oG8Q3zfsM08cdPoRb0WlIZklR6mKD7L8nH/zfGu8PIJv94\nGGB9RZ9U4A69r3ePmy8MvrzfNxHKtH6svwIDAQABo0IwQDAdBgNVHQ4EFgQUs/eK\npNYPiABZ6FEXT9V+7IYigZ0wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB\nAf8wDQYJKoZIhvcNAQEMBQADggIBADQlpiWM0cv2nZ0H5jVsBq0x2q62Q0LwqATs\nCFvyub7gxNCytRuoA8stmPOEu/lg8Igxj4FIjoyhIrWUVxyiLU7No4P+WjEUOwUT\nxIpkEOtvGUQ9fiOlcGHtIZDNBlZq7WpktXAxeV55RPPsor26p2FNAMRFfZQh0sLX\nhKgk8iulSSggqx8ezgPye63FaiYEi4c/dzRj3HOCnsZiwZZU02df5YpNFjxSwZvE\n41cjGpsrpWMfQFI2s53RbeXp47lSAxYE4NzjBFMe+EwFuEveBCJBEAH5rvYu3pi2\norsJ424TqWEQV1tCsCkQz+Yq/Okal7yHAkKDeOXcP7oN4A+TdXc2pdqxuVCnBO0R\nmWz2JpGSSeJjiTk/OPwRsPNWtwG/KXL04o2ta3jiPpJuICVtWDAc9R3auBEgJl5r\nShRmBdszG0LmzsHuZPCFSYC15RBDCOBsa8bDRJ8pBFU2Wi/CVXCACEuavgoveA4F\na5bt38o0PWxsBP+MpocCdVtDMqzQhxy9IohKuXWAGresoIvKDg3xFk6rBOrjfVwJ\nelwi/xAisojHPJVQv9W1zVIoHp+EQg/4MQC21NbIX2RoioB+V3hK439b/w7deU8x\n2M8cl1OG0nPfbnARl5GPM7vJgi470jto4SeMg6HMAW3Egb56tQcNLwI9U8mZnNvR\ngUMrkAgL\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDIzCCAqigAwIBAgIQFJgmZtx8zY9AU2d7uZnshTAKBggqhkjOPQQDAzCBlDEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v\nbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE+MDwGA1UEAxM1TWlj\ncm9zb2Z0IEVDQyBQcm9kdWN0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw\nMTgwHhcNMTgwMjI3MjA0MjA4WhcNNDMwMjI3MjA1MDQ2WjCBlDELMAkGA1UEBhMC\nVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV\nBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE+MDwGA1UEAxM1TWljcm9zb2Z0IEVD\nQyBQcm9kdWN0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTgwdjAQBgcq\nhkjOPQIBBgUrgQQAIgNiAATHERYqdh1Wjr65YmXUw8608MMw7I9t1245vMhJq6u4\n40N41YEGXe/HfZ/O1rOQdd4MsJDeI7rI0T5n4BmpG4YxHl80Le4X/RX7fieKMqHq\nyY/JfhjLLzssSHp9pvQBB6yjgbwwgbkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB\n/wQFMAMBAf8wHQYDVR0OBBYEFEPvcIe4nb/siBncxsRrdQ11NDMIMBAGCSsGAQQB\ngjcVAQQDAgEAMGUGA1UdIAReMFwwBgYEVR0gADBSBgwrBgEEAYI3TIN9AQEwQjBA\nBggrBgEFBQcCARY0aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2Nz\nL1JlcG9zaXRvcnkuaHRtADAKBggqhkjOPQQDAwNpADBmAjEAocBJRF0yVSfMPpBu\nJSKdJFubUTXHkUlJKqP5b08czd2c4bVXyZ7CIkWbBhVwHEW/AjEAxdMo63LHPrCs\nJwl/Yj1geeWS8UUquaUC5GC7/nornGCntZkU8rC+8LsFllZWj8Fo\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDFzCCAp6gAwIBAgIQFTh14WR+0bBHtO+vQRKCRTAKBggqhkjOPQQDAzCBjzEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v\nbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE5MDcGA1UEAxMwTWlj\ncm9zb2Z0IEVDQyBUUyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE4MB4X\nDTE4MDIyNzIwNTEzNFoXDTQzMDIyNzIxMDAxMlowgY8xCzAJBgNVBAYTAlVTMRMw\nEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN\naWNyb3NvZnQgQ29ycG9yYXRpb24xOTA3BgNVBAMTME1pY3Jvc29mdCBFQ0MgVFMg\nUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxODB2MBAGByqGSM49AgEGBSuB\nBAAiA2IABN7Nu3Ag8SUgtJTo17Q7D26H3ausz01AL4Eza1kJGNaHDSYjnLSNlZ12\nn6W5BkLmrTayxLOuejwI1cudOl5FIWwL4yD1m8LdRDPjQrnq8ihCkqr+DAfKihOZ\nO2IA7drzNaOBvDCBuTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAd\nBgNVHQ4EFgQU6EfIQpqwna5vCyg7mBWP47HogLIwEAYJKwYBBAGCNxUBBAMCAQAw\nZQYDVR0gBF4wXDAGBgRVHSAAMFIGDCsGAQQBgjdMg30BATBCMEAGCCsGAQUFBwIB\nFjRodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y\neS5odG0AMAoGCCqGSM49BAMDA2cAMGQCMBSGUMAmGuvqoRR3OlvfYzmlM8dQQNVr\nNWsPtN99VrnhpZ14GYKhQ24a11ijVQNC2wIwGJS0HjqNZPoMJxuHE0rStzoAlMby\n5WO/r+P63JPV50aaa4FpPgLfUQ2PKHFBiZEv\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6\nMQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu\nMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV\nBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw\nMzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg\nU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo\nb3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ\nn0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q\np1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq\nNwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF\n8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3\nHAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa\nmqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi\n7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF\nytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P\nqafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ\nv3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6\nTsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1\nvALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD\nggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4\nWxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo\nzMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR\n5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ\nGfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf\n5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq\n0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D\nP78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM\nqJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP\n0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf\nE2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw\nCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw\nJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT\nEENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0\nWjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT\nLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX\nBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE\nKI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm\nFy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8\nEF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J\nUG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn\nnvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFajCCA1KgAwIBAgIQEoG5GPN5OkKTzpFYYeTtXDANBgkqhkiG9w0BAQsFADBP\nMSUwIwYDVQQDDBxEaWdpZGVudGl0eSBTZXJ2aWNlcyBSb290IENBMRkwFwYDVQQK\nDBBEaWdpZGVudGl0eSBCLlYuMQswCQYDVQQGEwJOTDAeFw0xODA3MTAxMDA1NDJa\nFw00MzA3MDQxMDA1NDJaME8xJTAjBgNVBAMMHERpZ2lkZW50aXR5IFNlcnZpY2Vz\nIFJvb3QgQ0ExGTAXBgNVBAoMEERpZ2lkZW50aXR5IEIuVi4xCzAJBgNVBAYTAk5M\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkgc7BfM91cHK5ubHBvp5\nqD9oZ0R3M2TDH13YclmDY8+TzKWTEwFBxAoPps9nGjI0oLpAnEe+QqzeGwdcSCMz\nUp0p87dcxjVCaoZ0Z8jJmhNVk1BfRi9AKfCmnnx7WlTaiiryAZtKje7PbBBF9fAg\nETq9jlh6mEKXkwNiDzx8YSia2lVNJMB8zwvL2R3ZzWm6i82ONMX0dVdGK4KNbjzl\nCJV6b0qLfeOEf35CKtmxIaAm4po4F7Gq3TLkTKar+cQmB14GlbnPrZ/J/8sj0jno\nJEiIErHVz7TE7D2L/nVvxxFyEui62prSfXFrXtmMfjGG31jdLJlKrLAtzcrcYC9r\nMKJaizzLGzD8ETNJSdlW1ugh3rS6PHrXGCUegPaL5gWXddR0aIVDCnSLHLEtuZ8E\n2KGX1KY0UsyNMoStie3m+EWMc5wdNeYO562Y90nJCpmWUKIujX/uqRoeqawntsxZ\ny0qS6PLXjqeNXU7VdQeg1Hgj2bUfWuOxQBqg8X5taMR8OVq+StI1k/VmNNb9C5Sq\nmK6iLS5AcsCrrgBzijeIevxCmoXderIy/t3EhjSEf3saacC3PrST3Aax4Bjifoey\nKMXVaU7xy8PTUjwFIZzZZawZq/+xZSw4emoEM6esnyguzsJMk5jwwgGqkBhH07or\nMKnNaVXYH2M8NzM8Ze/v5x0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUwnhnF2uPPk6xWJaOekLaZz/EF10wDgYDVR0PAQH/BAQDAgEGMA0GCSqG\nSIb3DQEBCwUAA4ICAQAeojNQBng8utKsHlJ2xUc7zr06qqTAr7Vcp3Us4yBks7WF\nVwnfPpPPlgYyHtZOMxc/6KIIuV2qgC6d71JeFw/gB3yJ40EY7YxUrlayfECIFit8\nxUWuwuZPNvhz/bQOmUBJha8hvhKT0/5mQPzRU6Alf512EWBIMEydrInciCS/olMz\nsYrL4t5hQ3h/euHtJI58CL80zjOUdXNu9M8oMt+9IhjNIbykHN6wpP+OGiPHX3RT\nebYAe2wyf1ztO3GwGgTiDuOjb39TvWZ/tbkfG6xz05NSo1kDOK1bZ2hiGifJ9r1/\nHa2dMHYUWDvzMKpCeUcQs3/ZOsrZmUpHnFuEEp9l+MeAtfQ/HNBeWfx4RIGniT6I\nXZKWsXRipuzpYnVbzelCESyLFCKaB4wG5IOoyleSWQZosjk6mlEIReIGA+U2T4he\nlL0UPK9V+DJ1M1/LUbsSGUZlAXNBZgWMvxhL/zk5j27g4lnW8Jy8DD46eIFPJFna\nRErXT7avmuxE9Xeb28MjkPZGGL2/L9F+KEAUMX26IAV4pHbdFg4KeqxpRv7wAe5q\n0m0OjjsVLnwjj3fh5X38GAOU3iGUJttGiVT4I7NYK/4v9vSWG5NlrXkDLMTfITh0\n5Jod9kVHOXLVcV37vghtFtWot2FjKqcowAemtd6V7ZKqbPvNXE1ZWuZdIJuGlw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHMDCCBRigAwIBAgICD6AwDQYJKoZIhvcNAQENBQAwZTELMAkGA1UEBhMCQ1ox\nFzAVBgNVBGETDk5UUkNaLTQ3MTE0OTgzMR0wGwYDVQQKDBTEjGVza8OhIHBvxaF0\nYSwgcy5wLjEeMBwGA1UEAxMVUG9zdFNpZ251bSBSb290IFFDQSA0MB4XDTE4MDcy\nNjA5NTYwOFoXDTM4MDcyNjA5NTYwOFowZTELMAkGA1UEBhMCQ1oxFzAVBgNVBGET\nDk5UUkNaLTQ3MTE0OTgzMR0wGwYDVQQKDBTEjGVza8OhIHBvxaF0YSwgcy5wLjEe\nMBwGA1UEAxMVUG9zdFNpZ251bSBSb290IFFDQSA0MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEAxmaNgqB+vosiJXgQwAiLmhl/1a0AFA5k3t4hcB3IYUL6\nVRyLnjvonYJHfLuOAn6dS9zi++i3PZkRqB1xHkfCJNFClXxk4tfbmhDeTJ6mQjx+\nfu2wywPtxrtd/Dn0xO6Kc7Mb/ffwaFSSh6f0bZt61RLov4JPNKOvhq9qjOQgjGZy\nrBGIle60IppJm8bl0A5bmRL4FQygNwIascskyl0Vy69LHx4CNUIwtgN7b1s++leV\nNpETeLFpCtPdLoxEswg/kJuMRf8XaBZmGJIYSArCKIVYyC/gO7PRUmiwv2yLYdm7\n9xvCd1xoIXHqPd23bqQs4vr5O0QzmYjU6kZbuLV8GIBuVFOH35tjtOUxMrZ+2Dja\nyuNcNc7OGnAoofqXvD5dfp5snqP+ZZYlVPXi9Y+N5e4PLt0rdud+uiLDW27ekSXR\nhvJMBxJxSb8XFgKPUbMnatCNTmtFaD9nfv5Uhlx7kfn2XzO61rnzuf2CcgSlNiT7\nTQSXepGBIPjg+5QYJlhacazdL7JHdUTjJqYVbnA/Zje68lzDMfL1wDSMExh2HWGL\nVGJZj6inVKBZB+4suo7FtdqyzT9AmVW9a1ekPlk7g/s93freyoA/EIwHy/Hvosk7\nVivLdYwU8IdUbX8JMA1QaxVgkMe6F7A7EKvFujf1L/nAnPt5CC0A2niFS+XBMikC\nAwEAAaOCAegwggHkMIGlBgNVHR8EgZ0wgZowMaAvoC2GK2h0dHA6Ly9jcmwucG9z\ndHNpZ251bS5jei9jcmwvcHNyb290cWNhNC5jcmwwMqAwoC6GLGh0dHA6Ly9jcmwy\nLnBvc3RzaWdudW0uY3ovY3JsL3Bzcm9vdHFjYTQuY3JsMDGgL6AthitodHRwOi8v\nY3JsLnBvc3RzaWdudW0uZXUvY3JsL3Bzcm9vdHFjYTQuY3JsMIHVBgNVHSAEgc0w\ngcowgccGBFUdIAAwgb4wgbsGCCsGAQUFBwICMIGuGoGrVGVudG8gY2VydGlmaWth\ndCBwcm8gZWxla3Ryb25pY2tvdSBwZWNldCBieWwgdnlkYW4gdiBzb3VsYWR1IHMg\nbmFyaXplbmltIEVVIGMuIDkxMC8yMDE0LlRoaXMgaXMgYSBjZXJ0aWZpY2F0ZSBm\nb3IgZWxlY3Ryb25pYyBzZWFsIGFjY29yZGluZyB0byBSZWd1bGF0aW9uIChFVSkg\nTm8gOTEwLzIwMTQuMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEG\nMB8GA1UdIwQYMBaAFJMYNh+paXBRNapPP6yNUH4mBSkKMB0GA1UdDgQWBBSTGDYf\nqWlwUTWqTz+sjVB+JgUpCjANBgkqhkiG9w0BAQ0FAAOCAgEAO01Radk3mUuojS9G\n+JksIhH6qWebQZg0UpN2v5H22JEI+HfBat2ept+TMmB9o9D51rhRoC8Y85yS0WB9\nJJCMauZcF77PjF2LTT4pO/bvEgI3ahrjf63iJiTNHFNztqyzKuOBGNAqQ2S0bV9a\nGNcAqvSbF7gJbyDE/74EFz9Qq0BHnmQJH4xQN3uzGJPM8XkRvxRgj+SD/tXnqGGI\nPWurj4J6GGBsIfr6ecYReq9B2syPC9E4uB8qFfvEQunA9NJ2mLLoCqtTICU3/t95\nIvUVOBl1o6q+QmYEfmUg2qJuIBbtXb5WhQ5hkRfIBFlQ8upyZQZaXXqlmJmjZJzk\ndNk7hstyRP7BhVdgyCyHZtBTX2p+cEO644M0fzw58ORo0s1zvG/tooRm9tWg+5ry\nhLmG2Xcrll4V+QxjFgmG8wFakq2AqNq4W7PxDHiAl/xqnh/kNgwkI+7VoTHrdqrz\nCSbyAwzjDd9T2kgRxQG8U6vfuEt84iNtySCdmp6pWPNPkfjNOGCQEv7GamcUlHw4\n11SfvD70YnW5nxgNdmqxcDcUtxzGngcXtFa/qAjxWR7TS25ESNkzzKAZELQs9ORy\nDLQkgzbYhCLdvDolc33xA0+Ge1bjzpH6PbpGDZxmWKTFM2ZJQQYNvWH7P55T3pbE\n53TUes0DYl+ICmA+jPmN4YzcGrI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx\nCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE\nAwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1\nNTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ\nMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP\nADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq\nAMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9\nvVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9\nlRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD\nn3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT\n7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o\n6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC\nTEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6\nWT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R\nDolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI\npEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj\nYzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy\nrOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw\nAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ\n8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi\n0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM\nA8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS\nSRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K\nTTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF\n6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er\n3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt\nTy3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT\nVmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW\nysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA\nrBPuUBQemMc=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw\nCQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw\nFgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S\nQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5\nMzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL\nDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS\nQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB\nBAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH\nsbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK\nUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\nVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu\nSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC\nMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy\nv+c=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP\nMQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0\nZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa\nFw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3\nYSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw\nqNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv\nVcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6\nlZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz\nQs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ\nKILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK\nFgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj\nHluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr\ny+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ\n/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM\na/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6\nfsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG\nSIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi\n7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc\nSE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza\nZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc\nXzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg\niLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho\nL5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF\nNe85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr\nkkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+\nvhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU\nYDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA\nMEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD\nVQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy\nMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt\nc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ\nOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG\nvGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud\n316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo\n0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE\ny132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF\nzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE\n+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN\nI/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs\nx2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa\nByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC\n4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4\n7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg\nJuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti\n2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk\npnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF\nFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt\nrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk\nZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5\nu+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP\n4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6\nN3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3\nvouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx\nCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD\nExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw\nMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex\nHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq\nR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd\nyXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ\n7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8\n+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL\nBQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg\nQ2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv\nb3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG\nEwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u\nIEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ\nn56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd\n2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF\nVwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ\nGoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF\nli/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU\nr8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2\neY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb\nMlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg\njwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB\n7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW\n5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE\nITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0\n90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z\nxiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu\nQEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4\nFstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH\n22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP\nxOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn\ndFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5\nXc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b\nnV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ\nCvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH\nu/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj\nd9qDRIueVSjAi1jTkD5OGwDxFa2DK5o=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGrDCCBJSgAwIBAgIJANLVi0S/gZNCMA0GCSqGSIb3DQEBDQUAMIGYMQswCQYD\nVQQGEwJCUjETMBEGA1UECgwKSUNQLUJyYXNpbDE9MDsGA1UECww0SW5zdGl0dXRv\nIE5hY2lvbmFsIGRlIFRlY25vbG9naWEgZGEgSW5mb3JtYWNhbyAtIElUSTE1MDMG\nA1UEAwwsQXV0b3JpZGFkZSBDZXJ0aWZpY2Fkb3JhIFJhaXogQnJhc2lsZWlyYSB2\nMTAwHhcNMTkwNzAxMTkxNTU5WhcNMzIwNzAxMTIwMDU5WjCBmDELMAkGA1UEBhMC\nQlIxEzARBgNVBAoMCklDUC1CcmFzaWwxPTA7BgNVBAsMNEluc3RpdHV0byBOYWNp\nb25hbCBkZSBUZWNub2xvZ2lhIGRhIEluZm9ybWFjYW8gLSBJVEkxNTAzBgNVBAMM\nLEF1dG9yaWRhZGUgQ2VydGlmaWNhZG9yYSBSYWl6IEJyYXNpbGVpcmEgdjEwMIIC\nIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAk3AxKl1ZtP0pNyjChqO7qNkn\n+/sClZeqiV/Kd7KnnbkDbI2y3VWcUG7feCE/deIxot6GH6JXncRG794UZl+4doD0\nD0/cEwBd4DvrDSZm0RT40xhmYYOTxZDJxv+coTHdmsT5aNmSkktfjzYX4HQHh/7M\nem+kTOpT/3E4K6B7KVs9HkOT7nXx5yU1qYbVWqI0qpJM9mOTSFx8C9HiKcHvLCvt\n1ioXKPAmFuHPkayOcXP2MXeb+VRNjWKU4E+L2t5uZPKVx1M/9i1DztlLb4K8OfYg\nGaPDUSF1sxnoGk5qZHLleO6KjCpmuQepmgsBvxi2YNO7X2YUwQQx1AXNSolgtkAR\n5gt+1WzxhbFUhItQqlhqxgWHefLmiT5T/Ctz/P2v+zSO4efkkIzsi1iwD+ypZvM2\nlnIvB24RcSN6jzmCahLPX4CwjwIK6JsSoMVxIhpZHCguUP4LXqP8IWUZ6WgS/4zB\n7B9E0EICl2rM1PRy+6ulv+ZOW256e8a0pijUB+hXM1msUq9L92476FAAX8va3sP7\n+Uut94+bGHmubcTLImWUPrxNT7QyrvE3FyHicfiHioeFL2oV4cXTLZrEq2wS8R4P\nKPdSzNn5Z9e2uMEGYQaSNO+OwvVycpIhOBOqrm12wJ9ZhWKtM5UOo34/o37r5ZBI\nTYXAGbhqQDB9mWXwH+0CAwEAAaOB9jCB8zBOBgNVHSAERzBFMEMGBWBMAQEAMDow\nOAYIKwYBBQUHAgEWLGh0dHA6Ly9hY3JhaXouaWNwYnJhc2lsLmdvdi5ici9EUENh\nY3JhaXoucGRmMEAGA1UdHwQ5MDcwNaAzoDGGL2h0dHA6Ly9hY3JhaXouaWNwYnJh\nc2lsLmdvdi5ici9MQ1JhY3JhaXp2MTAuY3JsMB8GA1UdIwQYMBaAFHTzfv/8n1N6\n8Xzrqz6kptoYukVjMB0GA1UdDgQWBBR0837//J9TevF866s+pKbaGLpFYzAPBgNV\nHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQ0FAAOCAgEA\neCNhBSuy/Ih/T+1VOtAJju85SrtoE3vET1qXASpmjQllDHG/ph7VFNRAkC+gha+B\nCbjoA5oJ/8wwl+Qdp1KGz6nXXFTLx3osU+kjm0srmBf9nyXHPqvFyvBeB0A7sYb7\nTmII9GKD20oCxsdkccR/oE/JuTaNnGq0GYZ2aDb5v62uLi21Y6P9UBiTxZqQ4ojW\nET6kXNjlK238jpXv17FR8Sg3VusCvX7Q8eJkavvHHZDeWck2fSA+ycAc2JeL2Z0B\nMSxGWpH32WM9J8+6XqCJUXHiWEV0zCE8wDYiYC+047pTxQI/gB/FcU7jvylh98DJ\nkQPHd/Tp6Og3ynlDA9n9uBbxYHVRZs9vsZ/7xTFaxRe+zk8dhgKgZ/3RrcMFB570\n2t8LFbyuUE/kQVY6rZ0QJ9qMWQ7VPLRwRhiMeU3k8WDJb/tBbOXHBqldTbWyQ+mp\nMEDWhbrzE/IED82wAuO23Tb05cYk2xC7+Izef8fSc3XdJDuPSbcDpWukzyCDtSEH\nisLiGEtIbYRiPsF3czlQPsnIEVoTTCWxHCH1zYR6zScSv18Qh69qVe2J40K5jZoP\nGEOhq/oKhVJQAdvAFW5Odp7mF3Tk9nivjjsctJSxY26LFiV5GRV+07SSse4ti0aO\njO5PLg5SWjfcOtBG2rz02EIvQAmLcb0kGBtfdj0lW/w=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFrjCCA5agAwIBAgIQTU0GyxRpCYdFVPhZfRsTHzANBgkqhkiG9w0BAQwFADBo\nMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTkw\nNwYDVQQDEzBNaWNyb3NvZnQgRVYgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9y\naXR5IDIwMTcwHhcNMTkxMjE4MjE1OTU1WhcNNDIwNzE4MjIwOTA5WjBoMQswCQYD\nVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTkwNwYDVQQD\nEzBNaWNyb3NvZnQgRVYgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw\nMTcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnBEElv+W+kRZBP974\ntJEqQz+ojQ/+yJoYdk5gKLn2wHFl1ZWUJ0tNbWz3UyDWoejCgDZgsk00WGAn31g0\nn//s3oer16r86HNpME/ETik0DX1EnUrTdW7nwiH9Wt2HpytMyvZ4pIYmAquN3YlA\na3bp9rXaYRWHQ121o5OCR7FSoeqXusIF3iHhkHsnnDc1Asv/6xcAh5ZsRDT+57hQ\nu/lGT5oxj9hUX0bwXsolRDXkVfyUZElNLf4SJvd+chZnmP6SpQM/cFksZF1qY52R\nvyM7SlWascEkJY1OsB/1Mcgblc5QzEvRLGQYlNEoZ4sKpNkbGJuvqkfGJp4KMTVa\nxjmjLN2bQ4/69I31I8cHT4lpmtYDbdNjWSDoM5lZsFAp3InvUVRaqNjuLH2q2YmS\nYbj/qksMvFP7JkbdUZ08XGAJwAFrWa2DVltT+6CrJq3M2f8+eC47a/NFwQPowpNc\n/pULklyLnLSz3DpGrAC4JwkKPjXuQwbvGpQIKeZR8eMu+kTjJc2LF/vGywr6DLIe\n9G4OC4A76V8CiYu3NL4kjrw961XJsZ0rJTHK/Pct5msBdo7smgDXboc+vBK4JcvJ\nYQ+FGBGrJ4NMSS4s/Fexx+nCB87rt7XD3ZbvBxLQ3v6EwOeOd/PReLA2XuSALf7a\n7VIFAql1sYNS4JfynxdCP+7aawIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYD\nVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUQcr/FrIJTtwkyEvkXBYlmfgm7zswEAYJ\nKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAC1YTkfwguUdJ8YbuW3t\nX1mCYhL6dtmBHxLZnLeD3FXZqByzi1fFOfDYilOsAfQCwSmQ/6Nq+Hednq2lqa6v\nnYI9DZieatYYmzsZ8UFERMg2J+b46a4L+todIk0EQgw0VOA/KWxA+NAfDIZYaA/E\nrhnSj9y7a20ineb2OhYNAfiIyx5OnH51GsV1+xbhGMZ7QqF+EnlVyj7bWYoz6kSi\nW44lq0p9cGFu+pVe4AkvSKUpav7u6ZpRw7wgKvQio0YtiRKNX4TklUIQTcIXpN9L\n5ictFU1zwIgmsva2FLC5hyygu8NwG7csQNNrR944iyIv9Z2we3w/7A+IB8mdU4qp\nOR+ZiPuuzZa65vPnJzsJaeo5EwkWkQl+rK7c1687JNZMvajZcEinTT8QKkEcIbUO\nlHz/lRH0SqWFwJL2phWkjg5z56jkjLfT+11nZvR9JiQcwDI/VxmaE1Guvb8Ni5ov\nNJyhw1pxlqBJIEnMJQiyZ0NK7PAMZoHJMk5i/hadIXsfKeTKhZ1PdJUog1wGjrok\nMJB+eh6bMEolGD5LS/WTb5sIBBC8Lc7tHXfb8MenfbVDoqCjAImo4NBPff0RZlHn\n4/fCQT2Aj65qhHRld+35vpsCJvhkhEzfWHlrsGaE1q3UuwZIc8gpnSVNAuPf2sDm\nQJ6n4aMj2Bwn9Q/eyd3Sp9+5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICYDCCAeWgAwIBAgIQIq9OUsJgV5tEnB3eAcjjGTAKBggqhkjOPQQDAzBoMQsw\nCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTkwNwYD\nVQQDEzBNaWNyb3NvZnQgRVYgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5\nIDIwMTcwHhcNMTkxMjE4MjIyMjE2WhcNNDIwNzE4MjIzMTQzWjBoMQswCQYDVQQG\nEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTkwNwYDVQQDEzBN\naWNyb3NvZnQgRVYgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw\ndjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ03fnXAKvhJipfmjaLsLzJwAVzzNXWIcv2\nV/zPtYu95jSvILE9SarFZ1WC4e0w3WvtRct8LaGsrpcfP3ZgUX+syov/pZtB5dNK\n8iXVAdv9+2xOyZaj3fdTNCt3HjFWwr+jVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRj5/TgIgrMsBbPtD8G9YPw3/lbkTAQBgkr\nBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNpADBmAjEA3TGYWPADxBwh+2rbOUzD\nAnq/uihpq4VbMUHY1MPLetkIGBcb+TU8qn4nCY74Y0fdAjEA/KDz7stlJkG4kFf+\nU7+zxDvxU2jguTkFYMMbg+BWru/2RuJCYMC7U17GrFLHX4rt\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl\nMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw\nNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5\nIDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG\nEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N\naWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ\nNt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0\nZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1\nHLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm\ngGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ\njEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc\naDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG\nYaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6\nW6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K\nUGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH\n+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q\nW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/\nBAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC\nNxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC\nLgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC\ngMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6\ntZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh\nSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2\nTaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3\npvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR\nxpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp\nGWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9\ndOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN\nAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB\nRA+GsCyRxj3qrg+E\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw\nCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD\nVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw\nMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV\nUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy\nb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq\nhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR\nogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb\nhGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E\nBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3\nFQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV\nL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB\niudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG\nA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw\nFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx\nMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u\naXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b\nRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z\nYybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3\nQWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw\nyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+\nBlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ\nSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH\nr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0\n4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me\ndKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw\nq7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2\nnKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu\nH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA\nVC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC\nXtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd\n6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf\n+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi\nkvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7\nwry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB\nTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C\nMUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn\n4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I\naFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy\nqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFhDCCA2ygAwIBAgIQdlP+ufXH2+qLpHjUPj1r9jANBgkqhkiG9w0BAQwFADBc\nMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEyMDAGA1UE\nAxMpR2xvYmFsU2lnbiBDbGllbnQgQXV0aGVudGljYXRpb24gUm9vdCBSNDUwHhcN\nMjAwMzE4MDAwMDAwWhcNNDUwMzE4MDAwMDAwWjBcMQswCQYDVQQGEwJCRTEZMBcG\nA1UEChMQR2xvYmFsU2lnbiBudi1zYTEyMDAGA1UEAxMpR2xvYmFsU2lnbiBDbGll\nbnQgQXV0aGVudGljYXRpb24gUm9vdCBSNDUwggIiMA0GCSqGSIb3DQEBAQUAA4IC\nDwAwggIKAoICAQC+PrPi5LejQfhLmafaJmRr7a5Jg1F9bGDgnwvvOzrGtOhJO81t\npD4a1cpj6oN3AOJavVZsfIHB8NvmWtGbfW0ilijsmuO6t122ET7kesa4Gs8FIeko\nN2X05Mmt5l0kL0iGPt9vFc4qsqVe3JUEkuV4JvjfXDXhv4ZTZZPLGJjj2ewyDcoK\n8P9VeTgfXcyd7c4VtlifTlrgsdNJFBisCGDmz8N9Io5vJnlDcWbmR4+ENqZsAFJ2\ntERfGu8ixAY2guMcVpo9UvdTBFEoINGzdC0tYjcpw2S45fqp9UCl/msU4f1zGZoh\nI7HnzIajHCRItWw8IX8XU+lkriUXLPa7RJ44Z+9Ju1ty0xXdNRMfVUajRkmagvXP\nfNHseYLOSCvdVvoZrSW4i7Zw14Kj5z2vbkGmPWDOeU9qxMkmOUS9Aa8dYXH29fE1\nRiceAxngMXlscVHfw3ZlIpUe02tpvBBZGJFX4p9i6QuOtoeP4b+DzUpYshDd7uP8\nDxwBYH72OGpccrl5Hd3XQ0cd7u3v/Mis+1Ihf4OGa7zu6XZ+VQt8nt5kREQUrqrn\nJSowNhrxJ0Pwrf6jRddHyYF2IlzOjv3qDkEjuPjE9s1ljMt2mjytaoHEUb6tlA2M\nF5EoASwechJUUUKk6ywPlFQsJTuTwzGGZIahbEjmvVBWzFCnashetvqFrwIDAQAB\no0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU\ndYKqG7azjRmP/Kl5zD8CmvRPy9kwDQYJKoZIhvcNAQEMBQADggIBAAFMXi+4f3I1\nvLUYMIB9N3yRb3r0PK0gVhu4qTP3/qhcVFg5VTwz0Hq5NPyNVg3uAaYnG3EvtZp3\nRYcE2I9bA3IDOSQdD3iQxcb1+H/6kKkiGw1nxrZSUPSdqOmgxHV6k9qxpWrtDEfO\noE6qcrTE5593kWX2awznDQdhCoevRhDV1ACrtbruRdFn5vd4n/l6wsennGwLXQ7F\nyz/6I9G7n+o1Asg3NUEfmt0cRLqASoDZTgmV0j6yMJI0nO2dID8TDec2vQpRDMNq\nV4rp2V2votwv1Za8xwjov6IV61QzYeVtzz31iZDiTY+cQL8Ug/KkNnol3njRCY2e\nhQevcgRUIV0n7eVCEcs61mOs79L7fWrKhIHjCjJbkMDEjZKsCEsK39dW3NtmjHJe\nPchOl6vLAaC2mLNXgDHvEU5AgmILem7K9SV7Wf/jvp/+/OpA6RogYKyGS6DBqUqx\nqtTyM/4TObvvrhf5NssQ+3e64ulbA4fxaNzHlhVZ8jhUB0//AtQ48HBooCemDmQR\nKom2nr2CykmaRxG8u5h200NwxYhZ/M7nyxAhelShHb3N9+FOsxct6yTGx0pc2pgj\ni7Jl0l/HfPkqK6VeDVBy1a7c+0iLhWcyQIF+CvIJTXicyU1ozvrhsfzZQf7mCfEi\nksRCXNTngVc4/6oai4r3z4f34t95em4E\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICIjCCAamgAwIBAgIQdlP+rhgmQ29p9RzCdxbyXjAKBggqhkjOPQQDAzBTMQsw\nCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEpMCcGA1UEAxMg\nR2xvYmFsU2lnbiBDb2RlIFNpZ25pbmcgUm9vdCBFNDUwHhcNMjAwMzE4MDAwMDAw\nWhcNNDUwMzE4MDAwMDAwWjBTMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFs\nU2lnbiBudi1zYTEpMCcGA1UEAxMgR2xvYmFsU2lnbiBDb2RlIFNpZ25pbmcgUm9v\ndCBFNDUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAR2GW0DtfWEI6syai5h3YQlL+/o\neSeJg8ODdfO2eGoIbaKtISoCkAbsmkCceoaRuViFyCiaLgv34nap37K9qcPpKRl5\nCLJQ0MLFnQphDONdNwZKXP6EvcCAhPpLVSPg4j6jQjBAMA4GA1UdDwEB/wQEAwIB\nhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSnn93TVM3b+Gy/JmwO5Ndbb4DM\nQjAKBggqhkjOPQQDAwNnADBkAjBsjFa2xTeuLZAreO2xHkYI0sNKKO94GQiOJDRG\nT4dxYV+pEUpvMqsc0VJ7qjrq5ZoCMFUrdy/O+D+baEra16hLRQ1+smv2bNqxFeK8\nSBl3i1fBXRTXQQDMJlLQILgZT5bnmg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFcjCCA1qgAwIBAgIQdlP+uT3Z5+kmMqzWCr6sODANBgkqhkiG9w0BAQwFADBT\nMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEpMCcGA1UE\nAxMgR2xvYmFsU2lnbiBUaW1lc3RhbXBpbmcgUm9vdCBSNDUwHhcNMjAwMzE4MDAw\nMDAwWhcNNDUwMzE4MDAwMDAwWjBTMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xv\nYmFsU2lnbiBudi1zYTEpMCcGA1UEAxMgR2xvYmFsU2lnbiBUaW1lc3RhbXBpbmcg\nUm9vdCBSNDUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6dDPsJ9wS\nOCEbxdNhKNZavE/fi8yRhEMkV7xkIbw7HB89T4ytB7fzxdcC6REUgpqqtJRyO3EN\nGu9oa4V5jq9m6liYDbrBfHnS/82zbzFF0AV0BAByaid+uDc/Oojtl4P1qzVND59Z\nO/Uv31nFfKUydmCWyO3u+AR+GVFyqL9EQXq8ex47AJu8uuCWv5D+jZvDcosAEvgg\nOmA498HMhYr7h3kuoSsg5sughZEjtsQoB1Qo3uwQMU+K8s0UHx7dVRzqKDFM+SFq\nqM3zlmf6AUGbzQ8LaH+73vFD6hflsNxwIrNpNll0a8bliSp85QuBXas/j7jRdnLz\nfKKp4pdBv8yMRf5hyfZsBwsABOgVI0+CKi3278P6ETZIodH9ejk6NF2jLA6bd1Ag\nNEDdsQMxrV/pYodzlgNh95Sw2VxsT+cUxeHxew0jnM1wjB1q3kotiyq720IUBQeq\n+xTcMdP2H2zLvmhmRHBNbRf5cesFc46RknXraFwe9kRhGCli3RdmiOwouklv2z53\n/rkxH3UcGKKmR73Y7kiFO/2z4g8/KpjGmvqCb7GlpYYdWjr6pGx0D3dSYWp/hyne\nOZuL7rNFYDAklxUSKoUwkyaslqYt6HBtC6kyrSybKAp2QvJVYVGYlN7t9sUXbzwV\nELAOrbDexRb0ZdHML1pWCM+ZxPBVkcIseQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC\nAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQURrIcd+F7FfClOaFw3tHELupt\nst4wDQYJKoZIhvcNAQEMBQADggIBABZ8CmdKAzyTKj4cT0ZVsJqeiOHrU/1MVXmE\n+Wy2n6kKtomrVAcUFkpJWvS4LoYUxH4ZifmHiLzsstKVjOAM+fKUZqaYVxuh39Fx\nfYy1+HEC3RO2vvqwMcMsZ+saGA4aTdwszzFcYSipneNqLK5QSw460Gn7ijRE335L\njhqQCdox2sovpff0Nyw1DRpizTx7PFZ3ZZVclHNwn2EvaWQjHUx5B8IXfDrtqm1x\nAxRiRcy3PlTYUXFC6juSQqUvVIGjsAxWWFa75JjuZscR+ahFF+JlKore4qjOxS32\n9c6t8OMKCd1Te2ypbIZ+od42NQAPX4D9RbtxZkPURCzQuwFOmZ4+TeFeVh8FeoId\nssstpTO5OeXEt9pC4b3QlEKA+hiUO5NDqMiUOm1+nfxPoMLT5aWqECZvBiJb4AHi\nSr8Z5USesK2rGdLN60fEYoHs8MJ6jUz9wiW3vCxwjqqtUvQUPKp4HQTTydUlgqda\ny4x8H1cCO4cbyNf5VBodyhpLJ7HiSu/nmkAUT6U8n9WjvpQ1nMLXPyjupBcrQ71k\np9ev6VPnp3cexRIbMeJLxn+eHO6jOpRQXaZQBlJeRQMrtADgwe3YDcGuu0kJgYJa\nQkOvmWO4FNE8i93V8FTtcmfC9so+NYSHgA1SlVBB1rINGUAvthNN97Fg1HbFVzlu\nWqJeCnnc\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFcDCCA1igAwIBAgIQdlP+qExQq5+NMrUdA49X3DANBgkqhkiG9w0BAQwFADBS\nMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEoMCYGA1UE\nAxMfR2xvYmFsU2lnbiBTZWN1cmUgTWFpbCBSb290IFI0NTAeFw0yMDAzMTgwMDAw\nMDBaFw00NTAzMTgwMDAwMDBaMFIxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i\nYWxTaWduIG52LXNhMSgwJgYDVQQDEx9HbG9iYWxTaWduIFNlY3VyZSBNYWlsIFJv\nb3QgUjQ1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3HnMbQb5bbvg\nVgRsf+B1zC0FSehL3FTsW3eVcr9/Yp2FqYokUF9T5dt0b6QpWxMqCa2axS/C93Y7\noUVGqkPmJP4rsG8ycBlGWnkmL/w9fV9ky1fMYWGo2ZVu45Wgbn9HEhjW7wPJ+4r6\nmr2CFalVd0sRT1nga8Nx8wzYVNWBaD4TuRUuh4o8RCc2YiRu+CwFcjBhvUKRI8Sd\nJafZVJoUozGtgHkMp2NsmKOsV0czH2WW4dDSNdr5cfehpiW1QV3fPmDY0fafpfK4\nzBOqj/mybuGDLZPdPoUa3eixXCYBy0mF/PzS1H+FYoZ0+cvsNSKiDDCPO6t561by\n+kLz7fkfRYlAKa3qknTqUv1WtCvaou11wm6rzlKQS/be8EmPmkjUiBltRebMjLnd\nZGBgAkD4uc+8WOs9hbnGCtOcB2aPxxg5I0bhPB6jL1Bhkgs9K2zxo0c4V5GrDY/G\nnU0E0iZSXOWl/SotFioBaeepfeE2t7Eqxdmxjb25i87Mi6E+C0jNUJU0xNgIWdhr\nJvS+9dQiFwBXya6bBDAznwv731aiyW5Udtqxl2InWQ8RiiIbZJY/qPG3JEqNPFN8\nbYN2PbImSHP1RBYBLQkqjhaWUNBzBl27IkiCTApGWj+A/1zy8pqsLAjg1urwEjiB\nT6YQ7UarzBacC89kppkChURnRq39TecCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgGG\nMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKCTFShu7o8IsjXGnmJ5dKexDit7\nMA0GCSqGSIb3DQEBDAUAA4ICAQBFCvjRXKxigdAE17b/V1GJCwzL3iRlN/urnu1m\n9OoMGWmJuBmxMFa02fb3vsaul8tF9hGMOjBkTMGfWcBGQggGR2QXeOCVBwbWjKKs\nqdk/03tWT/zEhyjftisWI8CfH1vj1kReIk8jBIw1FrV5B4ZcL5fi9ghkptzbqIrj\npHt3DdEpkyggtFOjS05f3sH2dSP8Hzx4T3AxeC+iNVRxBKzIxG3D9pGx/s3uRG6B\n9kDFPioBv6tMsQM/DRHkD9Ik4yKIm59fRz1RSeAJN34XITF2t2dxSChLJdcQ6J9h\nWRbFPjJOHwzOo8wP5McRByIvOAjdW5frQmxZmpruetCd38XbCUMuCqoZPWvoajB6\nV+a/s2o5qY/j8U9laLa9nyiPoRZaCVA6Mi4dL0QRQqYA5jGY/y2hD+akYFbPedey\nTtew+m4MVyPHzh+lsUxtGUmeDn9wj3E/WCifdd1h4Dq3Obbul9Q1UfuLSWDIPGau\nl+6NJllXu3jwelAwCbBgqp9O3Mk+HjrcYpMzsDpUdG8sMUXRaxEyamh29j32ahNe\nJJjn6h2az3iCB2D3TRDTgZpFjZ6vm9yAx0OylWikww7oCkcVv1Qz3AHn1aYec9h6\nsr8vreNVMJ7fDkG84BH1oQyoIuHjAKNOcHyS4wTRekKKdZBZ45vRTKJkvXN5m2/y\ns8H2PA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFejCCA2KgAwIBAgIQdlP+sEyg1XHyFLOOLH8XQTANBgkqhkiG9w0BAQwFADBX\nMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEtMCsGA1UE\nAxMkR2xvYmFsU2lnbiBEb2N1bWVudCBTaWduaW5nIFJvb3QgUjQ1MB4XDTIwMDMx\nODAwMDAwMFoXDTQ1MDMxODAwMDAwMFowVzELMAkGA1UEBhMCQkUxGTAXBgNVBAoT\nEEdsb2JhbFNpZ24gbnYtc2ExLTArBgNVBAMTJEdsb2JhbFNpZ24gRG9jdW1lbnQg\nU2lnbmluZyBSb290IFI0NTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\nAKPQGKqmJaBoxSoYFVYt/dBLfaEecm4xsZ0STDc8LAzKutUukiBLkultAJxEbzgX\n7xlg8skghJR6OwgNa0hl/NAeJPXU3NpHUphO342nitTllKh8siw4i+XSLZwAGTM3\nirhsZWIblOjjm6R1ay2AGh0b5i+n7HHq6wQPsanAk1JhIC29UptoWDRLa0tbPm1y\n1jjYlUGTTnn9T9W1/MiApVkIN+iyet62eQxB4PFg1i7y5KFN2BOrz45kW3zc5jEp\nHg2Qtjjo0PY6TTDHePklFWfhz3/3k5B/3kD6aYt9oENfRfnCS5d/UWEuC2LOYNoN\nX3bMlJwd2IXs70V+vuoq0D8UjWkgfgxW/epp9KlEweatJ/9Ycah9LzufHn/ZcgXo\nkSSAGtQheY4uWvr5j7AQKDCNquDyk9s9cVGrs553LgaAN4oLTg+YejcboM1JpUEQ\nhMOfUG0vKI4u88+2x1SBbiychxEN7eP1hIsr/hSQu0ooVDRMZ/viKnN2JpFfx9o/\nNp/aJy8nDcDHOf7b4/k2aYKAvfXB8aAz7od2H4gJft3oQbS+DxCkBuXt4Qh7JfdH\nB7wqJQ8xOpGoqhMzkK8Op2DWgn1nTTQW4We7eeuCMEa0APhZuw78sxCRRSPY8TFC\nBLFgZ6hjg7KsP5/3GBiETFGFZpoqHNLbKbmbG0Ma6jPtAgMBAAGjQjBAMA4GA1Ud\nDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQHQVdLz+EcFlPV\nveuDbMyLKSGEvzANBgkqhkiG9w0BAQwFAAOCAgEAJJwyIaZykDsC3f64SqaO8Dew\nW/8uP7Enbtl+nvSPX36/u4OFcMSKj0ZdxgpRKQLIxqBD/cICE/I6IZLdRpXDdLg8\nVyIBhGhns1Beem4spPSj9QsM+VoNR4VFGk+bTNGokfOJqj5JqvWEsRe0S+ZeaRT9\nRBsK/yDOCP70ZXKtxSJc3PKljMXcHWzb95anN2oaMLxrWTDjDUjxuGS5F5XG5J+D\nprLujbvhniXMwFaoAQeRa6Qu6hPr2/FJb+U7OpYn/kRQ4Qw0qxgQwaZwieJSyB2/\nYtY0guX+x5gAYRCAdyd8rF1yQrgiD3Ig9wpH0FUGVU/vZG2z/DrgoVZPZ8lFVMQT\nIfurtfoxGlsGaU463x4gvCB/sCt0MtaodrM6PgseIETeh6b3UgsLjxT4MQOq6hHJ\n2ZVGwIS72OsrLwpQxDgjf2+zv8Mnt/VMhwFzSQflwIyt7MeBQo/bXWsO2yHystfX\nkieXNu3GS19zR7kMuA3cSUtFsr8xjuFVhCfpWBoxwg4m01/Ri70gXXHfl2Hd35XJ\n4Msv20ScC3QKfRuKtE+MKJZM6CnLilxY8bg9bsLd2myyB6mr6NHR0niwPtPFaY13\n54Rk+LFW8fsZ0Yhmbz0bZcglRTwfdDseHDjr8aMsUsG/6CH0Lo4yg58V6vQNo5RH\nRn7JhIJYRobXTF+4bZk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICITCCAaegAwIBAgIQdlP+qicdlUZd1vGe5biQCjAKBggqhkjOPQQDAzBSMQsw\nCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEoMCYGA1UEAxMf\nR2xvYmFsU2lnbiBTZWN1cmUgTWFpbCBSb290IEU0NTAeFw0yMDAzMTgwMDAwMDBa\nFw00NTAzMTgwMDAwMDBaMFIxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxT\naWduIG52LXNhMSgwJgYDVQQDEx9HbG9iYWxTaWduIFNlY3VyZSBNYWlsIFJvb3Qg\nRTQ1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+XmLgUc3iZY/RUlQfxomC5Myfi7A\nwKcImsNuj5s+CyLsN1O3b4qwvCc3S22pRjvZH/+loUS7LXO/nkEHXFObUQg6Wrtv\nOMcWkXjCShNpHYLfWi8AiJaiLhx0+Z1+ZjeKo0IwQDAOBgNVHQ8BAf8EBAMCAYYw\nDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3xNei1/CQAL9VreUTLYe1aaxFJYw\nCgYIKoZIzj0EAwMDaAAwZQIwE7C+13EgPuSrnM42En1fTB8qtWlFM1/TLVqy5IjH\n3go2QjJ5naZruuH5RCp7isMSAjEAoGYcToedh8ntmUwbCu4tYMM3xx3NtXKw2cbv\nvPL/P/BS3QjnqmR5w+RpV5EvpMt8\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFcjCCA1qgAwIBAgIQdlP+rHVGSJP15ddKSDpO+DANBgkqhkiG9w0BAQwFADBT\nMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEpMCcGA1UE\nAxMgR2xvYmFsU2lnbiBDb2RlIFNpZ25pbmcgUm9vdCBSNDUwHhcNMjAwMzE4MDAw\nMDAwWhcNNDUwMzE4MDAwMDAwWjBTMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xv\nYmFsU2lnbiBudi1zYTEpMCcGA1UEAxMgR2xvYmFsU2lnbiBDb2RlIFNpZ25pbmcg\nUm9vdCBSNDUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2LcUw3Xro\nq5A9A3KwOkuZFmGy5f+lZx03HOV+7JODqoT1o0ObmEWKuGNXXZsAiAQl6fhokkuC\n2EvJSgPzqH9qj4phJ72hRND99T8iwqNPkY2zBbIogpFd+1mIBQuXBsKY+CynMyTu\nUDpBzPCgsHsdTdKoWDiW6d/5G5G7ixAs0sdDHaIJdKGAr3vmMwoMWWuOvPSrWpd7\nf65V+4TwgP6ETNfiur3EdaFvvWEQdESymAfidKv/aNxsJj7pH+XgBIetMNMMjQN8\nVbgWcFwkeCAl62dniKu6TjSYa3AR3jjK1L6hwJzh3x4CAdg74WdDhLbP/HS3L4Sj\nv7oJNz1nbLFFXBlhq0GD9awd63cNRkdzzr+9lZXtnSuIEP76WOinV+Gzz6ha6Qcl\nmxLEnoByPZPcjJTfO0TmJoD80sMD8IwM0kXWLuePmJ7mBO5Cbmd+QhZxYucE+WDG\nZKG2nIEhTivGbWiUhsaZdHNnMXqR8tSMeW58prt+Rm9NxYUSK8+aIkQIqIU3zgdh\nVwYXEiTAxDFzoZg1V0d+EDpF2S2kUZCYqaAHN8RlGqocaxZ396eX7D8ZMJlvMfvq\nQLLn0sT6ydDwUHZ0WfqNbRcyvvjpfgP054d1mtRKkSyFAxMCK0KA8olqNs/ITKDO\nnvjLja0Wp9Pe1ZsYp8aSOvGCY/EuDiRk3wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC\nAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUHwC/RoAK/Hg5t6W0Q9lWULvO\nljswDQYJKoZIhvcNAQEMBQADggIBAF4runSXNERfdkgoQIST7gFu6aGz1oAl5nvk\nvAmRPQ/8dq3X1DAgu49g0JHWHPKc73gaK5QyAsEkllJSAtDz0fzymzlumeEfjkNB\nfZoeW8ldmoT8JuaH83RyJq2kG9k9O2pSoDwJHi8ee7MztEXH96yxr5NgrXauuLIV\neOuDauv/20arJOXuAvqQH1nAL13Wt12kXBC3clP4QU7M+ngaJUrK/oViQ2HDtDeq\ngdL01joPvY1ZfjBH3itr5yFQM1/UZ5vUuGefPCeZA/+FQ45zEsogzehh1bFm3BfW\nOW0P288jN6GCiU4caz/WoM2qB50+Qiaq1wzu+ke/GlJ+0XWB08mKYhdtT4igIaAm\nPq9t2WIwH+mYKK5ujdWOTHJmk4CNKuNVx2BnkEJWXCJRD7PcTjnuTd3ZHXgQVDtu\n0JdvA7UesiNzxhKymmTQ/JWFJKj/36Gw3JFArt8JM6u53ZK38cyRdDtp62eXG5C/\n58egb3G7V7+3j1rtekBqFs2AhC0v4QLUJJRDsxX8DCsb/XFv/Mu8dRc6XoPSybMv\nG9WcjX9U/n5+5Fajh6ed4VlSlEGPbVu+hpWa/xp23UDSUUpwtB8zYyN3P+wnHlnk\nCIftNIJKDz/+oB3B9WdzRYZ49Kop6SeHxhnbxhMUwzlJh02gl+BlE/Wdd1bp2rNY\nxzrywM2C\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICNDCCAbugAwIBAgIQdlP+urId2CfpaRai64G+WDAKBggqhkjOPQQDAzBcMQsw\nCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEyMDAGA1UEAxMp\nR2xvYmFsU2lnbiBDbGllbnQgQXV0aGVudGljYXRpb24gUm9vdCBFNDUwHhcNMjAw\nMzE4MDAwMDAwWhcNNDUwMzE4MDAwMDAwWjBcMQswCQYDVQQGEwJCRTEZMBcGA1UE\nChMQR2xvYmFsU2lnbiBudi1zYTEyMDAGA1UEAxMpR2xvYmFsU2lnbiBDbGllbnQg\nQXV0aGVudGljYXRpb24gUm9vdCBFNDUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATM\nzLQ6uxpN+J2RxHeB7RZ/AxF/uOlwhEiWQQmDYF30JJMqMh5eB/tHpIcqJNhXjFzZ\nqN8ReH+2RNXdr9UB2SY0X30xyMHu49a5/o+TAnCib2A7GXO1i3QKe51CF7wtPqej\nQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS1\ng9ZwBorGnYaCd9WpBWU2E/HGSDAKBggqhkjOPQQDAwNnADBkAjBmpdF/fTQJFg4O\n++53h4FKndiAh6BkaMtftnRYrMuymOKSEoktHT2xVGj4kvGNTkoCMBRVMnt2ZnSR\nayTUWpTi5WqA9np9zULzWHhjwekCe1TdHAEVncu/BBhVQCT6IvLZXg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICLDCCAbGgAwIBAgIQdlP+sK9LdZCiGuSi1fJ2tTAKBggqhkjOPQQDAzBXMQsw\nCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEtMCsGA1UEAxMk\nR2xvYmFsU2lnbiBEb2N1bWVudCBTaWduaW5nIFJvb3QgRTQ1MB4XDTIwMDMxODAw\nMDAwMFoXDTQ1MDMxODAwMDAwMFowVzELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEds\nb2JhbFNpZ24gbnYtc2ExLTArBgNVBAMTJEdsb2JhbFNpZ24gRG9jdW1lbnQgU2ln\nbmluZyBSb290IEU0NTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIblQ9C7AGVe1koK\nY4WeRQ+GIzJQVUljapzO96/0fiD5gDJbbrDv8sekLPtqWZAGdrcXjA51RDqAfMjc\nAj3yzqGes0tyy8aM/cLJqoyuM1zqeUvcachWpDwoQXB0jmoaSKNCMEAwDgYDVR0P\nAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFGGZArQQ/xA823ra\nbDpwJANg8eeOMAoGCCqGSM49BAMDA2kAMGYCMQCP9ck/sU7z99GdtLoPPQqXJxCT\n8lB8IonajNTKqWMkJiqLY4JjVMc08NGeehgLp+oCMQCxNY9K8vsmBsHTDY9i0bDE\noF3pk9ZhxOGhuVyo9fFnXqIpN8JLxmdy/oyQ+SSAd7c=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw\nCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg\nR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00\nMDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT\nZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw\nEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW\n+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9\nItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T\nAQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI\nzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW\ntL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1\n/q4AaOeMSQ+2b1tbFfLn\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw\nCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp\nZ2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2\nMDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ\nbmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG\nByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS\n7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp\n0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS\nB4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49\nBAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ\nLgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4\nDXZDjC5Ty3zfDBeWUA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN\nMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT\nHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN\nNDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs\nIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+\najWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0\n2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp\nwgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM\npG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD\nnU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po\nsMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx\nZre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd\nLvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX\nKyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe\nXoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL\ntgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv\nTiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN\nAQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw\nGXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H\nPNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF\nO4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ\nREtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik\nAdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv\n/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+\np6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw\nMUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF\nqUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK\novfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw\nCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T\nZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN\nMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG\nA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT\nZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC\nWvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+\n6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B\nAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa\nqCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q\n4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf\nMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD\nEy1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw\nHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY\nMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp\nYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa\nef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz\nSDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf\niOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X\nME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3\nIuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS\nVYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE\nSJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu\n+Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt\n8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L\nHaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt\nzwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P\nAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c\nmTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ\nYKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52\ngDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA\nFv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB\nJYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX\nDhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui\nTdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5\ndHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65\nLvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp\n0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY\nQqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGlTCCBH2gAwIBAgIRANJ/u8HeNZ5SFq1hSVhgmcQwDQYJKoZIhvcNAQEMBQAw\ngYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK\nZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD\nVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIx\nMDMyMjAwMDAwMFoXDTM4MDExODIzNTk1OVowXzELMAkGA1UEBhMCR0IxGDAWBgNV\nBAoTD1NlY3RpZ28gTGltaXRlZDE2MDQGA1UEAxMtU2VjdGlnbyBQdWJsaWMgU2Vy\ndmVyIEF1dGhlbnRpY2F0aW9uIFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOC\nAg8AMIICCgKCAgEAk77VNlJ12AEjoBxHQknuY7a3If3EldVIKyZ8FFMQ2nn9K7ct\npNQs+uoy3UnCub0PSD17WphUr55dMXRPB/xQId2kz2hPGxJjbSWZTCqZ80gwYfqB\nfB6nCErcPiscHxhMcao1jK34bug7StnllALWiYQTqm3ITzPMUJY3kjPcX4jnn1TZ\nSPCYQ9Zm/Z8XOEPFAVEL1+MjDxRdWxTnS77d9MjaAzfR1jmhIVEwg7Bt1zBOlluR\n8HAkq79FgWRDDb0hOi886Z4NyyC1QifM2m+b7mQwkDnNk2WBITG1I1AzNyLjOO34\nMTDMRf5i+dFdMnlCh99qzFYZQE3Oqrv5tXZJlPEn+JGlg+UGs2MOgNzgElWApjtm\ntDmHLcjw0NEU6eQNTQ72XVdyxTscR1ad4tX7gWGMzE2AkDRbt9cUddzYBEifwMEo\niLTpHMqnsfFWt3tJTFnlIBWohAIp+jiUaZpJBo/NH3kUFxIMg3reH7GX7vmXeCik\nyESS6X0mBaZYcpt5E9gRX67FOGI0aLKGMI74kGGeMmz1BzbNokxu7Io27fLmmRVE\ncMN8vJw5wLTha/eDJSNX2RKA5UnwdQ/vjescm1QotCE8/HwK/+97a3X/ix2gGQWr\n+vgrgULoOLq7+6r9PeDzyt9Ol5cp7fMYVumllqy9w5CYsuD5otSmR0N8bc8CAwEA\nAaOCASAwggEcMB8GA1UdIwQYMBaAFFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1Ud\nDgQWBBRWc1hklfmSGrASKgRieaFAFYghSTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T\nAQH/BAUwAwEB/zAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEQYDVR0g\nBAowCDAGBgRVHSAAMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRy\ndXN0LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDA1\nBggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVz\ndC5jb20wDQYJKoZIhvcNAQEMBQADggIBADpvBIlq7bMU0cFDT/9P9+BsgCkRgQs0\nS6Bf7vJSlWMHwby0VGvxCS0hrbi0K2BINZbEbsVsgpQq04431yyoVn3Hldorgq24\nRldRDOOipEZDTFB9wC9HYt1thHF00XeG2C8KC1plwoEzKAIhPvefI/C3cT0CfTXJ\nuFjUbKIgSwjNjw6YHtLgoy/hd5+JLUlLco/gzFX/qWbT7tEquOMYpsNKWZj8TLqP\nq6zMiG4Na6feEZte6YPXGrMWlTWN341vDedc+yxQqSug79HJUQcOZs7KyDWztmae\nQxsPE49UV/8XwrfZtZaYyrs4FpD94Z4Q8dzXGL8+qEJjxgcza7W6PROaClubavd1\nVKPm8+aCW77u7SxpR2TFGL6kPdxsKyFijpcunR5V79sUyROfNdzjrAcFWZXK8sbb\n9FlnwuVG677JLv+ZVTX5AxLvW5OB4zt5uS+zB62wJ/Wv+jXGAttSAcJec4iFgCWH\nRvdi/jJoSzRLa3nEzx6pFIzclSCnh0u1xCeLcUBypSiPga8W+6PkuoyQq8U9qs9E\noxG5NvrvlyshwUS9yvcZRGw7Ljlx4jJH/BhIPR8kIBCQj1vna9TziZOrw1Of8hDU\nbHKFG9Pm8Dp2vbjz/2JH39qvxshPKVllGfq+5klPm7yZRUYTiCMAbqwNdL/nsqF2\nRnnyp58XRStJ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw\nCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT\nU0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2\nMDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh\ndGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG\nByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm\nacCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN\nSeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME\nGDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW\nuCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp\n15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN\nb0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO\nMQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD\nDBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX\nDTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw\nb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC\nAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP\nL3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY\nt6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins\nS657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3\nPnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO\nL9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3\nR2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w\ndr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS\n+YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS\nd66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG\nAtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f\ngTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j\nBBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z\nNbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt\nhEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM\nQtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf\nR4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ\nDPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW\nP4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy\nlrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq\nbLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w\nAgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q\nr5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji\nMho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU\n98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "apps/checker/checker/dns.go",
    "content": "package checker\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/rs/zerolog/log\"\n)\n\n\ntype DnsResponse struct {\n\tA     []string `json:\"a,omitempty\"`\n\tAAAA  []string `json:\"aaaa,omitempty\"`\n\tCNAME string   `json:\"cname,omitempty\"`\n\tMX    []string `json:\"mx,omitempty\"`\n\tNS  []string `json:\"ns,omitempty\"`\n\tTXT []string `json:\"txt,omitempty\"`\n}\n\nfunc Dns(ctx context.Context, host string) (*DnsResponse, error) {\n\tlogger:= log.Ctx(ctx).With().Str(\"monitor\", host).Logger()\n\n\tips, err := net.LookupIP(host)\n\tif err != nil {\n\t\tlogger.Error().Err(err).Msg(\"DNS IP lookup failed\")\n\t\treturn nil, fmt.Errorf(\"failed to lookup IPs: %w\", err)\n\t}\n\n\tA := []string{}\n\tAAAA := []string{}\n\n\tfor _, ip := range ips {\n\t\tif ip.To4() != nil {\n\t\t\tA = append(A, ip.String())\n\t\t} else {\n\t\t\tAAAA = append(AAAA, ip.String())\n\t\t}\n\t}\n\tCNAME,err := lookupCNAME(host)\n\tif err != nil {\n\t\tlogger.Error().Err(err).Msg(\"DNS CNAME record lookup failed\")\n\t\treturn nil, fmt.Errorf(\"failed to lookup CNAME record: %w\", err)\n\t}\n\tMXRecords := lookupMX(host)\n\n\tNS,err  := lookupNS(host)\n\tif err != nil {\n\t\tlogger.Error().Err(err).Msg(\"DNS NS record lookup failed\")\n\t\treturn nil, fmt.Errorf(\"failed to lookup NS record: %w\", err)\n\t}\n\tTXT := lookupTXT(host)\n\n\n\tresponse := &DnsResponse{\n\t\tA:     A,\n\t\tAAAA:  AAAA,\n\t\tCNAME: CNAME,\n\t\tMX:    MXRecords,\n\t\tNS:    NS,\n\t\tTXT: TXT,\n\t}\n\n\treturn response, nil\n}\n\n\n\nfunc lookupCNAME(domain string) (string, error) {\n\tcname, err := net.LookupCNAME(domain)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn cname, nil\n}\n\nfunc lookupMX(domain string) ([]string) {\n\tmx := []string{}\n\tmxRecords,_ := net.LookupMX(domain)\n\n\n\tfor _, r := range mxRecords {\n\t\tmx = append(mx, fmt.Sprintf(\"%s:%d\", r.Host, r.Pref))\n\t}\n\treturn mx\n}\n\nfunc lookupNS(domain string) ([]string, error) {\n\n\thosts := []string{}\n\tisSubdomain := isSubdomain(domain)\n\tif isSubdomain {\n\t\treturn hosts, nil\n\t}\n\tnsRecords, err := net.LookupNS(domain)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, ns := range nsRecords {\n\t\thosts = append(hosts, ns.Host)\n\t}\n\treturn hosts, nil\n}\n\nfunc lookupTXT(domain string) ([]string) {\n\trecords := []string{}\n\ttxtRecords, err := net.LookupTXT(domain)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tfor _, txt := range txtRecords {\n\t\trecords = append(records, txt)\n\t}\n\treturn records\n}\n\n\nfunc isSubdomain(domain string) bool {\n\tparent := strings.Split(domain, \".\")\n\tif len(parent) < 3 {\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "apps/checker/checker/dns_test.go",
    "content": "package checker_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n)\n\n\nfunc TestPingDNS(t *testing.T) {\n\tctx := t.Context()\n\tdata, err := checker.Dns(ctx, \"openstat.us\")\n\tif err != nil {\n\t\tt.Errorf(\"Dns() error = %v\", err)\n\t}\n\tif len(data.A) == 0 {\n\t\tt.Errorf(\"Dns() A records = %v\", data.A)\n\t}\n}\n"
  },
  {
    "path": "apps/checker/checker/http.go",
    "content": "package checker\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptrace\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n\t\"github.com/rs/zerolog/log\"\n)\n\ntype Timing struct {\n\tDnsStart          int64 `json:\"dnsStart\"`\n\tDnsDone           int64 `json:\"dnsDone\"`\n\tConnectStart      int64 `json:\"connectStart\"`\n\tConnectDone       int64 `json:\"connectDone\"`\n\tTlsHandshakeStart int64 `json:\"tlsHandshakeStart\"`\n\tTlsHandshakeDone  int64 `json:\"tlsHandshakeDone\"`\n\tFirstByteStart    int64 `json:\"firstByteStart\"`\n\tFirstByteDone     int64 `json:\"firstByteDone\"`\n\tTransferStart     int64 `json:\"transferStart\"`\n\tTransferDone      int64 `json:\"transferDone\"`\n}\n\ntype Response struct {\n\tHeaders   map[string]string `json:\"headers,omitempty\"`\n\tBody      string            `json:\"body,omitempty\"`\n\tError     string            `json:\"error,omitempty\"`\n\tRegion    string            `json:\"region\"`\n\tJobType   string            `json:\"jobType\"`\n\tLatency   int64             `json:\"latency\"`\n\tTimestamp int64             `json:\"timestamp\"`\n\tStatus    int               `json:\"status,omitempty\"`\n\tTiming    Timing            `json:\"timing\"`\n}\n\n// decodeBase64Body decodes a data URL base64 body if needed\nfunc decodeBase64Body(body string) ([]byte, error) {\n\tdata := strings.Split(body, \",\")\n\tif len(data) == 2 {\n\t\treturn base64.StdEncoding.DecodeString(data[1])\n\t}\n\treturn nil, fmt.Errorf(\"invalid base64 data url format\")\n}\n\n// FIXME: This should only return the TCP Timing Data;\nfunc Http(ctx context.Context, client *http.Client, inputData request.HttpCheckerRequest) (Response, error) {\n\tlogger := log.Ctx(ctx).With().Str(\"monitor\", inputData.URL).Logger()\n\n\tvar bodyBytes []byte\n\tif inputData.Method == http.MethodPost {\n\t\tcontentType := \"\"\n\t\tfor _, header := range inputData.Headers {\n\t\t\tif header.Key == \"Content-Type\" {\n\t\t\t\tcontentType = header.Value\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif contentType == \"application/octet-stream\" {\n\t\t\tdecoded, err := decodeBase64Body(inputData.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn Response{}, fmt.Errorf(\"error while decoding base64: %w\", err)\n\t\t\t}\n\t\t\tbodyBytes = decoded\n\t\t} else {\n\t\t\tbodyBytes = []byte(inputData.Body)\n\t\t}\n\t} else {\n\t\tbodyBytes = []byte(inputData.Body)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, inputData.Method, inputData.URL, bytes.NewReader(bodyBytes))\n\tif err != nil {\n\t\tlogger.Error().Err(err).Msg(\"error while creating req\")\n\t\treturn Response{}, fmt.Errorf(\"unable to create req: %w\", err)\n\t}\n\treq.Header.Set(\"User-Agent\", \"OpenStatus/1.0\")\n\tfor _, header := range inputData.Headers {\n\t\tif header.Key != \"\" {\n\t\t\treq.Header.Set(header.Key, header.Value)\n\t\t}\n\t}\n\n\t// Maybe we should remove the default post to application JSON\n\t// Default POST Content-Type\n\tif inputData.Method == http.MethodPost && req.Header.Get(\"Content-Type\") == \"\" {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\ttiming := Timing{}\n\n\ttrace := &httptrace.ClientTrace{\n\t\tDNSStart:          func(_ httptrace.DNSStartInfo) { timing.DnsStart = time.Now().UTC().UnixMilli() },\n\t\tDNSDone:           func(_ httptrace.DNSDoneInfo) { timing.DnsDone = time.Now().UTC().UnixMilli() },\n\t\tConnectStart:      func(_, _ string) { timing.ConnectStart = time.Now().UTC().UnixMilli() },\n\t\tConnectDone:       func(_, _ string, _ error) { timing.ConnectDone = time.Now().UTC().UnixMilli() },\n\t\tTLSHandshakeStart: func() { timing.TlsHandshakeStart = time.Now().UTC().UnixMilli() },\n\t\tTLSHandshakeDone:  func(_ tls.ConnectionState, _ error) { timing.TlsHandshakeDone = time.Now().UTC().UnixMilli() },\n\t\tGotConn: func(_ httptrace.GotConnInfo) {\n\t\t\ttiming.FirstByteStart = time.Now().UTC().UnixMilli()\n\t\t},\n\t\tGotFirstResponseByte: func() {\n\t\t\ttiming.FirstByteDone = time.Now().UTC().UnixMilli()\n\t\t\ttiming.TransferStart = time.Now().UTC().UnixMilli()\n\t\t},\n\t}\n\n\treq = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))\n\n\tstart := time.Now()\n\n\tresponse, err := client.Do(req)\n\tlatency := time.Since(start).Milliseconds()\n\n\tif err != nil {\n\n\t\tvar urlErr *url.Error\n\t\tif errors.As(err, &urlErr) && urlErr.Timeout() {\n\t\t\treturn Response{\n\t\t\t\tLatency:   latency,\n\t\t\t\tTiming:    timing,\n\t\t\t\tTimestamp: start.UTC().UnixMilli(),\n\t\t\t\tError:     fmt.Sprintf(\"Timeout after %d ms\", latency),\n\t\t\t}, nil\n\t\t}\n\n\t\tlogger.Error().Err(err).Msg(\"error while pinging\")\n\n\t\treturn Response{}, err\n\t}\n\n\tdefer response.Body.Close()\n\n\tbody, err := io.ReadAll(response.Body)\n\n\ttiming.TransferDone = time.Now().UTC().UnixMilli()\n\n\tif err != nil {\n\t\treturn Response{\n\t\t\tLatency:   latency,\n\t\t\tTiming:    timing,\n\t\t\tTimestamp: start.UTC().UnixMilli(),\n\t\t\tError:     fmt.Sprintf(\"Cannot read response body: %s\", err.Error()),\n\t\t}, err\n\t}\n\n\theaders := make(map[string]string)\n\tfor key := range response.Header {\n\t\theaders[key] = response.Header.Get(key)\n\t}\n\n\treturn Response{\n\t\tTimestamp: start.UTC().UnixMilli(),\n\t\tStatus:    response.StatusCode,\n\t\tHeaders:   headers,\n\t\tTiming:    timing,\n\t\tLatency:   latency,\n\t\tBody:      string(body),\n\t}, nil\n\n}\n"
  },
  {
    "path": "apps/checker/checker/http_test.go",
    "content": "package checker_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n)\n\n// RoundTripFunc .\ntype RoundTripFunc func(req *http.Request) *http.Response\n\n// RoundTrip .\nfunc (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn f(req), nil\n}\n\n// NewTestClient returns *http.Client with Transport replaced to avoid making real calls\nfunc NewTestClient(fn RoundTripFunc) *http.Client {\n\treturn &http.Client{\n\t\tTransport: RoundTripFunc(fn),\n\t}\n}\n\nfunc Test_ping(t *testing.T) {\n\n\ttype args struct {\n\t\tclient    *http.Client\n\t\tinputData request.HttpCheckerRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    checker.Response\n\t\twantErr bool\n\t}{\n\t\t{name: \"200\", args: args{client: NewTestClient(func(req *http.Request) *http.Response {\n\t\t\treturn &http.Response{\n\t\t\t\tStatusCode: http.StatusOK,\n\t\t\t\tBody:       io.NopCloser(bytes.NewBufferString(`OK`)),\n\t\t\t\tHeader:     make(http.Header),\n\t\t\t}\n\t\t}), inputData: request.HttpCheckerRequest{URL: \"https://openstat.us\", CronTimestamp: 1, Headers: []struct {\n\t\t\tKey   string `json:\"key\"`\n\t\t\tValue string `json:\"value\"`\n\t\t}{{Key: \"\", Value: \"\"}}}}, want: checker.Response{Status: 200, Body: \"OK\"}, wantErr: false},\n\n\t\t{name: \"200 with headers\", args: args{client: NewTestClient(func(req *http.Request) *http.Response {\n\t\t\tassert.Equal(t, \"Value\", req.Header.Get(\"Test\"))\n\t\t\treturn &http.Response{\n\t\t\t\tStatusCode: http.StatusOK,\n\t\t\t\tBody:       io.NopCloser(bytes.NewBufferString(`OK`)),\n\t\t\t\tHeader:     make(http.Header),\n\t\t\t}\n\t\t}), inputData: request.HttpCheckerRequest{URL: \"https://openstat.us\", CronTimestamp: 1, Headers: []struct {\n\t\t\tKey   string `json:\"key\"`\n\t\t\tValue string `json:\"value\"`\n\t\t}{{Key: \"Test\", Value: \"Value\"}}}}, want: checker.Response{Status: 200, Body: \"OK\"}, wantErr: false},\n\n\t\t{name: \"500\", args: args{client: NewTestClient(func(req *http.Request) *http.Response {\n\t\t\treturn &http.Response{\n\t\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\t\tBody:       io.NopCloser(bytes.NewBufferString(`OK`)),\n\t\t\t\tHeader:     make(http.Header),\n\t\t\t}\n\t\t}), inputData: request.HttpCheckerRequest{URL: \"https://openstat.us/500\", CronTimestamp: 1}},\n\t\t\twant: checker.Response{Status: 500, Body: \"OK\"}, wantErr: false},\n\n\t\t{name: \"Wrong url should return an error\", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: \"https://somethingthatwillfail.ed\", CronTimestamp: 1}},\n\t\t\twant: checker.Response{Status: 0}, wantErr: true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := checker.Http(context.Background(), tt.args.client, tt.args.inputData)\n\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Ping() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif got.Status != tt.want.Status {\n\t\t\t\tt.Errorf(\"Ping() = %v, want %v\", got, tt.want)\n\t\t\t}\n\n\t\t\tif got.Body != tt.want.Body {\n\t\t\t\tt.Errorf(\"Ping() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "apps/checker/checker/tcp.go",
    "content": "package checker\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype TCPData struct {\n\tWorkspaceID string `json:\"workspaceId\"`\n\tMonitorID   string `json:\"monitorId\"`\n\tTimestamp   int64  `json:\"timestamp\"`\n}\n\ntype TCPResponseTiming struct {\n\tTCPStart int64 `json:\"tcpStart\"`\n\tTCPDone  int64 `json:\"tcpDone\"`\n}\n\ntype TCPResponse struct {\n\tRegion       string            `json:\"region\"`\n\tErrorMessage string            `json:\"errorMessage\"`\n\tJobType      string            `json:\"jobType\"`\n\tRequestId    int64             `json:\"requestId,omitempty\"`\n\tWorkspaceID  int64             `json:\"workspaceId\"`\n\tMonitorID    int64             `json:\"monitorId\"`\n\tTimestamp    int64             `json:\"timestamp\"`\n\tLatency      int64             `json:\"latency\"`\n\tTiming       TCPResponseTiming `json:\"timing\"`\n\tError        uint8             `json:\"error,omitempty\"`\n}\n\nfunc PingTCP(timeout int, url string) (TCPResponseTiming, error) {\n\tstart := time.Now().UTC().UnixMilli()\n\tconn, err := net.DialTimeout(\"tcp\", url, time.Duration(timeout)*time.Second)\n\tstop := time.Now().UTC().UnixMilli()\n\n\tif err != nil {\n\t\tif e := err.(*net.OpError).Timeout(); e {\n\t\t\treturn TCPResponseTiming{}, fmt.Errorf(\"timeout after %d ms\", timeout*1000)\n\t\t}\n\t\tif strings.Contains(err.Error(), \"connection refused\") {\n\t\t\treturn TCPResponseTiming{}, fmt.Errorf(\"connection refused\")\n\t\t}\n\t\treturn TCPResponseTiming{}, fmt.Errorf(\"dial error: %w\", err)\n\t}\n\tdefer conn.Close()\n\n\treturn TCPResponseTiming{TCPStart: start, TCPDone: stop}, nil\n}\n"
  },
  {
    "path": "apps/checker/checker/tcp_test.go",
    "content": "package checker_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n)\n\nfunc TestPingTcp(t *testing.T) {\n\ttype args struct {\n\t\turl     string\n\t\ttimeout int\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    checker.TCPResponseTiming\n\t\twantErr bool\n\t}{\n\t\t{name: \"will failed\", args: args{url: \"error\", timeout: 60}, wantErr: true},\n\t\t{name: \"will be ok\", args: args{url: \"openstat.us:443\", timeout: 60}, wantErr: false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := checker.PingTCP(tt.args.timeout, tt.args.url)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"PingTcp() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got.TCPStart == 0 && tt.wantErr == false {\n\t\t\t\tt.Errorf(\"PingTcp() = %v\", got)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got.TCPDone == 0 && tt.wantErr == false {\n\t\t\t\tt.Errorf(\"PingTcp() = %v\", got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "apps/checker/checker/update.go",
    "content": "package checker\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/api/option\"\n\n\t\"cloud.google.com/go/auth\"\n\tcloudtasks \"cloud.google.com/go/cloudtasks/apiv2\"\n\ttaskspb \"cloud.google.com/go/cloudtasks/apiv2/cloudtaskspb\"\n)\n\ntype UpdateData struct {\n\tMonitorId     string `json:\"monitorId\"`\n\tStatus        string `json:\"status\"`\n\tMessage       string `json:\"message,omitempty\"`\n\tRegion        string `json:\"region\"`\n\tCronTimestamp int64  `json:\"cronTimestamp\"`\n\tStatusCode    int    `json:\"statusCode,omitempty\"`\n\tLatency       int64  `json:\"latency,omitempty\"`\n}\n\nfunc UpdateStatus(ctx context.Context, updateData UpdateData) error {\n\n\turl := \"https://openstatus-workflows.fly.dev/updateStatus\"\n\tbasic := \"Basic \" + os.Getenv(\"CRON_SECRET\")\n\tpayloadBuf := new(bytes.Buffer)\n\tc := os.Getenv(\"GCP_PRIVATE_KEY\")\n\tc = strings.ReplaceAll(c, \"\\\\n\", \"\\n\")\n\topts := &auth.Options2LO{\n\t\tEmail:        os.Getenv(\"GCP_CLIENT_EMAIL\"),\n\t\tPrivateKey:   []byte(c),\n\t\tPrivateKeyID: os.Getenv(\"GCP_PRIVATE_KEY_ID\"),\n\t\tScopes: []string{\n\t\t\t\"https://www.googleapis.com/auth/cloud-platform\",\n\t\t},\n\t\tTokenURL: \"https://oauth2.googleapis.com/token\",\n\t}\n\n\ttp, err := auth.New2LOTokenProvider(opts)\n\tif err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"error while creating token provider\")\n\t\treturn err\n\t}\n\n\tcreds := auth.NewCredentials(&auth.CredentialsOptions{\n\t\tTokenProvider: tp,\n\t})\n\n\tclient, err := cloudtasks.NewClient(ctx, option.WithAuthCredentials(creds))\n\tif err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"error while creating cloud tasks client\")\n\n\t}\n\tdefer client.Close()\n\n\tif err := json.NewEncoder(payloadBuf).Encode(updateData); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"error while updating status\")\n\t\treturn err\n\t}\n\tprojectID := os.Getenv(\"GCP_PROJECT_ID\")\n\tqueuePath := fmt.Sprintf(\"projects/%s/locations/europe-west1/queues/alerting\", projectID)\n\treq := &taskspb.CreateTaskRequest{\n\t\tParent: queuePath,\n\t\tTask: &taskspb.Task{\n\t\t\t// https://godoc.org/google.golang.org/genproto/googleapis/cloud/tasks/v2#HttpRequest\n\t\t\tMessageType: &taskspb.Task_HttpRequest{\n\t\t\t\tHttpRequest: &taskspb.HttpRequest{\n\t\t\t\t\tHttpMethod: taskspb.HttpMethod_POST,\n\t\t\t\t\tUrl:        url,\n\t\t\t\t\tHeaders:    map[string]string{\"Authorization\": basic, \"Content-Type\": \"application/json\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// Add a payload message if one is present.\n\treq.Task.GetHttpRequest().Body = payloadBuf.Bytes()\n\n\t_, err = client.CreateTask(ctx, req)\n\tif err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"error while creating the cloud task\")\n\t\treturn fmt.Errorf(\"cloudtasks.CreateTask: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "apps/checker/cmd/private/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/madflojo/tasks\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/job\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/scheduler\"\n\n\tv1 \"github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1\"\n)\n\nconst (\n\tconfigRefreshInterval = 10 * time.Minute\n)\n\nfunc main() {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\t// Graceful shutdown on interrupt\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)\n\tgo func() {\n\t\t<-sigChan\n\t\tcancel()\n\t}()\n\tfmt.Println(\"Launching openstatus private location checker\")\n\ts := tasks.New()\n\tdefer s.Stop()\n\n\tapiKey := getEnv(\"OPENSTATUS_KEY\", \"\")\n\n\tmonitorManager := scheduler.MonitorManager{\n\t\tClient:    getClient(apiKey),\n\t\tJobRunner: job.NewJobRunner(),\n\t\tScheduler: s,\n\t}\n\tconfigTicker := time.NewTicker(configRefreshInterval)\n\tdefer configTicker.Stop()\n\n\tmonitorManager.UpdateMonitors(ctx)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-configTicker.C:\n\t\t\tfmt.Println(\"fetching monitors\")\n\t\t\tmonitorManager.UpdateMonitors(ctx)\n\t\t}\n\t}\n}\n\nfunc getEnv(key, fallback string) string {\n\tif value, ok := os.LookupEnv(key); ok {\n\t\treturn value\n\t}\n\treturn fallback\n}\n\nfunc getClient(apiKey string) v1.PrivateLocationServiceClient {\n\tingestUrl := getEnv(\"OPENSTATUS_INGEST_URL\", \"https://openstatus-private-location.fly.dev\")\n\n\tclient := v1.NewPrivateLocationServiceClient(\n\t\thttp.DefaultClient,\n\t\tingestUrl,\n\t\tconnect.WithHTTPGet(),\n\t\tconnect.WithInterceptors(NewAuthInterceptor(apiKey)),\n\t)\n\n\treturn client\n}\n\nfunc NewAuthInterceptor(token string) connect.UnaryInterceptorFunc {\n\n\tinterceptor := func(next connect.UnaryFunc) connect.UnaryFunc {\n\t\treturn connect.UnaryFunc(func(\n\t\t\tctx context.Context,\n\t\t\treq connect.AnyRequest,\n\t\t) (connect.AnyResponse, error) {\n\t\t\tif req.Spec().IsClient {\n\t\t\t\t// Send a token with client requests.\n\t\t\t\treq.Header().Set(\"openstatus-token\", token)\n\t\t\t}\n\n\t\t\treturn next(ctx, req)\n\t\t})\n\t}\n\treturn connect.UnaryInterceptorFunc(interceptor)\n\n}\n"
  },
  {
    "path": "apps/checker/cmd/server/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math/rand/v2\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n\t\"github.com/openstatushq/openstatus/apps/checker/handlers\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/logger\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird\"\n\t\"github.com/rs/zerolog/log\"\n\t\"go.opentelemetry.io/contrib/bridges/otelslog\"\n\t// otelz \"go.opentelemetry.io/contrib/bridges/otelzerolog\"\n\t\"go.opentelemetry.io/otel/log/global\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\totlploghttp \"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp\"\n\tsdklog \"go.opentelemetry.io/otel/sdk/log\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.39.0\"\n)\n\nfunc shouldSample(event map[string]any) bool {\n\tstatusCode, _ := event[\"status_code\"].(int)\n\tdurationMs, _ := event[\"duration_ms\"].(int)\n\n\t// Always capture: server errors\n\tif statusCode >= 500 {\n\t\treturn true\n\t}\n\n\t// Always capture: explicit errors\n\tif _, hasError := event[\"error\"]; hasError {\n\t\treturn true\n\t}\n\n\t// Always capture: slow requests (above p99 - 2s threshold)\n\tif durationMs > 2000 {\n\t\treturn true\n\t}\n\n\t// Higher sampling for client errors (4xx) - 100%\n\tif statusCode >= 400 && statusCode < 500 {\n\t\treturn true\n\t}\n\n\t// Random sample successful, fast requests at 20%\n\treturn rand.Float64() < 0.2\n}\n\n// MapToAttrs converts a map[string]any to a slice of slog.Attr\nfunc MapToAttrs(m map[string]any) []slog.Attr {\n\tattrs := make([]slog.Attr, 0, len(m))\n\tfor k, v := range m {\n\t\tattrs = append(attrs, toAttr(k, v))\n\t}\n\treturn attrs\n}\n\nfunc toAttr(key string, value any) slog.Attr {\n\tswitch v := value.(type) {\n\tcase string:\n\t\treturn slog.String(key, v)\n\tcase int:\n\t\treturn slog.Int(key, v)\n\tcase int64:\n\t\treturn slog.Int64(key, v)\n\tcase float64:\n\t\treturn slog.Float64(key, v)\n\tcase bool:\n\t\treturn slog.Bool(key, v)\n\tcase time.Time:\n\t\treturn slog.Time(key, v)\n\tcase time.Duration:\n\t\treturn slog.Duration(key, v)\n\tcase map[string]any:\n\t\treturn slog.Group(key, mapToAny(v)...)\n\tdefault:\n\t\treturn slog.Any(key, v)\n\t}\n}\n\nfunc mapToAny(m map[string]any) []any {\n\targs := make([]any, 0, len(m)*2)\n\tfor k, v := range m {\n\t\targs = append(args, toAttr(k, v))\n\t}\n\treturn args\n}\n\nfunc Logger() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tstartTime := time.Now()\n\n\t\t// Generate or get request ID\n\t\trequestID := c.GetHeader(\"X-Request-ID\")\n\t\tif requestID == \"\" {\n\t\t\trequestID = uuid.New().String()\n\t\t}\n\t\tc.Set(\"requestId\", requestID)\n\n\t\t// Build wide event context at request start\n\t\tevent := map[string]any{\n\t\t\t\"timestamp\":    startTime.Format(time.RFC3339),\n\t\t\t\"request_id\":   requestID,\n\t\t\t\"method\":       c.Request.Method,\n\t\t\t\"path\":         c.Request.URL.Path,\n\t\t\t\"url\":          c.Request.Host + c.Request.URL.String(),\n\t\t\t\"user_agent\":   c.GetHeader(\"User-Agent\"),\n\t\t\t\"content_type\": c.GetHeader(\"Content-Type\"),\n\t\t}\n\t\tc.Set(\"event\", event)\n\n\t\t// Process request\n\t\tc.Next()\n\n\t\t// After request - capture response details\n\t\tduration := time.Since(startTime).Milliseconds()\n\t\tstatus := c.Writer.Status()\n\n\t\tevent[\"status_code\"] = status\n\t\tevent[\"duration_ms\"] = int(duration)\n\n\t\t// var requestErr error\n\t\tif len(c.Errors) > 0 {\n\t\t\tevent[\"outcome\"] = \"error\"\n\t\t\tlastErr := c.Errors.Last()\n\t\t\tevent[\"error\"] = map[string]any{\n\t\t\t\t\"type\":    \"GinError\",\n\t\t\t\t\"message\": lastErr.Error(),\n\t\t\t}\n\t\t} else {\n\t\t\tevent[\"outcome\"] = \"success\"\n\t\t}\n\n\t\tif shouldSample(event) {\n\t\t\tattrs := MapToAttrs(event)\n\t\t\tslog.LogAttrs(c.Request.Context(),slog.LevelInfo, \"request done\", attrs...)\n\t\t}\n\n\t\tlog.Debug().\n\t\t\tInt(\"status_code\", status).\n\t\t\tInt64(\"duration_ms\", duration).\n\t\t\tStr(\"request_id\", requestID).\n\t\t\tMsg(\"Request completed\")\n\t}\n}\n\nfunc main() {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tdone := make(chan os.Signal, 1)\n\tsignal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)\n\n\tgo func() {\n\t\t<-done\n\t\tcancel()\n\t}()\n\n\t// environment variables.\n\tvar region string\n\tcronSecret := env(\"CRON_SECRET\", \"\")\n\ttinyBirdToken := env(\"TINYBIRD_TOKEN\", \"\")\n\tlogLevel := env(\"LOG_LEVEL\", \"info\")\n\tcloudProvider := env(\"CLOUD_PROVIDER\", \"fly\")\n\taxiomToken := env(\"AXIOM_TOKEN\", \"\")\n\taxiomDataset := env(\"AXIOM_DATASET\", \"dev\")\n\tswitch cloudProvider {\n\tcase \"fly\":\n\t\tregion = env(\"FLY_REGION\", env(\"REGION\", \"local\"))\n\n\tcase \"koyeb\":\n\t\tregion = fmt.Sprintf(\"koyeb_%s\", env(\"KOYEB_REGION\", env(\"REGION\", \"local\")))\n\n\tcase \"railway\":\n\t\tregion = fmt.Sprintf(\"railway_%s\", env(\"RAILWAY_REPLICA_REGION\", env(\"REGION\", \"local\")))\n\tdefault:\n\t\tlog.Fatal().Msgf(\"unsupported cloud provider: %s\", cloudProvider)\n\t}\n\tlogger.Configure(logLevel)\n\n\t// Define resource with service name, version, and environment\n\tres := resource.NewWithAttributes(\n\t\tsemconv.SchemaURL,\n\t\tsemconv.ServiceNameKey.String(\"openstatus-checker\"),\n\t\tsemconv.ServiceVersionKey.String(\"1.0.0\"),\n\t\tattribute.String(\"environment\", \"production\"),\n\t\tattribute.String(\"cloud.provider\", cloudProvider),\n\t\tattribute.String(\"cloud.region\", region),\n\t)\n\n\t// Set up OTLP log exporter for Axiom\n\texporter, err := otlploghttp.New(ctx,\n\t\totlploghttp.WithEndpointURL(\"https://eu-central-1.aws.edge.axiom.co/v1/logs\"),\n\t\totlploghttp.WithHeaders(map[string]string{\n\t\t\t\"Authorization\":   \"Bearer \" + axiomToken,\n\t\t\t\"X-Axiom-Dataset\": axiomDataset,\n\t\t}),\n\t)\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to create OTLP exporter\")\n\t}\n\n\t// Create log provider with resource and batch processor\n\tlogProvider := sdklog.NewLoggerProvider(\n\t\tsdklog.WithResource(res),\n\t\tsdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)),\n\n\t)\n\tdefer logProvider.Shutdown(ctx)\n\n\tglobal.SetLoggerProvider(logProvider)\n\tslog.SetDefault(otelslog.NewLogger(\"openstatus-checker\"))\n\thttpClient := &http.Client{\n\t\tTimeout: 45 * time.Second,\n\t}\n\n\tdefer httpClient.CloseIdleConnections()\n\n\ttinybirdClient := tinybird.NewClient(httpClient, tinyBirdToken)\n\n\th := &handlers.Handler{\n\t\tSecret:        cronSecret,\n\t\tCloudProvider: cloudProvider,\n\t\tRegion:        region,\n\t\tTbClient:      tinybirdClient,\n\t}\n\n\trouter := gin.New()\n\trouter.Use(gin.Recovery())\n\trouter.Use(Logger())\n\trouter.POST(\"/checker\", h.HTTPCheckerHandler)\n\trouter.POST(\"/checker/http\", h.HTTPCheckerHandler)\n\trouter.POST(\"/checker/tcp\", h.TCPHandler)\n\trouter.POST(\"/checker/dns\", h.DNSHandler)\n\trouter.POST(\"/ping/:region\", h.PingRegionHandler)\n\trouter.POST(\"/tcp/:region\", h.TCPHandlerRegion)\n\trouter.POST(\"/dns/:region\", h.DNSHandlerRegion)\n\n\trouter.GET(\"/health\", func(c *gin.Context) {\n\t\tc.JSON(http.StatusOK, gin.H{\"message\": \"pong\", \"region\": region, \"provider\": cloudProvider})\n\t})\n\n\thttpServer := &http.Server{\n\t\tAddr:    fmt.Sprintf(\"0.0.0.0:%s\", env(\"PORT\", \"8080\")),\n\t\tHandler: router,\n\t}\n\n\tgo func() {\n\t\tif err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to start http server\")\n\t\t\tcancel()\n\t\t}\n\t}()\n\n\t<-ctx.Done()\n\tif err := httpServer.Shutdown(ctx); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to shutdown http server\")\n\n\t\treturn\n\t}\n}\n\nfunc env(key, fallback string) string {\n\tif value, ok := os.LookupEnv(key); ok {\n\t\treturn value\n\t}\n\n\treturn fallback\n}\n"
  },
  {
    "path": "apps/checker/fly.toml",
    "content": "# fly.toml app configuration file generated for openstatus-checker on 2023-11-30T20:23:20+01:00\n#\n# See https://fly.io/docs/reference/configuration/ for information about how to use this file.\n#\n\napp = \"openstatus-checker\"\nprimary_region = \"ams\"\n\n[build]\n  dockerfile = \"./Dockerfile\"\n\n[deploy]\n  strategy = \"canary\"\n\n\n[env]\n  PORT = \"8080\"\n\n[http_service]\n  internal_port = 8080\n  force_https = true\n  auto_stop_machines = \"off\"\n  auto_start_machines = false\n  processes = [\"app\"]\n\n[[vm]]\n  cpu_kind = \"shared\"\n  cpus = 2\n  memory_mb = 512\n\n\n[[http_service.checks]]\n  grace_period = \"10s\"\n  interval = \"15s\"\n  method = \"GET\"\n  timeout = \"5s\"\n  path = \"/health\"\n\n[http_service.concurrency]\n    type = \"requests\"\n    hard_limit = 1000\n    soft_limit = 500\n"
  },
  {
    "path": "apps/checker/go.mod",
    "content": "module github.com/openstatushq/openstatus/apps/checker\n\ngo 1.25.2\n\nrequire (\n\tcloud.google.com/go/auth v0.18.2\n\tcloud.google.com/go/cloudtasks v1.13.7\n\tconnectrpc.com/connect v1.19.1\n\tgithub.com/cenkalti/backoff/v4 v4.3.0\n\tgithub.com/cenkalti/backoff/v5 v5.0.3\n\tgithub.com/gin-gonic/gin v1.12.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/madflojo/tasks v1.2.1\n\tgithub.com/rs/zerolog v1.34.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgo.opentelemetry.io/contrib/bridges/otelslog v0.16.0\n\tgo.opentelemetry.io/otel v1.41.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.17.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0\n\tgo.opentelemetry.io/otel/log v0.17.0\n\tgo.opentelemetry.io/otel/metric v1.41.0\n\tgo.opentelemetry.io/otel/sdk v1.41.0\n\tgo.opentelemetry.io/otel/sdk/log v0.17.0\n\tgo.opentelemetry.io/otel/sdk/metric v1.41.0\n\tgoogle.golang.org/api v0.269.0\n\tgoogle.golang.org/protobuf v1.36.11\n)\n\nrequire (\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tgithub.com/bytedance/gopkg v0.1.3 // indirect\n\tgithub.com/bytedance/sonic v1.15.0 // indirect\n\tgithub.com/bytedance/sonic/loader v0.5.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.13 // indirect\n\tgithub.com/gin-contrib/sse v1.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.30.1 // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/goccy/go-yaml v1.19.2 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.17.0 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/quic-go/qpack v0.6.0 // indirect\n\tgithub.com/quic-go/quic-go v0.59.0 // indirect\n\tgithub.com/rs/xid v1.6.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.3.1 // indirect\n\tgo.mongodb.org/mongo-driver/v2 v2.5.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.66.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.41.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.9.0 // indirect\n\tgolang.org/x/arch v0.24.0 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/net v0.51.0 // indirect\n\tgolang.org/x/oauth2 v0.35.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect\n\tgoogle.golang.org/grpc v1.79.1 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "apps/checker/go.sum",
    "content": "cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/cloudtasks v1.13.7 h1:H2v8GEolNtMFfYzUpZBaZbydqU7drpyo99GtAgA+m4I=\ncloud.google.com/go/cloudtasks v1.13.7/go.mod h1:H0TThOUG+Ml34e2+ZtW6k6nt4i9KuH3nYAJ5mxh7OM4=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\nconnectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=\nconnectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=\ngithub.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=\ngithub.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=\ngithub.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=\ngithub.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=\ngithub.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=\ngithub.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=\ngithub.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=\ngithub.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=\ngithub.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=\ngithub.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=\ngithub.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=\ngithub.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=\ngithub.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=\ngithub.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\ngithub.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=\ngithub.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/madflojo/tasks v1.2.1 h1:0HMN1RCVf6yDjrlIbthkET1KCB+gxknQG3/SLO+HHj4=\ngithub.com/madflojo/tasks v1.2.1/go.mod h1:/WMv6u3Xb5eyy+aIM76ildaIT166GOxN/jya9oI7dyo=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=\ngithub.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=\ngithub.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=\ngithub.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=\ngithub.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=\ngo.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=\ngo.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/bridges/otelslog v0.16.0 h1:ZtVk8SzgioZhBJmJoezi6Jl5uuXoNVLnZxcJCDTqSbM=\ngo.opentelemetry.io/contrib/bridges/otelslog v0.16.0/go.mod h1:p0C45DA3hvvo+5hwDilrMIp43ddVBGmwWEHZft4pY6c=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.66.0 h1:w/o339tDd6Qtu3+ytwt+/jon2yjAs3Ot8Xq8pelfhSo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.66.0/go.mod h1:pdhNtM9C4H5fRdrnwO7NjxzQWhKSSxCHk/KluVqDVC0=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=\ngo.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=\ngo.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.17.0 h1:GcSx2UgcMuQEu0vHq823xR5LCN3WqEx5yKhqDkv1pwY=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.17.0/go.mod h1:ctNT8t8Vzx9sb1oWAozighT3guWorr8xdCboBvkT5yg=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0 h1:MMrOAN8H1FrvDyq9UJ4lu5/+ss49Qgfgb7Zpm0m8ABo=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0/go.mod h1:Na+2NNASJtF+uT4NxDe0G+NQb+bUgdPDfwxY/6JmS/c=\ngo.opentelemetry.io/otel/log v0.17.0 h1:blZWM4y7n+KSa9OywwGWyBMPpeVoCl/NCw+jMps8afM=\ngo.opentelemetry.io/otel/log v0.17.0/go.mod h1:VXhjKYep6/laSgf/tjdh2SMAt18Z9XotBFBO0jxSE24=\ngo.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=\ngo.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=\ngo.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=\ngo.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=\ngo.opentelemetry.io/otel/sdk/log v0.17.0 h1:stWOgJB8bWieSlX4VO+gD7BrRZ/Dh1H/u7115amleGE=\ngo.opentelemetry.io/otel/sdk/log v0.17.0/go.mod h1:LQKPUyHraLka2sRvNQ5+W456+sElomqR7VWpOnOefZg=\ngo.opentelemetry.io/otel/sdk/log/logtest v0.17.0 h1:Z4S9W5piCH88itCkWDtX5ppRgO0UTkLXVK/6tPOMM2w=\ngo.opentelemetry.io/otel/sdk/log/logtest v0.17.0/go.mod h1:d9iIX/BwLfu1BTPxO0wi4ucyCenCckfuf9LC0aJDjqM=\ngo.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=\ngo.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=\ngo.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=\ngo.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=\ngo.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=\ngo.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngolang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=\ngolang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=\ngolang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg=\ngoogle.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE=\ngoogle.golang.org/genproto v0.0.0-20260226221140-a57be14db171 h1:RxhCsti413yL0IjU9dVvuTbCISo8gs3RW1jPMStck+4=\ngoogle.golang.org/genproto v0.0.0-20260226221140-a57be14db171/go.mod h1:uhvzakVEqAuXU3TC2JCsxIRe5f77l+JySE3EqPoMyqM=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=\ngoogle.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "apps/checker/handlers/checker.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/assertions\"\n\totelOS \"github.com/openstatushq/openstatus/apps/checker/pkg/otel\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n)\n\ntype statusCode int\n\nfunc (s statusCode) IsSuccessful() bool {\n\treturn s >= 200 && s < 300\n}\n\ntype PingData struct {\n\tID            string `json:\"id\"`\n\tWorkspaceID   string `json:\"workspaceId\"`\n\tMonitorID     string `json:\"monitorId\"`\n\tURL           string `json:\"url\"`\n\tMethod        string `json:\"method\"`\n\tRegion        string `json:\"region\"`\n\tMessage       string `json:\"message,omitempty\"`\n\tTiming        string `json:\"timing,omitempty\"`\n\tHeaders       string `json:\"headers,omitempty\"`\n\tAssertions    string `json:\"assertions\"`\n\tBody          string `json:\"body,omitempty\"`\n\tTrigger       string `json:\"trigger,omitempty\"`\n\tRequestStatus string `json:\"requestStatus,omitempty\"`\n\tLatency       int64  `json:\"latency\"`\n\tCronTimestamp int64  `json:\"cronTimestamp\"`\n\tTimestamp     int64  `json:\"timestamp\"`\n\tStatusCode    int    `json:\"statusCode,omitempty\"`\n\tError         uint8  `json:\"error\"`\n}\n\nfunc (h Handler) HTTPCheckerHandler(c *gin.Context) {\n\tctx := c.Request.Context()\n\tconst defaultRetry = 3\n\tdataSourceName := \"ping_response__v8\"\n\n\tif c.GetHeader(\"Authorization\") != fmt.Sprintf(\"Basic %s\", h.Secret) {\n\t\tc.JSON(http.StatusUnauthorized, gin.H{\"error\": \"unauthorized\"})\n\n\t\treturn\n\t}\n\n\tif h.CloudProvider == \"fly\" {\n\t\t// if the request has been routed to a wrong region, we forward it to the correct one.\n\t\tregion := c.GetHeader(\"fly-prefer-region\")\n\t\tif region != \"\" && region != h.Region {\n\t\t\tc.Header(\"fly-replay\", fmt.Sprintf(\"region=%s\", region))\n\t\t\tc.String(http.StatusAccepted, \"Forwarding request to %s\", region)\n\n\t\t\treturn\n\t\t}\n\t}\n\n\tvar req request.HttpCheckerRequest\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to decode checker request\")\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid request\"})\n\n\t\treturn\n\t}\n\t//  We need a new client for each request to avoid connection reuse.\n\trequestClient := &http.Client{\n\t\tTimeout: time.Duration(req.Timeout) * time.Millisecond,\n\t}\n\n\t// Configure redirect policy based on FollowRedirects setting\n\tif !req.FollowRedirects {\n\t\trequestClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t}\n\t} else {\n\t\t// Explicitly limit the number of redirects to 10 (Go's default)\n\t\trequestClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\t\tif len(via) >= 10 {\n\t\t\t\treturn http.ErrUseLastResponse\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\tdefer requestClient.CloseIdleConnections()\n\n\t// Might be a more efficient way to do it\n\tvar i interface{} = req.RawAssertions\n\tjsonBytes, _ := json.Marshal(i)\n\tassertionAsString := string(jsonBytes)\n\n\tif assertionAsString == \"null\" {\n\t\tassertionAsString = \"\"\n\t}\n\n\ttrigger := \"cron\"\n\tif req.Trigger != \"\" {\n\t\ttrigger = req.Trigger\n\t}\n\n\tvar called int\n\n\tvar result checker.Response\n\n\tretry := defaultRetry\n\tif req.Retry != 0 {\n\t\tretry = int(req.Retry)\n\t}\n\n\top := func() error {\n\t\tcalled++\n\t\tres, err := checker.Http(ctx, requestClient, req)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to ping: %w\", err)\n\t\t}\n\n\t\t// In TB we need to store them as string\n\t\ttimingAsString, err := json.Marshal(res.Timing)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error while parsing timing data %s: %w\", req.URL, err)\n\t\t}\n\n\t\theadersAsString, err := json.Marshal(res.Headers)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error while parsing headers %s: %w\", req.URL, err)\n\t\t}\n\n\t\tid, err := uuid.NewV7()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error while generating uuid %w\", err)\n\t\t}\n\n\t\tvar requestStatus = \"\"\n\t\tswitch req.Status {\n\t\tcase \"active\":\n\t\t\trequestStatus = \"success\"\n\t\tcase \"error\":\n\t\t\trequestStatus = \"error\"\n\t\tcase \"degraded\":\n\t\t\trequestStatus = \"degraded\"\n\t\t}\n\n\t\tdata := PingData{\n\t\t\tID:            id.String(),\n\t\t\tLatency:       res.Latency,\n\t\t\tStatusCode:    res.Status,\n\t\t\tMonitorID:     req.MonitorID,\n\t\t\tRegion:        h.Region,\n\t\t\tWorkspaceID:   req.WorkspaceID,\n\t\t\tTimestamp:     res.Timestamp,\n\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\tURL:           req.URL,\n\t\t\tMethod:        req.Method,\n\t\t\tTiming:        string(timingAsString),\n\t\t\tHeaders:       string(headersAsString),\n\t\t\tBody:          string(res.Body),\n\t\t\tTrigger:       trigger,\n\t\t\tRequestStatus: requestStatus,\n\t\t}\n\n\t\tvar isSuccessfull bool = true\n\t\tisSuccessfull, err = EvaluateHTTPAssertions(req.RawAssertions, data, res)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// let's retry at least once if the status code is not successful.\n\t\tif !isSuccessfull && called < retry {\n\t\t\treturn fmt.Errorf(\"unable to ping: %v with status %v\", res, res.Status)\n\t\t}\n\n\t\tresult = res\n\t\tresult.Region = h.Region\n\t\tresult.JobType = \"http\"\n\n\t\t// it's in error if not successful\n\t\tif isSuccessfull {\n\t\t\tdata.Error = 0\n\t\t\tif req.DegradedAfter != 0 && res.Latency > req.DegradedAfter {\n\t\t\t\tdata.Body = res.Body\n\n\t\t\t} else {\n\t\t\t\tdata.Body = \"\"\n\n\t\t\t}\n\t\t\t// Small trick to avoid sending the body at the moment to TB\n\t\t} else {\n\t\t\tdata.Error = 1\n\t\t\tresult.Error = \"Error\"\n\t\t}\n\n\t\tdata.Assertions = assertionAsString\n\n\t\tif !isSuccessfull && req.Status != \"error\" {\n\t\t\t// Q: Why here we do not check if the status was previously active?\n\t\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\t\tMonitorId:     req.MonitorID,\n\t\t\t\tStatus:        \"error\",\n\t\t\t\tStatusCode:    res.Status,\n\t\t\t\tRegion:        h.Region,\n\t\t\t\tMessage:       res.Error,\n\t\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\t\tLatency:       res.Latency,\n\t\t\t})\n\t\t\tdata.RequestStatus = \"error\"\n\t\t}\n\t\t// it's degraded\n\t\tif isSuccessfull && req.DegradedAfter > 0 && res.Latency > req.DegradedAfter && req.Status != \"degraded\" {\n\t\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\t\tMonitorId:     req.MonitorID,\n\t\t\t\tStatus:        \"degraded\",\n\t\t\t\tRegion:        h.Region,\n\t\t\t\tStatusCode:    res.Status,\n\t\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\t\tLatency:       res.Latency,\n\t\t\t})\n\t\t\tdata.RequestStatus = \"degraded\"\n\t\t}\n\t\t// it's active\n\t\tif isSuccessfull && req.DegradedAfter == 0 && req.Status != \"active\" {\n\t\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\t\tMonitorId:     req.MonitorID,\n\t\t\t\tStatus:        \"active\",\n\t\t\t\tRegion:        h.Region,\n\t\t\t\tStatusCode:    res.Status,\n\t\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\t\tLatency:       res.Latency,\n\t\t\t})\n\t\t\tdata.RequestStatus = \"success\"\n\t\t}\n\t\t// it's active\n\t\tif isSuccessfull && res.Latency < req.DegradedAfter && req.DegradedAfter != 0 && req.Status != \"active\" {\n\t\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\t\tMonitorId:     req.MonitorID,\n\t\t\t\tStatus:        \"active\",\n\t\t\t\tRegion:        h.Region,\n\t\t\t\tStatusCode:    res.Status,\n\t\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\t\tLatency:       res.Latency,\n\t\t\t})\n\t\t\tdata.RequestStatus = \"success\"\n\t\t}\n\n\t\tif err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to send event to tinybird\")\n\t\t}\n\n\n\t\te, f := c.Get(\"event\")\n\t\tif f {\n\t\t\tt := e.(map[string]any)\n\t\t\tt[\"checker\"] = map[string]string{\n\t\t\t\t\"uri\": req.URL,\n\t\t\t\t\"workspace_id\": req.WorkspaceID,\n\t\t\t\t\"monitor_id\":req.MonitorID,\n\t\t\t\t\"trigger\": trigger,\n\t\t\t\t\"type\": \"http\",\n\t\t\t}\n\t\t\tc.Set(\"event\", t)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), uint64(retry))); err != nil {\n\t\tid, e := uuid.NewV7()\n\t\tif e != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(e).Msg(\"failed to send event to tinybird\")\n\t\t\treturn\n\t\t}\n\n\t\tdata := PingData{\n\t\t\tID:            id.String(),\n\t\t\tURL:           req.URL,\n\t\t\tMethod:        req.Method,\n\t\t\tRegion:        h.Region,\n\t\t\tMessage:       err.Error(),\n\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\tTimestamp:     req.CronTimestamp,\n\t\t\tMonitorID:     req.MonitorID,\n\t\t\tWorkspaceID:   req.WorkspaceID,\n\t\t\tError:         1,\n\t\t\tAssertions:    assertionAsString,\n\t\t\tBody:          \"\",\n\t\t\tTrigger:       trigger,\n\t\t\tRequestStatus: \"error\",\n\t\t}\n\n\t\tif err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to send event to tinybird\")\n\t\t}\n\n\t\tif req.Status != \"error\" {\n\t\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\t\tMonitorId:     req.MonitorID,\n\t\t\t\tStatus:        \"error\",\n\t\t\t\tMessage:       err.Error(),\n\t\t\t\tRegion:        h.Region,\n\t\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\t})\n\t\t}\n\t}\n\n\tif req.OtelConfig.Endpoint != \"\" {\n\t\totelOS.RecordHTTPMetrics(ctx, req, result, h.Region)\n\t}\n\n\treturnData := c.Query(\"data\")\n\tif returnData == \"true\" {\n\n\t\tif len(result.Body) > 1024 {\n\t\t\tresult.Body = result.Body[:1000]\n\t\t}\n\n\t\tc.JSON(http.StatusOK, result)\n\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, nil)\n}\n\nfunc EvaluateHTTPAssertions(raw []json.RawMessage, data PingData, res checker.Response) (bool, error) {\n\tstatusCode := statusCode(res.Status)\n\tif len(raw) == 0 {\n\t\treturn statusCode.IsSuccessful(), nil\n\t}\n\tisSuccessful := true\n\tfor _, a := range raw {\n\t\tvar assert request.Assertion\n\t\tif err := json.Unmarshal(a, &assert); err != nil {\n\t\t\treturn false, fmt.Errorf(\"unable to unmarshal assertion: %w\", err)\n\t\t}\n\t\tswitch assert.AssertionType {\n\t\tcase request.AssertionHeader:\n\t\t\tvar target assertions.HeaderTarget\n\t\t\tif err := json.Unmarshal(a, &target); err != nil {\n\t\t\t\treturn false, fmt.Errorf(\"unable to unmarshal HeaderTarget: %w\", err)\n\t\t\t}\n\t\t\tisSuccessful = isSuccessful && target.HeaderEvaluate(data.Headers)\n\t\tcase request.AssertionTextBody:\n\t\t\tvar target assertions.StringTargetType\n\t\t\tif err := json.Unmarshal(a, &target); err != nil {\n\t\t\t\treturn false, fmt.Errorf(\"unable to unmarshal StringTargetType: %w\", err)\n\t\t\t}\n\t\t\tisSuccessful = isSuccessful && target.StringEvaluate(data.Body)\n\t\tcase request.AssertionStatus:\n\t\t\tvar target assertions.StatusTarget\n\t\t\tif err := json.Unmarshal(a, &target); err != nil {\n\t\t\t\treturn false, fmt.Errorf(\"unable to unmarshal StatusTarget: %w\", err)\n\t\t\t}\n\t\t\tisSuccessful = isSuccessful && target.StatusEvaluate(int64(res.Status))\n\t\tcase request.AssertionJsonBody:\n\t\t\t// TODO: Implement JSON body assertion\n\t\tdefault:\n\t\t\tfmt.Println(\"unknown assertion type: \", assert.AssertionType)\n\t\t\t// TODO: Handle unknown assertion type\n\t\t}\n\t}\n\treturn isSuccessful, nil\n}\n"
  },
  {
    "path": "apps/checker/handlers/checker_test.go",
    "content": "package handlers_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\t\"github.com/openstatushq/openstatus/apps/checker/handlers\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHandler_HTTPCheckerHandler(t *testing.T) {\n\thclient := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {\n\t\treturn &http.Response{\n\t\t\tStatusCode: http.StatusAccepted,\n\t\t\tBody:       io.NopCloser(strings.NewReader(`Status Accepted`)),\n\t\t}\n\t})}\n\tclient := tinybird.NewClient(hclient, \"apiKey\")\n\n\tt.Run(\"it should return 401 if there's no auth\", func(t *testing.T) {\n\n\t\tregion := \"local\"\n\t\th := handlers.Handler{\n\t\t\tTbClient:      client,\n\t\t\tSecret:        \"\",\n\t\t\tCloudProvider: \"fly\",\n\t\t\tRegion:        region,\n\t\t}\n\t\trouter := gin.New()\n\t\trouter.POST(\"/checker/:region\", h.HTTPCheckerHandler)\n\n\t\tw := httptest.NewRecorder()\n\n\t\tdata := request.HttpCheckerRequest{\n\t\t\tURL: \"https://www.openstatus.dev\",\n\t\t}\n\t\tdataJson, _ := json.Marshal(data)\n\t\treq, _ := http.NewRequest(http.MethodPost, \"/checker/\"+region, strings.NewReader(string(dataJson)))\n\t\trouter.ServeHTTP(w, req)\n\n\t\tassert.Equal(t, 401, w.Code)\n\t})\n\n\tt.Run(\"it should return 400 if the payload is not ok\", func(t *testing.T) {\n\t\tregion := \"local\"\n\n\t\th := handlers.Handler{\n\t\t\tTbClient:      client,\n\t\t\tSecret:        \"test\",\n\t\t\tCloudProvider: \"fly\",\n\t\t\tRegion:        region,\n\t\t}\n\t\trouter := gin.New()\n\t\trouter.POST(\"/checker/:region\", h.HTTPCheckerHandler)\n\n\t\tw := httptest.NewRecorder()\n\n\t\tdata := request.PingRequest{\n\t\t\tURL: \"https://www.openstatus.dev\",\n\t\t}\n\t\tdataJson, _ := json.Marshal(data)\n\t\treq, _ := http.NewRequest(http.MethodPost, \"/checker/\"+region, strings.NewReader(string(dataJson)))\n\t\treq.Header.Set(\"Authorization\", \"Basic test\")\n\t\trouter.ServeHTTP(w, req)\n\n\t\tassert.Equal(t, 400, w.Code)\n\t\tassert.Contains(t, w.Body.String(), \"{\\\"error\\\":\\\"invalid request\\\"}\")\n\t})\n\n\tt.Run(\"it should return 200 if the payload is not ok\", func(t *testing.T) {\n\t\tregion := \"local\"\n\n\t\thttptest.NewRequest(http.MethodGet, \"http://www.openstatus.dev\", nil)\n\t\thttptest.NewRecorder()\n\n\t\th := handlers.Handler{\n\t\t\tTbClient:      client,\n\t\t\tSecret:        \"test\",\n\t\t\tCloudProvider: \"fly\",\n\t\t\tRegion:        region,\n\t\t}\n\t\trouter := gin.New()\n\t\trouter.POST(\"/checker/:region\", h.HTTPCheckerHandler)\n\n\t\tw := httptest.NewRecorder()\n\n\t\tdata := request.HttpCheckerRequest{\n\t\t\tURL:    \"https://www.openstatus.dev\",\n\t\t\tMethod: \"GET\",\n\t\t\tBody:   \"\",\n\t\t}\n\t\tdataJson, _ := json.Marshal(data)\n\t\treq, _ := http.NewRequest(http.MethodPost, \"/checker/\"+region, strings.NewReader(string(dataJson)))\n\t\treq.Header.Set(\"Authorization\", \"Basic test\")\n\t\trouter.ServeHTTP(w, req)\n\n\t\tassert.Equal(t, 200, w.Code)\n\t\tfmt.Println(w.Body.String())\n\t})\n}\n\nfunc TestEvaluateAssertions_raw(t *testing.T) {\n\t// Helper to marshal assertion\n\tmarshal := func(a any) json.RawMessage {\n\t\tb, _ := json.Marshal(a)\n\t\treturn b\n\t}\n\n\t// Success if no assertions and status code is 200\n\tt.Run(\"no assertions, status code 200\", func(t *testing.T) {\n\t\traw := []json.RawMessage{}\n\t\tdata := handlers.PingData{}\n\t\tres := checker.Response{Status: 200}\n\t\tok, err := handlers.EvaluateHTTPAssertions(raw, data, res)\n\t\tassert.True(t, ok)\n\t\tassert.NoError(t, err)\n\t})\n\n\t// Header assertion success\n\tt.Run(\"header assertion success\", func(t *testing.T) {\n\t\tassertion := request.Assertion{AssertionType: request.AssertionHeader}\n\t\ttarget := struct {\n\t\t\trequest.Assertion\n\t\t\tComparator request.StringComparator `json:\"compare\"`\n\t\t\tKey        string                   `json:\"key\"`\n\t\t\tTarget     string                   `json:\"target\"`\n\t\t}{\n\t\t\tassertion,\n\t\t\trequest.StringContains,\n\t\t\t\"X-Test\",\n\t\t\t\"ok\",\n\t\t}\n\t\trawMsg := marshal(target)\n\t\traw := []json.RawMessage{rawMsg}\n\t\tdata := handlers.PingData{Headers: `{\"X-Test\":\"ok-value\"}`}\n\t\tres := checker.Response{Status: 200}\n\n\t\tok, err := handlers.EvaluateHTTPAssertions(raw, data, res)\n\t\tassert.True(t, ok)\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"header assertion failed\", func(t *testing.T) {\n\t\tassertion := request.Assertion{AssertionType: request.AssertionHeader}\n\t\ttarget := struct {\n\t\t\trequest.Assertion\n\t\t\tComparator request.StringComparator `json:\"compare\"`\n\t\t\tKey        string                   `json:\"key\"`\n\t\t\tTarget     string                   `json:\"target\"`\n\t\t}{\n\t\t\tassertion,\n\t\t\trequest.StringContains,\n\t\t\t\"X-Test\",\n\t\t\t\"not-ok\",\n\t\t}\n\t\trawMsg := marshal(target)\n\t\traw := []json.RawMessage{rawMsg}\n\t\tdata := handlers.PingData{Headers: `{\"X-Test\":\"ok-value\"}`}\n\t\tres := checker.Response{Status: 200}\n\n\t\tok, err := handlers.EvaluateHTTPAssertions(raw, data, res)\n\t\tassert.False(t, ok)\n\t\tassert.NoError(t, err)\n\t})\n\n\t// Text body assertion failure\n\tt.Run(\"text body assertion failure\", func(t *testing.T) {\n\t\tassertion := request.Assertion{AssertionType: request.AssertionTextBody}\n\t\ttarget := struct {\n\t\t\trequest.Assertion\n\t\t\tComparator request.StringComparator `json:\"compare\"`\n\t\t\tTarget     string                   `json:\"target\"`\n\t\t}{\n\t\t\tassertion,\n\t\t\trequest.StringEquals,\n\t\t\t\"fail\",\n\t\t}\n\t\trawMsg := marshal(target)\n\t\traw := []json.RawMessage{rawMsg}\n\t\tdata := handlers.PingData{Body: \"ok\"}\n\t\tres := checker.Response{Status: 200}\n\n\t\tok, err := handlers.EvaluateHTTPAssertions(raw, data, res)\n\t\tassert.False(t, ok)\n\t\tassert.NoError(t, err)\n\t})\n\n\t// Text body assertion failure\n\tt.Run(\"text body assertion success\", func(t *testing.T) {\n\t\tassertion := request.Assertion{AssertionType: request.AssertionTextBody}\n\t\ttarget := struct {\n\t\t\trequest.Assertion\n\t\t\tComparator request.StringComparator `json:\"compare\"`\n\t\t\tTarget     string                   `json:\"target\"`\n\t\t}{\n\t\t\tassertion,\n\t\t\trequest.StringEquals,\n\t\t\t\"success\",\n\t\t}\n\t\trawMsg := marshal(target)\n\t\traw := []json.RawMessage{rawMsg}\n\t\tdata := handlers.PingData{Body: \"success\"}\n\t\tres := checker.Response{Status: 200}\n\n\t\tok, err := handlers.EvaluateHTTPAssertions(raw, data, res)\n\t\tassert.True(t, ok)\n\t\tassert.NoError(t, err)\n\t})\n\t// Status assertion success\n\tt.Run(\"status assertion success\", func(t *testing.T) {\n\t\tassertion := request.Assertion{AssertionType: request.AssertionStatus}\n\t\ttarget := struct {\n\t\t\trequest.Assertion\n\t\t\tComparator request.NumberComparator `json:\"compare\"`\n\t\t\tTarget     int64                    `json:\"target\"`\n\t\t}{\n\t\t\tassertion,\n\t\t\trequest.NumberEquals,\n\t\t\t200,\n\t\t}\n\t\trawMsg := marshal(target)\n\t\traw := []json.RawMessage{rawMsg}\n\t\tdata := handlers.PingData{}\n\t\tres := checker.Response{Status: 200}\n\n\t\tok, err := handlers.EvaluateHTTPAssertions(raw, data, res)\n\t\tassert.True(t, ok)\n\t\tassert.NoError(t, err)\n\t})\n\n\t// Malformed assertion\n\tt.Run(\"malformed assertion\", func(t *testing.T) {\n\t\traw := []json.RawMessage{[]byte(`{not valid json}`)}\n\t\tdata := handlers.PingData{}\n\t\tres := checker.Response{Status: 200}\n\t\tok, err := handlers.EvaluateHTTPAssertions(raw, data, res)\n\t\tassert.False(t, ok)\n\t\tassert.Error(t, err)\n\t})\n\n\t// Unknown assertion type\n\tt.Run(\"unknown assertion type\", func(t *testing.T) {\n\t\tassertion := request.Assertion{AssertionType: \"unknown\"}\n\t\trawMsg := marshal(assertion)\n\t\traw := []json.RawMessage{rawMsg}\n\t\tdata := handlers.PingData{}\n\t\tres := checker.Response{Status: 200}\n\t\tok, err := handlers.EvaluateHTTPAssertions(raw, data, res)\n\t\tassert.True(t, ok) // Should not fail, just skip\n\t\tassert.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "apps/checker/handlers/dns.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/assertions\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n)\n\ntype DNSResponse struct {\n\tID            string `json:\"id\"`\n\tErrorMessage  string `json:\"errorMessage\"`\n\tRegion        string `json:\"region\"`\n\tTrigger       string `json:\"trigger\"`\n\tURI           string `json:\"uri\"`\n\tRequestStatus string `json:\"requestStatus,omitempty\"`\n\tAssertions    string `json:\"assertions\"`\n\n\tRecords map[string][]string `json:\"records\"`\n\n\tRequestId     int64 `json:\"requestId,omitempty\"`\n\tWorkspaceID   int64 `json:\"workspaceId\"`\n\tMonitorID     int64 `json:\"monitorId\"`\n\tTimestamp     int64 `json:\"timestamp\"`\n\tLatency       int64 `json:\"latency\"`\n\tCronTimestamp int64 `json:\"cronTimestamp\"`\n\n\tError uint8 `json:\"error\"`\n}\n\nfunc (h Handler) DNSHandler(c *gin.Context) {\n\tctx := c.Request.Context()\n\tconst defaultRetry = 3\n\tdataSourceName := \"dns_response__v0\"\n\n\t// Authorization check\n\tif c.GetHeader(\"Authorization\") != fmt.Sprintf(\"Basic %s\", h.Secret) {\n\t\tc.JSON(http.StatusUnauthorized, gin.H{\"error\": \"unauthorized\"})\n\t\treturn\n\t}\n\n\t// Fly region forwarding\n\tif h.CloudProvider == \"fly\" {\n\t\tregion := c.GetHeader(\"fly-prefer-region\")\n\t\tif region != \"\" && region != h.Region {\n\t\t\tc.Header(\"fly-replay\", fmt.Sprintf(\"region=%s\", region))\n\t\t\tc.String(http.StatusAccepted, \"Forwarding request to %s\", region)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Parse request\n\tvar req request.DNSCheckerRequest\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to decode checker request\")\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid request\"})\n\t\treturn\n\t}\n\n\tworkspaceId, err := strconv.ParseInt(req.WorkspaceID, 10, 64)\n\tif err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid workspace id\"})\n\t\treturn\n\t}\n\n\tmonitorId, err := strconv.ParseInt(req.MonitorID, 10, 64)\n\tif err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid monitor id\"})\n\t\treturn\n\t}\n\n\ttrigger := req.Trigger\n\tif trigger == \"\" {\n\t\ttrigger = \"cron\"\n\t}\n\n\tretry := defaultRetry\n\tif req.Retry != 0 {\n\t\tretry = int(req.Retry)\n\t}\n\n\tid, e := uuid.NewV7()\n\tif e != nil {\n\t\tlog.Ctx(ctx).Error().Err(e).Msg(\"failed to generate UUID\")\n\t\treturn\n\t}\n\n\tstatusMap := map[string]string{\n\t\t\"active\":   \"success\",\n\t\t\"error\":    \"error\",\n\t\t\"degraded\": \"degraded\",\n\t}\n\trequestStatus := statusMap[req.Status]\n\n\tdata := DNSResponse{\n\t\tID:            id.String(),\n\t\tRegion:        h.Region,\n\t\tTrigger:       trigger,\n\t\tURI:           req.URI,\n\t\tWorkspaceID:   workspaceId,\n\t\tMonitorID:     monitorId,\n\t\tCronTimestamp: req.CronTimestamp,\n\t\tRequestStatus: requestStatus,\n\t\tTimestamp:     time.Now().UTC().UnixMilli(),\n\t}\n\n\tvar (\n\t\tlatency      int64\n\t\tisSuccessful = true\n\t\tcalled       int\n\t)\n\n\top := func() (*checker.DnsResponse, error) {\n\t\tcalled++\n\t\tlog.Ctx(ctx).Debug().Msgf(\"performing dns check for %s (attempt %d/%d)\", req.URI, called, retry)\n\t\tstart := time.Now().UTC().UnixMilli()\n\t\tresponse, err := checker.Dns(ctx, req.URI)\n\t\tlatency = time.Now().UTC().UnixMilli() - start\n\n\t\tif err != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"dns check failed\")\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(req.RawAssertions) > 0 {\n\t\t\tlog.Ctx(ctx).Debug().Msgf(\"evaluating %d dns assertions\", len(req.RawAssertions))\n\t\t\tisSuccessful, err = EvaluateDNSAssertions(req.RawAssertions, response)\n\t\t\tif err != nil {\n\t\t\t\treturn response, backoff.Permanent(err)\n\t\t\t}\n\t\t}\n\t\tif !isSuccessful && called < retry {\n\t\t\treturn nil, backoff.RetryAfter(1)\n\t\t}\n\t\tif !isSuccessful {\n\t\t\tlog.Ctx(ctx).Debug().Msg(\"dns assertions failed\")\n\t\t\treturn response, backoff.Permanent(fmt.Errorf(\"assertion failed\"))\n\t\t}\n\t\treturn response, nil\n\t}\n\n\tresult, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry)))\n\tdata.Latency = latency\n\tif result != nil {\n\t\tdata.Records = FormatDNSResult(result)\n\t}\n\n\tif len(req.RawAssertions) > 0 {\n\t\tif j, err := json.Marshal(req.RawAssertions); err == nil {\n\t\t\tdata.Assertions = string(j)\n\t\t} else {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to marshal assertions\")\n\t\t}\n\t}\n\n\t// Status update logic\n\tswitch {\n\tcase !isSuccessful:\n\t\tlog.Ctx(ctx).Debug().Msg(\"DNS check failed assertions\")\n\t\tdata.RequestStatus = \"error\"\n\t\tdata.Error = 1\n\t\tdata.ErrorMessage = err.Error()\n\t\tif req.Status != \"error\" {\n\t\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\t\tMonitorId:     req.MonitorID,\n\t\t\t\tStatus:        \"error\",\n\t\t\t\tRegion:        h.Region,\n\t\t\t\tMessage:       err.Error(),\n\t\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\t\tLatency:       latency,\n\t\t\t})\n\t\t}\n\tcase isSuccessful && req.DegradedAfter > 0 && latency > req.DegradedAfter && req.Status != \"degraded\":\n\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\tMonitorId:     req.MonitorID,\n\t\t\tStatus:        \"degraded\",\n\t\t\tRegion:        h.Region,\n\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\tLatency:       latency,\n\t\t})\n\t\tdata.RequestStatus = \"degraded\"\n\tcase isSuccessful && ((req.DegradedAfter == 0 && req.Status != \"active\") || (latency < req.DegradedAfter && req.DegradedAfter != 0 && req.Status != \"active\")):\n\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\tMonitorId:     req.MonitorID,\n\t\t\tStatus:        \"active\",\n\t\t\tRegion:        h.Region,\n\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\tLatency:       latency,\n\t\t})\n\t\tdata.RequestStatus = \"success\"\n\t}\n\n\tif err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to send event to tinybird\")\n\t}\n\n\tevent, f := c.Get(\"event\")\n\tif f {\n\t\tt := event.(map[string]any)\n\t\tt[\"checker\"] = map[string]string{\n\t\t\t\"uri\": req.URI,\n\t\t\t\"workspace_id\": req.WorkspaceID,\n\t\t\t\"monitor_id\":req.MonitorID,\n\t\t\t\"trigger\": trigger,\n\t\t\t\"type\": \"dns\",\n\t\t}\n\t\tc.Set(\"event\", t)\n\t}\n\n\tc.JSON(http.StatusOK, data)\n}\n\nfunc (h Handler) DNSHandlerRegion(c *gin.Context) {\n\tctx := c.Request.Context()\n\tdataSourceName := \"check_dns_response__v0\"\n\tconst defaultRetry = 3\n\n\t// Authorization check\n\tif c.GetHeader(\"Authorization\") != fmt.Sprintf(\"Basic %s\", h.Secret) {\n\t\tc.JSON(http.StatusUnauthorized, gin.H{\"error\": \"unauthorized\"})\n\t\treturn\n\t}\n\n\t// Fly region forwarding\n\tif h.CloudProvider == \"fly\" {\n\t\tregion := c.GetHeader(\"fly-prefer-region\")\n\t\tif region != \"\" && region != h.Region {\n\t\t\tc.Header(\"fly-replay\", fmt.Sprintf(\"region=%s\", region))\n\t\t\tc.String(http.StatusAccepted, \"Forwarding request to %s\", region)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Parse request\n\tvar req request.DNSCheckerRequest\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to decode checker request\")\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid request\"})\n\t\treturn\n\t}\n\n\tretry := defaultRetry\n\tif req.Retry != 0 {\n\t\tretry = int(req.Retry)\n\t}\n\n\tid, e := uuid.NewV7()\n\tif e != nil {\n\t\tlog.Ctx(ctx).Error().Err(e).Msg(\"failed to generate UUID\")\n\t\treturn\n\t}\n\n\tworkspaceId, _ := strconv.Atoi(req.WorkspaceID)\n\n\tstatusMap := map[string]string{\n\t\t\"active\":   \"success\",\n\t\t\"error\":    \"error\",\n\t\t\"degraded\": \"degraded\",\n\t}\n\trequestStatus := statusMap[req.Status]\n\n\tdata := DNSResponse{\n\t\tID:            id.String(),\n\t\tRegion:        h.Region,\n\t\tURI:           req.URI,\n\t\tWorkspaceID:   int64(workspaceId),\n\t\tCronTimestamp: req.CronTimestamp,\n\t\tRequestStatus: requestStatus,\n\t\tTimestamp:     time.Now().UTC().UnixMilli(),\n\t}\n\n\tvar (\n\t\tlatency      int64\n\t\tisSuccessful = true\n\t\tcalled       int\n\t)\n\n\top := func() (*checker.DnsResponse, error) {\n\t\tcalled++\n\t\tlog.Ctx(ctx).Debug().Msgf(\"performing dns check for %s (attempt %d/%d)\", req.URI, called, retry)\n\t\tstart := time.Now().UTC().UnixMilli()\n\t\tresponse, err := checker.Dns(ctx, req.URI)\n\t\tlatency = time.Now().UTC().UnixMilli() - start\n\n\t\tif err != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"dns check failed\")\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(req.RawAssertions) > 0 {\n\t\t\tlog.Ctx(ctx).Debug().Msgf(\"evaluating %d dns assertions\", len(req.RawAssertions))\n\t\t\tisSuccessful, err = EvaluateDNSAssertions(req.RawAssertions, response)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, backoff.Permanent(err)\n\t\t\t}\n\t\t}\n\t\tif !isSuccessful && called < retry {\n\t\t\treturn nil, backoff.RetryAfter(1)\n\t\t}\n\t\tif !isSuccessful {\n\t\t\tlog.Ctx(ctx).Debug().Msg(\"dns assertions failed\")\n\t\t\treturn response, backoff.Permanent(fmt.Errorf(\"assertion failed\"))\n\t\t}\n\t\treturn response, nil\n\t}\n\n\tresult, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry)))\n\tdata.Latency = latency\n\n\tif len(req.RawAssertions) > 0 {\n\t\tif j, err := json.Marshal(req.RawAssertions); err == nil {\n\t\t\tdata.Assertions = string(j)\n\t\t} else {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to marshal assertions\")\n\t\t}\n\t}\n\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"message\": \"uri not reachable\"})\n\t\treturn\n\t}\n\n\tdata.Records = FormatDNSResult(result)\n\tif req.RequestId != 0 {\n\t\tif err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to send event to tinybird\")\n\t\t}\n\t}\n\n\tc.JSON(http.StatusOK, data)\n\n}\n\nfunc FormatDNSResult(result *checker.DnsResponse) map[string][]string {\n\tr := make(map[string][]string)\n\ta := make([]string, 0)\n\taaaa := make([]string, 0)\n\tmx := make([]string, 0)\n\tns := make([]string, 0)\n\ttxt := make([]string, 0)\n\n\tfor _, v := range result.A {\n\t\ta = append(a, v)\n\t}\n\tr[\"A\"] = a\n\n\tfor _, v := range result.AAAA {\n\t\taaaa = append(aaaa, v)\n\t}\n\tr[\"AAAA\"] = aaaa\n\n\tr[\"CNAME\"] = []string{result.CNAME}\n\tfor _, v := range result.MX {\n\t\tmx = append(mx, v)\n\t}\n\tr[\"MX\"] = mx\n\tfor _, v := range result.NS {\n\t\tns = append(ns, v)\n\t}\n\tr[\"NS\"] = ns\n\tfor _, v := range result.TXT {\n\t\ttxt = append(txt, v)\n\t}\n\tr[\"TXT\"] = txt\n\treturn r\n}\n\nfunc EvaluateDNSAssertions(rawAssertions []json.RawMessage, response *checker.DnsResponse) (bool, error) {\n\tfor _, a := range rawAssertions {\n\t\tvar assert assertions.RecordTarget\n\t\tif err := json.Unmarshal(a, &assert); err != nil {\n\t\t\treturn false, fmt.Errorf(\"unable to parse assertion: %w\", err)\n\t\t}\n\t\tvar isSuccessfull bool\n\t\tswitch assert.Key {\n\t\tcase request.RecordA:\n\t\t\tisSuccessfull = assert.RecordEvaluate(response.A)\n\t\tcase request.RecordAAAA:\n\t\t\tisSuccessfull = assert.RecordEvaluate(response.AAAA)\n\t\tcase request.RecordCNAME:\n\t\t\tisSuccessfull = assert.RecordEvaluate([]string{response.CNAME})\n\t\tcase request.RecordMX:\n\t\t\tisSuccessfull = assert.RecordEvaluate(response.MX)\n\t\tcase request.RecordNS:\n\t\t\tisSuccessfull = assert.RecordEvaluate(response.NS)\n\t\tcase request.RecordTXT:\n\t\t\tisSuccessfull = assert.RecordEvaluate(response.TXT)\n\t\tdefault:\n\t\t\treturn false, fmt.Errorf(\"unknown record type in assertion: %s\", assert.Key)\n\t\t}\n\t\tif !isSuccessfull {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "apps/checker/handlers/dns_test.go",
    "content": "package handlers_test\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"testing\"\n\n\t// Adjust the import path if necessary to point to the correct package\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\t\"github.com/openstatushq/openstatus/apps/checker/handlers\"\n)\n\n// Mock DNSResult struct to match the expected input for FormatDNSResult.\n// If the real struct is in another package, import it accordingly.\ntype DNSResult struct {\n\tA     []string\n\tAAAA  []string\n\tCNAME string\n\tMX    []string\n\tNS    []string\n\tTXT   []string\n}\n\nfunc TestFormatDNSResult(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    DNSResult\n\t\texpected map[string][]string\n\t}{\n\t\t{\n\t\t\tname: \"All fields populated\",\n\t\t\tinput: DNSResult{\n\t\t\t\tA:     []string{\"1.2.3.4\", \"5.6.7.8\"},\n\t\t\t\tAAAA:  []string{\"::1\", \"2001:db8::1\"},\n\t\t\t\tCNAME: \"example.com\",\n\t\t\t\tMX:    []string{\"mx1.example.com\", \"mx2.example.com\"},\n\t\t\t\tNS:    []string{\"ns1.example.com\", \"ns2.example.com\"},\n\t\t\t\tTXT:   []string{\"v=spf1\", \"google-site-verification=abc\"},\n\t\t\t},\n\t\t\texpected: map[string][]string{\n\t\t\t\t\"A\":     {\"1.2.3.4\", \"5.6.7.8\"},\n\t\t\t\t\"AAAA\":  {\"::1\", \"2001:db8::1\"},\n\t\t\t\t\"CNAME\": {\"example.com\"},\n\t\t\t\t\"MX\":    {\"mx1.example.com\", \"mx2.example.com\"},\n\t\t\t\t\"NS\":    {\"ns1.example.com\", \"ns2.example.com\"},\n\t\t\t\t\"TXT\":   {\"v=spf1\", \"google-site-verification=abc\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Empty fields\",\n\t\t\tinput: DNSResult{\n\t\t\t\tA:     []string{},\n\t\t\t\tAAAA:  []string{},\n\t\t\t\tCNAME: \"\",\n\t\t\t\tMX:    []string{},\n\t\t\t\tNS:    []string{},\n\t\t\t\tTXT:   []string{},\n\t\t\t},\n\t\t\texpected: map[string][]string{\n\t\t\t\t\"A\":     {},\n\t\t\t\t\"AAAA\":  {},\n\t\t\t\t\"CNAME\": {\"\"},\n\t\t\t\t\"MX\":    {},\n\t\t\t\t\"NS\":    {},\n\t\t\t\t\"TXT\":   {},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single values\",\n\t\t\tinput: DNSResult{\n\t\t\t\tA:     []string{\"8.8.8.8\"},\n\t\t\t\tAAAA:  []string{\"fe80::1\"},\n\t\t\t\tCNAME: \"single.example.com\",\n\t\t\t\tMX:    []string{\"mx.single.com\"},\n\t\t\t\tNS:    []string{\"ns.single.com\"},\n\t\t\t\tTXT:   []string{\"single-txt\"},\n\t\t\t},\n\t\t\texpected: map[string][]string{\n\t\t\t\t\"A\":     {\"8.8.8.8\"},\n\t\t\t\t\"AAAA\":  {\"fe80::1\"},\n\t\t\t\t\"CNAME\": {\"single.example.com\"},\n\t\t\t\t\"MX\":    {\"mx.single.com\"},\n\t\t\t\t\"NS\":    {\"ns.single.com\"},\n\t\t\t\t\"TXT\":   {\"single-txt\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := handlers.FormatDNSResult(&checker.DnsResponse{\n\t\t\t\tA:     tt.input.A,\n\t\t\t\tAAAA:  tt.input.AAAA,\n\t\t\t\tCNAME: tt.input.CNAME,\n\t\t\t\tMX:    tt.input.MX,\n\t\t\t\tNS:    tt.input.NS,\n\t\t\t\tTXT:   tt.input.TXT,\n\t\t\t})\n\t\t\tif !reflect.DeepEqual(got, tt.expected) {\n\t\t\t\tt.Errorf(\"FormatDNSResult() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEvaluateDNSAssertions(t *testing.T) {\n\ttype args struct {\n\t\trawAssertions []json.RawMessage\n\t\tresponse      *checker.DnsResponse\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\targs        args\n\t\twantSuccess bool\n\t\twantErr     bool\n\t}{\n\t\t{\n\t\t\tname: \"A record matches\",\n\t\t\targs: args{\n\t\t\t\trawAssertions: []json.RawMessage{\n\t\t\t\t\tjson.RawMessage(`{\"key\":\"A\",\"compare\":\"eq\",\"target\":\"1.2.3.4\"}`),\n\t\t\t\t},\n\t\t\t\tresponse: &checker.DnsResponse{\n\t\t\t\t\tA: []string{\"1.2.3.4\", \"5.6.7.8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantSuccess: true,\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"CNAME does not match\",\n\t\t\targs: args{\n\t\t\t\trawAssertions: []json.RawMessage{\n\t\t\t\t\tjson.RawMessage(`{\"key\":\"CNAME\",\"compare\":\"eq\",\"target\":\"not-example.com\"}`),\n\t\t\t\t},\n\t\t\t\tresponse: &checker.DnsResponse{\n\t\t\t\t\tCNAME: \"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantSuccess: false,\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"CNAME Contains\",\n\t\t\targs: args{\n\t\t\t\trawAssertions: []json.RawMessage{\n\t\t\t\t\tjson.RawMessage(`{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"CNAME\",\"compare\":\"contains\",\"target\":\"openstatus.dev\"}`),\n\t\t\t\t},\n\t\t\t\tresponse: &checker.DnsResponse{\n\t\t\t\t\tCNAME: \"openstatus.dev.\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantSuccess: true,\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"Unknown record type\",\n\t\t\targs: args{\n\t\t\t\trawAssertions: []json.RawMessage{\n\t\t\t\t\tjson.RawMessage(`{\"key\":\"FOO\",\"compare\":\"eq\",\"target\":\"bar\"}`),\n\t\t\t\t},\n\t\t\t\tresponse: &checker.DnsResponse{},\n\t\t\t},\n\t\t\twantSuccess: false,\n\t\t\twantErr:     true,\n\t\t},\n\t\t{\n\t\t\tname: \"Invalid assertion JSON\",\n\t\t\targs: args{\n\t\t\t\trawAssertions: []json.RawMessage{\n\t\t\t\t\tjson.RawMessage(`not a json`),\n\t\t\t\t},\n\t\t\t\tresponse: &checker.DnsResponse{},\n\t\t\t},\n\t\t\twantSuccess: false,\n\t\t\twantErr:     true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := handlers.EvaluateDNSAssertions(tt.args.rawAssertions, tt.args.response)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif got != tt.wantSuccess {\n\t\t\t\tt.Errorf(\"EvaluateDNSAssertions() = %v, want %v\", got, tt.wantSuccess)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "apps/checker/handlers/handler.go",
    "content": "package handlers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird\"\n)\n\ntype Handler struct {\n\tTbClient      tinybird.Client\n\tSecret        string\n\tCloudProvider string\n\tRegion        string\n}\n\n// Authorization could be handle by middleware\n\nfunc NewHTTPClient() *http.Client {\n\treturn &http.Client{}\n}\n"
  },
  {
    "path": "apps/checker/handlers/ping.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n\t\"github.com/rs/zerolog/log\"\n)\n\ntype PingResponse struct {\n\tBody        string `json:\"body,omitempty\"`\n\tHeaders     string `json:\"headers,omitempty\"`\n\tRegion      string `json:\"region\"`\n\tTiming      string `json:\"timing,omitempty\"`\n\tRequestId   int64  `json:\"requestId,omitempty\"`\n\tWorkspaceId int64  `json:\"workspaceId,omitempty\"`\n\tLatency     int64  `json:\"latency\"`\n\tTimestamp   int64  `json:\"timestamp\"`\n\tStatusCode  int    `json:\"statusCode,omitempty\"`\n}\n\ntype Response struct {\n\tHeaders     map[string]string `json:\"headers,omitempty\"`\n\tError       string            `json:\"error,omitempty\"`\n\tBody        string            `json:\"body,omitempty\"`\n\tRegion      string            `json:\"region\"`\n\tTags        []string          `json:\"tags,omitempty\"`\n\tRequestId   int64             `json:\"requestId,omitempty\"`\n\tWorkspaceId int64             `json:\"workspaceId,omitempty\"`\n\tLatency     int64             `json:\"latency\"`\n\tTimestamp   int64             `json:\"timestamp\"`\n\tTiming      checker.Timing    `json:\"timing\"`\n\tStatus      int               `json:\"status,omitempty\"`\n}\n\nfunc (h Handler) PingRegionHandler(c *gin.Context) {\n\tctx := c.Request.Context()\n\n\tdataSourceName := \"check_response_http__v0\"\n\tregion := c.Param(\"region\")\n\n\tif region == \"\" {\n\t\tc.String(http.StatusBadRequest, \"region is required\")\n\n\t\treturn\n\t}\n\n\tfmt.Printf(\"Start of /ping/%s\\n\", region)\n\n\tif c.GetHeader(\"Authorization\") != fmt.Sprintf(\"Basic %s\", h.Secret) {\n\t\tc.JSON(http.StatusUnauthorized, gin.H{\"error\": \"unauthorized\"})\n\n\t\treturn\n\t}\n\n\tif h.CloudProvider == \"fly\" {\n\t\tif region != h.Region {\n\t\t\tc.Header(\"fly-replay\", fmt.Sprintf(\"region=%s\", region))\n\t\t\tc.String(http.StatusAccepted, \"Forwarding request to %s\", region)\n\n\t\t\treturn\n\t\t}\n\t}\n\n\t//  We need a new client for each request to avoid connection reuse.\n\trequestClient := &http.Client{\n\t\tTimeout: 45 * time.Second,\n\t}\n\n\tdefer requestClient.CloseIdleConnections()\n\n\tvar req request.PingRequest\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to decode checker request\")\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid request\"})\n\n\t\treturn\n\t}\n\n\tvar res checker.Response\n\n\top := func() error {\n\n\t\theaders := make([]struct {\n\t\t\tKey   string `json:\"key\"`\n\t\t\tValue string `json:\"value\"`\n\t\t}, 0)\n\n\t\tfor key, value := range req.Headers {\n\t\t\theaders = append(headers, struct {\n\t\t\t\tKey   string `json:\"key\"`\n\t\t\t\tValue string `json:\"value\"`\n\t\t\t}{Key: key, Value: value})\n\t\t}\n\n\t\tinput := request.HttpCheckerRequest{\n\t\t\tHeaders: headers,\n\t\t\tURL:     req.URL,\n\t\t\tMethod:  req.Method,\n\t\t\tBody:    req.Body,\n\t\t}\n\n\t\tr, err := checker.Http(c.Request.Context(), requestClient, input)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to ping: %w\", err)\n\t\t}\n\n\t\ttimingAsString, err := json.Marshal(r.Timing)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error while parsing timing data %s: %w\", req.URL, err)\n\t\t}\n\n\t\theadersAsString, err := json.Marshal(r.Headers)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\ttbData := PingResponse{\n\t\t\tRequestId:   req.RequestId,\n\t\t\tWorkspaceId: req.WorkspaceId,\n\t\t\tStatusCode:  r.Status,\n\t\t\tLatency:     r.Latency,\n\t\t\tBody:        r.Body,\n\t\t\tHeaders:     string(headersAsString),\n\t\t\tTimestamp:   r.Timestamp,\n\t\t\tTiming:      string(timingAsString),\n\t\t\tRegion:      h.Region,\n\t\t}\n\n\t\tres = r\n\t\tres.Region = h.Region\n\n\t\tif tbData.RequestId != 0 {\n\t\t\tif err := h.TbClient.SendEvent(ctx, tbData, dataSourceName); err != nil {\n\t\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to send event to tinybird\")\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\tif err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 3)); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"message\": \"url not reachable\"})\n\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, res)\n}\n"
  },
  {
    "path": "apps/checker/handlers/ping_test.go",
    "content": "package handlers_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/handlers\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype RoundTripFunc func(req *http.Request) *http.Response\n\nfunc (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn f(req), nil\n}\n\nfunc TestHandler_PingRegion(t *testing.T) {\n\n\thclient := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {\n\t\treturn &http.Response{\n\t\t\tStatusCode: http.StatusAccepted,\n\t\t\tBody:       io.NopCloser(strings.NewReader(`Status Accepted`)),\n\t\t}\n\t})}\n\tclient := tinybird.NewClient(hclient, \"apiKey\")\n\n\tt.Run(\"it should return 401 if there's no auth\", func(t *testing.T) {\n\n\t\tregion := \"local\"\n\t\th := handlers.Handler{\n\t\t\tTbClient:      client,\n\t\t\tSecret:        \"\",\n\t\t\tCloudProvider: \"fly\",\n\t\t\tRegion:        region,\n\t\t}\n\t\trouter := gin.New()\n\t\trouter.POST(\"/checker/:region\", h.PingRegionHandler)\n\n\t\tw := httptest.NewRecorder()\n\n\t\tdata := request.PingRequest{\n\t\t\tURL: \"https://www.openstatus.dev\",\n\t\t}\n\t\tdataJson, _ := json.Marshal(data)\n\t\treq, _ := http.NewRequest(http.MethodPost, \"/checker/\"+region, strings.NewReader(string(dataJson)))\n\t\trouter.ServeHTTP(w, req)\n\n\t\tassert.Equal(t, 401, w.Code)\n\t})\n\n\tt.Run(\"it should return 400 if the payload is not ok\", func(t *testing.T) {\n\t\tregion := \"local\"\n\n\t\th := handlers.Handler{\n\t\t\tTbClient:      client,\n\t\t\tSecret:        \"test\",\n\t\t\tCloudProvider: \"fly\",\n\t\t\tRegion:        region,\n\t\t}\n\t\trouter := gin.New()\n\t\trouter.POST(\"/checker/:region\", h.PingRegionHandler)\n\n\t\tw := httptest.NewRecorder()\n\n\t\tdata := request.HttpCheckerRequest{\n\t\t\tURL: \"https://www.openstatus.dev\",\n\t\t}\n\t\tdataJson, _ := json.Marshal(data)\n\t\treq, _ := http.NewRequest(http.MethodPost, \"/checker/\"+region, strings.NewReader(string(dataJson)))\n\t\treq.Header.Set(\"Authorization\", \"Basic test\")\n\t\trouter.ServeHTTP(w, req)\n\n\t\tassert.Equal(t, 400, w.Code)\n\t\tassert.Contains(t, w.Body.String(), \"{\\\"error\\\":\\\"invalid request\\\"}\")\n\t})\n\n\tt.Run(\"it should return 200 if the payload is ok\", func(t *testing.T) {\n\t\tregion := \"local\"\n\n\t\thttptest.NewRequest(http.MethodGet, \"http://www.openstatus.dev\", nil)\n\t\thttptest.NewRecorder()\n\n\t\th := handlers.Handler{\n\t\t\tTbClient:      client,\n\t\t\tSecret:        \"test\",\n\t\t\tCloudProvider: \"fly\",\n\t\t\tRegion:        region,\n\t\t}\n\t\trouter := gin.New()\n\t\trouter.POST(\"/checker/:region\", h.PingRegionHandler)\n\n\t\tw := httptest.NewRecorder()\n\n\t\tdata := request.PingRequest{\n\t\t\tURL:     \"https://www.openstatus.dev\",\n\t\t\tMethod:  \"GET\",\n\t\t\tHeaders: map[string]string{},\n\t\t\tBody:    \"\",\n\t\t}\n\t\tdataJson, _ := json.Marshal(data)\n\t\treq, _ := http.NewRequest(http.MethodPost, \"/checker/\"+region, strings.NewReader(string(dataJson)))\n\t\treq.Header.Set(\"Authorization\", \"Basic test\")\n\t\trouter.ServeHTTP(w, req)\n\n\t\tassert.Equal(t, 200, w.Code)\n\t\tfmt.Println(w.Body.String())\n\t})\n}\n"
  },
  {
    "path": "apps/checker/handlers/tcp.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\totelOS \"github.com/openstatushq/openstatus/apps/checker/pkg/otel\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n\t\"github.com/rs/zerolog/log\"\n)\n\n// Only used for Tinybird.\ntype TCPData struct {\n\tID            string `json:\"id\"`\n\tTiming        string `json:\"timing\"`\n\tErrorMessage  string `json:\"errorMessage\"`\n\tRegion        string `json:\"region\"`\n\tTrigger       string `json:\"trigger\"`\n\tURI           string `json:\"uri\"`\n\tRequestStatus string `json:\"requestStatus,omitempty\"`\n\n\tRequestId     int64 `json:\"requestId,omitempty\"`\n\tWorkspaceID   int64 `json:\"workspaceId\"`\n\tMonitorID     int64 `json:\"monitorId\"`\n\tTimestamp     int64 `json:\"timestamp\"`\n\tLatency       int64 `json:\"latency\"`\n\tCronTimestamp int64 `json:\"cronTimestamp\"`\n\n\tError uint8 `json:\"error\"`\n}\n\nfunc (h Handler) TCPHandler(c *gin.Context) {\n\tctx := c.Request.Context()\n\tdataSourceName := \"tcp_response__v0\"\n\n\tif c.GetHeader(\"Authorization\") != fmt.Sprintf(\"Basic %s\", h.Secret) {\n\t\tc.JSON(http.StatusUnauthorized, gin.H{\"error\": \"unauthorized\"})\n\n\t\treturn\n\t}\n\n\tif h.CloudProvider == \"fly\" {\n\t\t// if the request has been routed to a wrong region, we forward it to the correct one.\n\t\tregion := c.GetHeader(\"fly-prefer-region\")\n\t\tif region != \"\" && region != h.Region {\n\t\t\tc.Header(\"fly-replay\", fmt.Sprintf(\"region=%s\", region))\n\t\t\tc.String(http.StatusAccepted, \"Forwarding request to %s\", region)\n\n\t\t\treturn\n\t\t}\n\t}\n\n\tvar req request.TCPCheckerRequest\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to decode checker request\")\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid request\"})\n\n\t\treturn\n\t}\n\n\tworkspaceId, err := strconv.ParseInt(req.WorkspaceID, 10, 64)\n\n\tif err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid request\"})\n\n\t\treturn\n\t}\n\n\tmonitorId, err := strconv.ParseInt(req.MonitorID, 10, 64)\n\n\tif err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid request\"})\n\n\t\treturn\n\t}\n\n\tvar trigger = \"cron\"\n\tif req.Trigger != \"\" {\n\t\ttrigger = req.Trigger\n\t}\n\n\te, f := c.Get(\"event\")\n\tif f {\n\t\tt := e.(map[string]any)\n\t\tt[\"checker\"] = map[string]string{\n\t\t\t\"uri\": req.URI,\n\t\t\t\"workspace_id\": req.WorkspaceID,\n\t\t\t\"monitor_id\":req.MonitorID,\n\t\t\t\"trigger\": trigger,\n\t\t\t\"type\": \"tcp\",\n\t\t}\n\t\tc.Set(\"event\", t)\n\t}\n\n\tvar response checker.TCPResponse\n\n\tvar retry int\n\tif req.Retry != 0 {\n\t\tretry = int(req.Retry)\n\t} else {\n\t\tretry = 3\n\t}\n\n\top := func() error {\n\t\tres, err := checker.PingTCP(int(req.Timeout), req.URI)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to check tcp %s\", err)\n\t\t}\n\n\t\ttimingAsString, err := json.Marshal(res)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error while parsing timing data %s: %w\", req.URI, err)\n\t\t}\n\n\t\tlatency := res.TCPDone - res.TCPStart\n\n\t\tvar requestStatus = \"\"\n\t\tswitch req.Status {\n\t\tcase \"active\":\n\t\t\trequestStatus = \"success\"\n\t\tcase \"error\":\n\t\t\trequestStatus = \"error\"\n\n\t\tcase \"degraded\":\n\t\t\trequestStatus = \"degraded\"\n\t\t}\n\n\t\tid, err := uuid.NewV7()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error while generating uuid %w\", err)\n\t\t}\n\n\t\tdata := TCPData{\n\t\t\tID:            id.String(),\n\t\t\tWorkspaceID:   workspaceId,\n\t\t\tTimestamp:     res.TCPStart,\n\t\t\tError:         0,\n\t\t\tErrorMessage:  \"\",\n\t\t\tRegion:        h.Region,\n\t\t\tMonitorID:     monitorId,\n\t\t\tTiming:        string(timingAsString),\n\t\t\tLatency:       latency,\n\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\tTrigger:       trigger,\n\t\t\tURI:           req.URI,\n\t\t\tRequestStatus: requestStatus,\n\t\t}\n\n\t\tresponse = checker.TCPResponse{\n\t\t\tTimestamp: res.TCPStart,\n\t\t\tTiming: checker.TCPResponseTiming{\n\t\t\t\tTCPStart: res.TCPStart,\n\t\t\t\tTCPDone:  res.TCPDone,\n\t\t\t},\n\t\t\tLatency: latency,\n\t\t\tRegion:  h.Region,\n\t\t\tJobType: \"tcp\",\n\t\t}\n\n\t\tif req.DegradedAfter == 0 && req.Status != \"active\" {\n\t\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\t\tMonitorId:     req.MonitorID,\n\t\t\t\tStatus:        \"active\",\n\t\t\t\tRegion:        h.Region,\n\t\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\t\tLatency:       latency,\n\t\t\t})\n\t\t\tdata.RequestStatus = \"success\"\n\t\t}\n\n\t\tif (req.DegradedAfter > 0 && latency < req.DegradedAfter) && req.Status != \"active\" {\n\t\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\t\tMonitorId:     req.MonitorID,\n\t\t\t\tStatus:        \"active\",\n\t\t\t\tRegion:        h.Region,\n\t\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\t\tLatency:       latency,\n\t\t\t})\n\t\t\tdata.RequestStatus = \"success\"\n\n\t\t}\n\n\t\tif req.DegradedAfter > 0 && latency > req.DegradedAfter && req.Status != \"degraded\" {\n\t\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\t\tMonitorId:     req.MonitorID,\n\t\t\t\tStatus:        \"degraded\",\n\t\t\t\tRegion:        h.Region,\n\t\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\t\tLatency:       latency,\n\t\t\t})\n\t\t\tdata.RequestStatus = \"degraded\"\n\n\t\t}\n\n\t\tif err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to send event to tinybird\")\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), uint64(retry))); err != nil {\n\n\t\tid, e := uuid.NewV7()\n\t\tif e != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(e).Msg(\"failed to send event to tinybird\")\n\t\t\treturn\n\t\t}\n\t\tdata := TCPData{\n\t\t\tID:            id.String(),\n\t\t\tWorkspaceID:   workspaceId,\n\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\tErrorMessage:  err.Error(),\n\t\t\tRegion:        h.Region,\n\t\t\tMonitorID:     monitorId,\n\t\t\tError:         1,\n\t\t\tTrigger:       trigger,\n\t\t\tURI:           req.URI,\n\t\t\tRequestStatus: \"error\",\n\t\t}\n\t\tif err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to send event to tinybird\")\n\t\t}\n\t\tchecker.UpdateStatus(ctx, checker.UpdateData{\n\t\t\tMonitorId:     req.MonitorID,\n\t\t\tStatus:        \"error\",\n\t\t\tMessage:       err.Error(),\n\t\t\tRegion:        h.Region,\n\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t})\n\n\t}\n\n\treturnData := c.Query(\"data\")\n\tif returnData == \"true\" {\n\t\tc.JSON(http.StatusOK, response)\n\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, nil)\n}\n\nfunc (h Handler) TCPHandlerRegion(c *gin.Context) {\n\tctx := c.Request.Context()\n\tdataSourceName := \"check_tcp_response__v1\"\n\n\tregion := c.Param(\"region\")\n\tif region == \"\" {\n\t\tc.String(http.StatusBadRequest, \"region is required\")\n\n\t\treturn\n\t}\n\n\tif c.GetHeader(\"Authorization\") != fmt.Sprintf(\"Basic %s\", h.Secret) {\n\t\tc.JSON(http.StatusUnauthorized, gin.H{\"error\": \"unauthorized\"})\n\n\t\treturn\n\t}\n\n\tif h.CloudProvider == \"fly\" {\n\t\t// if the request has been routed to a wrong region, we forward it to the correct one.\n\t\tregion := c.GetHeader(\"fly-prefer-region\")\n\t\tif region != \"\" && region != h.Region {\n\t\t\tc.Header(\"fly-replay\", fmt.Sprintf(\"region=%s\", region))\n\t\t\tc.String(http.StatusAccepted, \"Forwarding request to %s\", region)\n\n\t\t\treturn\n\t\t}\n\t}\n\n\tvar req request.TCPCheckerRequest\n\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to decode checker request\")\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid request\"})\n\n\t\treturn\n\t}\n\n\tvar called int\n\n\tvar response checker.TCPResponse\n\n\top := func() error {\n\t\tcalled++\n\t\ttimestamp := time.Now().UTC().UnixMilli()\n\t\tres, err := checker.PingTCP(int(req.Timeout), req.URI)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to check tcp %s\", err)\n\t\t}\n\n\t\tresponse = checker.TCPResponse{\n\t\t\tTimestamp: timestamp,\n\t\t\tTiming: checker.TCPResponseTiming{\n\t\t\t\tTCPStart: res.TCPStart,\n\t\t\t\tTCPDone:  res.TCPDone,\n\t\t\t},\n\t\t\tLatency: res.TCPDone - res.TCPStart,\n\t\t\tRegion:  h.Region,\n\t\t\tJobType: \"tcp\",\n\t\t}\n\n\t\ttimingAsString, err := json.Marshal(res)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error while parsing timing data %s: %w\", req.URI, err)\n\t\t}\n\n\t\tlatency := res.TCPDone - res.TCPStart\n\n\t\tdata := TCPData{\n\t\t\tCronTimestamp: req.CronTimestamp,\n\t\t\tTimestamp:     res.TCPStart,\n\t\t\tError:         0,\n\t\t\tErrorMessage:  \"\",\n\t\t\tRegion:        h.Region,\n\t\t\tTiming:        string(timingAsString),\n\t\t\tLatency:       latency,\n\t\t\tRequestId:     req.RequestId,\n\t\t\tTrigger:       \"api\",\n\t\t\tURI:           req.URI,\n\t\t}\n\n\t\tif req.RequestId != 0 {\n\t\t\tif err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {\n\t\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to send event to tinybird\")\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 3)); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"message\": \"uri not reachable\"})\n\n\t\treturn\n\t}\n\n\tif req.OtelConfig.Endpoint != \"\" {\n\n\t\totelOS.RecordTCPMetrics(ctx, req, response, region)\n\n\t}\n\n\tc.JSON(http.StatusOK, response)\n}\n"
  },
  {
    "path": "apps/checker/justfile",
    "content": "build-probe:\n    go build -o probe ./cmd/server/main.go\n\nbuild-private:\n    go build -o provider ./cmd/private/main.go\n\ndev-probe:\n    air -c  .probe.air.toml\n\n\ndev-private:\n    air -c  .private.air.toml\n\nclean:\n\techo \"Cleaning...\"\n\trm -f main\n\ntest:\n\techo \"Running tests...\"\n\tgo test ./... -v\n"
  },
  {
    "path": "apps/checker/pkg/assertions/assertions.go",
    "content": "package assertions\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n)\n\ntype BodyString struct {\n\tAssertionType request.AssertionType    `json:\"type\"`\n\tComparator    request.StringComparator `json:\"compare\"`\n\tTarget        string                   `json:\"target\"`\n}\n\ntype StatusTarget struct {\n\tAssertionType request.AssertionType    `json:\"type\"`\n\tComparator    request.NumberComparator `json:\"compare\"`\n\tTarget        int64                    `json:\"target\"`\n}\n\ntype HeaderTarget struct {\n\tAssertionType request.AssertionType    `json:\"type\"`\n\tComparator    request.StringComparator `json:\"compare\"`\n\tTarget        string                   `json:\"target\"`\n\tKey           string                   `json:\"key\"`\n}\n\ntype StringTargetType struct {\n\tComparator request.StringComparator `json:\"compare\"`\n\tTarget     string                   `json:\"target\"`\n}\n\ntype RecordTarget struct {\n\tComparator request.RecordComparator `json:\"compare\"`\n\tTarget     string                   `json:\"target\"`\n\tKey        request.Record           `json:\"key\"`\n}\n\nfunc (target StringTargetType) StringEvaluate(s string) bool {\n\tswitch target.Comparator {\n\tcase request.StringContains:\n\t\treturn strings.Contains(s, target.Target)\n\tcase request.StringNotContains:\n\t\treturn !strings.Contains(s, target.Target)\n\tcase request.StringEmpty:\n\t\treturn s == \"\"\n\tcase request.StringNotEmpty:\n\t\treturn s != \"\"\n\tcase request.StringEquals:\n\t\treturn s == target.Target\n\tcase request.StringNotEquals:\n\t\treturn s != target.Target\n\tcase request.StringGreaterThan:\n\t\treturn s > target.Target\n\tcase request.StringGreaterThanEqual:\n\t\treturn s >= target.Target\n\tcase request.StringLowerThan:\n\t\treturn s < target.Target\n\tcase request.StringLowerThanEqual:\n\t\treturn s <= target.Target\n\t}\n\n\treturn false\n}\n\nfunc (target HeaderTarget) HeaderEvaluate(s string) bool {\n\theaders := make(map[string]any)\n\n\tif err := json.Unmarshal([]byte(s), &headers); err != nil {\n\t\treturn false\n\t}\n\n\tv, found := headers[target.Key]\n\tif !found {\n\t\treturn false\n\t}\n\n\tt := StringTargetType{Comparator: target.Comparator, Target: target.Target}\n\t// convert all headers to array\n\tstr := fmt.Sprintf(\"%v\", v)\n\n\treturn t.StringEvaluate(str)\n}\n\nfunc (target StatusTarget) StatusEvaluate(value int64) bool {\n\n\tswitch target.Comparator {\n\tcase request.NumberEquals:\n\t\tif target.Target != value {\n\t\t\treturn false\n\t\t}\n\tcase request.NumberNotEquals:\n\t\tif target.Target == value {\n\t\t\treturn false\n\t\t}\n\n\tcase request.NumberGreaterThan:\n\t\tif target.Target >= value {\n\t\t\treturn false\n\t\t}\n\tcase request.NumberGreaterThanEqual:\n\t\tif target.Target > value {\n\t\t\treturn false\n\t\t}\n\tcase request.NumberLowerThan:\n\t\tif target.Target <= value {\n\t\t\treturn false\n\t\t}\n\tcase request.NumberLowerThanEqual:\n\t\tif target.Target < value {\n\t\t\treturn false\n\t\t}\n\tdefault:\n\t\tfmt.Println(\"something strange \", target)\n\t}\n\treturn true\n}\n\nfunc (target RecordTarget) RecordEvaluate(s []string) bool {\n\tswitch target.Comparator {\n\tcase request.RecordEquals:\n\t\tif !slices.Contains(s, target.Target) {\n\t\t\treturn false\n\t\t}\n\n\tcase request.RecordNotEquals:\n\t\tif slices.Contains(s, target.Target) {\n\t\t\treturn false\n\t\t}\n\n\tcase request.RecordContains:\n\tfor _, record := range s {\n\t\t\tif strings.Contains(record, target.Target) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\treturn false\n\tcase request.RecordNotContains:\n\t\tfor _, record := range s {\n\t\t\tif strings.Contains(record, target.Target) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "apps/checker/pkg/assertions/assertions_test.go",
    "content": "package assertions\n\nimport (\n\t\"testing\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n)\n\nfunc TestIntTarget_IntEvaluate(t *testing.T) {\n\ttype fields struct {\n\t\tAssertionType request.AssertionType\n\t\tComparator    request.NumberComparator\n\t\tTarget        int64\n\t}\n\ttype args struct {\n\t\tvalue int64\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   bool\n\t}{\n\t\t{\n\t\t\tname: \"Equals true\", fields: fields{Comparator: request.NumberEquals, Target: 200}, args: args{value: 200}, want: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Equals false\", fields: fields{Comparator: request.NumberEquals, Target: 200}, args: args{value: 201}, want: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Not Equals true\", fields: fields{Comparator: request.NumberNotEquals, Target: 200}, args: args{value: 201}, want: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Not Equals false\", fields: fields{Comparator: request.NumberNotEquals, Target: 200}, args: args{value: 200}, want: false,\n\t\t},\n\t\t{\n\t\t\tname: \"greater than true\", fields: fields{Comparator: request.NumberGreaterThan, Target: 200}, args: args{value: 201}, want: true,\n\t\t},\n\t\t{\n\t\t\tname: \"greater than false 1\", fields: fields{Comparator: request.NumberGreaterThan, Target: 200}, args: args{value: 200}, want: false,\n\t\t},\n\t\t{\n\t\t\tname: \"greater than false\", fields: fields{Comparator: request.NumberGreaterThan, Target: 200}, args: args{value: 199}, want: false,\n\t\t},\n\t\t{\n\t\t\tname: \"greater than equal true\", fields: fields{Comparator: request.NumberGreaterThanEqual, Target: 200}, args: args{value: 201}, want: true,\n\t\t},\n\t\t{\n\t\t\tname: \"greater than equal true 1\", fields: fields{Comparator: request.NumberGreaterThanEqual, Target: 200}, args: args{value: 200}, want: true,\n\t\t},\n\t\t{\n\t\t\tname: \"greater than equal false\", fields: fields{Comparator: request.NumberGreaterThanEqual, Target: 200}, args: args{value: 199}, want: false,\n\t\t},\n\t\t{\n\t\t\tname: \"lower than true\", fields: fields{Comparator: request.NumberLowerThan, Target: 200}, args: args{value: 199}, want: true,\n\t\t},\n\t\t{\n\t\t\tname: \"lower  than false 1\", fields: fields{Comparator: request.NumberLowerThan, Target: 200}, args: args{value: 200}, want: false,\n\t\t},\n\t\t{\n\t\t\tname: \"lower than false\", fields: fields{Comparator: request.NumberLowerThan, Target: 200}, args: args{value: 201}, want: false,\n\t\t},\n\t\t{\n\t\t\tname: \"lower than Equal true\", fields: fields{Comparator: request.NumberLowerThanEqual, Target: 200}, args: args{value: 199}, want: true,\n\t\t},\n\t\t{\n\t\t\tname: \"lower  than  equal true 1\", fields: fields{Comparator: request.NumberLowerThanEqual, Target: 200}, args: args{value: 200}, want: true,\n\t\t},\n\t\t{\n\t\t\tname: \"lower than equal false\", fields: fields{Comparator: request.NumberLowerThan, Target: 200}, args: args{value: 201}, want: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttarget := StatusTarget{\n\t\t\t\tAssertionType: tt.fields.AssertionType,\n\t\t\t\tComparator:    tt.fields.Comparator,\n\t\t\t\tTarget:        tt.fields.Target,\n\t\t\t}\n\t\t\tif got := target.StatusEvaluate(tt.args.value); got != tt.want {\n\t\t\t\tt.Errorf(\"IntTarget.IntEvaluate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHeaderTarget_HeaderEvaluate(t *testing.T) {\n\ttype fields struct {\n\t\tAssertionType request.AssertionType\n\t\tComparator    request.StringComparator\n\t\tTarget        string\n\t\tKey           string\n\t}\n\ttype args struct {\n\t\ts string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   bool\n\t}{\n\t\t{name: \"Header 1\", fields: fields{Comparator: request.StringEmpty, Target: \"\", Key: \"headers1\"}, args: args{s: `{\"Content-Type\":\"text/plain;charset=UTF-8\",\"Strict-Transport-Security\":\"max-age=3153600000\",\"Vary\":\"Accept-Encoding\"}`}, want: false},\n\t\t{name: \"Header 2\", fields: fields{Comparator: request.StringNotEmpty, Target: \"\", Key: \"headers1\"}, args: args{s: `{\"Content-Type\":\"text/plain;charset=UTF-8\",\"Strict-Transport-Security\":\"max-age=3153600000\",\"headers1\":\"Accept-Encoding\"}`}, want: true},\n\t\t{name: \"it should return false if it can not decode the headers\", fields: fields{Comparator: request.StringContains, Target: \"Accept-Encoding\", Key: \"Vary\"}, args: args{s: `}`}, want: false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttarget := HeaderTarget{\n\t\t\t\tAssertionType: tt.fields.AssertionType,\n\t\t\t\tComparator:    tt.fields.Comparator,\n\t\t\t\tTarget:        tt.fields.Target,\n\t\t\t\tKey:           tt.fields.Key,\n\t\t\t}\n\t\t\tif got := target.HeaderEvaluate(tt.args.s); got != tt.want {\n\t\t\t\tt.Errorf(\"HeaderTarget.HeaderEvaluate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRecordTarget_RecordEvaluate(t *testing.T) {\n\ttype fields struct {\n\t\tComparator request.RecordComparator\n\t\tTarget     string\n\t}\n\ttype args struct {\n\t\ts []string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   bool\n\t}{\n\t\t{\n\t\t\tname:   \"RecordEquals true\",\n\t\t\tfields: fields{Comparator: request.RecordEquals, Target: \"foo\"},\n\t\t\targs:   args{s: []string{\"foo\", \"bar\"}},\n\t\t\twant:   true,\n\t\t},\n\t\t{\n\t\t\tname:   \"RecordEquals false\",\n\t\t\tfields: fields{Comparator: request.RecordEquals, Target: \"baz\"},\n\t\t\targs:   args{s: []string{\"foo\", \"bar\"}},\n\t\t\twant:   false,\n\t\t},\n\t\t{\n\t\t\tname:   \"RecordNotEquals true\",\n\t\t\tfields: fields{Comparator: request.RecordNotEquals, Target: \"baz\"},\n\t\t\targs:   args{s: []string{\"foo\", \"bar\"}},\n\t\t\twant:   true,\n\t\t},\n\t\t{\n\t\t\tname:   \"RecordNotEquals false\",\n\t\t\tfields: fields{Comparator: request.RecordNotEquals, Target: \"foo\"},\n\t\t\targs:   args{s: []string{\"foo\", \"bar\"}},\n\t\t\twant:   false,\n\t\t},\n\t\t{\n\t\t\tname:   \"RecordContains true\",\n\t\t\tfields: fields{Comparator: request.RecordContains, Target: \"ba\"},\n\t\t\targs:   args{s: []string{\"foo\", \"bar\"}},\n\t\t\twant:   true,\n\t\t},\n\t\t{\n\t\t\tname:   \"RecordContains false\",\n\t\t\tfields: fields{Comparator: request.RecordContains, Target: \"baz\"},\n\t\t\targs:   args{s: []string{\"foo\", \"bar\"}},\n\t\t\twant:   false,\n\t\t},\n\t\t{\n\t\t\tname:   \"RecordNotContains true\",\n\t\t\tfields: fields{Comparator: request.RecordNotContains, Target: \"baz\"},\n\t\t\targs:   args{s: []string{\"foo\", \"bar\"}},\n\t\t\twant:   true,\n\t\t},\n\t\t{\n\t\t\tname:   \"RecordNotContains false\",\n\t\t\tfields: fields{Comparator: request.RecordNotContains, Target: \"ba\"},\n\t\t\targs:   args{s: []string{\"foo\", \"bar\"}},\n\t\t\twant:   false,\n\t\t},\n\t\t{\n\t\t\tname:   \"Empty slice\",\n\t\t\tfields: fields{Comparator: request.RecordEquals, Target: \"foo\"},\n\t\t\targs:   args{s: []string{}},\n\t\t\twant:   false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttarget := RecordTarget{\n\t\t\t\tComparator: tt.fields.Comparator,\n\t\t\t\tTarget:     tt.fields.Target,\n\t\t\t}\n\t\t\tif got := target.RecordEvaluate(tt.args.s); got != tt.want {\n\t\t\t\tt.Errorf(\"RecordTarget.RecordEvaluate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "apps/checker/pkg/job/dns_job.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\n\tv1 \"github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1\"\n)\n\ntype DNSPrivateRegionData struct {}\n\nfunc (jobRunner) DNSJob(ctx context.Context, monitor *v1.DNSMonitor) ( *DNSPrivateRegionData, error) {\n\n\treturn nil,nil\n}\n"
  },
  {
    "path": "apps/checker/pkg/job/http_job.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/google/uuid\"\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/assertions\"\n\tv1 \"github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n)\n\nfunc ProtoNumberAssertionToComparator(assertion v1.NumberComparator) (request.NumberComparator, error) {\n\tswitch assertion {\n\tcase v1.NumberComparator_NUMBER_COMPARATOR_EQUAL:\n\t\treturn request.NumberEquals, nil\n\tcase v1.NumberComparator_NUMBER_COMPARATOR_NOT_EQUAL:\n\t\treturn request.NumberNotEquals, nil\n\tcase v1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN:\n\t\treturn request.NumberGreaterThan, nil\n\tcase v1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL:\n\t\treturn request.NumberGreaterThanEqual, nil\n\tcase v1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN:\n\t\treturn request.NumberLowerThan, nil\n\tcase v1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL:\n\t\treturn request.NumberLowerThanEqual, nil\n\tdefault:\n\n\t}\n\treturn \"\", fmt.Errorf(\"unknown comparator type: %v\", assertion)\n}\n\nfunc ProtoStringAssertionToComparator(assertion v1.StringComparator) (request.StringComparator, error) {\n\tswitch assertion {\n\tcase v1.StringComparator_STRING_COMPARATOR_CONTAINS:\n\t\treturn request.StringContains, nil\n\tcase v1.StringComparator_STRING_COMPARATOR_NOT_CONTAINS:\n\t\treturn request.StringNotContains, nil\n\tcase v1.StringComparator_STRING_COMPARATOR_EQUAL:\n\t\treturn request.StringEquals, nil\n\tcase v1.StringComparator_STRING_COMPARATOR_NOT_EQUAL:\n\t\treturn request.StringNotEquals, nil\n\tcase v1.StringComparator_STRING_COMPARATOR_EMPTY:\n\t\treturn request.StringEmpty, nil\n\tcase v1.StringComparator_STRING_COMPARATOR_NOT_EMPTY:\n\t\treturn request.StringNotEmpty, nil\n\t}\n\treturn \"\", fmt.Errorf(\"unknown comparator type: %v\", assertion)\n}\n\nfunc (jr jobRunner) HTTPJob(ctx context.Context, monitor *v1.HTTPMonitor) (*HttpPrivateRegionData, error) {\n\n\tretry := monitor.Retry\n\tif retry == 0 {\n\t\tretry = 3\n\t}\n\n\trequestClient := &http.Client{\n\t\tTimeout: time.Duration(monitor.Timeout) * time.Millisecond,\n\t}\n\tdefer requestClient.CloseIdleConnections()\n\n\tif !monitor.FollowRedirects {\n\t\trequestClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t}\n\t} else {\n\t\trequestClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\t\tif len(via) >= 10 {\n\t\t\t\treturn http.ErrUseLastResponse\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tvar degradedAfter int64\n\tif monitor.DegradedAt != nil {\n\t\tdegradedAfter = *monitor.DegradedAt\n\t}\n\n\theaders := make([]struct {\n\t\tKey   string `json:\"key\"`\n\t\tValue string `json:\"value\"`\n\t}, 0)\n\tif monitor.Headers != nil {\n\t\tfor _, header := range monitor.Headers {\n\t\t\theaders = append(headers, struct {\n\t\t\t\tKey   string `json:\"key\"`\n\t\t\t\tValue string `json:\"value\"`\n\t\t\t}{\n\t\t\t\tKey:   header.Key,\n\t\t\t\tValue: header.Value,\n\t\t\t})\n\t\t}\n\t}\n\n\treq := request.HttpCheckerRequest{\n\t\tURL:             monitor.Url,\n\t\tMonitorID:       monitor.Id,\n\t\tMethod:          monitor.Method,\n\t\tBody:            monitor.Body,\n\t\tRetry:           monitor.Retry,\n\t\tTimeout:         monitor.Timeout,\n\t\tDegradedAfter:   degradedAfter,\n\t\tFollowRedirects: monitor.FollowRedirects,\n\t\tHeaders:         headers,\n\t}\n\n\tvar called int\n\n\top := func() (*HttpPrivateRegionData, error) {\n\t\tcalled++\n\t\tres, err := checker.Http(ctx, requestClient, req)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to ping: %w\", err)\n\t\t}\n\n\t\ttimingBytes, err := json.Marshal(res.Timing)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error while parsing timing data %s: %w\", req.URL, err)\n\t\t}\n\t\theadersBytes, err := json.Marshal(res.Headers)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error while parsing headers %s: %w\", req.URL, err)\n\t\t}\n\t\tid, err := uuid.NewV7()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error while generating uuid: %w\", err)\n\t\t}\n\n\t\tstatus := statusCode(res.Status)\n\t\tisSuccessful := status.IsSuccessful()\n\t\tif len(monitor.HeaderAssertions) > 0 {\n\t\t\theadersAsString, err := json.Marshal(res.Headers)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error while parsing headers %s: %w\", req.URL, err)\n\t\t\t}\n\t\t\tfor _, assertion := range monitor.HeaderAssertions {\n\n\t\t\t\ta, err := ProtoStringAssertionToComparator(assertion.Comparator)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error while parsing header assertion comparator: %w\", err)\n\t\t\t\t}\n\t\t\t\tassert := assertions.HeaderTarget{\n\t\t\t\t\tComparator: a,\n\t\t\t\t\tTarget:     assertion.Target,\n\t\t\t\t\tKey:        assertion.Key,\n\t\t\t\t}\n\t\t\t\tassert.HeaderEvaluate(string(headersAsString))\n\t\t\t}\n\t\t}\n\n\t\tif len(monitor.StatusCodeAssertions) > 0 {\n\t\t\tfor _, assertion := range monitor.StatusCodeAssertions {\n\t\t\t\ta, err := ProtoNumberAssertionToComparator(assertion.Comparator)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error while parsing header assertion comparator: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tassert := assertions.StatusTarget{\n\t\t\t\t\tComparator: a,\n\t\t\t\t\tTarget:     assertion.Target,\n\t\t\t\t}\n\t\t\t\tisSuccessful = isSuccessful && assert.StatusEvaluate(int64(res.Status))\n\t\t\t}\n\t\t}\n\t\tif len(monitor.BodyAssertions) > 0 {\n\t\t\tfor _, assertion := range monitor.BodyAssertions {\n\t\t\t\ta, err := ProtoStringAssertionToComparator(assertion.Comparator)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error while parsing header assertion comparator: %w\", err)\n\t\t\t\t}\n\t\t\t\tassert := assertions.StringTargetType{\n\t\t\t\t\tComparator: a,\n\t\t\t\t\tTarget:     assertion.Target,\n\t\t\t\t}\n\t\t\t\tisSuccessful = isSuccessful && assert.StringEvaluate(res.Body)\n\t\t\t}\n\t\t}\n\n\t\trequestStatus := \"success\"\n\t\tif !isSuccessful {\n\t\t\trequestStatus = \"error\"\n\t\t} else if req.DegradedAfter > 0 && res.Latency > req.DegradedAfter {\n\t\t\trequestStatus = \"degraded\"\n\t\t}\n\n\t\tdata := HttpPrivateRegionData{\n\t\t\tID:            id.String(),\n\t\t\tLatency:       res.Latency,\n\t\t\tStatusCode:    res.Status,\n\t\t\tTimestamp:     res.Timestamp,\n\t\t\tCronTimestamp: res.Timestamp,\n\t\t\tURL:           req.URL,\n\t\t\t// Method:        req.Method,\n\t\t\tTiming:        string(timingBytes),\n\t\t\tHeaders:       string(headersBytes),\n\t\t\tBody:          \"\",\n\t\t\tRequestStatus: requestStatus,\n\t\t\t// Assertions:    assertionAsString,\n\t\t\tError: 0,\n\t\t}\n\n\t\tif isSuccessful {\n\t\t\tif req.DegradedAfter != 0 && res.Latency > req.DegradedAfter {\n\t\t\t\tdata.Body = res.Body\n\t\t\t}\n\t\t} else {\n\t\t\tdata.Error = 1\n\t\t\tif called < int(retry) {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to ping: %v with status %v\", res, res.Status)\n\t\t\t}\n\t\t}\n\n\t\treturn &data, nil\n\t}\n\n\tresp, err := backoff.Retry(ctx, op, backoff.WithMaxTries(uint(retry)), backoff.WithBackOff(backoff.NewExponentialBackOff()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "apps/checker/pkg/job/http_job_test.go",
    "content": "package job_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/job\"\n\tv1 \"github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// Save original checker.Http for restoration\n\nfunc TestHTTPJob_Success(t *testing.T) {\n\n\t// Mock checker.Http to simulate success\n\n\tmonitor := &v1.HTTPMonitor{\n\t\tUrl:     \"https://openstat.us\",\n\t\tMethod:  \"GET\",\n\t\tTimeout: 10000,\n\t\tRetry:   2,\n\t}\n\n\tdata, err := job.NewJobRunner().HTTPJob(context.Background(), monitor)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\tif data.RequestStatus != \"success\" {\n\t\tt.Errorf(\"expected RequestStatus 'success', got '%s'\", data.RequestStatus)\n\t}\n\tif data.Error != 0 {\n\t\tt.Errorf(\"expected Error 0, got %d\", data.Error)\n\t}\n}\n\nfunc TestHTTPJob_Failure(t *testing.T) {\n\n\tmonitor := &v1.HTTPMonitor{\n\t\tUrl:     \"https://localhost:1234\",\n\t\tMethod:  \"GET\",\n\t\tTimeout: 1000,\n\t\tRetry:   1,\n\t}\n\n\tdata, err := job.NewJobRunner().HTTPJob(context.Background(), monitor)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error, got nil\")\n\t}\n\tif data != nil {\n\t\tt.Errorf(\"expected data to be nil on error, got %+v\", data)\n\t}\n}\n\nfunc TestProtoStringAssertionToComparator(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     v1.StringComparator\n\t\twant      request.StringComparator\n\t\texpectErr bool\n\t}{\n\t\t{\n\t\t\tname:      \"Contains\",\n\t\t\tinput:     v1.StringComparator_STRING_COMPARATOR_CONTAINS,\n\t\t\twant:      request.StringContains,\n\t\t\texpectErr: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"NotContains\",\n\t\t\tinput:     v1.StringComparator_STRING_COMPARATOR_NOT_CONTAINS,\n\t\t\twant:      request.StringNotContains,\n\t\t\texpectErr: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Equals\",\n\t\t\tinput:     v1.StringComparator_STRING_COMPARATOR_EQUAL,\n\t\t\twant:      request.StringEquals,\n\t\t\texpectErr: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"NotEquals\",\n\t\t\tinput:     v1.StringComparator_STRING_COMPARATOR_NOT_EQUAL,\n\t\t\twant:      request.StringNotEquals,\n\t\t\texpectErr: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Empty\",\n\t\t\tinput:     v1.StringComparator_STRING_COMPARATOR_EMPTY,\n\t\t\twant:      request.StringEmpty,\n\t\t\texpectErr: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"NotEmpty\",\n\t\t\tinput:     v1.StringComparator_STRING_COMPARATOR_NOT_EMPTY,\n\t\t\twant:      request.StringNotEmpty,\n\t\t\texpectErr: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Unknown\",\n\t\t\tinput:     v1.StringComparator(999),\n\t\t\twant:      \"\",\n\t\t\texpectErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := job.ProtoStringAssertionToComparator(tt.input)\n\t\t\tif tt.expectErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProtoNumberAssertionToComparator(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     v1.NumberComparator\n\t\twant      request.NumberComparator\n\t\texpectErr bool\n\t}{\n\t\t{\"Equal\", v1.NumberComparator_NUMBER_COMPARATOR_EQUAL, request.NumberEquals, false},\n\t\t{\"NotEqual\", v1.NumberComparator_NUMBER_COMPARATOR_NOT_EQUAL, request.NumberNotEquals, false},\n\t\t{\"GreaterThan\", v1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN, request.NumberGreaterThan, false},\n\t\t{\"GreaterThanOrEqual\", v1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL, request.NumberGreaterThanEqual, false},\n\t\t{\"LessThan\", v1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN, request.NumberLowerThan, false},\n\t\t{\"LessThanOrEqual\", v1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL, request.NumberLowerThanEqual, false},\n\t\t{\"Unknown\", v1.NumberComparator(999), \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := job.ProtoNumberAssertionToComparator(tt.input)\n\t\t\tif tt.expectErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "apps/checker/pkg/job/job.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\n\tv1 \"github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1\"\n)\n\ntype statusCode int\n\nfunc (s statusCode) IsSuccessful() bool {\n\treturn s >= 200 && s < 300\n}\n\ntype HttpPrivateRegionData struct {\n\tID            string `json:\"id\"`\n\tURL           string `json:\"url\"`\n\tMessage       string `json:\"message,omitempty\"`\n\tTiming        string `json:\"timing,omitempty\"`\n\tHeaders       string `json:\"headers,omitempty\"`\n\tBody          string `json:\"body,omitempty\"`\n\tRequestStatus string `json:\"requestStatus,omitempty\"`\n\tLatency       int64  `json:\"latency\"`\n\tCronTimestamp int64  `json:\"cronTimestamp\"`\n\tTimestamp     int64  `json:\"timestamp\"`\n\tStatusCode    int    `json:\"statusCode,omitempty\"`\n\tError         uint8  `json:\"error\"`\n}\n\ntype JobRunner interface {\n\tTCPJob(ctx context.Context, monitor *v1.TCPMonitor) (*TCPPrivateRegionData, error)\n\tHTTPJob(ctx context.Context, monitor *v1.HTTPMonitor) (*HttpPrivateRegionData, error)\n\tDNSJob(ctx context.Context, monitor *v1.DNSMonitor) (*DNSPrivateRegionData, error)\n}\n\ntype jobRunner struct{}\n\nfunc NewJobRunner() JobRunner {\n\treturn &jobRunner{}\n}\n"
  },
  {
    "path": "apps/checker/pkg/job/monitors.go",
    "content": "package job\n\ntype Monitor struct {\n\tID            int         `json:\"id\"`\n\tName          string      `json:\"name\"`\n\tURL           string      `json:\"url\"`\n\tPeriodicity   string      `json:\"periodicity\"`\n\tDescription   string      `json:\"description\"`\n\tMethod        string      `json:\"method\"`\n\tRegions       []string    `json:\"regions\"`\n\tActive        bool        `json:\"active\"`\n\tPublic        bool        `json:\"public\"`\n\tTimeout       int         `json:\"timeout\"`\n\tDegradedAfter int         `json:\"degraded_after,omitempty\"`\n\tBody          string      `json:\"body\"`\n\tHeaders       []Header    `json:\"headers,omitempty\"`\n\tAssertions    []Assertion `json:\"assertions,omitempty\"`\n\tRetry         int         `json:\"retry\"`\n\tJobType       string      `json:\"jobType\"`\n}\n\ntype Header struct {\n\tKey   string `json:\"key\"`\n\tValue string `json:\"value\"`\n}\n\ntype Assertion struct {\n\tType    string `json:\"type\"`\n\tCompare string `json:\"compare\"`\n\tKey     string `json:\"key\"`\n\tTarget  any    `json:\"target\"`\n}\n\ntype Timing struct {\n\tDnsStart          int64 `json:\"dnsStart\"`\n\tDnsDone           int64 `json:\"dnsDone\"`\n\tConnectStart      int64 `json:\"connectStart\"`\n\tConnectDone       int64 `json:\"connectDone\"`\n\tTlsHandshakeStart int64 `json:\"tlsHandshakeStart\"`\n\tTlsHandshakeDone  int64 `json:\"tlsHandshakeDone\"`\n\tFirstByteStart    int64 `json:\"firstByteStart\"`\n\tFirstByteDone     int64 `json:\"firstByteDone\"`\n\tTransferStart     int64 `json:\"transferStart\"`\n\tTransferDone      int64 `json:\"transferDone\"`\n}\n"
  },
  {
    "path": "apps/checker/pkg/job/tcp_job.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/google/uuid\"\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\tv1 \"github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1\"\n)\n\n// AssertionResult tracks the results of running assertions\ntype AssertionResult struct {\n\tType    string\n\tSuccess bool\n\tMessage string\n}\n\n// TCPPrivateRegionData represents the result of a TCP monitor check\ntype TCPPrivateRegionData struct {\n\tID            string `json:\"id\"`\n\tURI           string `json:\"uri\"`\n\tRequestStatus string `json:\"request_status\"`\n\tMessage       string `json:\"message\"`\n\tLatency       int64  `json:\"latency\"`\n\tTimestamp     int64  `json:\"timestamp\"`\n\tCronTimestamp int64  `json:\"cron_timestamp\"`\n\tError         int    `json:\"error\"`\n\tTiming        string `json:\"timing\"`\n}\n\n// runAssertions performs all configured assertions for TCP and returns their results\n\nfunc (jobRunner) TCPJob(ctx context.Context, monitor *v1.TCPMonitor) (*TCPPrivateRegionData, error) {\n\tretry := monitor.Retry\n\tif retry == 0 {\n\t\tretry = 3\n\t}\n\n\tvar degradedAfter int64\n\tif monitor.DegradedAt != nil {\n\t\tdegradedAfter = *monitor.DegradedAt\n\t}\n\n\tvar called int\n\n\top := func() (*TCPPrivateRegionData, error) {\n\t\tcalled++\n\t\tres, err := checker.PingTCP(int(monitor.Timeout), monitor.Uri)\n\t\tif err != nil {\n\t\t\tif called < int(retry) {\n\t\t\t\treturn nil, fmt.Errorf(\"TCP connection failed: %w\", err)\n\t\t\t}\n\t\t\t// On final attempt, return the error in the result\n\t\t\tid, uuidErr := uuid.NewV7()\n\t\t\tif uuidErr != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to generate UUID: %w\", uuidErr)\n\t\t\t}\n\n\t\t\treturn &TCPPrivateRegionData{\n\t\t\t\tID:            id.String(),\n\t\t\t\tLatency:       0,\n\t\t\t\tTimestamp:     res.TCPStart,\n\t\t\t\tCronTimestamp: res.TCPStart,\n\t\t\t\tURI:           monitor.Uri,\n\t\t\t\tRequestStatus: \"error\",\n\t\t\t\tError:         1,\n\t\t\t\tMessage:       err.Error(),\n\t\t\t}, nil\n\t\t}\n\n\t\tlatency := res.TCPDone - res.TCPStart\n\n\t\tvar requestStatus = \"active\"\n\n\t\tif degradedAfter > 0 && latency > degradedAfter {\n\t\t\trequestStatus = \"degraded\"\n\t\t}\n\n\t\tid, err := uuid.NewV7()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to generate UUID: %w\", err)\n\t\t}\n\t\ttimingAsString, err := json.Marshal(res)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error while parsing timing data %s: %w\", monitor.Uri, err)\n\t\t}\n\n\t\tdata := &TCPPrivateRegionData{\n\t\t\tID:            id.String(),\n\t\t\tLatency:       latency,\n\t\t\tTimestamp:     res.TCPStart,\n\t\t\tCronTimestamp: res.TCPStart,\n\t\t\tURI:           monitor.Uri,\n\t\t\tRequestStatus: requestStatus,\n\t\t\tError:         0,\n\t\t\tMessage:       fmt.Sprintf(\"Successfully connected to %s\", monitor.Uri),\n\t\t\tTiming:        string(timingAsString),\n\t\t}\n\n\t\treturn data, nil\n\t}\n\n\tresp, err := backoff.Retry(ctx, op,\n\t\tbackoff.WithMaxTries(uint(retry)),\n\t\tbackoff.WithBackOff(backoff.NewExponentialBackOff()),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"TCP job failed after %d retries: %w\", retry, err)\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "apps/checker/pkg/job/tcp_job_test.go",
    "content": "package job_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/job\"\n\tv1 \"github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1\"\n)\n\nfunc TestTCPJob_Success(t *testing.T) {\n\n\tmonitor := &v1.TCPMonitor{\n\t\tUri:     \"openstatus.dev:80\",\n\t\tTimeout: 1,\n\t\tRetry:   1,\n\t}\n\tdata, err := job.NewJobRunner().TCPJob(context.Background(), monitor)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\tif data.RequestStatus != \"active\" {\n\t\tt.Errorf(\"expected RequestStatus 'active', got '%s'\", data.RequestStatus)\n\t}\n\tif data.Error != 0 {\n\t\tt.Errorf(\"expected Error 0, got %d\", data.Error)\n\t}\n}\n\nfunc TestTCPJob_Failure(t *testing.T) {\n\n\tmonitor := &v1.TCPMonitor{\n\t\tUri:     \"localhost:1234\",\n\t\tTimeout: 1,\n\t\tRetry:   1,\n\t}\n\n\tdata, err := job.NewJobRunner().TCPJob(context.Background(), monitor)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\tif data.RequestStatus != \"error\" {\n\t\tt.Errorf(\"expected RequestStatus 'error', got '%s'\", data.RequestStatus)\n\t}\n\tif data.Error != 1 {\n\t\tt.Errorf(\"expected Error 1, got %d\", data.Error)\n\t}\n}\n"
  },
  {
    "path": "apps/checker/pkg/logger/logger.go",
    "content": "package logger\n\nimport (\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n)\n\nfunc Configure(logLevel string) {\n\tlevel, err := zerolog.ParseLevel(logLevel)\n\tif err != nil {\n\t\tlevel = zerolog.InfoLevel\n\t}\n\tzerolog.SetGlobalLevel(level)\n\n\tzerolog.DefaultContextLogger = func() *zerolog.Logger {\n\t\tlogger := log.With().Caller().Logger()\n\t\treturn &logger\n\t}()\n}\n"
  },
  {
    "path": "apps/checker/pkg/otel/otel.go",
    "content": "package otel\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp\"\n\t\"go.opentelemetry.io/otel/metric\"\n\tsdkMetrics \"go.opentelemetry.io/otel/sdk/metric\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.39.0\"\n)\n\nfunc setupOTelSDK(ctx context.Context, url string, headers map[string]string) (shutdown func(context.Context) error, err error) {\n\tres, err := newResource()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmeterProvider, err := newMeterProvider(ctx, res, url, headers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\totel.SetMeterProvider(meterProvider)\n\n\treturn meterProvider.Shutdown, nil\n}\n\nfunc newResource() (*resource.Resource, error) {\n\treturn resource.Merge(resource.Default(),\n\t\tresource.NewWithAttributes(semconv.SchemaURL,\n\t\t\tsemconv.ServiceName(\"openstatus-synthetic-check\"),\n\t\t\tsemconv.ServiceVersion(\"0.1.0\"),\n\t\t))\n}\n\nfunc newMeterProvider(\n\tctx context.Context,\n\tres *resource.Resource,\n\turl string,\n\theaders map[string]string,\n) (*sdkMetrics.MeterProvider, error) {\n\texporter, err := otlpmetrichttp.New(ctx,\n\t\totlpmetrichttp.WithEndpointURL(url),\n\t\totlpmetrichttp.WithHeaders(headers),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn sdkMetrics.NewMeterProvider(\n\t\tsdkMetrics.WithResource(res),\n\t\tsdkMetrics.WithReader(sdkMetrics.NewPeriodicReader(exporter,\n\t\t\tsdkMetrics.WithInterval(3*time.Second))),\n\t), nil\n}\n\n// withMeter sets up the OTel SDK, passes a Meter to the callback, then shuts down.\nfunc withMeter(ctx context.Context, endpoint string, headers map[string]string, fn func(metric.Meter)) {\n\tshutdown, err := setupOTelSDK(ctx, endpoint, headers)\n\tif err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"Error setting up otel\")\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tif err := shutdown(ctx); err != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"Error shutting down otel\")\n\t\t}\n\t}()\n\n\tfn(otel.Meter(\"OpenStatus\"))\n}\n\n// recordGauge creates a Float64Gauge and records a value.\nfunc recordGauge(ctx context.Context, meter metric.Meter, name, description string, value float64, att metric.MeasurementOption) error {\n\tgauge, err := meter.Float64Gauge(name,\n\t\tmetric.WithDescription(description), metric.WithUnit(\"ms\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgauge.Record(ctx, value, att)\n\n\treturn nil\n}\n\nfunc recordErrorCounter(ctx context.Context, meter metric.Meter, att metric.MeasurementOption) {\n\tcounter, err := meter.Int64Counter(\"openstatus.error\", metric.WithDescription(\"Status of the check\"))\n\tif err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"Error setting up counter\")\n\t\treturn\n\t}\n\n\tcounter.Add(ctx, 1, att)\n}\n\nfunc RecordHTTPMetrics(ctx context.Context, req request.HttpCheckerRequest, result checker.Response, region string) {\n\twithMeter(ctx, req.OtelConfig.Endpoint, req.OtelConfig.Headers, func(meter metric.Meter) {\n\t\tatt := metric.WithAttributes(\n\t\t\tattribute.String(\"openstatus.probes\", region),\n\t\t\tattribute.String(\"openstatus.target\", req.URL),\n\t\t\tsemconv.HTTPResponseStatusCode(result.Status),\n\t\t)\n\n\t\tif result.Error != \"\" {\n\t\t\trecordErrorCounter(ctx, meter, att)\n\t\t\treturn\n\t\t}\n\n\t\tstatus, err := meter.Int64Counter(\"openstatus.status\", metric.WithDescription(\"Status of the check\"))\n\t\tif err != nil {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"Error setting up counter\")\n\t\t}\n\n\t\tstatus.Add(ctx, 1, att)\n\n\t\ttimings := []struct {\n\t\t\tname        string\n\t\t\tdescription string\n\t\t\tvalue       float64\n\t\t}{\n\t\t\t{\"openstatus.http.request.duration\", \"Duration of the check\", float64(result.Latency)},\n\t\t\t{\"openstatus.http.dns.duration\", \"Duration of the DNS lookup\", float64(result.Timing.DnsDone - result.Timing.DnsStart)},\n\t\t\t{\"openstatus.http.connection.duration\", \"Duration of the connection\", float64(result.Timing.ConnectDone - result.Timing.ConnectStart)},\n\t\t\t{\"openstatus.http.tls.duration\", \"Duration of the TLS handshake\", float64(result.Timing.TlsHandshakeDone - result.Timing.TlsHandshakeStart)},\n\t\t\t{\"openstatus.http.ttfb.duration\", \"Duration of the TTFB\", float64(result.Timing.FirstByteDone - result.Timing.FirstByteStart)},\n\t\t\t{\"openstatus.http.transfer.duration\", \"Duration of the transfer\", float64(result.Timing.TransferDone - result.Timing.TransferStart)},\n\t\t}\n\n\t\tfor _, t := range timings {\n\t\t\tif err := recordGauge(ctx, meter, t.name, t.description, t.value, att); err != nil {\n\t\t\t\tlog.Ctx(ctx).Error().Err(err).Str(\"metric\", t.name).Msg(\"Error creating gauge\")\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc RecordTCPMetrics(ctx context.Context, req request.TCPCheckerRequest, result checker.TCPResponse, region string) {\n\twithMeter(ctx, req.OtelConfig.Endpoint, req.OtelConfig.Headers, func(meter metric.Meter) {\n\t\tatt := metric.WithAttributes(\n\t\t\tattribute.String(\"openstatus.probes\", region),\n\t\t\tattribute.String(\"openstatus.target\", req.URI),\n\t\t)\n\n\t\tif result.Error == 1 {\n\t\t\trecordErrorCounter(ctx, meter, att)\n\t\t\treturn\n\t\t}\n\n\t\ttimings := []struct {\n\t\t\tname        string\n\t\t\tdescription string\n\t\t\tvalue       float64\n\t\t}{\n\t\t\t{\"openstatus.tcp.request.duration\", \"Duration of the check\", float64(result.Latency)},\n\t\t\t{\"openstatus.tcp.tcp.duration\", \"Duration of the TCP connection\", float64(result.Timing.TCPDone - result.Timing.TCPStart)},\n\t\t}\n\n\t\tfor _, t := range timings {\n\t\t\tif err := recordGauge(ctx, meter, t.name, t.description, t.value, att); err != nil {\n\t\t\t\tlog.Ctx(ctx).Error().Err(err).Str(\"metric\", t.name).Msg(\"Error creating gauge\")\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "apps/checker/pkg/otel/otel_test.go",
    "content": "package otel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/checker\"\n\t\"github.com/openstatushq/openstatus/apps/checker/request\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/metric\"\n\tsdkMetrics \"go.opentelemetry.io/otel/sdk/metric\"\n\t\"go.opentelemetry.io/otel/sdk/metric/metricdata\"\n)\n\nfunc newTestMeter(t *testing.T) (metric.Meter, *sdkMetrics.ManualReader) {\n\tt.Helper()\n\treader := sdkMetrics.NewManualReader()\n\tprovider := sdkMetrics.NewMeterProvider(sdkMetrics.WithReader(reader))\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, provider.Shutdown(context.Background()))\n\t})\n\treturn provider.Meter(\"test\"), reader\n}\n\nfunc collectMetrics(t *testing.T, reader *sdkMetrics.ManualReader) metricdata.ResourceMetrics {\n\tt.Helper()\n\tvar rm metricdata.ResourceMetrics\n\trequire.NoError(t, reader.Collect(context.Background(), &rm))\n\treturn rm\n}\n\nfunc newOTLPTestServer(t *testing.T) *httptest.Server {\n\tt.Helper()\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tt.Cleanup(server.Close)\n\treturn server\n}\n\n// --- recordGauge tests ---\n\nfunc TestRecordGauge(t *testing.T) {\n\tmeter, reader := newTestMeter(t)\n\tctx := context.Background()\n\tatt := metric.WithAttributes(attribute.String(\"test.key\", \"test-value\"))\n\n\terr := recordGauge(ctx, meter, \"test.gauge\", \"A test gauge\", 42.5, att)\n\trequire.NoError(t, err)\n\n\trm := collectMetrics(t, reader)\n\trequire.Len(t, rm.ScopeMetrics, 1)\n\n\tsm := rm.ScopeMetrics[0]\n\trequire.Len(t, sm.Metrics, 1)\n\n\tm := sm.Metrics[0]\n\tassert.Equal(t, \"test.gauge\", m.Name)\n\tassert.Equal(t, \"A test gauge\", m.Description)\n\tassert.Equal(t, \"ms\", m.Unit)\n\n\tgauge, ok := m.Data.(metricdata.Gauge[float64])\n\trequire.True(t, ok, \"expected Gauge[float64] data type\")\n\trequire.Len(t, gauge.DataPoints, 1)\n\tassert.Equal(t, 42.5, gauge.DataPoints[0].Value)\n\n\tattrs := gauge.DataPoints[0].Attributes\n\tval, found := attrs.Value(attribute.Key(\"test.key\"))\n\tassert.True(t, found)\n\tassert.Equal(t, \"test-value\", val.AsString())\n}\n\nfunc TestRecordGauge_MultipleMetrics(t *testing.T) {\n\tmeter, reader := newTestMeter(t)\n\tctx := context.Background()\n\tatt := metric.WithAttributes(attribute.String(\"region\", \"us-east-1\"))\n\n\tgauges := []struct {\n\t\tname  string\n\t\tdesc  string\n\t\tvalue float64\n\t}{\n\t\t{\"http.dns.duration\", \"DNS duration\", 10.0},\n\t\t{\"http.tls.duration\", \"TLS duration\", 25.0},\n\t\t{\"http.ttfb.duration\", \"TTFB duration\", 50.0},\n\t}\n\n\tfor _, g := range gauges {\n\t\trequire.NoError(t, recordGauge(ctx, meter, g.name, g.desc, g.value, att))\n\t}\n\n\trm := collectMetrics(t, reader)\n\trequire.Len(t, rm.ScopeMetrics, 1)\n\tassert.Len(t, rm.ScopeMetrics[0].Metrics, 3)\n}\n\n// --- recordErrorCounter tests ---\n\nfunc TestRecordErrorCounter(t *testing.T) {\n\tmeter, reader := newTestMeter(t)\n\tctx := context.Background()\n\tatt := metric.WithAttributes(attribute.String(\"region\", \"us-east-1\"))\n\n\trecordErrorCounter(ctx, meter, att)\n\n\trm := collectMetrics(t, reader)\n\trequire.Len(t, rm.ScopeMetrics, 1)\n\trequire.Len(t, rm.ScopeMetrics[0].Metrics, 1)\n\n\tm := rm.ScopeMetrics[0].Metrics[0]\n\tassert.Equal(t, \"openstatus.error\", m.Name)\n\n\tsum, ok := m.Data.(metricdata.Sum[int64])\n\trequire.True(t, ok, \"expected Sum[int64] data type\")\n\trequire.Len(t, sum.DataPoints, 1)\n\tassert.Equal(t, int64(1), sum.DataPoints[0].Value)\n}\n\n// --- setupOTelSDK tests ---\n\nfunc TestSetupOTelSDK(t *testing.T) {\n\tserver := newOTLPTestServer(t)\n\tctx := context.Background()\n\n\tshutdown, err := setupOTelSDK(ctx, server.URL, nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, shutdown)\n\tassert.NoError(t, shutdown(ctx))\n}\n\nfunc TestSetupOTelSDK_InvalidURL(t *testing.T) {\n\tctx := context.Background()\n\n\tshutdown, err := setupOTelSDK(ctx, \"://invalid\", nil)\n\tif err != nil {\n\t\tassert.Nil(t, shutdown, \"shutdown should be nil when setup fails\")\n\t} else {\n\t\trequire.NotNil(t, shutdown)\n\t\t_ = shutdown(ctx)\n\t}\n}\n\n// --- withMeter tests ---\n\nfunc TestWithMeter(t *testing.T) {\n\tserver := newOTLPTestServer(t)\n\tcalled := false\n\n\twithMeter(context.Background(), server.URL, nil, func(meter metric.Meter) {\n\t\tcalled = true\n\t\tassert.NotNil(t, meter)\n\t})\n\n\tassert.True(t, called, \"callback should be called\")\n}\n\nfunc TestWithMeter_InvalidEndpoint(t *testing.T) {\n\tcalled := false\n\n\t// Must not panic. The OTLP exporter no longer fails at creation for\n\t// invalid URLs (it defers the error to export time), so the callback\n\t// will still be invoked.\n\twithMeter(context.Background(), \"://invalid\", nil, func(meter metric.Meter) {\n\t\tcalled = true\n\t})\n\n\tassert.True(t, called, \"callback should be called since exporter defers URL validation\")\n}\n\n// --- RecordHTTPMetrics tests ---\n\nfunc TestRecordHTTPMetrics_Success(t *testing.T) {\n\tserver := newOTLPTestServer(t)\n\n\treq := request.HttpCheckerRequest{\n\t\tURL:       \"https://example.com\",\n\t\tMonitorID: \"mon-1\",\n\t}\n\treq.OtelConfig.Endpoint = server.URL\n\n\tresult := checker.Response{\n\t\tStatus:  200,\n\t\tLatency: 150,\n\t\tTiming: checker.Timing{\n\t\t\tDnsStart:          0,\n\t\t\tDnsDone:           10,\n\t\t\tConnectStart:      10,\n\t\t\tConnectDone:       30,\n\t\t\tTlsHandshakeStart: 30,\n\t\t\tTlsHandshakeDone:  55,\n\t\t\tFirstByteStart:    55,\n\t\t\tFirstByteDone:     100,\n\t\t\tTransferStart:     100,\n\t\t\tTransferDone:      150,\n\t\t},\n\t}\n\n\t// Should not panic.\n\tRecordHTTPMetrics(context.Background(), req, result, \"us-east-1\")\n}\n\nfunc TestRecordHTTPMetrics_Error(t *testing.T) {\n\tserver := newOTLPTestServer(t)\n\n\treq := request.HttpCheckerRequest{\n\t\tURL:       \"https://example.com\",\n\t\tMonitorID: \"mon-1\",\n\t}\n\treq.OtelConfig.Endpoint = server.URL\n\n\tresult := checker.Response{\n\t\tStatus: 500,\n\t\tError:  \"connection refused\",\n\t}\n\n\t// Should record error counter and not panic.\n\tRecordHTTPMetrics(context.Background(), req, result, \"us-east-1\")\n}\n\nfunc TestRecordHTTPMetrics_SetupFailure(t *testing.T) {\n\treq := request.HttpCheckerRequest{\n\t\tURL:       \"https://example.com\",\n\t\tMonitorID: \"mon-1\",\n\t}\n\treq.OtelConfig.Endpoint = \"://invalid\"\n\n\tresult := checker.Response{\n\t\tStatus:  200,\n\t\tLatency: 100,\n\t}\n\n\t// Must not panic — this was the original nil pointer bug.\n\tRecordHTTPMetrics(context.Background(), req, result, \"us-east-1\")\n}\n\n// --- RecordTCPMetrics tests ---\n\nfunc TestRecordTCPMetrics_Success(t *testing.T) {\n\tserver := newOTLPTestServer(t)\n\n\treq := request.TCPCheckerRequest{\n\t\tURI:       \"example.com:443\",\n\t\tMonitorID: \"mon-2\",\n\t}\n\treq.OtelConfig.Endpoint = server.URL\n\n\tresult := checker.TCPResponse{\n\t\tLatency: 45,\n\t\tTiming: checker.TCPResponseTiming{\n\t\t\tTCPStart: 0,\n\t\t\tTCPDone:  45,\n\t\t},\n\t}\n\n\t// Should not panic.\n\tRecordTCPMetrics(context.Background(), req, result, \"us-east-1\")\n}\n\nfunc TestRecordTCPMetrics_Error(t *testing.T) {\n\tserver := newOTLPTestServer(t)\n\n\treq := request.TCPCheckerRequest{\n\t\tURI:       \"example.com:443\",\n\t\tMonitorID: \"mon-2\",\n\t}\n\treq.OtelConfig.Endpoint = server.URL\n\n\tresult := checker.TCPResponse{\n\t\tError: 1,\n\t}\n\n\t// Should record error counter and not panic.\n\tRecordTCPMetrics(context.Background(), req, result, \"us-east-1\")\n}\n\nfunc TestRecordTCPMetrics_SetupFailure(t *testing.T) {\n\treq := request.TCPCheckerRequest{\n\t\tURI:       \"example.com:443\",\n\t\tMonitorID: \"mon-2\",\n\t}\n\treq.OtelConfig.Endpoint = \"://invalid\"\n\n\tresult := checker.TCPResponse{\n\t\tLatency: 45,\n\t}\n\n\t// Must not panic — same nil pointer guard as HTTP.\n\tRecordTCPMetrics(context.Background(), req, result, \"us-east-1\")\n}\n"
  },
  {
    "path": "apps/checker/pkg/scheduler/scheduler.go",
    "content": "package scheduler\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"sync\"\n\t\"time\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/madflojo/tasks\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/job\"\n\tv1 \"github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1\"\n)\n\nconst (\n\tInterval10s = \"10s\"\n\tInterval30s = \"30s\"\n\tInterval1m  = \"1m\"\n\tInterval5m  = \"5m\"\n\tInterval10m = \"10m\"\n\tInterval30m = \"30m\"\n\tInterval1h  = \"1h\"\n)\n\ntype MonitorManager struct {\n\tClient    v1.PrivateLocationServiceClient\n\tJobRunner job.JobRunner\n\tScheduler *tasks.Scheduler\n\tmu        sync.Mutex\n}\n\n// UpdateMonitors fetches the latest monitors and starts/stops jobs as needed\nfunc (mm *MonitorManager) UpdateMonitors(ctx context.Context) {\n\tres, err := mm.Client.Monitors(ctx, &connect.Request[v1.MonitorsRequest]{})\n\tif err != nil {\n\t\tlog.Printf(\"Failed to fetch monitors: %v\", err)\n\t\treturn\n\t}\n\n\tcurrentIDs := make(map[string]struct{})\n\n\t// HTTP monitors: start jobs for new monitors\n\tfor _, m := range res.Msg.HttpMonitors {\n\t\tcurrentIDs[m.Id] = struct{}{}\n\t\t_, err := mm.Scheduler.Lookup(m.Id)\n\t\tif err != nil {\n\t\t\tinterval := time.Duration(intervalToSecond(m.Periodicity)) * time.Second\n\t\t\ttask := tasks.Task{\n\t\t\t\tInterval:          interval,\n\t\t\t\tRunOnce:           false,\n\t\t\t\tRunSingleInstance: true,\n\t\t\t\t// StartAfter: time.Duration(1) * time.Second,\n\t\t\t\tErrFunc: func(e error) {\n\t\t\t\t\tlog.Printf(\"An error occurred when executing task  %s\", e)\n\t\t\t\t},\n\t\t\t\tFuncWithTaskContext: func(ctx tasks.TaskContext) error {\n\t\t\t\t\tmonitor := m\n\t\t\t\t\tc := context.Background()\n\t\t\t\t\tlog.Printf(\"Starting job for monitor %s (%s)\", monitor.Id, monitor.Url)\n\t\t\t\t\tdata, err := mm.JobRunner.HTTPJob(c, monitor)\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"Monitor check failed for %s (%s): %v\", monitor.Id, monitor.Url, err)\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tresp, ingestErr := mm.Client.IngestHTTP(c, &connect.Request[v1.IngestHTTPRequest]{\n\t\t\t\t\t\tMsg: &v1.IngestHTTPRequest{\n\t\t\t\t\t\t\tMonitorId:     monitor.Id,\n\t\t\t\t\t\t\tId:            data.ID,\n\t\t\t\t\t\t\tUrl:           monitor.Url,\n\t\t\t\t\t\t\tMessage:       data.Message,\n\t\t\t\t\t\t\tLatency:       data.Latency,\n\t\t\t\t\t\t\tTiming:        data.Timing,\n\t\t\t\t\t\t\tHeaders:       data.Headers,\n\t\t\t\t\t\t\tBody:          data.Body,\n\t\t\t\t\t\t\tRequestStatus: data.RequestStatus,\n\t\t\t\t\t\t\tStatusCode:    int64(data.StatusCode),\n\t\t\t\t\t\t\tError:         int64(data.Error),\n\t\t\t\t\t\t\tCronTimestamp: data.CronTimestamp,\n\t\t\t\t\t\t\tTimestamp:     data.Timestamp,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\tif ingestErr != nil {\n\t\t\t\t\t\tlog.Printf(\"Failed to ingest HTTP result for %s (%s): %v\", monitor.Id, monitor.Url, ingestErr)\n\t\t\t\t\t\treturn ingestErr\n\t\t\t\t\t}\n\t\t\t\t\tlog.Printf(\"Monitor check succeeded for %s (%s), ingest response: %v\", monitor.Id, monitor.Url, resp)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\terr := mm.Scheduler.AddWithID(m.Id, &task)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to add HTTP monitor job for %s (%s): %v\", m.Id, m.Url, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Printf(\"Started monitoring job for %s (%s)\", m.Id, m.Url)\n\t\t\tcontinue\n\t\t}\n\n\t}\n\n\t// TCP monitors: start jobs for new monitors\n\tfor _, m := range res.Msg.TcpMonitors {\n\t\tcurrentIDs[m.Id] = struct{}{}\n\t\t_, err := mm.Scheduler.Lookup(m.Id)\n\t\tif err != nil {\n\n\t\t\tinterval := time.Duration(intervalToSecond(m.Periodicity)) * time.Second\n\t\t\ttask := tasks.Task{\n\t\t\t\tInterval: interval,\n\t\t\t\tRunOnce:  false,\n\t\t\t\t// StartAfter: time.Now().Add(5 * time.Millisecond),\n\t\t\t\tRunSingleInstance: true,\n\t\t\t\tFuncWithTaskContext: func(ctx tasks.TaskContext) error {\n\n\t\t\t\t\tmonitor := m\n\t\t\t\t\tlog.Printf(\"Starting TCP job for monitor %s (%s)\", monitor.Id, monitor.Uri)\n\t\t\t\t\tdata, err := mm.JobRunner.TCPJob(ctx.Context, monitor)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"TCP monitor check failed for %s (%s): %v\", monitor.Id, monitor.Uri, err)\n\t\t\t\t\t}\n\t\t\t\t\tresp, ingestErr := mm.Client.IngestTCP(ctx.Context, &connect.Request[v1.IngestTCPRequest]{\n\t\t\t\t\t\tMsg: &v1.IngestTCPRequest{\n\t\t\t\t\t\t\tMonitorId:     monitor.Id,\n\t\t\t\t\t\t\tId:            data.ID,\n\t\t\t\t\t\t\tUri:           monitor.Uri,\n\t\t\t\t\t\t\tMessage:       data.Message,\n\t\t\t\t\t\t\tLatency:       data.Latency,\n\t\t\t\t\t\t\tRequestStatus: data.RequestStatus,\n\t\t\t\t\t\t\tError:         int64(data.Error),\n\t\t\t\t\t\t\tCronTimestamp: data.CronTimestamp,\n\t\t\t\t\t\t\tTimestamp:     data.Timestamp,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\tif ingestErr != nil {\n\t\t\t\t\t\tlog.Printf(\"Failed to ingest TCP result for %s (%s): %v\", monitor.Id, monitor.Uri, ingestErr)\n\t\t\t\t\t\treturn ingestErr\n\t\t\t\t\t}\n\t\t\t\t\tlog.Printf(\"TCP monitor check succeeded for %s (%s), ingest response: %v\", monitor.Id, monitor.Uri, resp)\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\terr := mm.Scheduler.AddWithID(m.Id, &task)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to add TCP monitor job for %s (%s): %v\", m.Id, m.Uri, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Printf(\"Started TCP monitoring job for %s (%s)\", m.Id, m.Uri)\n\t\t}\n\t}\n\n\tfor _, m := range res.Msg.DnsMonitors {\n\t\tcurrentIDs[m.Id] = struct{}{}\n\t\t_, err := mm.Scheduler.Lookup(m.Id)\n\t\tif err != nil {\n\n\t\t\tinterval := time.Duration(intervalToSecond(m.Periodicity)) * time.Second\n\t\t\ttask := tasks.Task{\n\t\t\t\tInterval: interval,\n\t\t\t\tRunOnce:  false,\n\t\t\t\t// StartAfter: time.Now().Add(5 * time.Millisecond),\n\t\t\t\tRunSingleInstance: true,\n\t\t\t\tFuncWithTaskContext: func(ctx tasks.TaskContext) error {\n\n\t\t\t\t\tmonitor := m\n\t\t\t\t\tlog.Printf(\"Starting TCP job for monitor %s (%s)\", monitor.Id, monitor.Uri)\n\t\t\t\t\t_, err := mm.JobRunner.DNSJob(ctx.Context, monitor)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"TCP monitor check failed for %s (%s): %v\", monitor.Id, monitor.Uri, err)\n\t\t\t\t\t}\n\t\t\t\t\tresp, ingestErr := mm.Client.IngestDNS(ctx.Context, &connect.Request[v1.IngestDNSRequest]{\n\t\t\t\t\t\tMsg: &v1.IngestDNSRequest{\n\t\t\t\t\t\t\tMonitorId: monitor.Id,\n\n\t\t\t\t\t\t\t// Id:            data.ID,\n\t\t\t\t\t\t\t// Uri:           monitor.Uri,\n\t\t\t\t\t\t\t// Message:       data.Message,\n\t\t\t\t\t\t\t// Latency:       data.Latency,\n\t\t\t\t\t\t\t// RequestStatus: data.RequestStatus,\n\t\t\t\t\t\t\t// Error:         int64(data.Error),\n\t\t\t\t\t\t\t// CronTimestamp: data.CronTimestamp,\n\t\t\t\t\t\t\t// Timestamp:     data.Timestamp,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\tif ingestErr != nil {\n\t\t\t\t\t\tlog.Printf(\"Failed to ingest TCP result for %s (%s): %v\", monitor.Id, monitor.Uri, ingestErr)\n\t\t\t\t\t\treturn ingestErr\n\t\t\t\t\t}\n\t\t\t\t\tlog.Printf(\"TCP monitor check succeeded for %s (%s), ingest response: %v\", monitor.Id, monitor.Uri, resp)\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\terr := mm.Scheduler.AddWithID(m.Id, &task)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to add TCP monitor job for %s (%s): %v\", m.Id, m.Uri, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Printf(\"Started TCP monitoring job for %s (%s)\", m.Id, m.Uri)\n\t\t}\n\t}\n\n\tfor id := range mm.Scheduler.Tasks() {\n\t\tif _, stillExists := currentIDs[id]; !stillExists {\n\t\t\tmm.Scheduler.Del(id)\n\t\t}\n\t}\n\n}\n\nfunc intervalToSecond(interval string) int {\n\tswitch interval {\n\tcase Interval30s:\n\t\treturn 30\n\tcase Interval1m:\n\t\treturn 60\n\tcase Interval5m:\n\t\treturn 300\n\tcase Interval10m:\n\t\treturn 600\n\tcase Interval30m:\n\t\treturn 1800\n\tcase Interval1h:\n\t\treturn 3600\n\tcase Interval10s:\n\t\treturn 10\n\tdefault:\n\t\treturn 0\n\t}\n}\n"
  },
  {
    "path": "apps/checker/pkg/scheduler/scheduler_test.go",
    "content": "package scheduler_test\n\nimport (\n\t\"context\"\n\t\"sync/atomic\"\n\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/madflojo/tasks\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/job\"\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/scheduler\"\n\tv1 \"github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1\"\n)\n\n// mockJobRunner implements job.JobRunner for testing\ntype mockJobRunner struct {\n\tHTTPJobCalled atomic.Bool\n\tTCPJobCalled  atomic.Bool\n\tDNSJobCalled  atomic.Bool\n\tmu            sync.Mutex\n}\n\nfunc (m *mockJobRunner) HTTPJob(ctx context.Context, monitor *v1.HTTPMonitor) (*job.HttpPrivateRegionData, error) {\n\tm.HTTPJobCalled.Store(true)\n\treturn &job.HttpPrivateRegionData{}, nil\n}\nfunc (m *mockJobRunner) TCPJob(ctx context.Context, monitor *v1.TCPMonitor) (*job.TCPPrivateRegionData, error) {\n\n\tm.TCPJobCalled.Store(true)\n\treturn &job.TCPPrivateRegionData{}, nil\n}\n\nfunc (m *mockJobRunner) DNSJob(ctx context.Context, monitor *v1.DNSMonitor) (*job.DNSPrivateRegionData, error) {\n\n\tm.TCPJobCalled.Store(true)\n\treturn &job.DNSPrivateRegionData{}, nil\n}\n// mockClient implements v1.PrivateLocationServiceClient for testing\ntype mockClient struct {\n\tMonitorsFunc   func(ctx context.Context, req *connect.Request[v1.MonitorsRequest]) (*connect.Response[v1.MonitorsResponse], error)\n\tIngestHTTPFunc func(ctx context.Context, req *connect.Request[v1.IngestHTTPRequest]) (*connect.Response[v1.IngestHTTPResponse], error)\n\tIngestTCPFunc  func(ctx context.Context, req *connect.Request[v1.IngestTCPRequest]) (*connect.Response[v1.IngestTCPResponse], error)\n\tIngestDNSFunc  func(ctx context.Context, req *connect.Request[v1.IngestDNSRequest]) (*connect.Response[v1.IngestDNSResponse], error)\n}\n\nfunc (m *mockClient) Monitors(ctx context.Context, req *connect.Request[v1.MonitorsRequest]) (*connect.Response[v1.MonitorsResponse], error) {\n\treturn m.MonitorsFunc(ctx, req)\n}\nfunc (m *mockClient) IngestHTTP(ctx context.Context, req *connect.Request[v1.IngestHTTPRequest]) (*connect.Response[v1.IngestHTTPResponse], error) {\n\treturn m.IngestHTTPFunc(ctx, req)\n}\nfunc (m *mockClient) IngestTCP(ctx context.Context, req *connect.Request[v1.IngestTCPRequest]) (*connect.Response[v1.IngestTCPResponse], error) {\n\treturn m.IngestTCPFunc(ctx, req)\n}\nfunc (m *mockClient) IngestDNS(ctx context.Context, req *connect.Request[v1.IngestDNSRequest]) (*connect.Response[v1.IngestDNSResponse], error) {\n\treturn m.IngestDNSFunc(ctx, req)\n}\n\nfunc TestMonitorManager_StartAndStopJobs_WithJobRunner(t *testing.T) {\n\tctx := t.Context()\n\n\thttpMonitor := &v1.HTTPMonitor{Id: \"http1\", Url: \"http://openstat.us\", Periodicity: \"10s\"}\n\ttcpMonitor := &v1.TCPMonitor{Id: \"tcp1\", Uri: \"openstatus:80\", Periodicity: \"10s\"}\n\n\tclient := &mockClient{\n\t\tMonitorsFunc: func(ctx context.Context, req *connect.Request[v1.MonitorsRequest]) (*connect.Response[v1.MonitorsResponse], error) {\n\t\t\treturn connect.NewResponse(&v1.MonitorsResponse{\n\t\t\t\tHttpMonitors: []*v1.HTTPMonitor{httpMonitor},\n\t\t\t\tTcpMonitors:  []*v1.TCPMonitor{tcpMonitor},\n\t\t\t}), nil\n\t\t},\n\t\tIngestHTTPFunc: func(ctx context.Context, req *connect.Request[v1.IngestHTTPRequest]) (*connect.Response[v1.IngestHTTPResponse], error) {\n\t\t\treturn connect.NewResponse(&v1.IngestHTTPResponse{}), nil\n\t\t},\n\t\tIngestTCPFunc: func(ctx context.Context, req *connect.Request[v1.IngestTCPRequest]) (*connect.Response[v1.IngestTCPResponse], error) {\n\t\t\treturn connect.NewResponse(&v1.IngestTCPResponse{}), nil\n\t\t},\n\n\t}\n\tjobRunner := &mockJobRunner{}\n\n\ts := tasks.New()\n\tdefer s.Stop()\n\n\tmm := &scheduler.MonitorManager{\n\n\t\tClient:    client,\n\t\tJobRunner: jobRunner,\n\t\tScheduler: s,\n\t}\n\n\tmm.UpdateMonitors(ctx)\n\ttime.Sleep(12 * time.Second) // allow jobs to run\n\n\tif !jobRunner.HTTPJobCalled.Load() == true {\n\t\tt.Errorf(\"expected HTTPJob to be called\")\n\t}\n\tif !jobRunner.TCPJobCalled.Load() == true {\n\t\tt.Errorf(\"expected TCPJob to be called\")\n\t}\n\n\t// Remove monitors and ensure jobs are stopped\n\tclient.MonitorsFunc = func(ctx context.Context, req *connect.Request[v1.MonitorsRequest]) (*connect.Response[v1.MonitorsResponse], error) {\n\t\treturn connect.NewResponse(&v1.MonitorsResponse{\n\t\t\tHttpMonitors: []*v1.HTTPMonitor{},\n\t\t\tTcpMonitors:  []*v1.TCPMonitor{},\n\t\t}), nil\n\t}\n\tmm.UpdateMonitors(ctx)\n\ttime.Sleep(1 * time.Second)\n\n\tif _, err := mm.Scheduler.Lookup(\"http1\"); err == nil {\n\t\tt.Errorf(\"expected HTTP job to be removed\")\n\t}\n\tif _, err := mm.Scheduler.Lookup(\"tcp1\"); err == nil {\n\t\tt.Errorf(\"expected TCP job to be removed\")\n\t}\n\n}\n"
  },
  {
    "path": "apps/checker/pkg/tinybird/client.go",
    "content": "package tinybird\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/rs/zerolog/log\"\n)\n\nfunc getBaseURL() string {\n\t// Use local Tinybird container if available (Docker/self-hosted)\n\t// https://www.tinybird.co/docs/api-reference\n\tif tinybirdURL := os.Getenv(\"TINYBIRD_URL\"); tinybirdURL != \"\" {\n\t\treturn tinybirdURL + \"/v0/events\"\n\t}\n\treturn \"https://api.tinybird.co/v0/events\"\n}\n\ntype Client interface {\n\tSendEvent(ctx context.Context, event any, dataSourceName string) error\n}\n\ntype client struct {\n\thttpClient *http.Client\n\tapiKey     string\n\tbaseURL    string\n}\n\nfunc NewClient(httpClient *http.Client, apiKey string) Client {\n\treturn client{\n\t\thttpClient: httpClient,\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    getBaseURL(),\n\t}\n}\n\nfunc (c client) SendEvent(ctx context.Context, event any, dataSourceName string) error {\n\trequestURL, err := url.Parse(c.baseURL)\n\tif err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"unable to parse url\")\n\t\treturn fmt.Errorf(\"unable to parse url: %w\", err)\n\t}\n\n\tq := requestURL.Query()\n\tq.Add(\"name\", dataSourceName)\n\trequestURL.RawQuery = q.Encode()\n\n\tvar payload bytes.Buffer\n\tif err := json.NewEncoder(&payload).Encode(event); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"unable to encode payload\")\n\t\treturn fmt.Errorf(\"unable to encode payload: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(payload.Bytes()))\n\tif err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"unable to create request\")\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", c.apiKey))\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"unable to send request\")\n\t\treturn fmt.Errorf(\"unable to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusAccepted {\n\t\tlog.Ctx(ctx).Error().Str(\"status\", resp.Status).Msg(\"unexpected status code\")\n\t\treturn fmt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "apps/checker/pkg/tinybird/client_test.go",
    "content": "package tinybird_test\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype interceptorHTTPClient struct {\n\tf func(req *http.Request) (*http.Response, error)\n}\n\nfunc (i *interceptorHTTPClient) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn i.f(req)\n}\n\nfunc (i *interceptorHTTPClient) GetHTTPClient() *http.Client {\n\treturn &http.Client{\n\t\tTransport: i,\n\t}\n}\n\nfunc TestSendEvent(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\tt.Run(\"it should return an error if it can not send the event\", func(t *testing.T) {\n\t\tinterceptor := &interceptorHTTPClient{\n\t\t\tf: func(req *http.Request) (*http.Response, error) {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to send request\")\n\t\t\t},\n\t\t}\n\n\t\tclient := tinybird.NewClient(interceptor.GetHTTPClient(), \"apiKey\")\n\n\t\terr := client.SendEvent(ctx, \"event\", \"test\")\n\t\trequire.Error(t, err)\n\t})\n\n\tt.Run(\"it should return an error if the response status code is not 200\", func(t *testing.T) {\n\t\tinterceptor := &interceptorHTTPClient{\n\t\t\tf: func(req *http.Request) (*http.Response, error) {\n\t\t\t\treturn &http.Response{\n\t\t\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t}\n\n\t\tclient := tinybird.NewClient(interceptor.GetHTTPClient(), \"apiKey\")\n\n\t\terr := client.SendEvent(ctx, \"event\", \"test\")\n\t\trequire.Error(t, err)\n\t})\n\n\tt.Run(\"it should succeed and return nothing\", func(t *testing.T) {\n\t\tvar url string\n\t\tinterceptor := &interceptorHTTPClient{\n\t\t\tf: func(req *http.Request) (*http.Response, error) {\n\t\t\t\turl = req.URL.String()\n\t\t\t\treturn &http.Response{\n\t\t\t\t\tStatusCode: http.StatusAccepted,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t}\n\n\t\tclient := tinybird.NewClient(interceptor.GetHTTPClient(), \"apiKey\")\n\n\t\terr := client.SendEvent(ctx, \"event\", \"test\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"https://api.tinybird.co/v0/events?name=test\", url)\n\t})\n}\n"
  },
  {
    "path": "apps/checker/private-location.Dockerfile",
    "content": "FROM --platform=$BUILDPLATFORM golang:1.25-alpine as builder\n\nWORKDIR /go/src/app\n\nRUN apk add --no-cache tzdata\nENV TZ=UTC\n\nENV CGO_ENABLED=0\n\nCOPY go.* .\nRUN go mod download\n\nCOPY . .\nRUN GOOS=${TARGETOS} GOARCH=${TARGETARCH}  go build -trimpath -ldflags \"-s -w\" -o private ./cmd/private\n\nFROM scratch\n\nWORKDIR /opt/bin\n\nCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\nCOPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo\n\nCOPY --from=builder /go/src/app/private /opt/bin/private\n\nENV TZ=UTC\nENV USER=1000\nENV GIN_MODE=release\n\nCMD [ \"/opt/bin/private\" ]\n"
  },
  {
    "path": "apps/checker/proto/private_location/v1/assertions.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/assertions.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype NumberComparator int32\n\nconst (\n\tNumberComparator_NUMBER_COMPARATOR_UNSPECIFIED           NumberComparator = 0\n\tNumberComparator_NUMBER_COMPARATOR_EQUAL                 NumberComparator = 1\n\tNumberComparator_NUMBER_COMPARATOR_NOT_EQUAL             NumberComparator = 2\n\tNumberComparator_NUMBER_COMPARATOR_GREATER_THAN          NumberComparator = 3\n\tNumberComparator_NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL NumberComparator = 4\n\tNumberComparator_NUMBER_COMPARATOR_LESS_THAN             NumberComparator = 5\n\tNumberComparator_NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL    NumberComparator = 6\n)\n\n// Enum value maps for NumberComparator.\nvar (\n\tNumberComparator_name = map[int32]string{\n\t\t0: \"NUMBER_COMPARATOR_UNSPECIFIED\",\n\t\t1: \"NUMBER_COMPARATOR_EQUAL\",\n\t\t2: \"NUMBER_COMPARATOR_NOT_EQUAL\",\n\t\t3: \"NUMBER_COMPARATOR_GREATER_THAN\",\n\t\t4: \"NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\",\n\t\t5: \"NUMBER_COMPARATOR_LESS_THAN\",\n\t\t6: \"NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\",\n\t}\n\tNumberComparator_value = map[string]int32{\n\t\t\"NUMBER_COMPARATOR_UNSPECIFIED\":           0,\n\t\t\"NUMBER_COMPARATOR_EQUAL\":                 1,\n\t\t\"NUMBER_COMPARATOR_NOT_EQUAL\":             2,\n\t\t\"NUMBER_COMPARATOR_GREATER_THAN\":          3,\n\t\t\"NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\": 4,\n\t\t\"NUMBER_COMPARATOR_LESS_THAN\":             5,\n\t\t\"NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\":    6,\n\t}\n)\n\nfunc (x NumberComparator) Enum() *NumberComparator {\n\tp := new(NumberComparator)\n\t*p = x\n\treturn p\n}\n\nfunc (x NumberComparator) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (NumberComparator) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_private_location_v1_assertions_proto_enumTypes[0].Descriptor()\n}\n\nfunc (NumberComparator) Type() protoreflect.EnumType {\n\treturn &file_private_location_v1_assertions_proto_enumTypes[0]\n}\n\nfunc (x NumberComparator) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use NumberComparator.Descriptor instead.\nfunc (NumberComparator) EnumDescriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{0}\n}\n\ntype StringComparator int32\n\nconst (\n\tStringComparator_STRING_COMPARATOR_UNSPECIFIED           StringComparator = 0\n\tStringComparator_STRING_COMPARATOR_CONTAINS              StringComparator = 1\n\tStringComparator_STRING_COMPARATOR_NOT_CONTAINS          StringComparator = 2\n\tStringComparator_STRING_COMPARATOR_EQUAL                 StringComparator = 3\n\tStringComparator_STRING_COMPARATOR_NOT_EQUAL             StringComparator = 4\n\tStringComparator_STRING_COMPARATOR_EMPTY                 StringComparator = 5\n\tStringComparator_STRING_COMPARATOR_NOT_EMPTY             StringComparator = 6\n\tStringComparator_STRING_COMPARATOR_GREATER_THAN          StringComparator = 7\n\tStringComparator_STRING_COMPARATOR_GREATER_THAN_OR_EQUAL StringComparator = 8\n\tStringComparator_STRING_COMPARATOR_LESS_THAN             StringComparator = 9\n\tStringComparator_STRING_COMPARATOR_LESS_THAN_OR_EQUAL    StringComparator = 10\n)\n\n// Enum value maps for StringComparator.\nvar (\n\tStringComparator_name = map[int32]string{\n\t\t0:  \"STRING_COMPARATOR_UNSPECIFIED\",\n\t\t1:  \"STRING_COMPARATOR_CONTAINS\",\n\t\t2:  \"STRING_COMPARATOR_NOT_CONTAINS\",\n\t\t3:  \"STRING_COMPARATOR_EQUAL\",\n\t\t4:  \"STRING_COMPARATOR_NOT_EQUAL\",\n\t\t5:  \"STRING_COMPARATOR_EMPTY\",\n\t\t6:  \"STRING_COMPARATOR_NOT_EMPTY\",\n\t\t7:  \"STRING_COMPARATOR_GREATER_THAN\",\n\t\t8:  \"STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\",\n\t\t9:  \"STRING_COMPARATOR_LESS_THAN\",\n\t\t10: \"STRING_COMPARATOR_LESS_THAN_OR_EQUAL\",\n\t}\n\tStringComparator_value = map[string]int32{\n\t\t\"STRING_COMPARATOR_UNSPECIFIED\":           0,\n\t\t\"STRING_COMPARATOR_CONTAINS\":              1,\n\t\t\"STRING_COMPARATOR_NOT_CONTAINS\":          2,\n\t\t\"STRING_COMPARATOR_EQUAL\":                 3,\n\t\t\"STRING_COMPARATOR_NOT_EQUAL\":             4,\n\t\t\"STRING_COMPARATOR_EMPTY\":                 5,\n\t\t\"STRING_COMPARATOR_NOT_EMPTY\":             6,\n\t\t\"STRING_COMPARATOR_GREATER_THAN\":          7,\n\t\t\"STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\": 8,\n\t\t\"STRING_COMPARATOR_LESS_THAN\":             9,\n\t\t\"STRING_COMPARATOR_LESS_THAN_OR_EQUAL\":    10,\n\t}\n)\n\nfunc (x StringComparator) Enum() *StringComparator {\n\tp := new(StringComparator)\n\t*p = x\n\treturn p\n}\n\nfunc (x StringComparator) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (StringComparator) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_private_location_v1_assertions_proto_enumTypes[1].Descriptor()\n}\n\nfunc (StringComparator) Type() protoreflect.EnumType {\n\treturn &file_private_location_v1_assertions_proto_enumTypes[1]\n}\n\nfunc (x StringComparator) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use StringComparator.Descriptor instead.\nfunc (StringComparator) EnumDescriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{1}\n}\n\ntype RecordComparator int32\n\nconst (\n\tRecordComparator_RECORD_COMPARATOR_UNSPECIFIED  RecordComparator = 0\n\tRecordComparator_RECORD_COMPARATOR_EQUAL        RecordComparator = 1\n\tRecordComparator_RECORD_COMPARATOR_NOT_EQUAL    RecordComparator = 2\n\tRecordComparator_RECORD_COMPARATOR_CONTAINS     RecordComparator = 3\n\tRecordComparator_RECORD_COMPARATOR_NOT_CONTAINS RecordComparator = 4\n)\n\n// Enum value maps for RecordComparator.\nvar (\n\tRecordComparator_name = map[int32]string{\n\t\t0: \"RECORD_COMPARATOR_UNSPECIFIED\",\n\t\t1: \"RECORD_COMPARATOR_EQUAL\",\n\t\t2: \"RECORD_COMPARATOR_NOT_EQUAL\",\n\t\t3: \"RECORD_COMPARATOR_CONTAINS\",\n\t\t4: \"RECORD_COMPARATOR_NOT_CONTAINS\",\n\t}\n\tRecordComparator_value = map[string]int32{\n\t\t\"RECORD_COMPARATOR_UNSPECIFIED\":  0,\n\t\t\"RECORD_COMPARATOR_EQUAL\":        1,\n\t\t\"RECORD_COMPARATOR_NOT_EQUAL\":    2,\n\t\t\"RECORD_COMPARATOR_CONTAINS\":     3,\n\t\t\"RECORD_COMPARATOR_NOT_CONTAINS\": 4,\n\t}\n)\n\nfunc (x RecordComparator) Enum() *RecordComparator {\n\tp := new(RecordComparator)\n\t*p = x\n\treturn p\n}\n\nfunc (x RecordComparator) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (RecordComparator) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_private_location_v1_assertions_proto_enumTypes[2].Descriptor()\n}\n\nfunc (RecordComparator) Type() protoreflect.EnumType {\n\treturn &file_private_location_v1_assertions_proto_enumTypes[2]\n}\n\nfunc (x RecordComparator) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use RecordComparator.Descriptor instead.\nfunc (RecordComparator) EnumDescriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{2}\n}\n\ntype StatusCodeAssertion struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTarget        int64                  `protobuf:\"varint,1,opt,name=target,proto3\" json:\"target,omitempty\"`\n\tComparator    NumberComparator       `protobuf:\"varint,2,opt,name=comparator,proto3,enum=private_location.v1.NumberComparator\" json:\"comparator,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *StatusCodeAssertion) Reset() {\n\t*x = StatusCodeAssertion{}\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *StatusCodeAssertion) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StatusCodeAssertion) ProtoMessage() {}\n\nfunc (x *StatusCodeAssertion) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StatusCodeAssertion.ProtoReflect.Descriptor instead.\nfunc (*StatusCodeAssertion) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *StatusCodeAssertion) GetTarget() int64 {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn 0\n}\n\nfunc (x *StatusCodeAssertion) GetComparator() NumberComparator {\n\tif x != nil {\n\t\treturn x.Comparator\n\t}\n\treturn NumberComparator_NUMBER_COMPARATOR_UNSPECIFIED\n}\n\ntype BodyAssertion struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTarget        string                 `protobuf:\"bytes,1,opt,name=target,proto3\" json:\"target,omitempty\"`\n\tComparator    StringComparator       `protobuf:\"varint,2,opt,name=comparator,proto3,enum=private_location.v1.StringComparator\" json:\"comparator,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BodyAssertion) Reset() {\n\t*x = BodyAssertion{}\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BodyAssertion) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BodyAssertion) ProtoMessage() {}\n\nfunc (x *BodyAssertion) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BodyAssertion.ProtoReflect.Descriptor instead.\nfunc (*BodyAssertion) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *BodyAssertion) GetTarget() string {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn \"\"\n}\n\nfunc (x *BodyAssertion) GetComparator() StringComparator {\n\tif x != nil {\n\t\treturn x.Comparator\n\t}\n\treturn StringComparator_STRING_COMPARATOR_UNSPECIFIED\n}\n\ntype HeaderAssertion struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTarget        string                 `protobuf:\"bytes,1,opt,name=target,proto3\" json:\"target,omitempty\"`\n\tComparator    StringComparator       `protobuf:\"varint,2,opt,name=comparator,proto3,enum=private_location.v1.StringComparator\" json:\"comparator,omitempty\"`\n\tKey           string                 `protobuf:\"bytes,3,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HeaderAssertion) Reset() {\n\t*x = HeaderAssertion{}\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HeaderAssertion) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HeaderAssertion) ProtoMessage() {}\n\nfunc (x *HeaderAssertion) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HeaderAssertion.ProtoReflect.Descriptor instead.\nfunc (*HeaderAssertion) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *HeaderAssertion) GetTarget() string {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn \"\"\n}\n\nfunc (x *HeaderAssertion) GetComparator() StringComparator {\n\tif x != nil {\n\t\treturn x.Comparator\n\t}\n\treturn StringComparator_STRING_COMPARATOR_UNSPECIFIED\n}\n\nfunc (x *HeaderAssertion) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\ntype RecordAssertion struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tRecord        string                 `protobuf:\"bytes,1,opt,name=record,proto3\" json:\"record,omitempty\"`\n\tComparator    RecordComparator       `protobuf:\"varint,2,opt,name=comparator,proto3,enum=private_location.v1.RecordComparator\" json:\"comparator,omitempty\"`\n\tTarget        string                 `protobuf:\"bytes,3,opt,name=target,proto3\" json:\"target,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RecordAssertion) Reset() {\n\t*x = RecordAssertion{}\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RecordAssertion) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RecordAssertion) ProtoMessage() {}\n\nfunc (x *RecordAssertion) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RecordAssertion.ProtoReflect.Descriptor instead.\nfunc (*RecordAssertion) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *RecordAssertion) GetRecord() string {\n\tif x != nil {\n\t\treturn x.Record\n\t}\n\treturn \"\"\n}\n\nfunc (x *RecordAssertion) GetComparator() RecordComparator {\n\tif x != nil {\n\t\treturn x.Comparator\n\t}\n\treturn RecordComparator_RECORD_COMPARATOR_UNSPECIFIED\n}\n\nfunc (x *RecordAssertion) GetTarget() string {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn \"\"\n}\n\nvar File_private_location_v1_assertions_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_assertions_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"$private_location/v1/assertions.proto\\x12\\x13private_location.v1\\\"t\\n\" +\n\t\"\\x13StatusCodeAssertion\\x12\\x16\\n\" +\n\t\"\\x06target\\x18\\x01 \\x01(\\x03R\\x06target\\x12E\\n\" +\n\t\"\\n\" +\n\t\"comparator\\x18\\x02 \\x01(\\x0e2%.private_location.v1.NumberComparatorR\\n\" +\n\t\"comparator\\\"n\\n\" +\n\t\"\\rBodyAssertion\\x12\\x16\\n\" +\n\t\"\\x06target\\x18\\x01 \\x01(\\tR\\x06target\\x12E\\n\" +\n\t\"\\n\" +\n\t\"comparator\\x18\\x02 \\x01(\\x0e2%.private_location.v1.StringComparatorR\\n\" +\n\t\"comparator\\\"\\x82\\x01\\n\" +\n\t\"\\x0fHeaderAssertion\\x12\\x16\\n\" +\n\t\"\\x06target\\x18\\x01 \\x01(\\tR\\x06target\\x12E\\n\" +\n\t\"\\n\" +\n\t\"comparator\\x18\\x02 \\x01(\\x0e2%.private_location.v1.StringComparatorR\\n\" +\n\t\"comparator\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x03 \\x01(\\tR\\x03key\\\"\\x88\\x01\\n\" +\n\t\"\\x0fRecordAssertion\\x12\\x16\\n\" +\n\t\"\\x06record\\x18\\x01 \\x01(\\tR\\x06record\\x12E\\n\" +\n\t\"\\n\" +\n\t\"comparator\\x18\\x02 \\x01(\\x0e2%.private_location.v1.RecordComparatorR\\n\" +\n\t\"comparator\\x12\\x16\\n\" +\n\t\"\\x06target\\x18\\x03 \\x01(\\tR\\x06target*\\x8f\\x02\\n\" +\n\t\"\\x10NumberComparator\\x12!\\n\" +\n\t\"\\x1dNUMBER_COMPARATOR_UNSPECIFIED\\x10\\x00\\x12\\x1b\\n\" +\n\t\"\\x17NUMBER_COMPARATOR_EQUAL\\x10\\x01\\x12\\x1f\\n\" +\n\t\"\\x1bNUMBER_COMPARATOR_NOT_EQUAL\\x10\\x02\\x12\\\"\\n\" +\n\t\"\\x1eNUMBER_COMPARATOR_GREATER_THAN\\x10\\x03\\x12+\\n\" +\n\t\"'NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\\x10\\x04\\x12\\x1f\\n\" +\n\t\"\\x1bNUMBER_COMPARATOR_LESS_THAN\\x10\\x05\\x12(\\n\" +\n\t\"$NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\\x10\\x06*\\x91\\x03\\n\" +\n\t\"\\x10StringComparator\\x12!\\n\" +\n\t\"\\x1dSTRING_COMPARATOR_UNSPECIFIED\\x10\\x00\\x12\\x1e\\n\" +\n\t\"\\x1aSTRING_COMPARATOR_CONTAINS\\x10\\x01\\x12\\\"\\n\" +\n\t\"\\x1eSTRING_COMPARATOR_NOT_CONTAINS\\x10\\x02\\x12\\x1b\\n\" +\n\t\"\\x17STRING_COMPARATOR_EQUAL\\x10\\x03\\x12\\x1f\\n\" +\n\t\"\\x1bSTRING_COMPARATOR_NOT_EQUAL\\x10\\x04\\x12\\x1b\\n\" +\n\t\"\\x17STRING_COMPARATOR_EMPTY\\x10\\x05\\x12\\x1f\\n\" +\n\t\"\\x1bSTRING_COMPARATOR_NOT_EMPTY\\x10\\x06\\x12\\\"\\n\" +\n\t\"\\x1eSTRING_COMPARATOR_GREATER_THAN\\x10\\a\\x12+\\n\" +\n\t\"'STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\\x10\\b\\x12\\x1f\\n\" +\n\t\"\\x1bSTRING_COMPARATOR_LESS_THAN\\x10\\t\\x12(\\n\" +\n\t\"$STRING_COMPARATOR_LESS_THAN_OR_EQUAL\\x10\\n\" +\n\t\"*\\xb7\\x01\\n\" +\n\t\"\\x10RecordComparator\\x12!\\n\" +\n\t\"\\x1dRECORD_COMPARATOR_UNSPECIFIED\\x10\\x00\\x12\\x1b\\n\" +\n\t\"\\x17RECORD_COMPARATOR_EQUAL\\x10\\x01\\x12\\x1f\\n\" +\n\t\"\\x1bRECORD_COMPARATOR_NOT_EQUAL\\x10\\x02\\x12\\x1e\\n\" +\n\t\"\\x1aRECORD_COMPARATOR_CONTAINS\\x10\\x03\\x12\\\"\\n\" +\n\t\"\\x1eRECORD_COMPARATOR_NOT_CONTAINS\\x10\\x04BJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_assertions_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_assertions_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_assertions_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_assertions_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_assertions_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_assertions_proto_rawDesc), len(file_private_location_v1_assertions_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_assertions_proto_rawDescData\n}\n\nvar file_private_location_v1_assertions_proto_enumTypes = make([]protoimpl.EnumInfo, 3)\nvar file_private_location_v1_assertions_proto_msgTypes = make([]protoimpl.MessageInfo, 4)\nvar file_private_location_v1_assertions_proto_goTypes = []any{\n\t(NumberComparator)(0),       // 0: private_location.v1.NumberComparator\n\t(StringComparator)(0),       // 1: private_location.v1.StringComparator\n\t(RecordComparator)(0),       // 2: private_location.v1.RecordComparator\n\t(*StatusCodeAssertion)(nil), // 3: private_location.v1.StatusCodeAssertion\n\t(*BodyAssertion)(nil),       // 4: private_location.v1.BodyAssertion\n\t(*HeaderAssertion)(nil),     // 5: private_location.v1.HeaderAssertion\n\t(*RecordAssertion)(nil),     // 6: private_location.v1.RecordAssertion\n}\nvar file_private_location_v1_assertions_proto_depIdxs = []int32{\n\t0, // 0: private_location.v1.StatusCodeAssertion.comparator:type_name -> private_location.v1.NumberComparator\n\t1, // 1: private_location.v1.BodyAssertion.comparator:type_name -> private_location.v1.StringComparator\n\t1, // 2: private_location.v1.HeaderAssertion.comparator:type_name -> private_location.v1.StringComparator\n\t2, // 3: private_location.v1.RecordAssertion.comparator:type_name -> private_location.v1.RecordComparator\n\t4, // [4:4] is the sub-list for method output_type\n\t4, // [4:4] is the sub-list for method input_type\n\t4, // [4:4] is the sub-list for extension type_name\n\t4, // [4:4] is the sub-list for extension extendee\n\t0, // [0:4] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_assertions_proto_init() }\nfunc file_private_location_v1_assertions_proto_init() {\n\tif File_private_location_v1_assertions_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_assertions_proto_rawDesc), len(file_private_location_v1_assertions_proto_rawDesc)),\n\t\t\tNumEnums:      3,\n\t\t\tNumMessages:   4,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_assertions_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_assertions_proto_depIdxs,\n\t\tEnumInfos:         file_private_location_v1_assertions_proto_enumTypes,\n\t\tMessageInfos:      file_private_location_v1_assertions_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_assertions_proto = out.File\n\tfile_private_location_v1_assertions_proto_goTypes = nil\n\tfile_private_location_v1_assertions_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/checker/proto/private_location/v1/dns_monitor.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/dns_monitor.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype DNSMonitor struct {\n\tstate            protoimpl.MessageState `protogen:\"open.v1\"`\n\tId               string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tUri              string                 `protobuf:\"bytes,2,opt,name=uri,proto3\" json:\"uri,omitempty\"`\n\tTimeout          int64                  `protobuf:\"varint,3,opt,name=timeout,proto3\" json:\"timeout,omitempty\"`\n\tDegradedAt       *int64                 `protobuf:\"varint,4,opt,name=degraded_at,json=degradedAt,proto3,oneof\" json:\"degraded_at,omitempty\"`\n\tPeriodicity      string                 `protobuf:\"bytes,5,opt,name=periodicity,proto3\" json:\"periodicity,omitempty\"`\n\tRetry            int64                  `protobuf:\"varint,6,opt,name=retry,proto3\" json:\"retry,omitempty\"`\n\tRecordAssertions []*RecordAssertion     `protobuf:\"bytes,13,rep,name=record_assertions,json=recordAssertions,proto3\" json:\"record_assertions,omitempty\"`\n\tunknownFields    protoimpl.UnknownFields\n\tsizeCache        protoimpl.SizeCache\n}\n\nfunc (x *DNSMonitor) Reset() {\n\t*x = DNSMonitor{}\n\tmi := &file_private_location_v1_dns_monitor_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DNSMonitor) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DNSMonitor) ProtoMessage() {}\n\nfunc (x *DNSMonitor) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_dns_monitor_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DNSMonitor.ProtoReflect.Descriptor instead.\nfunc (*DNSMonitor) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_dns_monitor_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *DNSMonitor) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *DNSMonitor) GetUri() string {\n\tif x != nil {\n\t\treturn x.Uri\n\t}\n\treturn \"\"\n}\n\nfunc (x *DNSMonitor) GetTimeout() int64 {\n\tif x != nil {\n\t\treturn x.Timeout\n\t}\n\treturn 0\n}\n\nfunc (x *DNSMonitor) GetDegradedAt() int64 {\n\tif x != nil && x.DegradedAt != nil {\n\t\treturn *x.DegradedAt\n\t}\n\treturn 0\n}\n\nfunc (x *DNSMonitor) GetPeriodicity() string {\n\tif x != nil {\n\t\treturn x.Periodicity\n\t}\n\treturn \"\"\n}\n\nfunc (x *DNSMonitor) GetRetry() int64 {\n\tif x != nil {\n\t\treturn x.Retry\n\t}\n\treturn 0\n}\n\nfunc (x *DNSMonitor) GetRecordAssertions() []*RecordAssertion {\n\tif x != nil {\n\t\treturn x.RecordAssertions\n\t}\n\treturn nil\n}\n\nvar File_private_location_v1_dns_monitor_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_dns_monitor_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"%private_location/v1/dns_monitor.proto\\x12\\x13private_location.v1\\x1a$private_location/v1/assertions.proto\\\"\\x89\\x02\\n\" +\n\t\"\\n\" +\n\t\"DNSMonitor\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x10\\n\" +\n\t\"\\x03uri\\x18\\x02 \\x01(\\tR\\x03uri\\x12\\x18\\n\" +\n\t\"\\atimeout\\x18\\x03 \\x01(\\x03R\\atimeout\\x12$\\n\" +\n\t\"\\vdegraded_at\\x18\\x04 \\x01(\\x03H\\x00R\\n\" +\n\t\"degradedAt\\x88\\x01\\x01\\x12 \\n\" +\n\t\"\\vperiodicity\\x18\\x05 \\x01(\\tR\\vperiodicity\\x12\\x14\\n\" +\n\t\"\\x05retry\\x18\\x06 \\x01(\\x03R\\x05retry\\x12Q\\n\" +\n\t\"\\x11record_assertions\\x18\\r \\x03(\\v2$.private_location.v1.RecordAssertionR\\x10recordAssertionsB\\x0e\\n\" +\n\t\"\\f_degraded_atBJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_dns_monitor_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_dns_monitor_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_dns_monitor_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_dns_monitor_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_dns_monitor_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_dns_monitor_proto_rawDesc), len(file_private_location_v1_dns_monitor_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_dns_monitor_proto_rawDescData\n}\n\nvar file_private_location_v1_dns_monitor_proto_msgTypes = make([]protoimpl.MessageInfo, 1)\nvar file_private_location_v1_dns_monitor_proto_goTypes = []any{\n\t(*DNSMonitor)(nil),      // 0: private_location.v1.DNSMonitor\n\t(*RecordAssertion)(nil), // 1: private_location.v1.RecordAssertion\n}\nvar file_private_location_v1_dns_monitor_proto_depIdxs = []int32{\n\t1, // 0: private_location.v1.DNSMonitor.record_assertions:type_name -> private_location.v1.RecordAssertion\n\t1, // [1:1] is the sub-list for method output_type\n\t1, // [1:1] is the sub-list for method input_type\n\t1, // [1:1] is the sub-list for extension type_name\n\t1, // [1:1] is the sub-list for extension extendee\n\t0, // [0:1] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_dns_monitor_proto_init() }\nfunc file_private_location_v1_dns_monitor_proto_init() {\n\tif File_private_location_v1_dns_monitor_proto != nil {\n\t\treturn\n\t}\n\tfile_private_location_v1_assertions_proto_init()\n\tfile_private_location_v1_dns_monitor_proto_msgTypes[0].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_dns_monitor_proto_rawDesc), len(file_private_location_v1_dns_monitor_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   1,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_dns_monitor_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_dns_monitor_proto_depIdxs,\n\t\tMessageInfos:      file_private_location_v1_dns_monitor_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_dns_monitor_proto = out.File\n\tfile_private_location_v1_dns_monitor_proto_goTypes = nil\n\tfile_private_location_v1_dns_monitor_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/checker/proto/private_location/v1/http_monitor.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/http_monitor.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Headers struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey           string                 `protobuf:\"bytes,1,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tValue         string                 `protobuf:\"bytes,2,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Headers) Reset() {\n\t*x = Headers{}\n\tmi := &file_private_location_v1_http_monitor_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Headers) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Headers) ProtoMessage() {}\n\nfunc (x *Headers) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_http_monitor_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Headers.ProtoReflect.Descriptor instead.\nfunc (*Headers) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_http_monitor_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Headers) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *Headers) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\ntype HTTPMonitor struct {\n\tstate                protoimpl.MessageState `protogen:\"open.v1\"`\n\tId                   string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tUrl                  string                 `protobuf:\"bytes,2,opt,name=url,proto3\" json:\"url,omitempty\"`\n\tPeriodicity          string                 `protobuf:\"bytes,3,opt,name=periodicity,proto3\" json:\"periodicity,omitempty\"`\n\tMethod               string                 `protobuf:\"bytes,4,opt,name=method,proto3\" json:\"method,omitempty\"`\n\tBody                 string                 `protobuf:\"bytes,5,opt,name=body,proto3\" json:\"body,omitempty\"`\n\tTimeout              int64                  `protobuf:\"varint,6,opt,name=timeout,proto3\" json:\"timeout,omitempty\"`\n\tDegradedAt           *int64                 `protobuf:\"varint,7,opt,name=degraded_at,json=degradedAt,proto3,oneof\" json:\"degraded_at,omitempty\"`\n\tRetry                int64                  `protobuf:\"varint,8,opt,name=retry,proto3\" json:\"retry,omitempty\"`\n\tFollowRedirects      bool                   `protobuf:\"varint,9,opt,name=follow_redirects,json=followRedirects,proto3\" json:\"follow_redirects,omitempty\"`\n\tHeaders              []*Headers             `protobuf:\"bytes,10,rep,name=headers,proto3\" json:\"headers,omitempty\"`\n\tStatusCodeAssertions []*StatusCodeAssertion `protobuf:\"bytes,11,rep,name=status_code_assertions,json=statusCodeAssertions,proto3\" json:\"status_code_assertions,omitempty\"`\n\tBodyAssertions       []*BodyAssertion       `protobuf:\"bytes,12,rep,name=body_assertions,json=bodyAssertions,proto3\" json:\"body_assertions,omitempty\"`\n\tHeaderAssertions     []*HeaderAssertion     `protobuf:\"bytes,13,rep,name=header_assertions,json=headerAssertions,proto3\" json:\"header_assertions,omitempty\"`\n\tunknownFields        protoimpl.UnknownFields\n\tsizeCache            protoimpl.SizeCache\n}\n\nfunc (x *HTTPMonitor) Reset() {\n\t*x = HTTPMonitor{}\n\tmi := &file_private_location_v1_http_monitor_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPMonitor) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPMonitor) ProtoMessage() {}\n\nfunc (x *HTTPMonitor) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_http_monitor_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPMonitor.ProtoReflect.Descriptor instead.\nfunc (*HTTPMonitor) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_http_monitor_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *HTTPMonitor) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetUrl() string {\n\tif x != nil {\n\t\treturn x.Url\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetPeriodicity() string {\n\tif x != nil {\n\t\treturn x.Periodicity\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetMethod() string {\n\tif x != nil {\n\t\treturn x.Method\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetBody() string {\n\tif x != nil {\n\t\treturn x.Body\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetTimeout() int64 {\n\tif x != nil {\n\t\treturn x.Timeout\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPMonitor) GetDegradedAt() int64 {\n\tif x != nil && x.DegradedAt != nil {\n\t\treturn *x.DegradedAt\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPMonitor) GetRetry() int64 {\n\tif x != nil {\n\t\treturn x.Retry\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPMonitor) GetFollowRedirects() bool {\n\tif x != nil {\n\t\treturn x.FollowRedirects\n\t}\n\treturn false\n}\n\nfunc (x *HTTPMonitor) GetHeaders() []*Headers {\n\tif x != nil {\n\t\treturn x.Headers\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPMonitor) GetStatusCodeAssertions() []*StatusCodeAssertion {\n\tif x != nil {\n\t\treturn x.StatusCodeAssertions\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPMonitor) GetBodyAssertions() []*BodyAssertion {\n\tif x != nil {\n\t\treturn x.BodyAssertions\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPMonitor) GetHeaderAssertions() []*HeaderAssertion {\n\tif x != nil {\n\t\treturn x.HeaderAssertions\n\t}\n\treturn nil\n}\n\nvar File_private_location_v1_http_monitor_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_http_monitor_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"&private_location/v1/http_monitor.proto\\x12\\x13private_location.v1\\x1a$private_location/v1/assertions.proto\\\"1\\n\" +\n\t\"\\aHeaders\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value\\\"\\xc6\\x04\\n\" +\n\t\"\\vHTTPMonitor\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x10\\n\" +\n\t\"\\x03url\\x18\\x02 \\x01(\\tR\\x03url\\x12 \\n\" +\n\t\"\\vperiodicity\\x18\\x03 \\x01(\\tR\\vperiodicity\\x12\\x16\\n\" +\n\t\"\\x06method\\x18\\x04 \\x01(\\tR\\x06method\\x12\\x12\\n\" +\n\t\"\\x04body\\x18\\x05 \\x01(\\tR\\x04body\\x12\\x18\\n\" +\n\t\"\\atimeout\\x18\\x06 \\x01(\\x03R\\atimeout\\x12$\\n\" +\n\t\"\\vdegraded_at\\x18\\a \\x01(\\x03H\\x00R\\n\" +\n\t\"degradedAt\\x88\\x01\\x01\\x12\\x14\\n\" +\n\t\"\\x05retry\\x18\\b \\x01(\\x03R\\x05retry\\x12)\\n\" +\n\t\"\\x10follow_redirects\\x18\\t \\x01(\\bR\\x0ffollowRedirects\\x126\\n\" +\n\t\"\\aheaders\\x18\\n\" +\n\t\" \\x03(\\v2\\x1c.private_location.v1.HeadersR\\aheaders\\x12^\\n\" +\n\t\"\\x16status_code_assertions\\x18\\v \\x03(\\v2(.private_location.v1.StatusCodeAssertionR\\x14statusCodeAssertions\\x12K\\n\" +\n\t\"\\x0fbody_assertions\\x18\\f \\x03(\\v2\\\".private_location.v1.BodyAssertionR\\x0ebodyAssertions\\x12Q\\n\" +\n\t\"\\x11header_assertions\\x18\\r \\x03(\\v2$.private_location.v1.HeaderAssertionR\\x10headerAssertionsB\\x0e\\n\" +\n\t\"\\f_degraded_atBJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_http_monitor_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_http_monitor_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_http_monitor_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_http_monitor_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_http_monitor_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_http_monitor_proto_rawDesc), len(file_private_location_v1_http_monitor_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_http_monitor_proto_rawDescData\n}\n\nvar file_private_location_v1_http_monitor_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_private_location_v1_http_monitor_proto_goTypes = []any{\n\t(*Headers)(nil),             // 0: private_location.v1.Headers\n\t(*HTTPMonitor)(nil),         // 1: private_location.v1.HTTPMonitor\n\t(*StatusCodeAssertion)(nil), // 2: private_location.v1.StatusCodeAssertion\n\t(*BodyAssertion)(nil),       // 3: private_location.v1.BodyAssertion\n\t(*HeaderAssertion)(nil),     // 4: private_location.v1.HeaderAssertion\n}\nvar file_private_location_v1_http_monitor_proto_depIdxs = []int32{\n\t0, // 0: private_location.v1.HTTPMonitor.headers:type_name -> private_location.v1.Headers\n\t2, // 1: private_location.v1.HTTPMonitor.status_code_assertions:type_name -> private_location.v1.StatusCodeAssertion\n\t3, // 2: private_location.v1.HTTPMonitor.body_assertions:type_name -> private_location.v1.BodyAssertion\n\t4, // 3: private_location.v1.HTTPMonitor.header_assertions:type_name -> private_location.v1.HeaderAssertion\n\t4, // [4:4] is the sub-list for method output_type\n\t4, // [4:4] is the sub-list for method input_type\n\t4, // [4:4] is the sub-list for extension type_name\n\t4, // [4:4] is the sub-list for extension extendee\n\t0, // [0:4] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_http_monitor_proto_init() }\nfunc file_private_location_v1_http_monitor_proto_init() {\n\tif File_private_location_v1_http_monitor_proto != nil {\n\t\treturn\n\t}\n\tfile_private_location_v1_assertions_proto_init()\n\tfile_private_location_v1_http_monitor_proto_msgTypes[1].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_http_monitor_proto_rawDesc), len(file_private_location_v1_http_monitor_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_http_monitor_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_http_monitor_proto_depIdxs,\n\t\tMessageInfos:      file_private_location_v1_http_monitor_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_http_monitor_proto = out.File\n\tfile_private_location_v1_http_monitor_proto_goTypes = nil\n\tfile_private_location_v1_http_monitor_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/checker/proto/private_location/v1/private_location.connect.go",
    "content": "// Code generated by protoc-gen-connect-go. DO NOT EDIT.\n//\n// Source: private_location/v1/private_location.proto\n\npackage v1\n\nimport (\n\tconnect \"connectrpc.com/connect\"\n\tcontext \"context\"\n\terrors \"errors\"\n\thttp \"net/http\"\n\tstrings \"strings\"\n)\n\n// This is a compile-time assertion to ensure that this generated file and the connect package are\n// compatible. If you get a compiler error that this constant is not defined, this code was\n// generated with a version of connect newer than the one compiled into your binary. You can fix the\n// problem by either regenerating this code with an older version of connect or updating the connect\n// version compiled into your binary.\nconst _ = connect.IsAtLeastVersion1_13_0\n\nconst (\n\t// PrivateLocationServiceName is the fully-qualified name of the PrivateLocationService service.\n\tPrivateLocationServiceName = \"private_location.v1.PrivateLocationService\"\n)\n\n// These constants are the fully-qualified names of the RPCs defined in this package. They're\n// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.\n//\n// Note that these are different from the fully-qualified method names used by\n// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to\n// reflection-formatted method names, remove the leading slash and convert the remaining slash to a\n// period.\nconst (\n\t// PrivateLocationServiceMonitorsProcedure is the fully-qualified name of the\n\t// PrivateLocationService's Monitors RPC.\n\tPrivateLocationServiceMonitorsProcedure = \"/private_location.v1.PrivateLocationService/Monitors\"\n\t// PrivateLocationServiceIngestTCPProcedure is the fully-qualified name of the\n\t// PrivateLocationService's IngestTCP RPC.\n\tPrivateLocationServiceIngestTCPProcedure = \"/private_location.v1.PrivateLocationService/IngestTCP\"\n\t// PrivateLocationServiceIngestHTTPProcedure is the fully-qualified name of the\n\t// PrivateLocationService's IngestHTTP RPC.\n\tPrivateLocationServiceIngestHTTPProcedure = \"/private_location.v1.PrivateLocationService/IngestHTTP\"\n\t// PrivateLocationServiceIngestDNSProcedure is the fully-qualified name of the\n\t// PrivateLocationService's IngestDNS RPC.\n\tPrivateLocationServiceIngestDNSProcedure = \"/private_location.v1.PrivateLocationService/IngestDNS\"\n)\n\n// PrivateLocationServiceClient is a client for the private_location.v1.PrivateLocationService\n// service.\ntype PrivateLocationServiceClient interface {\n\tMonitors(context.Context, *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error)\n\tIngestTCP(context.Context, *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error)\n\tIngestHTTP(context.Context, *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error)\n\tIngestDNS(context.Context, *connect.Request[IngestDNSRequest]) (*connect.Response[IngestDNSResponse], error)\n}\n\n// NewPrivateLocationServiceClient constructs a client for the\n// private_location.v1.PrivateLocationService service. By default, it uses the Connect protocol with\n// the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed requests. To use\n// the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or connect.WithGRPCWeb() options.\n//\n// The URL supplied here should be the base URL for the Connect or gRPC server (for example,\n// http://api.acme.com or https://acme.com/grpc).\nfunc NewPrivateLocationServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PrivateLocationServiceClient {\n\tbaseURL = strings.TrimRight(baseURL, \"/\")\n\tprivateLocationServiceMethods := File_private_location_v1_private_location_proto.Services().ByName(\"PrivateLocationService\").Methods()\n\treturn &privateLocationServiceClient{\n\t\tmonitors: connect.NewClient[MonitorsRequest, MonitorsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+PrivateLocationServiceMonitorsProcedure,\n\t\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"Monitors\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tingestTCP: connect.NewClient[IngestTCPRequest, IngestTCPResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+PrivateLocationServiceIngestTCPProcedure,\n\t\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestTCP\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tingestHTTP: connect.NewClient[IngestHTTPRequest, IngestHTTPResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+PrivateLocationServiceIngestHTTPProcedure,\n\t\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestHTTP\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tingestDNS: connect.NewClient[IngestDNSRequest, IngestDNSResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+PrivateLocationServiceIngestDNSProcedure,\n\t\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestDNS\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t}\n}\n\n// privateLocationServiceClient implements PrivateLocationServiceClient.\ntype privateLocationServiceClient struct {\n\tmonitors   *connect.Client[MonitorsRequest, MonitorsResponse]\n\tingestTCP  *connect.Client[IngestTCPRequest, IngestTCPResponse]\n\tingestHTTP *connect.Client[IngestHTTPRequest, IngestHTTPResponse]\n\tingestDNS  *connect.Client[IngestDNSRequest, IngestDNSResponse]\n}\n\n// Monitors calls private_location.v1.PrivateLocationService.Monitors.\nfunc (c *privateLocationServiceClient) Monitors(ctx context.Context, req *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error) {\n\treturn c.monitors.CallUnary(ctx, req)\n}\n\n// IngestTCP calls private_location.v1.PrivateLocationService.IngestTCP.\nfunc (c *privateLocationServiceClient) IngestTCP(ctx context.Context, req *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error) {\n\treturn c.ingestTCP.CallUnary(ctx, req)\n}\n\n// IngestHTTP calls private_location.v1.PrivateLocationService.IngestHTTP.\nfunc (c *privateLocationServiceClient) IngestHTTP(ctx context.Context, req *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error) {\n\treturn c.ingestHTTP.CallUnary(ctx, req)\n}\n\n// IngestDNS calls private_location.v1.PrivateLocationService.IngestDNS.\nfunc (c *privateLocationServiceClient) IngestDNS(ctx context.Context, req *connect.Request[IngestDNSRequest]) (*connect.Response[IngestDNSResponse], error) {\n\treturn c.ingestDNS.CallUnary(ctx, req)\n}\n\n// PrivateLocationServiceHandler is an implementation of the\n// private_location.v1.PrivateLocationService service.\ntype PrivateLocationServiceHandler interface {\n\tMonitors(context.Context, *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error)\n\tIngestTCP(context.Context, *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error)\n\tIngestHTTP(context.Context, *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error)\n\tIngestDNS(context.Context, *connect.Request[IngestDNSRequest]) (*connect.Response[IngestDNSResponse], error)\n}\n\n// NewPrivateLocationServiceHandler builds an HTTP handler from the service implementation. It\n// returns the path on which to mount the handler and the handler itself.\n//\n// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf\n// and JSON codecs. They also support gzip compression.\nfunc NewPrivateLocationServiceHandler(svc PrivateLocationServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {\n\tprivateLocationServiceMethods := File_private_location_v1_private_location_proto.Services().ByName(\"PrivateLocationService\").Methods()\n\tprivateLocationServiceMonitorsHandler := connect.NewUnaryHandler(\n\t\tPrivateLocationServiceMonitorsProcedure,\n\t\tsvc.Monitors,\n\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"Monitors\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tprivateLocationServiceIngestTCPHandler := connect.NewUnaryHandler(\n\t\tPrivateLocationServiceIngestTCPProcedure,\n\t\tsvc.IngestTCP,\n\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestTCP\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tprivateLocationServiceIngestHTTPHandler := connect.NewUnaryHandler(\n\t\tPrivateLocationServiceIngestHTTPProcedure,\n\t\tsvc.IngestHTTP,\n\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestHTTP\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tprivateLocationServiceIngestDNSHandler := connect.NewUnaryHandler(\n\t\tPrivateLocationServiceIngestDNSProcedure,\n\t\tsvc.IngestDNS,\n\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestDNS\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\treturn \"/private_location.v1.PrivateLocationService/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase PrivateLocationServiceMonitorsProcedure:\n\t\t\tprivateLocationServiceMonitorsHandler.ServeHTTP(w, r)\n\t\tcase PrivateLocationServiceIngestTCPProcedure:\n\t\t\tprivateLocationServiceIngestTCPHandler.ServeHTTP(w, r)\n\t\tcase PrivateLocationServiceIngestHTTPProcedure:\n\t\t\tprivateLocationServiceIngestHTTPHandler.ServeHTTP(w, r)\n\t\tcase PrivateLocationServiceIngestDNSProcedure:\n\t\t\tprivateLocationServiceIngestDNSHandler.ServeHTTP(w, r)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n}\n\n// UnimplementedPrivateLocationServiceHandler returns CodeUnimplemented from all methods.\ntype UnimplementedPrivateLocationServiceHandler struct{}\n\nfunc (UnimplementedPrivateLocationServiceHandler) Monitors(context.Context, *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"private_location.v1.PrivateLocationService.Monitors is not implemented\"))\n}\n\nfunc (UnimplementedPrivateLocationServiceHandler) IngestTCP(context.Context, *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"private_location.v1.PrivateLocationService.IngestTCP is not implemented\"))\n}\n\nfunc (UnimplementedPrivateLocationServiceHandler) IngestHTTP(context.Context, *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"private_location.v1.PrivateLocationService.IngestHTTP is not implemented\"))\n}\n\nfunc (UnimplementedPrivateLocationServiceHandler) IngestDNS(context.Context, *connect.Request[IngestDNSRequest]) (*connect.Response[IngestDNSResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"private_location.v1.PrivateLocationService.IngestDNS is not implemented\"))\n}\n"
  },
  {
    "path": "apps/checker/proto/private_location/v1/private_location.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/private_location.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\t_ \"google.golang.org/protobuf/types/known/structpb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype MonitorsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MonitorsRequest) Reset() {\n\t*x = MonitorsRequest{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MonitorsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MonitorsRequest) ProtoMessage() {}\n\nfunc (x *MonitorsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MonitorsRequest.ProtoReflect.Descriptor instead.\nfunc (*MonitorsRequest) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{0}\n}\n\ntype MonitorsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tHttpMonitors  []*HTTPMonitor         `protobuf:\"bytes,1,rep,name=http_monitors,json=httpMonitors,proto3\" json:\"http_monitors,omitempty\"`\n\tTcpMonitors   []*TCPMonitor          `protobuf:\"bytes,2,rep,name=tcp_monitors,json=tcpMonitors,proto3\" json:\"tcp_monitors,omitempty\"`\n\tDnsMonitors   []*DNSMonitor          `protobuf:\"bytes,3,rep,name=dns_monitors,json=dnsMonitors,proto3\" json:\"dns_monitors,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MonitorsResponse) Reset() {\n\t*x = MonitorsResponse{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MonitorsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MonitorsResponse) ProtoMessage() {}\n\nfunc (x *MonitorsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MonitorsResponse.ProtoReflect.Descriptor instead.\nfunc (*MonitorsResponse) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *MonitorsResponse) GetHttpMonitors() []*HTTPMonitor {\n\tif x != nil {\n\t\treturn x.HttpMonitors\n\t}\n\treturn nil\n}\n\nfunc (x *MonitorsResponse) GetTcpMonitors() []*TCPMonitor {\n\tif x != nil {\n\t\treturn x.TcpMonitors\n\t}\n\treturn nil\n}\n\nfunc (x *MonitorsResponse) GetDnsMonitors() []*DNSMonitor {\n\tif x != nil {\n\t\treturn x.DnsMonitors\n\t}\n\treturn nil\n}\n\ntype IngestTCPRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tMonitorId     string                 `protobuf:\"bytes,2,opt,name=monitorId,proto3\" json:\"monitorId,omitempty\"`\n\tLatency       int64                  `protobuf:\"varint,3,opt,name=latency,proto3\" json:\"latency,omitempty\"`\n\tTimestamp     int64                  `protobuf:\"varint,4,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tCronTimestamp int64                  `protobuf:\"varint,5,opt,name=cronTimestamp,proto3\" json:\"cronTimestamp,omitempty\"`\n\tUri           string                 `protobuf:\"bytes,6,opt,name=uri,proto3\" json:\"uri,omitempty\"`\n\tMessage       string                 `protobuf:\"bytes,7,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tRequestStatus string                 `protobuf:\"bytes,8,opt,name=requestStatus,proto3\" json:\"requestStatus,omitempty\"`\n\tError         int64                  `protobuf:\"varint,9,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tTiming        string                 `protobuf:\"bytes,10,opt,name=timing,proto3\" json:\"timing,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestTCPRequest) Reset() {\n\t*x = IngestTCPRequest{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestTCPRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestTCPRequest) ProtoMessage() {}\n\nfunc (x *IngestTCPRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestTCPRequest.ProtoReflect.Descriptor instead.\nfunc (*IngestTCPRequest) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *IngestTCPRequest) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetMonitorId() string {\n\tif x != nil {\n\t\treturn x.MonitorId\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetLatency() int64 {\n\tif x != nil {\n\t\treturn x.Latency\n\t}\n\treturn 0\n}\n\nfunc (x *IngestTCPRequest) GetTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestTCPRequest) GetCronTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.CronTimestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestTCPRequest) GetUri() string {\n\tif x != nil {\n\t\treturn x.Uri\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetRequestStatus() string {\n\tif x != nil {\n\t\treturn x.RequestStatus\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetError() int64 {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn 0\n}\n\nfunc (x *IngestTCPRequest) GetTiming() string {\n\tif x != nil {\n\t\treturn x.Timing\n\t}\n\treturn \"\"\n}\n\ntype IngestTCPResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestTCPResponse) Reset() {\n\t*x = IngestTCPResponse{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestTCPResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestTCPResponse) ProtoMessage() {}\n\nfunc (x *IngestTCPResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestTCPResponse.ProtoReflect.Descriptor instead.\nfunc (*IngestTCPResponse) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{3}\n}\n\ntype IngestHTTPRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tMonitorId     string                 `protobuf:\"bytes,2,opt,name=monitorId,proto3\" json:\"monitorId,omitempty\"`\n\tLatency       int64                  `protobuf:\"varint,3,opt,name=latency,proto3\" json:\"latency,omitempty\"`\n\tTimestamp     int64                  `protobuf:\"varint,4,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tCronTimestamp int64                  `protobuf:\"varint,5,opt,name=cronTimestamp,proto3\" json:\"cronTimestamp,omitempty\"`\n\tUrl           string                 `protobuf:\"bytes,6,opt,name=url,proto3\" json:\"url,omitempty\"`\n\tRequestStatus string                 `protobuf:\"bytes,7,opt,name=requestStatus,proto3\" json:\"requestStatus,omitempty\"`\n\tMessage       string                 `protobuf:\"bytes,8,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tBody          string                 `protobuf:\"bytes,9,opt,name=body,proto3\" json:\"body,omitempty\"`\n\tHeaders       string                 `protobuf:\"bytes,10,opt,name=headers,proto3\" json:\"headers,omitempty\"`\n\tTiming        string                 `protobuf:\"bytes,11,opt,name=timing,proto3\" json:\"timing,omitempty\"`\n\tStatusCode    int64                  `protobuf:\"varint,12,opt,name=statusCode,proto3\" json:\"statusCode,omitempty\"`\n\tError         int64                  `protobuf:\"varint,13,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestHTTPRequest) Reset() {\n\t*x = IngestHTTPRequest{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestHTTPRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestHTTPRequest) ProtoMessage() {}\n\nfunc (x *IngestHTTPRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestHTTPRequest.ProtoReflect.Descriptor instead.\nfunc (*IngestHTTPRequest) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *IngestHTTPRequest) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetMonitorId() string {\n\tif x != nil {\n\t\treturn x.MonitorId\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetLatency() int64 {\n\tif x != nil {\n\t\treturn x.Latency\n\t}\n\treturn 0\n}\n\nfunc (x *IngestHTTPRequest) GetTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestHTTPRequest) GetCronTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.CronTimestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestHTTPRequest) GetUrl() string {\n\tif x != nil {\n\t\treturn x.Url\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetRequestStatus() string {\n\tif x != nil {\n\t\treturn x.RequestStatus\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetBody() string {\n\tif x != nil {\n\t\treturn x.Body\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetHeaders() string {\n\tif x != nil {\n\t\treturn x.Headers\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetTiming() string {\n\tif x != nil {\n\t\treturn x.Timing\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetStatusCode() int64 {\n\tif x != nil {\n\t\treturn x.StatusCode\n\t}\n\treturn 0\n}\n\nfunc (x *IngestHTTPRequest) GetError() int64 {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn 0\n}\n\ntype IngestHTTPResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestHTTPResponse) Reset() {\n\t*x = IngestHTTPResponse{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestHTTPResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestHTTPResponse) ProtoMessage() {}\n\nfunc (x *IngestHTTPResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestHTTPResponse.ProtoReflect.Descriptor instead.\nfunc (*IngestHTTPResponse) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{5}\n}\n\ntype Records struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tRecord        []string               `protobuf:\"bytes,1,rep,name=record,proto3\" json:\"record,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Records) Reset() {\n\t*x = Records{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Records) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Records) ProtoMessage() {}\n\nfunc (x *Records) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Records.ProtoReflect.Descriptor instead.\nfunc (*Records) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *Records) GetRecord() []string {\n\tif x != nil {\n\t\treturn x.Record\n\t}\n\treturn nil\n}\n\ntype IngestDNSRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tMonitorId     string                 `protobuf:\"bytes,2,opt,name=monitorId,proto3\" json:\"monitorId,omitempty\"`\n\tLatency       int64                  `protobuf:\"varint,3,opt,name=latency,proto3\" json:\"latency,omitempty\"`\n\tTimestamp     int64                  `protobuf:\"varint,4,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tCronTimestamp int64                  `protobuf:\"varint,5,opt,name=cronTimestamp,proto3\" json:\"cronTimestamp,omitempty\"`\n\tUri           string                 `protobuf:\"bytes,6,opt,name=uri,proto3\" json:\"uri,omitempty\"`\n\tRequestStatus string                 `protobuf:\"bytes,7,opt,name=requestStatus,proto3\" json:\"requestStatus,omitempty\"`\n\tMessage       string                 `protobuf:\"bytes,8,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tRecords       map[string]*Records    `protobuf:\"bytes,9,rep,name=records,proto3\" json:\"records,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tTiming        string                 `protobuf:\"bytes,10,opt,name=timing,proto3\" json:\"timing,omitempty\"`\n\tError         int64                  `protobuf:\"varint,11,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestDNSRequest) Reset() {\n\t*x = IngestDNSRequest{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestDNSRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestDNSRequest) ProtoMessage() {}\n\nfunc (x *IngestDNSRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestDNSRequest.ProtoReflect.Descriptor instead.\nfunc (*IngestDNSRequest) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *IngestDNSRequest) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetMonitorId() string {\n\tif x != nil {\n\t\treturn x.MonitorId\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetLatency() int64 {\n\tif x != nil {\n\t\treturn x.Latency\n\t}\n\treturn 0\n}\n\nfunc (x *IngestDNSRequest) GetTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestDNSRequest) GetCronTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.CronTimestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestDNSRequest) GetUri() string {\n\tif x != nil {\n\t\treturn x.Uri\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetRequestStatus() string {\n\tif x != nil {\n\t\treturn x.RequestStatus\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetRecords() map[string]*Records {\n\tif x != nil {\n\t\treturn x.Records\n\t}\n\treturn nil\n}\n\nfunc (x *IngestDNSRequest) GetTiming() string {\n\tif x != nil {\n\t\treturn x.Timing\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetError() int64 {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn 0\n}\n\ntype IngestDNSResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestDNSResponse) Reset() {\n\t*x = IngestDNSResponse{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestDNSResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestDNSResponse) ProtoMessage() {}\n\nfunc (x *IngestDNSResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestDNSResponse.ProtoReflect.Descriptor instead.\nfunc (*IngestDNSResponse) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{8}\n}\n\nvar File_private_location_v1_private_location_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_private_location_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"*private_location/v1/private_location.proto\\x12\\x13private_location.v1\\x1a\\x1cgoogle/protobuf/struct.proto\\x1a%private_location/v1/dns_monitor.proto\\x1a&private_location/v1/http_monitor.proto\\x1a%private_location/v1/tcp_monitor.proto\\\"\\x11\\n\" +\n\t\"\\x0fMonitorsRequest\\\"\\xe1\\x01\\n\" +\n\t\"\\x10MonitorsResponse\\x12E\\n\" +\n\t\"\\rhttp_monitors\\x18\\x01 \\x03(\\v2 .private_location.v1.HTTPMonitorR\\fhttpMonitors\\x12B\\n\" +\n\t\"\\ftcp_monitors\\x18\\x02 \\x03(\\v2\\x1f.private_location.v1.TCPMonitorR\\vtcpMonitors\\x12B\\n\" +\n\t\"\\fdns_monitors\\x18\\x03 \\x03(\\v2\\x1f.private_location.v1.DNSMonitorR\\vdnsMonitors\\\"\\x9e\\x02\\n\" +\n\t\"\\x10IngestTCPRequest\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x1c\\n\" +\n\t\"\\tmonitorId\\x18\\x02 \\x01(\\tR\\tmonitorId\\x12\\x18\\n\" +\n\t\"\\alatency\\x18\\x03 \\x01(\\x03R\\alatency\\x12\\x1c\\n\" +\n\t\"\\ttimestamp\\x18\\x04 \\x01(\\x03R\\ttimestamp\\x12$\\n\" +\n\t\"\\rcronTimestamp\\x18\\x05 \\x01(\\x03R\\rcronTimestamp\\x12\\x10\\n\" +\n\t\"\\x03uri\\x18\\x06 \\x01(\\tR\\x03uri\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\a \\x01(\\tR\\amessage\\x12$\\n\" +\n\t\"\\rrequestStatus\\x18\\b \\x01(\\tR\\rrequestStatus\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\t \\x01(\\x03R\\x05error\\x12\\x16\\n\" +\n\t\"\\x06timing\\x18\\n\" +\n\t\" \\x01(\\tR\\x06timing\\\"\\x13\\n\" +\n\t\"\\x11IngestTCPResponse\\\"\\xed\\x02\\n\" +\n\t\"\\x11IngestHTTPRequest\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x1c\\n\" +\n\t\"\\tmonitorId\\x18\\x02 \\x01(\\tR\\tmonitorId\\x12\\x18\\n\" +\n\t\"\\alatency\\x18\\x03 \\x01(\\x03R\\alatency\\x12\\x1c\\n\" +\n\t\"\\ttimestamp\\x18\\x04 \\x01(\\x03R\\ttimestamp\\x12$\\n\" +\n\t\"\\rcronTimestamp\\x18\\x05 \\x01(\\x03R\\rcronTimestamp\\x12\\x10\\n\" +\n\t\"\\x03url\\x18\\x06 \\x01(\\tR\\x03url\\x12$\\n\" +\n\t\"\\rrequestStatus\\x18\\a \\x01(\\tR\\rrequestStatus\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\b \\x01(\\tR\\amessage\\x12\\x12\\n\" +\n\t\"\\x04body\\x18\\t \\x01(\\tR\\x04body\\x12\\x18\\n\" +\n\t\"\\aheaders\\x18\\n\" +\n\t\" \\x01(\\tR\\aheaders\\x12\\x16\\n\" +\n\t\"\\x06timing\\x18\\v \\x01(\\tR\\x06timing\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"statusCode\\x18\\f \\x01(\\x03R\\n\" +\n\t\"statusCode\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\r \\x01(\\x03R\\x05error\\\"\\x14\\n\" +\n\t\"\\x12IngestHTTPResponse\\\"!\\n\" +\n\t\"\\aRecords\\x12\\x16\\n\" +\n\t\"\\x06record\\x18\\x01 \\x03(\\tR\\x06record\\\"\\xc6\\x03\\n\" +\n\t\"\\x10IngestDNSRequest\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x1c\\n\" +\n\t\"\\tmonitorId\\x18\\x02 \\x01(\\tR\\tmonitorId\\x12\\x18\\n\" +\n\t\"\\alatency\\x18\\x03 \\x01(\\x03R\\alatency\\x12\\x1c\\n\" +\n\t\"\\ttimestamp\\x18\\x04 \\x01(\\x03R\\ttimestamp\\x12$\\n\" +\n\t\"\\rcronTimestamp\\x18\\x05 \\x01(\\x03R\\rcronTimestamp\\x12\\x10\\n\" +\n\t\"\\x03uri\\x18\\x06 \\x01(\\tR\\x03uri\\x12$\\n\" +\n\t\"\\rrequestStatus\\x18\\a \\x01(\\tR\\rrequestStatus\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\b \\x01(\\tR\\amessage\\x12L\\n\" +\n\t\"\\arecords\\x18\\t \\x03(\\v22.private_location.v1.IngestDNSRequest.RecordsEntryR\\arecords\\x12\\x16\\n\" +\n\t\"\\x06timing\\x18\\n\" +\n\t\" \\x01(\\tR\\x06timing\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\v \\x01(\\x03R\\x05error\\x1aX\\n\" +\n\t\"\\fRecordsEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x122\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\v2\\x1c.private_location.v1.RecordsR\\x05value:\\x028\\x01\\\"\\x13\\n\" +\n\t\"\\x11IngestDNSResponse2\\x90\\x03\\n\" +\n\t\"\\x16PrivateLocationService\\x12Y\\n\" +\n\t\"\\bMonitors\\x12$.private_location.v1.MonitorsRequest\\x1a%.private_location.v1.MonitorsResponse\\\"\\x00\\x12\\\\\\n\" +\n\t\"\\tIngestTCP\\x12%.private_location.v1.IngestTCPRequest\\x1a&.private_location.v1.IngestTCPResponse\\\"\\x00\\x12_\\n\" +\n\t\"\\n\" +\n\t\"IngestHTTP\\x12&.private_location.v1.IngestHTTPRequest\\x1a'.private_location.v1.IngestHTTPResponse\\\"\\x00\\x12\\\\\\n\" +\n\t\"\\tIngestDNS\\x12%.private_location.v1.IngestDNSRequest\\x1a&.private_location.v1.IngestDNSResponse\\\"\\x00BJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_private_location_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_private_location_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_private_location_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_private_location_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_private_location_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_private_location_proto_rawDesc), len(file_private_location_v1_private_location_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_private_location_proto_rawDescData\n}\n\nvar file_private_location_v1_private_location_proto_msgTypes = make([]protoimpl.MessageInfo, 10)\nvar file_private_location_v1_private_location_proto_goTypes = []any{\n\t(*MonitorsRequest)(nil),    // 0: private_location.v1.MonitorsRequest\n\t(*MonitorsResponse)(nil),   // 1: private_location.v1.MonitorsResponse\n\t(*IngestTCPRequest)(nil),   // 2: private_location.v1.IngestTCPRequest\n\t(*IngestTCPResponse)(nil),  // 3: private_location.v1.IngestTCPResponse\n\t(*IngestHTTPRequest)(nil),  // 4: private_location.v1.IngestHTTPRequest\n\t(*IngestHTTPResponse)(nil), // 5: private_location.v1.IngestHTTPResponse\n\t(*Records)(nil),            // 6: private_location.v1.Records\n\t(*IngestDNSRequest)(nil),   // 7: private_location.v1.IngestDNSRequest\n\t(*IngestDNSResponse)(nil),  // 8: private_location.v1.IngestDNSResponse\n\tnil,                        // 9: private_location.v1.IngestDNSRequest.RecordsEntry\n\t(*HTTPMonitor)(nil),        // 10: private_location.v1.HTTPMonitor\n\t(*TCPMonitor)(nil),         // 11: private_location.v1.TCPMonitor\n\t(*DNSMonitor)(nil),         // 12: private_location.v1.DNSMonitor\n}\nvar file_private_location_v1_private_location_proto_depIdxs = []int32{\n\t10, // 0: private_location.v1.MonitorsResponse.http_monitors:type_name -> private_location.v1.HTTPMonitor\n\t11, // 1: private_location.v1.MonitorsResponse.tcp_monitors:type_name -> private_location.v1.TCPMonitor\n\t12, // 2: private_location.v1.MonitorsResponse.dns_monitors:type_name -> private_location.v1.DNSMonitor\n\t9,  // 3: private_location.v1.IngestDNSRequest.records:type_name -> private_location.v1.IngestDNSRequest.RecordsEntry\n\t6,  // 4: private_location.v1.IngestDNSRequest.RecordsEntry.value:type_name -> private_location.v1.Records\n\t0,  // 5: private_location.v1.PrivateLocationService.Monitors:input_type -> private_location.v1.MonitorsRequest\n\t2,  // 6: private_location.v1.PrivateLocationService.IngestTCP:input_type -> private_location.v1.IngestTCPRequest\n\t4,  // 7: private_location.v1.PrivateLocationService.IngestHTTP:input_type -> private_location.v1.IngestHTTPRequest\n\t7,  // 8: private_location.v1.PrivateLocationService.IngestDNS:input_type -> private_location.v1.IngestDNSRequest\n\t1,  // 9: private_location.v1.PrivateLocationService.Monitors:output_type -> private_location.v1.MonitorsResponse\n\t3,  // 10: private_location.v1.PrivateLocationService.IngestTCP:output_type -> private_location.v1.IngestTCPResponse\n\t5,  // 11: private_location.v1.PrivateLocationService.IngestHTTP:output_type -> private_location.v1.IngestHTTPResponse\n\t8,  // 12: private_location.v1.PrivateLocationService.IngestDNS:output_type -> private_location.v1.IngestDNSResponse\n\t9,  // [9:13] is the sub-list for method output_type\n\t5,  // [5:9] is the sub-list for method input_type\n\t5,  // [5:5] is the sub-list for extension type_name\n\t5,  // [5:5] is the sub-list for extension extendee\n\t0,  // [0:5] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_private_location_proto_init() }\nfunc file_private_location_v1_private_location_proto_init() {\n\tif File_private_location_v1_private_location_proto != nil {\n\t\treturn\n\t}\n\tfile_private_location_v1_dns_monitor_proto_init()\n\tfile_private_location_v1_http_monitor_proto_init()\n\tfile_private_location_v1_tcp_monitor_proto_init()\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_private_location_proto_rawDesc), len(file_private_location_v1_private_location_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   10,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_private_location_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_private_location_proto_depIdxs,\n\t\tMessageInfos:      file_private_location_v1_private_location_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_private_location_proto = out.File\n\tfile_private_location_v1_private_location_proto_goTypes = nil\n\tfile_private_location_v1_private_location_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/checker/proto/private_location/v1/tcp_monitor.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/tcp_monitor.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype TCPMonitor struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tUri           string                 `protobuf:\"bytes,2,opt,name=uri,proto3\" json:\"uri,omitempty\"`\n\tTimeout       int64                  `protobuf:\"varint,3,opt,name=timeout,proto3\" json:\"timeout,omitempty\"`\n\tDegradedAt    *int64                 `protobuf:\"varint,4,opt,name=degraded_at,json=degradedAt,proto3,oneof\" json:\"degraded_at,omitempty\"`\n\tPeriodicity   string                 `protobuf:\"bytes,5,opt,name=periodicity,proto3\" json:\"periodicity,omitempty\"`\n\tRetry         int64                  `protobuf:\"varint,6,opt,name=retry,proto3\" json:\"retry,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TCPMonitor) Reset() {\n\t*x = TCPMonitor{}\n\tmi := &file_private_location_v1_tcp_monitor_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TCPMonitor) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TCPMonitor) ProtoMessage() {}\n\nfunc (x *TCPMonitor) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_tcp_monitor_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TCPMonitor.ProtoReflect.Descriptor instead.\nfunc (*TCPMonitor) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_tcp_monitor_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *TCPMonitor) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *TCPMonitor) GetUri() string {\n\tif x != nil {\n\t\treturn x.Uri\n\t}\n\treturn \"\"\n}\n\nfunc (x *TCPMonitor) GetTimeout() int64 {\n\tif x != nil {\n\t\treturn x.Timeout\n\t}\n\treturn 0\n}\n\nfunc (x *TCPMonitor) GetDegradedAt() int64 {\n\tif x != nil && x.DegradedAt != nil {\n\t\treturn *x.DegradedAt\n\t}\n\treturn 0\n}\n\nfunc (x *TCPMonitor) GetPeriodicity() string {\n\tif x != nil {\n\t\treturn x.Periodicity\n\t}\n\treturn \"\"\n}\n\nfunc (x *TCPMonitor) GetRetry() int64 {\n\tif x != nil {\n\t\treturn x.Retry\n\t}\n\treturn 0\n}\n\nvar File_private_location_v1_tcp_monitor_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_tcp_monitor_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"%private_location/v1/tcp_monitor.proto\\x12\\x13private_location.v1\\\"\\xb6\\x01\\n\" +\n\t\"\\n\" +\n\t\"TCPMonitor\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x10\\n\" +\n\t\"\\x03uri\\x18\\x02 \\x01(\\tR\\x03uri\\x12\\x18\\n\" +\n\t\"\\atimeout\\x18\\x03 \\x01(\\x03R\\atimeout\\x12$\\n\" +\n\t\"\\vdegraded_at\\x18\\x04 \\x01(\\x03H\\x00R\\n\" +\n\t\"degradedAt\\x88\\x01\\x01\\x12 \\n\" +\n\t\"\\vperiodicity\\x18\\x05 \\x01(\\tR\\vperiodicity\\x12\\x14\\n\" +\n\t\"\\x05retry\\x18\\x06 \\x01(\\x03R\\x05retryB\\x0e\\n\" +\n\t\"\\f_degraded_atBJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_tcp_monitor_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_tcp_monitor_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_tcp_monitor_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_tcp_monitor_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_tcp_monitor_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_tcp_monitor_proto_rawDesc), len(file_private_location_v1_tcp_monitor_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_tcp_monitor_proto_rawDescData\n}\n\nvar file_private_location_v1_tcp_monitor_proto_msgTypes = make([]protoimpl.MessageInfo, 1)\nvar file_private_location_v1_tcp_monitor_proto_goTypes = []any{\n\t(*TCPMonitor)(nil), // 0: private_location.v1.TCPMonitor\n}\nvar file_private_location_v1_tcp_monitor_proto_depIdxs = []int32{\n\t0, // [0:0] is the sub-list for method output_type\n\t0, // [0:0] is the sub-list for method input_type\n\t0, // [0:0] is the sub-list for extension type_name\n\t0, // [0:0] is the sub-list for extension extendee\n\t0, // [0:0] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_tcp_monitor_proto_init() }\nfunc file_private_location_v1_tcp_monitor_proto_init() {\n\tif File_private_location_v1_tcp_monitor_proto != nil {\n\t\treturn\n\t}\n\tfile_private_location_v1_tcp_monitor_proto_msgTypes[0].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_tcp_monitor_proto_rawDesc), len(file_private_location_v1_tcp_monitor_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   1,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_tcp_monitor_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_tcp_monitor_proto_depIdxs,\n\t\tMessageInfos:      file_private_location_v1_tcp_monitor_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_tcp_monitor_proto = out.File\n\tfile_private_location_v1_tcp_monitor_proto_goTypes = nil\n\tfile_private_location_v1_tcp_monitor_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/checker/request/request.go",
    "content": "package request\n\nimport (\n\t\"encoding/json\"\n)\n\ntype AssertionType string\n\nconst (\n\tAssertionHeader    AssertionType = \"header\"\n\tAssertionTextBody  AssertionType = \"textBody\"\n\tAssertionStatus    AssertionType = \"status\"\n\tAssertionJsonBody  AssertionType = \"jsonBody\"\n\tAssertionDnsRecord AssertionType = \"dnsRecord\"\n)\n\ntype StringComparator string\n\nconst (\n\tStringContains         StringComparator = \"contains\"\n\tStringNotContains      StringComparator = \"not_contains\"\n\tStringEquals           StringComparator = \"eq\"\n\tStringNotEquals        StringComparator = \"not_eq\"\n\tStringEmpty            StringComparator = \"empty\"\n\tStringNotEmpty         StringComparator = \"not_empty\"\n\tStringGreaterThan      StringComparator = \"gt\"\n\tStringGreaterThanEqual StringComparator = \"gte\"\n\tStringLowerThan        StringComparator = \"lt\"\n\tStringLowerThanEqual   StringComparator = \"lte\"\n)\n\ntype NumberComparator string\n\nconst (\n\tNumberEquals           NumberComparator = \"eq\"\n\tNumberNotEquals        NumberComparator = \"not_eq\"\n\tNumberGreaterThan      NumberComparator = \"gt\"\n\tNumberGreaterThanEqual NumberComparator = \"gte\"\n\tNumberLowerThan        NumberComparator = \"lt\"\n\tNumberLowerThanEqual   NumberComparator = \"lte\"\n)\n\ntype RecordComparator string\n\nconst (\n\tRecordEquals      RecordComparator = \"eq\"\n\tRecordNotEquals   RecordComparator = \"not_eq\"\n\tRecordContains    RecordComparator = \"contains\"\n\tRecordNotContains RecordComparator = \"not_contains\"\n)\n\ntype Record string\n\nconst (\n\tRecordA     Record = \"A\"\n\tRecordAAAA  Record = \"AAAA\"\n\tRecordCNAME Record = \"CNAME\"\n\tRecordMX    Record = \"MX\"\n\tRecordNS    Record = \"NS\"\n\tRecordTXT   Record = \"TXT\"\n)\n\ntype Assertion struct {\n\tAssertionType AssertionType   `json:\"type\"`\n\tComparator    json.RawMessage `json:\"compare\"`\n\tRawTarget     json.RawMessage `json:\"target\"`\n}\n\ntype HttpCheckerRequest struct {\n\tHeaders []struct {\n\t\tKey   string `json:\"key\"`\n\t\tValue string `json:\"value\"`\n\t} `json:\"headers,omitempty\"`\n\tWorkspaceID     string            `json:\"workspaceId\"`\n\tURL             string            `json:\"url\"`\n\tMonitorID       string            `json:\"monitorId\"`\n\tMethod          string            `json:\"method\"`\n\tStatus          string            `json:\"status\"`\n\tBody            string            `json:\"body\"`\n\tTrigger         string            `json:\"trigger,omitempty\"`\n\tRawAssertions   []json.RawMessage `json:\"assertions,omitempty\"`\n\tCronTimestamp   int64             `json:\"cronTimestamp\"`\n\tTimeout         int64             `json:\"timeout\"`\n\tDegradedAfter   int64             `json:\"degradedAfter,omitempty\"`\n\tRetry           int64             `json:\"retry,omitempty\"`\n\tFollowRedirects bool              `json:\"followRedirects,omitempty\"`\n\tOtelConfig      struct {\n\t\tEndpoint string            `json:\"endpoint\"`\n\t\tHeaders  map[string]string `json:\"headers,omitempty\"`\n\t} `json:\"otelConfig\"`\n}\n\ntype TCPCheckerRequest struct {\n\tStatus        string            `json:\"status\"`\n\tWorkspaceID   string            `json:\"workspaceId\"`\n\tURI           string            `json:\"uri\"`\n\tMonitorID     string            `json:\"monitorId\"`\n\tTrigger       string            `json:\"trigger,omitempty\"`\n\tRawAssertions []json.RawMessage `json:\"assertions,omitempty\"`\n\tRequestId     int64             `json:\"requestId,omitempty\"`\n\tCronTimestamp int64             `json:\"cronTimestamp\"`\n\tTimeout       int64             `json:\"timeout\"`\n\tDegradedAfter int64             `json:\"degradedAfter,omitempty\"`\n\tRetry         int64             `json:\"retry,omitempty\"`\n\tOtelConfig    struct {\n\t\tEndpoint string            `json:\"endpoint\"`\n\t\tHeaders  map[string]string `json:\"headers,omitempty\"`\n\t} `json:\"otelConfig\"`\n}\n\ntype TCPRequest struct {\n\tWorkspaceID   string `json:\"workspaceId\"`\n\tURL           string `json:\"url\"`\n\tMonitorID     string `json:\"monitorId\"`\n\tCronTimestamp int64  `json:\"cronTimestamp\"`\n\tTimeout       int64  `json:\"timeout\"`\n}\n\ntype PingRequest struct {\n\tHeaders     map[string]string `json:\"headers\"`\n\tURL         string            `json:\"url\"`\n\tMethod      string            `json:\"method\"`\n\tBody        string            `json:\"body\"`\n\tRequestId   int64             `json:\"requestId\"`\n\tWorkspaceId int64             `json:\"workspaceId\"`\n}\n\ntype DNSCheckerRequest struct {\n\tStatus        string            `json:\"status\"`\n\tWorkspaceID   string            `json:\"workspaceId\"`\n\tURI           string            `json:\"uri\"`\n\tMonitorID     string            `json:\"monitorId\"`\n\tTrigger       string            `json:\"trigger,omitempty\"`\n\tRawAssertions []json.RawMessage `json:\"assertions,omitempty\"`\n\tRequestId     int64             `json:\"requestId,omitempty\"`\n\tCronTimestamp int64             `json:\"cronTimestamp\"`\n\tTimeout       int64             `json:\"timeout\"`\n\tDegradedAfter int64             `json:\"degradedAfter,omitempty\"`\n\tRetry         int64             `json:\"retry,omitempty\"`\n\tOtelConfig    struct {\n\t\tEndpoint string            `json:\"endpoint\"`\n\t\tHeaders  map[string]string `json:\"headers,omitempty\"`\n\t} `json:\"otelConfig\"`\n}\n"
  },
  {
    "path": "apps/dashboard/.dockerignore",
    "content": "# This file is generated by Dofigen v2.5.1\n# See https://github.com/lenra-io/dofigen\n\n"
  },
  {
    "path": "apps/dashboard/.gitignore",
    "content": ".vercel\n"
  },
  {
    "path": "apps/dashboard/Dockerfile",
    "content": "# syntax=docker/dockerfile:1.11\n# This file is generated by Dofigen v2.5.1\n# See https://github.com/lenra-io/dofigen\n\n# builder\nFROM node@sha256:0afb7822fac7bf9d7c1bf3b6e6c496dee6b2b64d8dfa365501a3c68e8eba94b2 AS builder\nLABEL \\\n    org.opencontainers.image.base.digest=\"sha256:0afb7822fac7bf9d7c1bf3b6e6c496dee6b2b64d8dfa365501a3c68e8eba94b2\" \\\n    org.opencontainers.image.base.name=\"docker.io/node:24-slim\"\nENV \\\n    PROJECT_ID_VERCEL=\"test\" \\\n    CRON_SECRET=\"test\" \\\n    DATABASE_URL=\"http://libsql:8080\" \\\n    DATABASE_AUTH_TOKEN=\"test\" \\\n    UPSTASH_REDIS_REST_TOKEN=\"test\" \\\n    UPSTASH_REDIS_REST_URL=\"test\" \\\n    AUTH_SECRET=\"build-time-placeholder-min-32-chars-long\" \\\n    OPENPANEL_CLIENT_SECRET=\"test\" \\\n    VERCEL_AUTH_BEARER_TOKEN=\"test\" \\\n    TEAM_ID_VERCEL=\"test\" \\\n    TINY_BIRD_API_KEY=\"test\" \\\n    UNKEY_TOKEN=\"test\" \\\n    NEXT_PUBLIC_URL=\"http://localhost:3002\" \\\n    STRIPE_SECRET_KEY=\"test\" \\\n    UNKEY_API_ID=\"test\" \\\n    NEXT_PUBLIC_OPENPANEL_CLIENT_ID=\"test\" \\\n    PATH=\"$PNPM_HOME:$PATH\" \\\n    NODE_ENV=\"production\" \\\n    SELF_HOST=\"true\" \\\n    PNPM_HOME=\"/pnpm\" \\\n    RESEND_API_KEY=\"test\"\nWORKDIR /app\nCOPY \\\n    --link \\\n    \".\" \"/app/\"\nRUN <<EOF\ncorepack enable\npnpm install --frozen-lockfile\npnpm turbo run build --filter=@openstatus/dashboard\nEOF\n\n# runtime\nFROM node@sha256:0afb7822fac7bf9d7c1bf3b6e6c496dee6b2b64d8dfa365501a3c68e8eba94b2 AS runtime\nLABEL \\\n    io.dofigen.version=\"2.5.1\" \\\n    org.opencontainers.image.base.digest=\"sha256:0afb7822fac7bf9d7c1bf3b6e6c496dee6b2b64d8dfa365501a3c68e8eba94b2\" \\\n    org.opencontainers.image.base.name=\"docker.io/node:24-slim\"\nWORKDIR /app/apps/dashboard\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --chmod=555 \\\n    --link \\\n    \"/app/apps/dashboard/.next/standalone/apps/dashboard/\" \"./\"\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/app/node_modules/\" \"/app/node_modules/\"\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/app/apps/dashboard/.next/static/\" \"./.next/static/\"\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/app/apps/dashboard/public/\" \"./public/\"\nUSER 0:0\nRUN <<EOF\napt-get update\napt-get install -y --no-install-recommends curl\nrm -rf /var/lib/apt/lists/*\nEOF\nUSER 1000:1000\nEXPOSE 3000\nHEALTHCHECK \\\n    --interval=30s \\\n    --timeout=10s \\\n    --start-period=45s \\\n    --retries=3 \\\n    CMD curl -f http://localhost:3000/ || exit 1\nCMD [\"node\", \"server.js\"]\n"
  },
  {
    "path": "apps/dashboard/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"src/app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}\n"
  },
  {
    "path": "apps/dashboard/docker-compose.yaml",
    "content": "name: server\nservices:\n    server:\n        build:\n         context: ../..\n         dockerfile: apps/dashboard/Dockerfile\n        ports:\n            - 3000:3000\n        image: dashboard\n        env_file:\n            - ../../.env.docker\n        command: .\n"
  },
  {
    "path": "apps/dashboard/dofigen.yml",
    "content": "builders:\n  # Stage 1: Next.js build with Node.js\n  builder:\n    fromImage: node:24-slim\n    workdir: /app\n    copy:\n      - . /app/\n    env:\n      NODE_ENV: production\n      PNPM_HOME: /pnpm\n      PATH: $PNPM_HOME:$PATH\n      # Build-time environment variables (placeholder values, overwritten by .env.docker at runtime)\n      DATABASE_URL: http://libsql:8080\n      DATABASE_AUTH_TOKEN: test\n      NEXT_PUBLIC_OPENPANEL_CLIENT_ID: test\n      NEXT_PUBLIC_URL: http://localhost:3002\n      TEAM_ID_VERCEL: test\n      PROJECT_ID_VERCEL: test\n      VERCEL_AUTH_BEARER_TOKEN: test\n      OPENPANEL_CLIENT_SECRET: test\n      RESEND_API_KEY: test\n      UPSTASH_REDIS_REST_URL: test\n      UPSTASH_REDIS_REST_TOKEN: test\n      UNKEY_TOKEN: test\n      UNKEY_API_ID: test\n      TINY_BIRD_API_KEY: test\n      CRON_SECRET: test\n      STRIPE_SECRET_KEY: test\n      AUTH_SECRET: build-time-placeholder-min-32-chars-long\n      SELF_HOST: \"true\"\n    run:\n      - corepack enable\n      - pnpm install --frozen-lockfile\n      - pnpm turbo run build --filter=@openstatus/dashboard\n\n# Runtime stage\nfromImage: node:24-slim\nworkdir: /app/apps/dashboard\n\n# Copy artifacts from builder\ncopy:\n  # Copy Next.js standalone output\n  - fromBuilder: builder\n    source: /app/apps/dashboard/.next/standalone/apps/dashboard/\n    target: ./\n    chmod: \"555\"\n  # Copy root node_modules (required for pnpm symlinks)\n  - fromBuilder: builder\n    source: /app/node_modules/\n    target: /app/node_modules/\n  # Copy static assets\n  - fromBuilder: builder\n    source: /app/apps/dashboard/.next/static/\n    target: ./.next/static/\n  # Copy public directory\n  - fromBuilder: builder\n    source: /app/apps/dashboard/public/\n    target: ./public/\n\n# Install curl for health checks\nroot:\n  run:\n    - apt-get update\n    - apt-get install -y --no-install-recommends curl\n    - rm -rf /var/lib/apt/lists/*\n\n# Security: run as non-root user\nuser: \"1000:1000\"\n\n# Expose port\nexpose: \"3000\"\n\n# Health check\nhealthcheck:\n  interval: 30s\n  timeout: 10s\n  start: 45s\n  retries: 3\n  cmd: curl -f http://localhost:3000/ || exit 1\n\n# Start application\ncmd:\n  - node\n  - server.js\n"
  },
  {
    "path": "apps/dashboard/env.ts",
    "content": "const file = Bun.file(\"./.env.example\");\nawait Bun.write(\"./.env\", file);\n"
  },
  {
    "path": "apps/dashboard/instrumentation-client.ts",
    "content": "// This file configures the initialization of Sentry on the client.\n// The config you add here will be used whenever a users loads a page in their browser.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\n\nimport * as Sentry from \"@sentry/nextjs\";\n\nSentry.init({\n  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_FRONTEND,\n\n  // Adjust this value in production, or use tracesSampler for greater control\n  tracesSampleRate: 0.5,\n\n  // Setting this option to true will print useful information to the console while you're setting up Sentry.\n  debug: false,\n\n  replaysOnErrorSampleRate: 1.0,\n\n  // This sets the sample rate to be 10%. You may want this to be 100% while\n  // in development and sample at a lower rate in production\n  replaysSessionSampleRate: 0.1,\n\n  // You can remove this option if you're not planning to use the Sentry Session Replay feature:\n  integrations: [\n    Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),\n    Sentry.captureConsoleIntegration({ levels: [\"error\"] }),\n  ],\n});\n\nexport const onRouterTransitionStart = Sentry.captureRouterTransitionStart;\n\nexport const onRequestError = Sentry.captureRequestError;\n"
  },
  {
    "path": "apps/dashboard/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "apps/dashboard/next.config.ts",
    "content": "import { withSentryConfig } from \"@sentry/nextjs\";\nimport type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  output: process.env.SELF_HOST === \"true\" ? \"standalone\" : undefined,\n  images: {\n    remotePatterns: [\n      new URL(\"https://openstatus.dev/**\"),\n      new URL(\"https://**.public.blob.vercel-storage.com/**\"),\n      new URL(\"https://www.openstatus.dev/**\"),\n    ],\n  },\n  logging: {\n    fetches: {\n      fullUrl: true,\n    },\n  },\n};\n\n// For detailed options, refer to the official documentation:\n// - Webpack plugin options: https://github.com/getsentry/sentry-webpack-plugin#options\n// - Next.js Sentry setup guide: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/\nconst sentryConfig = {\n  // Prevent log output unless running in a CI environment (helps reduce noise in logs)\n  silent: !process.env.CI,\n  org: \"openstatus\",\n  project: \"openstatus\",\n  authToken: process.env.SENTRY_AUTH_TOKEN,\n\n  // Upload a larger set of source maps for improved stack trace accuracy (increases build time)\n  widenClientFileUpload: true,\n\n  // If set to true, transpiles Sentry SDK to be compatible with IE11 (increases bundle size)\n  transpileClientSDK: false,\n\n  // Tree-shake Sentry logger statements to reduce bundle size\n  webpack: {\n    treeshake: {\n      removeDebugLogging: true,\n    },\n  },\n};\n\nexport default withSentryConfig(nextConfig, sentryConfig);\n"
  },
  {
    "path": "apps/dashboard/package.json",
    "content": "{\n  \"name\": \"@openstatus/dashboard\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@auth/core\": \"0.40.0\",\n    \"@auth/drizzle-adapter\": \"1.10.0\",\n    \"@date-fns/tz\": \"1.2.0\",\n    \"@date-fns/utc\": \"2.1.0\",\n    \"@dnd-kit/core\": \"6.3.1\",\n    \"@dnd-kit/modifiers\": \"9.0.0\",\n    \"@dnd-kit/sortable\": \"10.0.0\",\n    \"@dnd-kit/utilities\": \"3.2.2\",\n    \"@hookform/devtools\": \"4.4.0\",\n    \"@hookform/resolvers\": \"5.1.0\",\n    \"@libsql/client\": \"0.15.15\",\n    \"@openpanel/nextjs\": \"1.2.0\",\n    \"@openstatus/analytics\": \"workspace:*\",\n    \"@openstatus/api\": \"workspace:*\",\n    \"@openstatus/assertions\": \"workspace:*\",\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/emails\": \"workspace:*\",\n    \"@openstatus/error\": \"workspace:*\",\n    \"@openstatus/header-analysis\": \"workspace:*\",\n    \"@openstatus/icons\": \"workspace:*\",\n    \"@openstatus/importers\": \"workspace:*\",\n    \"@openstatus/notification-discord\": \"workspace:*\",\n    \"@openstatus/notification-emails\": \"workspace:*\",\n    \"@openstatus/notification-google-chat\": \"workspace:*\",\n    \"@openstatus/notification-grafana-oncall\": \"workspace:*\",\n    \"@openstatus/notification-ntfy\": \"workspace:*\",\n    \"@openstatus/notification-opsgenie\": \"workspace:*\",\n    \"@openstatus/notification-pagerduty\": \"workspace:*\",\n    \"@openstatus/notification-slack\": \"workspace:*\",\n    \"@openstatus/notification-telegram\": \"workspace:*\",\n    \"@openstatus/notification-twillio-whatsapp\": \"workspace:*\",\n    \"@openstatus/notification-webhook\": \"workspace:*\",\n    \"@openstatus/react\": \"workspace:*\",\n    \"@openstatus/regions\": \"workspace:*\",\n    \"@openstatus/theme-store\": \"workspace:*\",\n    \"@openstatus/tinybird\": \"workspace:*\",\n    \"@openstatus/tracker\": \"workspace:*\",\n    \"@openstatus/ui\": \"workspace:*\",\n    \"@openstatus/upstash\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:*\",\n    \"@radix-ui/react-dialog\": \"1.1.14\",\n    \"@radix-ui/react-dropdown-menu\": \"2.1.15\",\n    \"@radix-ui/react-portal\": \"1.1.9\",\n    \"@radix-ui/react-select\": \"2.2.5\",\n    \"@radix-ui/react-slot\": \"1.2.3\",\n    \"@radix-ui/react-tooltip\": \"1.2.7\",\n    \"@sentry/nextjs\": \"10.31.0\",\n    \"@stripe/stripe-js\": \"2.1.6\",\n    \"@tanstack/react-query\": \"5.81.5\",\n    \"@tanstack/react-table\": \"8.21.3\",\n    \"@trpc/client\": \"11.4.4\",\n    \"@trpc/next\": \"11.4.4\",\n    \"@trpc/react-query\": \"11.4.4\",\n    \"@trpc/server\": \"11.4.4\",\n    \"@trpc/tanstack-react-query\": \"11.4.4\",\n    \"@unkey/api\": \"2.2.0\",\n    \"class-variance-authority\": \"0.7.1\",\n    \"clsx\": \"2.1.1\",\n    \"cmdk\": \"1.1.1\",\n    \"date-fns\": \"3.6.0\",\n    \"lucide-react\": \"0.525.0\",\n    \"next\": \"16.1.6\",\n    \"next-auth\": \"5.0.0-beta.29\",\n    \"next-themes\": \"0.4.6\",\n    \"nuqs\": \"2.8.5\",\n    \"random-word-slugs\": \"0.1.7\",\n    \"react\": \"19.2.3\",\n    \"react-day-picker\": \"8.10.1\",\n    \"react-dom\": \"19.2.3\",\n    \"react-hook-form\": \"7.68.0\",\n    \"recharts\": \"2.15.0\",\n    \"rehype-react\": \"8.0.0\",\n    \"remark-gfm\": \"4.0.1\",\n    \"remark-parse\": \"11.0.0\",\n    \"remark-rehype\": \"11.1.2\",\n    \"sonner\": \"2.0.5\",\n    \"stripe\": \"13.8.0\",\n    \"superjson\": \"2.2.2\",\n    \"tailwind-merge\": \"3.3.1\",\n    \"unified\": \"11.0.5\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"4.1.11\",\n    \"@tailwindcss/typography\": \"0.5.10\",\n    \"@types/dom-speech-recognition\": \"0.0.6\",\n    \"@types/node\": \"24.0.8\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"shadcn\": \"3.8.4\",\n    \"tailwindcss\": \"4.1.11\",\n    \"tw-animate-css\": \"1.3.4\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "apps/dashboard/postcss.config.mjs",
    "content": "const config = {\n  plugins: [\"@tailwindcss/postcss\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "apps/dashboard/sentry.edge.config.ts",
    "content": "// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).\n// The config you add here will be used whenever one of the edge features is loaded.\n// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\nimport * as Sentry from \"@sentry/nextjs\";\nimport { TRPCError } from \"@trpc/server\";\n\n// tRPC error codes that should not be reported to Sentry (expected client errors)\nconst IGNORED_TRPC_CODES: TRPCError[\"code\"][] = [\n  \"UNAUTHORIZED\",\n  \"NOT_FOUND\",\n  \"BAD_REQUEST\",\n];\n\nSentry.init({\n  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,\n\n  // Adjust this value in production, or use tracesSampler for greater control\n  tracesSampleRate: 0,\n\n  // Setting this option to true will print useful information to the console while you're setting up Sentry.\n  debug: false,\n  integrations: [Sentry.captureConsoleIntegration({ levels: [\"error\"] })],\n\n  beforeSend(event, hint) {\n    if (\n      hint.originalException instanceof TRPCError &&\n      IGNORED_TRPC_CODES.includes(hint.originalException.code)\n    ) {\n      return null;\n    }\n    return event;\n  },\n});\n"
  },
  {
    "path": "apps/dashboard/sentry.server.config.ts",
    "content": "// This file configures the initialization of Sentry on the server.\n// The config you add here will be used whenever the server handles a request.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\n\nimport * as Sentry from \"@sentry/nextjs\";\nimport { TRPCError } from \"@trpc/server\";\n\n// tRPC error codes that should not be reported to Sentry (expected client errors)\nconst IGNORED_TRPC_CODES: TRPCError[\"code\"][] = [\n  \"UNAUTHORIZED\",\n  \"NOT_FOUND\",\n  \"BAD_REQUEST\",\n];\n\nSentry.init({\n  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,\n\n  // Adjust this value in production, or use tracesSampler for greater control\n  tracesSampleRate: 0.2,\n\n  // Setting this option to true will print useful information to the console while you're setting up Sentry.\n  debug: false,\n  integrations: [Sentry.captureConsoleIntegration({ levels: [\"error\"] })],\n\n  beforeSend(event, hint) {\n    if (\n      hint.originalException instanceof TRPCError &&\n      IGNORED_TRPC_CODES.includes(hint.originalException.code)\n    ) {\n      return null;\n    }\n    return event;\n  },\n});\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/agents/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Bot } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb items={[{ type: \"page\", label: \"Agents\", icon: Bot }]} />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/agents/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <Breadcrumb />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/agents/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { Book } from \"lucide-react\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n      <TooltipProvider>\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Button variant=\"ghost\" size=\"sm\" className=\"group h-7 w-7\" asChild>\n              <a\n                href={\"https://docs.openstatus.dev/reference/cli-reference\"}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                <Book className=\"h-4 w-4 text-muted-foreground group-hover:text-foreground\" />\n              </a>\n            </Button>\n          </TooltipTrigger>\n          <TooltipContent>View Documentation</TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/agents/page.tsx",
    "content": "\"use client\";\n\nimport { Code } from \"@/components/common/code\";\nimport { Link } from \"@/components/common/link\";\nimport { Note } from \"@/components/common/note\";\nimport {\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { Section } from \"@/components/content/section\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Info } from \"lucide-react\";\n\nconst messages = [\n  {\n    message:\n      \"@openstatus create an incident for the payment API – high latency detected.\",\n    description: \"Open a new incident and notify your subscribers.\",\n  },\n  {\n    message:\n      \"@openstatus keep the status page updated that we are still monitoring the issue.\",\n    description: \"Update the status report to 'Monitoring'.\",\n  },\n  {\n    message: \"@openstatus resolve the ongoing incident on my API status page.\",\n    description: \"Close an active incident and update your subscribers.\",\n  },\n  {\n    message:\n      \"@openstatus schedule a maintenance window for my database next Friday from 2–3 PM.\",\n    description: \"Plan downtime so subscribers are informed in advance.\",\n  },\n];\n\nexport default function Page() {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Agents</SectionTitle>\n          <SectionDescription>\n            Use our Slack agent to manage your status pages and incidents.{\" \"}\n            <Link\n              href=\"https://www.openstatus.dev/blog/openstatus-slack-agent\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              Read more\n            </Link>\n            .\n          </SectionDescription>\n        </SectionHeader>\n        {!workspace?.limits[\"slack-agent\"] ? (\n          <Note color=\"info\" size=\"sm\">\n            <Info />\n            This is a paid feature. Upgrade your plan to use the Slack agent.\n          </Note>\n        ) : null}\n        <Button size=\"sm\" asChild>\n          <Link href=\"/settings/integrations\">Install the Slack agent</Link>\n        </Button>\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Messages</SectionTitle>\n          <SectionDescription>\n            Here are some examples of messages that the Slack agent can handle.\n          </SectionDescription>\n        </SectionHeader>\n        <Note size=\"sm\">\n          <Info />\n          Mention the @openstatus bot to trigger a response. This keeps threads\n          clean without the bot spamming your team.\n        </Note>\n        <ul className=\"flex flex-col gap-2\">\n          {messages.map((message, i) => (\n            <li key={i} className=\"flex flex-col gap-0.5\">\n              <p className=\"text-muted-foreground text-xs\">\n                {message.description}\n              </p>\n              <Code>{message.message}</Code>\n            </li>\n          ))}\n        </ul>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/cli/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Terminal } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb items={[{ type: \"page\", label: \"CLI\", icon: Terminal }]} />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/cli/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <Breadcrumb />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/cli/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { Book } from \"lucide-react\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n      <TooltipProvider>\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Button variant=\"ghost\" size=\"sm\" className=\"group h-7 w-7\" asChild>\n              <a\n                href={\"https://docs.openstatus.dev/reference/cli-reference\"}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                <Book className=\"h-4 w-4 text-muted-foreground group-hover:text-foreground\" />\n              </a>\n            </Button>\n          </TooltipTrigger>\n          <TooltipContent>View Documentation</TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/cli/page.tsx",
    "content": "import { Code } from \"@/components/common/code\";\nimport { Link } from \"@/components/common/link\";\nimport {\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { Section } from \"@/components/content/section\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { FileDown, FileJson, Key, Terminal } from \"lucide-react\";\nimport React from \"react\";\n\nconst OS = [\"macOs\", \"Windows\", \"Linux\"] as const;\n\nconst installs = [\n  {\n    title: \"Install CLI\",\n    icon: Terminal,\n    description:\n      \"Install the OpenStatus CLI to set up your monitors straight in your code.\",\n    command: {\n      macOs: [\n        \"brew install openstatusHQ/cli/openstatus --cask\",\n        \"curl -fsSL https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.sh | bash\",\n      ],\n      Linux: [\n        \"curl -fsSL https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.sh | bash\",\n      ],\n      Windows: [\n        \"iwr https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.ps1| iex\",\n      ],\n    },\n  },\n  {\n    title: \"Add API Key\",\n    icon: Key,\n    description: (\n      <>\n        Create an API key in your workspace{\" \"}\n        <Link href=\"/settings/general\">settings.</Link>\n      </>\n    ),\n    command: {\n      macOs: [\"export OPENSTATUS_API_TOKEN=<your-api-token>\"],\n      Windows: [\"set OPENSTATUS_API_TOKEN=<your-api-token>\"],\n      Linux: [\"export OPENSTATUS_API_TOKEN=<your-api-token>\"],\n    },\n  },\n  {\n    title: \"Import Monitors\",\n    icon: FileDown,\n    description: \"Import monitors from your workspace to a YAML file.\",\n    command: \"openstatus monitors import\",\n  },\n  {\n    title: \"Manage Monitors\",\n    icon: FileJson,\n    description:\n      \"Add, remove, or update monitors from a YAML file and apply your changes.\",\n    command: \"openstatus monitors apply\",\n  },\n] satisfies {\n  title: string;\n  icon: React.ElementType;\n  description: React.ReactNode;\n  command: string | Record<(typeof OS)[number], string[]>;\n}[];\n\nconst commands = [\n  {\n    command: \"openstatus monitors list [options]\",\n    description: \"List all monitors in your workspace.\",\n  },\n  {\n    command: \"openstatus monitors info [monitor-id] [options]\",\n    description: \"Get information about a specific monitor.\",\n  },\n  {\n    command: \"openstatus monitors trigger [monitor-id] [options]\",\n    description: \"Trigger a monitor.\",\n  },\n  {\n    command: \"openstatus run [options]\",\n    description: \"Run a list of monitors.\",\n  },\n];\n\nconst templates = [\n  {\n    description: \"MCP server\",\n    template: `# yaml-language-server: $schema=https://www.openstatus.dev/schema.json\n\nmcp-server:\n  name: \"HF MCP Server\"\n  description: \"Hugging Face MCP server monitoring\"\n  frequency: \"1m\"\n  active: true\n  regions: [\"iad\", \"ams\", \"lax\"]\n  retry: 3\n  kind: http\n  request:\n    url: https://hf.co/mcp\n    method: POST\n    body: >\n      {\n        \"jsonrpc\": \"2.0\",\n        \"id\": \"openstatus\",\n        \"method\": \"ping\"\n      }\n    headers:\n      User-Agent: OpenStatus\n      Accept: application/json, text/event-stream\n      Content-Type: application/json\n  assertions:\n    - kind: statusCode\n      compare: eq\n      target: 200\n    - kind: textBody\n      compare: eq\n      target: '{\"result\":{},\"jsonrpc\":\"2.0\",\"id\":\"openstatus\"}'\n`,\n  },\n];\n\nexport default function Page() {\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>CLI</SectionTitle>\n          <SectionDescription>\n            Get started with the CLI to export and manage your monitors in your\n            code.{\" \"}\n            <Link\n              href=\"https://docs.openstatus.dev/reference/cli-reference/\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              Read more\n            </Link>\n            .\n          </SectionDescription>\n        </SectionHeader>\n        <Tabs defaultValue={OS[0]} className=\"flex flex-col gap-3\">\n          <TabsList>\n            {OS.map((os) => (\n              <TabsTrigger key={os} value={os}>\n                {os}\n              </TabsTrigger>\n            ))}\n          </TabsList>\n          {OS.map((os) => (\n            <TabsContent key={os} value={os} className=\"flex flex-col gap-6\">\n              {installs.map((step, i) => {\n                const commands =\n                  typeof step.command === \"string\"\n                    ? step.command\n                    : step.command[os];\n                return (\n                  <div key={i} className=\"flex flex-col gap-3\">\n                    <div className=\"flex flex-col gap-1\">\n                      <p className=\"flex items-center gap-2 font-medium text-sm\">\n                        <step.icon className=\"size-4\" />\n                        {step.title}\n                      </p>\n                      <p className=\"text-muted-foreground text-sm\">\n                        {step.description}\n                      </p>\n                    </div>\n                    {typeof commands === \"string\" ? (\n                      <Code>{commands}</Code>\n                    ) : (\n                      <>\n                        {commands.map((command, i) => (\n                          <React.Fragment key={command}>\n                            <Code>{command}</Code>\n                            {i < commands.length - 1 && (\n                              <span className=\"text-muted-foreground\">or</span>\n                            )}\n                          </React.Fragment>\n                        ))}\n                      </>\n                    )}\n                  </div>\n                );\n              })}\n            </TabsContent>\n          ))}\n        </Tabs>\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Commands</SectionTitle>\n          <SectionDescription>\n            We have a few more commands to run. Check the{\" \"}\n            <Link\n              href=\"https://docs.openstatus.dev/reference/cli-reference/\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              documentation\n            </Link>{\" \"}\n            to read more.\n          </SectionDescription>\n        </SectionHeader>\n        <ul className=\"flex flex-col gap-2\">\n          {commands.map((command, i) => (\n            <li key={i} className=\"flex flex-col gap-0.5\">\n              <p className=\"text-muted-foreground text-xs\">\n                {command.description}\n              </p>\n              <Code>{command.command}</Code>\n            </li>\n          ))}\n        </ul>\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>GitHub Action</SectionTitle>\n          <SectionDescription>\n            We provide you with a github action in case you'd like to use the\n            CLI within your CI/CD workflows. Check the{\" \"}\n            <Link\n              href=\"https://github.com/openstatusHQ/openstatus-github-action\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              GitHub integration\n            </Link>{\" \"}\n            page or our{\" \"}\n            <Link\n              href=\"https://docs.openstatus.dev/guides/how-to-run-synthetic-test-github-action/\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              guide\n            </Link>{\" \"}\n            to to run synthetic tests in a GitHub action.\n          </SectionDescription>\n        </SectionHeader>\n        {/* TODO: add code example */}\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Templates</SectionTitle>\n          <SectionDescription>\n            We have a few templates to help you get started. Check the{\" \"}\n            <Link\n              href=\"https://github.com/openstatusHQ/cli-template\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <code>@openstatusHQ/cli-template</code>\n            </Link>{\" \"}\n            repository for more.\n          </SectionDescription>\n        </SectionHeader>\n        <div className=\"flex flex-col gap-6\">\n          {templates.map((template, i) => (\n            <div key={i} className=\"flex flex-col gap-0.5\">\n              <p className=\"text-muted-foreground text-xs\">\n                {template.description}\n              </p>\n              <Code>{template.template}</Code>\n            </div>\n          ))}\n        </div>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/invite/client.tsx",
    "content": "\"use client\";\n\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useQueryStates } from \"nuqs\";\nimport { useTransition } from \"react\";\nimport { toast } from \"sonner\";\nimport { searchParamsParsers } from \"./search-params\";\n\nexport function Client() {\n  const trpc = useTRPC();\n  const [isPending, startTransition] = useTransition();\n  const [{ token }] = useQueryStates(searchParamsParsers);\n  const { data: invitation, error } = useQuery({\n    ...trpc.invitation.get.queryOptions({ token }),\n    retry: false,\n  });\n  const acceptInvitationMutation = useMutation(\n    trpc.invitation.accept.mutationOptions({\n      onSuccess: (workspace) => {\n        if (!workspace) return;\n        document.cookie = `workspace-slug=${workspace.slug}; path=/;`;\n        window.location.href = \"/overview\";\n      },\n    }),\n  );\n\n  // TODO: check if we can have a high level wrapper for isTRPCClientError errors\n  if (isTRPCClientError(error)) {\n    return (\n      <SectionGroup>\n        <Section>\n          <SectionHeader>\n            <SectionTitle className=\"text-destructive\">Error</SectionTitle>\n            <SectionDescription className=\"font-mono\">\n              {error.message}\n            </SectionDescription>\n          </SectionHeader>\n        </Section>\n      </SectionGroup>\n    );\n  }\n\n  if (!invitation) return null;\n  if (invitation.acceptedAt) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Invitation</SectionTitle>\n          <SectionDescription>\n            You&apos;ve been invited to join the workspace{\" \"}\n            {invitation.workspace.name ? (\n              <span className=\"font-semibold\">{invitation.workspace.name}</span>\n            ) : (\n              <span className=\"font-mono\">{invitation.workspace.slug}</span>\n            )}\n            .\n          </SectionDescription>\n        </SectionHeader>\n        <Button\n          size=\"sm\"\n          onClick={() => {\n            startTransition(async () => {\n              try {\n                const promise = acceptInvitationMutation.mutateAsync({\n                  id: invitation.id,\n                });\n                toast.promise(promise, {\n                  loading: \"Accepting invitation...\",\n                  success: \"Invitation accepted\",\n                  error: (error) => {\n                    if (isTRPCClientError(error)) {\n                      return error.message;\n                    }\n                    return \"Failed to accept invitation\";\n                  },\n                });\n                await promise;\n              } catch (error) {\n                console.error(error);\n              }\n            });\n          }}\n        >\n          {isPending ? \"Accepting...\" : \"Accept Invitation\"}\n        </Button>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/invite/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <NavBreadcrumb items={[{ type: \"page\", label: \"Invite\" }]} />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/invite/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/invite/page.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { redirect } from \"next/navigation\";\nimport type { SearchParams } from \"nuqs\";\nimport { Client } from \"./client\";\nimport { searchParamsCache } from \"./search-params\";\n\nexport default async function InvitePage(props: {\n  searchParams: Promise<SearchParams>;\n}) {\n  const { token } = await searchParamsCache.parse(props.searchParams);\n\n  if (!token) {\n    return redirect(\"/overview\");\n  }\n\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(trpc.invitation.get.queryOptions({ token }));\n\n  return (\n    <HydrateClient>\n      <Client />\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/invite/search-params.tsx",
    "content": "import { createSearchParamsCache, parseAsString } from \"nuqs/server\";\n\nexport const searchParamsParsers = {\n  token: parseAsString,\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/layout.tsx",
    "content": "import { AppSidebar } from \"@/components/nav/app-sidebar\";\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport {\n  SidebarInset,\n  SidebarProvider,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport { cookies } from \"next/headers\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const cookieStore = await cookies();\n  const hasState = cookieStore.has(\"sidebar_state\");\n  const defaultOpen = hasState\n    ? cookieStore.get(\"sidebar_state\")?.value === \"true\"\n    : true;\n\n  return (\n    <HydrateSidebar>\n      <SidebarProvider defaultOpen={defaultOpen}>\n        <AppSidebar />\n        <SidebarInset>{children}</SidebarInset>\n      </SidebarProvider>\n    </HydrateSidebar>\n  );\n}\n\nasync function HydrateSidebar({ children }: { children: React.ReactNode }) {\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(trpc.page.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.monitor.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.workspace.get.queryOptions());\n  await queryClient.prefetchQuery(trpc.user.get.queryOptions());\n\n  return <HydrateClient>{children}</HydrateClient>;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/(list)/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Activity } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[{ type: \"page\", label: \"Monitors\", icon: Activity }]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/(list)/client.tsx",
    "content": "\"use client\";\n\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { columns } from \"@/components/data-table/monitors/columns\";\nimport { MonitorDataTableActionBar } from \"@/components/data-table/monitors/data-table-action-bar\";\nimport { MonitorDataTableToolbar } from \"@/components/data-table/monitors/data-table-toolbar\";\nimport {\n  MetricCardButton,\n  MetricCardGroup,\n  MetricCardHeader,\n  MetricCardSkeleton,\n  MetricCardTitle,\n  MetricCardValue,\n} from \"@/components/metric/metric-card\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { DataTablePaginationSimple } from \"@/components/ui/data-table/data-table-pagination\";\nimport { getMonitorListMetrics } from \"@/data/metrics.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { ColumnFiltersState, SortingState } from \"@tanstack/react-table\";\nimport { ArrowDown, CheckCircle, ListFilter } from \"lucide-react\";\nimport { useQueryStates } from \"nuqs\";\nimport { useEffect, useState } from \"react\";\nimport { searchParamsParsers } from \"./search-params\";\n\nconst icons = {\n  default: {\n    active: CheckCircle,\n    inactive: ListFilter,\n  },\n  p95: {\n    active: ArrowDown,\n    inactive: ListFilter,\n  },\n} as const;\n\nexport function Client() {\n  const trpc = useTRPC();\n  const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());\n  const { data: tags } = useQuery(trpc.monitorTag.list.queryOptions());\n  const [searchParams, setSearchParams] = useQueryStates(searchParamsParsers);\n  const [sorting, setSorting] = useState<SortingState>([]);\n  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);\n\n  const monitorsByType = {\n    http:\n      monitors\n        ?.filter((m) => m.jobType === \"http\")\n        .map((m) => m.id.toString()) ?? [],\n    tcp:\n      monitors\n        ?.filter((m) => m.jobType === \"tcp\")\n        .map((m) => m.id.toString()) ?? [],\n  };\n  const { http: httpMonitors, tcp: tcpMonitors } = monitorsByType;\n\n  // HMM: why do we need two queries?\n  const { data: globalHttpMetrics, isLoading: isLoadingHttp } = useQuery({\n    ...trpc.tinybird.globalMetrics.queryOptions({\n      monitorIds: httpMonitors,\n      type: \"http\",\n    }),\n    enabled: httpMonitors.length > 0,\n  });\n\n  const { data: globalTcpMetrics, isLoading: isLoadingTcp } = useQuery({\n    ...trpc.tinybird.globalMetrics.queryOptions({\n      monitorIds: tcpMonitors,\n      type: \"tcp\",\n    }),\n    enabled: tcpMonitors.length > 0,\n  });\n\n  // TODO: ideally we read from the searchParamsCache and there is no layout shift\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  useEffect(() => {\n    if (searchParams.status) {\n      setColumnFilters([{ id: \"status\", value: [searchParams.status] }]);\n    }\n    if (searchParams.sort) {\n      setSorting([searchParams.sort]);\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  if (!monitors) return null;\n\n  const metrics = getMonitorListMetrics(monitors, [\n    ...(globalHttpMetrics?.data ?? []),\n    ...(globalTcpMetrics?.data ?? []),\n  ]);\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Monitors</SectionTitle>\n          <SectionDescription>\n            Create and manage your monitors.\n          </SectionDescription>\n        </SectionHeader>\n        <MetricCardGroup>\n          {metrics.map((metric) => {\n            const statusArray = columnFilters.find((f) => f.id === \"status\")\n              ?.value as string[] | undefined;\n\n            let isActive = false;\n            if (metric.key === \"p95\") {\n              isActive = !!sorting.find((s) => s.id === \"p95\" && s.desc);\n            } else {\n              isActive =\n                Array.isArray(statusArray) && statusArray.includes(metric.key);\n            }\n\n            const iconGroup = metric.key === \"p95\" ? icons.p95 : icons.default;\n            const Icon = iconGroup[isActive ? \"active\" : \"inactive\"];\n\n            return (\n              <MetricCardButton\n                key={metric.title}\n                variant={metric.variant}\n                onClick={() => {\n                  if (metric.key === \"p95\") {\n                    if (sorting.length === 0 || !isActive) {\n                      setSearchParams({ sort: { id: \"p95\", desc: true } });\n                      setSorting([{ id: \"p95\", desc: true }]);\n                    } else {\n                      setSearchParams({ sort: null });\n                      setSorting([]);\n                    }\n                  } else {\n                    if (columnFilters.length === 0 || !isActive) {\n                      setSearchParams({ status: metric.key });\n                      setColumnFilters([{ id: \"status\", value: [metric.key] }]);\n                    } else {\n                      setSearchParams({ status: null });\n                      setColumnFilters([]);\n                    }\n                  }\n                }}\n              >\n                <MetricCardHeader className=\"flex w-full items-center justify-between gap-2\">\n                  <MetricCardTitle className=\"truncate\">\n                    {metric.title}\n                  </MetricCardTitle>\n                  <Icon className=\"size-4\" />\n                </MetricCardHeader>\n                {metric.key === \"p95\" && (isLoadingHttp || isLoadingTcp) ? (\n                  <MetricCardSkeleton className=\"h-6 w-12\" />\n                ) : (\n                  <MetricCardValue>{metric.value}</MetricCardValue>\n                )}\n              </MetricCardButton>\n            );\n          })}\n        </MetricCardGroup>\n      </Section>\n      <Section>\n        <DataTable\n          columns={columns}\n          data={monitors.map((monitor) => ({\n            ...monitor,\n            globalMetrics:\n              isLoadingHttp || isLoadingTcp\n                ? undefined\n                : monitor.jobType === \"http\"\n                  ? globalHttpMetrics?.data?.find(\n                      (m) => m.monitorId === monitor.id.toString(),\n                    ) ?? false\n                  : globalTcpMetrics?.data?.find(\n                      (m) => m.monitorId === monitor.id.toString(),\n                    ) ?? false,\n          }))}\n          actionBar={MonitorDataTableActionBar}\n          toolbarComponent={(props) => (\n            <MonitorDataTableToolbar {...props} tags={tags ?? []} />\n          )}\n          paginationComponent={DataTablePaginationSimple}\n          columnFilters={columnFilters}\n          setColumnFilters={setColumnFilters}\n          sorting={sorting}\n          setSorting={setSorting}\n          defaultColumnVisibility={{\n            active: false,\n            url: false,\n            jobType: false,\n          }}\n        />\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/(list)/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <Breadcrumb />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/(list)/nav-actions.tsx",
    "content": "\"use client\";\n\nimport { UpgradeDialog } from \"@/components/dialogs/upgrade\";\nimport { NavFeedback } from \"@/components/nav/nav-feedback\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Link from \"next/link\";\nimport { useState } from \"react\";\n\nexport function NavActions() {\n  const trpc = useTRPC();\n  const [openDialog, setOpenDialog] = useState(false);\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());\n\n  if (!workspace || !monitors) return null;\n\n  const limitReached = monitors.length >= workspace.limits.monitors;\n\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n      {limitReached ? (\n        <Button\n          size=\"sm\"\n          data-limited={limitReached}\n          className=\"data-[limited=true]:opacity-80\"\n          onClick={() => setOpenDialog(true)}\n        >\n          Create Monitor\n        </Button>\n      ) : (\n        <Button size=\"sm\" asChild>\n          <Link href=\"/monitors/create\">Create Monitor</Link>\n        </Button>\n      )}\n      <UpgradeDialog open={openDialog} onOpenChange={setOpenDialog} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/(list)/page.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport type { SearchParams } from \"nuqs\";\nimport { Client } from \"./client\";\nimport { searchParamsCache } from \"./search-params\";\n\nexport default async function Page({\n  searchParams,\n}: {\n  searchParams: Promise<SearchParams>;\n}) {\n  const queryClient = getQueryClient();\n\n  await searchParamsCache.parse(searchParams);\n  await queryClient.prefetchQuery(trpc.monitor.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.monitorTag.list.queryOptions());\n\n  return (\n    <HydrateClient>\n      <Client />\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/(list)/search-params.ts",
    "content": "import {\n  createParser,\n  createSearchParamsCache,\n  parseAsStringEnum,\n} from \"nuqs/server\";\n\nexport const parseAsSort = createParser({\n  parse(queryValue) {\n    const [id, desc] = queryValue.split(\".\");\n    if (!id && !desc) return null;\n    return { id, desc: desc === \"desc\" };\n  },\n  serialize(value) {\n    return `${value.id}.${value.desc ? \"desc\" : \"asc\"}`;\n  },\n});\n\nexport const searchParamsParsers = {\n  status: parseAsStringEnum([\"active\", \"degraded\", \"error\", \"inactive\"]),\n  sort: parseAsSort,\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Activity } from \"lucide-react\";\nimport { useParams, usePathname } from \"next/navigation\";\nimport { MONITOR_TABS } from \"./constants\";\n\nexport function Breadcrumb() {\n  const { id } = useParams<{ id: string }>();\n  const pathname = usePathname();\n  const trpc = useTRPC();\n  const { data: monitor } = useQuery(\n    trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  if (!monitor) return null;\n\n  const segment = pathname.split(\"/\").pop() ?? \"\";\n  const currentTab = MONITOR_TABS.find((tab) => tab.value === segment);\n\n  return (\n    <NavBreadcrumb\n      items={[\n        { type: \"link\", label: \"Monitors\", href: \"/monitors\", icon: Activity },\n        {\n          type: \"link\",\n          label: monitor.name,\n          href: `/monitors/${id}/overview`,\n        },\n        ...(currentTab\n          ? [\n              {\n                type: \"page\" as const,\n                label: currentTab.label,\n                icon: currentTab.icon,\n              },\n            ]\n          : []),\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/constants.ts",
    "content": "import type { LucideIcon } from \"lucide-react\";\nimport { Cog, LayoutGrid, Logs, Siren } from \"lucide-react\";\n\nexport const MONITOR_TABS: {\n  value: string;\n  label: string;\n  icon: LucideIcon;\n}[] = [\n  { value: \"overview\", label: \"Overview\", icon: LayoutGrid },\n  { value: \"logs\", label: \"Logs\", icon: Logs },\n  { value: \"incidents\", label: \"Incidents\", icon: Siren },\n  { value: \"edit\", label: \"Settings\", icon: Cog },\n];\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/edit/layout.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(trpc.monitorTag.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.privateLocation.list.queryOptions());\n\n  return <HydrateClient>{children}</HydrateClient>;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/edit/page.tsx",
    "content": "\"use client\";\n\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormMonitorUpdate } from \"@/components/forms/monitor/update\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function Page() {\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const { data: monitor } = useQuery(\n    trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  if (!monitor) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>{monitor.name}</SectionTitle>\n          <SectionDescription>Customize your monitor.</SectionDescription>\n        </SectionHeader>\n        <FormMonitorUpdate />\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/incidents/layout.tsx",
    "content": "import { getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { SidebarProvider } from \"@openstatus/ui/components/ui/sidebar\";\nimport { Sidebar } from \"../sidebar\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string }>;\n}) {\n  const queryClient = getQueryClient();\n  const { id } = await params;\n  await queryClient.prefetchQuery(\n    trpc.incident.list.queryOptions({ monitorId: Number.parseInt(id) }),\n  );\n\n  return (\n    <SidebarProvider defaultOpen={false}>\n      <div className=\"w-full flex-1\">{children}</div>\n      <div className=\"hidden lg:block\">\n        <Sidebar />\n      </div>\n    </SidebarProvider>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/incidents/page.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateDescription,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { columns } from \"@/components/data-table/incidents/columns\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { DataTablePaginationSimple } from \"@/components/ui/data-table/data-table-pagination\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function Page() {\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const { data: incidents } = useQuery(\n    trpc.incident.list.queryOptions({\n      monitorId: Number.parseInt(id),\n    }),\n  );\n  const { data: monitor } = useQuery(\n    trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  if (!incidents || !monitor) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>{monitor.name}</SectionTitle>\n          <SectionDescription>\n            {monitor.jobType === \"http\" ? (\n              <a href={monitor.url} target=\"_blank\" rel=\"noopener noreferrer\">\n                {monitor.url}\n              </a>\n            ) : (\n              monitor.url\n            )}\n          </SectionDescription>\n        </SectionHeader>\n        {incidents.length === 0 ? (\n          <EmptyStateContainer>\n            <EmptyStateTitle>No incidents</EmptyStateTitle>\n            <EmptyStateDescription>\n              No incidents found for this monitor.\n            </EmptyStateDescription>\n          </EmptyStateContainer>\n        ) : (\n          <DataTable\n            columns={columns}\n            data={incidents}\n            paginationComponent={DataTablePaginationSimple}\n          />\n        )}\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\nimport { Tabs } from \"./tabs\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string }>;\n}) {\n  const { id } = await params;\n  const queryClient = getQueryClient();\n\n  await queryClient.prefetchQuery(\n    trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  await queryClient.prefetchQuery(trpc.notification.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.privateLocation.list.queryOptions());\n\n  return (\n    <HydrateClient>\n      <div>\n        <AppHeader>\n          <AppHeaderContent>\n            <AppSidebarTrigger />\n            <Breadcrumb />\n          </AppHeaderContent>\n          <AppHeaderActions>\n            <NavActions />\n          </AppHeaderActions>\n        </AppHeader>\n        <Tabs />\n        <main className=\"flex-1\">{children}</main>\n      </div>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/client.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  BillingOverlay,\n  BillingOverlayButton,\n  BillingOverlayContainer,\n  BillingOverlayDescription,\n} from \"@/components/content/billing-overlay\";\nimport {\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\n\nimport { Section } from \"@/components/content/section\";\nimport { ButtonReset } from \"@/components/controls-search/button-reset\";\nimport { CommandRegion } from \"@/components/controls-search/command-region\";\nimport { DropdownStatus } from \"@/components/controls-search/dropdown-status\";\nimport { DropdownTrigger } from \"@/components/controls-search/dropdown-trigger\";\nimport { PopoverDate } from \"@/components/controls-search/popover-date\";\nimport { getColumns } from \"@/components/data-table/response-logs/columns\";\nimport { Sheet } from \"@/components/data-table/response-logs/data-table-sheet\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { DataTablePagination } from \"@/components/ui/data-table/data-table-pagination\";\nimport { DataTableSkeleton } from \"@/components/ui/data-table/data-table-skeleton\";\nimport { exampleLogs } from \"@/data/response-logs\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { PaginationState } from \"@tanstack/react-table\";\nimport { Lock } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\nimport { useQueryStates } from \"nuqs\";\nimport { useCallback, useMemo } from \"react\";\nimport { searchParamsParsers } from \"./search-params\";\n\nexport function Client() {\n  const trpc = useTRPC();\n  const { id } = useParams<{ id: string }>();\n  const [\n    { regions, status, selected, trigger, from, to, pageIndex, pageSize },\n    setSearchParams,\n  ] = useQueryStates(searchParamsParsers);\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const { data: monitor } = useQuery(\n    trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  const enabled = workspace && workspace?.plan !== \"free\";\n  const { data: _logs, isLoading } = useQuery({\n    ...trpc.tinybird.list.queryOptions({ monitorId: id, from, to }),\n    enabled,\n  });\n  const { data: _log } = useQuery({\n    ...trpc.tinybird.get.queryOptions({ id: selected, monitorId: id }),\n    enabled: !!selected && enabled,\n  });\n\n  const pagination = useMemo(\n    () => ({ pageIndex, pageSize }),\n    [pageIndex, pageSize],\n  );\n\n  const setPagination = useCallback(\n    (p: PaginationState | ((old: PaginationState) => PaginationState)) => {\n      const next = typeof p === \"function\" ? p({ pageIndex, pageSize }) : p;\n\n      if (next.pageIndex !== pageIndex || next.pageSize !== pageSize) {\n        setSearchParams({\n          pageIndex: next.pageIndex,\n          pageSize: next.pageSize,\n        });\n      }\n    },\n    [pageIndex, pageSize, setSearchParams],\n  );\n\n  const columns = useMemo(\n    () => getColumns(monitor?.privateLocations ?? []),\n    [monitor?.privateLocations],\n  );\n\n  if (!workspace || !monitor) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>{monitor.name}</SectionTitle>\n          <SectionDescription>\n            {monitor.jobType === \"http\" ? (\n              <a href={monitor.url} target=\"_blank\" rel=\"noopener noreferrer\">\n                {monitor.url}\n              </a>\n            ) : (\n              monitor.url\n            )}\n          </SectionDescription>\n        </SectionHeader>\n        <div className=\"flex flex-wrap items-center gap-2\">\n          <PopoverDate />\n          {monitor.jobType === \"http\" ? <DropdownStatus /> : null}\n          <DropdownTrigger />\n          <CommandRegion\n            regions={monitor.regions}\n            privateLocations={monitor?.privateLocations}\n          />\n          <ButtonReset />\n        </div>\n      </Section>\n      <Section>\n        {isLoading ? (\n          <DataTableSkeleton rows={10} />\n        ) : !enabled ? (\n          <BillingPlaceholder />\n        ) : (\n          <DataTable\n            data={_logs?.data ?? []}\n            columns={columns}\n            onRowClick={(row) => {\n              if (!row.original.id) return;\n              setSearchParams({ selected: row.original.id });\n            }}\n            columnFilters={[\n              { id: \"trigger\", value: trigger },\n              { id: \"requestStatus\", value: status },\n              { id: \"region\", value: regions },\n            ].filter((i) => Boolean(i.value))}\n            pagination={pagination}\n            setPagination={setPagination}\n            paginationComponent={DataTablePagination}\n            defaultColumnVisibility={\n              monitor.jobType === \"tcp\" || monitor.jobType === \"dns\"\n                ? { timing: false, statusCode: false }\n                : {}\n            }\n            // NOTE: required to control the pagination\n            autoResetPageIndex={false}\n          />\n        )}\n        <Sheet\n          data={_log?.data?.length ? _log.data[0] : null}\n          privateLocations={monitor?.privateLocations ?? []}\n          onClose={() =>\n            setTimeout(() => setSearchParams({ selected: null }), 300)\n          }\n        />\n      </Section>\n    </SectionGroup>\n  );\n}\n\nfunction BillingPlaceholder() {\n  const columns = useMemo(() => getColumns([]), []);\n  return (\n    <BillingOverlayContainer>\n      <DataTable data={exampleLogs} columns={columns} />\n      <BillingOverlay>\n        <BillingOverlayButton asChild>\n          <Link href=\"/settings/billing\">\n            <Lock />\n            Upgrade\n          </Link>\n        </BillingOverlayButton>\n        <BillingOverlayDescription>\n          Access response headers, timing phases and more for each request.{\" \"}\n          <Link\n            href=\"https://docs.openstatus.dev/monitoring/monitor-data-collected/\"\n            rel=\"noreferrer\"\n            target=\"_blank\"\n          >\n            Learn more\n          </Link>\n          .\n        </BillingOverlayDescription>\n      </BillingOverlay>\n    </BillingOverlayContainer>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/page.tsx",
    "content": "import type { SearchParams } from \"nuqs/server\";\nimport { Client } from \"./client\";\nimport { searchParamsCache } from \"./search-params\";\n\nexport default async function Page({\n  searchParams,\n}: {\n  searchParams: Promise<SearchParams>;\n}) {\n  // NOTE: store in cache to avoid flicker on clients first render\n  await searchParamsCache.parse(searchParams);\n\n  return <Client />;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/search-params.ts",
    "content": "import { PERIODS, STATUS, TRIGGER } from \"@/data/metrics.client\";\nimport { endOfDay } from \"date-fns\";\nimport { startOfDay } from \"date-fns\";\nimport {\n  createSearchParamsCache,\n  parseAsArrayOf,\n  parseAsInteger,\n  parseAsIsoDateTime,\n  parseAsString,\n  parseAsStringLiteral,\n} from \"nuqs/server\";\n\nexport const searchParamsParsers = {\n  period: parseAsStringLiteral(PERIODS).withDefault(\"1d\"),\n  regions: parseAsArrayOf(parseAsString),\n  status: parseAsStringLiteral(STATUS),\n  trigger: parseAsStringLiteral(TRIGGER),\n  selected: parseAsString,\n  from: parseAsIsoDateTime.withDefault(startOfDay(new Date())),\n  to: parseAsIsoDateTime.withDefault(endOfDay(new Date())),\n  pageIndex: parseAsInteger.withDefault(0),\n  pageSize: parseAsInteger.withDefault(20),\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/nav-actions.tsx",
    "content": "\"use client\";\n\nimport { DataTableSheetTest } from \"@/components/data-table/response-logs/data-table-sheet-test\";\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { NavFeedback } from \"@/components/nav/nav-feedback\";\nimport { getActions } from \"@/data/monitors.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { deserialize } from \"@openstatus/assertions\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { Zap } from \"lucide-react\";\nimport { useParams, usePathname, useRouter } from \"next/navigation\";\nimport { useState } from \"react\";\nimport { toast } from \"sonner\";\n\ntype TestTCP = RouterOutputs[\"checker\"][\"testTcp\"];\ntype TestHTTP = RouterOutputs[\"checker\"][\"testHttp\"];\ntype TestDNS = RouterOutputs[\"checker\"][\"testDns\"];\n\nexport function NavActions() {\n  const { id } = useParams<{ id: string }>();\n  const [test, setTest] = useState<TestTCP | TestHTTP | TestDNS | null>(null);\n  const queryClient = useQueryClient();\n  const trpc = useTRPC();\n  const router = useRouter();\n  const pathname = usePathname();\n\n  const { data: monitor } = useQuery(\n    trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  const deleteMonitorMutation = useMutation(\n    trpc.monitor.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.monitor.list.queryKey(),\n        });\n        if (pathname.includes(`/monitors/${id}`)) {\n          router.push(\"/monitors\");\n        }\n      },\n    }),\n  );\n\n  const cloneMonitorMutation = useMutation(\n    trpc.monitor.clone.mutationOptions({\n      onSuccess: (newMonitor) => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.monitor.list.queryKey(),\n        });\n        router.push(`/monitors/${newMonitor.id}`);\n      },\n    }),\n  );\n\n  const testHttpMutation = useMutation(trpc.checker.testHttp.mutationOptions());\n  const testTcpMutation = useMutation(trpc.checker.testTcp.mutationOptions());\n  const testDnsMutation = useMutation(trpc.checker.testDns.mutationOptions());\n\n  const actions = getActions({\n    edit: () => router.push(`/monitors/${id}/edit`),\n    \"copy-id\": async () => {\n      await navigator.clipboard.writeText(id);\n      toast.success(\"Monitor ID copied to clipboard\");\n    },\n    clone: () => {\n      const promise = cloneMonitorMutation.mutateAsync({\n        id: Number.parseInt(id),\n      });\n      toast.promise(promise, {\n        loading: \"Cloning monitor...\",\n        success: \"Monitor cloned\",\n        error: (error) => {\n          if (isTRPCClientError(error)) {\n            return error.message;\n          }\n          return \"Failed to clone monitor\";\n        },\n      });\n    },\n  });\n\n  async function testAction() {\n    if (monitor?.jobType === \"http\") {\n      const assertions = deserialize(monitor.assertions ?? \"[]\");\n      const promise = testHttpMutation.mutateAsync({\n        url: monitor.url,\n        body: monitor.body,\n        method: monitor.method,\n        headers: monitor.headers,\n        assertions: assertions.map((a) => a.schema),\n      });\n\n      toast.promise(promise, {\n        loading: \"Testing HTTP request...\",\n        success: (data) => {\n          setTest(data);\n          return \"HTTP test completed successfully\";\n        },\n        error: (error) => {\n          if (isTRPCClientError(error)) {\n            return error.message;\n          }\n          return \"HTTP test failed\";\n        },\n      });\n    } else if (monitor?.jobType === \"tcp\") {\n      const promise = testTcpMutation.mutateAsync({ url: monitor.url });\n\n      toast.promise(promise, {\n        loading: \"Testing TCP connection...\",\n        success: (data) => {\n          setTest(data);\n          return \"TCP test completed successfully\";\n        },\n        error: (error) => {\n          if (isTRPCClientError(error)) {\n            return error.message;\n          }\n          return \"TCP test failed\";\n        },\n      });\n    } else if (monitor?.jobType === \"dns\") {\n      const assertions = deserialize(monitor.assertions ?? \"[]\");\n      const promise = testDnsMutation.mutateAsync({\n        url: monitor.url,\n        assertions: assertions.map((a) => a.schema),\n      });\n\n      toast.promise(promise, {\n        loading: \"Testing DNS request...\",\n        success: (data) => {\n          setTest(data);\n          return \"DNS test completed successfully\";\n        },\n        error: (error) => {\n          if (isTRPCClientError(error)) {\n            return error.message;\n          }\n          return \"DNS test failed\";\n        },\n      });\n    }\n  }\n\n  if (!monitor) return null;\n\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n      <div className=\"hidden font-medium text-muted-foreground lg:inline-block\">\n        {!monitor.active ? (\n          <span className=\"relative ml-1.5 inline-flex\">\n            <span className=\"relative inline-flex h-2.5 w-2.5 rounded-full bg-muted-foreground/70\" />\n          </span>\n        ) : monitor.status === \"active\" ? (\n          <span className=\"relative ml-1.5 inline-flex\">\n            <span className=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-success/80 opacity-75\" />\n            <span className=\"relative inline-flex h-2.5 w-2.5 rounded-full bg-success\" />\n          </span>\n        ) : monitor.status === \"error\" ? (\n          <span className=\"relative ml-1.5 inline-flex\">\n            <span className=\"relative inline-flex h-2.5 w-2.5 rounded-full bg-destructive\" />\n          </span>\n        ) : (\n          <span className=\"relative ml-1.5 inline-flex\">\n            <span className=\"relative inline-flex h-2.5 w-2.5 rounded-full bg-warning\" />\n          </span>\n        )}\n      </div>\n      <TooltipProvider>\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"group h-7 w-7\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              <Zap className=\"text-muted-foreground group-hover:text-foreground\" />\n            </Button>\n          </TooltipTrigger>\n          <TooltipContent>Test Monitor</TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n      <QuickActions\n        actions={actions}\n        deleteAction={{\n          confirmationValue: monitor.name ?? \"monitor\",\n          submitAction: async () => {\n            await deleteMonitorMutation.mutateAsync({\n              id: Number.parseInt(id),\n            });\n          },\n        }}\n      />\n      <DataTableSheetTest\n        data={test}\n        monitor={monitor}\n        onClose={async () => {\n          await new Promise((resolve) => setTimeout(() => resolve(true), 300));\n          setTest(null);\n        }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx",
    "content": "\"use client\";\n\nimport { ChartAreaLatency } from \"@/components/chart/chart-area-latency\";\nimport { ChartAreaTimingPhases } from \"@/components/chart/chart-area-timing-phases\";\nimport { ChartBarUptime } from \"@/components/chart/chart-bar-uptime\";\nimport { ChartLineRegions } from \"@/components/chart/chart-line-regions\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { ButtonReset } from \"@/components/controls-search/button-reset\";\nimport { CommandRegion } from \"@/components/controls-search/command-region\";\nimport { DropdownInterval } from \"@/components/controls-search/dropdown-interval\";\nimport { DropdownPercentile } from \"@/components/controls-search/dropdown-percentile\";\nimport { DropdownPeriod } from \"@/components/controls-search/dropdown-period\";\nimport { AuditLogsWrapper } from \"@/components/data-table/audit-logs/wrapper\";\nimport { getColumns as getRegionColumns } from \"@/components/data-table/response-logs/regions/columns\";\nimport { GlobalUptimeSection } from \"@/components/metric/global-uptime/section\";\nimport { PopoverQuantile } from \"@/components/popovers/popover-quantile\";\nimport { PopoverResolution } from \"@/components/popovers/popover-resolution\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { DataTablePagination } from \"@/components/ui/data-table/data-table-pagination\";\nimport { mapRegionMetrics } from \"@/data/metrics.client\";\nimport { periodToFromDate } from \"@/data/metrics.client\";\nimport type { RegionMetric } from \"@/data/region-metrics\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { monitorRegions } from \"@openstatus/db/src/schema/constants\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { endOfDay } from \"date-fns\";\nimport { useParams } from \"next/navigation\";\nimport { useQueryStates } from \"nuqs\";\nimport React, { useMemo } from \"react\";\nimport { searchParamsParsers } from \"./search-params\";\n\nconst TIMELINE_INTERVAL = 30; // in days\n\nexport function Client() {\n  const trpc = useTRPC();\n  const { id } = useParams<{ id: string }>();\n  const [{ period, regions, percentile, interval }] =\n    useQueryStates(searchParamsParsers);\n  const { data: monitor } = useQuery(\n    trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  const selectedRegions = regions ?? undefined;\n  const fromDate = periodToFromDate[period];\n  const toDate = endOfDay(new Date());\n\n  const regionTimelineQuery = {\n    ...trpc.tinybird.metricsRegions.queryOptions({\n      monitorId: id,\n      period: period,\n      type: (monitor?.jobType ?? \"http\") as \"http\" | \"tcp\",\n      regions: selectedRegions,\n      // Request 30-minute buckets by default\n      interval: 30,\n      fromDate: fromDate.toISOString(),\n      toDate: toDate.toISOString(),\n    }),\n    enabled: !!monitor,\n  } as const;\n\n  const { data: regionTimeline, isLoading } = useQuery(regionTimelineQuery);\n\n  const regionMetrics: RegionMetric[] = React.useMemo(() => {\n    return mapRegionMetrics(\n      regionTimeline,\n      // NOTE: while loading, we show the selected regions with empty data,\n      // once the data is loaded, we show all the regions that we get from TB\n      isLoading\n        ? monitor?.regions ?? []\n        : [\n            ...monitorRegions,\n            ...(monitor?.privateLocations?.map((location) =>\n              location.id.toString(),\n            ) ?? []),\n          ],\n      percentile,\n    );\n  }, [regionTimeline, monitor, percentile, isLoading]);\n\n  const regionColumns = useMemo(\n    () => getRegionColumns(monitor?.privateLocations ?? []),\n    [monitor?.privateLocations],\n  );\n\n  if (!monitor) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>{monitor.name}</SectionTitle>\n          <SectionDescription>\n            {monitor.jobType === \"http\" ? (\n              <a href={monitor.url} target=\"_blank\" rel=\"noopener noreferrer\">\n                {monitor.url}\n              </a>\n            ) : (\n              monitor.url\n            )}\n          </SectionDescription>\n        </SectionHeader>\n        <div className=\"flex flex-wrap gap-2\">\n          <div>\n            <DropdownPeriod /> including{\" \"}\n            <CommandRegion\n              regions={monitor.regions}\n              privateLocations={monitor.privateLocations}\n            />\n          </div>\n          <div>\n            <ButtonReset only={[\"period\", \"regions\"]} />\n          </div>\n        </div>\n        <GlobalUptimeSection\n          monitorId={id}\n          jobType={monitor.jobType as \"http\" | \"tcp\"}\n          period={period}\n          regions={selectedRegions}\n        />\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Uptime</SectionTitle>\n          <SectionDescription>\n            Uptime across all the selected regions\n          </SectionDescription>\n        </SectionHeader>\n        <ChartBarUptime\n          monitorId={id}\n          type={monitor.jobType as \"http\" | \"tcp\"}\n          period={period}\n          regions={selectedRegions}\n        />\n      </Section>\n      <Section>\n        {/* TODO: based on http, we have Timing Phases instead of Latency */}\n        <SectionHeader>\n          <SectionTitle>Latency</SectionTitle>\n          <SectionDescription>\n            Response time across all the regions\n          </SectionDescription>\n        </SectionHeader>\n        <div className=\"flex flex-wrap gap-2\">\n          <div>\n            The <DropdownPercentile />{\" \"}\n            <PopoverQuantile>quantile</PopoverQuantile> within a{\" \"}\n            <DropdownInterval />{\" \"}\n            <PopoverResolution>resolution</PopoverResolution>\n          </div>\n          <div>\n            <ButtonReset only={[\"percentile\", \"interval\"]} />\n          </div>\n        </div>\n        {monitor.jobType === \"http\" ? (\n          <ChartAreaTimingPhases\n            monitorId={id}\n            degradedAfter={monitor.degradedAfter}\n            type={monitor.jobType as \"http\"}\n            period={period}\n            percentile={percentile}\n            interval={interval}\n            regions={selectedRegions}\n          />\n        ) : (\n          <ChartAreaLatency\n            monitorId={id}\n            percentile={percentile}\n            degradedAfter={monitor.degradedAfter}\n            type={monitor.jobType as \"http\" | \"tcp\"}\n            period={period}\n            regions={selectedRegions}\n          />\n        )}\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Regions</SectionTitle>\n          <SectionDescription>\n            Every selected region&apos;s latency trend\n          </SectionDescription>\n        </SectionHeader>\n        <div className=\"flex flex-wrap gap-2\">\n          <div>\n            The <DropdownPercentile />{\" \"}\n            <PopoverQuantile>quantile</PopoverQuantile> trend over the{\" \"}\n            <DropdownPeriod />\n          </div>\n          <div>\n            <ButtonReset only={[\"percentile\", \"period\"]} />\n          </div>\n        </div>\n        <Tabs defaultValue=\"table\">\n          <TabsList>\n            <TabsTrigger value=\"table\">Table</TabsTrigger>\n            <TabsTrigger value=\"chart\">Chart</TabsTrigger>\n          </TabsList>\n          <TabsContent value=\"table\">\n            <DataTable\n              data={regionMetrics}\n              columns={regionColumns}\n              paginationComponent={({ table }) => (\n                <DataTablePagination table={table} />\n              )}\n            />\n          </TabsContent>\n          <TabsContent value=\"chart\">\n            <ChartLineRegions\n              className=\"mt-3\"\n              regions={regionMetrics.map((region) => region.region)}\n              privateLocations={monitor?.privateLocations ?? []}\n              data={regionMetrics.reduce(\n                (acc, region) => {\n                  region.trend.forEach((t) => {\n                    const existing = acc.find(\n                      (d) => d.timestamp === t.timestamp,\n                    );\n                    if (existing) {\n                      existing[region.region] = t[region.region];\n                    } else {\n                      acc.push({\n                        timestamp: t.timestamp,\n                        [region.region]: t[region.region],\n                      });\n                    }\n                  });\n                  return acc;\n                },\n                [] as { timestamp: number; [key: string]: number }[],\n              )}\n            />\n          </TabsContent>\n        </Tabs>\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Timeline</SectionTitle>\n          <SectionDescription>\n            What happened to your monitor over the last {TIMELINE_INTERVAL} days\n          </SectionDescription>\n        </SectionHeader>\n        <AuditLogsWrapper monitorId={id} interval={TIMELINE_INTERVAL} />\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/layout.tsx",
    "content": "import { SidebarProvider } from \"@openstatus/ui/components/ui/sidebar\";\nimport { Sidebar } from \"../sidebar\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <SidebarProvider defaultOpen={false}>\n      {/* blur-2xl */}\n      <div className=\"w-full flex-1\">{children}</div>\n      <div className=\"hidden lg:block\">\n        <Sidebar />\n      </div>\n    </SidebarProvider>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/page.tsx",
    "content": "import type { SearchParams } from \"nuqs/server\";\nimport { Client } from \"./client\";\nimport { searchParamsCache } from \"./search-params\";\n\nexport default async function Page({\n  searchParams,\n}: {\n  searchParams: Promise<SearchParams>;\n}) {\n  // NOTE: store in cache to avoid flicker on clients first render\n  await searchParamsCache.parse(searchParams);\n\n  return <Client />;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/search-params.ts",
    "content": "import { INTERVALS } from \"@/data/metrics.client\";\nimport {\n  createSearchParamsCache,\n  parseAsArrayOf,\n  parseAsNumberLiteral,\n  parseAsString,\n  parseAsStringLiteral,\n} from \"nuqs/server\";\n\nconst PERIOD = [\"1d\", \"7d\", \"14d\"] as const;\nconst PERCENTILE = [\"p50\", \"p75\", \"p90\", \"p95\", \"p99\"] as const;\n\nexport const searchParamsParsers = {\n  period: parseAsStringLiteral(PERIOD).withDefault(\"1d\"),\n  regions: parseAsArrayOf(parseAsString),\n  percentile: parseAsStringLiteral(PERCENTILE).withDefault(\"p50\"),\n  interval: parseAsNumberLiteral(INTERVALS).withDefault(30),\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default async function Page({\n  params,\n}: {\n  params: Promise<{ id: string }>;\n}) {\n  const { id } = await params;\n  redirect(`/monitors/${id}/overview`);\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/sidebar.tsx",
    "content": "\"use client\";\n\nimport { TableCellLink } from \"@/components/data-table/table-cell-link\";\nimport { SidebarRight } from \"@/components/nav/sidebar-right\";\nimport { monitorTypes } from \"@/data/monitors.client\";\nimport { formatMilliseconds } from \"@/lib/formatter\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { deserialize } from \"@openstatus/assertions\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Logs } from \"lucide-react\";\nimport { useParams, useRouter } from \"next/navigation\";\n\nexport function Sidebar() {\n  const router = useRouter();\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const { data: monitor } = useQuery(\n    trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  if (!monitor) return null;\n\n  const assertions = monitor.assertions ? deserialize(monitor.assertions) : [];\n  const type = monitorTypes.find((type) => type.id === monitor.jobType);\n\n  return (\n    <SidebarRight\n      header=\"Monitor\"\n      metadata={[\n        {\n          label: \"Overview\",\n          items: [\n            {\n              label: \"External Name\",\n              value: monitor.externalName || monitor.name,\n            },\n            {\n              label: \"Status\",\n              // FIXME: dynamic\n              value: <span className=\"text-success\">Normal</span>,\n            },\n            {\n              label: \"Type\",\n              value: type ? (\n                <span className=\"flex items-center gap-1\">\n                  <span className=\"uppercase\">{type.label}</span>\n                  <type.icon className=\"h-2.5 w-2.5 text-muted-foreground\" />\n                </span>\n              ) : (\n                <span className=\"uppercase\">{monitor.jobType}</span>\n              ),\n            },\n            {\n              label: \"Endpoint\",\n              value: monitor.url.replace(/^https?:\\/\\//, \"\"),\n            },\n            {\n              label: \"Regions\",\n              value:\n                monitor.regions.length > 6\n                  ? `${monitor.regions.length} regions`\n                  : monitor.regions.join(\", \"),\n            },\n            {\n              label: \"Tags\",\n              value: (\n                <div className=\"group/badges -space-x-2 flex flex-wrap\">\n                  {monitor.tags.map((tag) => (\n                    <Badge\n                      key={tag.id}\n                      variant=\"outline\"\n                      className=\"relative flex translate-x-0 items-center gap-1.5 rounded-full bg-background transition-transform hover:z-10 hover:translate-x-1\"\n                    >\n                      <div\n                        className=\"size-2.5 rounded-full\"\n                        style={{ backgroundColor: tag.color }}\n                      />\n                      {tag.name}\n                    </Badge>\n                  ))}\n                </div>\n              ),\n            },\n          ],\n        },\n        {\n          label: \"Configuration\",\n          items: [\n            { label: \"Periodicity\", value: monitor.periodicity },\n            {\n              label: \"Timeout\",\n              value: formatMilliseconds(monitor.timeout),\n            },\n            { label: \"Public\", value: String(monitor.public) },\n            { label: \"Active\", value: String(monitor.active) },\n            {\n              label: \"Follow redirects\",\n              value: String(monitor.followRedirects),\n            },\n          ],\n        },\n        {\n          label: \"Notifications\",\n          items: monitor.notifications.flatMap((notification) => {\n            const arr = [];\n            arr.push({\n              label: \"Name\",\n              value: (\n                <TableCellLink\n                  // TODO: add the ?id= to the href and open the sheet\n                  href={\"/notifications\"}\n                  value={notification.name}\n                />\n              ),\n            });\n            arr.push({\n              label: \"Type\",\n              value: notification.provider,\n              isNested: true,\n            });\n            arr.push({\n              label: \"Value\",\n              value: notification.data, // TODO: improve this based on the provider - we might wanna parse it!\n              isNested: true,\n            });\n            return arr;\n          }),\n        },\n        {\n          label: \"Assertions\",\n          items:\n            assertions.length > 0\n              ? assertions.flatMap((assertion) => {\n                  const arr = [];\n\n                  arr.push({\n                    label: \"Type\",\n                    value: assertion.schema.type,\n                  });\n\n                  arr.push({\n                    label: \"Compare\",\n                    value: assertion.schema.compare,\n                    isNested: true,\n                  });\n\n                  if (\n                    (assertion.schema.type === \"header\" ||\n                      assertion.schema.type === \"dnsRecord\") &&\n                    assertion.schema.key\n                  ) {\n                    arr.push({\n                      label: \"Key\",\n                      value: assertion.schema.key,\n                      isNested: true,\n                    });\n                  }\n\n                  arr.push({\n                    label: \"Value\",\n                    value: assertion.schema.target,\n                    isNested: true,\n                  });\n\n                  return arr;\n                })\n              : [],\n        },\n        // {\n        //   label: \"Last Logs\",\n        //   items: [\n        //     ...Array.from({ length: 20 }).map((_, index) => {\n        //       const date = new Date(new Date().getTime() - index * 500000);\n        //       return {\n        //         label: [\n        //           \"Amsterdam\",\n        //           \"Frankfurt\",\n        //           \"New York\",\n        //           \"Singapore\",\n        //           \"Johannesburg\",\n        //         ][index % 5],\n        //         value: (\n        //           <div className=\"flex items-center justify-between gap-2\">\n        //             <CircleCheck className=\"h-4 w-4 text-success\" />\n        //             <TooltipProvider>\n        //               <Tooltip>\n        //                 <TooltipTrigger>\n        //                   <span className=\"underline decoration-muted-foreground/50 decoration-dashed underline-offset-2\">\n        //                     {date.toLocaleTimeString(\"en-US\", {\n        //                       hour: \"2-digit\",\n        //                       minute: \"2-digit\",\n        //                     })}\n        //                   </span>\n        //                 </TooltipTrigger>\n        //                 <TooltipContent align=\"center\" side=\"left\">\n        //                   {date.toLocaleString(\"en-US\")}\n        //                 </TooltipContent>\n        //               </Tooltip>\n        //             </TooltipProvider>\n        //           </div>\n        //         ),\n        //       };\n        //     }),\n        //   ],\n        // },\n      ]}\n      footerButton={{\n        onClick: () => router.push(`/monitors/${id}/logs`),\n        children: (\n          <>\n            <Logs />\n            <span>View all logs</span>\n          </>\n        ),\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/[id]/tabs.tsx",
    "content": "\"use client\";\n\nimport { NavTabs } from \"@/components/nav/nav-tabs\";\nimport { useParams } from \"next/navigation\";\nimport { MONITOR_TABS } from \"./constants\";\n\nexport function Tabs() {\n  const { id } = useParams<{ id: string }>();\n\n  return (\n    <NavTabs\n      items={MONITOR_TABS.map((tab) => ({\n        ...tab,\n        href: `/monitors/${id}/${tab.value}`,\n      }))}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/create/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Activity } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[\n        {\n          type: \"link\",\n          label: \"Monitors\",\n          href: \"/monitors\",\n          icon: Activity,\n        },\n        { type: \"page\", label: \"Create Monitor\" },\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/create/layout.tsx",
    "content": "import { AppHeader, AppHeaderContent } from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\nimport { Breadcrumb } from \"./breadcrumb\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <Breadcrumb />\n        </AppHeaderContent>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/monitors/create/page.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport { EmptyStateDescription } from \"@/components/content/empty-state\";\nimport {\n  Section,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormGeneral } from \"@/components/forms/monitor/form-general\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { useRouter } from \"next/navigation\";\n\nexport default function Page() {\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const router = useRouter();\n\n  const triggerCheckMutation = useMutation(\n    trpc.checker.triggerChecker.mutationOptions({}),\n  );\n\n  const createMonitorMutation = useMutation(\n    trpc.monitor.new.mutationOptions({\n      onSuccess: (data) => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.monitor.list.queryKey(),\n        });\n        if (data.active) {\n          triggerCheckMutation.mutate({ id: data.id });\n        }\n        router.push(`/monitors/${data.id}/edit`);\n      },\n    }),\n  );\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Create Monitor</SectionTitle>\n        </SectionHeader>\n        <FormGeneral\n          onSubmit={async (data) => {\n            await createMonitorMutation.mutateAsync({\n              name: data.name,\n              jobType: data.type,\n              url: data.url,\n              method: data.method,\n              headers: data.headers,\n              body: data.body,\n              active: data.active,\n              assertions: data.assertions,\n              saveCheck: data.saveCheck,\n              skipCheck: data.skipCheck,\n            });\n          }}\n        />\n      </Section>\n      <Section>\n        <EmptyStateContainer>\n          <EmptyStateTitle>Create and start customizing</EmptyStateTitle>\n          <EmptyStateDescription>\n            Change the <span className=\"text-foreground\">periodicity</span>, set\n            up the <span className=\"text-foreground\">regions</span>,{\" \"}\n            <span className=\"text-foreground\">timeout</span> or{\" \"}\n            <span className=\"text-foreground\">degraded</span> duration and\n            more...\n          </EmptyStateDescription>\n        </EmptyStateContainer>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/notifications/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Bell } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[{ type: \"page\", label: \"Notifications\", icon: Bell }]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/notifications/client.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  ActionCard,\n  ActionCardDescription,\n  ActionCardHeader,\n  ActionCardTitle,\n} from \"@/components/content/action-card\";\nimport { ActionCardGroup } from \"@/components/content/action-card\";\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { columns } from \"@/components/data-table/notifications/columns\";\nimport { FormSheetNotifier } from \"@/components/forms/notifications/sheet\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { config } from \"@/data/notifications.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { useQueryStates } from \"nuqs\";\nimport { searchParamsParsers } from \"./search-params\";\n\n// FIXME: WARNING we are using the `web` api url here\nconst BASE_URL =\n  process.env.NODE_ENV === \"development\"\n    ? \"http://localhost:3000\"\n    : \"https://www.openstatus.dev\";\n\nexport function Client() {\n  const trpc = useTRPC();\n  const { data: notifications, refetch } = useQuery(\n    trpc.notification.list.queryOptions(),\n  );\n  const [searchParams] = useQueryStates(searchParamsParsers);\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());\n  const createNotifierMutation = useMutation(\n    trpc.notification.new.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  if (!notifications || !monitors || !workspace) return null;\n\n  const limitReached =\n    notifications.length >= workspace.limits[\"notification-channels\"];\n\n  return (\n    <SectionGroup>\n      <SectionHeader>\n        <SectionTitle>Notifications</SectionTitle>\n        <SectionDescription>\n          Define your notifications to receive alerts when downtime occurs.\n        </SectionDescription>\n      </SectionHeader>\n      <Section>\n        {notifications.length === 0 ? (\n          <EmptyStateContainer>\n            <EmptyStateTitle>No notifier found</EmptyStateTitle>\n          </EmptyStateContainer>\n        ) : (\n          <DataTable columns={columns} data={notifications} />\n        )}\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Create a new notifier</SectionTitle>\n          <SectionDescription>\n            Define your notifications to receive alerts when downtime occurs.{\" \"}\n            <Link\n              href=\"https://docs.openstatus.dev/reference/notification/\"\n              rel=\"noreferrer\"\n              target=\"_blank\"\n            >\n              Learn more\n            </Link>\n            .\n          </SectionDescription>\n        </SectionHeader>\n        <ActionCardGroup className=\"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\">\n          {Object.keys(config).map((notifier) => {\n            const key = notifier as keyof typeof config;\n            const Icon = config[key].icon;\n            let enabled = true;\n\n            if (key in workspace.limits) {\n              enabled =\n                workspace.limits[\n                  key as \"opsgenie\" | \"sms\" | \"opsgenie\" | \"whatsapp\"\n                ];\n            }\n\n            if (limitReached) {\n              enabled = false;\n            }\n\n            if (!searchParams.channel && key === \"pagerduty\") {\n              const PAGERDUTY_URL = `https://app.pagerduty.com/install/integration?app_id=${process.env.NEXT_PUBLIC_PAGERDUTY_APP_ID}&redirect_url=${BASE_URL}/api/callback/pagerduty?workspace=${workspace.slug}&version=2`;\n              return (\n                <a\n                  key={key}\n                  href={PAGERDUTY_URL}\n                  data-disabled={!enabled}\n                  className=\"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50\"\n                >\n                  <ActionCard className=\"h-full w-full\">\n                    <ActionCardHeader>\n                      <div className=\"flex items-center gap-2\">\n                        <div className=\"flex size-6 items-center justify-center rounded-md border border-border bg-muted\">\n                          <Icon className=\"size-3\" />\n                        </div>\n                        <ActionCardTitle>{config[key].label}</ActionCardTitle>\n                      </div>\n                      <ActionCardDescription>\n                        Send notifications to {config[key].label}\n                      </ActionCardDescription>\n                    </ActionCardHeader>\n                  </ActionCard>\n                </a>\n              );\n            }\n\n            return (\n              <FormSheetNotifier\n                key={notifier}\n                provider={key}\n                monitors={monitors}\n                defaultOpen={searchParams.channel === key}\n                onSubmit={async (values) => {\n                  await createNotifierMutation.mutateAsync({\n                    provider: key,\n                    name: values.name,\n                    data: { [key]: values.data },\n                    monitors: values.monitors,\n                  });\n                }}\n                disabled={!enabled}\n              >\n                <ActionCard className=\"h-full w-full\">\n                  <ActionCardHeader>\n                    <div className=\"flex items-center gap-2\">\n                      <div className=\"flex size-6 items-center justify-center rounded-md border border-border bg-muted\">\n                        <Icon className=\"size-3\" />\n                      </div>\n                      <ActionCardTitle>{config[key].label}</ActionCardTitle>\n                    </div>\n                    <ActionCardDescription>\n                      Send notifications to {config[key].label}\n                    </ActionCardDescription>\n                  </ActionCardHeader>\n                </ActionCard>\n              </FormSheetNotifier>\n            );\n          })}\n          <ActionCard className=\"border-dashed\">\n            <ActionCardHeader>\n              <div className=\"flex items-center gap-2\">\n                <div className=\"flex size-6 items-center justify-center rounded-md border border-border bg-muted\" />\n                <ActionCardTitle className=\"text-muted-foreground\">\n                  Your Notifier\n                </ActionCardTitle>\n              </div>\n              <ActionCardDescription>\n                Missing a channel?{\" \"}\n                <Link href=\"mailto:ping@openstatus.dev\">Contact us</Link>\n              </ActionCardDescription>\n            </ActionCardHeader>\n          </ActionCard>\n        </ActionCardGroup>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/notifications/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(trpc.notification.list.queryOptions());\n  return (\n    <HydrateClient>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <Breadcrumb />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/notifications/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/notifications/page.tsx",
    "content": "import type { SearchParams } from \"nuqs\";\nimport { Client } from \"./client\";\nimport { searchParamsCache } from \"./search-params\";\n\nexport default async function Page({\n  searchParams,\n}: {\n  searchParams: Promise<SearchParams>;\n}) {\n  await searchParamsCache.parse(searchParams);\n  return <Client />;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/notifications/search-params.ts",
    "content": "import {\n  createSearchParamsCache,\n  parseAsString,\n  parseAsStringEnum,\n} from \"nuqs/server\";\n\nexport const searchParamsParsers = {\n  config: parseAsString,\n  channel: parseAsStringEnum([\"pagerduty\"]),\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/onboarding/client.tsx",
    "content": "\"use client\";\n\nimport {\n  ActionCard,\n  ActionCardDescription,\n  ActionCardGroup,\n  ActionCardHeader,\n  ActionCardTitle,\n} from \"@/components/content/action-card\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { CreateMonitorForm } from \"@/components/forms/onboarding/create-monitor\";\nimport { CreatePageForm } from \"@/components/forms/onboarding/create-page\";\nimport { LearnFromForm } from \"@/components/forms/onboarding/learn-from\";\nimport { extractDomain } from \"@/lib/domains\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { ArrowUpRight } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { useQueryStates } from \"nuqs\";\nimport { useEffect } from \"react\";\nimport { searchParamsParsers } from \"./search-params\";\n\nconst moreActions = [\n  {\n    id: \"notifier\",\n    title: \"Create a notifier\",\n    description: \"Get notified when your website or API is down.\",\n    href: \"/notifications\",\n  },\n  {\n    id: \"workspace\",\n    title: \"Setup workspace\",\n    description: \"Add a name to your workspace and share it with your team.\",\n    href: \"/settings/general\",\n  },\n  {\n    id: \"monitor\",\n    title: \"Update monitor\",\n    description: \"Change region, schedule, timeout and more.\",\n    href: \"/monitors\",\n  },\n  {\n    id: \"cal\",\n    title: \"Schedule a call\",\n    description: \"Book a meeting with us to get you started with OpenStatus.\",\n    href: \"https://openstatus.dev/cal\",\n  },\n  {\n    id: \"docs\",\n    title: \"Documentation\",\n    description: \"Read our documentation to get started with OpenStatus.\",\n    href: \"https://docs.openstatus.dev\",\n  },\n  {\n    id: \"changelog\",\n    title: \"Changelog\",\n    description: \"See what's new in OpenStatus.\",\n    href: \"https://openstatus.dev/changelog\",\n  },\n  {\n    id: \"discord\",\n    title: \"Discord\",\n    description: \"Join our Discord server if you get stuck.\",\n    href: \"https://discord.gg/openstatus\",\n  },\n  {\n    id: \"github\",\n    title: \"GitHub\",\n    description: \"Leave a star on GitHub, request features or report issues.\",\n    href: \"https://github.com/openstatus-dev/openstatus\",\n  },\n];\n\nexport function Client() {\n  const [{ step, callbackUrl }, setSearchParams] =\n    useQueryStates(searchParamsParsers);\n  const router = useRouter();\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const { data: workspace, refetch } = useQuery(\n    trpc.workspace.get.queryOptions(),\n  );\n  const triggerCheckMutation = useMutation(\n    trpc.checker.triggerChecker.mutationOptions({}),\n  );\n  const createMonitorMutation = useMutation(\n    trpc.monitor.new.mutationOptions({\n      onSuccess: async (data) => {\n        await setSearchParams({ step: \"2\" });\n        if (data.active) {\n          triggerCheckMutation.mutate({ id: data.id });\n        }\n        refetch();\n        queryClient.invalidateQueries({\n          queryKey: trpc.monitor.list.queryKey(),\n        });\n      },\n    }),\n  );\n  const createPageMutation = useMutation(\n    trpc.page.create.mutationOptions({\n      onSuccess: async () => {\n        await setSearchParams({ step: \"next\" });\n        refetch();\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n      },\n    }),\n  );\n  const createFeedbackMutation = useMutation(\n    trpc.feedback.submit.mutationOptions({}),\n  );\n\n  useEffect(() => {\n    if (!callbackUrl) return;\n    // Validate and normalize the callbackUrl to prevent XSS via javascript: or other dangerous schemes\n    try {\n      const url = new URL(callbackUrl, window.location.origin);\n      if (url.pathname === \"/\" || url.pathname === \"\") return;\n      if (url.origin !== window.location.origin) return;\n      if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return;\n      // Navigate using the parsed URL to avoid raw-input parsing discrepancies\n      router.push(`${url.pathname}${url.search}${url.hash}`);\n    } catch {\n      // Malformed URLs are not safe to navigate to\n    }\n  }, [callbackUrl, router]);\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Getting Started</SectionTitle>\n          <SectionDescription>\n            Welcome to OpenStatus. Let&apos;s get you set up.\n          </SectionDescription>\n        </SectionHeader>\n      </Section>\n      {step === \"1\" && (\n        <Section>\n          <SectionHeader className=\"h-8 flex-row items-center justify-between\">\n            <SectionDescription className=\"tabular-nums\">\n              Step <span className=\"font-medium text-foreground\">1</span> of{\" \"}\n              <span className=\"font-medium text-foreground\">2</span>\n            </SectionDescription>\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              className=\"text-muted-foreground\"\n              onClick={() => setSearchParams({ step: \"2\" })}\n            >\n              Skip\n            </Button>\n          </SectionHeader>\n          <FormCard>\n            <FormCardHeader>\n              <FormCardTitle>Create a monitor</FormCardTitle>\n              <FormCardDescription>\n                Get uptime, response time and more for your website or API.\n              </FormCardDescription>\n            </FormCardHeader>\n            <FormCardContent>\n              <CreateMonitorForm\n                id=\"create-monitor-form\"\n                onSubmit={async (values) => {\n                  await createMonitorMutation.mutateAsync({\n                    url: values.url,\n                    name: new URL(values.url).hostname,\n                    method: \"GET\",\n                    headers: [],\n                    assertions: [],\n                    jobType: \"http\",\n                    active: true,\n                  });\n                }}\n              />\n            </FormCardContent>\n            <FormCardFooter>\n              <Button form=\"create-monitor-form\">Submit</Button>\n            </FormCardFooter>\n          </FormCard>\n        </Section>\n      )}\n      {step === \"2\" && (\n        <Section>\n          <SectionHeader className=\"h-8 flex-row items-center justify-between\">\n            <SectionDescription className=\"tabular-nums\">\n              Step <span className=\"font-medium text-foreground\">2</span> of{\" \"}\n              <span className=\"font-medium text-foreground\">2</span>\n            </SectionDescription>\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              className=\"text-muted-foreground\"\n              onClick={() => setSearchParams({ step: \"next\" })}\n            >\n              Skip\n            </Button>\n          </SectionHeader>\n          <FormCard>\n            <FormCardHeader>\n              <FormCardTitle>Create a page</FormCardTitle>\n              <FormCardDescription>\n                Inform your users about the status of your website or API.\n              </FormCardDescription>\n            </FormCardHeader>\n            <FormCardContent>\n              <CreatePageForm\n                id=\"create-page-form\"\n                defaultValues={{\n                  slug: extractDomain(createMonitorMutation.data?.url ?? \"\"),\n                }}\n                onSubmit={async (values) => {\n                  if (!workspace?.id) return;\n\n                  await createPageMutation.mutateAsync({\n                    slug: values.slug,\n                    title: values.slug.replace(/-/g, \" \"),\n                    description: \"\",\n                    monitors: createMonitorMutation.data?.id\n                      ? [{ monitorId: createMonitorMutation.data.id, order: 0 }]\n                      : [],\n                    workspaceId: workspace.id,\n                    legacyPage: false,\n                  });\n                }}\n              />\n            </FormCardContent>\n            <FormCardFooter>\n              <Button form=\"create-page-form\">Submit</Button>\n            </FormCardFooter>\n          </FormCard>\n        </Section>\n      )}\n      {step === \"next\" && (\n        <>\n          <Section>\n            <SectionHeader className=\"h-8 flex-row items-center justify-between\">\n              <SectionDescription>\n                We&apos;d love to know what you are looking for with openstatus.\n                This will help us improve our product and services.\n              </SectionDescription>\n            </SectionHeader>\n            <LearnFromForm\n              onSubmit={async (values) => {\n                await createFeedbackMutation.mutateAsync({\n                  message: `I want to use OpenStatus for *${values.from}${\n                    values.other ? `: ${values.other || \"others\"}` : \"\"\n                  }*`,\n                });\n              }}\n            />\n          </Section>\n          <Section>\n            <SectionHeader>\n              <SectionDescription className=\"tabular-nums\">\n                What&apos;s next?\n              </SectionDescription>\n            </SectionHeader>\n            <ActionCardGroup className=\"sm:grid-cols-2\">\n              {moreActions.map((action) => {\n                const isExternal = action.href.startsWith(\"http\");\n                const isMonitor = action.id === \"monitor\";\n                const href =\n                  isMonitor && createMonitorMutation.data?.id\n                    ? `${action.href}/${createMonitorMutation.data.id}`\n                    : action.href;\n                return (\n                  <Link\n                    key={action.id}\n                    href={href}\n                    target={isExternal ? \"_blank\" : undefined}\n                    rel={isExternal ? \"noopener noreferrer\" : undefined}\n                  >\n                    <ActionCard className=\"h-full w-full\">\n                      <ActionCardHeader>\n                        <ActionCardTitle className=\"flex items-center justify-between gap-2\">\n                          {action.title}\n                          {isExternal && (\n                            <ArrowUpRight className=\"size-4 shrink-0 text-muted-foreground group-hover/action-card:text-foreground\" />\n                          )}\n                        </ActionCardTitle>\n                        <ActionCardDescription>\n                          {action.description}\n                        </ActionCardDescription>\n                      </ActionCardHeader>\n                    </ActionCard>\n                  </Link>\n                );\n              })}\n            </ActionCardGroup>\n          </Section>\n        </>\n      )}\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/onboarding/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <NavBreadcrumb items={[{ type: \"page\", label: \"Onboarding\" }]} />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/onboarding/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/onboarding/page.tsx",
    "content": "import { Client } from \"./client\";\nimport { searchParamsCache } from \"./search-params\";\n\nimport type { SearchParams } from \"nuqs\";\n\nexport default async function Page({\n  searchParams,\n}: {\n  searchParams: Promise<SearchParams>;\n}) {\n  await searchParamsCache.parse(searchParams);\n\n  return <Client />;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/onboarding/search-params.ts",
    "content": "import {\n  createSearchParamsCache,\n  parseAsString,\n  parseAsStringLiteral,\n} from \"nuqs/server\";\n\nconst STEPS = [\"1\", \"2\", \"next\"] as const;\n\nexport const searchParamsParsers = {\n  step: parseAsStringLiteral(STEPS).withDefault(\"1\"),\n  callbackUrl: parseAsString,\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/overview/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { LayoutGrid } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[{ type: \"page\", label: \"Overview\", icon: LayoutGrid }]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/overview/data-table-status-reports.tsx",
    "content": "\"use client\";\n\nimport { DataTable as UpdatesDataTable } from \"@/components/data-table/status-report-updates/data-table\";\nimport { columns as statusReportsColumns } from \"@/components/data-table/status-reports/columns\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport type { RouterOutputs } from \"@openstatus/api\";\n\ntype StatusReport = RouterOutputs[\"statusReport\"][\"list\"][number];\n\nexport function DataTableStatusReports({\n  statusReports,\n}: {\n  statusReports: StatusReport[];\n}) {\n  return (\n    <DataTable\n      columns={statusReportsColumns}\n      data={statusReports}\n      onRowClick={(row) =>\n        row.getCanExpand() ? row.toggleExpanded() : undefined\n      }\n      rowComponent={({ row }) => (\n        <UpdatesDataTable\n          updates={row.original.updates}\n          reportId={row.original.id}\n        />\n      )}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/overview/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default async function Layout({\n  children,\n}: { children: React.ReactNode }) {\n  const queryClient = getQueryClient();\n\n  await queryClient.prefetchQuery(trpc.monitor.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.page.list.queryOptions());\n  await queryClient.prefetchQuery(\n    trpc.incident.list.queryOptions({\n      period: \"7d\",\n    }),\n  );\n  await queryClient.prefetchQuery(\n    trpc.statusReport.list.queryOptions({\n      period: \"7d\",\n    }),\n  );\n  await queryClient.prefetchQuery(\n    trpc.maintenance.list.queryOptions({\n      period: \"7d\",\n    }),\n  );\n\n  return (\n    <HydrateClient>\n      <div>\n        <AppHeader>\n          <AppHeaderContent>\n            <AppSidebarTrigger />\n            <Breadcrumb />\n          </AppHeaderContent>\n          <AppHeaderActions>\n            <NavActions />\n          </AppHeaderActions>\n        </AppHeader>\n        <main className=\"w-full flex-1\">{children}</main>\n      </div>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/overview/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/overview/page.tsx",
    "content": "\"use client\";\n\nimport {\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\n\nimport { Note, NoteButton } from \"@/components/common/note\";\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport { Section } from \"@/components/content/section\";\nimport { columns as incidentsColumns } from \"@/components/data-table/incidents/columns\";\nimport { columns as maintenancesColumns } from \"@/components/data-table/maintenances/columns\";\nimport {\n  MetricCard,\n  MetricCardGroup,\n  MetricCardHeader,\n  MetricCardTitle,\n  MetricCardValue,\n} from \"@/components/metric/metric-card\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { cn } from \"@/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { formatDistanceToNowStrict } from \"date-fns\";\nimport { Bot, List, Search } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { DataTableStatusReports } from \"./data-table-status-reports\";\n\n// FIXME: the page is server side\n// whenever I change the maintenances, the page is not updated\n// we need to move the queryClient to the layout and prefetch the data there\n\nexport default function Page() {\n  const trpc = useTRPC();\n\n  const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());\n  const { data: pages } = useQuery(trpc.page.list.queryOptions());\n  const { data: incidents } = useQuery(\n    trpc.incident.list.queryOptions({\n      period: \"7d\",\n    }),\n  );\n  const { data: statusReports } = useQuery(\n    trpc.statusReport.list.queryOptions({\n      period: \"7d\",\n    }),\n  );\n  const { data: maintenances } = useQuery(\n    trpc.maintenance.list.queryOptions({\n      period: \"7d\",\n    }),\n  );\n\n  if (!monitors || !pages || !incidents || !statusReports || !maintenances)\n    return null;\n\n  const lastIncident = incidents.length > 0 ? incidents[0] : null;\n  const lastStatusReport = statusReports.length > 0 ? statusReports[0] : null;\n  const lastMaintenance = maintenances.length > 0 ? maintenances[0] : null;\n\n  const incidentDistance = lastIncident\n    ? formatDistanceToNowStrict(lastIncident.startedAt, {\n        addSuffix: true,\n      })\n    : \"None\";\n\n  const statusReportDistance = lastStatusReport?.createdAt\n    ? formatDistanceToNowStrict(lastStatusReport.createdAt, {\n        addSuffix: true,\n      })\n    : \"None\";\n\n  const maintenanceDistance = lastMaintenance?.createdAt\n    ? formatDistanceToNowStrict(lastMaintenance.createdAt, {\n        addSuffix: true,\n      })\n    : \"None\";\n\n  const metrics = [\n    {\n      title: \"Monitors\",\n      value: monitors.length,\n      href: \"/monitors\",\n      variant: \"default\" as const,\n      icon: List,\n    },\n    {\n      title: \"Status Pages\",\n      value: pages.length,\n      href: \"/status-pages\",\n      variant: \"default\" as const,\n      icon: List,\n    },\n    {\n      title:\n        lastIncident?.resolvedAt === undefined && lastIncident\n          ? \"Active Incident\"\n          : \"Recent Incident\",\n      value: incidentDistance,\n      disabled: !lastIncident?.monitorId,\n      href: `/monitors/${lastIncident?.monitorId}/incidents`,\n      variant:\n        lastIncident?.resolvedAt === undefined && lastIncident\n          ? (\"warning\" as const)\n          : (\"default\" as const),\n      icon: Search,\n    },\n    {\n      title: \"Last Report\",\n      value: statusReportDistance,\n      disabled: !lastStatusReport?.pageId,\n      href: `/status-pages/${lastStatusReport?.pageId}/status-reports`,\n      variant: \"default\" as const,\n      icon: Search,\n    },\n    {\n      title: \"Last Maintenance\",\n      value: maintenanceDistance,\n      disabled: !lastMaintenance?.pageId,\n      href: `/status-pages/${lastMaintenance?.pageId}/maintenances`,\n      variant: \"default\" as const,\n      icon: Search,\n    },\n  ];\n\n  return (\n    <SectionGroup>\n      <Note>\n        <Bot />\n        Use our Slack agent to manage your status pages and incidents.\n        <NoteButton variant=\"default\" asChild>\n          <Link href=\"/agents\">Learn more</Link>\n        </NoteButton>\n      </Note>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Overview</SectionTitle>\n          <SectionDescription>\n            Welcome to your OpenStatus dashboard.\n          </SectionDescription>\n        </SectionHeader>\n        <MetricCardGroup>\n          {metrics.map((metric) => (\n            <Link\n              href={metric.href}\n              key={metric.title}\n              className={cn(metric.disabled && \"pointer-events-none\")}\n              aria-disabled={metric.disabled}\n            >\n              <MetricCard variant={metric.variant}>\n                <MetricCardHeader className=\"flex items-center justify-between gap-2\">\n                  <MetricCardTitle className=\"truncate\">\n                    {metric.title}\n                  </MetricCardTitle>\n                  <metric.icon className=\"size-4\" />\n                </MetricCardHeader>\n                <MetricCardValue>{metric.value}</MetricCardValue>\n              </MetricCard>\n            </Link>\n          ))}\n        </MetricCardGroup>\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Incidents</SectionTitle>\n          <SectionDescription>\n            Incidents over the last 7 days.\n          </SectionDescription>\n        </SectionHeader>\n        {incidents.length > 0 ? (\n          <DataTable columns={incidentsColumns} data={incidents} />\n        ) : (\n          <EmptyStateContainer>\n            <EmptyStateTitle>No incidents found</EmptyStateTitle>\n          </EmptyStateContainer>\n        )}\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Reports</SectionTitle>\n          <SectionDescription>Reports over the last 7 days.</SectionDescription>\n        </SectionHeader>\n        {statusReports.length > 0 ? (\n          <DataTableStatusReports statusReports={statusReports} />\n        ) : (\n          <EmptyStateContainer>\n            <EmptyStateTitle>No reports found</EmptyStateTitle>\n          </EmptyStateContainer>\n        )}\n      </Section>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Maintenance</SectionTitle>\n          <SectionDescription>\n            Maintenance over the last 7 days.\n          </SectionDescription>\n        </SectionHeader>\n        {maintenances.length > 0 ? (\n          <DataTable columns={maintenancesColumns} data={maintenances} />\n        ) : (\n          <EmptyStateContainer>\n            <EmptyStateTitle>No maintenances found</EmptyStateTitle>\n          </EmptyStateContainer>\n        )}\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function Page() {\n  redirect(\"/overview\");\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/private-locations/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Globe } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[{ type: \"page\", label: \"Private Locations\", icon: Globe }]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/private-locations/client.tsx",
    "content": "\"use client\";\nimport { Link } from \"@/components/common/link\";\nimport {\n  BillingOverlay,\n  BillingOverlayButton,\n  BillingOverlayContainer,\n  BillingOverlayDescription,\n} from \"@/components/content/billing-overlay\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { columns } from \"@/components/data-table/private-locations/columns\";\nimport { UpgradeDialog } from \"@/components/dialogs/upgrade\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Lock } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst EXAMPLES = [\n  {\n    id: 1,\n    name: \"Private Location 1\",\n    token: \"my-secret-token\",\n    createdAt: new Date(\"2025-01-01\"),\n    updatedAt: new Date(\"2025-01-01\"),\n    workspaceId: 1,\n    lastSeenAt: new Date(\"2025-10-08\"),\n    privateLocationToMonitors: [],\n    monitors: [],\n  },\n  {\n    id: 2,\n    name: \"Private Location 2\",\n    token: \"my-secret-token\",\n    createdAt: new Date(\"2025-01-01\"),\n    updatedAt: new Date(\"2025-01-01\"),\n    workspaceId: 1,\n    lastSeenAt: new Date(\"2025-06-08\"),\n    privateLocationToMonitors: [],\n    monitors: [],\n  },\n] satisfies RouterOutputs[\"privateLocation\"][\"list\"];\n\nexport function Client() {\n  const trpc = useTRPC();\n  const [openDialog, setOpenDialog] = useState(false);\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const { data: privateLocations } = useQuery(\n    trpc.privateLocation.list.queryOptions(),\n  );\n\n  if (!privateLocations || !workspace) return null;\n\n  return (\n    <SectionGroup>\n      <SectionHeader>\n        <SectionTitle>Private Locations</SectionTitle>\n        <SectionDescription>\n          Create and manage your private locations.\n        </SectionDescription>\n      </SectionHeader>\n      <Section>\n        {workspace.limits[\"private-locations\"] === false ? (\n          <BillingOverlayContainer>\n            <DataTable\n              columns={columns}\n              data={[...EXAMPLES, ...EXAMPLES, ...EXAMPLES]}\n            />\n            <BillingOverlay>\n              <BillingOverlayButton onClick={() => setOpenDialog(true)}>\n                <Lock />\n                Upgrade\n              </BillingOverlayButton>\n              <BillingOverlayDescription>\n                Create private locations to monitor your internal services.{\" \"}\n                <Link\n                  href=\"https://docs.openstatus.dev/tutorial/how-to-create-private-location/\"\n                  rel=\"noreferrer\"\n                  target=\"_blank\"\n                >\n                  Learn more\n                </Link>\n                .\n              </BillingOverlayDescription>\n            </BillingOverlay>\n            <UpgradeDialog\n              open={openDialog}\n              onOpenChange={setOpenDialog}\n              limit=\"private-locations\"\n            />\n          </BillingOverlayContainer>\n        ) : (\n          <DataTable columns={columns} data={privateLocations} />\n        )}\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/private-locations/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(trpc.notification.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.privateLocation.list.queryOptions());\n  return (\n    <HydrateClient>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <Breadcrumb />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/private-locations/nav-actions.tsx",
    "content": "\"use client\";\n\nimport { UpgradeDialog } from \"@/components/dialogs/upgrade\";\nimport { FormSheetPrivateLocation } from \"@/components/forms/private-location/sheet\";\nimport { NavFeedback } from \"@/components/nav/nav-feedback\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\n\nexport function NavActions() {\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const [openDialog, setOpenDialog] = useState(false);\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());\n  const createPrivateLocationMutation = useMutation(\n    trpc.privateLocation.new.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.privateLocation.list.queryKey(),\n        });\n      },\n    }),\n  );\n\n  if (!workspace || !monitors) return null;\n\n  const limitReached = !workspace.limits[\"private-locations\"];\n\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n      {limitReached ? (\n        <Button\n          size=\"sm\"\n          data-disabled={limitReached}\n          className=\"data-[disabled=true]:opacity-50\"\n          onClick={() => setOpenDialog(true)}\n        >\n          Create Private Location\n        </Button>\n      ) : (\n        <FormSheetPrivateLocation\n          monitors={monitors}\n          onSubmit={async (values) => {\n            await createPrivateLocationMutation.mutateAsync({\n              name: values.name,\n              monitors: values.monitors,\n              token: values.token,\n            });\n          }}\n        >\n          <Button size=\"sm\">Create Private Location</Button>\n        </FormSheetPrivateLocation>\n      )}\n      <UpgradeDialog\n        open={openDialog}\n        onOpenChange={setOpenDialog}\n        limit=\"private-locations\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/private-locations/page.tsx",
    "content": "import { Client } from \"./client\";\n\nexport default async function Page() {\n  return <Client />;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/(list)/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <NavBreadcrumb items={[{ type: \"page\", label: \"Settings\" }]} />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/(list)/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/(list)/page.tsx",
    "content": "import { Link } from \"@/components/common/link\";\nimport {\n  ActionCard,\n  ActionCardDescription,\n  ActionCardGroup,\n  ActionCardHeader,\n  ActionCardTitle,\n} from \"@/components/content/action-card\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\n\nconst settings = [\n  {\n    title: \"General\",\n    description: \"Manage your workspace settings.\",\n    href: \"/settings/general\",\n  },\n  {\n    title: \"Billing\",\n    description: \"Manage your billing information and payment methods.\",\n    href: \"/settings/billing\",\n  },\n  {\n    title: \"Account\",\n    description: \"Manage your account information.\",\n    href: \"/settings/account\",\n  },\n  {\n    title: \"Integrations\",\n    description: \"Connect third-party services to your workspace.\",\n    href: \"/settings/integrations\",\n  },\n];\nexport default function Page() {\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Settings</SectionTitle>\n          <SectionDescription>\n            All your settings in one place.\n          </SectionDescription>\n        </SectionHeader>\n        <ActionCardGroup>\n          {settings.map((setting) => (\n            <Link href={setting.href} key={setting.href}>\n              <ActionCard className=\"h-full w-full\">\n                <ActionCardHeader>\n                  <ActionCardTitle>{setting.title}</ActionCardTitle>\n                  <ActionCardDescription>\n                    {setting.description}\n                  </ActionCardDescription>\n                </ActionCardHeader>\n              </ActionCard>\n            </Link>\n          ))}\n        </ActionCardGroup>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/account/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Cog, User } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[\n        {\n          type: \"link\",\n          label: \"Settings\",\n          icon: Cog,\n          href: \"/settings/general\",\n        },\n        { type: \"page\", label: \"Account\", icon: User },\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/account/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Tabs } from \"../tabs\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(trpc.member.list.queryOptions());\n\n  return (\n    <HydrateClient>\n      <div>\n        <AppHeader>\n          <AppHeaderContent>\n            <AppSidebarTrigger />\n            <Breadcrumb />\n          </AppHeaderContent>\n          <AppHeaderActions>\n            <NavActions />\n          </AppHeaderActions>\n        </AppHeader>\n        <Tabs />\n        <main className=\"w-full flex-1\">{children}</main>\n      </div>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/account/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/account/page.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  Section,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormAlertDialog } from \"@/components/forms/form-alert-dialog\";\nimport {\n  FormCardDescription,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n  FormCardUpgrade,\n} from \"@/components/forms/form-card\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardFooter,\n} from \"@/components/forms/form-card\";\nimport { ThemeToggle } from \"@/components/theme-toggle\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { signOut } from \"next-auth/react\";\n\nexport default function Page() {\n  const trpc = useTRPC();\n  const { data: user } = useQuery(trpc.user.get.queryOptions());\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const { data: members } = useQuery(trpc.member.list.queryOptions());\n\n  const deleteAccountMutation = useMutation(\n    trpc.user.deleteAccount.mutationOptions(),\n  );\n\n  if (!user || !workspace || !members) return null;\n\n  const isOwner = members.find((m) => m.user.id === user.id)?.role === \"owner\";\n  const hasPaidPlan = !!workspace.plan && workspace.plan !== \"free\";\n  const isDeleteDisabled = isOwner && hasPaidPlan;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Account</SectionTitle>\n        </SectionHeader>\n        <FormCard>\n          <FormCardUpgrade />\n          <FormCardHeader>\n            <FormCardTitle>Personal Information</FormCardTitle>\n            <FormCardDescription>\n              Manage your personal information.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent>\n            <form className=\"grid gap-4\">\n              <div className=\"grid gap-1.5\">\n                <Label>Name</Label>\n                <Input defaultValue={user?.name ?? undefined} />\n              </div>\n              <div className=\"grid gap-1.5\">\n                <Label>Email</Label>\n                <Input defaultValue={user?.email ?? undefined} />\n              </div>\n            </form>\n          </FormCardContent>\n          <FormCardFooter className=\"[&>:last-child]:ml-0\">\n            <FormCardFooterInfo>\n              Please contact us if you want to change your email or name.\n            </FormCardFooterInfo>\n          </FormCardFooter>\n        </FormCard>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Appearance</FormCardTitle>\n            <FormCardDescription>\n              Choose your preferred theme.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"pb-4\">\n            <ThemeToggle />\n          </FormCardContent>\n        </FormCard>\n        <FormCard variant=\"destructive\">\n          <FormCardHeader>\n            <FormCardTitle>Delete Account</FormCardTitle>\n            <FormCardDescription>\n              This will permanently delete your account and remove you from all\n              workspaces. This action cannot be undone.\n            </FormCardDescription>\n          </FormCardHeader>\n          {isDeleteDisabled ? (\n            <FormCardContent>\n              <p className=\"text-destructive text-sm\">\n                You must cancel your subscription before deleting your account.\n                Go to{\" \"}\n                <a\n                  href=\"/settings/billing\"\n                  className=\"font-medium underline underline-offset-4\"\n                >\n                  Billing\n                </a>{\" \"}\n                to manage your subscription.\n              </p>\n            </FormCardContent>\n          ) : null}\n          <FormCardFooter variant=\"destructive\">\n            <FormCardFooterInfo>\n              Need help? Contact us at{\" \"}\n              <Link href=\"mailto:ping@openstatus.dev\">ping@openstatus.dev</Link>\n              .\n            </FormCardFooterInfo>\n            <FormAlertDialog\n              confirmationValue={user.email || user.name || \"delete-account\"}\n              submitAction={async () => {\n                await deleteAccountMutation.mutateAsync();\n                await signOut({ redirectTo: \"/\" });\n              }}\n            >\n              <Button\n                variant=\"destructive\"\n                size=\"sm\"\n                disabled={isDeleteDisabled}\n              >\n                Delete\n              </Button>\n            </FormAlertDialog>\n          </FormCardFooter>\n        </FormCard>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/billing/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Cog, CreditCard } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[\n        {\n          type: \"link\",\n          label: \"Settings\",\n          icon: Cog,\n          href: \"/settings/general\",\n        },\n        { type: \"page\", label: \"Billing\", icon: CreditCard },\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/billing/client.tsx",
    "content": "\"use client\";\n\nimport { BillingAddons } from \"@/components/content/billing-addons\";\nimport { BillingProgress } from \"@/components/content/billing-progress\";\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { DataTable } from \"@/components/data-table/billing/data-table\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardGroup,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { useRouter } from \"next/navigation\";\nimport { useQueryStates } from \"nuqs\";\nimport { useEffect, useMemo, useTransition } from \"react\";\nimport { toast } from \"sonner\";\nimport { searchParamsParsers } from \"./search-params\";\n\nconst BASE_URL =\n  process.env.NODE_ENV === \"production\"\n    ? \"https://app.openstatus.dev\"\n    : \"http://localhost:3000\";\n\nfunction calculateTotalRequests(limits: Limits) {\n  const monitors = limits.monitors;\n  const maxRegions = limits[\"max-regions\"];\n  const periodicity = limits.periodicity;\n\n  if (periodicity.includes(\"30s\")) {\n    return monitors * maxRegions * 2 * 60 * 24 * 30;\n  }\n\n  if (periodicity.includes(\"1m\")) {\n    return monitors * maxRegions * 60 * 24 * 30;\n  }\n\n  if (periodicity.includes(\"5m\")) {\n    return monitors * maxRegions * 12 * 24 * 30;\n  }\n\n  if (periodicity.includes(\"10m\")) {\n    return monitors * maxRegions * 6 * 24 * 30;\n  }\n\n  if (periodicity.includes(\"30m\")) {\n    return monitors * maxRegions * 2 * 24 * 30;\n  }\n\n  if (periodicity.includes(\"1h\")) {\n    return monitors * maxRegions * 24 * 30;\n  }\n\n  return 0;\n}\n\nexport function Client() {\n  const trpc = useTRPC();\n  const router = useRouter();\n  const [isPending, startTransition] = useTransition();\n  const [{ success }, setSearchParams] = useQueryStates(searchParamsParsers);\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const customerPortalMutation = useMutation(\n    trpc.stripeRouter.getUserCustomerPortal.mutationOptions({\n      onSuccess: (url) => {\n        if (!url) return;\n        router.push(url);\n      },\n    }),\n  );\n  const { data: httpWorkspace30d } = useQuery({\n    ...trpc.tinybird.workspace30d.queryOptions({\n      type: \"http\",\n    }),\n    enabled: !!workspace,\n  });\n\n  const { data: tcpWorkspace30d } = useQuery({\n    ...trpc.tinybird.workspace30d.queryOptions({\n      type: \"tcp\",\n    }),\n    enabled: !!workspace,\n  });\n\n  useEffect(() => {\n    if (success) {\n      setTimeout(() => {\n        toast.success(\"Billing information updated\", {\n          duration: 5_000,\n          onAutoClose: () => setSearchParams({ success: null }),\n          onDismiss: () => setSearchParams({ success: null }),\n        });\n      }, 500);\n    }\n  }, [success, setSearchParams]);\n\n  const totalRequests = useMemo(() => {\n    const httpRequests = httpWorkspace30d?.data?.reduce(\n      (acc, curr) => acc + curr.count,\n      0,\n    );\n    const tcpRequests = tcpWorkspace30d?.data?.reduce(\n      (acc, curr) => acc + curr.count,\n      0,\n    );\n    return (httpRequests ?? 0) + (tcpRequests ?? 0);\n  }, [httpWorkspace30d, tcpWorkspace30d]);\n\n  if (!workspace) return null;\n\n  const planAddons = allPlans[workspace.plan].addons;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Billing</SectionTitle>\n          <SectionDescription>\n            Manage your billing information and payment methods.\n          </SectionDescription>\n        </SectionHeader>\n        <FormCardGroup>\n          <FormCard>\n            <FormCardHeader>\n              <FormCardTitle>Usage</FormCardTitle>\n              <FormCardDescription>\n                Overview of your current usage, limits and addons.\n              </FormCardDescription>\n            </FormCardHeader>\n            <FormCardContent>\n              <div className=\"flex flex-col gap-2\">\n                <BillingProgress\n                  label=\"Monitors\"\n                  value={workspace.usage?.monitors ?? 0}\n                  max={workspace.limits.monitors}\n                />\n                <BillingProgress\n                  label=\"Status Pages\"\n                  value={workspace.usage?.pages ?? 0}\n                  max={workspace.limits[\"status-pages\"]}\n                />\n                <BillingProgress\n                  label=\"Page Components\"\n                  value={workspace.usage?.pageComponents ?? 0}\n                  max={workspace.limits[\"page-components\"]}\n                />\n                <BillingProgress\n                  label=\"Notifications\"\n                  value={workspace.usage?.notifications ?? 0}\n                  max={workspace.limits[\"notification-channels\"]}\n                />\n                <BillingProgress\n                  label=\"Total requests in the last 30 days\"\n                  value={totalRequests}\n                  max={calculateTotalRequests(workspace.limits)}\n                />\n              </div>\n            </FormCardContent>\n            <FormCardSeparator />\n            <FormCardContent>\n              <FormCardHeader className=\"col-span-full px-0 pt-0 pb-0\">\n                <FormCardTitle>Add-ons</FormCardTitle>\n                <FormCardDescription>\n                  Extend your limits with additional features.\n                </FormCardDescription>\n              </FormCardHeader>\n              <div className=\"flex flex-col gap-2 pt-4\">\n                {planAddons[\"email-domain-protection\"] ? (\n                  <BillingAddons\n                    label={planAddons[\"email-domain-protection\"].title}\n                    description={\n                      planAddons[\"email-domain-protection\"].description\n                    }\n                    addon=\"email-domain-protection\"\n                    workspace={workspace}\n                  />\n                ) : null}\n                {planAddons[\"white-label\"] ? (\n                  <BillingAddons\n                    label={planAddons[\"white-label\"].title}\n                    description={planAddons[\"white-label\"].description}\n                    addon=\"white-label\"\n                    workspace={workspace}\n                  />\n                ) : null}\n                {planAddons[\"status-pages\"] ? (\n                  <BillingAddons\n                    label={planAddons[\"status-pages\"].title}\n                    description={planAddons[\"status-pages\"].description}\n                    addon=\"status-pages\"\n                    workspace={workspace}\n                  />\n                ) : null}\n                {Object.keys(planAddons).length === 0 ? (\n                  <EmptyStateContainer>\n                    <EmptyStateTitle>No add-ons available</EmptyStateTitle>\n                  </EmptyStateContainer>\n                ) : null}\n              </div>\n            </FormCardContent>\n            <FormCardFooter>\n              <FormCardFooterInfo>\n                Access your{\" \"}\n                <span className=\"font-medium\">billing information</span>,{\" \"}\n                <span className=\"font-medium\">invoices</span> and{\" \"}\n                <span className=\"font-medium\">payment methods</span> via Stripe.\n              </FormCardFooterInfo>\n              <Button\n                size=\"sm\"\n                onClick={() => {\n                  startTransition(async () => {\n                    await customerPortalMutation.mutateAsync({\n                      workspaceSlug: workspace.slug,\n                      returnUrl: `${BASE_URL}/settings/billing`,\n                    });\n                  });\n                }}\n                disabled={isPending}\n              >\n                {isPending ? \"Loading...\" : \"Customer Portal\"}\n              </Button>\n            </FormCardFooter>\n          </FormCard>\n          <FormCard>\n            <FormCardHeader>\n              <FormCardTitle>Plans</FormCardTitle>\n              <FormCardDescription>\n                Choose a plan that fits your needs.\n              </FormCardDescription>\n            </FormCardHeader>\n            <FormCardSeparator />\n            <FormCardContent className=\"pb-4\">\n              <DataTable />\n            </FormCardContent>\n          </FormCard>\n        </FormCardGroup>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/billing/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { Tabs } from \"../tabs\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <Breadcrumb />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <Tabs />\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/billing/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/billing/page.tsx",
    "content": "import type { SearchParams } from \"nuqs\";\nimport { Client } from \"./client\";\nimport { searchParamsCache } from \"./search-params\";\n\nexport default async function Page({\n  searchParams,\n}: {\n  searchParams: Promise<SearchParams>;\n}) {\n  await searchParamsCache.parse(searchParams);\n\n  return <Client />;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/billing/search-params.ts",
    "content": "import { createSearchParamsCache, parseAsBoolean } from \"nuqs/server\";\n\nexport const searchParamsParsers = {\n  success: parseAsBoolean,\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/general/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Cog, SlidersHorizontal } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[\n        {\n          type: \"link\",\n          label: \"Settings\",\n          icon: Cog,\n          href: \"/settings/general\",\n        },\n        { type: \"page\", label: \"General\", icon: SlidersHorizontal },\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/general/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Tabs } from \"../tabs\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(trpc.member.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.invitation.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.apiKeyRouter.getAll.queryOptions());\n\n  return (\n    <HydrateClient>\n      <div>\n        <AppHeader>\n          <AppHeaderContent>\n            <AppSidebarTrigger />\n            <Breadcrumb />\n          </AppHeaderContent>\n          <AppHeaderActions>\n            <NavActions />\n          </AppHeaderActions>\n        </AppHeader>\n        <Tabs />\n        <main className=\"w-full flex-1\">{children}</main>\n      </div>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/general/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/general/page.tsx",
    "content": "\"use client\";\n\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormCardGroup } from \"@/components/forms/form-card\";\nimport { FormApiKey } from \"@/components/forms/settings/form-api-key\";\nimport { FormMembers } from \"@/components/forms/settings/form-members\";\nimport { FormSlug } from \"@/components/forms/settings/form-slug\";\nimport { FormWorkspace } from \"@/components/forms/settings/form-workspace\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\n\nconst BASE_URL = \"https://app.openstatus.dev/invite\";\n\nexport default function Page() {\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const updateWorkspaceNameMutation = useMutation(\n    trpc.workspace.updateName.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.workspace.list.queryKey(),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.workspace.get.queryKey(),\n        });\n      },\n    }),\n  );\n  const sendInvitationMutation = useMutation(\n    trpc.emailRouter.sendTeamInvitation.mutationOptions(),\n  );\n  const createInvitationMutation = useMutation(\n    trpc.invitation.create.mutationOptions({\n      onSuccess: (data) => {\n        sendInvitationMutation.mutate({ id: data.id, baseUrl: BASE_URL });\n        queryClient.invalidateQueries({\n          queryKey: trpc.invitation.list.queryKey(),\n        });\n      },\n    }),\n  );\n\n  if (!workspace) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>General</SectionTitle>\n          <SectionDescription>\n            Manage your workspace settings.\n          </SectionDescription>\n        </SectionHeader>\n        <FormCardGroup>\n          <FormWorkspace\n            defaultValues={{ name: workspace.name || \"\" }}\n            onSubmit={async (values) => {\n              await updateWorkspaceNameMutation.mutateAsync({\n                name: values.name,\n              });\n            }}\n          />\n          <FormSlug defaultValues={{ slug: workspace.slug }} />\n          <FormMembers\n            onCreate={async (values) => {\n              await createInvitationMutation.mutateAsync({\n                email: values.email,\n              });\n            }}\n            locked={\n              (typeof workspace.limits.members === \"number\" &&\n                workspace.limits.members === 1) ||\n              workspace.limits.members !== \"Unlimited\"\n            }\n          />\n          <FormApiKey />\n        </FormCardGroup>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/integrations/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { Blocks, Cog } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[\n        {\n          type: \"link\",\n          label: \"Settings\",\n          icon: Cog,\n          href: \"/settings/general\",\n        },\n        { type: \"page\", label: \"Integrations\", icon: Blocks },\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/integrations/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Tabs } from \"../tabs\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(trpc.integrationRouter.list.queryOptions());\n  await queryClient.prefetchQuery(trpc.workspace.get.queryOptions());\n\n  return (\n    <HydrateClient>\n      <div>\n        <AppHeader>\n          <AppHeaderContent>\n            <AppSidebarTrigger />\n            <Breadcrumb />\n          </AppHeaderContent>\n          <AppHeaderActions>\n            <NavActions />\n          </AppHeaderActions>\n        </AppHeader>\n        <Tabs />\n        <main className=\"w-full flex-1\">{children}</main>\n      </div>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/integrations/nav-actions.tsx",
    "content": "import { NavFeedback } from \"@/components/nav/nav-feedback\";\n\nexport function NavActions() {\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/integrations/page.tsx",
    "content": "\"use client\";\n\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormCardGroup } from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { SlackIntegrationCard } from \"./slack-card\";\n\nexport default function Page() {\n  const trpc = useTRPC();\n  //  FIXME: we should use workspace limit here\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n\n  const { data: integrations } = useQuery(\n    trpc.integrationRouter.list.queryOptions(),\n  );\n\n  if (!integrations) return null;\n\n  const slackIntegration = integrations.find((i) => i.name === \"slack-agent\");\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Integrations</SectionTitle>\n          <SectionDescription>\n            Connect third-party services to your workspace.\n          </SectionDescription>\n        </SectionHeader>\n        <FormCardGroup>\n          <SlackIntegrationCard\n            locked={!workspace?.limits[\"slack-agent\"]}\n            integration={\n              slackIntegration\n                ? {\n                    id: slackIntegration.id,\n                    externalId: slackIntegration.externalId,\n                    data: slackIntegration.data as {\n                      teamName?: string;\n                    },\n                  }\n                : null\n            }\n          />\n        </FormCardGroup>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/integrations/slack-card.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n  FormCardUpgrade,\n} from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { Lock } from \"lucide-react\";\nimport { useRouter } from \"next/navigation\";\n\nconst SERVER_URL =\n  process.env.NODE_ENV === \"production\"\n    ? \"https://api.openstatus.dev\"\n    : \"http://localhost:3000\";\n\ninterface SlackIntegrationCardProps {\n  locked?: boolean;\n  integration: {\n    id: number;\n    externalId: string;\n    data: { teamName?: string };\n  } | null;\n}\n\nexport function SlackIntegrationCard({\n  locked,\n  integration,\n}: SlackIntegrationCardProps) {\n  const router = useRouter();\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const isConnected = !!integration;\n\n  const deleteIntegration = useMutation(\n    trpc.integrationRouter.deleteIntegration.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.integrationRouter.list.queryKey(),\n        });\n        router.refresh();\n      },\n    }),\n  );\n\n  const generateToken = useMutation(\n    trpc.integrationRouter.generateInstallToken.mutationOptions({\n      onSuccess: (data) => {\n        window.location.href = `${SERVER_URL}/slack/install?token=${data.token}`;\n      },\n    }),\n  );\n\n  const handleInstall = () => {\n    generateToken.mutate();\n  };\n\n  const handleDisconnect = () => {\n    if (!integration) return;\n    deleteIntegration.mutate({ integrationId: integration.id });\n  };\n\n  return (\n    <FormCard>\n      {locked ? <FormCardUpgrade /> : null}\n      <FormCardHeader>\n        <div className=\"flex items-center gap-2\">\n          <FormCardTitle>Slack</FormCardTitle>\n          {isConnected && <Badge variant=\"secondary\">Connected</Badge>}\n        </div>\n        <FormCardDescription>\n          Manage status reports directly from Slack. Mention the bot in a\n          channel to create and update incidents.\n        </FormCardDescription>\n      </FormCardHeader>\n      <FormCardContent>\n        {isConnected ? (\n          <p className=\"text-muted-foreground text-sm\">\n            Connected to{\" \"}\n            <strong>{integration.data?.teamName ?? \"Slack workspace\"}</strong>\n          </p>\n        ) : (\n          <p className=\"text-muted-foreground text-sm\">\n            Connect your Slack workspace to get started.\n          </p>\n        )}\n      </FormCardContent>\n      <FormCardFooter>\n        <FormCardFooterInfo>\n          Learn more about{\" \"}\n          <Link\n            href=\"https://www.openstatus.dev/blog/openstatus-slack-agent\"\n            rel=\"noreferrer\"\n            target=\"_blank\"\n          >\n            Slack Agent\n          </Link>\n          .\n        </FormCardFooterInfo>\n        {locked ? (\n          <Button type=\"button\" asChild>\n            <Link href=\"/settings/billing\">\n              <Lock />\n              Upgrade\n            </Link>\n          </Button>\n        ) : isConnected ? (\n          <Button\n            variant=\"destructive\"\n            size=\"sm\"\n            onClick={handleDisconnect}\n            disabled={deleteIntegration.isPending}\n          >\n            {deleteIntegration.isPending ? \"Disconnecting...\" : \"Disconnect\"}\n          </Button>\n        ) : (\n          <Button\n            size=\"sm\"\n            onClick={handleInstall}\n            disabled={generateToken.isPending}\n          >\n            {generateToken.isPending ? \"Connecting...\" : \"Add to Slack\"}\n          </Button>\n        )}\n      </FormCardFooter>\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/settings/tabs.tsx",
    "content": "\"use client\";\n\nimport { NavTabs } from \"@/components/nav/nav-tabs\";\nimport { Blocks, Cog, CreditCard, User } from \"lucide-react\";\n\nexport function Tabs() {\n  return (\n    <NavTabs\n      items={[\n        {\n          value: \"general\",\n          label: \"General\",\n          icon: Cog,\n          href: \"/settings/general\",\n        },\n        {\n          value: \"account\",\n          label: \"Account\",\n          icon: User,\n          href: \"/settings/account\",\n        },\n        {\n          value: \"billing\",\n          label: \"Billing\",\n          icon: CreditCard,\n          href: \"/settings/billing\",\n        },\n        {\n          value: \"integrations\",\n          label: \"Integrations\",\n          icon: Blocks,\n          href: \"/settings/integrations\",\n        },\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/(list)/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { PanelTop } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[{ type: \"page\", label: \"Status Pages\", icon: PanelTop }]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/(list)/client.tsx",
    "content": "\"use client\";\n\nimport { Note, NoteButton } from \"@/components/common/note\";\nimport {\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { columns } from \"@/components/data-table/status-pages/columns\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { DataTablePaginationSimple } from \"@/components/ui/data-table/data-table-pagination\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Palette } from \"lucide-react\";\nimport Link from \"next/link\";\n\nexport function Client() {\n  const trpc = useTRPC();\n  const { data: statusPages } = useQuery(trpc.page.list.queryOptions());\n\n  // TODO: add skeleton\n  if (!statusPages) return null;\n\n  return (\n    <SectionGroup>\n      <Note>\n        <Palette />\n        Create your own custom themes for your status pages.\n        <NoteButton variant=\"default\" asChild>\n          <Link href=\"https://themes.openstatus.dev\" target=\"_blank\">\n            Learn more\n          </Link>\n        </NoteButton>\n      </Note>\n      <SectionHeader>\n        <SectionTitle>Status Pages</SectionTitle>\n        <SectionDescription>\n          Create and manage your status pages.\n        </SectionDescription>\n        <DataTable\n          columns={columns}\n          data={statusPages}\n          paginationComponent={DataTablePaginationSimple}\n        />\n      </SectionHeader>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/(list)/layout.tsx",
    "content": "import {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <Breadcrumb />\n        </AppHeaderContent>\n        <AppHeaderActions>\n          <NavActions />\n        </AppHeaderActions>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/(list)/nav-actions.tsx",
    "content": "\"use client\";\n\nimport { UpgradeDialog } from \"@/components/dialogs/upgrade\";\nimport { NavFeedback } from \"@/components/nav/nav-feedback\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Link from \"next/link\";\nimport { useState } from \"react\";\n\nexport function NavActions() {\n  const [openDialog, setOpenDialog] = useState(false);\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const { data: statusPages } = useQuery(trpc.page.list.queryOptions());\n\n  if (!workspace || !statusPages) return null;\n\n  const limitReached = statusPages.length >= workspace.limits[\"status-pages\"];\n\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n      {limitReached ? (\n        <Button\n          size=\"sm\"\n          data-limited={limitReached}\n          className=\"data-[limited=true]:opacity-80\"\n          onClick={() => setOpenDialog(true)}\n        >\n          Create Status Page\n        </Button>\n      ) : (\n        <Button size=\"sm\" asChild>\n          <Link href=\"/status-pages/create\">Create Status Page</Link>\n        </Button>\n      )}\n      <UpgradeDialog\n        open={openDialog}\n        onOpenChange={setOpenDialog}\n        limit=\"status-pages\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/(list)/page.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Client } from \"./client\";\n\nexport default async function Page() {\n  const queryClient = getQueryClient();\n\n  await queryClient.prefetchQuery(trpc.page.list.queryOptions());\n\n  return (\n    <HydrateClient>\n      <Client />\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { PanelTop } from \"lucide-react\";\nimport { useParams, usePathname } from \"next/navigation\";\nimport { STATUS_PAGE_TABS } from \"./constants\";\n\nexport function Breadcrumb() {\n  const { id } = useParams<{ id: string }>();\n  const pathname = usePathname();\n  const trpc = useTRPC();\n  const { data: statusPage } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  if (!statusPage) return null;\n\n  const segments = pathname.split(\"/\");\n  const currentTab = STATUS_PAGE_TABS.find((tab) =>\n    segments.includes(tab.value),\n  );\n\n  return (\n    <NavBreadcrumb\n      items={[\n        {\n          type: \"link\",\n          label: \"Status Pages\",\n          href: \"/status-pages\",\n          icon: PanelTop,\n        },\n        {\n          type: \"link\",\n          label: statusPage.title,\n          href: `/status-pages/${id}/status-reports`,\n        },\n        ...(currentTab\n          ? [\n              {\n                type: \"page\" as const,\n                label: currentTab.label,\n                icon: currentTab.icon,\n              },\n            ]\n          : []),\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/components/layout.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { SidebarProvider } from \"@openstatus/ui/components/ui/sidebar\";\nimport { Sidebar } from \"../sidebar\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string }>;\n}) {\n  const { id } = await params;\n  const queryClient = getQueryClient();\n\n  await Promise.all([\n    queryClient.prefetchQuery(\n      trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n    ),\n    queryClient.prefetchQuery(trpc.monitor.list.queryOptions()),\n    queryClient.prefetchQuery(\n      trpc.pageComponent.list.queryOptions({ pageId: Number.parseInt(id) }),\n    ),\n  ]);\n\n  return (\n    <HydrateClient>\n      <SidebarProvider defaultOpen={false}>\n        <div className=\"w-full flex-1\">{children}</div>\n        <div className=\"hidden lg:block\">\n          <Sidebar />\n        </div>\n      </SidebarProvider>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/components/page.tsx",
    "content": "\"use client\";\n\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormComponentsUpdate } from \"@/components/forms/components/update\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function Page() {\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const { data: statusPage } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  if (!statusPage) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>{statusPage.title}</SectionTitle>\n          <SectionDescription>\n            Configure your page components.\n          </SectionDescription>\n        </SectionHeader>\n        <FormComponentsUpdate />\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/constants.ts",
    "content": "import type { LucideIcon } from \"lucide-react\";\nimport { Cog, Hammer, LayoutTemplate, Megaphone, Users } from \"lucide-react\";\n\nexport const STATUS_PAGE_TABS: {\n  value: string;\n  label: string;\n  icon: LucideIcon;\n}[] = [\n  { value: \"status-reports\", label: \"Status Reports\", icon: Megaphone },\n  { value: \"maintenances\", label: \"Maintenances\", icon: Hammer },\n  { value: \"subscribers\", label: \"Subscribers\", icon: Users },\n  { value: \"components\", label: \"Components\", icon: LayoutTemplate },\n  { value: \"edit\", label: \"Settings\", icon: Cog },\n];\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/edit/page.tsx",
    "content": "\"use client\";\n\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormStatusPageUpdate } from \"@/components/forms/status-page/update\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function Page() {\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const { data: statusPage } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  if (!statusPage) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>{statusPage.title}</SectionTitle>\n          <SectionDescription>Customize your status page.</SectionDescription>\n        </SectionHeader>\n        <FormStatusPageUpdate />\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/layout.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nimport {\n  AppHeader,\n  AppHeaderActions,\n  AppHeaderContent,\n} from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Breadcrumb } from \"./breadcrumb\";\nimport { NavActions } from \"./nav-actions\";\nimport { Tabs } from \"./tabs\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string }>;\n}) {\n  const { id } = await params;\n  const queryClient = getQueryClient();\n\n  const pageData = await queryClient.fetchQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  if (!pageData?.id) {\n    redirect(\"/status-pages\");\n  }\n\n  await queryClient.prefetchQuery(trpc.monitor.list.queryOptions());\n\n  return (\n    <HydrateClient>\n      <div>\n        <AppHeader>\n          <AppHeaderContent>\n            <AppSidebarTrigger />\n            <Breadcrumb />\n          </AppHeaderContent>\n          <AppHeaderActions>\n            <NavActions />\n          </AppHeaderActions>\n        </AppHeader>\n        <Tabs />\n        <main className=\"flex-1\">{children}</main>\n      </div>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/layout.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { SidebarProvider } from \"@openstatus/ui/components/ui/sidebar\";\nimport { Sidebar } from \"../sidebar\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string }>;\n}) {\n  const { id } = await params;\n  const queryClient = getQueryClient();\n\n  await queryClient.prefetchQuery(\n    trpc.maintenance.list.queryOptions({\n      pageId: Number.parseInt(id),\n    }),\n  );\n\n  return (\n    <HydrateClient>\n      <SidebarProvider defaultOpen={false}>\n        <div className=\"w-full flex-1\">{children}</div>\n        <div className=\"hidden lg:block\">\n          <Sidebar />\n        </div>\n      </SidebarProvider>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/page.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionHeaderRow,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { columns } from \"@/components/data-table/maintenances/columns\";\nimport { FormSheetMaintenance } from \"@/components/forms/maintenance/sheet\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { Plus } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\n\nexport default function Page() {\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const { data: statusPage } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  const { data: maintenances, refetch } = useQuery(\n    trpc.maintenance.list.queryOptions({\n      pageId: Number.parseInt(id),\n    }),\n  );\n  const sendMaintenanceUpdateMutation = useMutation(\n    trpc.emailRouter.sendMaintenance.mutationOptions(),\n  );\n  const createMaintenanceMutation = useMutation(\n    trpc.maintenance.new.mutationOptions({\n      onSuccess: (maintenance) => {\n        // TODO: move to server\n        if (maintenance.notifySubscribers) {\n          sendMaintenanceUpdateMutation.mutateAsync({ id: maintenance.id });\n        }\n        //\n        refetch();\n      },\n    }),\n  );\n\n  if (!statusPage || !maintenances) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeaderRow>\n          <SectionHeader>\n            <SectionTitle>{statusPage.title}</SectionTitle>\n            <SectionDescription>\n              List of all maintenances. Looking for{\" \"}\n              <Link href={`/status-pages/${id}/status-reports`}>\n                status reports\n              </Link>\n              ?\n            </SectionDescription>\n          </SectionHeader>\n          <div>\n            <FormSheetMaintenance\n              pageComponents={statusPage.pageComponents}\n              onSubmit={async (values) => {\n                await createMaintenanceMutation.mutateAsync({\n                  pageId: Number.parseInt(id),\n                  title: values.title,\n                  message: values.message,\n                  startDate: values.startDate,\n                  endDate: values.endDate,\n                  pageComponents: values.pageComponents,\n                  notifySubscribers: values.notifySubscribers,\n                });\n              }}\n            >\n              <Button data-section=\"action\" size=\"sm\">\n                <Plus />\n                Create Maintenance\n              </Button>\n            </FormSheetMaintenance>\n          </div>\n        </SectionHeaderRow>\n        <DataTable columns={columns} data={maintenances} />\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/nav-actions.tsx",
    "content": "\"use client\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { NavFeedback } from \"@/components/nav/nav-feedback\";\nimport { getActions } from \"@/data/status-pages.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { Globe } from \"lucide-react\";\nimport { useParams, usePathname, useRouter } from \"next/navigation\";\nimport { toast } from \"sonner\";\n\nexport function NavActions() {\n  const { id } = useParams<{ id: string }>();\n  const router = useRouter();\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const pathname = usePathname();\n\n  const { data: statusPage } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n\n  const deleteStatusPageMutation = useMutation(\n    trpc.page.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n        if (pathname.includes(`/status-pages/${id}`)) {\n          router.push(\"/status-pages\");\n        }\n      },\n    }),\n  );\n\n  const actions = getActions({\n    edit: () => router.push(`/status-pages/${id}/edit`),\n    \"copy-id\": async () => {\n      await navigator.clipboard.writeText(id);\n      toast.success(\"Status Page ID copied to clipboard\");\n    },\n  });\n\n  if (!statusPage) return null;\n\n  return (\n    <div className=\"flex items-center gap-2 text-sm\">\n      <NavFeedback />\n      <TooltipProvider>\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Button variant=\"ghost\" size=\"sm\" className=\"group h-7 w-7\" asChild>\n              <a\n                href={`https://${\n                  statusPage.customDomain || `${statusPage.slug}.openstatus.dev`\n                }`}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                <Globe className=\"h-4 w-4 text-muted-foreground group-hover:text-foreground\" />\n              </a>\n            </Button>\n          </TooltipTrigger>\n          <TooltipContent>View Page</TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n      <QuickActions\n        actions={actions}\n        deleteAction={{\n          confirmationValue: statusPage.title ?? \"status page\",\n          submitAction: async () => {\n            await deleteStatusPageMutation.mutateAsync({\n              id: Number.parseInt(id),\n            });\n          },\n        }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default async function Page({\n  params,\n}: {\n  params: Promise<{ id: string }>;\n}) {\n  const { id } = await params;\n  redirect(`/status-pages/${id}/status-reports`);\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/sidebar.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport { TableCellLink } from \"@/components/data-table/table-cell-link\";\nimport { SidebarRight } from \"@/components/nav/sidebar-right\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { ExternalLink } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\n\nexport function Sidebar() {\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const { data: statusPage } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  const { copy } = useCopyToClipboard();\n\n  if (!statusPage) return null;\n\n  const BADGE_URL = `https://${statusPage.slug}.openstatus.dev/badge/v2`;\n\n  return (\n    <SidebarRight\n      header=\"Status Page\"\n      metadata={[\n        {\n          label: \"Overview\",\n          items: [\n            {\n              label: \"Slug\",\n              value: (\n                <Link\n                  href={`https://${\n                    statusPage.customDomain ||\n                    `${statusPage.slug}.openstatus.dev`\n                  }`}\n                  target=\"_blank\"\n                >\n                  {statusPage.slug}\n                </Link>\n              ),\n            },\n            {\n              label: \"Access Type\",\n              value: statusPage.accessType,\n            },\n            { label: \"Domain\", value: statusPage.customDomain || \"-\" },\n            {\n              label: \"Favicon\",\n              value: statusPage.icon ? (\n                <div className=\"size-4 overflow-hidden rounded border bg-muted\">\n                  <img src={statusPage.icon} alt=\"favicon\" />\n                </div>\n              ) : (\n                \"-\"\n              ),\n            },\n            {\n              label: \"Badge\",\n              value: (\n                <TooltipProvider>\n                  <Tooltip>\n                    <TooltipTrigger className=\"align-middle\">\n                      <img\n                        className=\"h-5 rounded-sm border\"\n                        src={BADGE_URL}\n                        alt=\"badge\"\n                      />\n                    </TooltipTrigger>\n                    <TooltipContent\n                      className=\"cursor-pointer\"\n                      side=\"left\"\n                      onClick={() => copy(BADGE_URL, { withToast: true })}\n                    >\n                      {BADGE_URL}\n                    </TooltipContent>\n                  </Tooltip>\n                </TooltipProvider>\n              ),\n            },\n          ],\n        },\n        {\n          label: \"Configuration\",\n          items: [\n            {\n              label: \"Theme\",\n              value: statusPage.configuration?.theme ?? \"-\",\n            },\n            {\n              label: \"Bar Value\",\n              value: statusPage.configuration?.type ?? \"-\",\n            },\n            {\n              label: \"Card Value\",\n              value: statusPage.configuration?.value ?? \"-\",\n            },\n            {\n              label: \"Show Uptime\",\n              value: statusPage.configuration?.uptime ? \"Yes\" : \"No\",\n            },\n          ],\n        },\n        {\n          label: \"Monitors\",\n          items: statusPage.pageComponents.flatMap((component) => {\n            const arr = [];\n            arr.push({\n              label: \"Name\",\n              value: (\n                <TableCellLink\n                  href={`/status-pages/${statusPage.id}/components`}\n                  value={component.name}\n                />\n              ),\n            });\n            arr.push({\n              label: \"Type\",\n              value: component.type,\n              isNested: true,\n            });\n            return arr;\n          }),\n        },\n      ]}\n      footerButton={{\n        onClick: () =>\n          typeof window !== \"undefined\" &&\n          window.open(\n            `https://${\n              statusPage.customDomain || `${statusPage.slug}.openstatus.dev`\n            }`,\n            \"_blank\",\n          ),\n        children: (\n          <>\n            <ExternalLink />\n            <span>Visit Status Page</span>\n          </>\n        ),\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/status-reports/[reportId]/layout.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string; reportId: string }>;\n}) {\n  const { id, reportId } = await params;\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(\n    trpc.statusReport.get.queryOptions({ id: Number.parseInt(reportId) }),\n  );\n  await queryClient.prefetchQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  return <HydrateClient>{children}</HydrateClient>;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/status-reports/[reportId]/page.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateDescription,\n} from \"@/components/content/empty-state\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionHeaderRow,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormCardGroup } from \"@/components/forms/form-card\";\nimport { FormSheetWithDirtyProtection } from \"@/components/forms/form-sheet\";\nimport type { FormValues } from \"@/components/forms/status-report-update/form\";\nimport { FormStatusReportUpdateCard } from \"@/components/forms/status-report-update/form-status-report\";\nimport { FormSheetStatusReportUpdate } from \"@/components/forms/status-report-update/sheet\";\nimport { getNextStatus } from \"@/data/status-report-updates.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { Plus } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\n\nexport default function Page() {\n  const { reportId } = useParams<{ id: string; reportId: string }>();\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n\n  const { data: statusReport, refetch } = useQuery(\n    trpc.statusReport.get.queryOptions({ id: Number.parseInt(reportId) }),\n  );\n\n  const sendStatusReportUpdateMutation = useMutation(\n    trpc.emailRouter.sendStatusReport.mutationOptions(),\n  );\n\n  const createStatusReportUpdateMutation = useMutation(\n    trpc.statusReport.createStatusReportUpdate.mutationOptions({\n      onSuccess: (update) => {\n        if (update?.notifySubscribers) {\n          sendStatusReportUpdateMutation.mutateAsync({ id: update.id });\n        }\n        refetch();\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n      },\n    }),\n  );\n\n  const updateStatusReportUpdateMutation = useMutation(\n    trpc.statusReport.updateStatusReportUpdate.mutationOptions({\n      onSuccess: () => {\n        refetch();\n      },\n    }),\n  );\n\n  if (!statusReport) return null;\n\n  const updates = [...statusReport.updates].sort(\n    (a, b) => b.date.getTime() - a.date.getTime(),\n  );\n\n  const affected = statusReport.pageComponents\n    .map((component) => component.name)\n    .join(\", \");\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeaderRow>\n          <SectionHeader>\n            <SectionTitle>{statusReport.title}</SectionTitle>\n            <SectionDescription>\n              Manage updates for this status report. Affects{\" \"}\n              <span className=\"text-foreground\">\n                {affected ? affected : \"zero\"}\n              </span>{\" \"}\n              component(s).\n            </SectionDescription>\n          </SectionHeader>\n        </SectionHeaderRow>\n\n        <EmptyStateContainer className=\"my-8 border-dashed\">\n          <EmptyStateDescription>Status Page Report</EmptyStateDescription>\n          <FormSheetStatusReportUpdate\n            defaultValues={{\n              status: getNextStatus(statusReport.status),\n            }}\n            onSubmit={async (values: FormValues) => {\n              await createStatusReportUpdateMutation.mutateAsync({\n                statusReportId: statusReport.id,\n                message: values.message,\n                status: values.status,\n                date: values.date,\n                notifySubscribers: values.notifySubscribers,\n              });\n            }}\n          >\n            <Button size=\"sm\">\n              <Plus />\n              Create Status Update\n            </Button>\n          </FormSheetStatusReportUpdate>\n        </EmptyStateContainer>\n\n        <FormCardGroup>\n          {updates.map((update, index) => (\n            <FormSheetWithDirtyProtection key={update.id}>\n              <FormStatusReportUpdateCard\n                id={`update-form-${update.id}`}\n                index={index}\n                update={update}\n                defaultValues={{\n                  status: update.status,\n                  message: update.message,\n                  date: update.date,\n                }}\n                onSubmit={async (values: FormValues) => {\n                  await updateStatusReportUpdateMutation.mutateAsync({\n                    id: update.id,\n                    statusReportId: statusReport.id,\n                    message: values.message,\n                    status: values.status,\n                    date: values.date,\n                  });\n                }}\n              />\n            </FormSheetWithDirtyProtection>\n          ))}\n        </FormCardGroup>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/status-reports/layout.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { SidebarProvider } from \"@openstatus/ui/components/ui/sidebar\";\nimport { Sidebar } from \"../sidebar\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string }>;\n}) {\n  const queryClient = getQueryClient();\n  const { id } = await params;\n  await queryClient.prefetchQuery(\n    trpc.statusReport.list.queryOptions({ pageId: Number.parseInt(id) }),\n  );\n\n  return (\n    <HydrateClient>\n      <SidebarProvider defaultOpen={false}>\n        <div className=\"w-full flex-1\">{children}</div>\n        <div className=\"hidden lg:block\">\n          <Sidebar />\n        </div>\n      </SidebarProvider>\n    </HydrateClient>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/status-reports/page.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionHeaderRow,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { DataTable as UpdatesDataTable } from \"@/components/data-table/status-report-updates/data-table\";\nimport { columns } from \"@/components/data-table/status-reports/columns\";\nimport { FormSheetStatusReport } from \"@/components/forms/status-report/sheet\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { Plus } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\n\nexport default function Page() {\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const { data: page } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  const { data: statusReports, refetch } = useQuery(\n    trpc.statusReport.list.queryOptions({ pageId: Number.parseInt(id) }),\n  );\n  const sendStatusReportUpdateMutation = useMutation(\n    trpc.emailRouter.sendStatusReport.mutationOptions(),\n  );\n  const createStatusReportMutation = useMutation(\n    trpc.statusReport.create.mutationOptions({\n      onSuccess: async (statusReport) => {\n        // TODO: move to server\n        if (statusReport.notifySubscribers) {\n          await sendStatusReportUpdateMutation.mutateAsync({\n            id: statusReport.id,\n          });\n        }\n        //\n        refetch();\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n      },\n    }),\n  );\n\n  if (!statusReports || !page) return null;\n\n  const hasUnresolvedIssue = statusReports.some(\n    (report) => report.status !== \"resolved\",\n  );\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeaderRow>\n          <SectionHeader>\n            <SectionTitle>{page.title}</SectionTitle>\n            <SectionDescription>\n              List of all status reports. Looking for{\" \"}\n              <Link href={`/status-pages/${id}/maintenances`}>\n                maintenances\n              </Link>\n              ?\n            </SectionDescription>\n          </SectionHeader>\n          <div>\n            <FormSheetStatusReport\n              warning={\n                hasUnresolvedIssue ? (\n                  <>\n                    An unresolved report already exists. Consider adding a{\" \"}\n                    <span className=\"font-semibold\">status report update</span>{\" \"}\n                    instead.\n                  </>\n                ) : undefined\n              }\n              pageComponents={page.pageComponents}\n              onSubmit={async (values) => {\n                // NOTE: for type safety, we need to check if the values have a date property\n                // because of the union type\n                if (\"date\" in values) {\n                  await createStatusReportMutation.mutateAsync({\n                    title: values.title,\n                    status: values.status,\n                    pageId: Number.parseInt(id),\n                    pageComponents: values.pageComponents,\n                    date: values.date,\n                    message: values.message,\n                    notifySubscribers: values.notifySubscribers,\n                  });\n                }\n              }}\n            >\n              <Button data-section=\"action\" size=\"sm\">\n                <Plus />\n                Create Status Report\n              </Button>\n            </FormSheetStatusReport>\n          </div>\n        </SectionHeaderRow>\n        <DataTable\n          columns={columns}\n          data={statusReports}\n          onRowClick={(row) =>\n            row.getCanExpand() ? row.toggleExpanded() : undefined\n          }\n          rowComponent={({ row }) => (\n            <UpdatesDataTable\n              updates={row.original.updates}\n              reportId={row.original.id}\n            />\n          )}\n        />\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/subscribers/layout.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string }>;\n}) {\n  const queryClient = getQueryClient();\n  const { id } = await params;\n\n  await queryClient.prefetchQuery(\n    trpc.pageSubscriber.list.queryOptions({ pageId: Number.parseInt(id) }),\n  );\n\n  return <HydrateClient>{children}</HydrateClient>;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/subscribers/page.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  BillingOverlay,\n  BillingOverlayButton,\n  BillingOverlayContainer,\n  BillingOverlayDescription,\n} from \"@/components/content/billing-overlay\";\nimport {\n  EmptyStateContainer,\n  EmptyStateDescription,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport {\n  SectionDescription,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\n\nimport { Section } from \"@/components/content/section\";\nimport { columns } from \"@/components/data-table/subscribers/columns\";\nimport { UpgradeDialog } from \"@/components/dialogs/upgrade\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Lock } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\nimport { useState } from \"react\";\n\ntype Subscriber = RouterOutputs[\"pageSubscriber\"][\"list\"][number];\n\nconst EXAMPLES = [\n  {\n    id: 1,\n    email: \"max@openstatus.dev\",\n    createdAt: new Date(),\n    pageId: 1,\n    channelType: \"email\",\n    acceptedAt: new Date(),\n    unsubscribedAt: null,\n    components: [],\n    isEntirePage: true,\n    webhookUrl: null,\n  },\n  {\n    id: 2,\n    email: \"thibault@openstatus.dev\",\n    createdAt: new Date(),\n    pageId: 1,\n    channelType: \"email\",\n    acceptedAt: new Date(),\n    unsubscribedAt: null,\n    components: [],\n    isEntirePage: true,\n    webhookUrl: null,\n  },\n] satisfies Subscriber[];\n\nexport default function Page() {\n  const { id } = useParams<{ id: string }>();\n  const [openDialog, setOpenDialog] = useState(false);\n  const trpc = useTRPC();\n  const { data: page } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  const { data: subscribers } = useQuery(\n    trpc.pageSubscriber.list.queryOptions({ pageId: Number.parseInt(id) }),\n  );\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n\n  if (!workspace) return null;\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>{page?.title}</SectionTitle>\n          <SectionDescription>List of all subscribers.</SectionDescription>\n        </SectionHeader>\n      </Section>\n      <Section>\n        {workspace.limits[\"status-subscribers\"] === false ? (\n          <BillingOverlayContainer>\n            <DataTable\n              columns={columns}\n              data={[...EXAMPLES, ...EXAMPLES, ...EXAMPLES]}\n            />\n            <BillingOverlay>\n              <BillingOverlayButton onClick={() => setOpenDialog(true)}>\n                <Lock />\n                Upgrade\n              </BillingOverlayButton>\n              <BillingOverlayDescription>\n                Keep your users in the loop with status page updates.{\" \"}\n                <Link\n                  href=\"https://docs.openstatus.dev/reference/subscriber/\"\n                  rel=\"noreferrer\"\n                  target=\"_blank\"\n                >\n                  Learn more\n                </Link>\n                .\n              </BillingOverlayDescription>\n            </BillingOverlay>\n            <UpgradeDialog\n              open={openDialog}\n              onOpenChange={setOpenDialog}\n              limit=\"status-subscribers\"\n            />\n          </BillingOverlayContainer>\n        ) : subscribers?.length ? (\n          <DataTable columns={columns} data={subscribers} />\n        ) : (\n          <EmptyStateContainer>\n            <EmptyStateTitle>No subscribers</EmptyStateTitle>\n            <EmptyStateDescription>\n              No emails have been subscribed to this status page.\n            </EmptyStateDescription>\n          </EmptyStateContainer>\n        )}\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/[id]/tabs.tsx",
    "content": "\"use client\";\n\nimport { NavTabs } from \"@/components/nav/nav-tabs\";\nimport { useParams } from \"next/navigation\";\nimport { STATUS_PAGE_TABS } from \"./constants\";\n\nexport function Tabs() {\n  const { id } = useParams<{ id: string }>();\n\n  return (\n    <NavTabs\n      items={STATUS_PAGE_TABS.map((tab) => ({\n        ...tab,\n        href: `/status-pages/${id}/${tab.value}`,\n      }))}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/create/breadcrumb.tsx",
    "content": "\"use client\";\n\nimport { NavBreadcrumb } from \"@/components/nav/nav-breadcrumb\";\nimport { PanelTop } from \"lucide-react\";\n\nexport function Breadcrumb() {\n  return (\n    <NavBreadcrumb\n      items={[\n        {\n          type: \"link\",\n          label: \"Status Pages\",\n          href: \"/status-pages\",\n          icon: PanelTop,\n        },\n        { type: \"page\", label: \"Create Status Page\" },\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/create/client.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateDescription,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport { EmptyStateContainer } from \"@/components/content/empty-state\";\nimport {\n  Section,\n  SectionGroup,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormGeneral } from \"@/components/forms/status-page/form-general\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useRouter } from \"next/navigation\";\nimport { useTransition } from \"react\";\n\nexport function Client() {\n  const [isPending, startTransition] = useTransition();\n  const trpc = useTRPC();\n  const router = useRouter();\n  const queryClient = useQueryClient();\n  const { refetch } = useQuery(trpc.page.list.queryOptions());\n  const createStatusPageMutation = useMutation(\n    trpc.page.new.mutationOptions({\n      onSuccess: (data) => {\n        refetch();\n        // NOTE: invalidate workspace to update the usage\n        queryClient.invalidateQueries({\n          queryKey: trpc.workspace.get.queryKey(),\n        });\n        startTransition(() => {\n          router.push(`/status-pages/${data.id}/edit`);\n        });\n      },\n    }),\n  );\n\n  return (\n    <SectionGroup>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Create Status Page</SectionTitle>\n        </SectionHeader>\n        <FormGeneral\n          disabled={isPending}\n          onSubmit={async (values) => {\n            await createStatusPageMutation.mutateAsync({\n              title: values.title,\n              slug: values.slug,\n              icon: values.icon,\n              description: values.description,\n            });\n          }}\n        />\n      </Section>\n      <Section>\n        <EmptyStateContainer>\n          <EmptyStateTitle>Create and start customizing</EmptyStateTitle>\n          <EmptyStateDescription>\n            Connect your <span className=\"text-foreground\">monitors</span>, set\n            up a <span className=\"text-foreground\">custom domain</span>,{\" \"}\n            <span className=\"text-foreground\">password protect</span> it and\n            more...\n          </EmptyStateDescription>\n        </EmptyStateContainer>\n      </Section>\n    </SectionGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/create/layout.tsx",
    "content": "import { AppHeader, AppHeaderContent } from \"@/components/nav/app-header\";\nimport { AppSidebarTrigger } from \"@/components/nav/app-sidebar\";\n\nimport { Breadcrumb } from \"./breadcrumb\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <AppHeader>\n        <AppHeaderContent>\n          <AppSidebarTrigger />\n          <Breadcrumb />\n        </AppHeaderContent>\n      </AppHeader>\n      <main className=\"w-full flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/(dashboard)/status-pages/create/page.tsx",
    "content": "import { Client } from \"./client\";\n\nexport default function Page() {\n  return <Client />;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/api/auth/[...nextauth]/route.ts",
    "content": "import { handlers } from \"@/lib/auth\";\n\nexport const { GET, POST } = handlers;\n"
  },
  {
    "path": "apps/dashboard/src/app/api/trpc/edge/[trpc]/route.ts",
    "content": "import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport type { NextRequest } from \"next/server\";\n\nimport { auth } from \"@/lib/auth\";\nimport { createTRPCContext } from \"@openstatus/api\";\nimport { edgeRouter } from \"@openstatus/api/src/edge\";\n\nexport const runtime = \"edge\";\n\nconst handler = (req: NextRequest) =>\n  fetchRequestHandler({\n    endpoint: \"/api/trpc/edge\",\n    router: edgeRouter,\n    req: req,\n    createContext: () => createTRPCContext({ req, auth }),\n    onError: ({ error }) => {\n      console.log(\"Error in tRPC handler (edge)\");\n      console.error(error);\n    },\n  });\n\nexport { handler as GET, handler as POST };\n"
  },
  {
    "path": "apps/dashboard/src/app/api/trpc/lambda/[trpc]/route.ts",
    "content": "import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport type { NextRequest } from \"next/server\";\n\nimport { auth } from \"@/lib/auth\";\nimport { createTRPCContext } from \"@openstatus/api\";\nimport { lambdaRouter } from \"@openstatus/api/src/lambda\";\n\n// Stripe is incompatible with Edge runtimes due to using Node.js events\n// export const runtime = \"edge\";\n\nconst handler = (req: NextRequest) =>\n  fetchRequestHandler({\n    endpoint: \"/api/trpc/lambda\",\n    router: lambdaRouter,\n    req: req,\n    createContext: () => createTRPCContext({ req, auth }),\n    onError: ({ error }) => {\n      console.log(\"Error in tRPC handler (lambda)\");\n      console.error(error);\n    },\n  });\n\nexport { handler as GET, handler as POST };\n"
  },
  {
    "path": "apps/dashboard/src/app/global-error.tsx",
    "content": "\"use client\";\n\nimport * as Sentry from \"@sentry/nextjs\";\nimport NextError from \"next/error\";\nimport { useEffect } from \"react\";\n\nexport default function GlobalError({\n  error,\n}: {\n  error: Error & { digest?: string };\n}) {\n  useEffect(() => {\n    Sentry.captureException(error);\n  }, [error]);\n\n  return (\n    <html lang=\"en\">\n      <body>\n        {/* This is the default Next.js error component but it doesn't allow omitting the statusCode property yet. */}\n        {/* biome-ignore lint/suspicious/noExplicitAny: <explanation> */}\n        <NextError statusCode={undefined as any} />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"@openstatus/ui/globals\";\n@import \"tw-animate-css\";\n@plugin \"@tailwindcss/typography\";\n\n/* safelist */\n@source inline(\"has-data-[slot=slider-range]:bg-red-500\");\n\n@theme {\n  --breakpoint-xs: 30rem;\n}\n\n@theme inline {\n  --font-cal: var(--font-cal-sans);\n  --font-commit-mono: var(--font-commit-mono);\n  --font-sans: var(--font-inter);\n  --font-sans: var(--font-geist-sans);\n  --font-mono: var(--font-geist-mono);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}"
  },
  {
    "path": "apps/dashboard/src/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Geist, Geist_Mono, Inter } from \"next/font/google\";\nimport \"./globals.css\";\nimport { TailwindIndicator } from \"@/components/tailwind-indicator\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { auth } from \"@/lib/auth\";\nimport { TRPCReactProvider } from \"@/lib/trpc/client\";\nimport { OpenPanelComponent } from \"@openpanel/nextjs\";\nimport { Toaster } from \"@openstatus/ui/components/ui/sonner\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { SessionProvider } from \"next-auth/react\";\nimport LocalFont from \"next/font/local\";\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\nimport { ogMetadata, twitterMetadata } from \"./metadata\";\nimport { defaultMetadata } from \"./metadata\";\n\nconst cal = LocalFont({\n  src: \"../../public/fonts/CalSans-SemiBold.ttf\",\n  variable: \"--font-cal-sans\",\n});\n\nconst commitMono = LocalFont({\n  src: [\n    {\n      path: \"../../public/fonts/CommitMono-400-Regular.otf\",\n      weight: \"400\",\n      style: \"normal\",\n    },\n    {\n      path: \"../../public/fonts/CommitMono-400-Italic.otf\",\n      weight: \"400\",\n      style: \"italic\",\n    },\n    {\n      path: \"../../public/fonts/CommitMono-700-Regular.otf\",\n      weight: \"700\",\n      style: \"normal\",\n    },\n    {\n      path: \"../../public/fonts/CommitMono-700-Italic.otf\",\n      weight: \"700\",\n      style: \"italic\",\n    },\n  ],\n  variable: \"--font-commit-mono\",\n});\n\nconst inter = Inter({\n  subsets: [\"latin\"],\n  variable: \"--font-inter\",\n});\n\nconst geistSans = Geist({\n  variable: \"--font-geist-sans\",\n  subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n});\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  twitter: {\n    ...twitterMetadata,\n  },\n  openGraph: {\n    ...ogMetadata,\n  },\n};\n\n// export const dynamic = \"error\";\n\nexport default async function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  const session = await auth();\n\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body\n        className={cn(\n          geistSans.variable,\n          geistMono.variable,\n          cal.variable,\n          commitMono.variable,\n          inter.variable,\n          \"font-sans antialiased \",\n        )}\n      >\n        <SessionProvider session={session}>\n          <TRPCReactProvider>\n            <NuqsAdapter>\n              <ThemeProvider\n                attribute=\"class\"\n                defaultTheme=\"system\"\n                enableSystem\n                disableTransitionOnChange\n              >\n                {children}\n                <TailwindIndicator />\n                <Toaster richColors expand />\n                {process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID && (\n                  <OpenPanelComponent\n                    clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID}\n                    trackScreenViews\n                    trackOutgoingLinks\n                    trackAttributes\n                    sessionReplay={{ enabled: true }}\n                  />\n                )}\n              </ThemeProvider>\n            </NuqsAdapter>\n          </TRPCReactProvider>\n        </SessionProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/login/_components/actions.ts",
    "content": "\"use server\";\n\nimport { signIn } from \"@/lib/auth\";\n\nexport async function signInWithResendAction(formData: FormData) {\n  try {\n    await signIn(\"resend\", formData);\n  } catch (e) {\n    console.error(e);\n  }\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/login/_components/magic-link-form.tsx",
    "content": "\"use client\";\n\nimport { useFormStatus } from \"react-dom\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { toast } from \"sonner\";\nimport { signInWithResendAction } from \"./actions\";\n\n/**\n * @deprecated - only to be used in development mode\n */\nexport default function MagicLinkForm() {\n  const { pending } = useFormStatus();\n\n  return (\n    <form\n      action={async (formData) => {\n        try {\n          await signInWithResendAction(formData);\n          toast.success(\"Check your terminal for the magic link.\");\n        } catch (e) {\n          console.error(e);\n          toast.error(\"Error sending magic link.\");\n        }\n      }}\n      className=\"grid gap-2\"\n    >\n      <div className=\"grid gap-1.5\">\n        <Label htmlFor=\"email\">Email</Label>\n        <Input id=\"email\" name=\"email\" type=\"email\" required />\n      </div>\n      <Button variant=\"secondary\" className=\"w-full\">\n        {pending ? \"Logging...\" : \"Log Magic Link\"}\n      </Button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/login/layout.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nimport { AuthLayout } from \"@/components/layout/auth-layout\";\nimport { auth } from \"@/lib/auth\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const session = await auth();\n  if (session) redirect(\"/\");\n\n  return <AuthLayout>{children}</AuthLayout>;\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/login/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Link from \"next/link\";\n\nimport { signIn } from \"@/lib/auth\";\nimport { GitHubIcon } from \"@openstatus/icons\";\nimport { GoogleIcon } from \"@openstatus/icons\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport type { SearchParams } from \"nuqs/server\";\nimport MagicLinkForm from \"./_components/magic-link-form\";\nimport { searchParamsCache } from \"./search-params\";\n\nexport const metadata: Metadata = {\n  title: \"Sign In\",\n  description:\n    \"Sign in to openstatus. Monitor your services and keep your users informed.\",\n  robots: {\n    index: true,\n    follow: true,\n  },\n  alternates: {\n    canonical: \"https://app.openstatus.dev/login\",\n  },\n};\n\nexport default async function Page(props: {\n  searchParams: Promise<SearchParams>;\n}) {\n  const searchParams = await props.searchParams;\n  const { redirectTo } = searchParamsCache.parse(searchParams);\n\n  return (\n    <div className=\"my-4 grid w-full max-w-lg gap-6\">\n      <div className=\"flex flex-col gap-1 text-center\">\n        <h1 className=\"font-semibold text-3xl tracking-tight\">Sign In</h1>\n        <p className=\"text-muted-foreground text-sm\">\n          Get started now. No credit card required.\n        </p>\n      </div>\n      <div className=\"grid gap-3 p-4\">\n        {process.env.NODE_ENV === \"development\" ||\n        process.env.SELF_HOST === \"true\" ? (\n          <div className=\"grid gap-3\">\n            <MagicLinkForm />\n            <Separator />\n          </div>\n        ) : null}\n        <form\n          action={async () => {\n            \"use server\";\n            await signIn(\"github\", { redirectTo: redirectTo ?? undefined });\n          }}\n          className=\"w-full\"\n        >\n          <Button type=\"submit\" className=\"w-full\">\n            Sign in with GitHub <GitHubIcon className=\"ml-2 h-4 w-4\" />\n          </Button>\n        </form>\n        <form\n          action={async () => {\n            \"use server\";\n            await signIn(\"google\", { redirectTo: redirectTo ?? undefined });\n          }}\n          className=\"w-full\"\n        >\n          <Button type=\"submit\" className=\"w-full\" variant=\"outline\">\n            Sign in with Google <GoogleIcon className=\"ml-2 h-4 w-4\" />\n          </Button>\n        </form>\n      </div>\n      <p className=\"px-8 text-center text-muted-foreground text-sm\">\n        By clicking continue, you agree to our{\" \"}\n        <Link\n          href=\"https://openstatus.dev/legal/terms\"\n          className=\"underline underline-offset-4 hover:text-primary hover:no-underline\"\n        >\n          Terms of Service\n        </Link>{\" \"}\n        and{\" \"}\n        <Link\n          href=\"https://openstatus.dev/legal/privacy\"\n          className=\"underline underline-offset-4 hover:text-primary hover:no-underline\"\n        >\n          Privacy Policy\n        </Link>\n        .\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/login/search-params.ts",
    "content": "import { createSearchParamsCache, parseAsString } from \"nuqs/server\";\n\nexport const searchParamsParsers = {\n  redirectTo: parseAsString,\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/dashboard/src/app/metadata.ts",
    "content": "import type { Metadata } from \"next\";\n\nexport const TITLE = \"openstatus\";\nexport const DESCRIPTION =\n  \"Open-source platform to monitor your services and keep your users informed.\";\n\nconst OG_TITLE = \"openstatus\";\nconst OG_DESCRIPTION = \"Monitor your services and keep your users informed.\";\nconst FOOTER = \"app.openstatus.dev\";\nconst IMAGE = \"assets/og/dashboard-v2.png\";\n\nexport const defaultMetadata: Metadata = {\n  title: {\n    template: `%s | ${TITLE}`,\n    default: TITLE,\n  },\n  description: DESCRIPTION,\n  metadataBase: new URL(\"https://www.openstatus.dev\"),\n  robots: {\n    index: false,\n    follow: false,\n  },\n};\n\nexport const twitterMetadata: Metadata[\"twitter\"] = {\n  title: TITLE,\n  description: DESCRIPTION,\n  card: \"summary_large_image\",\n  images: [\n    `/api/og?title=${OG_TITLE}&description=${OG_DESCRIPTION}&footer=${FOOTER}&image=${IMAGE}`,\n  ],\n};\n\nexport const ogMetadata: Metadata[\"openGraph\"] = {\n  title: TITLE,\n  description: DESCRIPTION,\n  type: \"website\",\n  images: [\n    `/api/og?title=${OG_TITLE}&description=${OG_DESCRIPTION}&footer=${FOOTER}&image=${IMAGE}`,\n  ],\n};\n"
  },
  {
    "path": "apps/dashboard/src/app/not-found.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\n\nexport default function NotFound() {\n  const router = useRouter();\n\n  return (\n    <main className=\"flex min-h-screen w-full flex-col space-y-6 bg-background p-4 md:p-8\">\n      <div className=\"flex flex-1 flex-col items-center justify-center gap-8\">\n        <div className=\"mx-auto max-w-xl rounded-lg border bg-card text-center\">\n          <div className=\"flex flex-col gap-4 p-6 sm:p-12\">\n            <div className=\"flex flex-col gap-1\">\n              <p className=\"font-mono text-foreground\">404 Page not found</p>\n              <h2 className=\"font-cal text-2xl text-foreground\">\n                Oops, something went wrong.\n              </h2>\n              <p className=\"text-muted-foreground text-sm sm:text-base\">\n                The page you are looking for doesn&apos;t exist.\n              </p>\n            </div>\n            <div className=\"flex flex-col items-center justify-center gap-4 sm:flex-row\">\n              <Button\n                variant=\"outline\"\n                size=\"lg\"\n                onClick={router.back}\n                className=\"cursor-pointer\"\n              >\n                Go Back\n              </Button>\n              <Button size=\"lg\" asChild>\n                <Link href=\"/\">Home</Link>\n              </Button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/react-table.d.ts",
    "content": "import \"@tanstack/react-table\";\n\ndeclare module \"@tanstack/react-table\" {\n  interface ColumnMeta {\n    headerClassName?: string;\n    cellClassName?: string;\n  }\n}\n"
  },
  {
    "path": "apps/dashboard/src/app/robots.ts",
    "content": "import type { MetadataRoute } from \"next\";\n\nexport default function robots(): MetadataRoute.Robots {\n  return {\n    rules: {\n      userAgent: \"*\",\n      disallow: \"/\",\n    },\n  };\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/chart/chart-area-latency.tsx",
    "content": "\"use client\";\n\nimport {\n  Area,\n  AreaChart,\n  CartesianGrid,\n  Label,\n  ReferenceLine,\n  XAxis,\n  YAxis,\n} from \"recharts\";\n\nimport { type PERCENTILES, mapLatency } from \"@/data/metrics.client\";\nimport { periodToFromDate } from \"@/data/metrics.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartLegend,\n  ChartLegendContent,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { endOfDay } from \"date-fns\";\nimport { ChartTooltipNumber } from \"./chart-tooltip-number\";\n\nconst chartConfig = {\n  latency: {\n    label: \"Latency\",\n    color: \"var(--success)\",\n  },\n} satisfies ChartConfig;\n\n// TODO: create new pipes for timing phase metrics\n\nexport function ChartAreaLatency({\n  monitorId,\n  degradedAfter,\n  period,\n  type,\n  percentile,\n  regions,\n}: {\n  monitorId: string;\n  degradedAfter: number | null;\n  percentile: (typeof PERCENTILES)[number];\n  period: \"1d\" | \"7d\" | \"14d\";\n  type: \"http\" | \"tcp\";\n  regions: string[] | undefined;\n}) {\n  const trpc = useTRPC();\n  const fromDate = periodToFromDate[period];\n  const toDate = endOfDay(new Date());\n\n  const { data: latency } = useQuery(\n    trpc.tinybird.metricsLatency.queryOptions({\n      monitorId,\n      period,\n      type,\n      regions,\n      fromDate: fromDate.toISOString(),\n      toDate: toDate.toISOString(),\n    }),\n  );\n\n  const refinedLatency = latency ? mapLatency(latency, percentile) : [];\n\n  return (\n    <ChartContainer config={chartConfig} className=\"h-[250px] w-full\">\n      <AreaChart accessibilityLayer data={refinedLatency}>\n        <CartesianGrid vertical={false} />\n        <XAxis\n          dataKey=\"timestamp\"\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          //   tickFormatter={(value) => value.slice(0, 3)}\n        />\n        <ChartTooltip\n          cursor={false}\n          content={\n            <ChartTooltipContent\n              indicator=\"dot\"\n              formatter={(value, name) => (\n                <ChartTooltipNumber\n                  chartConfig={chartConfig}\n                  value={value}\n                  name={name}\n                />\n              )}\n            />\n          }\n        />\n        <Area\n          dataKey=\"latency\"\n          type=\"monotone\"\n          fill=\"var(--color-latency)\"\n          fillOpacity={0.4}\n          stroke=\"var(--color-latency)\"\n          stackId=\"a\"\n        />\n        {degradedAfter ? (\n          <ReferenceLine\n            y={degradedAfter}\n            stroke=\"var(--warning)\"\n            strokeDasharray=\"3 3\"\n          >\n            <Label\n              value=\"Degraded\"\n              position=\"insideBottomRight\"\n              fill=\"var(--warning)\"\n            />\n          </ReferenceLine>\n        ) : null}\n        <YAxis\n          domain={[\"dataMin\", \"dataMax\"]}\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          orientation=\"right\"\n          tickFormatter={(value) => `${value}ms`}\n        />\n        <ChartLegend content={<ChartLegendContent />} />\n      </AreaChart>\n    </ChartContainer>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/chart/chart-area-timing-phases.tsx",
    "content": "\"use client\";\n\nimport {\n  Area,\n  AreaChart,\n  CartesianGrid,\n  Label,\n  ReferenceLine,\n  XAxis,\n  YAxis,\n} from \"recharts\";\n\nimport {\n  type INTERVALS,\n  type PERCENTILES,\n  mapTimingPhases,\n} from \"@/data/metrics.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartLegend,\n  ChartLegendContent,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport {\n  ChartTooltipNumber,\n  ChartTooltipNumberRaw,\n} from \"./chart-tooltip-number\";\n\nconst chartConfig = {\n  dns: {\n    label: \"DNS\",\n    color: \"var(--chart-1)\",\n  },\n  connect: {\n    label: \"Connect\",\n    color: \"var(--chart-2)\",\n  },\n  tls: {\n    label: \"TLS\",\n    color: \"var(--chart-3)\",\n  },\n  ttfb: {\n    label: \"TTFB\",\n    color: \"var(--chart-4)\",\n  },\n  transfer: {\n    label: \"Transfer\",\n    color: \"var(--chart-5)\",\n  },\n} satisfies ChartConfig;\n\n// TODO: create new pipes for timing phase metrics\n\nexport function ChartAreaTimingPhases({\n  monitorId,\n  degradedAfter,\n  period,\n  percentile,\n  interval,\n  type,\n  regions,\n}: {\n  monitorId: string;\n  degradedAfter: number | null;\n  period: \"1d\" | \"7d\" | \"14d\";\n  percentile: (typeof PERCENTILES)[number];\n  interval: (typeof INTERVALS)[number];\n  regions: string[] | undefined;\n  type: \"http\";\n}) {\n  const trpc = useTRPC();\n\n  const { data: timingPhases } = useQuery(\n    trpc.tinybird.metricsTimingPhases.queryOptions({\n      monitorId,\n      period,\n      type,\n      interval,\n      regions,\n    }),\n  );\n\n  const refinedTimingPhases = timingPhases\n    ? mapTimingPhases(timingPhases, percentile)\n    : [];\n\n  return (\n    <ChartContainer config={chartConfig} className=\"h-[250px] w-full\">\n      <AreaChart accessibilityLayer data={refinedTimingPhases}>\n        <CartesianGrid vertical={false} />\n        <XAxis\n          dataKey=\"timestamp\"\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          //   tickFormatter={(value) => value.slice(0, 3)}\n        />\n        <ChartTooltip\n          cursor={false}\n          content={\n            <ChartTooltipContent\n              indicator=\"dot\"\n              formatter={(value, name, item, index) => {\n                if (index !== 4) {\n                  return (\n                    <ChartTooltipNumber\n                      chartConfig={chartConfig}\n                      value={value}\n                      name={name}\n                    />\n                  );\n                }\n\n                const total =\n                  item.payload?.dns +\n                  item.payload?.connect +\n                  item.payload?.tls +\n                  item.payload?.ttfb +\n                  item.payload?.transfer;\n\n                return (\n                  <>\n                    <ChartTooltipNumber\n                      chartConfig={chartConfig}\n                      value={value}\n                      name={name}\n                    />\n                    <ChartTooltipNumberRaw\n                      value={total}\n                      label=\"Total\"\n                      className=\"flex h-0 basis-full items-center border-t font-medium text-foreground text-xs\"\n                    />\n                  </>\n                );\n              }}\n            />\n          }\n        />\n        <Area\n          dataKey=\"dns\"\n          type=\"monotone\"\n          fill=\"var(--color-dns)\"\n          fillOpacity={0.4}\n          stroke=\"var(--color-dns)\"\n          stackId=\"a\"\n        />\n        <Area\n          dataKey=\"connect\"\n          type=\"monotone\"\n          fill=\"var(--color-connect)\"\n          fillOpacity={0.4}\n          stroke=\"var(--color-connect)\"\n          stackId=\"a\"\n        />\n        <Area\n          dataKey=\"tls\"\n          type=\"monotone\"\n          fill=\"var(--color-tls)\"\n          fillOpacity={0.4}\n          stroke=\"var(--color-tls)\"\n          stackId=\"a\"\n        />\n        <Area\n          dataKey=\"ttfb\"\n          type=\"monotone\"\n          fill=\"var(--color-ttfb)\"\n          fillOpacity={0.4}\n          stroke=\"var(--color-ttfb)\"\n          stackId=\"a\"\n        />\n        <Area\n          dataKey=\"transfer\"\n          type=\"monotone\"\n          fill=\"var(--color-transfer)\"\n          fillOpacity={0.4}\n          stroke=\"var(--color-transfer)\"\n          stackId=\"a\"\n        />\n        {degradedAfter ? (\n          <ReferenceLine\n            y={degradedAfter}\n            stroke=\"var(--warning)\"\n            strokeDasharray=\"3 3\"\n          >\n            <Label\n              value=\"Degraded\"\n              position=\"insideBottomRight\"\n              fill=\"var(--warning)\"\n            />\n          </ReferenceLine>\n        ) : null}\n        <YAxis\n          domain={[\"dataMin\", \"dataMax\"]}\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          orientation=\"right\"\n          tickFormatter={(value) => `${value}ms`}\n        />\n        <ChartLegend content={<ChartLegendContent />} />\n      </AreaChart>\n    </ChartContainer>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/chart/chart-bar-uptime-light.tsx",
    "content": "\"use client\";\n\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { Bar, BarChart, XAxis } from \"recharts\";\n\nimport { mapUptime } from \"@/data/metrics.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { Region } from \"@openstatus/db/src/schema/constants\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { useQuery } from \"@tanstack/react-query\";\n// import { startOfDay, subDays } from \"date-fns\";\n\nconst chartConfig = {\n  ok: {\n    label: \"Success\",\n    color: \"var(--color-success)\",\n  },\n  degraded: {\n    label: \"Degraded\",\n    color: \"var(--color-warning)\",\n  },\n  error: {\n    label: \"Error\",\n    color: \"var(--color-destructive)\",\n  },\n} satisfies ChartConfig;\n\nexport function ChartBarUptimeLight({\n  monitorId,\n  type,\n  regions,\n}: {\n  monitorId: string;\n  type: \"http\" | \"tcp\";\n  regions?: Region[];\n}) {\n  const trpc = useTRPC();\n\n  const { data: uptime, isLoading } = useQuery(\n    trpc.tinybird.uptime.queryOptions({\n      interval: 60 * 24,\n      //   fromDate: startOfDay(subDays(new Date(), 7)).toISOString(), // FIXME:\n      period: \"7d\",\n      monitorId,\n      regions,\n      type,\n    }),\n  );\n\n  if (isLoading) {\n    return <Skeleton className=\" my-auto h-5 w-full\" />;\n  }\n\n  const refinedUptime = uptime ? mapUptime(uptime) : [];\n\n  if (refinedUptime.length === 0) {\n    return <span className=\"text-muted-foreground\">-</span>;\n  }\n\n  return (\n    <ChartContainer config={chartConfig} className=\"h-[28px] w-full\">\n      <BarChart accessibilityLayer data={refinedUptime} barCategoryGap={1}>\n        <ChartTooltip\n          cursor={false}\n          allowEscapeViewBox={{ x: false, y: true }}\n          wrapperStyle={{ zIndex: 1 }}\n          content={<ChartTooltipContent indicator=\"dot\" />}\n        />\n        <Bar dataKey=\"ok\" stackId=\"a\" fill=\"var(--color-ok)\" />\n        <Bar dataKey=\"error\" stackId=\"a\" fill=\"var(--color-error)\" />\n        <Bar dataKey=\"degraded\" stackId=\"a\" fill=\"var(--color-degraded)\" />\n        <XAxis\n          dataKey=\"interval\"\n          tickLine={false}\n          tickMargin={8}\n          minTickGap={10}\n          axisLine={false}\n          hide\n        />\n      </BarChart>\n    </ChartContainer>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/chart/chart-bar-uptime.tsx",
    "content": "\"use client\";\n\nimport { Bar, BarChart, CartesianGrid, XAxis, YAxis } from \"recharts\";\n\nimport {\n  type PERIODS,\n  mapUptime,\n  periodToFromDate,\n  periodToInterval,\n} from \"@/data/metrics.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartLegend,\n  ChartLegendContent,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { endOfDay } from \"date-fns\";\n\nconst chartConfig = {\n  ok: {\n    label: \"Success\",\n    color: \"var(--color-success)\",\n  },\n  degraded: {\n    label: \"Degraded\",\n    color: \"var(--color-warning)\",\n  },\n  error: {\n    label: \"Error\",\n    color: \"var(--color-destructive)\",\n  },\n} satisfies ChartConfig;\n\nexport function ChartBarUptime({\n  monitorId,\n  period,\n  type,\n  regions,\n}: {\n  monitorId: string;\n  period: (typeof PERIODS)[number];\n  type: \"http\" | \"tcp\";\n  regions: string[] | undefined;\n}) {\n  const isMobile = useIsMobile();\n  const trpc = useTRPC();\n  const fromDate = periodToFromDate[period];\n  const toDate = endOfDay(new Date());\n  const interval = periodToInterval[period];\n\n  const { data: uptime } = useQuery(\n    trpc.tinybird.uptime.queryOptions({\n      monitorId,\n      fromDate: fromDate.toISOString(),\n      toDate: toDate.toISOString(),\n      regions,\n      interval,\n      type,\n    }),\n  );\n\n  const refinedUptime = uptime ? mapUptime(uptime) : [];\n\n  return (\n    <ChartContainer config={chartConfig} className=\"h-[130px] w-full\">\n      <BarChart\n        accessibilityLayer\n        data={refinedUptime}\n        barCategoryGap={isMobile ? 0 : 2}\n      >\n        <CartesianGrid vertical={false} />\n        <ChartTooltip\n          cursor={false}\n          content={<ChartTooltipContent indicator=\"dot\" />}\n        />\n        <Bar dataKey=\"ok\" stackId=\"a\" fill=\"var(--color-ok)\" />\n        <Bar dataKey=\"error\" stackId=\"a\" fill=\"var(--color-error)\" />\n        <Bar dataKey=\"degraded\" stackId=\"a\" fill=\"var(--color-degraded)\" />\n        <YAxis\n          domain={[\"dataMin\", \"dataMax\"]}\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          orientation=\"right\"\n        />\n        <XAxis\n          dataKey=\"interval\"\n          tickLine={false}\n          tickMargin={8}\n          minTickGap={10}\n          axisLine={false}\n        />\n        <ChartLegend content={<ChartLegendContent />} />\n      </BarChart>\n    </ChartContainer>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/chart/chart-line-region.tsx",
    "content": "\"use client\";\n\nimport { CartesianGrid, Line, LineChart, XAxis, YAxis } from \"recharts\";\n\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { ChartTooltipNumber } from \"./chart-tooltip-number\";\n\nconst chartConfig = {\n  latency: {\n    label: \"Latency\",\n    color: \"var(--success)\",\n  },\n} satisfies ChartConfig;\n\nexport type TrendPoint = {\n  timestamp: number; // unix millis\n  latency: number; // milliseconds\n};\n\nexport function ChartLineRegion({\n  className,\n  data,\n}: {\n  className?: string;\n  data: TrendPoint[];\n}) {\n  const trendData = data ?? [];\n\n  const chartData = trendData.map((d) => ({\n    timestamp: new Date(d.timestamp).toLocaleString(\"default\", {\n      hour: \"numeric\",\n      minute: \"numeric\",\n      day: \"numeric\",\n      month: \"short\",\n    }),\n    latency: d.latency,\n  }));\n\n  return (\n    <ChartContainer\n      config={chartConfig}\n      className={cn(\"h-[100px] w-full\", className)}\n    >\n      <LineChart\n        accessibilityLayer\n        data={chartData}\n        margin={{\n          left: 12,\n          right: 12,\n        }}\n      >\n        <CartesianGrid vertical={false} />\n        <XAxis dataKey=\"timestamp\" hide />\n        <ChartTooltip\n          cursor={false}\n          content={\n            <ChartTooltipContent\n              className=\"w-[180px]\"\n              formatter={(value, name) => (\n                <ChartTooltipNumber\n                  chartConfig={chartConfig}\n                  value={value}\n                  name={name}\n                />\n              )}\n            />\n          }\n        />\n        <Line\n          dataKey=\"latency\"\n          type=\"monotone\"\n          stroke=\"var(--color-latency)\"\n          strokeWidth={2}\n          dot={false}\n        />\n        <YAxis\n          domain={[\"dataMin\", \"dataMax\"]}\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          orientation=\"right\"\n          tickFormatter={(value) => `${value}ms`}\n        />\n      </LineChart>\n    </ChartContainer>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/chart/chart-line-regions.tsx",
    "content": "\"use client\";\n\nimport { CartesianGrid, Line, LineChart, XAxis, YAxis } from \"recharts\";\n\nimport { getRegionColor } from \"@/data/regions\";\nimport { cn } from \"@/lib/utils\";\nimport type { PrivateLocation } from \"@openstatus/db/src/schema\";\nimport { monitorRegions } from \"@openstatus/db/src/schema/constants\";\nimport { getRegionInfo } from \"@openstatus/regions\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartLegend,\n  ChartLegendContent,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { ChartTooltipNumber } from \"./chart-tooltip-number\";\n\nfunction getChartConfig(privateLocations?: PrivateLocation[]) {\n  return [\n    ...monitorRegions,\n    ...(privateLocations?.map((location) => location.id.toString()) ?? []),\n  ].reduce((config, region) => {\n    const privateLocation = privateLocations?.find(\n      (location) => String(location.id) === String(region),\n    );\n    const regionInfo = getRegionInfo(region, {\n      location: privateLocation?.name,\n    });\n    const color = getRegionColor(region);\n\n    if (regionInfo && color) {\n      config[region] = {\n        label: `${regionInfo.location} (${regionInfo.provider})`,\n        color,\n      };\n    }\n\n    return config;\n  }, {} as ChartConfig) satisfies ChartConfig;\n}\n\nexport type TrendPoint = {\n  timestamp: number; // unix millis\n  [key: string]: number; // milliseconds\n};\n\nexport function ChartLineRegions({\n  className,\n  data,\n  regions,\n  privateLocations,\n}: {\n  className?: string;\n  data: TrendPoint[];\n  regions: string[] | undefined;\n  privateLocations?: PrivateLocation[];\n}) {\n  const isMobile = useIsMobile();\n  const trendData = data ?? [];\n  const chartConfig = getChartConfig(privateLocations);\n  const chartData = trendData.map((d) => ({\n    ...d,\n    timestamp: new Date(d.timestamp).toLocaleString(\"default\", {\n      hour: \"numeric\",\n      minute: \"numeric\",\n      day: \"numeric\",\n      month: \"short\",\n    }),\n  }));\n\n  return (\n    <ChartContainer\n      config={chartConfig}\n      className={cn(\"h-[250px] w-full\", className)}\n    >\n      <LineChart\n        accessibilityLayer\n        data={chartData}\n        margin={{\n          left: 12,\n          right: 12,\n        }}\n      >\n        <CartesianGrid vertical={false} />\n        <XAxis dataKey=\"timestamp\" />\n        <ChartTooltip\n          cursor={false}\n          content={\n            <ChartTooltipContent\n              formatter={(value, name) => (\n                <ChartTooltipNumber\n                  chartConfig={chartConfig}\n                  value={value}\n                  name={name}\n                />\n              )}\n            />\n          }\n        />\n        {regions?.map((region) => {\n          return (\n            <Line\n              key={region}\n              dataKey={region}\n              type=\"monotone\"\n              stroke={`var(--color-${region})`}\n              strokeWidth={2}\n              dot={false}\n            />\n          );\n        })}\n        <YAxis\n          domain={[\"dataMin\", \"dataMax\"]}\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          orientation=\"right\"\n          tickFormatter={(value) => `${value}ms`}\n        />\n        {regions && regions.length <= 6 && !isMobile ? (\n          <ChartLegend\n            className=\"flex-wrap\"\n            content={<ChartLegendContent className=\"text-nowrap\" />}\n          />\n        ) : null}\n      </LineChart>\n    </ChartContainer>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/chart/chart-tooltip-number.tsx",
    "content": "import type { ChartConfig } from \"@openstatus/ui/components/ui/chart\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport type {\n  NameType,\n  ValueType,\n} from \"recharts/types/component/DefaultTooltipContent\";\n\ninterface ChartTooltipNumberProps {\n  chartConfig: ChartConfig;\n  value: ValueType;\n  name: NameType;\n}\n\nexport function ChartTooltipNumber({\n  value,\n  name,\n  chartConfig,\n}: ChartTooltipNumberProps) {\n  return (\n    <ChartTooltipNumberRaw\n      value={value}\n      label={chartConfig[name as keyof typeof chartConfig]?.label || name}\n      style={\n        {\n          \"--color-bg\": `var(--color-${name})`,\n        } as React.CSSProperties\n      }\n    />\n  );\n}\n\nexport function ChartTooltipNumberRaw({\n  value,\n  label,\n  style,\n  className,\n}: {\n  value: ValueType;\n  label: React.ReactNode;\n  style?: React.CSSProperties;\n  className?: string;\n}) {\n  return (\n    <>\n      <div\n        className={cn(\n          \"h-2.5 w-2.5 shrink-0 rounded-[2px] bg-(--color-bg)\",\n          className,\n        )}\n        style={style}\n      />\n      <span>{label}</span>\n      <div className=\"ml-auto flex items-baseline gap-0.5 font-medium font-mono text-foreground tabular-nums\">\n        {value}\n        <span className=\"font-normal text-muted-foreground\">ms</span>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/common/code.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Check, Copy } from \"lucide-react\";\n\nexport function Code({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"pre\">) {\n  const { copy, isCopied } = useCopyToClipboard();\n\n  return (\n    <div className=\"relative\">\n      <pre\n        className={cn(\n          \"overflow-x-auto rounded-md border bg-muted p-2 text-xs\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </pre>\n      <Button\n        variant=\"outline\"\n        size=\"icon\"\n        className=\"absolute top-1 right-1 size-6 p-1 backdrop-blur-md\"\n        onClick={() =>\n          copy(children?.toString() ?? \"\", {\n            withToast: false,\n            timeout: 1000,\n          })\n        }\n      >\n        {isCopied ? <Check className=\"size-3\" /> : <Copy className=\"size-3\" />}\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/common/hover-card-timestamp.tsx",
    "content": "\"use client\";\n\nimport { UTCDate } from \"@date-fns/utc\";\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@openstatus/ui/components/ui/hover-card\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { format, formatDistanceToNowStrict } from \"date-fns\";\nimport { Copy } from \"lucide-react\";\nimport { Check } from \"lucide-react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\n\n// TODO: move to TableCellDate?\n\ntype HoverCardContentProps = ComponentPropsWithoutRef<typeof HoverCardContent>;\n\ninterface HoverCardTimestampProps {\n  date: Date;\n  side?: HoverCardContentProps[\"side\"];\n  sideOffset?: HoverCardContentProps[\"sideOffset\"];\n  align?: HoverCardContentProps[\"align\"];\n  alignOffset?: HoverCardContentProps[\"alignOffset\"];\n  children?: React.ReactNode;\n}\n\nexport function HoverCardTimestamp({\n  date,\n  side = \"right\",\n  align = \"start\",\n  alignOffset = -4,\n  sideOffset,\n  children,\n}: HoverCardTimestampProps) {\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\n  return (\n    <HoverCard openDelay={0} closeDelay={0}>\n      <HoverCardTrigger asChild>{children}</HoverCardTrigger>\n      <HoverCardContent\n        className=\"z-10 w-auto p-2\"\n        {...{ side, align, alignOffset, sideOffset }}\n      >\n        <dl className=\"flex flex-col gap-1\">\n          <Row value={String(date.getTime())} label=\"Timestamp\" />\n          <Row\n            value={format(new UTCDate(date), \"LLL dd, y HH:mm:ss\")}\n            label=\"UTC\"\n          />\n          <Row value={format(date, \"LLL dd, y HH:mm:ss\")} label={timezone} />\n          <Row\n            value={formatDistanceToNowStrict(date, { addSuffix: true })}\n            label=\"Relative\"\n          />\n        </dl>\n      </HoverCardContent>\n    </HoverCard>\n  );\n}\n\nfunction Row({ value, label }: { value: string; label: string }) {\n  const { copy, isCopied } = useCopyToClipboard();\n\n  return (\n    <div\n      className=\"group flex items-center justify-between gap-4 text-sm\"\n      onClick={(e) => {\n        e.stopPropagation();\n        copy(value, {});\n      }}\n    >\n      <dt className=\"text-muted-foreground\">{label}</dt>\n      <dd className=\"flex items-center gap-1 truncate font-mono\">\n        <span className=\"invisible group-hover:visible\">\n          {!isCopied ? (\n            <Copy className=\"h-3 w-3\" />\n          ) : (\n            <Check className=\"h-3 w-3\" />\n          )}\n        </span>\n        {value}\n      </dd>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/common/icon-cloud-provider.tsx",
    "content": "import { Fly, Koyeb, Railway } from \"@openstatus/icons\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Globe } from \"lucide-react\";\n\nexport function IconCloudProvider({\n  provider,\n  className,\n}: React.ComponentProps<\"svg\"> & {\n  provider: string;\n}) {\n  switch (provider) {\n    case \"fly\":\n      return <Fly className={cn(\"size-4\", className)} />;\n    case \"koyeb\":\n      return <Koyeb className={cn(\"size-4\", className)} />;\n    case \"railway\":\n      return <Railway className={cn(\"size-4\", className)} />;\n    default:\n      return <Globe className={cn(\"size-4\", className)} />;\n  }\n}\n\nexport function IconCloudProviderTooltip(\n  props: React.ComponentProps<typeof IconCloudProvider>,\n) {\n  return (\n    <TooltipProvider>\n      <Tooltip delayDuration={0}>\n        <TooltipTrigger\n          type=\"button\"\n          className=\"rounded-sm outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-offset-2\"\n        >\n          <IconCloudProvider {...props} />\n        </TooltipTrigger>\n        <TooltipContent className=\"capitalize\">{props.provider}</TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/common/input-with-addons.tsx",
    "content": "import * as React from \"react\";\n\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport interface InputWithAddonsProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {\n  leading?: React.ReactNode;\n  trailing?: React.ReactNode;\n}\n\n// \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n//         \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n//         \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n\nconst InputWithAddons = React.forwardRef<\n  HTMLInputElement,\n  InputWithAddonsProps\n>(({ leading, trailing, className, ...props }, ref) => {\n  return (\n    <div className={cn(\"flex rounded-md shadow-xs\", className)}>\n      {leading ? (\n        <span className=\"inline-flex items-center rounded-s-md border border-input bg-muted px-3 text-muted-foreground text-sm\">\n          {leading}\n        </span>\n      ) : null}\n      <Input\n        ref={ref}\n        className={cn(\"z-1 shadow-none\", {\n          \"-ms-px rounded-s-none\": leading,\n          \"-me-px rounded-e-none\": trailing,\n        })}\n        placeholder=\"google.com\"\n        type=\"text\"\n        {...props}\n      />\n      {trailing ? (\n        <span className=\"inline-flex items-center rounded-e-md border border-input bg-muted px-3 text-muted-foreground text-sm\">\n          {trailing}\n        </span>\n      ) : null}\n    </div>\n  );\n});\n\nInputWithAddons.displayName = \"InputWithAddons\";\n\nexport { InputWithAddons };\n"
  },
  {
    "path": "apps/dashboard/src/components/common/kbd.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nconst kbdVariants = cva(\n  \"-me-1 ms-2 inline-flex h-5 max-h-full items-center rounded border px-1 font-[inherit] font-medium text-[0.625rem]\",\n  {\n    variants: {\n      variant: {\n        default: \"border-input bg-background text-muted-foreground/70\",\n        secondary: \"bg-secondary text-secondary-foreground\",\n        ghost: \"border-transparent\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport function Kbd({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"kbd\"> & VariantProps<typeof kbdVariants>) {\n  return (\n    <kbd className={cn(kbdVariants({ variant, className }))} {...props}>\n      {children}\n    </kbd>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/common/link.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport NextLink from \"next/link\";\n\n// TODO: we could add cva variants for the link\n\nexport function Link({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof NextLink>) {\n  const isExternal = props.href?.toString().startsWith(\"http\");\n  const externalProps = isExternal\n    ? { target: \"_blank\", rel: \"noopener noreferrer\" }\n    : {};\n\n  return (\n    <NextLink\n      className={cn(\"font-medium text-foreground\", className)}\n      {...externalProps}\n      {...props}\n    >\n      {children}\n    </NextLink>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/common/note.tsx",
    "content": "import { Button } from \"@openstatus/ui/components/ui/button\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\n\nconst noteVariants = cva(\n  \"flex items-center gap-2 rounded-xl border [&>svg]:text-current [&>svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"border-border\",\n        ghost: \"border-none bg-transparent\",\n      },\n      color: {\n        default: \"text-foreground bg-sidebar\",\n        warning: \"text-warning border-warning/50 bg-warning/5\",\n        error: \"text-destructive border-destructive/50 bg-destructive/5\",\n        success: \"text-success border-success/50 bg-success/5\",\n        info: \"text-info border-info/50 bg-info/5\",\n      },\n      size: {\n        default: \"px-3 py-2 text-base [&>svg]:size-4\",\n        sm: \"px-2.5 py-1.5 text-sm [&>svg]:size-3.5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      color: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport function Note({\n  children,\n  className,\n  variant = \"default\",\n  color = \"default\",\n  size = \"default\",\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof noteVariants>) {\n  return (\n    <div\n      data-variant={variant}\n      className={cn(noteVariants({ variant, color, size, className }))}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function NoteButton({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  return (\n    <Button\n      size=\"sm\"\n      className={cn(\"-mr-1 ml-auto shrink-0\", className)}\n      {...props}\n    >\n      {children}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/common/wheel-picker.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Context -------------------------------------------------------------------------------------------------------------\n// ---------------------------------------------------------------------------------------------------------------------\n\ninterface WheelPickerContextValue {\n  items: string[];\n  currentIndex: number;\n  onIndexChange: (index: number) => void;\n  theta: number; // angle between two items\n  radius: number; // translateZ distance\n  count: number; // items including placeholders\n}\n\nconst WheelPickerContext = React.createContext<WheelPickerContextValue | null>(\n  null,\n);\n\nconst useWheelPickerContext = () => {\n  const ctx = React.useContext(WheelPickerContext);\n  if (!ctx) {\n    throw new Error(\n      \"[WheelPicker] sub component must be rendered within <WheelPicker /> root\",\n    );\n  }\n  return ctx;\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n// WheelPicker (Root / Provider) ---------------------------------------------------------------------------------------\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface WheelPickerProps extends React.HTMLAttributes<HTMLDivElement> {\n  items: string[];\n  /** 0-based index of the currently selected item. Defaults to 0. */\n  currentIndex: number;\n  /** Callback that is invoked with the currently selected item (string) whenever the selection changes */\n  onIndexChange: (index: number) => void;\n  /** Radius (in `px`) of the carousel – tweak to fit line height of text (Default: 28) */\n  radius?: number;\n}\n\nconst WheelPicker = React.forwardRef<HTMLDivElement, WheelPickerProps>(\n  (\n    {\n      items,\n      currentIndex,\n      onIndexChange,\n      radius: radiusProp = 28,\n      className,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    // internal render count includes two placeholders at start & end\n    const count = items.length + 2;\n    const theta = (2 * Math.PI) / count;\n\n    const contextValue = React.useMemo<WheelPickerContextValue>(\n      () => ({\n        items,\n        currentIndex,\n        onIndexChange,\n        theta,\n        radius: radiusProp,\n        count,\n      }),\n      [items, currentIndex, onIndexChange, theta, radiusProp, count],\n    );\n\n    return (\n      <WheelPickerContext.Provider value={contextValue}>\n        <div ref={ref} className={className} {...props}>\n          {children}\n        </div>\n      </WheelPickerContext.Provider>\n    );\n  },\n);\nWheelPicker.displayName = \"WheelPicker\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n// WheelPickerSelect ---------------------------------------------------------------------------------------------------\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type WheelPickerSelectProps = React.HTMLAttributes<HTMLDivElement>;\n\nconst WheelPickerSelect = React.forwardRef<\n  HTMLDivElement,\n  WheelPickerSelectProps\n>(({ className, children, ...props }, ref) => {\n  const { items, currentIndex, onIndexChange } = useWheelPickerContext();\n\n  const moveBy = React.useCallback(\n    (delta: number) => {\n      const itemsLength = items.length;\n      let newIndex = currentIndex + delta;\n\n      // Handle wrapping\n      if (newIndex < 0) {\n        newIndex = itemsLength - 1;\n      } else if (newIndex >= itemsLength) {\n        newIndex = 0;\n      }\n\n      onIndexChange(newIndex);\n    },\n    [currentIndex, items.length, onIndexChange],\n  );\n\n  const handleKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLDivElement>) => {\n      switch (e.key) {\n        case \"ArrowUp\":\n        case \"ArrowLeft\":\n          e.preventDefault();\n          moveBy(1);\n          break;\n        case \"ArrowDown\":\n        case \"ArrowRight\":\n          e.preventDefault();\n          moveBy(-1);\n          break;\n        case \"Home\":\n          e.preventDefault();\n          onIndexChange(0);\n          break;\n        case \"End\":\n          e.preventDefault();\n          onIndexChange(items.length - 1);\n          break;\n      }\n    },\n    [moveBy, onIndexChange, items.length],\n  );\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"wheel-select\"\n      role=\"listbox\"\n      aria-label=\"Select option\"\n      aria-activedescendant={`wheel-option-${currentIndex}`}\n      tabIndex={0}\n      className={cn(\n        \"relative h-6 w-full rounded-md border border-transparent text-left focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/50\",\n        className,\n      )}\n      onKeyDown={handleKeyDown}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n});\nWheelPickerSelect.displayName = \"WheelPickerSelect\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n// WheelPickerOptions --------------------------------------------------------------------------------------------------\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type WheelPickerOptionsProps = React.HTMLAttributes<HTMLDivElement>;\n\nconst WheelPickerOptions = React.forwardRef<\n  HTMLDivElement,\n  WheelPickerOptionsProps\n>(({ className, ...props }, ref) => {\n  const { items, currentIndex, onIndexChange, theta, radius } =\n    useWheelPickerContext();\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"wheel-options\"\n      className={cn(\n        \"h-full w-full rounded-md [perspective:1000px] [transform-style:preserve-3d]\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        className=\"relative h-full w-full transition-transform duration-500 ease-out [transform-style:preserve-3d]\"\n        style={{\n          transform: `translateZ(-${radius}px) rotateX(${\n            -(currentIndex + 1) * theta\n          }rad)`,\n        }}\n      >\n        {/* First placeholder */}\n        <WheelPickerEmpty position=\"first\" />\n\n        {/* Real items */}\n        {items.map((item, idx) => {\n          // Render index = item index + 1 (accounting for first placeholder)\n          const renderIdx = idx + 1;\n          const angle = theta * renderIdx;\n          const isSelected = idx === currentIndex;\n          return (\n            <div\n              key={item}\n              data-slot=\"wheel-option\"\n              id={`wheel-option-${idx}`}\n              role=\"option\"\n              aria-selected={isSelected}\n              className={cn(\n                \"absolute inset-0 flex cursor-pointer select-none items-center justify-start transition-transform duration-500 ease-out [backface-visibility:hidden]\",\n              )}\n              style={{\n                transform: `rotateX(${angle}rad) translateZ(${radius}px)`,\n                transformStyle: \"preserve-3d\",\n              }}\n              onClick={(e) => {\n                if (!isSelected) {\n                  e.preventDefault();\n                  e.stopPropagation();\n                  onIndexChange(idx);\n                }\n              }}\n            >\n              <span\n                className={cn(\n                  \"text-xs transition-colors\",\n                  isSelected ? \"text-foreground\" : \"text-muted-foreground/70\",\n                )}\n              >\n                {item}\n              </span>\n            </div>\n          );\n        })}\n\n        {/* Last placeholder */}\n        <WheelPickerEmpty position=\"last\" />\n      </div>\n    </div>\n  );\n});\nWheelPickerOptions.displayName = \"WheelPickerOptions\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n// WheelPickerEmpty (placeholders) -------------------------------------------------------------------------------------\n// ---------------------------------------------------------------------------------------------------------------------\n\ntype PlaceholderPosition = \"first\" | \"last\";\n\nexport interface WheelPickerEmptyProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  position: PlaceholderPosition;\n}\n\nconst WheelPickerEmpty = React.forwardRef<\n  HTMLDivElement,\n  WheelPickerEmptyProps\n>(({ position, className, ...props }, ref) => {\n  const { theta, radius, count, currentIndex } = useWheelPickerContext();\n\n  const renderIdx = position === \"first\" ? 0 : count - 1;\n  const angle = theta * renderIdx;\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"wheel-empty\"\n      id={`wheel-option-${renderIdx}`}\n      role=\"option\"\n      aria-selected={currentIndex === renderIdx}\n      aria-disabled\n      className={cn(\n        \"absolute inset-0 select-none [backface-visibility:hidden]\",\n        className,\n      )}\n      style={{\n        transform: `rotateX(${angle}rad) translateZ(${radius}px)`,\n        transformStyle: \"preserve-3d\",\n      }}\n      onClick={(e) => {\n        e.preventDefault();\n        e.stopPropagation();\n      }}\n      {...props}\n    />\n  );\n});\nWheelPickerEmpty.displayName = \"WheelPickerEmpty\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Exports -------------------------------------------------------------------------------------------------------------\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport { WheelPicker, WheelPickerSelect, WheelPickerOptions, WheelPickerEmpty };\n"
  },
  {
    "path": "apps/dashboard/src/components/content/action-card.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@openstatus/ui/components/ui/card\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function ActionCard({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <Card\n      className={cn(\"group/action-card gap-4 py-4 shadow-none\", className)}\n      {...props}\n    >\n      {children}\n    </Card>\n  );\n}\n\nexport function ActionCardHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <CardHeader className={cn(\"px-4 [.border-b]:pb-4\", className)} {...props}>\n      {children}\n    </CardHeader>\n  );\n}\n\nexport function ActionCardGroup({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"grid gap-4\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function ActionCardTitle({\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return <CardTitle {...props}>{children}</CardTitle>;\n}\n\nexport function ActionCardDescription({\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return <CardDescription {...props}>{children}</CardDescription>;\n}\n\nexport function ActionCardContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <CardContent className={cn(\"px-4\", className)} {...props}>\n      {children}\n    </CardContent>\n  );\n}\n\nexport function ActionCardFooter({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <CardFooter className={cn(\"px-4 [.border-t]:pt-4\", className)} {...props}>\n      {children}\n    </CardFooter>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/content/billing-addons.tsx",
    "content": "import {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { useCookieState } from \"@openstatus/ui/hooks/use-cookie-state\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport type { Addons } from \"@openstatus/db/src/schema/plan/schema\";\nimport { getAddonPriceConfig } from \"@openstatus/db/src/schema/plan/utils\";\nimport { ButtonGroup } from \"@openstatus/ui/components/ui/button-group\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { Check, MinusIcon, PlusIcon } from \"lucide-react\";\nimport { useEffect, useState, useTransition } from \"react\";\nimport { toast } from \"sonner\";\n\ntype Workspace = RouterOutputs[\"workspace\"][\"get\"];\n\ninterface BillingAddonsProps {\n  label: string;\n  description: React.ReactNode;\n  addon: keyof Addons;\n  workspace: Workspace;\n}\n\ninterface PriceConfig {\n  value: number;\n  currency: string;\n  locale: string;\n}\n\nexport function BillingAddons({\n  label,\n  description,\n  addon,\n  workspace,\n}: BillingAddonsProps) {\n  const [open, setOpen] = useState(false);\n  const [isPending, startTransition] = useTransition();\n  const [currency] = useCookieState(\"x-currency\", \"USD\");\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const checkoutSessionMutation = useMutation(\n    trpc.stripeRouter.addAddon.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.workspace.get.queryKey(),\n        });\n      },\n    }),\n  );\n  const plan = workspace.plan;\n  const defaultLimit = allPlans[workspace.plan].limits[addon];\n  const workspaceLimit = workspace.limits[addon];\n  const defaultValue =\n    typeof workspaceLimit === \"number\" && typeof defaultLimit === \"number\"\n      ? // current value - default value to evaluate the difference\n        workspaceLimit - defaultLimit\n      : workspaceLimit;\n  const [value, setValue] = useState<number | boolean>(defaultValue);\n  const price = getAddonPriceConfig(plan, addon, currency);\n\n  // Reset value when modal opens\n  useEffect(() => {\n    if (open) {\n      setValue(defaultValue);\n    }\n  }, [open, defaultValue]);\n\n  function submitAction() {\n    startTransition(async () => {\n      try {\n        // toggle the value if it's a boolean otherwise use the value\n        const newValue = typeof value === \"boolean\" ? !value : value;\n        const promise = checkoutSessionMutation.mutateAsync({\n          workspaceSlug: workspace.slug,\n          feature: addon,\n          value: newValue,\n        });\n        toast.promise(promise, {\n          loading: \"Updating...\",\n          success: () => {\n            setOpen(false);\n            return \"Billing information updated\";\n          },\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to update\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n  const hasAddon =\n    typeof defaultValue === \"number\"\n      ? defaultValue > 0\n      : defaultValue !== defaultLimit;\n  const isQuantity = typeof value === \"number\";\n\n  return (\n    <AlertDialog open={open} onOpenChange={setOpen}>\n      <div className=\"flex flex-col gap-2\">\n        <div className=\"grid grid-cols-3 gap-1.5 lg:grid-cols-5\">\n          <div className=\"col-span-3 space-y-0.5 text-sm\">\n            <Label>{label}</Label>\n            <div className=\"text-muted-foreground\">{description}</div>\n          </div>\n          <div className=\"flex items-center gap-1.5\">\n            <span className=\"font-mono text-foreground text-sm\">\n              {formatPrice(price)}\n              {isQuantity ? \"/mo./each\" : \"/mo.\"}\n            </span>\n            {hasAddon && !isQuantity ? (\n              <Check className=\"size-4 text-success\" />\n            ) : null}\n            {hasAddon && isQuantity ? (\n              <span className=\"font-mono text-success\">+{defaultValue}</span>\n            ) : null}\n          </div>\n          <div className=\"col-span-2 flex items-center justify-end gap-1.5 lg:col-span-1\">\n            <AlertDialogTrigger asChild>\n              <Button size=\"sm\" variant=\"secondary\">\n                {getButtonLabel(hasAddon, value)}\n              </Button>\n            </AlertDialogTrigger>\n          </div>\n        </div>\n      </div>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{label}</AlertDialogTitle>\n          <AlertDialogDescription>\n            {getDialogDescription(label, price, value, hasAddon)}\n          </AlertDialogDescription>\n        </AlertDialogHeader>\n        {isQuantity &&\n        typeof value === \"number\" &&\n        typeof defaultLimit === \"number\" ? (\n          <QuantityControl\n            value={value}\n            setValue={setValue}\n            defaultLimit={defaultLimit}\n          />\n        ) : null}\n        <AlertDialogFooter>\n          <AlertDialogCancel>Cancel</AlertDialogCancel>\n          <AlertDialogAction\n            onClick={(e) => {\n              e.preventDefault();\n              submitAction();\n            }}\n            disabled={\n              isPending ||\n              (typeof value === \"number\" &&\n                typeof defaultValue === \"number\" &&\n                value === defaultValue)\n            }\n          >\n            {getButtonLabel(hasAddon, value, isPending)}\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n\n// NOTE: could move to lib/formatter.ts\nfunction formatPrice(price: PriceConfig | null) {\n  if (!price) return \"N/A\";\n  return new Intl.NumberFormat(price.locale, {\n    style: \"currency\",\n    currency: price.currency,\n  }).format(price.value);\n}\n\nfunction getButtonLabel(\n  hasAddon: boolean,\n  value: number | boolean,\n  isPending = false,\n) {\n  if (isPending) return \"Updating...\";\n\n  const isBoolean = typeof value === \"boolean\";\n  const isQuantity = typeof value === \"number\";\n\n  if (isQuantity) return \"Update\";\n\n  if (isBoolean) {\n    return hasAddon ? \"Remove\" : \"Add\";\n  }\n\n  return null;\n}\n\nfunction getDialogDescription(\n  label: string,\n  price: PriceConfig | null,\n  value: number | boolean,\n  hasAddon: boolean,\n) {\n  const formattedPrice = formatPrice(price);\n  const isBoolean = typeof value === \"boolean\";\n  const isQuantity = typeof value === \"number\";\n  const priceSuffix = isQuantity ? \"/mo./each\" : \"/mo.\";\n\n  if (isBoolean) {\n    if (hasAddon) {\n      return `${label} will be removed from your subscription. You will save ${formattedPrice}${priceSuffix} on your next billing cycle.`;\n    }\n    return `${label} will be added to your subscription. You will be charged an additional ${formattedPrice}${priceSuffix} on your next billing cycle.`;\n  }\n\n  if (isQuantity) {\n    return `${label} will be updated to ${value} on your next billing cycle. You will be charged ${formattedPrice}${priceSuffix} on your next billing cycle.`;\n  }\n}\n\nfunction QuantityControl({\n  value,\n  setValue,\n  defaultLimit,\n}: {\n  value: number;\n  setValue: (value: number) => void;\n  defaultLimit: number | boolean;\n}) {\n  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newValue = Number.parseInt(e.target.value);\n    if (Number.isNaN(newValue)) {\n      setValue(typeof defaultLimit === \"number\" ? defaultLimit : 0);\n    } else {\n      setValue(\n        Math.max(typeof defaultLimit === \"number\" ? defaultLimit : 0, newValue),\n      );\n    }\n  };\n\n  return (\n    <div className=\"flex items-center justify-center gap-2 py-2\">\n      <ButtonGroup aria-label=\"Quantity\" className=\"h-fit\">\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          onClick={() => setValue(value - 1)}\n          disabled={value <= 0}\n        >\n          <MinusIcon />\n        </Button>\n        <Input\n          type=\"number\"\n          value={value}\n          className=\"w-16 text-right\"\n          step={1}\n          min={0}\n          onChange={handleChange}\n        />\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          onClick={() => setValue(value + 1)}\n        >\n          <PlusIcon />\n        </Button>\n      </ButtonGroup>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/content/billing-overlay.tsx",
    "content": "import { Button } from \"@openstatus/ui/components/ui/button\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function BillingOverlayContainer({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"relative\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function BillingOverlay({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"absolute inset-0 flex flex-col items-center justify-center gap-2 bg-gradient-to-b from-transparent to-50% to-background p-2\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function BillingOverlayButton({\n  children,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  return (\n    <Button size=\"sm\" {...props}>\n      {children}\n    </Button>\n  );\n}\n\nexport function BillingOverlayDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      className={cn(\n        \"max-w-xs text-center text-muted-foreground text-sm\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </p>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/content/billing-progress.tsx",
    "content": "import { Progress } from \"@openstatus/ui/components/ui/progress\";\n\ninterface BillingProgressProps {\n  label: string;\n  value: number;\n  max: number;\n}\n\nexport function BillingProgress({ label, value, max }: BillingProgressProps) {\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <div className=\"flex flex-col gap-0.5\">\n        <div className=\"flex justify-between text-muted-foreground text-sm\">\n          <div className=\"font-medium\">{label}</div>\n          <div className=\"font-mono\">\n            <span className=\"text-foreground\">\n              {new Intl.NumberFormat(\"de-DE\").format(value)}\n            </span>\n            /{new Intl.NumberFormat(\"de-DE\").format(max)}\n          </div>\n        </div>\n        <Progress value={(value / max) * 100} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/content/block-wrapper.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@openstatus/ui/components/ui/collapsible\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function BlockWrapper({\n  className,\n  children,\n  autoOpen,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & {\n  autoOpen?: boolean;\n}) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  const [isOpened, setIsOpened] = React.useState(false);\n\n  React.useEffect(() => {\n    if (ref.current && autoOpen) {\n      const height = ref.current.scrollHeight;\n      // NOTE: max-h-48 in tw equals 192px (48 * 4px)\n      if (height <= 192) {\n        setIsOpened(true);\n      }\n    }\n  }, [autoOpen]);\n\n  return (\n    <Collapsible open={isOpened} onOpenChange={setIsOpened}>\n      <div className={cn(\"relative overflow-hidden\", className)} {...props}>\n        <CollapsibleContent\n          forceMount\n          ref={ref}\n          className={cn(\"overflow-hidden\", !isOpened && \"max-h-48\")}\n        >\n          {children}\n        </CollapsibleContent>\n        {!isOpened ? (\n          <div\n            className={cn(\n              \"absolute flex items-center justify-center bg-gradient-to-b from-transparent to-90% to-background p-2\",\n              isOpened ? \"inset-x-0 bottom-0 h-12\" : \"inset-0\",\n            )}\n          >\n            <CollapsibleTrigger asChild>\n              <Button variant=\"outline\" size=\"sm\">\n                Expand\n              </Button>\n            </CollapsibleTrigger>\n          </div>\n        ) : null}\n      </div>\n    </Collapsible>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/content/empty-state.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport function EmptyStateContainer({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex h-full flex-col items-center justify-center gap-2 rounded-lg border border-border border-dashed p-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function EmptyStateTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"text-foreground\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function EmptyStateDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      className={cn(\"text-center text-muted-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </p>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/content/process-message.tsx",
    "content": "import type { AnchorHTMLAttributes } from \"react\";\nimport { Fragment, createElement } from \"react\";\nimport { jsx, jsxs } from \"react/jsx-runtime\";\nimport rehypeReact from \"rehype-react\";\nimport remarkParse from \"remark-parse\";\nimport remarkRehype from \"remark-rehype\";\nimport { unified } from \"unified\";\n\nexport function ProcessMessage({ value }: { value: string }) {\n  const result = unified()\n    .use(remarkParse)\n    .use(remarkRehype)\n    .use(rehypeReact, {\n      createElement,\n      Fragment,\n      jsx,\n      jsxs,\n      components: {\n        a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {\n          return (\n            <a\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"underline\"\n              {...props}\n            />\n          );\n        },\n      } as { [key: string]: React.ComponentType<unknown> },\n    })\n    .processSync(value).result;\n\n  return result;\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/content/section.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport function Section({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"section\">) {\n  return (\n    <section className={cn(\"space-y-4\", className)} {...props}>\n      {children}\n    </section>\n  );\n}\n\nexport function SectionHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function SectionHeaderRow({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col gap-1.5 sm:flex-row sm:items-end sm:justify-between\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function SectionDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      className={cn(\n        \"font-commit-mono text-muted-foreground text-sm tracking-tight\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </p>\n  );\n}\n\nexport function SectionTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"font-medium text-lg\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function SectionGroup({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"mx-auto w-full max-w-4xl space-y-8 px-4 py-8\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function SectionGroupHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"space-y-1.5\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function SectionGroupTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"font-bold text-4xl\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/controls-filter/.gitkeep",
    "content": ""
  },
  {
    "path": "apps/dashboard/src/components/controls-search/button-reset.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { X } from \"lucide-react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\nexport function ButtonReset({ only }: { only?: string[] }) {\n  const searchParams = useSearchParams();\n  const router = useRouter();\n\n  // Determine if at least one parameter that should be reset is present (or any parameter if `only` is undefined)\n  const hasParamsToReset = only\n    ? only.some((key) => searchParams.has(key))\n    : !!searchParams.toString();\n\n  if (!hasParamsToReset) return null;\n\n  const handleClick = () => {\n    // Clone the current search params so we can mutate them\n    const params = new URLSearchParams(searchParams.toString());\n\n    if (only && only.length > 0) {\n      // Remove only the specified keys\n      only.forEach((key) => params.delete(key));\n      const query = params.toString();\n      router.push(\n        query\n          ? `${window.location.pathname}?${query}`\n          : window.location.pathname,\n      );\n    } else {\n      // No `only` prop provided – remove all query parameters\n      router.push(window.location.pathname);\n    }\n  };\n\n  if (!hasParamsToReset) return null;\n\n  return (\n    <Button variant=\"ghost\" size=\"sm\" onClick={handleClick}>\n      <X />\n      Reset\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/controls-search/command-region.tsx",
    "content": "\"use client\";\n\nimport { IconCloudProvider } from \"@/components/common/icon-cloud-provider\";\nimport { Link } from \"@/components/common/link\";\nimport {\n  BillingOverlay,\n  BillingOverlayButton,\n  BillingOverlayDescription,\n} from \"@/components/content/billing-overlay\";\nimport type { REGIONS } from \"@/data/metrics.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { formatRegionCode, groupByContinent } from \"@openstatus/regions\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from \"@openstatus/ui/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Check, Globe, Lock } from \"lucide-react\";\nimport { parseAsArrayOf, parseAsString, useQueryState } from \"nuqs\";\n\nexport function CommandRegion({\n  regions,\n  privateLocations,\n}: {\n  regions: (typeof REGIONS)[number][];\n  privateLocations?: { id: number; name: string }[];\n}) {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const [selectedRegions, setSelectedRegions] = useQueryState(\n    \"regions\",\n    parseAsArrayOf(parseAsString).withDefault([\n      ...regions,\n      ...(privateLocations?.map((location) => location.id.toString()) ?? []),\n    ]),\n  );\n\n  const limited = workspace?.plan === \"free\";\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\">\n          {selectedRegions.length === regions.length\n            ? \"All Regions\"\n            : `${selectedRegions.length} Regions`}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        className=\"relative w-[250px] overflow-hidden p-0\"\n      >\n        <Command>\n          <CommandInput placeholder=\"Search region...\" disabled={limited} />\n          <CommandList>\n            <CommandGroup forceMount>\n              <CommandItem\n                onSelect={() => {\n                  const items = document.querySelectorAll(\n                    '[data-slot=\"command-item\"][data-disabled=\"false\"]',\n                  );\n                  const codes: (typeof REGIONS)[number][] = [];\n\n                  items.forEach((item) => {\n                    const code = item.getAttribute(\"data-value\");\n                    if (code && code !== \"select-all\") {\n                      codes.push(code as (typeof REGIONS)[number]);\n                    }\n                  });\n\n                  if (codes.length === selectedRegions.length) {\n                    setSelectedRegions([]);\n                  } else {\n                    setSelectedRegions(codes);\n                  }\n                }}\n                value=\"select-all\"\n                disabled={limited}\n              >\n                Toggle selection\n              </CommandItem>\n            </CommandGroup>\n            <CommandSeparator alwaysRender />\n            {Object.entries(groupByContinent).map(\n              ([continent, continentRegions]) => {\n                const allowedRegions = continentRegions.filter((region) =>\n                  regions.includes(region.code),\n                );\n\n                if (allowedRegions.length === 0) {\n                  return null;\n                }\n                return (\n                  <CommandGroup key={continent} heading={continent}>\n                    {allowedRegions.map((region) => (\n                      <CommandItem\n                        disabled={limited}\n                        key={region.code}\n                        value={region.code}\n                        keywords={[\n                          region.code,\n                          region.location,\n                          region.continent,\n                          region.flag,\n                        ]}\n                        onSelect={() => {\n                          setSelectedRegions((prev) =>\n                            prev.includes(region.code)\n                              ? prev.filter((r) => r !== region.code)\n                              : [...prev, region.code],\n                          );\n                        }}\n                      >\n                        <span>{region.flag}</span>\n                        <IconCloudProvider\n                          provider={region.provider}\n                          className=\"size-3\"\n                        />\n                        <span className=\"font-mono\">\n                          {formatRegionCode(region.code)}\n                        </span>\n                        <span className=\"truncate text-muted-foreground text-xs\">\n                          {region.location}\n                        </span>\n                        <Check\n                          className={cn(\n                            \"ml-auto\",\n                            selectedRegions.includes(region.code)\n                              ? \"opacity-100\"\n                              : \"opacity-0\",\n                          )}\n                        />\n                      </CommandItem>\n                    ))}\n                  </CommandGroup>\n                );\n              },\n            )}\n            {privateLocations && privateLocations.length > 0 ? (\n              <CommandGroup heading=\"Private Locations\">\n                {privateLocations.map((location) => (\n                  <CommandItem\n                    key={location.id}\n                    keywords={[location.name]}\n                    value={location.id.toString()}\n                    onSelect={() => {\n                      setSelectedRegions((prev) =>\n                        prev.includes(location.id.toString())\n                          ? prev.filter((r) => r !== location.id.toString())\n                          : [...prev, location.id.toString()],\n                      );\n                    }}\n                  >\n                    <Globe className=\"size-3\" />\n                    <span className=\"truncate font-mono\">{location.name}</span>\n                    <Check\n                      className={cn(\n                        \"ml-auto\",\n                        selectedRegions.includes(location.id.toString())\n                          ? \"opacity-100\"\n                          : \"opacity-0\",\n                      )}\n                    />\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            ) : null}\n            <CommandEmpty>No region found.</CommandEmpty>\n          </CommandList>\n        </Command>\n        {limited ? (\n          <BillingOverlay className=\"to-70%\">\n            <BillingOverlayButton asChild>\n              <Link href=\"/settings/billing\">\n                <Lock />\n                Upgrade\n              </Link>\n            </BillingOverlayButton>\n            <BillingOverlayDescription>\n              Filter by region is only available on paid plans.\n            </BillingOverlayDescription>\n          </BillingOverlay>\n        ) : null}\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/controls-search/command-tags.tsx",
    "content": "\"use client\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@openstatus/ui/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Check } from \"lucide-react\";\nimport { parseAsArrayOf, parseAsString, useQueryState } from \"nuqs\";\n\nexport function CommandTags() {\n  const trpc = useTRPC();\n  const { data: tags } = useQuery(trpc.monitorTag.list.queryOptions());\n  const [selectedTags, setSelectedTags] = useQueryState(\n    \"tags\",\n    parseAsArrayOf(parseAsString).withDefault([]).withOptions({\n      shallow: false,\n    }),\n  );\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\">\n          {selectedTags.length === (tags?.length ?? 0)\n            ? \"All Tags\"\n            : `${selectedTags.length} Tags`}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        className=\"relative w-[200px] overflow-hidden p-0\"\n      >\n        <Command>\n          <CommandInput placeholder=\"Search tag...\" />\n          <CommandList>\n            <CommandGroup>\n              {tags?.map((tag) => (\n                <CommandItem\n                  key={tag.id}\n                  value={tag.name}\n                  keywords={[tag.name]}\n                  onSelect={() => {\n                    setSelectedTags((prev) =>\n                      prev.includes(tag.name)\n                        ? prev.filter((r) => r !== tag.name)\n                        : [...prev, tag.name],\n                    );\n                  }}\n                >\n                  <div className=\"flex items-center gap-2\">\n                    <span\n                      className=\"size-2.5 rounded-full\"\n                      style={{ backgroundColor: tag.color }}\n                    />\n                    {tag.name}\n                  </div>\n                  <Check\n                    className={cn(\n                      \"ml-auto\",\n                      selectedTags.includes(tag.name)\n                        ? \"opacity-100\"\n                        : \"opacity-0\",\n                    )}\n                  />\n                </CommandItem>\n              ))}\n            </CommandGroup>\n            <CommandEmpty>No tag found.</CommandEmpty>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/controls-search/dropdown-interval.tsx",
    "content": "\"use client\";\n\nimport { INTERVALS } from \"@/data/metrics.client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport { Check } from \"lucide-react\";\nimport { parseAsNumberLiteral, useQueryState } from \"nuqs\";\n\nconst MAPPING = {\n  5: \"5 minutes\",\n  15: \"15 minutes\",\n  30: \"30 minutes\",\n  60: \"1 hour\",\n  120: \"2 hours\",\n  240: \"4 hours\",\n  480: \"8 hours\",\n  1440: \"1 day\",\n} as const;\n\nconst parseInterval = parseAsNumberLiteral(INTERVALS).withDefault(30);\n\nexport function DropdownInterval() {\n  const [interval, setInterval] = useQueryState(\"interval\", parseInterval);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\">\n          {MAPPING[interval]}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\">\n        <DropdownMenuGroup>\n          <DropdownMenuLabel className=\"font-medium text-muted-foreground text-xs\">\n            Resolution\n          </DropdownMenuLabel>\n          {INTERVALS.map((item) => (\n            <DropdownMenuItem key={item} onSelect={() => setInterval(item)}>\n              {MAPPING[item]}\n              {interval === item ? (\n                <Check className=\"ml-auto shrink-0\" />\n              ) : null}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/controls-search/dropdown-percentile.tsx",
    "content": "\"use client\";\n\nimport { PERCENTILES } from \"@/data/metrics.client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Check } from \"lucide-react\";\nimport { parseAsStringLiteral, useQueryState } from \"nuqs\";\n\nconst parsePercentile = parseAsStringLiteral(PERCENTILES).withDefault(\"p50\");\n\nexport function DropdownPercentile() {\n  const [percentile, setPercentile] = useQueryState(\n    \"percentile\",\n    parsePercentile,\n  );\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\" className=\"capitalize\">\n          {percentile}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\">\n        <DropdownMenuGroup>\n          <DropdownMenuLabel className=\"font-medium text-muted-foreground text-xs\">\n            Percentile\n          </DropdownMenuLabel>\n          {PERCENTILES.map((item) => (\n            <DropdownMenuItem\n              key={item}\n              onSelect={() => setPercentile(item)}\n              className={cn(\"capitalize\")}\n            >\n              {item}\n              {percentile === item ? (\n                <Check className=\"ml-auto shrink-0\" />\n              ) : null}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/controls-search/dropdown-period.tsx",
    "content": "\"use client\";\n\nimport { PERIODS } from \"@/data/metrics.client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport { Check } from \"lucide-react\";\nimport { parseAsStringLiteral, useQueryState } from \"nuqs\";\n\n// TODO: where to move it?\nexport const PERIOD_VALUES = [\n  {\n    value: \"1d\",\n    label: \"Last day\",\n  },\n  {\n    value: \"7d\",\n    label: \"Last 7 days\",\n  },\n  {\n    value: \"14d\",\n    label: \"Last 14 days\",\n  },\n] satisfies { value: (typeof PERIODS)[number]; label: string }[];\n\nconst parsePeriod = parseAsStringLiteral(PERIODS).withDefault(\"1d\");\n\nexport function DropdownPeriod() {\n  const [period, setPeriod] = useQueryState(\"period\", parsePeriod);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\">\n          {PERIOD_VALUES.find(({ value }) => value === period)?.label}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\">\n        <DropdownMenuGroup>\n          <DropdownMenuLabel className=\"font-medium text-muted-foreground text-xs\">\n            Period\n          </DropdownMenuLabel>\n          {PERIOD_VALUES.map(({ value, label }) => (\n            <DropdownMenuItem key={value} onSelect={() => setPeriod(value)}>\n              {label}\n              {period === value ? <Check className=\"ml-auto shrink-0\" /> : null}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/controls-search/dropdown-status.tsx",
    "content": "\"use client\";\n\nimport { STATUS } from \"@/data/metrics.client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Check } from \"lucide-react\";\nimport { parseAsStringLiteral, useQueryState } from \"nuqs\";\n\nconst parseStatus = parseAsStringLiteral(STATUS);\n\nexport function DropdownStatus() {\n  const [status, setStatus] = useQueryState(\"status\", parseStatus);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\" className=\"capitalize\">\n          {status ?? \"All Status\"}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\">\n        <DropdownMenuGroup>\n          <DropdownMenuLabel className=\"font-medium text-muted-foreground text-xs\">\n            Request Status\n          </DropdownMenuLabel>\n          {STATUS.map((item) => (\n            <DropdownMenuItem\n              key={item}\n              onSelect={() => setStatus(item)}\n              className={cn(\"capitalize\")}\n            >\n              {item}\n              {status === item ? <Check className=\"ml-auto shrink-0\" /> : null}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/controls-search/dropdown-trigger.tsx",
    "content": "\"use client\";\n\nimport { TRIGGER } from \"@/data/metrics.client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Check } from \"lucide-react\";\nimport { parseAsStringLiteral, useQueryState } from \"nuqs\";\n\nconst parseTrigger = parseAsStringLiteral(TRIGGER);\n\nexport function DropdownTrigger() {\n  const [trigger, setTrigger] = useQueryState(\"trigger\", parseTrigger);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\" className=\"capitalize\">\n          {trigger ?? \"All Trigger\"}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\">\n        <DropdownMenuGroup>\n          <DropdownMenuLabel className=\"font-medium text-muted-foreground text-xs\">\n            Trigger\n          </DropdownMenuLabel>\n          {TRIGGER.map((item) => (\n            <DropdownMenuItem\n              key={item}\n              onSelect={() => setTrigger(item)}\n              className={cn(\"capitalize\")}\n            >\n              {item === \"cron\" ? \"Scheduled\" : \"API\"}\n              {trigger === item ? <Check className=\"ml-auto shrink-0\" /> : null}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/controls-search/popover-date.tsx",
    "content": "import { DatePicker } from \"@/components/date-picker\";\nimport { formatDateRange } from \"@/lib/formatter\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { endOfDay, startOfDay, subDays, subHours } from \"date-fns\";\nimport { parseAsIsoDateTime, useQueryState } from \"nuqs\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { DateRange } from \"react-day-picker\";\n\nexport function PopoverDate() {\n  const [open, setOpen] = useState(false);\n  const today = useRef(new Date());\n  const [from, setFrom] = useQueryState(\n    \"from\",\n    parseAsIsoDateTime.withDefault(startOfDay(today.current)),\n  );\n  const [to, setTo] = useQueryState(\n    \"to\",\n    parseAsIsoDateTime.withDefault(endOfDay(today.current)),\n  );\n  const [range, setRange] = useState<DateRange>({ from, to });\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  const presets = useMemo(\n    () => [\n      {\n        id: \"today\",\n        label: \"Today\",\n        values: {\n          from: startOfDay(today.current),\n          to: endOfDay(today.current),\n        },\n        shortcut: \"t\",\n      },\n      {\n        id: \"yesterday\",\n        label: \"Yesterday\",\n        values: {\n          from: startOfDay(subDays(today.current, 1)),\n          to: endOfDay(subDays(today.current, 1)),\n        },\n        shortcut: \"y\",\n      },\n      {\n        id: \"lastHour\",\n        label: \"Last hour\",\n        values: {\n          from: subHours(today.current, 1),\n          to: today.current,\n        },\n        shortcut: \"h\",\n      },\n      {\n        id: \"last6Hours\",\n        label: \"Last 6 hours\",\n        values: {\n          from: subHours(today.current, 5),\n          to: today.current,\n        },\n        shortcut: \"s\",\n      },\n      {\n        id: \"last24Hours\",\n        label: \"Last 24 hours\",\n        values: {\n          from: subHours(today.current, 23),\n          to: today.current,\n        },\n        shortcut: \"d\",\n      },\n      {\n        id: \"last7Days\",\n        label: \"Last 7 days\",\n        values: {\n          from: subDays(today.current, 6),\n          to: today.current,\n        },\n        shortcut: \"w\",\n      },\n      {\n        id: \"last14Days\",\n        label: \"Last 14 days\",\n        values: {\n          from: subDays(today.current, 13),\n          to: today.current,\n        },\n        shortcut: \"b\",\n      },\n    ],\n    [today],\n  );\n\n  //   instead use `range` state\n  const selected = presets.find((period) => {\n    return (\n      from.getTime() === period.values.from.getTime() &&\n      to.getTime() === period.values.to.getTime()\n    );\n  });\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  useEffect(() => {\n    if (!open) {\n      setFrom(range.from ?? null);\n      setTo(range.to ?? null);\n    }\n  }, [open]);\n\n  useEffect(() => {\n    const down = (e: KeyboardEvent) => {\n      if (!open) return;\n\n      presets.map((preset) => {\n        if (preset.shortcut === e.key) {\n          setFrom(preset.values.from);\n          setTo(preset.values.to);\n          setRange({ from: preset.values.from, to: preset.values.to });\n        }\n      });\n    };\n    document.addEventListener(\"keydown\", down);\n    return () => document.removeEventListener(\"keydown\", down);\n  }, [presets, open, setFrom, setTo]);\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\">\n          {selected?.label ?? formatDateRange(from, to)}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0\" side=\"bottom\" align=\"start\">\n        <DatePicker presets={presets} range={range} onSelect={setRange} />\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/audit-logs/columns.tsx",
    "content": "\"use client\";\n\nimport { HoverCardTimestamp } from \"@/components/common/hover-card-timestamp\";\nimport { TableCellDate } from \"@/components/data-table/table-cell-date\";\nimport { config, getMetadata } from \"@/data/audit-logs.client\";\nimport { cn } from \"@/lib/utils\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport type { PrivateLocation } from \"@openstatus/db/src/schema\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\n\ntype AuditLog = RouterOutputs[\"tinybird\"][\"auditLog\"][\"data\"][number];\n\nexport function getColumns(\n  privateLocations?: PrivateLocation[],\n): ColumnDef<AuditLog>[] {\n  const metadata = getMetadata(privateLocations);\n  return [\n    {\n      id: \"icon\",\n      accessorFn: (row) => row.action,\n      header: () => null,\n      enableSorting: false,\n      enableHiding: false,\n      cell: ({ row }) => {\n        const value = row.getValue(\"action\");\n        const { icon: Icon, color } = config[value as keyof typeof config];\n        if (!Icon) return null;\n        return <Icon className={cn(\"size-4\", color)} />;\n      },\n      meta: {\n        headerClassName: \"w-7\",\n      },\n    },\n    {\n      accessorKey: \"action\",\n      header: \"Action\",\n      enableSorting: false,\n      enableHiding: false,\n      cell: ({ row }) => {\n        const value = row.getValue(\"action\");\n        const { title } = config[value as keyof typeof config];\n        if (!title) return null;\n        return <div>{title}</div>;\n      },\n    },\n    {\n      accessorKey: \"metadata\",\n      header: \"Information\",\n      enableSorting: false,\n      enableHiding: false,\n      cell: ({ row }) => {\n        const value = row.getValue(\"metadata\");\n        if (!value) return null;\n        return (\n          <div className=\"flex flex-wrap gap-2\">\n            {Object.entries(value)\n              .filter(([key, value]) =>\n                metadata[key as keyof typeof metadata]?.visible(value),\n              )\n              .map(([key, value]) => {\n                return (\n                  <Pill\n                    key={key}\n                    label={metadata[key as keyof typeof metadata].key}\n                    value={metadata[key as keyof typeof metadata]?.format(\n                      value,\n                    )}\n                  />\n                );\n              })}\n          </div>\n        );\n      },\n    },\n    {\n      accessorKey: \"timestamp\",\n      header: \"Timestamp\",\n      enableSorting: false,\n      enableHiding: false,\n      cell: ({ row }) => {\n        const value = row.getValue(\"timestamp\");\n        if (value instanceof Date) {\n          return (\n            <HoverCardTimestamp date={value}>\n              <TableCellDate value={value} className=\"font-mono\" />\n            </HoverCardTimestamp>\n          );\n        }\n        const date = new Date(Number(value));\n        if (Number.isNaN(date.getTime())) {\n          return <div className=\"font-mono\">{String(value)}</div>;\n        }\n\n        return (\n          <HoverCardTimestamp date={date}>\n            <TableCellDate value={date} className=\"font-mono\" />\n          </HoverCardTimestamp>\n        );\n      },\n    },\n  ];\n}\n\nfunction Pill({ label, value }: { label: string; value?: string }) {\n  if (!value) return null;\n  return (\n    <div className=\"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3\">\n      <div className=\"border-r bg-muted py-0.5 pr-1 pl-2 text-foreground/70\">\n        {label}\n      </div>\n      <div className=\"py-0.5 pr-2 pl-1 font-mono\">\n        {/* NOTE: if we have more number values, we might wanna change it */}\n        {value}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/audit-logs/wrapper.tsx",
    "content": "\"use client\";\n\nimport { BlockWrapper } from \"@/components/content/block-wrapper\";\nimport {\n  EmptyStateContainer,\n  EmptyStateDescription,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport { DataTable } from \"@/components/ui/data-table/data-table\";\nimport { DataTablePaginationSimple } from \"@/components/ui/data-table/data-table-pagination\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useMemo } from \"react\";\nimport { getColumns } from \"./columns\";\n\nexport function AuditLogsWrapper({\n  monitorId,\n  interval,\n}: {\n  monitorId: string;\n  interval: number;\n}) {\n  const trpc = useTRPC();\n\n  const { data: auditLogs, isLoading } = useQuery(\n    trpc.tinybird.auditLog.queryOptions({ monitorId, interval }),\n  );\n\n  const { data: privateLocations } = useQuery(\n    trpc.privateLocation.list.queryOptions(),\n  );\n\n  const columns = useMemo(\n    () => getColumns(privateLocations),\n    [privateLocations],\n  );\n\n  if (isLoading) {\n    return (\n      <div className=\"flex h-full max-h-48 min-h-48 flex-col\">\n        <Skeleton className=\"h-full w-full flex-1\" />\n      </div>\n    );\n  }\n\n  if (!auditLogs?.data || auditLogs.data.length === 0) {\n    return (\n      <EmptyStateContainer>\n        <EmptyStateTitle>No audit logs</EmptyStateTitle>\n        <EmptyStateDescription>\n          No audit logs found for this monitor.\n        </EmptyStateDescription>\n      </EmptyStateContainer>\n    );\n  }\n\n  return (\n    <BlockWrapper>\n      <DataTable\n        columns={columns}\n        data={auditLogs.data}\n        paginationComponent={DataTablePaginationSimple}\n      />\n    </BlockWrapper>\n  );\n}\n\nexport default AuditLogsWrapper;\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/billing/data-table.tsx",
    "content": "\"use client\";\n\nimport { Check } from \"lucide-react\";\nimport { Fragment, useTransition } from \"react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Table,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\n\nimport { config as featureGroups, plans } from \"@/data/plans\";\nimport { getStripe } from \"@/lib/stripe\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { cn } from \"@/lib/utils\";\nimport type { WorkspacePlan } from \"@openstatus/db/src/schema\";\nimport {\n  getAddonPriceConfig,\n  getPriceConfig,\n} from \"@openstatus/db/src/schema/plan/utils\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { useCookieState } from \"@openstatus/ui/hooks/use-cookie-state\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { useRouter } from \"next/navigation\";\n\nconst BASE_URL =\n  process.env.NODE_ENV === \"production\"\n    ? \"https://app.openstatus.dev\"\n    : \"http://localhost:3000\";\n\nexport function DataTable({ restrictTo }: { restrictTo?: WorkspacePlan[] }) {\n  const [currency] = useCookieState(\"x-currency\", \"USD\");\n  const trpc = useTRPC();\n  const router = useRouter();\n  const [isPending, startTransition] = useTransition();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n\n  const checkoutSessionMutation = useMutation(\n    trpc.stripeRouter.getCheckoutSession.mutationOptions({\n      onSuccess: async (data) => {\n        if (!data) return;\n\n        const stripe = await getStripe();\n        stripe?.redirectToCheckout({ sessionId: data.id });\n      },\n    }),\n  );\n\n  const customerPortalMutation = useMutation(\n    trpc.stripeRouter.getUserCustomerPortal.mutationOptions({\n      onSuccess: (url) => {\n        if (!url) return;\n        router.push(url);\n      },\n    }),\n  );\n\n  if (!workspace) return null;\n\n  const filteredPlans = Object.values(plans).filter((plan) =>\n    restrictTo ? restrictTo.includes(plan.id) : true,\n  );\n\n  return (\n    <Table className=\"relative table-fixed\">\n      <TableCaption>\n        A list to compare the different features by plan.\n      </TableCaption>\n      <TableHeader>\n        <TableRow className=\"hover:bg-transparent\">\n          <TableHead className=\"p-2 align-bottom\">\n            Features comparison\n          </TableHead>\n          {filteredPlans.map(({ id, ...plan }) => {\n            const isCurrentPlan = workspace.plan === id;\n            const price = getPriceConfig(id, currency);\n            return (\n              <TableHead\n                key={id}\n                className={cn(\n                  \"h-auto p-2 align-bottom text-foreground\",\n                  id === \"starter\" ? \"bg-muted/30\" : \"\",\n                )}\n              >\n                <div className=\"flex h-full flex-col justify-between gap-1\">\n                  <div className=\"flex flex-1 flex-col gap-1\">\n                    <p className=\"font-cal text-lg\">{plan.title}</p>\n                    <p className=\"text-wrap font-normal text-muted-foreground text-xs\">\n                      {plan.description}\n                    </p>\n                  </div>\n                  <p className=\"text-right\">\n                    <span className=\"font-mono text-lg\">\n                      {new Intl.NumberFormat(price.locale, {\n                        style: \"currency\",\n                        currency: price.currency,\n                      }).format(price.value)}\n                    </span>\n                    <span className=\"text-muted-foreground text-sm\">\n                      /month\n                    </span>\n                  </p>\n                  <Button\n                    size=\"sm\"\n                    type=\"button\"\n                    variant={id === \"starter\" ? \"default\" : \"outline\"}\n                    onClick={() => {\n                      startTransition(async () => {\n                        if (id === \"free\") {\n                          await customerPortalMutation.mutateAsync({\n                            workspaceSlug: workspace.slug,\n                            returnUrl: `${BASE_URL}/settings/billing`,\n                          });\n                          return;\n                        }\n                        await checkoutSessionMutation.mutateAsync({\n                          plan: id,\n                          // TODO: move to the server as we have the current workspace\n                          workspaceSlug: workspace.slug,\n                          successUrl: `${BASE_URL}/settings/billing?success=true`,\n                          cancelUrl: `${BASE_URL}/settings/billing`,\n                        });\n                      });\n                    }}\n                    disabled={isPending || isCurrentPlan}\n                  >\n                    {isCurrentPlan\n                      ? \"Current Plan\"\n                      : isPending\n                        ? \"Choosing...\"\n                        : \"Choose\"}\n                  </Button>\n                </div>\n              </TableHead>\n            );\n          })}\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        {Object.entries(featureGroups).map(\n          ([groupKey, { label, features }]) => (\n            <Fragment key={groupKey}>\n              <TableRow className=\"bg-muted/50\">\n                <TableCell\n                  colSpan={filteredPlans.length + 1}\n                  className=\"font-medium\"\n                >\n                  {label}\n                </TableCell>\n              </TableRow>\n              {features.map(\n                ({ value, label: featureLabel, monthly, badge }) => (\n                  <TableRow key={groupKey + value}>\n                    <TableCell>\n                      <div className=\"flex items-center gap-2 text-wrap\">\n                        {featureLabel}{\" \"}\n                        {badge ? (\n                          <Badge variant=\"outline\">{badge}</Badge>\n                        ) : null}\n                      </div>\n                    </TableCell>\n                    {filteredPlans.map((plan) => {\n                      const limitValue =\n                        plan.limits[value as keyof typeof plan.limits];\n                      const isAddon = value in plan.addons;\n\n                      function renderContent() {\n                        if (isAddon) {\n                          const price = getAddonPriceConfig(\n                            plan.id,\n                            value as keyof typeof plan.addons,\n                            currency,\n                          );\n                          if (!price) return null;\n\n                          const isNumber = typeof limitValue === \"number\";\n                          return (\n                            <div>\n                              <span>\n                                {isNumber\n                                  ? new Intl.NumberFormat(\"us\")\n                                      .format(limitValue)\n                                      .toString()\n                                  : null}\n                              </span>\n                              <span>\n                                <span className=\"text-muted-foreground\">\n                                  {isNumber ? \" + \" : \"\"}\n                                </span>\n                                <span>\n                                  {new Intl.NumberFormat(price.locale, {\n                                    style: \"currency\",\n                                    currency: price.currency,\n                                  }).format(price.value)}\n                                  {isNumber ? \"/mo./each\" : \"/mo.\"}\n                                </span>\n                              </span>\n                            </div>\n                          );\n                        }\n                        if (typeof limitValue === \"boolean\") {\n                          return limitValue ? (\n                            <Check className=\"h-4 w-4 text-foreground\" />\n                          ) : (\n                            <span className=\"text-muted-foreground/50\">\n                              &#8208;\n                            </span>\n                          );\n                        }\n                        if (typeof limitValue === \"number\") {\n                          return new Intl.NumberFormat(\"us\")\n                            .format(limitValue)\n                            .toString();\n                        }\n\n                        // TODO: create a format function for this in @data/plans\n                        if (value === \"regions\" && Array.isArray(limitValue)) {\n                          return limitValue?.length ?? 0;\n                        }\n\n                        if (\n                          Array.isArray(limitValue) &&\n                          limitValue.length > 0\n                        ) {\n                          return limitValue[0];\n                        }\n                        return limitValue;\n                      }\n\n                      return (\n                        <TableCell\n                          key={plan.id + value}\n                          className={cn(\n                            \"font-mono\",\n                            plan.id === \"starter\" && \"bg-muted/30\",\n                          )}\n                        >\n                          {renderContent()}\n                          {monthly ? \"/mo.\" : \"\"}\n                        </TableCell>\n                      );\n                    })}\n                  </TableRow>\n                ),\n              )}\n            </Fragment>\n          ),\n        )}\n      </TableBody>\n    </Table>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/dable-cell-skeleton.tsx",
    "content": "import { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function TableCellSkeleton({\n  className,\n  ...props\n}: React.ComponentProps<typeof Skeleton>) {\n  return <Skeleton className={cn(\"h-5 w-12\", className)} {...props} />;\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/data-table-sheet.tsx",
    "content": "\"use client\";\n\nimport {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetFooter,\n  SheetHeader,\n  SheetTitle,\n  SheetTrigger,\n} from \"@openstatus/ui/components/ui/sheet\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\n// TODO: rename to DataTableViewer?\n\nexport function DataTableSheetContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetContent>) {\n  return (\n    <SheetContent className={cn(\"max-h-screen gap-0\", className)} {...props}>\n      {children}\n    </SheetContent>\n  );\n}\n\nexport function DataTableSheetHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetHeader>) {\n  return (\n    <SheetHeader\n      className={cn(\"sticky top-0 border-b bg-background\", className)}\n      {...props}\n    >\n      {children}\n    </SheetHeader>\n  );\n}\n\nexport function DataTableSheetFooter({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetFooter>) {\n  return (\n    <SheetFooter\n      className={cn(\"sticky bottom-0 border-t bg-background\", className)}\n      {...props}\n    >\n      {children}\n    </SheetFooter>\n  );\n}\n\nexport function DataTableSheetFooterInfo({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"text-muted-foreground/70 text-xs\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport {\n  SheetTitle as DataTableSheetTitle,\n  SheetDescription as DataTableSheetDescription,\n  SheetTrigger as DataTableSheetTrigger,\n  Sheet as DataTableSheet,\n};\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/incidents/columns.tsx",
    "content": "\"use client\";\n\nimport { TableCellDate } from \"@/components/data-table/table-cell-date\";\nimport { TableCellLink } from \"@/components/data-table/table-cell-link\";\nimport { TableCellNumber } from \"@/components/data-table/table-cell-number\";\nimport { DataTableColumnHeader } from \"@/components/ui/data-table/data-table-column-header\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport { formatDistanceStrict } from \"date-fns\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\ntype Incident = RouterOutputs[\"incident\"][\"list\"][number];\n\nexport const columns: ColumnDef<Incident>[] = [\n  {\n    id: \"monitor\",\n    accessorFn: (row) => row.monitor.name,\n    header: \"Monitor\",\n    enableSorting: false,\n    enableHiding: false,\n    cell: ({ row }) => {\n      return (\n        <TableCellLink\n          value={row.getValue(\"monitor\")}\n          href={`/monitors/${row.original.monitor.id}/overview`}\n        />\n      );\n    },\n    meta: {\n      cellClassName: \"max-w-[150px] min-w-max\",\n    },\n  },\n  {\n    accessorKey: \"startedAt\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Started At\" />\n    ),\n    cell: ({ row }) => <TableCellDate value={row.getValue(\"startedAt\")} />,\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"acknowledgedAt\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Acknowledged\" />\n    ),\n    cell: ({ row }) => <TableCellDate value={row.getValue(\"acknowledgedAt\")} />,\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"resolvedAt\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Resolved At\" />\n    ),\n    cell: ({ row }) => <TableCellDate value={row.getValue(\"resolvedAt\")} />,\n    enableHiding: false,\n  },\n  {\n    id: \"duration\",\n    accessorFn: (row) =>\n      row.resolvedAt\n        ? formatDistanceStrict(row.startedAt, row.resolvedAt)\n        : \"ongoing\",\n    header: \"Duration\",\n    cell: ({ row }) => {\n      const value = row.getValue(\"duration\");\n      if (typeof value === \"string\") {\n        const [amount, unit] = value.split(\" \");\n        return <TableCellNumber value={amount} unit={unit} />;\n      }\n      return <TableCellNumber value={value} />;\n    },\n  },\n  {\n    id: \"actions\",\n    cell: ({ row }) => <DataTableRowActions row={row} />,\n    meta: {\n      cellClassName: \"w-8\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/incidents/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport type { Row } from \"@tanstack/react-table\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { getActions } from \"@/data/incidents.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useMemo, useState, useTransition } from \"react\";\nimport { toast } from \"sonner\";\n\ntype Incident = RouterOutputs[\"incident\"][\"list\"][number];\n\ninterface DataTableRowActionsProps {\n  row: Row<Incident>;\n}\n\nexport function DataTableRowActions({ row }: DataTableRowActionsProps) {\n  const [isPending, startTransition] = useTransition();\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const acknowledgeIncidentMutation = useMutation(\n    trpc.incident.acknowledge.mutationOptions({\n      onSuccess: () => {\n        queryClient.refetchQueries({\n          queryKey: trpc.incident.list.queryKey({\n            monitorId: row.original.monitorId,\n          }),\n        });\n      },\n    }),\n  );\n  const resolveIncidentMutation = useMutation(\n    trpc.incident.resolve.mutationOptions({\n      onSuccess: () => {\n        queryClient.refetchQueries({\n          queryKey: trpc.incident.list.queryKey({\n            monitorId: row.original.monitorId,\n          }),\n        });\n      },\n    }),\n  );\n  const deleteIncidentMutation = useMutation(\n    trpc.incident.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.refetchQueries({\n          queryKey: trpc.incident.list.queryKey({\n            monitorId: row.original.monitorId,\n          }),\n        });\n      },\n    }),\n  );\n\n  const [type, setType] = useState<\"acknowledge\" | \"resolve\" | null>(null);\n  const open = useMemo(() => type !== null, [type]);\n\n  const actions = getActions({\n    acknowledge: row.original.acknowledgedAt\n      ? undefined\n      : () => setType(\"acknowledge\"),\n    resolve: row.original.resolvedAt ? undefined : () => setType(\"resolve\"),\n  });\n\n  const handleConfirm = async () => {\n    try {\n      startTransition(async () => {\n        const promise =\n          type === \"acknowledge\"\n            ? acknowledgeIncidentMutation.mutateAsync({\n                id: row.original.id,\n              })\n            : resolveIncidentMutation.mutateAsync({\n                id: row.original.id,\n              });\n        toast.promise(promise, {\n          loading: \"Confirming...\",\n          success: \"Confirmed\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to confirm\";\n          },\n        });\n        await promise;\n        setType(null);\n      });\n    } catch (error) {\n      console.error(\"Failed to confirm:\", error);\n    }\n  };\n\n  return (\n    <>\n      <QuickActions\n        actions={actions}\n        deleteAction={{\n          confirmationValue: row.original.title || \"incident\",\n          submitAction: async () => {\n            await deleteIncidentMutation.mutateAsync({\n              id: row.original.id,\n            });\n          },\n        }}\n      />\n      <AlertDialog open={open} onOpenChange={() => setType(null)}>\n        <AlertDialogContent\n          onCloseAutoFocus={(event) => {\n            // NOTE: bug where the body is not clickable after closing the alert dialog\n            event.preventDefault();\n            document.body.style.pointerEvents = \"\";\n          }}\n        >\n          <AlertDialogHeader>\n            <AlertDialogTitle>Confirm your action</AlertDialogTitle>\n            <AlertDialogDescription>\n              You are about to <span className=\"font-semibold\">{type}</span>{\" \"}\n              this incident.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Cancel</AlertDialogCancel>\n            <AlertDialogAction\n              onClick={(e) => {\n                e.preventDefault();\n                handleConfirm();\n              }}\n              disabled={isPending}\n            >\n              {isPending ? \"Confirming...\" : \"Confirm\"}\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/maintenances/columns.tsx",
    "content": "\"use client\";\n\nimport { ProcessMessage } from \"@/components/content/process-message\";\nimport { TableCellDate } from \"@/components/data-table/table-cell-date\";\nimport { TableCellNumber } from \"@/components/data-table/table-cell-number\";\nimport { DataTableColumnHeader } from \"@/components/ui/data-table/data-table-column-header\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport { formatDistanceStrict } from \"date-fns\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\ntype Maintenance = RouterOutputs[\"maintenance\"][\"list\"][number];\n\nexport const columns: ColumnDef<Maintenance>[] = [\n  {\n    accessorKey: \"title\",\n    header: \"Title\",\n    enableSorting: false,\n    enableHiding: false,\n    meta: {\n      cellClassName: \"max-w-[200px] truncate\",\n    },\n  },\n  {\n    accessorKey: \"message\",\n    header: \"Message\",\n    enableSorting: false,\n    enableHiding: false,\n    cell: ({ row }) => {\n      const value = String(row.getValue(\"message\"));\n      return (\n        <div className=\"prose dark:prose-invert prose-sm line-clamp-3 max-w-[200px] truncate text-muted-foreground\">\n          <ProcessMessage value={value} />\n        </div>\n      );\n    },\n  },\n  {\n    accessorKey: \"from\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Start Date\" />\n    ),\n    cell: ({ row }) => <TableCellDate value={row.getValue(\"from\")} />,\n    enableHiding: false,\n  },\n  {\n    id: \"duration\",\n    accessorFn: (row) => formatDistanceStrict(row.from, row.to),\n    header: \"Duration\",\n    cell: ({ row }) => {\n      const value = row.getValue(\"duration\");\n      if (typeof value === \"string\") {\n        const [amount, unit] = value.split(\" \");\n        return <TableCellNumber value={amount} unit={unit} />;\n      }\n      return <TableCellNumber value={value} />;\n    },\n  },\n  {\n    id: \"actions\",\n    cell: ({ row }) => <DataTableRowActions row={row} />,\n    meta: {\n      cellClassName: \"w-8\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/maintenances/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { FormSheetMaintenance } from \"@/components/forms/maintenance/sheet\";\nimport { getActions } from \"@/data/maintenances.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { Row } from \"@tanstack/react-table\";\nimport { useRef } from \"react\";\n\ntype Maintenance = RouterOutputs[\"maintenance\"][\"list\"][number];\n\ninterface DataTableRowActionsProps {\n  row: Row<Maintenance>;\n}\n\nexport function DataTableRowActions({ row }: DataTableRowActionsProps) {\n  const trpc = useTRPC();\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const actions = getActions({\n    edit: () => buttonRef.current?.click(),\n  });\n  const { data: statusPage } = useQuery(\n    trpc.page.get.queryOptions({ id: row.original.pageId ?? 0 }),\n  );\n  const queryClient = useQueryClient();\n  const updateMaintenanceMutation = useMutation(\n    trpc.maintenance.update.mutationOptions({\n      onSuccess: () => {\n        queryClient.refetchQueries({\n          queryKey: trpc.maintenance.list.queryKey({\n            pageId: row.original.pageId ?? undefined,\n          }),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.maintenance.list.queryKey({\n            period: \"7d\",\n          }),\n        });\n      },\n    }),\n  );\n\n  const deleteMaintenanceMutation = useMutation(\n    trpc.maintenance.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.refetchQueries({\n          queryKey: trpc.maintenance.list.queryKey({\n            pageId: row.original.pageId ?? undefined,\n          }),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.maintenance.list.queryKey({\n            period: \"7d\",\n          }),\n        });\n      },\n    }),\n  );\n\n  return (\n    <>\n      <QuickActions\n        actions={actions}\n        deleteAction={{\n          confirmationValue: row.original.title ?? \"maintenance\",\n          submitAction: async () => {\n            await deleteMaintenanceMutation.mutateAsync({\n              id: row.original.id,\n            });\n          },\n        }}\n      />\n      <FormSheetMaintenance\n        pageComponents={statusPage?.pageComponents ?? []}\n        defaultValues={{\n          title: row.original.title,\n          message: row.original.message,\n          startDate: row.original.from,\n          endDate: row.original.to,\n          pageComponents: row.original.pageComponents?.map((c) => c.id) ?? [],\n        }}\n        onSubmit={async (values) => {\n          await updateMaintenanceMutation.mutateAsync({\n            id: row.original.id,\n            title: values.title,\n            message: values.message,\n            startDate: values.startDate,\n            endDate: values.endDate,\n            pageComponents: values.pageComponents,\n          });\n        }}\n      >\n        <button ref={buttonRef} type=\"button\" className=\"sr-only\">\n          Open sheet\n        </button>\n      </FormSheetMaintenance>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/monitors/columns.tsx",
    "content": "\"use client\";\n\nimport { TableCellLink } from \"@/components/data-table/table-cell-link\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\nimport { DataTableColumnHeader } from \"@/components/ui/data-table/data-table-column-header\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport { TableCellSkeleton } from \"../dable-cell-skeleton\";\nimport { TableCellDate } from \"../table-cell-date\";\nimport { TableCellNumber } from \"../table-cell-number\";\nimport { TableCellUnavailable } from \"../table-cell-unavailable\";\n\ntype Monitor = RouterOutputs[\"monitor\"][\"list\"][number] & {\n  globalMetrics?:\n    | RouterOutputs[\"tinybird\"][\"globalMetrics\"][\"data\"][number]\n    // NOTE: after loading the data, if the monitor has no metrics, the value will be `false`\n    | false;\n};\n\nexport const columns: ColumnDef<Monitor>[] = [\n  {\n    id: \"select\",\n    header: ({ table }) => (\n      <Checkbox\n        checked={\n          table.getIsAllPageRowsSelected() ||\n          (table.getIsSomePageRowsSelected() && \"indeterminate\")\n        }\n        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}\n        aria-label=\"Select all\"\n      />\n    ),\n    cell: ({ row }) => (\n      <Checkbox\n        checked={row.getIsSelected()}\n        onCheckedChange={(value) => row.toggleSelected(!!value)}\n        aria-label=\"Select row\"\n      />\n    ),\n    enableSorting: false,\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"name\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Name\" />\n    ),\n    cell: ({ row }) => {\n      return (\n        <TableCellLink\n          value={row.getValue(\"name\")}\n          href={`/monitors/${row.original.id}/overview`}\n        />\n      );\n    },\n    enableHiding: false,\n    meta: {\n      cellClassName: \"max-w-[150px] min-w-max\",\n    },\n  },\n  {\n    accessorKey: \"url\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Endpoint\" />\n    ),\n    cell: ({ row }) => {\n      return (\n        <TableCellLink value={row.getValue(\"url\")} href={row.original.url} />\n      );\n    },\n    enableHiding: true,\n    meta: {\n      cellClassName: \"max-w-[150px] min-w-max\",\n    },\n  },\n  {\n    accessorKey: \"jobType\",\n    header: \"Type\",\n    enableHiding: true,\n  },\n  {\n    id: \"status\",\n    accessorFn: (row) => {\n      console.log(row);\n      return row.active ? row.status : \"inactive\";\n    },\n    header: \"Status\",\n    cell: ({ row }) => {\n      const value = String(row.getValue(\"status\"));\n\n      switch (value) {\n        case \"active\":\n          return <div className=\"font-mono text-success\">{value}</div>;\n        case \"degraded\":\n          return <div className=\"font-mono text-warning\">{value}</div>;\n        case \"error\":\n          return <div className=\"font-mono text-destructive\">{value}</div>;\n        default:\n          return <div className=\"font-mono text-muted-foreground\">{value}</div>;\n      }\n    },\n    filterFn: (row, _, value) => {\n      if (Array.isArray(value)) {\n        if (value.includes(\"inactive\")) {\n          return !row.original.active;\n        }\n        if (value.includes(\"active\")) {\n          return !!row.original.active && row.original.status === \"active\";\n        }\n        return value.includes(row.original.status);\n      }\n      return row.original.status === value;\n    },\n    enableSorting: false,\n    enableHiding: false,\n    enableGlobalFilter: false,\n  },\n  {\n    accessorKey: \"active\",\n    enableHiding: true,\n    enableGlobalFilter: false,\n  },\n  {\n    accessorKey: \"tags\",\n    header: \"Tags\",\n    cell: ({ row }) => {\n      const value = row.getValue(\"tags\");\n      if (!Array.isArray(value)) return null;\n      if (value.length === 0) {\n        return <div className=\"text-muted-foreground\">-</div>;\n      }\n      return (\n        <div className=\"group/badges -space-x-2 flex flex-wrap\">\n          {value.map((tag) => (\n            <Badge\n              key={tag.id}\n              variant=\"outline\"\n              className=\"relative flex translate-x-0 items-center gap-1.5 rounded-full bg-background transition-transform hover:z-10 hover:translate-x-1\"\n            >\n              <div\n                className=\"size-2.5 rounded-full\"\n                style={{ backgroundColor: tag.color }}\n              />\n              <span>{tag.name}</span>\n            </Badge>\n          ))}\n        </div>\n      );\n    },\n    filterFn: (row, _, value) => {\n      const tagIds = row.original.tags.map((tag) => tag.id);\n      if (Array.isArray(value)) {\n        return value.some((v) => tagIds.includes(v));\n      }\n      return tagIds.includes(value);\n    },\n    getUniqueValues: (row) => row.tags.map((tag) => tag.id),\n    enableSorting: false,\n    enableHiding: false,\n    enableGlobalFilter: false,\n  },\n  {\n    id: \"lastIncident\",\n    header: \"Last Incident\",\n    accessorFn: (row) => row.incidents?.[0]?.createdAt,\n    cell: ({ row }) => {\n      const value = row.getValue(\"lastIncident\");\n      return <TableCellDate value={value} formatStr=\"LLL dd, y\" />;\n    },\n    enableHiding: false,\n    enableGlobalFilter: false,\n  },\n  // {\n  //   id: \"uptime\",\n  //   accessorFn: (row) => `uptime-${row.id}`,\n  //   header: \"Last Week\",\n  //   cell: ({ row }) => {\n  //     return (\n  //       <ChartBarUptimeLight\n  //         monitorId={String(row.original.id)}\n  //         type={row.original.jobType as \"http\" | \"tcp\"}\n  //       />\n  //     );\n  //   },\n  //   enableHiding: false,\n  //   enableGlobalFilter: false,\n  // },\n  {\n    id: \"lastTimestamp\",\n    header: \"Last Checked\",\n    accessorFn: (row) =>\n      typeof row.globalMetrics === \"object\"\n        ? row.globalMetrics.lastTimestamp\n        : row.globalMetrics,\n    cell: ({ row }) => {\n      const value = row.getValue(\"lastTimestamp\");\n      if (value === undefined) return <TableCellSkeleton className=\"w-full\" />;\n      return (\n        <TableCellDate\n          value={\n            typeof value === \"number\"\n              ? formatDistanceToNow(new Date(value), { addSuffix: true })\n              : value\n          }\n        />\n      );\n    },\n    enableHiding: false,\n    enableGlobalFilter: false,\n  },\n  {\n    id: \"p50\",\n    accessorFn: (row) =>\n      typeof row.globalMetrics === \"object\"\n        ? row.globalMetrics.p50Latency\n        : row.globalMetrics,\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"P50\" />\n    ),\n    cell: ({ row }) => {\n      const value = row.getValue(\"p50\");\n      if (value === undefined) return <TableCellSkeleton />;\n      if (!value) return <TableCellUnavailable />;\n      return <TableCellNumber value={value} unit=\"ms\" />;\n    },\n    enableHiding: false,\n  },\n  {\n    id: \"p90\",\n    accessorFn: (row) =>\n      typeof row.globalMetrics === \"object\"\n        ? row.globalMetrics.p90Latency\n        : row.globalMetrics,\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"P90\" />\n    ),\n    cell: ({ row }) => {\n      const value = row.getValue(\"p90\");\n      if (value === undefined) return <TableCellSkeleton />;\n      if (!value) return <TableCellUnavailable />;\n      return <TableCellNumber value={value} unit=\"ms\" />;\n    },\n    enableHiding: false,\n    enableGlobalFilter: false,\n  },\n  {\n    id: \"p95\",\n    accessorFn: (row) =>\n      typeof row.globalMetrics === \"object\"\n        ? row.globalMetrics.p95Latency\n        : row.globalMetrics,\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"P95\" />\n    ),\n    cell: ({ row }) => {\n      const value = row.getValue(\"p95\");\n      if (value === undefined) return <TableCellSkeleton />;\n      if (!value) return <TableCellUnavailable />;\n      return <TableCellNumber value={value} unit=\"ms\" />;\n    },\n    enableHiding: false,\n    enableGlobalFilter: false,\n  },\n  {\n    id: \"actions\",\n    cell: ({ row }) => <DataTableRowActions row={row} />,\n    meta: {\n      cellClassName: \"w-8\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/monitors/data-table-action-bar.tsx",
    "content": "\"use client\";\n\nimport { SelectTrigger } from \"@radix-ui/react-select\";\nimport type { Table } from \"@tanstack/react-table\";\nimport { Check, CheckCircle2, Copy, Trash2 } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport {\n  DataTableActionBar,\n  DataTableActionBarAction,\n  DataTableActionBarSelection,\n} from \"@/components/ui/data-table/data-table-action-bar\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n} from \"@openstatus/ui/components/ui/select\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { toast } from \"sonner\";\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\n\ntype Monitor = RouterOutputs[\"monitor\"][\"list\"][number];\n\nconst ACTIVE = [\n  { label: \"active\", value: true },\n  { label: \"inactive\", value: false },\n];\n\ninterface MonitorDataTableActionBarProps {\n  table: Table<Monitor>;\n}\n\nexport function MonitorDataTableActionBar({\n  table,\n}: MonitorDataTableActionBarProps) {\n  const [open, setOpen] = React.useState(false);\n  const [isPending, startTransition] = React.useTransition();\n  const [value, setValue] = React.useState(\"\");\n  const { copy, isCopied } = useCopyToClipboard();\n  const rows = table.getFilteredSelectedRowModel().rows;\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const deleteMonitorsMutation = useMutation(\n    trpc.monitor.deleteMonitors.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.monitor.list.queryKey(),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.workspace.get.queryKey(),\n        });\n        // Clear selection once deletion succeeds\n        table.toggleAllRowsSelected(false);\n      },\n    }),\n  );\n\n  const updateMonitorsMutation = useMutation(\n    trpc.monitor.updateMonitors.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.monitor.list.queryKey(),\n        });\n      },\n    }),\n  );\n\n  const confirmationValue = React.useMemo(\n    () => rows.map((row) => row.original.name).join(\", \"),\n    [rows],\n  );\n\n  const handleDelete = async () => {\n    try {\n      startTransition(async () => {\n        const promise = deleteMonitorsMutation.mutateAsync({\n          ids: rows.map((row) => row.original.id),\n        });\n        toast.promise(promise, {\n          loading: \"Deleting...\",\n          success: \"Deleted\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to delete\";\n          },\n        });\n        await promise;\n        setOpen(false);\n      });\n    } catch (error) {\n      console.error(\"Failed to delete:\", error);\n    }\n  };\n\n  return (\n    <DataTableActionBar table={table} visible={rows.length > 0}>\n      <DataTableActionBarSelection table={table} />\n      <Separator\n        orientation=\"vertical\"\n        className=\"hidden data-[orientation=vertical]:h-5 sm:block\"\n      />\n      <div className=\"flex items-center gap-1.5\">\n        <Select\n          onValueChange={(v) => {\n            toast.promise(\n              updateMonitorsMutation.mutateAsync({\n                ids: rows.map((row) => row.original.id),\n                active: v === \"active\",\n              }),\n              {\n                loading: \"Updating...\",\n                success: \"Updated\",\n                error: (error) => {\n                  if (isTRPCClientError(error)) {\n                    return error.message;\n                  }\n                  return \"Failed to update\";\n                },\n              },\n            );\n          }}\n        >\n          <SelectTrigger asChild>\n            <DataTableActionBarAction size=\"icon\" tooltip=\"Update status\">\n              <CheckCircle2 />\n            </DataTableActionBarAction>\n          </SelectTrigger>\n          <SelectContent align=\"center\">\n            <SelectGroup>\n              {ACTIVE.map((status) => (\n                <SelectItem\n                  key={status.label}\n                  value={status.label}\n                  className=\"capitalize\"\n                >\n                  {status.label}\n                </SelectItem>\n              ))}\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n        <AlertDialog open={open} onOpenChange={setOpen}>\n          <AlertDialogTrigger asChild>\n            <DataTableActionBarAction\n              size=\"icon\"\n              tooltip=\"Delete monitors\"\n              isPending={isPending || deleteMonitorsMutation.isPending}\n            >\n              <Trash2 />\n            </DataTableActionBarAction>\n          </AlertDialogTrigger>\n          <AlertDialogContent\n            onCloseAutoFocus={(event) => {\n              // Work-around: body becomes unclickable after closing the dialog\n              event.preventDefault();\n              document.body.style.pointerEvents = \"\";\n            }}\n          >\n            <AlertDialogHeader>\n              <AlertDialogTitle>\n                Delete {rows.length} monitor{rows.length > 1 ? \"s\" : \"\"}?\n              </AlertDialogTitle>\n              <AlertDialogDescription>\n                This action cannot be undone. This will permanently remove the\n                selected monitor(s) from the database.\n              </AlertDialogDescription>\n            </AlertDialogHeader>\n            <form id=\"form-alert-dialog\" className=\"space-y-1.5\">\n              <p className=\"text-muted-foreground text-sm\">\n                Type{\" \"}\n                <Button\n                  variant=\"secondary\"\n                  size=\"sm\"\n                  type=\"button\"\n                  className=\"font-normal [&_svg]:size-3\"\n                  onClick={() => copy(confirmationValue, { withToast: false })}\n                >\n                  {confirmationValue}\n                  {isCopied ? <Check /> : <Copy />}\n                </Button>{\" \"}\n                to confirm\n              </p>\n              <Input value={value} onChange={(e) => setValue(e.target.value)} />\n            </form>\n            <AlertDialogFooter>\n              <AlertDialogCancel onClick={(e) => e.stopPropagation()}>\n                Cancel\n              </AlertDialogCancel>\n              <AlertDialogAction\n                className=\"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\"\n                disabled={value !== confirmationValue || isPending}\n                form=\"form-alert-dialog\"\n                type=\"submit\"\n                onClick={(e) => {\n                  e.preventDefault();\n                  handleDelete();\n                }}\n              >\n                {isPending ? \"Deleting...\" : \"Delete\"}\n              </AlertDialogAction>\n            </AlertDialogFooter>\n          </AlertDialogContent>\n        </AlertDialog>\n      </div>\n    </DataTableActionBar>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/monitors/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport { ExportCodeDialog } from \"@/components/dialogs/export-code\";\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { getActions } from \"@/data/monitors.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport type { Row } from \"@tanstack/react-table\";\nimport { useRouter } from \"next/navigation\";\nimport { useState } from \"react\";\nimport { toast } from \"sonner\";\n\ntype Monitor = RouterOutputs[\"monitor\"][\"list\"][number];\ninterface DataTableRowActionsProps {\n  row: Row<Monitor>;\n}\n\nexport function DataTableRowActions({ row }: DataTableRowActionsProps) {\n  const [openDialog, setOpenDialog] = useState(false);\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const deleteMonitorMutation = useMutation(\n    trpc.monitor.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries(trpc.monitor.list.queryOptions());\n      },\n    }),\n  );\n  const router = useRouter();\n  const actions = getActions({\n    edit: () => router.push(`/monitors/${row.original.id}/edit`),\n    \"copy-id\": () => {\n      navigator.clipboard.writeText(row.original.id.toString());\n      toast.success(\"Monitor ID copied to clipboard\");\n    },\n    // export: () => setOpenDialog(true),\n  });\n\n  return (\n    <>\n      <QuickActions\n        actions={actions}\n        deleteAction={{\n          confirmationValue: row.original.name ?? \"monitor\",\n          submitAction: async () => {\n            await deleteMonitorMutation.mutateAsync({\n              id: row.original.id,\n            });\n          },\n        }}\n      />\n      <ExportCodeDialog open={openDialog} onOpenChange={setOpenDialog} />\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/monitors/data-table-toolbar.tsx",
    "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\n\nimport { DataTableFacetedFilter } from \"@/components/ui/data-table/data-table-faceted-filter\";\nimport type { RouterOutputs } from \"@openstatus/api\";\n\ntype Monitor = RouterOutputs[\"monitor\"][\"list\"][number];\ntype MonitorTag = RouterOutputs[\"monitorTag\"][\"list\"][number];\n\nexport interface MonitorDataTableToolbarProps {\n  table: Table<Monitor>;\n  tags: MonitorTag[];\n}\n\nexport function MonitorDataTableToolbar({\n  table,\n  tags,\n}: MonitorDataTableToolbarProps) {\n  const isFiltered = table.getState().columnFilters.length > 0;\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex flex-1 flex-wrap items-center space-x-2\">\n        <Input\n          placeholder=\"Filter by name, url, type...\"\n          value={(table.getState().globalFilter as string) ?? \"\"}\n          onChange={(event) => table.setGlobalFilter(event.target.value)}\n          className=\"h-8 w-[150px] lg:w-[250px]\"\n        />\n        {table.getColumn(\"tags\") && (\n          <DataTableFacetedFilter\n            column={table.getColumn(\"tags\")}\n            title=\"Tags\"\n            options={tags.map((tag) => ({\n              label: tag.name,\n              value: tag.id,\n            }))}\n          />\n        )}\n        {isFiltered && (\n          <Button\n            variant=\"ghost\"\n            onClick={() => table.resetColumnFilters()}\n            className=\"h-8 px-2 lg:px-3\"\n          >\n            Reset\n            <X />\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/notifications/columns.tsx",
    "content": "\"use client\";\n\nimport { DataTableColumnHeader } from \"@/components/ui/data-table/data-table-column-header\";\nimport { type NotifierProvider, config } from \"@/data/notifications.client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport Link from \"next/link\";\nimport { TableCellBadge } from \"../table-cell-badge\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\ntype Notifier = RouterOutputs[\"notification\"][\"list\"][number];\n\nexport const columns: ColumnDef<Notifier>[] = [\n  {\n    accessorKey: \"name\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Name\" />\n    ),\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"provider\",\n    header: \"Provider\",\n    enableSorting: false,\n    enableHiding: false,\n    cell: ({ row }) => {\n      const provider = row.getValue(\"provider\") as NotifierProvider;\n      const Icon = config[provider].icon;\n      return (\n        <Badge variant=\"secondary\" className=\"px-1.5 font-mono text-[10px]\">\n          <Icon className=\"size-2.5\" />\n          {config[provider].label}\n        </Badge>\n      );\n    },\n  },\n  {\n    accessorKey: \"monitors\",\n    header: \"Monitors\",\n    enableSorting: false,\n    enableHiding: false,\n    cell: ({ row }) => {\n      const value = row.getValue(\"monitors\");\n      if (Array.isArray(value) && value.length > 0 && \"name\" in value[0]) {\n        return (\n          <div className=\"flex flex-wrap gap-1\">\n            {value.map((m) => (\n              <Link href={`/monitors/${m.id}`} key={m.id}>\n                <TableCellBadge value={m.name} />\n              </Link>\n            ))}\n          </div>\n        );\n      }\n      return <span className=\"text-muted-foreground\">-</span>;\n    },\n    meta: {\n      cellClassName: \"tabular-nums font-mono\",\n    },\n  },\n  {\n    id: \"actions\",\n    cell: ({ row }) => <DataTableRowActions row={row} />,\n    meta: {\n      cellClassName: \"w-8\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/notifications/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { FormSheetNotifier } from \"@/components/forms/notifications/sheet\";\nimport { getActions } from \"@/data/notifications.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport type { Row } from \"@tanstack/react-table\";\nimport { useRef } from \"react\";\n\ntype Notifier = RouterOutputs[\"notification\"][\"list\"][number];\n\ninterface DataTableRowActionsProps {\n  row: Row<Notifier>;\n}\n\nexport function DataTableRowActions(props: DataTableRowActionsProps) {\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const actions = getActions({\n    edit: () => buttonRef.current?.click(),\n  });\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());\n  const updateNotifierMutation = useMutation(\n    trpc.notification.updateNotifier.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.notification.list.queryKey(),\n        });\n      },\n    }),\n  );\n  const deleteNotifierMutation = useMutation(\n    trpc.notification.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.notification.list.queryKey(),\n        });\n      },\n    }),\n  );\n\n  return (\n    <>\n      <QuickActions\n        actions={actions}\n        deleteAction={{\n          confirmationValue: props.row.original.name ?? \"notifier\",\n          submitAction: async () => {\n            await deleteNotifierMutation.mutateAsync({\n              id: props.row.original.id,\n            });\n          },\n        }}\n      />\n      <FormSheetNotifier\n        provider={props.row.original.provider}\n        defaultValues={{\n          name: props.row.original.name,\n          provider: props.row.original.provider,\n          // TBD: parse it?\n          data: JSON.parse(props.row.original.data ?? \"{}\"),\n          monitors: props.row.original.monitors.map((m) => m.id),\n        }}\n        monitors={monitors ?? []}\n        onSubmit={async (values) => {\n          await updateNotifierMutation.mutateAsync({\n            id: props.row.original.id,\n            name: values.name,\n            data: { [values.provider]: values.data },\n            monitors: values.monitors,\n          });\n        }}\n      >\n        <button ref={buttonRef} type=\"button\" className=\"sr-only\">\n          Open sheet\n        </button>\n      </FormSheetNotifier>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/page-components/columns.tsx",
    "content": "\"use client\";\n\nimport { DataTableColumnHeader } from \"@/components/ui/data-table/data-table-column-header\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\ntype PageComponent = RouterOutputs[\"pageComponent\"][\"list\"][number];\n\nexport const columns: ColumnDef<PageComponent>[] = [\n  {\n    accessorKey: \"name\",\n    header: \"Name\",\n    enableSorting: false,\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"description\",\n    header: \"Description\",\n    enableSorting: false,\n    cell: ({ row }) => {\n      const value = row.getValue(\"description\");\n      return (\n        <span className=\"max-w-[200px] truncate text-muted-foreground\">\n          {value ? String(value) : \"-\"}\n        </span>\n      );\n    },\n  },\n  {\n    accessorKey: \"type\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Type\" />\n    ),\n    cell: ({ row }) => {\n      const value = row.getValue(\"type\");\n      return <span className=\"capitalize\">{String(value)}</span>;\n    },\n  },\n  {\n    accessorKey: \"order\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Order\" />\n    ),\n    cell: ({ row }) => {\n      const value = row.getValue(\"order\");\n      return <span>{value != null ? String(value) : \"-\"}</span>;\n    },\n  },\n  {\n    id: \"actions\",\n    cell: ({ row }) => <DataTableRowActions row={row} />,\n    meta: {\n      cellClassName: \"w-8\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/page-components/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { getActions } from \"@/data/page-components.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport type { Row } from \"@tanstack/react-table\";\n\ntype PageComponent = RouterOutputs[\"pageComponent\"][\"list\"][number];\n\ninterface DataTableRowActionsProps {\n  row: Row<PageComponent>;\n}\n\nexport function DataTableRowActions({ row }: DataTableRowActionsProps) {\n  const trpc = useTRPC();\n  const actions = getActions({});\n  const queryClient = useQueryClient();\n\n  const deletePageComponentMutation = useMutation(\n    trpc.pageComponent.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.refetchQueries({\n          queryKey: trpc.pageComponent.list.queryKey({\n            pageId: row.original.pageId ?? undefined,\n          }),\n        });\n      },\n    }),\n  );\n\n  return (\n    <QuickActions\n      actions={actions}\n      deleteAction={{\n        confirmationValue: row.original.name ?? \"component\",\n        submitAction: async () => {\n          await deletePageComponentMutation.mutateAsync({\n            id: row.original.id,\n          });\n        },\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/private-locations/columns.tsx",
    "content": "\"use client\";\n\nimport { DataTableColumnHeader } from \"@/components/ui/data-table/data-table-column-header\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport Link from \"next/link\";\nimport { TableCellBadge } from \"../table-cell-badge\";\nimport { TableCellDate } from \"../table-cell-date\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\ntype PrivateLocation = RouterOutputs[\"privateLocation\"][\"list\"][number];\n\nexport const columns: ColumnDef<PrivateLocation>[] = [\n  {\n    accessorKey: \"name\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Name\" />\n    ),\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"lastSeenAt\",\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Last Seen At\" />\n    ),\n    enableHiding: false,\n    cell: ({ row }) => {\n      const value = row.getValue(\"lastSeenAt\");\n      return <TableCellDate value={value} />;\n    },\n  },\n  {\n    accessorKey: \"monitors\",\n    header: \"Monitors\",\n    enableSorting: false,\n    enableHiding: false,\n\n    cell: ({ row }) => {\n      const value = row.getValue(\"monitors\");\n      if (Array.isArray(value) && value.length > 0 && \"name\" in value[0]) {\n        return (\n          <div className=\"flex flex-wrap gap-1\">\n            {value.map((m) => (\n              <Link href={`/monitors/${m.id}`} key={m.id}>\n                <TableCellBadge value={m.name} />\n              </Link>\n            ))}\n          </div>\n        );\n      }\n      return <span className=\"text-muted-foreground\">-</span>;\n    },\n    meta: {\n      cellClassName: \"tabular-nums font-mono\",\n    },\n  },\n  {\n    id: \"actions\",\n    cell: ({ row }) => <DataTableRowActions row={row} />,\n    meta: {\n      cellClassName: \"w-8\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/private-locations/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { FormSheetPrivateLocation } from \"@/components/forms/private-location/sheet\";\nimport { getActions } from \"@/data/notifications.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport type { Row } from \"@tanstack/react-table\";\nimport { useRef } from \"react\";\n\ntype PrivateLocation = RouterOutputs[\"privateLocation\"][\"list\"][number];\n\ninterface DataTableRowActionsProps {\n  row: Row<PrivateLocation>;\n}\n\nexport function DataTableRowActions(props: DataTableRowActionsProps) {\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const actions = getActions({\n    edit: () => buttonRef.current?.click(),\n  });\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());\n  const updatePrivateLocationMutation = useMutation(\n    trpc.privateLocation.update.mutationOptions({\n      onSuccess: async () => {\n        await queryClient.invalidateQueries({\n          queryKey: trpc.privateLocation.list.queryKey(),\n        });\n      },\n    }),\n  );\n  const deletePrivateLocationMutation = useMutation(\n    trpc.privateLocation.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.privateLocation.list.queryKey(),\n        });\n      },\n    }),\n  );\n\n  return (\n    <>\n      <QuickActions\n        actions={actions}\n        deleteAction={{\n          confirmationValue: props.row.original.name ?? \"private location\",\n          submitAction: async () => {\n            await deletePrivateLocationMutation.mutateAsync({\n              id: props.row.original.id,\n            });\n          },\n        }}\n      />\n      <FormSheetPrivateLocation\n        defaultValues={{\n          name: props.row.original.name,\n          token: props.row.original.token.toString(),\n          monitors: props.row.original.monitors.map((m) => m.id),\n        }}\n        monitors={monitors ?? []}\n        onSubmit={async (values) => {\n          await updatePrivateLocationMutation.mutateAsync({\n            id: props.row.original.id,\n            name: values.name,\n            monitors: values.monitors,\n          });\n        }}\n      >\n        <button ref={buttonRef} type=\"button\" className=\"sr-only\">\n          Open sheet\n        </button>\n      </FormSheetPrivateLocation>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/response-logs/columns.tsx",
    "content": "\"use client\";\n\nimport { HoverCardTimestamp } from \"@/components/common/hover-card-timestamp\";\nimport { TableCellDate } from \"@/components/data-table/table-cell-date\";\nimport { TableCellNumber } from \"@/components/data-table/table-cell-number\";\nimport { getStatusCodeVariant, textColors } from \"@/data/status-codes\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport type { PrivateLocation } from \"@openstatus/db/src/schema\";\nimport { getRegionInfo } from \"@openstatus/regions\";\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@openstatus/ui/components/ui/hover-card\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport { Clock, Workflow } from \"lucide-react\";\n\ntype ResponseLog = RouterOutputs[\"tinybird\"][\"list\"][\"data\"][number];\n\n// export const columns: ColumnDef<ResponseLog>[] =\nexport function getColumns(\n  privateLocations: PrivateLocation[],\n): ColumnDef<ResponseLog>[] {\n  return [\n    {\n      accessorKey: \"requestStatus\",\n      header: () => null,\n      enableSorting: false,\n      enableHiding: false,\n      cell: ({ row }) => {\n        const value = row.getValue(\"requestStatus\");\n        if (value === \"error\") {\n          return <div className=\"h-2.5 w-2.5 rounded-[2px] bg-destructive\" />;\n        }\n        if (value === \"degraded\") {\n          return <div className=\"h-2.5 w-2.5 rounded-[2px] bg-warning\" />;\n        }\n        if (value === \"success\") {\n          return <div className=\"h-2.5 w-2.5 rounded-[2px] bg-success\" />;\n        }\n        return <div className=\"text-muted-foreground\">-</div>;\n      },\n    },\n    {\n      accessorKey: \"timestamp\",\n      header: \"Timestamp\",\n      enableSorting: false,\n      enableHiding: false,\n      cell: ({ row }) => {\n        const value = new Date(row.getValue(\"timestamp\"));\n        return (\n          <HoverCardTimestamp date={value}>\n            <TableCellDate\n              value={value}\n              className=\"font-mono text-foreground\"\n            />\n          </HoverCardTimestamp>\n        );\n      },\n    },\n    {\n      accessorKey: \"statusCode\",\n      header: \"Status\",\n      enableSorting: false,\n      enableHiding: false,\n      cell: ({ row }) => {\n        const log = row.original;\n        if (log.type === \"http\") {\n          const value = log.statusCode;\n          const variant = getStatusCodeVariant(value);\n          return (\n            <TableCellNumber value={value} className={textColors[variant]} />\n          );\n        }\n        return <div className=\"text-muted-foreground\">-</div>;\n      },\n    },\n    {\n      accessorKey: \"latency\",\n      header: \"Latency\",\n      enableSorting: false,\n      enableHiding: false,\n      cell: ({ row }) => {\n        return <TableCellNumber value={row.getValue(\"latency\")} unit=\"ms\" />;\n      },\n    },\n    {\n      accessorKey: \"region\",\n      header: \"Region\",\n      cell: ({ row }) => {\n        const value = row.getValue(\"region\");\n\n        if (typeof value !== \"string\") {\n          return <div className=\"text-muted-foreground\">-</div>;\n        }\n\n        const regionConfig = getRegionInfo(value, {\n          location: privateLocations.find(\n            (location) => String(location.id) === String(value),\n          )?.name,\n        });\n\n        return (\n          <div>\n            {regionConfig.location}{\" \"}\n            <span className=\"text-muted-foreground/70 text-xs\">\n              ({regionConfig.provider})\n            </span>\n          </div>\n        );\n      },\n      enableSorting: false,\n      enableHiding: false,\n      filterFn: \"arrIncludesSome\",\n      meta: {\n        cellClassName: \"text-muted-foreground font-mono\",\n      },\n    },\n    {\n      accessorKey: \"timing\",\n      header: \"Timing\",\n      cell: ({ row }) => {\n        const log = row.original;\n        if (log.type === \"http\" && log.timing) {\n          return <HoverCardTiming timing={log.timing} latency={log.latency} />;\n        }\n        return <div className=\"text-muted-foreground\">-</div>;\n      },\n      enableSorting: false,\n      enableHiding: false,\n    },\n    {\n      accessorKey: \"trigger\",\n      header: \"Trigger\",\n      cell: ({ row }) => {\n        const value = row.getValue(\"trigger\");\n        if (value === \"cron\" || value === \"api\") {\n          const Icon = value === \"cron\" ? Clock : Workflow;\n          const label = value === \"cron\" ? \"Scheduled\" : \"API\";\n          return (\n            <TooltipProvider>\n              <Tooltip>\n                <TooltipTrigger>\n                  <Icon className=\"size-3 text-muted-foreground\" />\n                </TooltipTrigger>\n                <TooltipContent side=\"right\">\n                  <p>{label}</p>\n                </TooltipContent>\n              </Tooltip>\n            </TooltipProvider>\n          );\n        }\n        return <div className=\"text-muted-foreground\">-</div>;\n      },\n      enableSorting: false,\n      enableHiding: false,\n      meta: {\n        cellClassName: \"font-mono\",\n        headerClassName: \"sr-only\",\n      },\n    },\n  ];\n}\n\nfunction HoverCardTiming({\n  timing,\n  latency,\n}: {\n  timing: NonNullable<Extract<ResponseLog, { type: \"http\" }>[\"timing\"]>;\n  latency: number;\n}) {\n  return (\n    <HoverCard openDelay={50} closeDelay={50}>\n      <HoverCardTrigger\n        className=\"opacity-70 hover:opacity-100 data-[state=open]:opacity-100\"\n        asChild\n      >\n        <div className=\"flex\">\n          {Object.entries(timing).map(([key, value], index) => (\n            <div\n              key={key}\n              className={cn(\"h-4\")}\n              style={{\n                width: `${(value / latency) * 100}%`,\n                backgroundColor: `var(--chart-${index + 1})`,\n              }}\n            />\n          ))}\n        </div>\n      </HoverCardTrigger>\n      <HoverCardContent side=\"bottom\" align=\"end\" className=\"z-10 w-auto p-2\">\n        <HoverCardTimingContent {...{ latency, timing }} />\n      </HoverCardContent>\n    </HoverCard>\n  );\n}\n\nfunction HoverCardTimingContent({\n  timing,\n  latency,\n}: {\n  timing: NonNullable<Extract<ResponseLog, { type: \"http\" }>[\"timing\"]>;\n  latency: number;\n}) {\n  return (\n    <div className=\"flex flex-col gap-1\">\n      {Object.entries(timing).map(([key, value], index) => {\n        return (\n          <div key={key} className=\"grid grid-cols-2 gap-4 text-xs\">\n            <div className=\"flex items-center gap-2\">\n              <div\n                className={cn(\"h-2 w-2 rounded-full\")}\n                style={{ backgroundColor: `var(--chart-${index + 1})` }}\n              />\n              <div className=\"font-mono text-accent-foreground uppercase\">\n                {key}\n              </div>\n            </div>\n            <div className=\"flex items-center justify-between gap-4\">\n              <div className=\"font-mono text-muted-foreground\">\n                {`${new Intl.NumberFormat(\"en-US\", {\n                  maximumFractionDigits: 2,\n                }).format((value / latency) * 100)}%`}\n              </div>\n              <div className=\"font-mono\">\n                {new Intl.NumberFormat(\"en-US\", {\n                  maximumFractionDigits: 3,\n                }).format(value)}\n                <span className=\"text-muted-foreground\">ms</span>\n              </div>\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/response-logs/data-table-basics.tsx",
    "content": "\"use client\";\n\nimport { IconCloudProvider } from \"@/components/common/icon-cloud-provider\";\nimport { BlockWrapper } from \"@/components/content/block-wrapper\";\nimport { TableCellDate } from \"@/components/data-table/table-cell-date\";\nimport { TableCellNumber } from \"@/components/data-table/table-cell-number\";\nimport { getStatusCodeVariant, textColors } from \"@/data/status-codes\";\nimport { formatMilliseconds, formatPercentage } from \"@/lib/formatter\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport type { PrivateLocation } from \"@openstatus/db/src/schema\";\nimport { getRegionInfo } from \"@openstatus/regions\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Braces, TableProperties } from \"lucide-react\";\n\ntype ResponseLog = RouterOutputs[\"tinybird\"][\"get\"][\"data\"][number];\n\nexport function DataTableBasics({\n  data,\n  privateLocations,\n}: {\n  data: ResponseLog;\n  privateLocations?: PrivateLocation[];\n}) {\n  if (data.type === \"http\") {\n    return (\n      <DataTableBasicsHTTP data={data} privateLocations={privateLocations} />\n    );\n  }\n  if (data.type === \"tcp\") {\n    return (\n      <DataTableBasicsTCP data={data} privateLocations={privateLocations} />\n    );\n  }\n  if (data.type === \"dns\") {\n    return (\n      <DataTableBasicsDNS data={data} privateLocations={privateLocations} />\n    );\n  }\n  return null;\n}\n\nexport function DataTableBasicsHTTP({\n  data,\n  privateLocations,\n}: {\n  data: Extract<ResponseLog, { type: \"http\" }> & {\n    trigger?: \"cron\" | \"api\" | \"test\" | null;\n  };\n  privateLocations?: PrivateLocation[];\n}) {\n  const privateLocataion = privateLocations?.find(\n    (location) => String(location.id) === String(data.region),\n  );\n  const regionConfig = getRegionInfo(data.region, {\n    location: privateLocataion?.name,\n  });\n  return (\n    <Table className=\"table-fixed\">\n      <colgroup>\n        <col className=\"w-1/3\" />\n        <col className=\"w-2/3\" />\n      </colgroup>\n      <TableBody>\n        <TableRow>\n          <TableHead colSpan={2}>Request</TableHead>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Result\n          </TableHead>\n          {/* TODO: add colored square like list (see columns) */}\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <div className=\"flex items-center gap-2\">\n              <div\n                className={cn(\"h-2.5 w-2.5 rounded-[2px] bg-muted\", {\n                  \"bg-destructive\": data?.requestStatus === \"error\",\n                  \"bg-warning\": data?.requestStatus === \"degraded\",\n                  \"bg-success\": data?.requestStatus === \"success\",\n                })}\n              />\n              <div className=\"capitalize\">\n                {data?.requestStatus ?? \"unknown\"}\n              </div>\n            </div>\n          </TableCell>\n        </TableRow>\n        {data.id ? (\n          <TableRow className=\"[&>:not(:last-child)]:border-r\">\n            <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n              ID\n            </TableHead>\n            <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n              {data.id}\n            </TableCell>\n          </TableRow>\n        ) : null}\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Timestamp\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <TableCellDate\n              value={new Date(data.cronTimestamp)}\n              className=\"text-foreground\"\n            />\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            URL\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            {data.url}\n          </TableCell>\n        </TableRow>\n        {/* TODO: store method in TB 🤦 */}\n        {/* <TableRow className=\"[&>:not(:last-child)]:border-r\">\n        <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Method\n        </TableHead>\n        <TableCell className=\"whitespace-normal font-mono\">\n            {data?.method}\n        </TableCell>\n        </TableRow> */}\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Status\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <TableCellNumber\n              value={data.statusCode}\n              className={textColors[getStatusCodeVariant(data.statusCode)]}\n            />\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Latency\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <TableCellNumber value={data?.latency} unit=\"ms\" />\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Region\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            {regionConfig?.code}{\" \"}\n            <span className=\"text-muted-foreground text-xs\">\n              {regionConfig?.location} {regionConfig?.flag}\n            </span>\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Cloud Provider\n          </TableHead>\n          <TableCell className=\"inline-flex max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <IconCloudProvider\n              provider={regionConfig?.provider}\n              className=\"mt-0.5\"\n            />\n            <span className=\"ml-1 text-muted-foreground\">\n              {regionConfig?.provider}\n            </span>\n          </TableCell>\n        </TableRow>\n        {data.trigger ? (\n          <TableRow className=\"[&>:not(:last-child)]:border-r\">\n            <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n              Trigger\n            </TableHead>\n            <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n              {data?.trigger}\n            </TableCell>\n          </TableRow>\n        ) : null}\n        {data.headers ? (\n          <>\n            <TableRow>\n              <TableHead colSpan={2}>Headers</TableHead>\n            </TableRow>\n            <TableRow className=\"hover:bg-transparent\">\n              <TableCell colSpan={2} className=\"p-0\">\n                <Tabs defaultValue=\"table\" className=\"w-full gap-0\">\n                  <TabsList className=\"w-full justify-start rounded-none border-b px-2\">\n                    <TabsTrigger value=\"table\">\n                      <TableProperties className=\"size-3 rotate-180\" />\n                    </TabsTrigger>\n                    <TabsTrigger value=\"raw\">\n                      <Braces className=\"size-3\" />\n                    </TabsTrigger>\n                  </TabsList>\n                  <TabsContent value=\"table\">\n                    <Table className=\"table-fixed\">\n                      <colgroup>\n                        <col className=\"w-1/3\" />\n                        <col className=\"w-2/3\" />\n                      </colgroup>\n                      <TableBody>\n                        {Object.entries(data?.headers ?? {}).map(\n                          ([key, value]) => (\n                            <TableRow\n                              key={key}\n                              className=\"[&>:not(:last-child)]:border-r\"\n                            >\n                              <TableHead className=\"overflow-x-auto bg-muted/50 font-normal text-muted-foreground\">\n                                {key}\n                              </TableHead>\n                              <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n                                {value}\n                              </TableCell>\n                            </TableRow>\n                          ),\n                        )}\n                      </TableBody>\n                    </Table>\n                  </TabsContent>\n                  <TabsContent value=\"raw\">\n                    <pre className=\"max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-4 font-mono text-sm\">\n                      {JSON.stringify(data?.headers, null, 2)}\n                    </pre>\n                  </TabsContent>\n                </Tabs>\n              </TableCell>\n            </TableRow>\n          </>\n        ) : null}\n        {data.timing ? (\n          <>\n            <TableRow>\n              <TableHead colSpan={2}>Timing</TableHead>\n            </TableRow>\n            {Object.entries(data?.timing ?? {}).map(([key, value], index) => (\n              <TableRow key={key} className=\"[&>:not(:last-child)]:border-r\">\n                <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n                  <span className=\"uppercase\">{key}</span>\n                </TableHead>\n                <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <div className=\"flex-1\">\n                      <span className=\"text-muted-foreground\">\n                        {formatPercentage(value / (data?.latency || 100))}\n                      </span>\n                    </div>\n                    <div className=\"flex w-full flex-1 items-center justify-end gap-2\">\n                      <span className=\"text-nowrap text-muted-foreground\">\n                        {formatMilliseconds(value)}\n                      </span>\n                      <div\n                        className=\"h-4\"\n                        style={{\n                          width: `${(value / (data?.latency || 100)) * 100}%`,\n                          backgroundColor: `var(--chart-${index + 1})`,\n                        }}\n                      />\n                    </div>\n                  </div>\n                </TableCell>\n              </TableRow>\n            ))}\n          </>\n        ) : null}\n        {data?.message ? (\n          <>\n            <TableRow>\n              <TableHead colSpan={2}>Message</TableHead>\n            </TableRow>\n            <TableRow>\n              <TableCell colSpan={2} className=\"p-0\">\n                <pre className=\"max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm\">\n                  {data.message}\n                </pre>\n              </TableCell>\n            </TableRow>\n          </>\n        ) : null}\n        {data.body ? (\n          <>\n            <TableRow>\n              <TableHead colSpan={2}>Body</TableHead>\n            </TableRow>\n            <TableRow>\n              <TableCell colSpan={2} className=\"p-0\">\n                <BlockWrapper autoOpen>\n                  <pre className=\"max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm\">\n                    {data.body}\n                  </pre>\n                </BlockWrapper>\n              </TableCell>\n            </TableRow>\n          </>\n        ) : null}\n        {data.assertions ? (\n          <>\n            <TableRow>\n              <TableHead colSpan={2}>Assertions</TableHead>\n            </TableRow>\n            <TableRow>\n              <TableCell colSpan={2} className=\"p-0\">\n                {!data.assertions || data.assertions === \"[]\" ? (\n                  <div className=\"p-2 font-mono text-muted-foreground text-sm\">\n                    Default status code 2xx assertion\n                  </div>\n                ) : (\n                  <pre className=\"max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm\">\n                    {JSON.stringify(data.assertions, null, 2)}\n                  </pre>\n                )}\n              </TableCell>\n            </TableRow>\n          </>\n        ) : null}\n      </TableBody>\n    </Table>\n  );\n}\n\nexport function DataTableBasicsTCP({\n  data,\n  privateLocations,\n}: {\n  data: Extract<ResponseLog, { type: \"tcp\" }> & {\n    trigger?: \"cron\" | \"api\" | \"test\" | null;\n  };\n  privateLocations?: PrivateLocation[];\n}) {\n  const privateLocataion = privateLocations?.find(\n    (location) => String(location.id) === String(data.region),\n  );\n  const regionConfig = getRegionInfo(data.region, {\n    location: privateLocataion?.name,\n  });\n  return (\n    <Table className=\"table-fixed\">\n      <colgroup>\n        <col className=\"w-1/3\" />\n        <col className=\"w-2/3\" />\n      </colgroup>\n      <TableBody>\n        <TableRow>\n          <TableHead colSpan={2}>Request</TableHead>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Result\n          </TableHead>\n          {/* TODO: add colored square like list (see columns) */}\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <div className=\"flex items-center gap-2\">\n              <div\n                className={cn(\"h-2.5 w-2.5 rounded-[2px] bg-muted\", {\n                  \"bg-destructive\": data?.requestStatus === \"error\",\n                  \"bg-warning\": data?.requestStatus === \"degraded\",\n                  \"bg-success\": data?.requestStatus === \"success\",\n                })}\n              />\n              <div className=\"capitalize\">\n                {data?.requestStatus ?? \"unknown\"}\n              </div>\n            </div>\n          </TableCell>\n        </TableRow>\n        {data.id ? (\n          <TableRow className=\"[&>:not(:last-child)]:border-r\">\n            <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n              ID\n            </TableHead>\n            <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n              {data.id}\n            </TableCell>\n          </TableRow>\n        ) : null}\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Timestamp\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <TableCellDate\n              value={new Date(data.cronTimestamp)}\n              className=\"text-foreground\"\n            />\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            URI\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            {data.uri}\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Latency\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <TableCellNumber value={data?.latency} unit=\"ms\" />\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Region\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            {regionConfig?.flag} {regionConfig?.code}{\" \"}\n            <span className=\"text-muted-foreground\">\n              {regionConfig?.location}\n            </span>\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Cloud Provider\n          </TableHead>\n          <TableCell className=\"inline-flex max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <IconCloudProvider\n              provider={regionConfig?.provider}\n              className=\"mt-0.5\"\n            />\n            <span className=\"ml-1 text-muted-foreground\">\n              {regionConfig?.provider}\n            </span>\n          </TableCell>\n        </TableRow>\n        {data.trigger ? (\n          <TableRow className=\"[&>:not(:last-child)]:border-r\">\n            <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n              Trigger\n            </TableHead>\n            <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n              {data?.trigger}\n            </TableCell>\n          </TableRow>\n        ) : null}\n        {data?.errorMessage ? (\n          <>\n            <TableRow>\n              <TableHead colSpan={2}>Error Message</TableHead>\n            </TableRow>\n            <TableRow>\n              <TableCell colSpan={2} className=\"p-0\">\n                <pre className=\"max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm\">\n                  {data.errorMessage}\n                </pre>\n              </TableCell>\n            </TableRow>\n          </>\n        ) : null}\n      </TableBody>\n    </Table>\n  );\n}\n\nexport function DataTableBasicsDNS({\n  data,\n  privateLocations,\n}: {\n  data: Extract<ResponseLog, { type: \"dns\" }> & {\n    trigger?: \"cron\" | \"api\" | \"test\" | null;\n  };\n  privateLocations?: PrivateLocation[];\n}) {\n  const privateLocataion = privateLocations?.find(\n    (location) => String(location.id) === String(data.region),\n  );\n  const regionConfig = getRegionInfo(data.region, {\n    location: privateLocataion?.name,\n  });\n  return (\n    <Table className=\"table-fixed\">\n      <colgroup>\n        <col className=\"w-1/3\" />\n        <col className=\"w-2/3\" />\n      </colgroup>\n      <TableBody>\n        <TableRow>\n          <TableHead colSpan={2}>Request</TableHead>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Result\n          </TableHead>\n          {/* TODO: add colored square like list (see columns) */}\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <div className=\"flex items-center gap-2\">\n              <div\n                className={cn(\"h-2.5 w-2.5 rounded-[2px] bg-muted\", {\n                  \"bg-destructive\": data?.requestStatus === \"error\",\n                  \"bg-warning\": data?.requestStatus === \"degraded\",\n                  \"bg-success\": data?.requestStatus === \"success\",\n                })}\n              />\n              <div className=\"capitalize\">\n                {data?.requestStatus ?? \"unknown\"}\n              </div>\n            </div>\n          </TableCell>\n        </TableRow>\n        {data.id ? (\n          <TableRow className=\"[&>:not(:last-child)]:border-r\">\n            <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n              ID\n            </TableHead>\n            <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n              {data.id}\n            </TableCell>\n          </TableRow>\n        ) : null}\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Timestamp\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <TableCellDate\n              value={new Date(data.cronTimestamp)}\n              className=\"text-foreground\"\n            />\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            URI\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            {data.uri}\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Latency\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <TableCellNumber value={data?.latency} unit=\"ms\" />\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Region\n          </TableHead>\n          <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n            {regionConfig?.flag} {regionConfig?.code}{\" \"}\n            <span className=\"text-muted-foreground\">\n              {regionConfig?.location}\n            </span>\n          </TableCell>\n        </TableRow>\n        <TableRow className=\"[&>:not(:last-child)]:border-r\">\n          <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n            Cloud Provider\n          </TableHead>\n          <TableCell className=\"inline-flex max-w-full overflow-x-auto whitespace-normal font-mono\">\n            <IconCloudProvider\n              provider={regionConfig?.provider}\n              className=\"mt-0.5\"\n            />\n            <span className=\"ml-1 text-muted-foreground\">\n              {regionConfig?.provider}\n            </span>\n          </TableCell>\n        </TableRow>\n        {data.trigger ? (\n          <TableRow className=\"[&>:not(:last-child)]:border-r\">\n            <TableHead className=\"bg-muted/50 font-normal text-muted-foreground\">\n              Trigger\n            </TableHead>\n            <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n              {data?.trigger}\n            </TableCell>\n          </TableRow>\n        ) : null}\n        {data?.records ? (\n          <>\n            <TableRow>\n              <TableHead colSpan={2}>Records</TableHead>\n            </TableRow>\n            <TableRow className=\"hover:bg-transparent\">\n              <TableCell colSpan={2} className=\"p-0\">\n                <Tabs defaultValue=\"table\" className=\"w-full gap-0\">\n                  <TabsList className=\"w-full justify-start rounded-none border-b px-2\">\n                    <TabsTrigger value=\"table\">\n                      <TableProperties className=\"size-3 rotate-180\" />\n                    </TabsTrigger>\n                    <TabsTrigger value=\"raw\">\n                      <Braces className=\"size-3\" />\n                    </TabsTrigger>\n                  </TabsList>\n                  <TabsContent value=\"table\">\n                    <Table className=\"table-fixed\">\n                      <colgroup>\n                        <col className=\"w-1/3\" />\n                        <col className=\"w-2/3\" />\n                      </colgroup>\n                      <TableBody>\n                        {Object.entries(data?.records ?? {}).map(\n                          ([key, value]) => (\n                            <TableRow\n                              key={key}\n                              className=\"[&>:not(:last-child)]:border-r\"\n                            >\n                              <TableHead className=\"overflow-x-auto bg-muted/50 font-normal text-muted-foreground\">\n                                {key.toUpperCase()}\n                              </TableHead>\n                              <TableCell className=\"max-w-full overflow-x-auto whitespace-normal font-mono\">\n                                {Array.isArray(value)\n                                  ? value.join(\", \")\n                                  : value}\n                              </TableCell>\n                            </TableRow>\n                          ),\n                        )}\n                      </TableBody>\n                    </Table>\n                  </TabsContent>\n                  <TabsContent value=\"raw\">\n                    <pre className=\"max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-4 font-mono text-sm\">\n                      {JSON.stringify(data?.records, null, 2)}\n                    </pre>\n                  </TabsContent>\n                </Tabs>\n              </TableCell>\n            </TableRow>\n          </>\n        ) : null}\n        {data?.errorMessage ? (\n          <>\n            <TableRow>\n              <TableHead colSpan={2}>Error Message</TableHead>\n            </TableRow>\n            <TableRow>\n              <TableCell colSpan={2} className=\"p-0\">\n                <pre className=\"max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm\">\n                  {data.errorMessage}\n                </pre>\n              </TableCell>\n            </TableRow>\n          </>\n        ) : null}\n        {data.assertions ? (\n          <>\n            <TableRow>\n              <TableHead colSpan={2}>Assertions</TableHead>\n            </TableRow>\n            <TableRow>\n              <TableCell colSpan={2} className=\"p-0\">\n                {!data.assertions || data.assertions === \"[]\" ? (\n                  <div className=\"p-2 font-mono text-muted-foreground text-sm\">\n                    No assertions\n                  </div>\n                ) : (\n                  <pre className=\"max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm\">\n                    {JSON.stringify(data.assertions, null, 2)}\n                  </pre>\n                )}\n              </TableCell>\n            </TableRow>\n          </>\n        ) : null}\n      </TableBody>\n    </Table>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/response-logs/data-table-sheet-test.tsx",
    "content": "\"use client\";\n\nimport {\n  DataTableSheet,\n  DataTableSheetContent,\n  DataTableSheetHeader,\n  DataTableSheetTitle,\n} from \"@/components/data-table/data-table-sheet\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { DataTableBasics } from \"./data-table-basics\";\n\ntype TestTCP = RouterOutputs[\"checker\"][\"testTcp\"];\ntype TestHTTP = RouterOutputs[\"checker\"][\"testHttp\"];\ntype TestDNS = RouterOutputs[\"checker\"][\"testDns\"];\ntype Monitor = NonNullable<RouterOutputs[\"monitor\"][\"get\"]>;\n\nexport function DataTableSheetTest({\n  data,\n  monitor,\n  onClose,\n}: {\n  data: TestTCP | TestHTTP | TestDNS | null;\n  monitor: Monitor;\n  onClose: () => void;\n}) {\n  if (!data) return null;\n\n  const _data = mapping(data, monitor);\n\n  if (!_data) return null;\n\n  return (\n    <DataTableSheet defaultOpen>\n      {/* NOTE: we are using onCloseAutoFocus to reset with a delay to avoid abrupt closing of the sheet */}\n      <DataTableSheetContent className=\"sm:max-w-lg\" onCloseAutoFocus={onClose}>\n        <DataTableSheetHeader className=\"px-2\">\n          <DataTableSheetTitle>Test Result</DataTableSheetTitle>\n        </DataTableSheetHeader>\n        <DataTableBasics data={_data} />\n      </DataTableSheetContent>\n    </DataTableSheet>\n  );\n}\n\nfunction mapping(data: TestTCP | TestHTTP | TestDNS, monitor: Monitor) {\n  switch (data.type) {\n    case \"http\":\n      return {\n        id: null,\n        trigger: null,\n        timestamp: data.timestamp,\n        cronTimestamp: data.timestamp,\n        type: data.type,\n        requestStatus: \"success\",\n        statusCode: data.status,\n        headers: data.headers,\n        region: data.region,\n        latency: data.latency,\n        timing: {\n          dns: data.timing.dnsDone - data.timing.dnsStart,\n          connect: data.timing.connectDone - data.timing.connectStart,\n          tls: data.timing.tlsHandshakeDone - data.timing.tlsHandshakeStart,\n          ttfb: data.timing.firstByteDone - data.timing.firstByteStart,\n          transfer: data.timing.transferDone - data.timing.transferStart,\n        },\n        url: monitor.url,\n        workspaceId: String(monitor.workspaceId),\n        error: false,\n        monitorId: String(monitor.id),\n        assertions: monitor.assertions ?? null,\n        message: null,\n        body: data.body ?? null,\n      } as const;\n    case \"tcp\":\n      return {\n        id: null,\n        trigger: null,\n        timestamp: data.timestamp,\n        cronTimestamp: data.timestamp,\n        region: data.region,\n        type: data.type,\n        requestStatus: \"success\",\n        error: false,\n        latency: data.latency ?? 0,\n        uri: monitor.url,\n        monitorId: String(monitor.id),\n        errorMessage: null,\n        assertions: null,\n      } as const;\n    // FIXM: add DNS props\n    case \"dns\":\n      return {\n        id: null,\n        trigger: null,\n        timestamp: data.timestamp,\n        cronTimestamp: data.timestamp,\n        region: data.region,\n        type: data.type,\n        requestStatus: \"success\",\n        monitorId: String(monitor.id),\n        error: false,\n        uri: monitor.url,\n        latency: data.latency ?? 0,\n        records: data.records,\n        errorMessage: null,\n        assertions: null,\n      } as const;\n    default:\n      return null;\n  }\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/response-logs/data-table-sheet.tsx",
    "content": "\"use client\";\n\nimport {\n  DataTableSheet,\n  DataTableSheetContent,\n  DataTableSheetFooter,\n  DataTableSheetHeader,\n  DataTableSheetTitle,\n} from \"@/components/data-table/data-table-sheet\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport type { PrivateLocation } from \"@openstatus/db/src/schema\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { Check, Copy } from \"lucide-react\";\nimport { DataTableBasics } from \"./data-table-basics\";\n\ntype ResponseLog = RouterOutputs[\"tinybird\"][\"get\"][\"data\"][number];\n\nexport function Sheet({\n  data,\n  privateLocations,\n  onClose,\n}: {\n  data: ResponseLog | null;\n  privateLocations?: PrivateLocation[];\n  onClose: () => void;\n}) {\n  const { copy, isCopied } = useCopyToClipboard();\n  if (!data) return null;\n\n  return (\n    <DataTableSheet defaultOpen onOpenChange={(open) => !open && onClose()}>\n      <DataTableSheetContent className=\"sm:max-w-lg\">\n        <DataTableSheetHeader className=\"px-2\">\n          <DataTableSheetTitle>Response Logs</DataTableSheetTitle>\n        </DataTableSheetHeader>\n        <DataTableBasics data={data} privateLocations={privateLocations} />\n        <Separator />\n        <DataTableSheetFooter>\n          <Button\n            variant=\"outline\"\n            onClick={() => {\n              if (typeof window !== \"undefined\") {\n                copy(window.location.href, {\n                  withToast: false,\n                });\n              }\n            }}\n          >\n            Copy Request Log URL\n            {isCopied ? <Check /> : <Copy />}\n          </Button>\n        </DataTableSheetFooter>\n      </DataTableSheetContent>\n    </DataTableSheet>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/response-logs/data-table-toolbar.tsx",
    "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\n\nimport { DataTableFacetedFilter } from \"@/components/ui/data-table/data-table-faceted-filter\";\nimport { regions } from \"@/data/regions\";\nimport { statusCodes } from \"@/data/status-codes\";\nimport type { RouterOutputs } from \"@openstatus/api\";\n\ntype ResponseLog = RouterOutputs[\"tinybird\"][\"list\"][\"data\"][number];\n\nexport interface ResponseLogsDataTableToolbarProps {\n  table: Table<ResponseLog>;\n}\n\nexport function ResponseLogsDataTableToolbar({\n  table,\n}: ResponseLogsDataTableToolbarProps) {\n  const isFiltered = table.getState().columnFilters.length > 0;\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex flex-1 flex-warp flex-wrap items-center gap-2\">\n        {table.getColumn(\"status\") && (\n          <DataTableFacetedFilter\n            column={table.getColumn(\"status\")}\n            title=\"Status\"\n            options={statusCodes.map((code) => ({\n              label: code.code.toString(),\n              value: code.code.toString(),\n            }))}\n          />\n        )}\n        {table.getColumn(\"region\") && (\n          <DataTableFacetedFilter\n            column={table.getColumn(\"region\")}\n            title=\"Region\"\n            options={regions.map((region) => ({\n              label: region.location,\n              value: region.code,\n            }))}\n          />\n        )}\n        {table.getColumn(\"error\") && (\n          <DataTableFacetedFilter\n            column={table.getColumn(\"error\")}\n            title=\"Error\"\n            options={[\n              { label: \"Yes\", value: \"true\" },\n              { label: \"No\", value: \"false\" },\n            ]}\n          />\n        )}\n        {isFiltered && (\n          <Button\n            variant=\"ghost\"\n            onClick={() => table.resetColumnFilters()}\n            className=\"h-8 px-2 lg:px-3\"\n          >\n            Reset\n            <X />\n          </Button>\n        )}\n      </div>\n      {/* <DataTableViewOptions table={table} /> */}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/response-logs/regions/columns.tsx",
    "content": "\"use client\";\n\nimport { ChartLineRegion } from \"@/components/chart/chart-line-region\";\nimport { TableCellNumber } from \"@/components/data-table/table-cell-number\";\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { DataTableColumnHeader } from \"@/components/ui/data-table/data-table-column-header\";\nimport type { RegionMetric } from \"@/data/region-metrics\";\nimport { getActions } from \"@/data/region-metrics.client\";\nimport type { PrivateLocation } from \"@openstatus/db/src/schema\";\nimport { formatRegionCode, getRegionInfo } from \"@openstatus/regions\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\n// import { toast } from \"sonner\";\nimport { useRouter } from \"next/navigation\";\n\nfunction TrendCell({ trend }: { trend: RegionMetric[\"trend\"] }) {\n  return <ChartLineRegion className=\"h-[50px]\" data={trend} />;\n}\n\nexport function getColumns(\n  privateLocations: PrivateLocation[],\n): ColumnDef<RegionMetric>[] {\n  return [\n    {\n      accessorKey: \"region\",\n      header: \"Region\",\n      cell: ({ row }) => {\n        const value = row.getValue(\"region\");\n        if (typeof value === \"string\") {\n          const region = getRegionInfo(value, {\n            location: privateLocations.find(\n              (location) => String(location.id) === String(value),\n            )?.name,\n          });\n          return (\n            <TooltipProvider>\n              <Tooltip>\n                <TooltipTrigger className=\"flex h-[50px] items-center gap-1\">\n                  {region.flag}{\" \"}\n                  <span className=\"max-w-[90px] truncate\">\n                    {formatRegionCode(region.code)}\n                  </span>\n                </TooltipTrigger>\n                <TooltipContent side=\"left\">\n                  {region.location} ({region.provider})\n                </TooltipContent>\n              </Tooltip>\n            </TooltipProvider>\n          );\n        }\n        return null;\n      },\n      enableSorting: false,\n      enableHiding: false,\n      meta: {\n        cellClassName: \"w-24 font-mono\",\n      },\n    },\n    {\n      accessorKey: \"trend\",\n      header: \"Trend\",\n      cell: ({ row }) => {\n        return <TrendCell trend={row.original.trend} />;\n      },\n      enableSorting: false,\n      enableHiding: false,\n      meta: {\n        cellClassName: \"w-full min-w-[200px] max-w-full\",\n      },\n    },\n    {\n      accessorKey: \"p50\",\n      header: ({ column }) => (\n        <DataTableColumnHeader column={column} title=\"P50\" />\n      ),\n      cell: ({ row }) => {\n        return <TableCellNumber value={row.getValue(\"p50\")} unit=\"ms\" />;\n      },\n      enableHiding: false,\n      meta: {\n        cellClassName: \"w-12\",\n      },\n    },\n    {\n      accessorKey: \"p90\",\n      header: ({ column }) => (\n        <DataTableColumnHeader column={column} title=\"P90\" />\n      ),\n      cell: ({ row }) => {\n        return <TableCellNumber value={row.getValue(\"p90\")} unit=\"ms\" />;\n      },\n      enableHiding: false,\n      meta: {\n        cellClassName: \"w-12\",\n      },\n    },\n    {\n      accessorKey: \"p99\",\n      header: ({ column }) => (\n        <DataTableColumnHeader column={column} title=\"P99\" />\n      ),\n      cell: ({ row }) => {\n        return <TableCellNumber value={row.getValue(\"p99\")} unit=\"ms\" />;\n      },\n      enableHiding: false,\n      meta: {\n        cellClassName: \"w-12\",\n      },\n    },\n    {\n      id: \"actions\",\n      cell: ({ row }) => {\n        // NOTE: works, but is not very react-esque\n        // eslint-disable-next-line react-hooks/rules-of-hooks\n        const router = useRouter();\n        const actions = getActions({\n          filter: async () => {\n            router.push(`?regions=${row.original.region}`);\n          },\n          // TODO: add triggerById in TRPC client\n          // trigger: async () => {\n          //   console.log(row.original);\n          //   const promise = new Promise((resolve) => setTimeout(resolve, 1000));\n          //   toast.promise(promise, {\n          //     loading: \"Checking...\",\n          //     success: \"Success\",\n          //     error: \"Failed\",\n          //   });\n          //   await promise;\n          // },\n        });\n        return <QuickActions actions={actions} />;\n      },\n      meta: {\n        headerClassName: \"w-12\",\n        cellClassName: \"text-right\",\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/settings/api-key/data-table.tsx",
    "content": "import { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { formatDate } from \"@/lib/formatter\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\nimport { useMutation } from \"@tanstack/react-query\";\n\ntype ApiKey = RouterOutputs[\"apiKeyRouter\"][\"getAll\"][number];\n\nexport function DataTable({\n  apiKeys,\n  refetch,\n}: {\n  apiKeys: ApiKey[];\n  refetch: () => void;\n}) {\n  const trpc = useTRPC();\n  const revokeApiKeyMutation = useMutation(\n    trpc.apiKeyRouter.revoke.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  return (\n    <div className=\"overflow-x-auto\">\n      <Table>\n        <TableHeader>\n          <TableRow>\n            <TableHead>Name</TableHead>\n            <TableHead>Description</TableHead>\n            <TableHead>Prefix</TableHead>\n            <TableHead>Expires</TableHead>\n            <TableHead>\n              <span className=\"sr-only\">Actions</span>\n            </TableHead>\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          {apiKeys.map((apiKey) => (\n            <TableRow key={apiKey.id}>\n              <TableCell className=\"font-medium\">{apiKey.name}</TableCell>\n              <TableCell className=\"max-w-[200px] truncate text-muted-foreground\">\n                {apiKey.description ?? \"-\"}\n              </TableCell>\n              <TableCell>\n                <code className=\"text-xs\">{apiKey.prefix}...</code>\n              </TableCell>\n              <TableCell className=\"text-sm\">\n                {apiKey.expiresAt ? formatDate(apiKey.expiresAt) : \"-\"}\n              </TableCell>\n              <TableCell>\n                <div className=\"flex justify-end\">\n                  <QuickActions\n                    deleteAction={{\n                      confirmationValue: apiKey.name ?? \"api key\",\n                      submitAction: async () =>\n                        await revokeApiKeyMutation.mutateAsync({\n                          keyId: apiKey.id,\n                        }),\n                    }}\n                  />\n                </div>\n              </TableCell>\n            </TableRow>\n          ))}\n        </TableBody>\n      </Table>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/settings/invitations/data-table.tsx",
    "content": "import {\n  EmptyStateContainer,\n  EmptyStateDescription,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { formatDate } from \"@/lib/formatter\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\n\nexport function DataTable() {\n  const trpc = useTRPC();\n  const { data: invitations, refetch } = useQuery(\n    trpc.invitation.list.queryOptions(),\n  );\n  const deleteInvitationMutation = useMutation(\n    trpc.invitation.delete.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  if (!invitations) return null;\n\n  if (invitations.length === 0) {\n    return (\n      <EmptyStateContainer>\n        <EmptyStateTitle>No pending invitations</EmptyStateTitle>\n        <EmptyStateDescription>\n          Only active invitations are shown here.\n        </EmptyStateDescription>\n      </EmptyStateContainer>\n    );\n  }\n\n  return (\n    <Table>\n      <TableHeader>\n        <TableRow>\n          <TableHead>Email</TableHead>\n          <TableHead>Role</TableHead>\n          <TableHead>Created At</TableHead>\n          <TableHead>Expires At</TableHead>\n          <TableHead>Accepted At</TableHead>\n          <TableHead>\n            <span className=\"sr-only\">Actions</span>\n          </TableHead>\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        {invitations.map((item) => (\n          <TableRow key={item.id}>\n            <TableCell>{item.email}</TableCell>\n            <TableCell>{item.role}</TableCell>\n            <TableCell>\n              {item.createdAt ? formatDate(item.createdAt) : \"-\"}\n            </TableCell>\n            <TableCell>{formatDate(item.expiresAt)}</TableCell>\n            <TableCell>\n              {item.acceptedAt ? formatDate(item.acceptedAt) : \"-\"}\n            </TableCell>\n            <TableCell>\n              <div className=\"flex justify-end\">\n                <QuickActions\n                  deleteAction={{\n                    confirmationValue: item.email ?? \"invitation\",\n                    submitAction: async () =>\n                      deleteInvitationMutation.mutateAsync({ id: item.id }),\n                  }}\n                />\n              </div>\n            </TableCell>\n          </TableRow>\n        ))}\n      </TableBody>\n    </Table>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/settings/members/data-table.tsx",
    "content": "import { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { formatDate } from \"@/lib/formatter\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\n\nexport function DataTable() {\n  const trpc = useTRPC();\n  const { data: members, refetch } = useQuery(trpc.member.list.queryOptions());\n  const deleteMemberMutation = useMutation(\n    trpc.member.delete.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  if (!members) return null;\n\n  return (\n    <Table>\n      <TableHeader>\n        <TableRow>\n          <TableHead>Name</TableHead>\n          <TableHead>Email</TableHead>\n          <TableHead>Role</TableHead>\n          <TableHead>Created</TableHead>\n          <TableHead>\n            <span className=\"sr-only\">Actions</span>\n          </TableHead>\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        {members.map((item) => (\n          <TableRow key={item.user.id}>\n            <TableCell>\n              {item.user.name ?? (\n                <span className=\"text-muted-foreground\">-</span>\n              )}\n            </TableCell>\n            <TableCell>{item.user.email}</TableCell>\n            <TableCell>{item.role}</TableCell>\n            <TableCell>\n              {formatDate(item.user.createdAt ?? item.createdAt)}\n            </TableCell>\n            <TableCell>\n              <div className=\"flex justify-end\">\n                <QuickActions\n                  deleteAction={{\n                    confirmationValue: item.user.email ?? \"user\",\n                    // FIXME: when deleting myself, throws an error, should have been caught by the toast.error\n                    submitAction: async () =>\n                      await deleteMemberMutation.mutateAsync({\n                        id: item.user.id,\n                      }),\n                  }}\n                />\n              </div>\n            </TableCell>\n          </TableRow>\n        ))}\n      </TableBody>\n    </Table>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/status-pages/columns.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport { TableCellLink } from \"@/components/data-table/table-cell-link\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\ntype StatusPage = RouterOutputs[\"page\"][\"list\"][number];\n\nexport const columns: ColumnDef<StatusPage>[] = [\n  {\n    accessorKey: \"title\",\n    header: \"Title\",\n    cell: ({ row }) => {\n      return (\n        <TableCellLink\n          href={`/status-pages/${row.original.id}/status-reports`}\n          value={row.getValue(\"title\")}\n        />\n      );\n    },\n    enableSorting: false,\n    enableHiding: false,\n    meta: {\n      cellClassName: \"max-w-[150px] min-w-max\",\n    },\n  },\n  {\n    accessorKey: \"icon\",\n    header: \"Favicon\",\n    cell: ({ row }) => {\n      const value = row.getValue(\"icon\");\n      if (!value || typeof value !== \"string\")\n        return <span className=\"text-muted-foreground\">-</span>;\n      return (\n        <img\n          src={`${value}`}\n          alt={`Favicon for ${row.getValue(\"title\")}`}\n          className=\"h-4 w-4 rounded border bg-muted\"\n        />\n      );\n    },\n    enableSorting: false,\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"slug\",\n    header: \"Slug\",\n    cell: ({ row }) => {\n      const domain = row.getValue(\"domain\");\n      const slug = row.getValue(\"slug\");\n      return (\n        <TableCellLink\n          href={domain ? `https://${domain}` : `https://${slug}.openstatus.dev`}\n          value={slug}\n        />\n      );\n    },\n    enableSorting: false,\n    enableHiding: false,\n    meta: {\n      cellClassName: \"font-mono\",\n    },\n  },\n  {\n    accessorKey: \"domain\",\n    accessorFn: (row) => row.customDomain,\n    header: \"Domain\",\n    cell: ({ row }) => {\n      const value = row.getValue(\"domain\");\n      if (typeof value !== \"string\")\n        return <span className=\"text-muted-foreground\">-</span>;\n      return (\n        <Link href={\"#\"} className=\"font-mono\">\n          {value}\n        </Link>\n      );\n    },\n    enableSorting: false,\n    enableHiding: false,\n  },\n  {\n    id: \"actions\",\n    cell: ({ row }) => <DataTableRowActions row={row} />,\n    meta: {\n      cellClassName: \"w-8\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/status-pages/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { getActions } from \"@/data/status-pages.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport type { Row } from \"@tanstack/react-table\";\nimport { useRouter } from \"next/navigation\";\nimport { toast } from \"sonner\";\n\ntype StatusPage = RouterOutputs[\"page\"][\"list\"][number];\n\ninterface DataTableRowActionsProps {\n  row: Row<StatusPage>;\n}\n\nexport function DataTableRowActions({ row }: DataTableRowActionsProps) {\n  const router = useRouter();\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const deleteStatusPageMutation = useMutation(\n    trpc.page.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n      },\n    }),\n  );\n  const actions = getActions({\n    edit: () => router.push(`/status-pages/${row.original.id}/edit`),\n    \"copy-id\": () => {\n      navigator.clipboard.writeText(row.original.id.toString());\n      toast.success(\"Monitor ID copied to clipboard\");\n    },\n  });\n\n  return (\n    <QuickActions\n      actions={actions}\n      deleteAction={{\n        confirmationValue: row.original.title ?? \"status page\",\n        submitAction: async () => {\n          await deleteStatusPageMutation.mutateAsync({\n            id: row.original.id,\n          });\n        },\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/status-report-updates/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { FormSheetStatusReportUpdate } from \"@/components/forms/status-report-update/sheet\";\nimport { getActions } from \"@/data/status-report-updates.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\nimport { useRef } from \"react\";\n\ntype StatusReportUpdate =\n  RouterOutputs[\"statusReport\"][\"list\"][number][\"updates\"][number];\n\ninterface DataTableRowActionsProps {\n  row: StatusReportUpdate;\n}\n\nexport function DataTableRowActions({ row }: DataTableRowActionsProps) {\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const updateStatusReportUpdateMutation = useMutation(\n    trpc.statusReport.updateStatusReportUpdate.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            pageId: Number.parseInt(id),\n          }),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            period: \"7d\",\n          }),\n        });\n      },\n    }),\n  );\n  const deleteStatusReportUpdateMutation = useMutation(\n    trpc.statusReport.deleteUpdate.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            pageId: Number.parseInt(id),\n          }),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            period: \"7d\",\n          }),\n        });\n      },\n    }),\n  );\n  const actions = getActions({\n    edit: () => buttonRef.current?.click(),\n  });\n\n  return (\n    <>\n      <QuickActions\n        actions={actions}\n        deleteAction={{\n          confirmationValue: row.status ?? \"status report update\",\n          submitAction: async () => {\n            await deleteStatusReportUpdateMutation.mutateAsync({\n              id: row.id,\n            });\n          },\n        }}\n      />\n      <FormSheetStatusReportUpdate\n        defaultValues={{\n          message: row.message,\n          date: row.date,\n          status: row.status,\n        }}\n        onSubmit={async (values) => {\n          await updateStatusReportUpdateMutation.mutateAsync({\n            id: row.id,\n            statusReportId: row.statusReportId,\n            message: values.message,\n            status: values.status,\n            date: values.date,\n          });\n        }}\n      >\n        <button ref={buttonRef} type=\"button\" className=\"sr-only\">\n          Open sheet\n        </button>\n      </FormSheetStatusReportUpdate>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/status-report-updates/data-table.tsx",
    "content": "\"use client\";\n\nimport { ProcessMessage } from \"@/components/content/process-message\";\nimport { TableCellDate } from \"@/components/data-table/table-cell-date\";\nimport { FormSheetStatusReportUpdate } from \"@/components/forms/status-report-update/sheet\";\nimport { icons } from \"@/data/icons\";\nimport { colors, getNextStatus } from \"@/data/status-report-updates.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { cn } from \"@/lib/utils\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { Plus } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\ntype StatusReportUpdates =\n  RouterOutputs[\"statusReport\"][\"list\"][number][\"updates\"];\n\nexport function DataTable({\n  updates,\n  reportId,\n}: {\n  updates: StatusReportUpdates;\n  reportId: number;\n}) {\n  const trpc = useTRPC();\n  const { id } = useParams<{ id: string }>();\n  const queryClient = useQueryClient();\n  const sendStatusReportUpdateMutation = useMutation(\n    trpc.emailRouter.sendStatusReport.mutationOptions(),\n  );\n  const createStatusReportUpdateMutation = useMutation(\n    trpc.statusReport.createStatusReportUpdate.mutationOptions({\n      onSuccess: (update) => {\n        // TODO: move to server\n        if (update?.notifySubscribers) {\n          sendStatusReportUpdateMutation.mutateAsync({ id: update.id });\n        }\n        //\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            pageId: Number.parseInt(id),\n          }),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            period: \"7d\",\n          }),\n        });\n      },\n    }),\n  );\n\n  return (\n    <Table className=\"w-full\">\n      <TableHeader>\n        <TableRow>\n          <TableHead className=\"w-7\">\n            <span className=\"sr-only\">Status</span>\n          </TableHead>\n          <TableHead>Message</TableHead>\n          <TableHead>Date</TableHead>\n          <TableHead className=\"w-[px]\">\n            <TooltipProvider>\n              <Tooltip>\n                <FormSheetStatusReportUpdate\n                  defaultValues={{\n                    status: getNextStatus(updates[updates.length - 1].status),\n                  }}\n                  onSubmit={async (values) => {\n                    await createStatusReportUpdateMutation.mutateAsync({\n                      statusReportId: reportId,\n                      message: values.message,\n                      status: values.status,\n                      date: values.date,\n                      notifySubscribers: values.notifySubscribers,\n                    });\n                  }}\n                >\n                  <TooltipTrigger asChild>\n                    <Button size=\"icon\" className=\"ml-auto flex h-7 w-7 p-0\">\n                      <Plus />\n                      <span className=\"sr-only\">Create Report Update</span>\n                    </Button>\n                  </TooltipTrigger>\n                </FormSheetStatusReportUpdate>\n                <TooltipContent side=\"left\" align=\"center\">\n                  Create Report Update\n                </TooltipContent>\n              </Tooltip>\n            </TooltipProvider>\n          </TableHead>\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        {updates.map((update) => {\n          const Icon = icons.status[update.status];\n          return (\n            <TableRow key={update.id}>\n              <TableCell>\n                <div className=\"p-1\">\n                  <Icon className={cn(colors[update.status])} size={20} />\n                </div>\n              </TableCell>\n              <TableCell>\n                <div className=\"prose dark:prose-invert prose-sm line-clamp-3 text-wrap text-muted-foreground\">\n                  <ProcessMessage value={update.message} />\n                </div>\n              </TableCell>\n              <TableCell className=\"w-[170px] text-muted-foreground\">\n                <TableCellDate value={update.date} />\n              </TableCell>\n              <TableCell className=\"w-8\">\n                <DataTableRowActions row={update} />\n              </TableCell>\n            </TableRow>\n          );\n        })}\n      </TableBody>\n    </Table>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/status-reports/columns.tsx",
    "content": "\"use client\";\n\nimport { TableCellBadge } from \"@/components/data-table/table-cell-badge\";\nimport { TableCellDate } from \"@/components/data-table/table-cell-date\";\nimport { TableCellLink } from \"@/components/data-table/table-cell-link\";\nimport { TableCellNumber } from \"@/components/data-table/table-cell-number\";\nimport { DataTableColumnHeader } from \"@/components/ui/data-table/data-table-column-header\";\nimport { colors } from \"@/data/status-report-updates.client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\ntype StatusReport = RouterOutputs[\"statusReport\"][\"list\"][number];\n\nexport const columns: ColumnDef<StatusReport>[] = [\n  {\n    id: \"expander\",\n    header: () => null,\n    cell: ({ row }) => {\n      return row.getCanExpand() ? (\n        <Button\n          {...{\n            className: \"size-7 shadow-none text-muted-foreground\",\n            onClick: (e) => {\n              e.stopPropagation();\n              row.toggleExpanded();\n            },\n            \"aria-expanded\": row.getIsExpanded(),\n            \"aria-label\": row.getIsExpanded()\n              ? `Collapse details for ${row.original.title}`\n              : `Expand details for ${row.original.title}`,\n            size: \"icon\",\n            variant: \"ghost\",\n          }}\n        >\n          {row.getIsExpanded() ? (\n            <ChevronUp className=\"opacity-60\" size={16} aria-hidden=\"true\" />\n          ) : (\n            <ChevronDown className=\"opacity-60\" size={16} aria-hidden=\"true\" />\n          )}\n        </Button>\n      ) : undefined;\n    },\n    meta: {\n      headerClassName: \"w-7\",\n    },\n  },\n  {\n    accessorKey: \"title\",\n    header: \"Title\",\n    cell: ({ row }) => {\n      const { id, pageId } = row.original;\n\n      return (\n        <TableCellLink\n          href={`/status-pages/${pageId}/status-reports/${id}`}\n          onClick={(e) => {\n            // avoid expanding the row\n            e.stopPropagation();\n          }}\n          value={row.getValue(\"title\")}\n        />\n      );\n    },\n    enableSorting: false,\n    enableHiding: false,\n    meta: {\n      cellClassName: \"max-w-[200px] truncate\",\n    },\n  },\n  {\n    accessorKey: \"status\",\n    header: \"Current Status\",\n    cell: ({ row }) => {\n      const value = String(row.getValue(\"status\"));\n      return (\n        <div\n          className={cn(\n            \"font-mono capitalize\",\n            colors[value as keyof typeof colors],\n          )}\n        >\n          {value}\n        </div>\n      );\n    },\n    enableSorting: false,\n    enableHiding: false,\n  },\n  {\n    id: \"updates\",\n    accessorFn: (row) => row.updates.length,\n    header: \"Total Updates\",\n    cell: ({ row }) => {\n      const value = row.getValue(\"updates\");\n      return <TableCellNumber value={value} />;\n    },\n  },\n  {\n    id: \"pageComponents\",\n    accessorFn: (row) => row?.pageComponents,\n    header: \"Affected\",\n    cell: ({ row }) => {\n      const value = row.getValue(\"pageComponents\");\n      if (Array.isArray(value) && value.length > 0 && \"name\" in value[0]) {\n        return (\n          <div className=\"flex flex-wrap gap-1\">\n            {value.map((m) =>\n              m.monitorId ? (\n                <Link href={`/monitors/${m.monitorId}/overview`} key={m.id}>\n                  <TableCellBadge value={m.name} />\n                </Link>\n              ) : (\n                <TableCellBadge value={m.name} key={m.id} />\n              ),\n            )}\n          </div>\n        );\n      }\n      return <div className=\"text-muted-foreground\">-</div>;\n    },\n  },\n  {\n    id: \"startedAt\",\n    accessorFn: (row) =>\n      row.updates.sort((a, b) => a.date.getTime() - b.date.getTime())[0]?.date,\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Started At\" />\n    ),\n    cell: ({ row }) => <TableCellDate value={row.getValue(\"startedAt\")} />,\n    enableHiding: false,\n    meta: {\n      cellClassName: \"w-[170px]\",\n    },\n  },\n  {\n    id: \"actions\",\n    cell: ({ row }) => <DataTableRowActions row={row} />,\n    meta: {\n      cellClassName: \"w-8\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/status-reports/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { FormSheetStatusReportUpdate } from \"@/components/forms/status-report-update/sheet\";\nimport { FormSheetStatusReport } from \"@/components/forms/status-report/sheet\";\nimport { getNextStatus } from \"@/data/status-report-updates.client\";\nimport { getActions } from \"@/data/status-reports.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport type { Row } from \"@tanstack/react-table\";\nimport { useRef } from \"react\";\n\ntype StatusReport = RouterOutputs[\"statusReport\"][\"list\"][number];\n\ninterface DataTableRowActionsProps {\n  row: Row<StatusReport>;\n}\n\n// NOTE: avoid using useParams to get status page :id\n// because we are using the table in the /overview page\n\nexport function DataTableRowActions({ row }: DataTableRowActionsProps) {\n  if (!row.original.pageId) return null;\n  const buttonCreateRef = useRef<HTMLButtonElement>(null);\n  const buttonUpdateRef = useRef<HTMLButtonElement>(null);\n  const actions = getActions({\n    \"create-update\": () => buttonCreateRef.current?.click(),\n    edit: () => buttonUpdateRef.current?.click(),\n    \"view-report\": () => {\n      if (typeof window !== \"undefined\") {\n        window.open(\n          `https://${\n            row.original.page.customDomain ||\n            `${row.original.page.slug}.openstatus.dev`\n          }/events/report/${row.original.id}`,\n          \"_blank\",\n        );\n      }\n    },\n  });\n  const trpc = useTRPC();\n  const queryClient = useQueryClient();\n  const { data: page } = useQuery(\n    trpc.page.get.queryOptions({ id: row.original.pageId }),\n  );\n  const sendStatusReportUpdateMutation = useMutation(\n    trpc.emailRouter.sendStatusReport.mutationOptions(),\n  );\n  const updateStatusReportMutation = useMutation(\n    trpc.statusReport.updateStatus.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            pageId: row.original.pageId ?? undefined,\n          }),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            period: \"7d\",\n          }),\n        });\n      },\n    }),\n  );\n  const createStatusReportUpdateMutation = useMutation(\n    trpc.statusReport.createStatusReportUpdate.mutationOptions({\n      onSuccess: (update) => {\n        // TODO: move to server\n        if (update) {\n          sendStatusReportUpdateMutation.mutateAsync({ id: update.id });\n        }\n        //\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            pageId: row.original.pageId ?? undefined,\n          }),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            period: \"7d\",\n          }),\n        });\n      },\n    }),\n  );\n  const deleteStatusReportMutation = useMutation(\n    trpc.statusReport.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            pageId: row.original.pageId ?? undefined,\n          }),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n        queryClient.invalidateQueries({\n          queryKey: trpc.statusReport.list.queryKey({\n            period: \"7d\",\n          }),\n        });\n      },\n    }),\n  );\n\n  return (\n    <>\n      <QuickActions\n        actions={actions}\n        deleteAction={{\n          confirmationValue: row.original.title ?? \"status report\",\n          submitAction: async () => {\n            await deleteStatusReportMutation.mutateAsync({\n              id: row.original.id,\n            });\n          },\n        }}\n      />\n      <FormSheetStatusReport\n        pageComponents={page?.pageComponents ?? []}\n        defaultValues={{\n          title: row.original.title,\n          status: row.original.status,\n          pageComponents: row.original.pageComponents?.map((c) => c.id) ?? [],\n        }}\n        onSubmit={async (values) => {\n          await updateStatusReportMutation.mutateAsync({\n            id: row.original.id,\n            pageComponents: values.pageComponents,\n            title: values.title,\n            status: values.status,\n          });\n        }}\n      >\n        <button ref={buttonUpdateRef} type=\"button\" className=\"sr-only\">\n          Open sheet\n        </button>\n      </FormSheetStatusReport>\n      <FormSheetStatusReportUpdate\n        defaultValues={{\n          status: getNextStatus(row.original.status),\n        }}\n        onSubmit={async (values) => {\n          await createStatusReportUpdateMutation.mutateAsync({\n            statusReportId: row.original.id,\n            message: values.message,\n            status: values.status,\n            date: values.date,\n          });\n        }}\n      >\n        <button ref={buttonCreateRef} type=\"button\" className=\"sr-only\">\n          Open sheet\n        </button>\n      </FormSheetStatusReportUpdate>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/subscribers/columns.tsx",
    "content": "\"use client\";\n\nimport { formatDate } from \"@/lib/formatter\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport { DataTableRowActions } from \"./data-table-row-actions\";\n\ntype Subscriber = RouterOutputs[\"pageSubscriber\"][\"list\"][number];\n\nexport const columns: ColumnDef<Subscriber>[] = [\n  {\n    accessorKey: \"email\",\n    header: \"Email\",\n    enableSorting: false,\n    enableHiding: false,\n  },\n  {\n    id: \"status\",\n    header: \"Status\",\n    enableSorting: false,\n    enableHiding: false,\n    cell: ({ row }) => {\n      const unsubscribedAt = row.original.unsubscribedAt;\n      const acceptedAt = row.original.acceptedAt;\n\n      if (unsubscribedAt) {\n        return <Badge variant=\"destructive\">Unsubscribed</Badge>;\n      }\n\n      if (!acceptedAt) {\n        return <Badge variant=\"outline\">Pending</Badge>;\n      }\n\n      return <Badge variant=\"secondary\">Active</Badge>;\n    },\n  },\n  {\n    accessorKey: \"createdAt\",\n    header: \"Created At\",\n    enableSorting: false,\n    enableHiding: false,\n    cell: ({ row }) => {\n      const value = row.getValue(\"createdAt\");\n      if (value instanceof Date) return formatDate(value);\n      if (!value) return \"-\";\n      return value;\n    },\n    meta: {\n      cellClassName: \"font-mono\",\n    },\n  },\n  {\n    accessorKey: \"acceptedAt\",\n    header: \"Accepted At\",\n    enableSorting: false,\n    enableHiding: false,\n    cell: ({ row }) => {\n      const value = row.getValue(\"acceptedAt\");\n      if (value instanceof Date) return formatDate(value);\n      if (!value) return \"-\";\n      return value;\n    },\n    meta: {\n      cellClassName: \"font-mono\",\n    },\n  },\n  {\n    accessorKey: \"unsubscribedAt\",\n    header: \"Unsubscribed At\",\n    enableSorting: false,\n    enableHiding: false,\n    cell: ({ row }) => {\n      const value = row.getValue(\"unsubscribedAt\");\n      if (value instanceof Date) return formatDate(value);\n      if (!value) return \"-\";\n      return value;\n    },\n    meta: {\n      cellClassName: \"font-mono\",\n    },\n  },\n  {\n    id: \"actions\",\n    cell: ({ row }) => <DataTableRowActions row={row} />,\n    meta: {\n      cellClassName: \"w-8\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/subscribers/data-table-row-actions.tsx",
    "content": "\"use client\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport type { Row } from \"@tanstack/react-table\";\n\ntype Subscriber = RouterOutputs[\"pageSubscriber\"][\"list\"][number];\n\ninterface DataTableRowActionsProps {\n  row: Row<Subscriber>;\n}\n\nexport function DataTableRowActions({ row }: DataTableRowActionsProps) {\n  const trpc = useTRPC();\n  const { refetch } = useQuery(\n    trpc.pageSubscriber.list.queryOptions({\n      pageId: row.original.pageId,\n    }),\n  );\n\n  const deleteAction = useMutation(\n    trpc.pageSubscriber.delete.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  return (\n    <QuickActions\n      actions={[]}\n      deleteAction={{\n        confirmationValue: row.original.email ?? \"subscriber\",\n        submitAction: async () => {\n          await deleteAction.mutateAsync({\n            id: row.original.id,\n            pageId: row.original.pageId,\n          });\n        },\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/table-cell-badge.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport function TableCellBadge({\n  value,\n  className,\n  ...props\n}: React.ComponentProps<typeof Badge> & { value: unknown }) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const [isTruncated, setIsTruncated] = useState(false);\n  const [open, setOpen] = useState(false);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  useEffect(() => {\n    if (ref.current) {\n      setIsTruncated(ref.current.scrollWidth > ref.current.clientWidth);\n    }\n  }, [ref]);\n\n  return (\n    <Badge\n      variant=\"outline\"\n      className={cn(\n        \"max-w-16 truncate font-mono\",\n        value ? \"text-foreground\" : \"text-foreground/70\",\n        className,\n      )}\n      {...props}\n    >\n      <TooltipProvider>\n        {isTruncated ? (\n          <Tooltip open={open} onOpenChange={setOpen}>\n            <TooltipTrigger\n              onPointerDown={(event) => event.preventDefault()}\n              asChild\n            >\n              <span ref={ref} className=\"block truncate\">\n                {String(value)}\n              </span>\n            </TooltipTrigger>\n            <TooltipContent>{String(value)}</TooltipContent>\n          </Tooltip>\n        ) : (\n          <span ref={ref} className=\"truncate\">\n            {String(value)}\n          </span>\n        )}\n      </TooltipProvider>\n    </Badge>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/table-cell-boolean.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport function TableCellBoolean({\n  value,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & { value: unknown }) {\n  const _value = Boolean(value);\n\n  return (\n    <div\n      className={cn(\n        \"font-mono\",\n        _value ? \"text-foreground\" : \"text-foreground/70\",\n        className,\n      )}\n      {...props}\n    >\n      {String(_value)}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/table-cell-date.tsx",
    "content": "import { HoverCardTimestamp } from \"@/components/common/hover-card-timestamp\";\nimport { cn } from \"@/lib/utils\";\nimport { format } from \"date-fns\";\n\nexport function TableCellDate({\n  value,\n  className,\n  formatStr = \"LLL dd, y HH:mm:ss\",\n  ...props\n}: React.ComponentProps<\"div\"> & { value: unknown; formatStr?: string }) {\n  if (value instanceof Date) {\n    return (\n      <HoverCardTimestamp date={value}>\n        <div className={cn(\"text-muted-foreground\", className)} {...props}>\n          {format(value, formatStr)}\n        </div>\n      </HoverCardTimestamp>\n    );\n  }\n  if (typeof value === \"string\") {\n    return (\n      <div className={cn(\"text-muted-foreground\", className)} {...props}>\n        {value}\n      </div>\n    );\n  }\n  return (\n    <div className={cn(\"text-muted-foreground\", className)} {...props}>\n      -\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/table-cell-link.tsx",
    "content": "import { Link } from \"@/components/common/link\";\nimport { cn } from \"@/lib/utils\";\nimport { ArrowUpRight, ChevronRight } from \"lucide-react\";\n\nexport function TableCellLink({\n  value,\n  className,\n  ...props\n}: React.ComponentProps<typeof Link> & {\n  value: unknown;\n}) {\n  if (typeof value === \"string\") {\n    const isExternal = props.href?.toString().startsWith(\"http\");\n    const externalProps = isExternal\n      ? { target: \"_blank\", rel: \"noopener noreferrer\" }\n      : {};\n    const Icon = isExternal ? ArrowUpRight : ChevronRight;\n    return (\n      <Link\n        className={cn(\n          \"group/link flex w-full items-center justify-between gap-2 hover:underline\",\n          className,\n        )}\n        {...externalProps}\n        {...props}\n      >\n        <span className=\"truncate\">{value}</span>\n        <Icon className=\"size-4 flex-shrink-0 text-muted-foreground group-hover/link:text-foreground\" />\n      </Link>\n    );\n  }\n  return <div className=\"text-muted-foreground\">-</div>;\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/table-cell-number.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport function TableCellNumber({\n  value,\n  className,\n  unit,\n  ...props\n}: React.ComponentProps<\"div\"> & { value: unknown; unit?: string }) {\n  const _value = Number(value);\n  if (Number.isNaN(_value)) {\n    return <div className=\"font-mono text-muted-foreground\">N/A</div>;\n  }\n\n  return (\n    <div className={cn(\"font-mono text-foreground\", className)} {...props}>\n      {_value}\n      {unit && <span className=\"p-0.5 text-muted-foreground\">{unit}</span>}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/data-table/table-cell-unavailable.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport function TableCellUnavailable({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"text-muted-foreground\", className)} {...props}>\n      N/A\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/date-picker.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport type { DateRange } from \"react-day-picker\";\n\nimport { Kbd } from \"@/components/common/kbd\";\nimport { formatDateForInput } from \"@/lib/formatter\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Calendar } from \"@openstatus/ui/components/ui/calendar\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { endOfDay } from \"date-fns\";\n\ntype DatePickerProps = {\n  range: DateRange;\n  onSelect: (range: DateRange) => void;\n  presets: { id: string; label: string; values: DateRange; shortcut: string }[];\n};\n\nexport function DatePicker({ range, onSelect, presets }: DatePickerProps) {\n  const [today] = useState(new Date());\n  const disableBefore = presets[presets.length - 1]?.values?.from;\n\n  return (\n    <div>\n      <div className=\"flex flex-row\">\n        <div className=\"relative py-4\">\n          <div className=\"h-full\">\n            <div className=\"flex flex-col px-1\">\n              <div className=\"px-3 py-1 font-medium text-muted-foreground text-xs\">\n                Presets\n              </div>\n              {presets.map((preset) => {\n                const isSelected =\n                  range.from?.getTime() === preset.values.from?.getTime() &&\n                  range.to?.getTime() === preset.values.to?.getTime();\n\n                return (\n                  <Button\n                    key={preset.id}\n                    variant={isSelected ? \"outline\" : \"ghost\"}\n                    size=\"sm\"\n                    className=\"w-full justify-between border border-transparent\"\n                    onClick={() => {\n                      onSelect(preset.values);\n                    }}\n                  >\n                    <span>{preset.label}</span>\n                    <Kbd className=\"font-mono uppercase\">{preset.shortcut}</Kbd>\n                  </Button>\n                );\n              })}\n            </div>\n          </div>\n        </div>\n        <Separator orientation=\"vertical\" className=\"h-auto! w-px\" />\n        <div className=\"flex flex-1 items-center justify-center\">\n          <Calendar\n            mode=\"range\"\n            selected={range}\n            onSelect={(newDate) => {\n              if (newDate) {\n                onSelect({\n                  ...newDate,\n                  to: newDate.to ? endOfDay(newDate.to) : undefined,\n                });\n              }\n            }}\n            className=\"p-2\"\n            disabled={[\n              { after: today }, // Dates before today\n              { before: disableBefore ?? today }, // Dates before last action\n            ]}\n          />\n        </div>\n      </div>\n      <Separator />\n      <div className=\"flex flex-col gap-2 px-3 py-4\">\n        <p className=\"px-1 font-medium text-muted-foreground text-xs\">\n          Custom Range\n        </p>\n        <div className=\"grid gap-2 sm:grid-cols-2\">\n          <div className=\"grid w-full gap-1.5\">\n            <Label htmlFor=\"from\" className=\"px-1\">\n              Start\n            </Label>\n            <Input\n              type=\"datetime-local\"\n              id=\"from\"\n              name=\"from\"\n              min={formatDateForInput(disableBefore ?? today)}\n              max={formatDateForInput(today)}\n              value={range.from ? formatDateForInput(range.from) : \"\"}\n              onChange={(e) => {\n                const newDate = new Date(e.target.value);\n                if (!Number.isNaN(newDate.getTime())) {\n                  onSelect({ ...range, from: newDate });\n                }\n              }}\n              disabled={!range.from}\n            />\n          </div>\n          <div className=\"grid w-full gap-1.5\">\n            <Label htmlFor=\"to\" className=\"px-1\">\n              End\n            </Label>\n            <Input\n              type=\"datetime-local\"\n              id=\"to\"\n              name=\"to\"\n              min={formatDateForInput(range.from ?? today)}\n              max={formatDateForInput(today)}\n              value={range.to ? formatDateForInput(range.to) : \"\"}\n              onChange={(e) => {\n                const newDate = new Date(e.target.value);\n                if (!Number.isNaN(newDate.getTime())) {\n                  onSelect({ ...range, to: newDate });\n                }\n              }}\n              disabled={!range.to}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/development-indicator.tsx",
    "content": "\"use client\";\n\nimport { Kbd } from \"@openstatus/ui/components/ui/kbd\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport * as Portal from \"@radix-ui/react-portal\";\n\nexport function DevelopmentIndicator() {\n  const isMobile = useIsMobile();\n\n  if (process.env.NODE_ENV !== \"production\") return null;\n\n  return (\n    <Portal.Root>\n      <div className=\"pointer-events-none fixed inset-0 z-[9999] border-2 border-destructive\" />\n      <div className=\"fixed inset-x-0 bottom-0 z-[9999] select-none\">\n        <div className=\"flex items-center justify-center\">\n          <TooltipProvider delayDuration={0}>\n            <Tooltip>\n              <TooltipTrigger>\n                <div className=\"w-fit rounded-t bg-destructive px-2 py-1 font-mono text-background text-xs\">\n                  In Beta\n                </div>\n              </TooltipTrigger>\n              <TooltipContent side=\"top\">\n                {!isMobile ? (\n                  <p>\n                    Press <Kbd className=\"-me-0 ms-0\">F</Kbd> key to provide\n                    feedback.\n                  </p>\n                ) : (\n                  <p>Use a larger screen to provide feedback.</p>\n                )}\n              </TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n        </div>\n      </div>\n    </Portal.Root>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/dialogs/export-code.tsx",
    "content": "import { Link } from \"@/components/common/link\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@openstatus/ui/components/ui/dialog\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport type { DialogProps } from \"@radix-ui/react-dialog\";\nimport { Check, Copy } from \"lucide-react\";\n\n// TODL: make it dynamic\nconst YML = `openstatus-marketing:\n  name: OpenStatus Marketing\n  description: Marketing website for OpenStatus\n  active: true\n  public: false\n  frequency: \"10m\"\n  regions: [\"ams\", \"fra\", \"gru\", \"sin\", \"iad\"]\n  kind: \"http\"\n  request:\n    url: https://api.openstatus.dev\n    method: GET`;\n\nexport function ExportCodeDialog(props: DialogProps) {\n  const { copy, isCopied } = useCopyToClipboard();\n\n  return (\n    <Dialog {...props}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Export Configuration</DialogTitle>\n          <DialogDescription>\n            Export and manage your monitor configuration using Infra as Code.\n          </DialogDescription>\n        </DialogHeader>\n        <Tabs defaultValue=\"yml\">\n          <TabsList>\n            <TabsTrigger value=\"yml\">YAML</TabsTrigger>\n            <TabsTrigger value=\"terraform\">Terraform</TabsTrigger>\n          </TabsList>\n          <TabsContent value=\"yml\" className=\"space-y-2\">\n            <pre className=\"relative rounded border bg-muted p-2 text-xs\">\n              {YML}\n              <Button\n                variant=\"outline\"\n                size=\"icon\"\n                className=\"absolute top-2 right-2 size-7 p-1\"\n                onClick={() => copy(YML, { withToast: false, timeout: 1000 })}\n              >\n                {isCopied ? (\n                  <Check className=\"size-3\" />\n                ) : (\n                  <Copy className=\"size-3\" />\n                )}\n              </Button>\n            </pre>\n            <p className=\"text-muted-foreground text-xs\">\n              Use a <code>monitor.openstatus.yml</code> file to configure your\n              monitors. <Link href=\"#\">Read more.</Link>\n            </p>\n          </TabsContent>\n          <TabsContent value=\"terraform\" className=\"space-y-2\">\n            <pre className=\"relative rounded border bg-muted p-2 text-xs\">\n              TODO:\n            </pre>\n            {/* TODO: only showcase if there are any assertions */}\n            <p className=\"text-destructive text-xs\">\n              The Terraform provider does not support assertions yet.\n            </p>\n            <p className=\"text-muted-foreground text-xs\">\n              Use a Terraform provider to manage your monitors.{\" \"}\n              <Link href=\"#\">Read more.</Link>\n            </p>\n          </TabsContent>\n        </Tabs>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/dialogs/upgrade.tsx",
    "content": "import { Link } from \"@/components/common/link\";\nimport { Note, NoteButton } from \"@/components/common/note\";\nimport { BillingAddons } from \"@/components/content/billing-addons\";\nimport { DataTable } from \"@/components/data-table/billing/data-table\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { WorkspacePlan } from \"@openstatus/db/src/schema\";\nimport { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport type { Addons, Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport { getPlansForLimit } from \"@openstatus/db/src/schema/plan/utils\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@openstatus/ui/components/ui/dialog\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport type { DialogProps } from \"@radix-ui/react-dialog\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { CalendarClock } from \"lucide-react\";\n\nconst PLANS = {\n  free: [\"starter\", \"team\"],\n  starter: [\"team\"],\n  team: [],\n} satisfies Record<WorkspacePlan, WorkspacePlan[]>;\n\nexport function UpgradeDialog(\n  props: DialogProps & {\n    limit?: keyof Limits;\n    restrictTo?: WorkspacePlan[];\n  },\n) {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n\n  if (!workspace) return null;\n\n  const planAddons = allPlans[workspace.plan].addons;\n\n  const getRestrictTo = () => {\n    if (props.restrictTo) return props.restrictTo;\n    if (props.limit) return getPlansForLimit(workspace.plan, props.limit);\n    return PLANS[workspace.plan];\n  };\n\n  const restrictTo = getRestrictTo();\n\n  const addon =\n    props.limit && Object.prototype.hasOwnProperty.call(planAddons, props.limit)\n      ? (props.limit as keyof Addons)\n      : null;\n\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"max-h-[80vh] overflow-y-auto sm:max-w-2xl\">\n        <DialogHeader>\n          <DialogTitle>Upgrade Workspace</DialogTitle>\n          <DialogDescription>\n            Upgrade your workspace to support more monitors, status pages,\n            regions, and much more. Get an overview within your{\" \"}\n            <Link\n              onClick={() => props.onOpenChange?.(false)}\n              href=\"/settings/billing\"\n            >\n              billing settings\n            </Link>\n            .\n          </DialogDescription>\n        </DialogHeader>\n        {addon && planAddons[addon] ? (\n          <>\n            <BillingAddons\n              label={planAddons[addon].title}\n              description={planAddons[addon].description}\n              addon={addon}\n              workspace={workspace}\n            />\n            <Separator />\n          </>\n        ) : null}\n        {restrictTo.length === 0 ? (\n          <Note>\n            <CalendarClock />\n            Please contact us to upgrade your plan.\n            <NoteButton variant=\"outline\" asChild>\n              <a\n                href=\"https://openstatus.dev/cal\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"text-nowrap\"\n              >\n                Book a call\n              </a>\n            </NoteButton>\n          </Note>\n        ) : (\n          <DataTable restrictTo={restrictTo} />\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/domains/domain-configuration.tsx",
    "content": "\"use client\";\n\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nimport { Note } from \"@/components/common/note\";\nimport { getSubdomain } from \"@/lib/domains\";\nimport { CircleCheck } from \"lucide-react\";\nimport DomainStatusIcon from \"./domain-status-icon\";\nimport { useDomainStatus } from \"./use-domain-status\";\n\nexport const InlineSnippet = ({\n  className,\n  children,\n}: {\n  className?: string;\n  children?: string;\n}) => {\n  return (\n    <span\n      className={cn(\n        \"inline-block rounded-md bg-muted px-1 py-0.5 font-mono\",\n        className,\n      )}\n    >\n      {children}\n    </span>\n  );\n};\n\n// FIXME: add loading state!\nexport default function DomainConfiguration({ domain }: { domain: string }) {\n  const { status, domainJson, isLoading } = useDomainStatus(domain);\n\n  if (!status || !domainJson) return null;\n\n  if (status === \"Valid Configuration\")\n    return (\n      <Note color=\"success\">\n        <CircleCheck />\n        Your domain is configured and you can use it to access your status page.\n      </Note>\n    );\n\n  const subdomain =\n    domainJson?.name && domainJson?.apexName\n      ? getSubdomain(domainJson.name, domainJson.apexName)\n      : null;\n\n  const txtVerification =\n    (status === \"Pending Verification\" &&\n      domainJson?.verification?.find((x) => x.type === \"TXT\")) ||\n    null;\n\n  return (\n    <div>\n      <div className=\"mb-4 flex items-center space-x-2\">\n        <DomainStatusIcon status={status} loading={isLoading} />\n        <p className=\"font-semibold\">{status}</p>\n        <Badge variant=\"secondary\">{domain}</Badge>\n      </div>\n      {txtVerification ? (\n        <>\n          <p className=\"text-sm\">\n            Please set the following TXT record on{\" \"}\n            <InlineSnippet>{domainJson.apexName}</InlineSnippet> to prove\n            ownership of <InlineSnippet>{domainJson.name}</InlineSnippet>:\n          </p>\n          <div className=\"my-5 flex items-start justify-start space-x-10 rounded-md bg-muted p-2\">\n            <div>\n              <p className=\"font-bold text-sm\">Type</p>\n              <p className=\"mt-2 font-mono text-sm\">{txtVerification.type}</p>\n            </div>\n            <div>\n              <p className=\"font-bold text-sm\">Name</p>\n              <p className=\"mt-2 font-mono text-sm\">\n                {txtVerification.domain.slice(\n                  0,\n                  txtVerification.domain.length -\n                    (domainJson?.apexName?.length || 0) -\n                    1,\n                )}\n              </p>\n            </div>\n            <div>\n              <p className=\"font-bold text-sm\">Value</p>\n              <p className=\"mt-2 font-mono text-sm\">\n                <span className=\"text-ellipsis\">{txtVerification.value}</span>\n              </p>\n            </div>\n          </div>\n          <p className=\"text-muted-foreground text-sm\">\n            Warning: if you are using this domain for another site, setting this\n            TXT record will transfer domain ownership away from that site and\n            break it. Please exercise caution when setting this record.\n          </p>\n        </>\n      ) : status === \"Unknown Error\" ? (\n        <p className=\"mb-5 text-sm\">{domainJson?.error?.message}</p>\n      ) : (\n        <>\n          <Tabs defaultValue={subdomain ? \"CNAME\" : \"A\"}>\n            <TabsList>\n              <TabsTrigger value=\"A\">\n                A Record{!subdomain && \" (recommended)\"}\n              </TabsTrigger>\n              <TabsTrigger value=\"CNAME\">\n                CNAME Record{subdomain && \" (recommended)\"}\n              </TabsTrigger>\n            </TabsList>\n            <TabsContent value=\"A\" className=\"space-y-2\">\n              <p className=\"text-sm\">\n                To configure your apex domain (\n                <InlineSnippet>{domainJson.apexName}</InlineSnippet>\n                ), set the following A record on your DNS provider to continue:\n              </p>\n              <div className=\"flex items-center justify-start space-x-10 rounded-md bg-muted p-2\">\n                <div>\n                  <p className=\"font-bold text-sm\">Type</p>\n                  <p className=\"mt-2 font-mono text-sm\">A</p>\n                </div>\n                <div>\n                  <p className=\"font-bold text-sm\">Name</p>\n                  <p className=\"mt-2 font-mono text-sm\">@</p>\n                </div>\n                <div>\n                  <p className=\"font-bold text-sm\">Value</p>\n                  <p className=\"mt-2 font-mono text-sm\">76.76.21.21</p>\n                </div>\n                <div>\n                  <p className=\"font-bold text-sm\">TTL</p>\n                  <p className=\"mt-2 font-mono text-sm\">86400</p>\n                </div>\n              </div>\n            </TabsContent>\n            <TabsContent value=\"CNAME\">\n              <div className=\"flex items-center justify-start space-x-10 rounded-md bg-muted p-2\">\n                <div>\n                  <p className=\"font-bold text-sm\">Type</p>\n                  <p className=\"mt-2 font-mono text-sm\">CNAME</p>\n                </div>\n                <div>\n                  <p className=\"font-bold text-sm\">Name</p>\n                  <p className=\"mt-2 font-mono text-sm\">{subdomain ?? \"www\"}</p>\n                </div>\n                <div>\n                  <p className=\"font-bold text-sm\">Value</p>\n                  <p className=\"mt-2 font-mono text-sm\">cname.vercel-dns.com</p>\n                </div>\n                <div>\n                  <p className=\"font-bold text-sm\">TTL</p>\n                  <p className=\"mt-2 font-mono text-sm\">86400</p>\n                </div>\n              </div>\n            </TabsContent>\n          </Tabs>\n          <p className=\"muted-foreground mt-5 text-sm\">\n            Note: for TTL, if <InlineSnippet>86400</InlineSnippet> is not\n            available, set the highest value possible. Also, domain propagation\n            can take up to an hour.\n          </p>\n        </>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/domains/domain-status-icon.tsx",
    "content": "\"use client\";\n\nimport { AlertCircle, CheckCircle2, LoaderCircle, XCircle } from \"lucide-react\";\n\nimport type { DomainVerificationStatusProps } from \"@openstatus/api/src/router/domain\";\n\nexport default function DomainStatusIcon({\n  status,\n  loading,\n}: {\n  status: DomainVerificationStatusProps;\n  loading?: boolean;\n}) {\n  return loading ? (\n    <LoaderCircle\n      className=\"animate-spin text-muted-foreground\"\n      stroke=\"currentColor\"\n    />\n  ) : status === \"Valid Configuration\" ? (\n    <CheckCircle2\n      fill=\"#22c55e\"\n      stroke=\"currentColor\"\n      className=\"text-background\"\n    />\n  ) : status === \"Pending Verification\" ? (\n    <AlertCircle\n      fill=\"#eab308\"\n      stroke=\"currentColor\"\n      className=\"text-background\"\n    />\n  ) : (\n    <XCircle fill=\"#ef4444\" stroke=\"currentColor\" className=\"text-background\" />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/domains/use-domain-status.ts",
    "content": "import type { DomainVerificationStatusProps } from \"@openstatus/api/src/router/domain\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useCallback } from \"react\";\n\nexport function useDomainStatus(domain?: string) {\n  const trpc = useTRPC();\n  const {\n    data: domainJson,\n    refetch: refetchDomain,\n    isLoading: isLoadingDomain,\n    isRefetching: isRefetchingDomain,\n  } = useQuery(trpc.domain.getDomainResponse.queryOptions({ domain }));\n  const {\n    data: configJson,\n    refetch: refetchConfig,\n    isLoading: isLoadingConfig,\n    isRefetching: isRefetchingConfig,\n  } = useQuery(trpc.domain.getConfigResponse.queryOptions({ domain }));\n  const {\n    data: verificationJson,\n    refetch: refetchVerification,\n    isLoading: isLoadingVerification,\n    isRefetching: isRefetchingVerification,\n  } = useQuery(\n    trpc.domain.verifyDomain.queryOptions(\n      { domain },\n      { enabled: !domainJson?.verified },\n    ),\n  );\n\n  const refreshAll = useCallback(() => {\n    refetchDomain();\n    refetchConfig();\n    refetchVerification();\n  }, [refetchDomain, refetchConfig, refetchVerification]);\n\n  let status: DomainVerificationStatusProps = \"Valid Configuration\";\n\n  if (domainJson?.error?.code === \"not_found\") {\n    // domain not found on Vercel project\n    status = \"Domain Not Found\";\n\n    // unknown error\n  } else if (domainJson?.error) {\n    status = \"Unknown Error\";\n\n    // if domain is not verified, we try to verify now\n  } else if (!domainJson?.verified) {\n    status = \"Pending Verification\";\n\n    // domain was just verified\n    if (verificationJson?.verified) {\n      status = \"Valid Configuration\";\n    }\n  } else if (configJson?.misconfigured) {\n    status = \"Invalid Configuration\";\n  } else {\n    status = \"Valid Configuration\";\n  }\n\n  return {\n    status,\n    domainJson,\n    refresh: refreshAll,\n    isLoading:\n      isLoadingDomain ||\n      isLoadingConfig ||\n      isLoadingVerification ||\n      isRefetchingDomain ||\n      isRefetchingConfig ||\n      isRefetchingVerification,\n  };\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/dropdowns/quick-actions.tsx",
    "content": "\"use client\";\n\nimport type * as React from \"react\";\nimport { useState, useTransition } from \"react\";\n\nimport {\n  Check,\n  Copy,\n  type LucideIcon,\n  MoreHorizontal,\n  Trash2,\n} from \"lucide-react\";\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport type { DropdownMenuContentProps } from \"@radix-ui/react-dropdown-menu\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { toast } from \"sonner\";\n\ninterface QuickActionsProps extends React.ComponentProps<typeof Button> {\n  align?: DropdownMenuContentProps[\"align\"];\n  side?: DropdownMenuContentProps[\"side\"];\n  actions?: {\n    id: string;\n    label: string;\n    icon: LucideIcon;\n    variant: \"default\" | \"destructive\";\n    onClick?: () => Promise<void> | void;\n  }[];\n  deleteAction?: {\n    /**\n     * The value that must be typed to confirm deletion. Also used in the dialog title.\n     */\n    confirmationValue: string;\n    submitAction?: () => Promise<void>;\n  };\n}\n\nexport function QuickActions({\n  align = \"end\",\n  side,\n  className,\n  actions,\n  deleteAction,\n  children,\n  ...props\n}: QuickActionsProps) {\n  const [value, setValue] = useState(\"\");\n  const [isPending, startTransition] = useTransition();\n  const [open, setOpen] = useState(false);\n  const { copy, isCopied } = useCopyToClipboard();\n\n  const handleDelete = async () => {\n    startTransition(async () => {\n      if (!deleteAction?.submitAction) return;\n      const promise = deleteAction.submitAction();\n      toast.promise(promise, {\n        loading: \"Deleting...\",\n        success: \"Deleted\",\n        error: (error) => {\n          if (isTRPCClientError(error)) {\n            return error.message;\n          }\n          return \"Failed to delete\";\n        },\n      });\n      try {\n        await promise;\n      } catch (error) {\n        console.error(\"Failed to delete:\", error);\n      } finally {\n        setOpen(false);\n      }\n    });\n  };\n\n  return (\n    <AlertDialog open={open} onOpenChange={setOpen}>\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          {children ?? (\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className={className ?? \"h-7 w-7 data-[state=open]:bg-accent\"}\n              {...props}\n            >\n              <MoreHorizontal />\n            </Button>\n          )}\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align={align} side={side} className=\"w-36\">\n          <DropdownMenuLabel className=\"sr-only\">\n            Quick Actions\n          </DropdownMenuLabel>\n          {actions\n            ?.filter((item) => item.id !== \"delete\")\n            .map((item) => (\n              <DropdownMenuGroup key={item.id}>\n                <DropdownMenuItem\n                  variant={item.variant}\n                  disabled={!item.onClick}\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    item.onClick?.();\n                  }}\n                >\n                  <item.icon className=\"text-muted-foreground\" />\n                  <span className=\"truncate\">{item.label}</span>\n                </DropdownMenuItem>\n              </DropdownMenuGroup>\n            ))}\n          {deleteAction && (\n            <>\n              {/* NOTE: add a separator only if actions exist */}\n              {actions?.length ? <DropdownMenuSeparator /> : null}\n              <AlertDialogTrigger asChild>\n                <DropdownMenuItem variant=\"destructive\">\n                  <Trash2 className=\"text-muted-foreground\" />\n                  Delete\n                </DropdownMenuItem>\n              </AlertDialogTrigger>\n            </>\n          )}\n        </DropdownMenuContent>\n      </DropdownMenu>\n      <AlertDialogContent\n        onCloseAutoFocus={(event) => {\n          // NOTE: bug where the body is not clickable after closing the alert dialog\n          event.preventDefault();\n          document.body.style.pointerEvents = \"\";\n        }}\n      >\n        <AlertDialogHeader>\n          <AlertDialogTitle>\n            Are you sure about deleting `{deleteAction?.confirmationValue}`?\n          </AlertDialogTitle>\n          <AlertDialogDescription>\n            This action cannot be undone. This will permanently remove the entry\n            from the database.\n          </AlertDialogDescription>\n        </AlertDialogHeader>\n        {deleteAction?.confirmationValue && (\n          <form id=\"form-alert-dialog\" className=\"space-y-1.5\">\n            <p className=\"text-muted-foreground text-sm\">\n              Type{\" \"}\n              <Button\n                variant=\"secondary\"\n                size=\"sm\"\n                type=\"button\"\n                className=\"font-normal [&_svg]:size-3\"\n                onClick={() =>\n                  copy(deleteAction.confirmationValue || \"\", {\n                    withToast: false,\n                  })\n                }\n              >\n                {deleteAction.confirmationValue}\n                {isCopied ? <Check /> : <Copy />}\n              </Button>{\" \"}\n              to confirm\n            </p>\n            <Input value={value} onChange={(e) => setValue(e.target.value)} />\n          </form>\n        )}\n        <AlertDialogFooter>\n          <AlertDialogCancel onClick={(e) => e.stopPropagation()}>\n            Cancel\n          </AlertDialogCancel>\n          <AlertDialogAction\n            className=\"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\"\n            disabled={\n              (deleteAction?.confirmationValue &&\n                value !== deleteAction?.confirmationValue) ||\n              isPending\n            }\n            form=\"form-alert-dialog\"\n            type=\"submit\"\n            onClick={(e) => {\n              e.preventDefault();\n              handleDelete();\n            }}\n          >\n            {isPending ? \"Deleting...\" : \"Delete\"}\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/components/form-components.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport { UpgradeDialog } from \"@/components/dialogs/upgrade\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { STATUS } from \"@/components/nav/nav-monitors\";\nimport {\n  Sortable,\n  SortableContent,\n  SortableItem,\n  SortableItemHandle,\n  SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport { cn } from \"@/lib/utils\";\nimport type { UniqueIdentifier } from \"@dnd-kit/core\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@openstatus/ui/components/ui/command\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport {\n  Check,\n  Eye,\n  EyeOff,\n  GripVertical,\n  Link2,\n  Link2Off,\n  Plug,\n  Plus,\n  Trash2,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useState, useTransition } from \"react\";\nimport { type UseFormReturn, useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\ntype PageComponent = RouterOutputs[\"pageComponent\"][\"list\"][number];\ntype Monitor = RouterOutputs[\"monitor\"][\"list\"][number];\ntype Workspace = RouterOutputs[\"workspace\"][\"get\"];\n\ntype ComponentGroup = {\n  id: number;\n  name: string;\n  components: PageComponent[];\n};\n\nconst componentSchema = z.object({\n  id: z.number(),\n  monitorId: z.number().nullish(),\n  order: z.number(),\n  name: z.string().min(1, { message: \"Name is required\" }),\n  description: z.string().optional(),\n  type: z.enum([\"monitor\", \"static\"]),\n});\n\nconst schema = z.object({\n  components: z.array(componentSchema),\n  groups: z.array(\n    z.object({\n      id: z.number(),\n      order: z.number(),\n      name: z.string(),\n      components: z.array(componentSchema).min(1, {\n        message: \"At least one component is required\",\n      }),\n    }),\n  ),\n});\n\nconst getSortedComponents = (\n  components: PageComponent[],\n  componentData: {\n    id: number;\n    order: number;\n    name?: string;\n    type?: \"monitor\" | \"static\";\n    monitorId?: number | null;\n  }[],\n  monitors: Monitor[],\n) => {\n  const orderMap = new Map(componentData?.map((c) => [c.id, c.order]) ?? []);\n\n  // Create a map of existing components\n  const componentMap = new Map(components.map((c) => [c.id, c]));\n\n  // Create a map of monitors for lookup\n  const monitorMap = new Map(monitors.map((m) => [m.id, m]));\n\n  // Create synthetic components for any in componentData that don't exist in components\n  componentData.forEach((c) => {\n    if (!componentMap.has(c.id)) {\n      // Look up monitor data if this is a monitor component\n      const monitor = c.monitorId ? monitorMap.get(c.monitorId) : null;\n\n      // Create synthetic PageComponent\n      componentMap.set(c.id, {\n        id: c.id,\n        name: c.name ?? \"\",\n        type: c.type ?? \"static\",\n        monitorId: c.monitorId ?? null,\n        monitor: monitor ?? null,\n        groupId: null,\n        groupOrder: null,\n        order: c.order,\n      } as PageComponent);\n    }\n  });\n\n  return Array.from(componentMap.values())\n    .filter((component) => orderMap.has(component.id))\n    .sort((a, b) => {\n      const aOrder = orderMap.get(a.id) ?? 0;\n      const bOrder = orderMap.get(b.id) ?? 0;\n      return aOrder - bOrder;\n    });\n};\n\nconst getSortedItems = (\n  components: PageComponent[],\n  componentData: {\n    id: number;\n    order: number;\n    name?: string;\n    type?: \"monitor\" | \"static\";\n    monitorId?: number | null;\n  }[],\n  groups: Array<{\n    id: number;\n    order: number;\n    name: string;\n    components: Array<{\n      id: number;\n      order: number;\n      name?: string;\n      type?: \"monitor\" | \"static\";\n      monitorId?: number | null;\n    }>;\n  }>,\n  monitors: Monitor[],\n): (PageComponent | ComponentGroup)[] => {\n  // Create map of component orders\n  const componentOrderMap = new Map(componentData.map((c) => [c.id, c.order]));\n\n  // Create a map of existing components\n  const componentMap = new Map(components.map((c) => [c.id, c]));\n\n  // Create a map of monitors for lookup\n  const monitorMap = new Map(monitors.map((m) => [m.id, m]));\n\n  // Create synthetic components for any in componentData that don't exist in components\n  componentData.forEach((c) => {\n    if (!componentMap.has(c.id)) {\n      // Look up monitor data if this is a monitor component\n      const monitor = c.monitorId ? monitorMap.get(c.monitorId) : null;\n\n      // Create synthetic PageComponent\n      componentMap.set(c.id, {\n        id: c.id,\n        name: c.name ?? \"\",\n        type: c.type ?? \"static\",\n        monitorId: c.monitorId ?? null,\n        monitor: monitor ?? null,\n        groupId: null,\n        groupOrder: null,\n        order: c.order,\n      } as PageComponent);\n    }\n  });\n\n  // Get all enhanced components (including synthetic ones)\n  const enhancedComponents = Array.from(componentMap.values());\n\n  // Create array of components with their orders\n  const componentsWithOrder = enhancedComponents\n    .filter((component) => componentOrderMap.has(component.id))\n    .map((component) => ({\n      item: component,\n      order: componentOrderMap.get(component.id) ?? 0,\n    }));\n\n  // Create array of groups with their orders\n  const groupsWithOrder = groups.map((group) => ({\n    item: {\n      id: group.id,\n      name: group.name,\n      components: getSortedComponents(\n        enhancedComponents,\n        group.components,\n        monitors,\n      ),\n    } as ComponentGroup,\n    order: group.order,\n  }));\n\n  // Combine and sort by order\n  return [...componentsWithOrder, ...groupsWithOrder]\n    .sort((a, b) => a.order - b.order)\n    .map((entry) => entry.item);\n};\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormComponents({\n  defaultValues,\n  onSubmit,\n  pageComponents,\n  allPageComponents,\n  monitors,\n  workspace,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  /** Page components available for selection (standalone, not in groups) */\n  pageComponents: PageComponent[];\n  /** All page components for the page (including those in groups) */\n  allPageComponents: PageComponent[];\n  /** Monitors available for selection */\n  monitors: Monitor[];\n  /**\n   * The workspace the page belongs to\n   */\n  workspace: Workspace;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? { components: [], groups: [] },\n  });\n  const [isPending, startTransition] = useTransition();\n  const watchComponents = form.watch(\"components\");\n  const watchGroups = form.watch(\"groups\");\n  const [openUpgradeDialog, setOpenUpgradeDialog] = useState(false);\n  const [data, setData] = useState<(PageComponent | ComponentGroup)[]>(\n    getSortedItems(\n      allPageComponents,\n      defaultValues?.components ?? [],\n      defaultValues?.groups ?? [],\n      monitors,\n    ),\n  );\n\n  // Get all monitor IDs that are already used (in standalone components or groups)\n  const usedMonitorIds = new Set([\n    ...(watchComponents ?? [])\n      .filter((c) => c.monitorId)\n      .map((c) => c.monitorId),\n    ...(watchGroups ?? [])\n      .flatMap((g) => g.components)\n      .filter((c) => c.monitorId)\n      .map((c) => c.monitorId),\n  ]);\n\n  useEffect(() => {\n    const sortedItems = getSortedItems(\n      allPageComponents,\n      watchComponents,\n      watchGroups ?? [],\n      monitors,\n    );\n    setData(sortedItems);\n  }, [watchComponents, watchGroups, allPageComponents, monitors]);\n\n  const validateLimit = useCallback(() => {\n    const limitReached = workspace.limits[\"page-components\"] <= data.length;\n    if (limitReached) {\n      setOpenUpgradeDialog(true);\n      return false;\n    }\n    return true;\n  }, [workspace, data.length]);\n\n  const onValueChange = useCallback(\n    (newItems: (PageComponent | ComponentGroup)[]) => {\n      setData(newItems);\n\n      // Update components with their position in the overall list\n      const existingComponents = form.getValues(\"components\") ?? [];\n      const components = newItems\n        .map((item, index) => ({ item, index }))\n        .filter(\n          (entry): entry is { item: PageComponent; index: number } =>\n            \"type\" in entry.item,\n        )\n        .map(({ item, index }) => {\n          const existingComponent = existingComponents.find(\n            (c) => c.id === item.id,\n          );\n          return {\n            id: item.id,\n            monitorId: item.monitorId,\n            order: index,\n            name: existingComponent?.name ?? item.name,\n            description: existingComponent?.description ?? \"\",\n            type: item.type,\n          };\n        });\n      form.setValue(\"components\", components);\n\n      // Update groups with their position in the overall list\n      const existingGroups = form.getValues(\"groups\") ?? [];\n      const groups = newItems\n        .map((item, index) => ({ item, index }))\n        .filter(\n          (entry): entry is { item: ComponentGroup; index: number } =>\n            \"components\" in entry.item && !(\"type\" in entry.item),\n        )\n        .map(({ item, index }) => {\n          const existingGroup = existingGroups.find((g) => g.id === item.id);\n          return existingGroup\n            ? {\n                ...existingGroup,\n                order: index,\n              }\n            : {\n                id: item.id,\n                order: index,\n                name: item.name,\n                components: [],\n              };\n        });\n      form.setValue(\"groups\", groups);\n    },\n    [form],\n  );\n\n  const getItemValue = useCallback(\n    (item: PageComponent | ComponentGroup) => item.id,\n    [],\n  );\n\n  const handleAddGroup = useCallback(() => {\n    if (!validateLimit()) return;\n\n    const newGroupId = Date.now();\n    const existingGroups = form.getValues(\"groups\") ?? [];\n    const existingComponents = form.getValues(\"components\") ?? [];\n    const order = existingGroups.length + existingComponents.length;\n    const newGroups = [\n      ...existingGroups,\n      { id: newGroupId, order, name: \"\", components: [] },\n    ];\n    form.setValue(\"groups\", newGroups);\n    setData((prev) => [...prev, { id: newGroupId, name: \"\", components: [] }]);\n  }, [form, validateLimit]);\n\n  const handleDeleteGroup = useCallback(\n    (groupId: number) => {\n      const existingGroups = form.getValues(\"groups\") ?? [];\n      form.setValue(\n        \"groups\",\n        existingGroups.filter((g) => g.id !== groupId),\n      );\n      setData((prev) => prev.filter((item) => item.id !== groupId));\n    },\n    [form],\n  );\n\n  const handleDeleteComponent = useCallback(\n    (componentId: number) => {\n      const existingComponents = form.getValues(\"components\") ?? [];\n      form.setValue(\n        \"components\",\n        existingComponents.filter((c) => c.id !== componentId),\n      );\n      setData((prev) => prev.filter((item) => item.id !== componentId));\n    },\n    [form],\n  );\n\n  const renderOverlay = useCallback(\n    ({ value }: { value: UniqueIdentifier }) => {\n      const index = data.findIndex((item) => item.id === value);\n      if (index === -1) return null;\n      const item = data[index];\n\n      if (\"type\" in item) {\n        return (\n          <ComponentRow\n            component={item}\n            form={form}\n            className=\"border-transparent border-x px-2\"\n            onDelete={handleDeleteComponent}\n            // FIXME: this is used to show an input instead of the name when dragging a component\n            // fieldNamePrefix={`components.${index}`}\n          />\n        );\n      }\n\n      const groups = form.getValues(\"groups\") ?? [];\n      const groupIndex = groups.findIndex((g) => g.id === item.id);\n      return (\n        <ComponentGroupRow\n          group={item}\n          groupIndex={groupIndex}\n          onDeleteGroup={handleDeleteGroup}\n          form={form}\n          allPageComponents={allPageComponents}\n          monitors={monitors}\n          validateLimit={validateLimit}\n        />\n      );\n    },\n    [\n      data,\n      handleDeleteGroup,\n      form,\n      allPageComponents,\n      monitors,\n      handleDeleteComponent,\n      validateLimit,\n    ],\n  );\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <>\n      <Form {...form}>\n        <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n          <FormCard>\n            <FormCardHeader>\n              <FormCardTitle>Components</FormCardTitle>\n              <FormCardDescription>\n                Manage your page components\n              </FormCardDescription>\n            </FormCardHeader>\n            <FormCardContent className=\"flex flex-row gap-2\">\n              <Button variant=\"outline\" type=\"button\" onClick={handleAddGroup}>\n                <Plus />\n                Add Component Group\n              </Button>\n              <FormField\n                control={form.control}\n                name=\"components\"\n                render={({ field }) => (\n                  <FormItem className=\"flex flex-col\">\n                    <FormLabel className=\"sr-only\">Components</FormLabel>\n                    <DropdownMenu>\n                      <DropdownMenuTrigger asChild>\n                        <Button variant=\"outline\" className=\"w-full\">\n                          <Plus />\n                          Add Component\n                        </Button>\n                      </DropdownMenuTrigger>\n                      <DropdownMenuContent align=\"start\">\n                        <DropdownMenuGroup>\n                          <DropdownMenuItem\n                            onClick={() => {\n                              if (!validateLimit()) return;\n                              form.setValue(\"components\", [\n                                ...field.value,\n                                {\n                                  id: Date.now(),\n                                  monitorId: null,\n                                  order: watchComponents.length,\n                                  name: \"\",\n                                  description: \"\",\n                                  type: \"static\" as const,\n                                },\n                              ]);\n                            }}\n                          >\n                            <Link2Off className=\"text-muted-foreground\" />\n                            Add Static Component\n                          </DropdownMenuItem>\n                          <DropdownMenuSub>\n                            <DropdownMenuSubTrigger className=\"gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0\">\n                              <Link2 className=\"text-muted-foreground\" />\n                              Add Monitor Component\n                            </DropdownMenuSubTrigger>\n                            <DropdownMenuSubContent className=\"p-0\">\n                              <Command>\n                                <CommandInput\n                                  placeholder=\"Search monitors...\"\n                                  className=\"h-9\"\n                                />\n                                <CommandList>\n                                  <CommandEmpty>\n                                    No monitors found.\n                                  </CommandEmpty>\n                                  <CommandGroup>\n                                    {monitors.map((monitor) => {\n                                      const isUsed = usedMonitorIds.has(\n                                        monitor.id,\n                                      );\n                                      const isSelected = field.value.some(\n                                        (c) => c.monitorId === monitor.id,\n                                      );\n                                      return (\n                                        <CommandItem\n                                          value={monitor.name}\n                                          key={monitor.id}\n                                          disabled={isUsed}\n                                          onSelect={() => {\n                                            if (isSelected) {\n                                              form.setValue(\n                                                \"components\",\n                                                field.value.filter(\n                                                  (c) =>\n                                                    c.monitorId !== monitor.id,\n                                                ),\n                                              );\n                                            } else {\n                                              if (!validateLimit()) return;\n                                              form.setValue(\"components\", [\n                                                ...field.value,\n                                                {\n                                                  id: Date.now(),\n                                                  monitorId: monitor.id,\n                                                  order: watchComponents.length,\n                                                  name: monitor.name,\n                                                  description:\n                                                    monitor.description,\n                                                  type: \"monitor\" as const,\n                                                },\n                                              ]);\n                                            }\n                                          }}\n                                        >\n                                          {monitor.name}\n                                          <Check\n                                            className={cn(\n                                              \"ml-auto\",\n                                              isSelected\n                                                ? \"opacity-100\"\n                                                : \"opacity-0\",\n                                            )}\n                                          />\n                                        </CommandItem>\n                                      );\n                                    })}\n                                  </CommandGroup>\n                                </CommandList>\n                              </Command>\n                            </DropdownMenuSubContent>\n                          </DropdownMenuSub>\n                          <DropdownMenuItem disabled>\n                            <Plug className=\"text-muted-foreground\" />\n                            Add Third-Party Component\n                          </DropdownMenuItem>\n                        </DropdownMenuGroup>\n                      </DropdownMenuContent>\n                    </DropdownMenu>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            </FormCardContent>\n            <FormCardSeparator />\n            <FormCardContent>\n              <Sortable\n                value={data}\n                onValueChange={onValueChange}\n                getItemValue={getItemValue}\n                orientation=\"vertical\"\n              >\n                {data.length ? (\n                  <SortableContent className=\"grid gap-2\">\n                    {data.map((item) => {\n                      if (\"type\" in item) {\n                        const components = form.getValues(\"components\") ?? [];\n                        const componentIndex = components.findIndex(\n                          (c) => c.id === item.id,\n                        );\n                        return (\n                          <ComponentRow\n                            key={`${item.id}-component`}\n                            className=\"border-transparent border-x px-2\"\n                            component={item}\n                            form={form}\n                            onDelete={handleDeleteComponent}\n                            fieldNamePrefix={\n                              componentIndex >= 0\n                                ? `components.${componentIndex}`\n                                : undefined\n                            }\n                          />\n                        );\n                      }\n                      const groups = form.getValues(\"groups\") ?? [];\n                      const groupIndex = groups.findIndex(\n                        (g) => g.id === item.id,\n                      );\n                      return (\n                        <ComponentGroupRow\n                          key={`${item.id}-group`}\n                          group={item}\n                          groupIndex={groupIndex}\n                          onDeleteGroup={handleDeleteGroup}\n                          form={form}\n                          allPageComponents={allPageComponents}\n                          monitors={monitors}\n                          validateLimit={validateLimit}\n                        />\n                      );\n                    })}\n                    <SortableOverlay>{renderOverlay}</SortableOverlay>\n                  </SortableContent>\n                ) : (\n                  <EmptyStateContainer>\n                    <EmptyStateTitle>No components selected</EmptyStateTitle>\n                  </EmptyStateContainer>\n                )}\n              </Sortable>\n            </FormCardContent>\n            <FormCardFooter>\n              <FormCardFooterInfo>\n                Learn more about{\" \"}\n                <Link href=\"https://docs.openstatus.dev/reference/status-page/#page-components\">\n                  page components\n                </Link>\n                .\n              </FormCardFooterInfo>\n              <Button type=\"submit\" disabled={isPending}>\n                {isPending ? \"Submitting...\" : \"Submit\"}\n              </Button>\n            </FormCardFooter>\n          </FormCard>\n        </form>\n      </Form>\n      <UpgradeDialog\n        limit=\"page-components\"\n        open={openUpgradeDialog}\n        onOpenChange={setOpenUpgradeDialog}\n      />\n    </>\n  );\n}\n\ninterface ComponentRowProps\n  extends Omit<React.ComponentPropsWithoutRef<typeof SortableItem>, \"value\"> {\n  component: PageComponent;\n  form: UseFormReturn<FormValues>;\n  onDelete: (componentId: number) => void;\n  /** The form field name prefix, e.g. \"components.0\" or \"groups.0.components.1\" */\n  fieldNamePrefix?: string;\n}\n\nfunction ComponentRow({\n  component,\n  className,\n  onDelete,\n  form,\n  fieldNamePrefix,\n  ...props\n}: ComponentRowProps) {\n  return (\n    <SortableItem\n      value={component.id}\n      asChild\n      className={cn(\"rounded-md\", className)}\n      {...props}\n    >\n      <div className=\"grid h-9 grid-cols-4 gap-2\">\n        <div className=\"flex flex-row items-center gap-1 self-center\">\n          <SortableItemHandle>\n            <GripVertical\n              size={16}\n              aria-hidden=\"true\"\n              className=\"text-muted-foreground\"\n            />\n          </SortableItemHandle>\n          {fieldNamePrefix ? (\n            <FormField\n              key={`${component.id}-name-${fieldNamePrefix}`}\n              control={form.control}\n              name={`${fieldNamePrefix}.name` as \"components.0.name\"}\n              render={({ field }) => (\n                <FormItem className=\"w-full\">\n                  <FormLabel className=\"sr-only\">Component name</FormLabel>\n                  <FormControl>\n                    <Input\n                      placeholder=\"Name\"\n                      className=\"w-full bg-background\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          ) : (\n            <span className=\"truncate rounded-md border border-transparent px-3 py-1 text-sm\">\n              {component.name}\n            </span>\n          )}\n        </div>\n        <div className=\"flex flex-row items-center gap-1 self-center\">\n          {fieldNamePrefix ? (\n            <FormField\n              key={`${component.id}-description-${fieldNamePrefix}`}\n              control={form.control}\n              name={\n                `${fieldNamePrefix}.description` as \"components.0.description\"\n              }\n              render={({ field }) => (\n                <FormItem className=\"w-full\">\n                  <FormLabel className=\"sr-only\">\n                    Component description\n                  </FormLabel>\n                  <FormControl>\n                    <Input\n                      placeholder=\"Description\"\n                      className=\"w-full bg-background\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          ) : (\n            <span className=\"truncate rounded-md border border-transparent px-3 py-1 text-sm\">\n              {component.description}\n            </span>\n          )}\n        </div>\n        <div className=\"flex items-center gap-2 self-center text-muted-foreground text-sm\">\n          {component.monitor && component.type === \"monitor\" ? (\n            <Link\n              href={`/monitors/${component.monitorId}/overview`}\n              onClick={(e) => e.stopPropagation()}\n              className=\"flex w-full items-center gap-2 truncate py-1.5 text-sm\"\n            >\n              <Link2 className=\"size-4 shrink-0\" />{\" \"}\n              <span className=\"truncate\">{component.monitor.name}</span>\n            </Link>\n          ) : (\n            <span className=\"flex items-center gap-2 text-muted-foreground text-sm\">\n              <Link2Off className=\"size-4 shrink-0\" />{\" \"}\n              <span className=\"truncate\">Static Component</span>\n            </span>\n          )}\n        </div>\n        <div className=\"flex justify-between\">\n          <div className=\"flex flex-1 items-center gap-2.5\">\n            {component.monitor && component.type === \"monitor\" ? (\n              <div className=\"flex items-center gap-2\">\n                <TooltipProvider delayDuration={0}>\n                  {component.monitor.public ? (\n                    <Tooltip>\n                      <TooltipTrigger>\n                        <Eye className=\"size-4 text-muted-foreground\" />\n                      </TooltipTrigger>\n                      <TooltipContent>Public</TooltipContent>\n                    </Tooltip>\n                  ) : (\n                    <Tooltip>\n                      <TooltipTrigger>\n                        <EyeOff className=\"size-4 text-muted-foreground\" />\n                      </TooltipTrigger>\n                      <TooltipContent>Private</TooltipContent>\n                    </Tooltip>\n                  )}\n                </TooltipProvider>\n              </div>\n            ) : null}\n            {component.monitor && component.type === \"monitor\" ? (\n              <div\n                className={cn(\n                  \"size-2 rounded-full\",\n                  STATUS[\n                    component.monitor.active\n                      ? component.monitor.status\n                      : \"inactive\"\n                  ],\n                )}\n              />\n            ) : null}\n          </div>\n          <AlertDialog>\n            <AlertDialogTrigger asChild>\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"text-destructive hover:bg-destructive/10 hover:text-destructive dark:hover:bg-destructive/20 [&_svg]:size-4 [&_svg]:text-destructive\"\n              >\n                <Trash2 />\n              </Button>\n            </AlertDialogTrigger>\n            <AlertDialogContent>\n              <AlertDialogHeader>\n                <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n                <AlertDialogDescription>\n                  Once saved, this will unlink the component from attached\n                  status reports and maintenances.\n                </AlertDialogDescription>\n                <ComponentAttachments\n                  statusReports={component.statusReports ?? []}\n                  maintenances={component.maintenances ?? []}\n                />\n              </AlertDialogHeader>\n              <AlertDialogFooter>\n                <AlertDialogCancel>Cancel</AlertDialogCancel>\n                <AlertDialogAction\n                  className=\"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\"\n                  onClick={() => onDelete(component.id)}\n                >\n                  Remove\n                </AlertDialogAction>\n              </AlertDialogFooter>\n            </AlertDialogContent>\n          </AlertDialog>\n        </div>\n      </div>\n    </SortableItem>\n  );\n}\n\ninterface ComponentGroupRowProps\n  extends Omit<React.ComponentPropsWithoutRef<typeof SortableItem>, \"value\"> {\n  group: ComponentGroup;\n  groupIndex: number;\n  onDeleteGroup: (groupId: number) => void;\n  form: UseFormReturn<FormValues>;\n  allPageComponents: PageComponent[];\n  monitors: Monitor[];\n  validateLimit: () => boolean;\n}\n\nfunction ComponentGroupRow({\n  group,\n  groupIndex,\n  onDeleteGroup,\n  form,\n  allPageComponents,\n  monitors,\n  validateLimit,\n}: ComponentGroupRowProps) {\n  const watchGroup = form.watch(`groups.${groupIndex}`);\n  const watchComponents = form.watch(\"components\");\n  const watchGroups = form.watch(\"groups\");\n  const [data, setData] = useState<PageComponent[]>(group.components);\n\n  // Calculate taken monitor IDs (in main list or other groups)\n  const takenMonitorIds = new Set([\n    ...watchComponents.filter((c) => c.monitorId).map((c) => c.monitorId),\n    ...watchGroups\n      .filter((g) => g.id !== group.id)\n      .flatMap((g) =>\n        g.components.filter((c) => c.monitorId).map((c) => c.monitorId),\n      ),\n  ]);\n\n  // FIXME: order is not being updated in the form\n  const onValueChange = useCallback(\n    (newComponents: PageComponent[]) => {\n      setData(newComponents);\n      // Update the form with the new component order\n      const existingComponents =\n        form.getValues(`groups.${groupIndex}.components`) ?? [];\n      form.setValue(\n        `groups.${groupIndex}.components`,\n        newComponents.map((c, index) => {\n          const existingComponent = existingComponents.find(\n            (ec) => ec.id === c.id,\n          );\n          return {\n            id: c.id,\n            monitorId: c.monitorId,\n            order: index,\n            name: existingComponent?.name ?? c.name,\n            description: existingComponent?.description ?? \"\",\n            type: c.type,\n          };\n        }),\n      );\n    },\n    [form, groupIndex],\n  );\n\n  useEffect(() => {\n    setData(\n      getSortedComponents(allPageComponents, watchGroup.components, monitors),\n    );\n  }, [watchGroup.components, allPageComponents, monitors]);\n\n  const getItemValue = useCallback((item: PageComponent) => item.id, []);\n\n  const handleDeleteComponent = useCallback(\n    (componentId: number) => {\n      const existingComponents =\n        form.getValues(`groups.${groupIndex}.components`) ?? [];\n      form.setValue(\n        `groups.${groupIndex}.components`,\n        existingComponents.filter((c) => c.id !== componentId),\n      );\n      setData((prev) => prev.filter((item) => item.id !== componentId));\n    },\n    [form, groupIndex],\n  );\n\n  const renderOverlay = useCallback(\n    ({ value }: { value: UniqueIdentifier }) => {\n      const component = data.find((item) => item.id === value);\n      if (!component) return null;\n\n      return (\n        <ComponentRow\n          component={component}\n          form={form}\n          onDelete={handleDeleteComponent}\n        />\n      );\n    },\n    [data, form, handleDeleteComponent],\n  );\n\n  return (\n    <SortableItem value={group.id} className=\"rounded-md border bg-muted\">\n      <div className=\"grid grid-cols-4 gap-2 px-2 pt-2\">\n        <div className=\"flex flex-row items-center gap-1 self-center\">\n          <SortableItemHandle>\n            <GripVertical\n              size={16}\n              aria-hidden=\"true\"\n              className=\"text-muted-foreground\"\n            />\n          </SortableItemHandle>\n          <FormField\n            key={`${group.id}-name-${groupIndex}`}\n            control={form.control}\n            name={`groups.${groupIndex}.name` as const}\n            render={({ field }) => (\n              <FormItem className=\"w-full\">\n                <FormLabel className=\"sr-only\">Group name</FormLabel>\n                <FormControl>\n                  <Input\n                    placeholder=\"Group Name\"\n                    className=\"w-full bg-background\"\n                    {...field}\n                  />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </div>\n        <FormField\n          key={`${group.id}-components-${groupIndex}`}\n          control={form.control}\n          name={`groups.${groupIndex}.components` as const}\n          render={({ field }) => (\n            <FormItem className=\"flex flex-col\">\n              <FormLabel className=\"sr-only\">Components</FormLabel>\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <Button variant=\"outline\" className=\"w-full\">\n                    <Plus />\n                    Add Component\n                  </Button>\n                </DropdownMenuTrigger>\n                <DropdownMenuContent align=\"start\">\n                  <DropdownMenuGroup>\n                    <DropdownMenuItem\n                      onClick={() => {\n                        if (!validateLimit()) return;\n                        const current = field.value ?? [];\n                        form.setValue(`groups.${groupIndex}.components`, [\n                          ...current,\n                          {\n                            id: Date.now(),\n                            monitorId: null,\n                            order: current.length,\n                            name: \"\",\n                            description: \"\",\n                            type: \"static\" as const,\n                          },\n                        ]);\n                      }}\n                    >\n                      <Link2Off className=\"text-muted-foreground\" />\n                      Add Static Component\n                    </DropdownMenuItem>\n                    <DropdownMenuSub>\n                      <DropdownMenuSubTrigger className=\"gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0\">\n                        <Link2 className=\"text-muted-foreground\" />\n                        Add Monitor Component\n                      </DropdownMenuSubTrigger>\n                      <DropdownMenuSubContent className=\"p-0\">\n                        <Command>\n                          <CommandInput\n                            placeholder=\"Search monitors...\"\n                            className=\"h-9\"\n                          />\n                          <CommandList>\n                            <CommandEmpty>No monitors found.</CommandEmpty>\n                            <CommandGroup>\n                              {monitors.map((monitor) => {\n                                const current = field.value ?? [];\n                                const isTaken = takenMonitorIds.has(monitor.id);\n                                const isSelected = current.some(\n                                  (c) => c.monitorId === monitor.id,\n                                );\n                                return (\n                                  <CommandItem\n                                    value={monitor.name}\n                                    key={monitor.id}\n                                    disabled={isTaken || isSelected}\n                                    onSelect={() => {\n                                      if (isSelected) {\n                                        form.setValue(\n                                          `groups.${groupIndex}.components`,\n                                          current.filter(\n                                            (c) => c.monitorId !== monitor.id,\n                                          ),\n                                        );\n                                      } else {\n                                        if (!validateLimit()) return;\n                                        form.setValue(\n                                          `groups.${groupIndex}.components`,\n                                          [\n                                            ...current,\n                                            {\n                                              id: Date.now(),\n                                              monitorId: monitor.id,\n                                              order: current.length,\n                                              name: monitor.name,\n                                              description: monitor.description,\n                                              type: \"monitor\" as const,\n                                            },\n                                          ],\n                                        );\n                                      }\n                                    }}\n                                  >\n                                    {monitor.name}\n                                    <Check\n                                      className={cn(\n                                        \"ml-auto\",\n                                        isSelected\n                                          ? \"opacity-100\"\n                                          : \"opacity-0\",\n                                      )}\n                                    />\n                                  </CommandItem>\n                                );\n                              })}\n                            </CommandGroup>\n                          </CommandList>\n                        </Command>\n                      </DropdownMenuSubContent>\n                    </DropdownMenuSub>\n                    <DropdownMenuItem disabled>\n                      <Plug className=\"text-muted-foreground\" />\n                      Add Third-Party Component\n                    </DropdownMenuItem>\n                  </DropdownMenuGroup>\n                </DropdownMenuContent>\n              </DropdownMenu>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n        <div />\n        <div className=\"flex justify-end\">\n          <AlertDialog>\n            <AlertDialogTrigger asChild>\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"text-destructive hover:bg-destructive/10 hover:text-destructive dark:hover:bg-destructive/20 [&_svg]:size-4 [&_svg]:text-destructive\"\n                // NOTE: delete directly if no components are in the group\n                {...(data.length === 0\n                  ? { onClick: () => onDeleteGroup(group.id) }\n                  : {})}\n              >\n                <Trash2 />\n              </Button>\n            </AlertDialogTrigger>\n            <AlertDialogContent>\n              <AlertDialogHeader>\n                <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n                <AlertDialogDescription>\n                  Once saved, this will delete all components in the group and\n                  unlink them from attached status reports and maintenances.\n                </AlertDialogDescription>\n                <ComponentAttachments\n                  statusReports={Array.from(\n                    new Map(\n                      group.components\n                        .flatMap((c) => c.statusReports ?? [])\n                        .map((sr) => [sr.id, sr]),\n                    ).values(),\n                  )}\n                  maintenances={Array.from(\n                    new Map(\n                      group.components\n                        .flatMap((c) => c.maintenances ?? [])\n                        .map((m) => [m.id, m]),\n                    ).values(),\n                  )}\n                />\n              </AlertDialogHeader>\n              <AlertDialogFooter>\n                <AlertDialogCancel>Cancel</AlertDialogCancel>\n                <AlertDialogAction\n                  className=\"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\"\n                  onClick={() => onDeleteGroup(group.id)}\n                >\n                  Remove\n                </AlertDialogAction>\n              </AlertDialogFooter>\n            </AlertDialogContent>\n          </AlertDialog>\n        </div>\n      </div>\n      <div className=\"mt-2 border-t px-2 pt-2 pb-2\">\n        <Sortable\n          value={data}\n          onValueChange={onValueChange}\n          getItemValue={getItemValue}\n          orientation=\"vertical\"\n        >\n          {data.length ? (\n            <SortableContent className=\"grid gap-2\">\n              {data.map((item, _index) => {\n                const groupComponents =\n                  form.getValues(`groups.${groupIndex}.components`) ?? [];\n                const componentIndex = groupComponents.findIndex(\n                  (c) => c.id === item.id,\n                );\n                return (\n                  <ComponentRow\n                    key={`${item.id}-component`}\n                    component={item}\n                    form={form}\n                    onDelete={handleDeleteComponent}\n                    fieldNamePrefix={\n                      componentIndex >= 0\n                        ? `groups.${groupIndex}.components.${componentIndex}`\n                        : undefined\n                    }\n                  />\n                );\n              })}\n              <SortableOverlay>{renderOverlay}</SortableOverlay>\n            </SortableContent>\n          ) : (\n            <EmptyStateContainer>\n              <EmptyStateTitle>No components selected</EmptyStateTitle>\n            </EmptyStateContainer>\n          )}\n        </Sortable>\n      </div>\n    </SortableItem>\n  );\n}\n\nfunction ComponentAttachments({\n  statusReports,\n  maintenances,\n}: {\n  statusReports: Array<{ id: number; title: string }>;\n  maintenances: Array<{ id: number; title: string }>;\n}) {\n  if (statusReports.length === 0 && maintenances.length === 0) {\n    return null;\n  }\n\n  const allItems = [\n    ...statusReports.map((report) => ({\n      id: `report-${report.id}`,\n      title: report.title,\n      type: \"report\" as const,\n    })),\n    ...maintenances.map((maintenance) => ({\n      id: `maintenance-${maintenance.id}`,\n      title: maintenance.title,\n      type: \"maintenance\" as const,\n    })),\n  ];\n\n  const displayLimit = 3;\n  const displayedItems = allItems.slice(0, displayLimit);\n  const remainingCount = allItems.length - displayLimit;\n\n  return (\n    <ul className=\"list-inside list-disc space-y-1 text-foreground text-sm\">\n      {displayedItems.map((item) => (\n        <li key={item.id}>\n          {item.title}{\" \"}\n          <span className=\"text-muted-foreground\">({item.type})</span>\n        </li>\n      ))}\n      {remainingCount > 0 && (\n        <li className=\"text-muted-foreground\">+ {remainingCount} more</li>\n      )}\n    </ul>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/components/form-import.tsx",
    "content": "\"use client\";\n\nimport { Note } from \"@/components/common/note\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { StatuspageIcon } from \"@openstatus/icons\";\nimport type { ImportSummary } from \"@openstatus/importers/types\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  RadioGroup,\n  RadioGroupItem,\n} from \"@openstatus/ui/components/ui/radio-group\";\nimport { Switch } from \"@openstatus/ui/components/ui/switch\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { AlertTriangle } from \"lucide-react\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  provider: z.enum([\"statuspage\"]),\n  apiKey: z.string().min(1, \"API key is required\"),\n  statuspagePageId: z.string().optional(),\n  includeStatusReports: z.boolean(),\n  includeSubscribers: z.boolean(),\n  includeComponents: z.boolean(),\n});\n\nexport type ImportFormValues = z.input<typeof schema>;\n\nfunction getPhaseCount(preview: ImportSummary, phase: string): number {\n  return preview.phases.find((p) => p.phase === phase)?.resources.length ?? 0;\n}\n\nconst PHASE_LABELS: Record<string, string> = {\n  componentGroups: \"Component Groups\",\n  components: \"Components\",\n  incidents: \"Status Reports\",\n  maintenances: \"Maintenances\",\n  subscribers: \"Subscribers\",\n};\n\nexport function FormImport({\n  pageId,\n  onSubmit,\n}: {\n  pageId: number;\n  onSubmit: (values: ImportFormValues) => Promise<ImportSummary>;\n}) {\n  const form = useForm<ImportFormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      provider: undefined,\n      apiKey: \"\",\n      statuspagePageId: \"\",\n      includeStatusReports: true,\n      includeSubscribers: false,\n      includeComponents: true,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const trpc = useTRPC();\n  const watchProvider = form.watch(\"provider\");\n  const watchApiKey = form.watch(\"apiKey\");\n  const watchStatuspagePageId = form.watch(\"statuspagePageId\");\n\n  const previewMutation = useMutation(\n    trpc.import.preview.mutationOptions({\n      onError: (error) => {\n        if (isTRPCClientError(error)) {\n          toast.error(error.message);\n        } else {\n          toast.error(\"Failed to preview import\");\n        }\n      },\n    }),\n  );\n\n  async function runPreview() {\n    const apiKey = form.getValues(\"apiKey\");\n    if (!apiKey) {\n      form.setError(\"apiKey\", { message: \"API key is required\" });\n      return;\n    }\n    previewMutation.mutate({\n      provider: \"statuspage\",\n      apiKey: watchApiKey,\n      statuspagePageId: watchStatuspagePageId || undefined,\n      pageId,\n    });\n  }\n\n  function submitAction(values: ImportFormValues) {\n    if (isPending || !previewMutation.data) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Importing...\",\n          success: (result) => {\n            if (result.status === \"partial\")\n              return \"Import completed with warnings\";\n            return \"Import completed\";\n          },\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Import failed\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Import</FormCardTitle>\n            <FormCardDescription>\n              Import components, incidents, and subscribers from an external\n              status page provider.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardSeparator />\n          <FormCardContent>\n            <FormField\n              control={form.control}\n              name=\"provider\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Provider</FormLabel>\n                  <FormControl>\n                    <RadioGroup\n                      onValueChange={field.onChange}\n                      defaultValue={field.value}\n                      className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\"\n                    >\n                      <FormItem className=\"relative flex cursor-pointer flex-row items-center gap-3 rounded-md border border-input px-2 py-3 text-center shadow-xs outline-none transition-[color,box-shadow] has-data-[state=checked]:border-primary/50 has-focus-visible:border-ring has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50\">\n                        <FormControl>\n                          <RadioGroupItem\n                            value=\"statuspage\"\n                            className=\"sr-only\"\n                          />\n                        </FormControl>\n                        <StatuspageIcon\n                          className=\"size-4 shrink-0 text-foreground\"\n                          aria-hidden=\"true\"\n                        />\n                        <FormLabel className=\"cursor-pointer font-medium text-foreground text-xs leading-none after:absolute after:inset-0\">\n                          Atlassian Statuspage\n                        </FormLabel>\n                      </FormItem>\n                      <div className=\"col-span-1 self-end text-muted-foreground text-xs sm:place-self-end\">\n                        Missing a provider?{\" \"}\n                        <a href=\"mailto:ping@openstatus.dev\">Contact us</a>\n                      </div>\n                    </RadioGroup>\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          {watchProvider ? (\n            <>\n              <FormCardSeparator />\n              <FormCardContent className=\"grid gap-4\">\n                <FormField\n                  control={form.control}\n                  name=\"apiKey\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormLabel>API Key</FormLabel>\n                      <FormControl>\n                        <Input\n                          type=\"password\"\n                          placeholder=\"OAuth API key\"\n                          {...field}\n                        />\n                      </FormControl>\n                      <FormMessage />\n                      <FormDescription>\n                        Your Statuspage API key. Found in your Statuspage\n                        account under Manage Account &gt; API.\n                      </FormDescription>\n                    </FormItem>\n                  )}\n                />\n                <FormField\n                  control={form.control}\n                  name=\"statuspagePageId\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormLabel>Page ID (optional)</FormLabel>\n                      <FormControl>\n                        <Input placeholder=\"e.g. abc123def456\" {...field} />\n                      </FormControl>\n                      <FormDescription>\n                        Import a specific page. Leave empty to import across\n                        pages.\n                      </FormDescription>\n                    </FormItem>\n                  )}\n                />\n                <Button\n                  type=\"button\"\n                  variant=\"secondary\"\n                  onClick={runPreview}\n                  disabled={previewMutation.isPending}\n                >\n                  {previewMutation.isPending\n                    ? \"Loading preview...\"\n                    : \"Preview Import\"}\n                </Button>\n              </FormCardContent>\n            </>\n          ) : null}\n          {previewMutation.data ? (\n            <>\n              <FormCardSeparator />\n              <FormCardContent className=\"grid gap-4\">\n                <div>\n                  <FormLabel>Preview</FormLabel>\n                  <div className=\"mt-2 flex flex-wrap gap-2\">\n                    {Object.entries(PHASE_LABELS).map(([key, label]) => {\n                      const count = getPhaseCount(previewMutation.data, key);\n                      if (count === 0) return null;\n                      return (\n                        <Badge key={key} variant=\"secondary\">\n                          {label}: {count}\n                        </Badge>\n                      );\n                    })}\n                  </div>\n                </div>\n                {previewMutation.data.errors.length > 0 ? (\n                  <Note color=\"error\" size=\"sm\">\n                    <AlertTriangle />\n                    <p className=\"text-sm\">\n                      {previewMutation.data.errors.join(\" \")}\n                    </p>\n                  </Note>\n                ) : null}\n                <FormField\n                  control={form.control}\n                  name=\"includeStatusReports\"\n                  render={({ field }) => (\n                    <FormItem className=\"flex flex-row items-center justify-between\">\n                      <div className=\"space-y-0.5\">\n                        <FormLabel>Status Reports & Maintenances</FormLabel>\n                        <FormDescription>\n                          Import incidents as status reports and scheduled\n                          maintenances.\n                        </FormDescription>\n                      </div>\n                      <FormControl>\n                        <Switch\n                          checked={field.value}\n                          onCheckedChange={field.onChange}\n                        />\n                      </FormControl>\n                    </FormItem>\n                  )}\n                />\n                <FormField\n                  control={form.control}\n                  name=\"includeComponents\"\n                  render={({ field }) => (\n                    <FormItem className=\"flex flex-row items-center justify-between\">\n                      <div className=\"space-y-0.5\">\n                        <FormLabel>Components</FormLabel>\n                        <FormDescription>\n                          Import components and groups.\n                        </FormDescription>\n                      </div>\n                      <FormControl>\n                        <Switch\n                          checked={field.value}\n                          onCheckedChange={field.onChange}\n                        />\n                      </FormControl>\n                    </FormItem>\n                  )}\n                />\n                <FormField\n                  control={form.control}\n                  name=\"includeSubscribers\"\n                  render={({ field }) => (\n                    <FormItem className=\"flex flex-row items-center justify-between\">\n                      <div className=\"space-y-0.5\">\n                        <FormLabel>Subscribers</FormLabel>\n                        <FormDescription>\n                          Import email subscribers.\n                        </FormDescription>\n                      </div>\n                      <FormControl>\n                        <Switch\n                          checked={field.value}\n                          onCheckedChange={field.onChange}\n                        />\n                      </FormControl>\n                    </FormItem>\n                  )}\n                />\n              </FormCardContent>\n            </>\n          ) : null}\n          <FormCardFooter>\n            <Button\n              type=\"submit\"\n              disabled={\n                !previewMutation.data ||\n                isPending ||\n                previewMutation.data.errors.length > 0\n              }\n            >\n              {isPending ? \"Importing...\" : \"Import\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/components/telegram-connection-flow.tsx",
    "content": "\"use client\";\n\nimport { useTelegramConnection } from \"@/hooks/use-telegram-connection\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport type { UseFormReturn } from \"react-hook-form\";\nimport type { FormValues } from \"../notifications/form-telegram\";\nimport { TelegramManualInput } from \"./telegram-manual-input\";\nimport { TelegramQRConnection } from \"./telegram-qr-connection\";\n\ninterface TelegramConnectionFlowProps {\n  form: UseFormReturn<FormValues>;\n  mode: \"qr\" | \"manual\" | null;\n  onModeChange: (mode: \"qr\" | \"manual\" | null) => void;\n}\n\nexport function TelegramConnectionFlow({\n  form,\n  mode,\n  onModeChange,\n}: TelegramConnectionFlowProps) {\n  const {\n    tokenData,\n    isTokenLoading,\n    flowStep,\n    privateChatId,\n    userName,\n    groupTitle,\n    isPolling,\n    resetConnection,\n    confirmPrivateChat,\n  } = useTelegramConnection({ form, mode });\n\n  return (\n    <Tabs\n      value={mode ?? \"qr\"}\n      onValueChange={(v) => onModeChange(v as \"qr\" | \"manual\")}\n    >\n      <TabsList className=\"w-full\">\n        <TabsTrigger value=\"qr\" className=\"flex-1\">\n          Connect with QR\n        </TabsTrigger>\n        <TabsTrigger value=\"manual\" className=\"flex-1\">\n          Enter ChatID manually\n        </TabsTrigger>\n      </TabsList>\n      <TabsContent value=\"qr\">\n        <TelegramQRConnection\n          form={form}\n          token={tokenData?.token}\n          isLoading={isTokenLoading}\n          isPolling={isPolling}\n          flowStep={flowStep}\n          privateChatId={privateChatId}\n          userName={userName}\n          groupTitle={groupTitle}\n          onReset={resetConnection}\n          onConfirmPrivateChat={confirmPrivateChat}\n        />\n      </TabsContent>\n      <TabsContent value=\"manual\">\n        <TelegramManualInput form={form} />\n      </TabsContent>\n    </Tabs>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/components/telegram-form-actions.tsx",
    "content": "\"use client\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport type { UseFormReturn } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport type { FormValues } from \"../notifications/form-telegram\";\n\ninterface TelegramFormActionsProps {\n  form: UseFormReturn<FormValues>;\n  isPending: boolean;\n}\n\nexport function TelegramFormActions({\n  form,\n  isPending,\n}: TelegramFormActionsProps) {\n  const [_, startTransition] = useTransition();\n  const trpc = useTRPC();\n  const sendTestMutation = useMutation(\n    trpc.notification.sendTest.mutationOptions(),\n  );\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data\");\n        const promise = sendTestMutation.mutateAsync({\n          provider,\n          data: {\n            telegram: { chatId: data.chatId },\n          },\n        });\n        toast.promise(promise, {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            if (error instanceof Error) {\n              return error.message;\n            }\n            return \"Failed to send test\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <div>\n      <Button\n        variant=\"outline\"\n        size=\"sm\"\n        type=\"button\"\n        onClick={testAction}\n        disabled={isPending}\n      >\n        Send Test\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/components/telegram-manual-input.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport type { UseFormReturn } from \"react-hook-form\";\nimport type { FormValues } from \"../notifications/form-telegram\";\n\ninterface TelegramManualInputProps {\n  form: UseFormReturn<FormValues>;\n  successMsg?: string;\n  showDescription?: boolean;\n}\n\nexport function TelegramManualInput({\n  form,\n  successMsg,\n  showDescription = true,\n}: TelegramManualInputProps) {\n  return (\n    <FormField\n      control={form.control}\n      name=\"data.chatId\"\n      render={({ field }) => (\n        <FormItem>\n          <FormLabel>Telegram Chat ID</FormLabel>\n          <FormControl>\n            <Input placeholder=\"1234567890\" {...field} />\n          </FormControl>\n          <FormMessage />\n          {successMsg && (\n            <div className=\"font-medium text-green-600 text-sm\">\n              {successMsg}\n            </div>\n          )}\n          {showDescription && (\n            <FormDescription>\n              Enter the Telegram chat ID to send notifications to.{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/reference/notification/#telegram\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                Learn more\n              </Link>\n            </FormDescription>\n          )}\n        </FormItem>\n      )}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/components/telegram-qr-connection.tsx",
    "content": "import { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport type { UseFormReturn } from \"react-hook-form\";\nimport type { FormValues } from \"../notifications/form-telegram\";\nimport { TelegramManualInput } from \"./telegram-manual-input\";\nimport TelegramQRCode from \"./telegram-qrcode\";\n\ninterface TelegramQRConnectionProps {\n  form: UseFormReturn<FormValues>;\n  token?: string;\n  isLoading: boolean;\n  isPolling?: boolean;\n  flowStep: \"private\" | \"group\";\n  privateChatId: string | null;\n  userName?: string | null;\n  groupTitle?: string | null;\n  onReset?: () => void;\n  onConfirmPrivateChat?: () => void;\n}\n\nexport function TelegramQRConnection({\n  form,\n  token,\n  isLoading,\n  isPolling,\n  flowStep,\n  privateChatId,\n  userName,\n  groupTitle,\n  onReset,\n  onConfirmPrivateChat,\n}: TelegramQRConnectionProps) {\n  const chatId = form.watch(\"data.chatId\");\n  const isGroup = !!groupTitle;\n\n  // When we have a chat ID (group or private), show the manual input with connection info\n  if (chatId) {\n    const successMsg = isGroup\n      ? `Connected to ${groupTitle}`\n      : `Connected to ${userName || \"Unknown\"}'s private chat`;\n    return (\n      <div className=\"flex flex-col gap-2\">\n        <TelegramManualInput\n          form={form}\n          successMsg={successMsg}\n          showDescription={false}\n        />\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={onReset}\n          className=\"w-full\"\n        >\n          {isGroup ? \"Reset Group ID\" : \"Add Group\"}\n        </Button>\n      </div>\n    );\n  }\n\n  // When we have a private chat ID, show read-only info with second QR code\n  if (privateChatId && flowStep === \"group\") {\n    return (\n      <div className=\"flex flex-col gap-2\">\n        {/* Show read-only private chat info */}\n        <div className=\"space-y-2\">\n          <Label>Private Chat ID</Label>\n          <Input value={privateChatId} readOnly className=\"bg-muted\" />\n          {userName && (\n            <div className=\"font-medium text-green-600 text-sm\">\n              {`Connected to: ${userName}`}\n            </div>\n          )}\n        </div>\n\n        {/* Show second QR code for group connection */}\n        <div className=\"text-muted-foreground text-sm\">\n          Step 2 of 2: Add bot to your group\n        </div>\n        <TelegramQRCode\n          chatType=\"group\"\n          token={token}\n          isLoading={isLoading}\n          isPolling={isPolling}\n        />\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={onConfirmPrivateChat}\n          className=\"w-full\"\n        >\n          Use private chat only\n        </Button>\n      </div>\n    );\n  }\n\n  // Initial state: show first QR code for private chat connection\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <div className=\"text-muted-foreground text-sm\">\n        Step 1 of 2: Connect your Telegram account\n      </div>\n      <TelegramQRCode\n        chatType=\"private\"\n        token={token}\n        isLoading={isLoading}\n        isPolling={isPolling}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/components/telegram-qrcode.tsx",
    "content": "import { QRCode } from \"@openstatus/ui/components/ui/qr-code\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { Loader2 } from \"lucide-react\";\n\nexport default function TelegramQRCode({\n  chatType,\n  token,\n  isLoading,\n  isPolling,\n}: {\n  chatType: \"group\" | \"private\";\n  token?: string | undefined;\n  isLoading: boolean;\n  isPolling?: boolean;\n}) {\n  const telegramBotUserName = process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME;\n\n  //   Grpoup : t.me/<bot_username>?startgroup=<parameter>&admin=<permissions>\n  // Private Chat: t.me/<bot_username>?start=<parameter>\n\n  const qrURL =\n    chatType === \"group\"\n      ? `https://t.me/${telegramBotUserName}?startgroup=${token}&admin=post_messages`\n      : `https://t.me/${telegramBotUserName}?start=${token}`;\n\n  return (\n    <div className=\"flex flex-col items-center justify-center gap-2\">\n      {isLoading ? (\n        <Skeleton className=\"h-[200px] w-[200px]\" />\n      ) : token ? (\n        <QRCode data={qrURL} className=\"overflow-hidden rounded-md\" />\n      ) : null}\n      <div className=\"flex items-center gap-2 text-muted-foreground text-sm\">\n        {isLoading ? (\n          \"Generating QR Code...\"\n        ) : isPolling ? (\n          <>\n            <Loader2 className=\"h-3 w-3 animate-spin\" />\n            {chatType === \"private\"\n              ? \"Retrieving your account...\"\n              : \"Waiting for group connection...\"}\n          </>\n        ) : chatType === \"private\" ? (\n          \"Scan the QR code to connect your account\"\n        ) : (\n          \"Scan to add the bot to your group\"\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/components/update.tsx",
    "content": "\"use client\";\n\nimport { FormComponents } from \"@/components/forms/components/form-components\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\nimport { useState } from \"react\";\nimport { FormCardGroup } from \"../form-card\";\nimport { FormConfiguration } from \"../status-page/form-configuration\";\nimport { FormImport, type ImportFormValues } from \"./form-import\";\n\nexport function FormComponentsUpdate() {\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const [formKey, setFormKey] = useState(0);\n  const { data: statusPage, refetch } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  const { data: pageComponents, refetch: refetchComponents } = useQuery(\n    trpc.pageComponent.list.queryOptions({ pageId: Number.parseInt(id) }),\n  );\n  const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n\n  const updateComponentsMutation = useMutation(\n    trpc.pageComponent.updateOrder.mutationOptions({\n      onSuccess: () => {\n        refetch();\n        refetchComponents();\n      },\n    }),\n  );\n\n  const updatePageConfigurationMutation = useMutation(\n    trpc.page.updatePageConfiguration.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  const importMutation = useMutation(\n    trpc.import.run.mutationOptions({\n      onSuccess: async () => {\n        await Promise.all([refetch(), refetchComponents()]);\n        setFormKey((k) => k + 1);\n      },\n    }),\n  );\n\n  if (!statusPage || !pageComponents || !monitors || !workspace) return null;\n\n  // Separate standalone components from grouped components\n  const standaloneComponents = pageComponents.filter((c) => !c.groupId);\n  const groupedComponents = pageComponents.filter((c) => c.groupId);\n\n  // Build groups from pageComponentGroups\n  const groups = statusPage.pageComponentGroups.map((group) => {\n    const componentsInGroup = groupedComponents.filter(\n      (c) => c.groupId === group.id,\n    );\n    // Find the order of the group (use the first component's order)\n    const firstComponent = componentsInGroup[0];\n    return {\n      id: group.id,\n      order: firstComponent?.order ?? 0,\n      name: group.name,\n      components: componentsInGroup.map((c) => ({\n        id: c.id,\n        monitorId: c.monitorId,\n        order: c.groupOrder ?? 0,\n        name: c.name,\n        description: c.description ?? \"\",\n        type: c.type,\n      })),\n    };\n  });\n\n  // Build default values for the form\n  const defaultValues = {\n    components: standaloneComponents.map((c) => ({\n      id: c.id,\n      monitorId: c.monitorId,\n      order: c.order ?? 0,\n      name: c.name,\n      description: c.description ?? \"\",\n      type: c.type,\n    })),\n    groups,\n  };\n\n  const configLink = `https://${\n    statusPage.slug\n  }.stpg.dev?configuration-token=${statusPage.createdAt?.getTime().toString()}`;\n\n  return (\n    <FormCardGroup>\n      {/* key forces remount after import so useForm picks up new defaultValues */}\n      <FormComponents\n        key={formKey}\n        pageComponents={standaloneComponents}\n        monitors={monitors}\n        allPageComponents={pageComponents}\n        defaultValues={defaultValues}\n        workspace={workspace}\n        onSubmit={async (values) => {\n          await updateComponentsMutation.mutateAsync({\n            pageId: Number.parseInt(id),\n            components: values.components,\n            groups: values.groups.map(({ id: _groupId, ...rest }) => rest),\n          });\n        }}\n      />\n      <FormConfiguration\n        defaultValues={{\n          configuration: statusPage.configuration ?? {},\n        }}\n        onSubmit={async (values) => {\n          await updatePageConfigurationMutation.mutateAsync({\n            id: Number.parseInt(id),\n            configuration: {\n              uptime:\n                typeof values.configuration.uptime === \"boolean\"\n                  ? values.configuration.uptime\n                  : values.configuration.uptime === \"true\",\n              value: values.configuration.value ?? \"duration\",\n              type: values.configuration.type ?? \"absolute\",\n              theme: values.configuration.theme ?? undefined,\n            },\n          });\n        }}\n        configLink={configLink}\n      />\n      <FormImport\n        pageId={statusPage.id}\n        onSubmit={async (values: ImportFormValues) => {\n          return await importMutation.mutateAsync({\n            provider: values.provider,\n            apiKey: values.apiKey,\n            pageId: statusPage.id,\n            statuspagePageId: values.statuspagePageId ?? undefined,\n            options: {\n              includeStatusReports: values.includeStatusReports,\n              includeSubscribers: values.includeSubscribers,\n              includeComponents: values.includeComponents,\n            },\n          });\n        }}\n      />\n    </FormCardGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/form-alert-dialog.tsx",
    "content": "\"use client\";\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { Check, Copy } from \"lucide-react\";\nimport { useState, useTransition } from \"react\";\nimport { toast } from \"sonner\";\n\ninterface FormAlertDialogProps {\n  confirmationValue: string;\n  submitAction: () => Promise<void>;\n  children?: React.ReactNode;\n}\n\nexport function FormAlertDialog({\n  confirmationValue,\n  submitAction,\n  children,\n}: FormAlertDialogProps) {\n  const [value, setValue] = useState(\"\");\n  const [isPending, startTransition] = useTransition();\n  const { copy, isCopied } = useCopyToClipboard();\n  const [open, setOpen] = useState(false);\n\n  const handleDelete = async () => {\n    try {\n      startTransition(async () => {\n        const promise = submitAction();\n        toast.promise(promise, {\n          loading: \"Deleting...\",\n          success: \"Deleted\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to delete\";\n          },\n        });\n        await promise;\n        setOpen(false);\n      });\n    } catch (error) {\n      console.error(\"Failed to revoke:\", error);\n    }\n  };\n\n  return (\n    <AlertDialog open={open} onOpenChange={setOpen}>\n      <AlertDialogTrigger asChild>\n        {children ?? (\n          <Button variant=\"destructive\" size=\"sm\">\n            Delete\n          </Button>\n        )}\n      </AlertDialogTrigger>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>\n            Are you sure about delete `{confirmationValue}`?\n          </AlertDialogTitle>\n          <AlertDialogDescription>\n            This action cannot be undone. This will permanently delete the item.\n          </AlertDialogDescription>\n        </AlertDialogHeader>\n        <form id=\"form-alert-dialog\" className=\"space-y-1.5\">\n          <p className=\"text-muted-foreground text-sm\">\n            Type{\" \"}\n            <Button\n              variant=\"secondary\"\n              size=\"sm\"\n              type=\"button\"\n              className=\"font-normal [&_svg]:size-3\"\n              onClick={() => copy(confirmationValue, { withToast: false })}\n            >\n              {confirmationValue}\n              {isCopied ? <Check /> : <Copy />}\n            </Button>{\" \"}\n            to confirm\n          </p>\n          <Input value={value} onChange={(e) => setValue(e.target.value)} />\n        </form>\n        <AlertDialogFooter>\n          <AlertDialogCancel>Cancel</AlertDialogCancel>\n          <AlertDialogAction\n            className=\"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\"\n            disabled={value !== confirmationValue || isPending}\n            form=\"form-alert-dialog\"\n            type=\"submit\"\n            onClick={(e) => {\n              e.preventDefault();\n              handleDelete();\n            }}\n          >\n            {isPending ? \"Deleting...\" : \"Delete\"}\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/form-card.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@openstatus/ui/components/ui/card\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nimport { type VariantProps, cva } from \"class-variance-authority\";\n\n// py-0\nconst formCardVariants = cva(\n  \"group relative w-full overflow-hidden py-0 shadow-none gap-4\",\n  {\n    variants: {\n      variant: {\n        default: \"\",\n        destructive: \"border-destructive\",\n        info: \"border-info\",\n      },\n      defaultVariants: {\n        variant: \"default\",\n      },\n    },\n  },\n);\n\n// NOTE: Add a formcardprovider to share the variant prop\n\nexport function FormCard({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof formCardVariants>) {\n  return (\n    <Card className={cn(formCardVariants({ variant }), className)} {...props}>\n      {children}\n    </Card>\n  );\n}\n\nexport function FormCardHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <CardHeader\n      className={cn(\n        \"px-4 pt-4 group-has-data-[slot=card-upgrade]:pointer-events-none group-has-data-[slot=card-upgrade]:opacity-50 [.border-b]:pb-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </CardHeader>\n  );\n}\n\nexport function FormCardTitle({ children }: { children: React.ReactNode }) {\n  return <CardTitle>{children}</CardTitle>;\n}\n\nexport function FormCardDescription({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return <CardDescription>{children}</CardDescription>;\n}\n\nexport function FormCardContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <CardContent\n      className={cn(\n        \"px-4 group-has-data-[slot=card-upgrade]:pointer-events-none group-has-data-[slot=card-upgrade]:opacity-50\",\n        \"has-data-[slot=card-content-upgrade]:pointer-events-none has-data-[slot=card-content-upgrade]:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </CardContent>\n  );\n}\n\nexport function FormCardSeparator({\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return <Separator {...props} />;\n}\n\nconst formCardFooterVariants = cva(\n  \"border-t flex items-center gap-2 pb-4 px-4 [&>:last-child]:ml-auto [.border-t]:pt-4\",\n  {\n    variants: {\n      variant: {\n        default: \"\",\n        destructive: \"border-destructive bg-destructive/5\",\n        info: \"border-info bg-info/5\",\n      },\n      defaultVariants: {\n        variant: \"default\",\n      },\n    },\n  },\n);\n\nexport function FormCardFooter({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof formCardFooterVariants>) {\n  return (\n    <CardFooter\n      className={cn(formCardFooterVariants({ variant }), className)}\n      {...props}\n    >\n      {children}\n    </CardFooter>\n  );\n}\n\nexport function FormCardFooterInfo({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer-info\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function FormCardGroup({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-group\"\n      className={cn(\"flex flex-col gap-4\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function FormCardUpgrade({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-upgrade\"\n      className={cn(\"hidden\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n// NOTE; this is for a very specific case where we don't want to disable the whole content\n// and instead disable specpfic card content (e.g. for add-ons)\nexport function FormCardContentUpgrade({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content-upgrade\"\n      className={cn(\"hidden\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function FormCardEmpty({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-empty\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 z-10 bg-background opacity-70 blur\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/form-sheet.tsx",
    "content": "\"use client\";\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetFooter,\n  SheetHeader,\n  SheetTitle,\n  SheetTrigger,\n} from \"@openstatus/ui/components/ui/sheet\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport React, {\n  createContext,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nexport function FormSheetContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetContent>) {\n  return (\n    <SheetContent className={cn(\"max-h-screen gap-0\", className)} {...props}>\n      {children}\n    </SheetContent>\n  );\n}\n\nexport function FormSheetHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetHeader>) {\n  return (\n    <SheetHeader\n      className={cn(\"sticky top-0 border-b bg-background\", className)}\n      {...props}\n    >\n      {children}\n    </SheetHeader>\n  );\n}\n\nexport function FormSheetFooter({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetFooter>) {\n  return (\n    <SheetFooter\n      className={cn(\"sticky bottom-0 border-t bg-background\", className)}\n      {...props}\n    >\n      {children}\n    </SheetFooter>\n  );\n}\n\nexport function FormSheetFooterInfo({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"text-muted-foreground/70 text-xs\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function FormSheetTrigger({\n  children,\n  className,\n  disabled,\n  ...props\n}: React.ComponentProps<typeof SheetTrigger>) {\n  return (\n    <SheetTrigger\n      className={cn(\n        \"cursor-pointer data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50\",\n        className,\n      )}\n      data-disabled={disabled}\n      disabled={disabled}\n      {...props}\n    >\n      {children}\n    </SheetTrigger>\n  );\n}\n\nexport function FormSheetAlertDialog({\n  onConfirm,\n  ...props\n}: React.ComponentProps<typeof AlertDialog> & {\n  onConfirm: () => void;\n}) {\n  return (\n    <AlertDialog {...props}>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>Discard changes?</AlertDialogTitle>\n          <AlertDialogDescription>\n            You have unsaved changes. Are you sure you want to discard them?\n          </AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel>Continue editing</AlertDialogCancel>\n          <AlertDialogAction onClick={onConfirm}>\n            Discard changes\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n\nconst FormSheetDirtyContext = createContext<{\n  isDirty: boolean;\n  setIsDirty: (dirty: boolean) => void;\n} | null>(null);\n\nexport function useFormSheetDirty() {\n  const context = useContext(FormSheetDirtyContext);\n  if (!context) {\n    throw new Error(\n      \"useFormSheetDirty must be used within FormSheetWithDirtyProtection\",\n    );\n  }\n  return context;\n}\n\nexport function FormSheetWithDirtyProtection({\n  children,\n  open: controlledOpen,\n  onOpenChange: controlledOnOpenChange,\n}: {\n  children: React.ReactNode;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}) {\n  const [internalOpen, setInternalOpen] = useState(false);\n  const [isDirty, setIsDirty] = useState(false);\n  const [showAlert, setShowAlert] = useState(false);\n  const shouldBypassAlert = useRef(false);\n\n  const open = controlledOpen ?? internalOpen;\n  const setOpen = controlledOnOpenChange ?? setInternalOpen;\n\n  // Reset states when sheet closes\n  useEffect(() => {\n    if (!open) {\n      setIsDirty(false);\n      shouldBypassAlert.current = false;\n    }\n  }, [open]);\n\n  const handleOpenChange = (newOpen: boolean) => {\n    if (!newOpen && isDirty && !shouldBypassAlert.current) {\n      // User is trying to close with unsaved changes\n      setShowAlert(true);\n    } else {\n      setOpen(newOpen);\n    }\n  };\n\n  const handleDiscardChanges = () => {\n    shouldBypassAlert.current = true;\n    setShowAlert(false);\n    setOpen(false);\n  };\n\n  const handleInteractOutside = (e: Event) => {\n    if (isDirty) {\n      e.preventDefault();\n      setShowAlert(true);\n    }\n  };\n\n  const handleEscapeKeyDown = (e: KeyboardEvent) => {\n    if (isDirty) {\n      e.preventDefault();\n      setShowAlert(true);\n    }\n  };\n\n  return (\n    <FormSheetDirtyContext.Provider value={{ isDirty, setIsDirty }}>\n      <Sheet open={open} onOpenChange={handleOpenChange}>\n        {/* Clone children and inject event handlers if it's SheetContent */}\n        {React.Children.map(children, (child) => {\n          if (\n            React.isValidElement(child) &&\n            (child.type === FormSheetContent || child.type === SheetContent)\n          ) {\n            return React.cloneElement(\n              child as React.ReactElement<{\n                onInteractOutside?: (e: Event) => void;\n                onEscapeKeyDown?: (e: KeyboardEvent) => void;\n              }>,\n              {\n                onInteractOutside: handleInteractOutside,\n                onEscapeKeyDown: handleEscapeKeyDown,\n              },\n            );\n          }\n          return child;\n        })}\n      </Sheet>\n      <FormSheetAlertDialog\n        open={showAlert}\n        onOpenChange={setShowAlert}\n        onConfirm={handleDiscardChanges}\n      />\n    </FormSheetDirtyContext.Provider>\n  );\n}\n\nexport {\n  SheetTitle as FormSheetTitle,\n  SheetDescription as FormSheetDescription,\n  Sheet as FormSheet,\n};\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/maintenance/form.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport { ProcessMessage } from \"@/components/content/process-message\";\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Calendar } from \"@openstatus/ui/components/ui/calendar\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { TabsContent } from \"@openstatus/ui/components/ui/tabs\";\nimport { TabsList, TabsTrigger } from \"@openstatus/ui/components/ui/tabs\";\nimport { Tabs } from \"@openstatus/ui/components/ui/tabs\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { addDays, format } from \"date-fns\";\nimport { CalendarIcon, ClockIcon } from \"lucide-react\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z\n  .object({\n    title: z.string().min(1, \"Title is required\"),\n    message: z.string(),\n    startDate: z.date(),\n    endDate: z.date(),\n    pageComponents: z.array(z.number()),\n    notifySubscribers: z.boolean().optional(),\n  })\n  .refine((data) => data.endDate > data.startDate, {\n    error: \"End date cannot be earlier than start date.\",\n    path: [\"endDate\"],\n  });\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function FormMaintenance({\n  defaultValues,\n  onSubmit,\n  className,\n  pageComponents,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  pageComponents: { id: number; name: string }[];\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(\n    trpc.workspace.getWorkspace.queryOptions(),\n  );\n  const mobile = useIsMobile();\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      title: \"\",\n      message: \"\",\n      startDate: new Date(),\n      endDate: addDays(new Date(), 1),\n      pageComponents: [],\n      notifySubscribers: true,\n    },\n  });\n  const watchEndDate = form.watch(\"endDate\");\n  const watchMessage = form.watch(\"message\");\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"title\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Title</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"DB migration...\" {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          {/* TODO: */}\n          <FormField\n            control={form.control}\n            name=\"startDate\"\n            render={({ field }) => (\n              <FormItem className=\"flex flex-col\">\n                <FormLabel>Start Date</FormLabel>\n                <Popover modal>\n                  <FormControl>\n                    <PopoverTrigger asChild>\n                      <Button\n                        type=\"button\"\n                        variant=\"outline\"\n                        size=\"sm\"\n                        className={cn(\n                          \"w-[240px] pl-3 text-left font-normal\",\n                          !field.value && \"text-muted-foreground\",\n                        )}\n                      >\n                        {field.value ? (\n                          format(field.value, \"PPP 'at' h:mm a\")\n                        ) : (\n                          <span>Pick a date</span>\n                        )}\n                        <CalendarIcon className=\"ml-auto h-4 w-4 opacity-50\" />\n                      </Button>\n                    </PopoverTrigger>\n                  </FormControl>\n                  <PopoverContent\n                    className=\"pointer-events-auto w-auto p-0\"\n                    align=\"start\"\n                    side={mobile ? \"bottom\" : \"left\"}\n                  >\n                    <Calendar\n                      mode=\"single\"\n                      selected={field.value}\n                      onSelect={(selectedDate) => {\n                        if (!selectedDate) return;\n\n                        const newDate = new Date(selectedDate);\n                        newDate.setHours(\n                          field.value.getHours(),\n                          field.value.getMinutes(),\n                          field.value.getSeconds(),\n                          field.value.getMilliseconds(),\n                        );\n                        field.onChange(newDate);\n\n                        // NOTE: if end date is before start date, set it to the same day as the start date\n                        if (watchEndDate && newDate > watchEndDate) {\n                          form.setValue(\"endDate\", newDate);\n                        }\n                      }}\n                      initialFocus\n                    />\n                    <div className=\"border-t p-3\">\n                      <div className=\"flex items-center gap-3\">\n                        <Label htmlFor=\"time-start\" className=\"text-xs\">\n                          Enter time\n                        </Label>\n                        <div className=\"relative grow\">\n                          <Input\n                            id=\"time-start\"\n                            type=\"time\"\n                            step=\"1\"\n                            value={\n                              field.value\n                                ? field.value.toTimeString().slice(0, 8)\n                                : new Date().toTimeString().slice(0, 8)\n                            }\n                            className=\"peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n                            onChange={(e) => {\n                              try {\n                                const timeValue = e.target.value;\n                                if (!timeValue || !field.value) return;\n\n                                const [hours, minutes, seconds] = timeValue\n                                  .split(\":\")\n                                  .map(Number);\n\n                                const newDate = new Date(field.value);\n                                newDate.setHours(\n                                  hours,\n                                  minutes,\n                                  seconds || 0,\n                                  0,\n                                );\n\n                                field.onChange(newDate);\n                              } catch (error) {\n                                console.error(error);\n                              }\n                            }}\n                          />\n                          <div className=\"pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80 peer-disabled:opacity-50\">\n                            <ClockIcon size={16} aria-hidden=\"true\" />\n                          </div>\n                        </div>\n                      </div>\n                    </div>\n                  </PopoverContent>\n                </Popover>\n                <FormDescription>\n                  When the maintenance starts. Shown in your timezone (\n                  <code className=\"font-commit-mono text-foreground/70\">\n                    {timezone}\n                  </code>\n                  ) and saved as Unix time (\n                  <code className=\"font-commit-mono text-foreground/70\">\n                    UTC\n                  </code>\n                  ).\n                </FormDescription>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"endDate\"\n            render={({ field }) => (\n              <FormItem className=\"flex flex-col\">\n                <FormLabel>End Date</FormLabel>\n                <Popover modal>\n                  <FormControl>\n                    <PopoverTrigger asChild>\n                      <Button\n                        type=\"button\"\n                        variant=\"outline\"\n                        size=\"sm\"\n                        className={cn(\n                          \"w-[240px] pl-3 text-left font-normal\",\n                          !field.value && \"text-muted-foreground\",\n                        )}\n                      >\n                        {field.value ? (\n                          format(field.value, \"PPP 'at' h:mm a\")\n                        ) : (\n                          <span>Pick a date</span>\n                        )}\n                        <CalendarIcon className=\"ml-auto h-4 w-4 opacity-50\" />\n                      </Button>\n                    </PopoverTrigger>\n                  </FormControl>\n                  <PopoverContent\n                    className=\"pointer-events-auto w-auto p-0\"\n                    align=\"start\"\n                    side={mobile ? \"bottom\" : \"left\"}\n                  >\n                    <Calendar\n                      mode=\"single\"\n                      selected={field.value}\n                      onSelect={(selectedDate) => {\n                        if (!selectedDate) return;\n\n                        const newDate = new Date(selectedDate);\n                        newDate.setHours(\n                          field.value.getHours(),\n                          field.value.getMinutes(),\n                          field.value.getSeconds(),\n                          field.value.getMilliseconds(),\n                        );\n                        field.onChange(newDate);\n                      }}\n                      initialFocus\n                    />\n                    <div className=\"border-t p-3\">\n                      <div className=\"flex items-center gap-3\">\n                        <Label htmlFor=\"time-end\" className=\"text-xs\">\n                          Enter time\n                        </Label>\n                        <div className=\"relative grow\">\n                          <Input\n                            id=\"time-end\"\n                            type=\"time\"\n                            step=\"1\"\n                            value={\n                              field.value\n                                ? field.value.toTimeString().slice(0, 8)\n                                : new Date().toTimeString().slice(0, 8)\n                            }\n                            className=\"peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n                            onChange={(e) => {\n                              try {\n                                const timeValue = e.target.value;\n                                if (!timeValue || !field.value) return;\n\n                                const [hours, minutes, seconds] = timeValue\n                                  .split(\":\")\n                                  .map(Number);\n\n                                const newDate = new Date(field.value);\n                                newDate.setHours(\n                                  hours,\n                                  minutes,\n                                  seconds || 0,\n                                  0,\n                                );\n\n                                field.onChange(newDate);\n                              } catch (error) {\n                                console.error(error);\n                              }\n                            }}\n                          />\n                          <div className=\"pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80 peer-disabled:opacity-50\">\n                            <ClockIcon size={16} aria-hidden=\"true\" />\n                          </div>\n                        </div>\n                      </div>\n                    </div>\n                  </PopoverContent>\n                </Popover>\n                <FormDescription>\n                  When the maintenance ends. Shown in your timezone (\n                  <code className=\"font-commit-mono text-foreground/70\">\n                    {timezone}\n                  </code>\n                  ) and saved as Unix time (\n                  <code className=\"font-commit-mono text-foreground/70\">\n                    UTC\n                  </code>\n                  ).\n                </FormDescription>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <Tabs defaultValue=\"tab-1\">\n            <TabsList>\n              <TabsTrigger value=\"tab-1\">Writing</TabsTrigger>\n              <TabsTrigger value=\"tab-2\">Preview</TabsTrigger>\n            </TabsList>\n            <TabsContent value=\"tab-1\">\n              <FormField\n                control={form.control}\n                name=\"message\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Message</FormLabel>\n                    <FormControl>\n                      <Textarea rows={6} {...field} />\n                    </FormControl>\n                    <FormMessage />\n                    <FormDescription>Markdown support</FormDescription>\n                  </FormItem>\n                )}\n              />\n            </TabsContent>\n            <TabsContent value=\"tab-2\">\n              <div className=\"grid gap-2\">\n                <Label>Preview</Label>\n                <div className=\"prose dark:prose-invert prose-sm rounded-md border px-3 py-2 text-foreground text-sm\">\n                  <ProcessMessage value={watchMessage} />\n                </div>\n              </div>\n            </TabsContent>\n          </Tabs>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"pageComponents\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Page Components</FormLabel>\n                <FormDescription>\n                  Connected page components will be affected for the period of\n                  time.\n                </FormDescription>\n                {pageComponents.length ? (\n                  <div className=\"grid gap-3\">\n                    <div className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id=\"all\"\n                          checked={\n                            field.value?.length === pageComponents.length\n                          }\n                          onCheckedChange={(checked) => {\n                            field.onChange(\n                              checked ? pageComponents.map((c) => c.id) : [],\n                            );\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor=\"all\">Select all</Label>\n                    </div>\n                    {pageComponents.map((item) => (\n                      <div key={item.id} className=\"flex items-center gap-2\">\n                        <FormControl>\n                          <Checkbox\n                            id={String(item.id)}\n                            checked={field.value?.includes(item.id)}\n                            onCheckedChange={(checked) => {\n                              const newValue = checked\n                                ? [...(field.value || []), item.id]\n                                : field.value?.filter((id) => id !== item.id);\n                              field.onChange(newValue);\n                            }}\n                          />\n                        </FormControl>\n                        <Label htmlFor={String(item.id)}>{item.name}</Label>\n                      </div>\n                    ))}\n                  </div>\n                ) : (\n                  <EmptyStateContainer>\n                    <EmptyStateTitle>No page components found</EmptyStateTitle>\n                  </EmptyStateContainer>\n                )}\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        {!defaultValues && workspace?.limits[\"status-subscribers\"] ? (\n          <>\n            <FormCardSeparator />\n            <FormCardContent>\n              <FormField\n                control={form.control}\n                name=\"notifySubscribers\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Notify Subscribers</FormLabel>\n                    <FormControl>\n                      <div className=\"flex items-center gap-2\">\n                        <Checkbox\n                          id=\"notifySubscribers\"\n                          checked={field.value}\n                          onCheckedChange={field.onChange}\n                        />\n                        <Label htmlFor=\"notifySubscribers\">\n                          Send email notification to subscribers\n                        </Label>\n                      </div>\n                    </FormControl>\n                    <FormMessage />\n                    <FormDescription>\n                      Subscribers will receive an email when creating a\n                      maintenance.\n                    </FormDescription>\n                  </FormItem>\n                )}\n              />\n            </FormCardContent>\n          </>\n        ) : null}\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/maintenance/sheet.tsx",
    "content": "\"use client\";\n\nimport { FormCard, FormCardGroup } from \"@/components/forms/form-card\";\nimport {\n  FormSheetContent,\n  FormSheetDescription,\n  FormSheetFooter,\n  FormSheetFooterInfo,\n  FormSheetHeader,\n  FormSheetTitle,\n  FormSheetTrigger,\n  FormSheetWithDirtyProtection,\n} from \"@/components/forms/form-sheet\";\nimport {\n  FormMaintenance,\n  type FormValues,\n} from \"@/components/forms/maintenance/form\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useState } from \"react\";\n\nexport function FormSheetMaintenance({\n  children,\n  defaultValues,\n  onSubmit,\n  pageComponents,\n  ...props\n}: Omit<React.ComponentProps<typeof FormSheetTrigger>, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  pageComponents: { id: number; name: string }[];\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const [open, setOpen] = useState(false);\n\n  return (\n    <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}>\n      <FormSheetTrigger {...props} asChild>\n        {children}\n      </FormSheetTrigger>\n      <FormSheetContent className=\"sm:max-w-lg\">\n        <FormSheetHeader>\n          <FormSheetTitle>Maintenance</FormSheetTitle>\n          <FormSheetDescription>\n            Configure and update the maintenance.\n          </FormSheetDescription>\n        </FormSheetHeader>\n        <FormCardGroup className=\"overflow-y-auto\">\n          <FormCard className=\"overflow-auto rounded-none border-none\">\n            <FormMaintenance\n              pageComponents={pageComponents}\n              onSubmit={async (values) => {\n                await onSubmit(values);\n                setOpen(false);\n              }}\n              defaultValues={defaultValues}\n              id=\"maintenance-form\"\n              className=\"my-4\"\n            />\n          </FormCard>\n        </FormCardGroup>\n        <FormSheetFooter>\n          {defaultValues ? (\n            <FormSheetFooterInfo>\n              Last Updated {/* TODO: use updatedAt */}\n              <time>{defaultValues.startDate.toLocaleString()}</time>\n            </FormSheetFooterInfo>\n          ) : null}\n          <Button type=\"submit\" form=\"maintenance-form\">\n            Submit\n          </Button>\n        </FormSheetFooter>\n      </FormSheetContent>\n    </FormSheetWithDirtyProtection>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-danger-zone.tsx",
    "content": "\"use client\";\n\nimport { FormAlertDialog } from \"@/components/forms/form-alert-dialog\";\nimport {\n  FormCard,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\n\nexport function FormDangerZone({\n  onSubmit,\n  title,\n}: {\n  onSubmit: () => Promise<void>;\n  title: string;\n}) {\n  return (\n    <FormCard variant=\"destructive\">\n      <FormCardHeader>\n        <FormCardTitle>Danger Zone</FormCardTitle>\n        <FormCardDescription>This action cannot be undone.</FormCardDescription>\n      </FormCardHeader>\n      <FormCardFooter variant=\"destructive\" className=\"justify-end\">\n        <FormAlertDialog confirmationValue={title} submitAction={onSubmit} />\n      </FormCardFooter>\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-follow-redirect.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Switch } from \"@openstatus/ui/components/ui/switch\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nexport const FOLLOW_REDIRECTS_DEFAULT = true;\n\nconst schema = z.object({\n  followRedirects: z.boolean().prefault(true),\n});\n\ntype FormValues = z.input<typeof schema>;\n\nexport function FormFollowRedirect({\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      followRedirects: FOLLOW_REDIRECTS_DEFAULT,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: \"Failed to save\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Follow Redirects</FormCardTitle>\n            <FormCardDescription>\n              Configure whether to follow redirects.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid gap-4\">\n            <FormField\n              control={form.control}\n              name=\"followRedirects\"\n              render={({ field }) => (\n                <FormItem className=\"flex flex-row items-center justify-between\">\n                  <div className=\"space-y-0.5\">\n                    <FormLabel>Follow redirects</FormLabel>\n                    <FormDescription>\n                      When enabled, the monitor will automatically follow HTTP\n                      redirects (3xx status codes) to their final destination.\n                      This is useful when monitoring URLs that may redirect to\n                      other locations.\n                    </FormDescription>\n                  </div>\n                  <FormControl>\n                    <Switch\n                      checked={field.value}\n                      onCheckedChange={field.onChange}\n                    />\n                  </FormControl>\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/monitoring/customization/follow-redirects/\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                follow redirects\n              </Link>\n              .\n            </FormCardFooterInfo>\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-general.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  dnsRecords,\n  headerAssertion,\n  jsonBodyAssertion,\n  numberCompareDictionary,\n  recordAssertion,\n  recordCompareDictionary,\n  statusAssertion,\n  stringCompareDictionary,\n  textBodyAssertion,\n} from \"@openstatus/assertions\";\nimport { monitorMethods } from \"@openstatus/db/src/schema\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  RadioGroup,\n  RadioGroupItem,\n} from \"@openstatus/ui/components/ui/radio-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { Switch } from \"@openstatus/ui/components/ui/switch\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { Globe, Network, Plus, Server, X } from \"lucide-react\";\nimport { useEffect, useState, useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst TYPES = [\"http\", \"tcp\", \"dns\"] as const;\nconst HTTP_ASSERTION_TYPES = [\"status\", \"header\", \"textBody\"] as const;\nconst DNS_ASSERTION_TYPES = dnsRecords;\n\nconst schema = z.object({\n  name: z.string().min(1, \"Name is required\"),\n  type: z.enum(TYPES),\n  method: z.enum(monitorMethods),\n  url: z.string().min(1, \"URL is required\"),\n  headers: z.array(\n    z.object({\n      key: z.string(),\n      value: z.string(),\n    }),\n  ),\n  active: z.boolean().optional().prefault(true),\n  assertions: z.array(\n    z.discriminatedUnion(\"type\", [\n      statusAssertion,\n      headerAssertion,\n      textBodyAssertion,\n      jsonBodyAssertion,\n      recordAssertion,\n    ]),\n  ),\n  body: z.string().optional(),\n  skipCheck: z.boolean().optional().prefault(false),\n  saveCheck: z.boolean().optional().prefault(false),\n});\n\ntype FormValues = z.input<typeof schema>;\n\nexport function FormGeneral({\n  defaultValues,\n  disabled,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  disabled?: boolean;\n}) {\n  const [error, setError] = useState<string | null>(null);\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      active: true,\n      name: \"\",\n      type: undefined,\n      method: \"GET\",\n      url: \"\",\n      headers: [],\n      body: \"\",\n      assertions: [],\n      skipCheck: false,\n      saveCheck: false,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const watchType = form.watch(\"type\");\n  const watchMethod = form.watch(\"method\");\n\n  useEffect(() => {\n    // NOTE: reset form when type changes\n    if (watchType && !defaultValues) {\n      form.setValue(\"assertions\", []);\n      form.setValue(\"body\", \"\");\n      form.setValue(\"headers\", []);\n      form.setValue(\"method\", \"GET\");\n      form.setValue(\"url\", \"\");\n    }\n  }, [watchType, defaultValues, form]);\n\n  function submitAction(values: FormValues) {\n    console.log(\"submitAction\", values);\n    if (isPending || disabled) return;\n\n    // Validate assertions based on type\n    for (let i = 0; i < values.assertions.length; i++) {\n      const assertion = values.assertions[i];\n\n      if (assertion.type === \"status\") {\n        if (typeof assertion.target !== \"number\" || assertion.target <= 0) {\n          form.setError(`assertions.${i}.target`, {\n            message: \"Status target must be a positive number\",\n          });\n          return;\n        }\n      } else if (assertion.type === \"header\") {\n        if (!assertion.key || assertion.key.trim() === \"\") {\n          form.setError(`assertions.${i}.key`, {\n            message: \"Header key is required\",\n          });\n          return;\n        }\n        if (!assertion.target || assertion.target.trim() === \"\") {\n          form.setError(`assertions.${i}.target`, {\n            message: \"Header target is required\",\n          });\n          return;\n        }\n      } else if (assertion.type === \"textBody\") {\n        if (!assertion.target || assertion.target.trim() === \"\") {\n          form.setError(`assertions.${i}.target`, {\n            message: \"Body target is required\",\n          });\n          return;\n        }\n      } else if (assertion.type === \"dnsRecord\") {\n        if (!assertion.key || assertion.key.trim() === \"\") {\n          form.setError(`assertions.${i}.key`, {\n            message: \"DNS record key is required\",\n          });\n          return;\n        }\n        if (!assertion.target || assertion.target.trim() === \"\") {\n          form.setError(`assertions.${i}.target`, {\n            message: \"DNS record target is required\",\n          });\n          return;\n        }\n      }\n    }\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              setError(error.message);\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Monitor Configuration</FormCardTitle>\n            <FormCardDescription>\n              Configure your monitor settings and endpoints.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid gap-4 sm:grid-cols-3\">\n            <FormField\n              control={form.control}\n              name=\"name\"\n              render={({ field }) => (\n                <FormItem className=\"sm:col-span-2\">\n                  <FormLabel>Name</FormLabel>\n                  <FormControl>\n                    <Input placeholder=\"OpenStatus API\" {...field} />\n                  </FormControl>\n                  <FormMessage />\n                  <FormDescription>\n                    Displayed on the status page.\n                  </FormDescription>\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"active\"\n              render={({ field }) => (\n                <FormItem className=\"flex flex-row items-center\">\n                  <FormLabel>Active</FormLabel>\n                  <FormControl>\n                    <Switch\n                      checked={field.value}\n                      onCheckedChange={field.onChange}\n                    />\n                  </FormControl>\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardSeparator />\n          <FormCardContent>\n            <FormField\n              control={form.control}\n              name=\"type\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Monitoring Type</FormLabel>\n                  <FormControl>\n                    <RadioGroup\n                      onValueChange={field.onChange}\n                      defaultValue={field.value}\n                      className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\"\n                      disabled={!!defaultValues?.type}\n                    >\n                      {[\n                        { value: \"http\", icon: Globe, label: \"HTTP\" },\n                        { value: \"tcp\", icon: Network, label: \"TCP\" },\n                        { value: \"dns\", icon: Server, label: \"DNS\" },\n                      ].map((type) => {\n                        return (\n                          <Tooltip key={type.value}>\n                            <TooltipTrigger asChild>\n                              <FormItem\n                                className={cn(\n                                  \"relative flex cursor-pointer flex-row items-center gap-3 rounded-md border border-input px-2 py-3 text-center shadow-xs outline-none transition-[color,box-shadow] has-aria-[invalid=true]:border-destructive has-data-[state=checked]:border-primary/50 has-focus-visible:border-ring has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50\",\n                                  defaultValues &&\n                                    defaultValues.type !== type.value &&\n                                    \"pointer-events-none opacity-50\",\n                                )}\n                              >\n                                <FormControl>\n                                  <RadioGroupItem\n                                    value={type.value}\n                                    className=\"sr-only\"\n                                    disabled={!!defaultValues?.type}\n                                  />\n                                </FormControl>\n                                <type.icon\n                                  className=\"shrink-0 text-muted-foreground\"\n                                  size={16}\n                                  aria-hidden=\"true\"\n                                />\n                                <FormLabel className=\"cursor-pointer font-medium text-foreground text-xs leading-none after:absolute after:inset-0\">\n                                  {type.label}\n                                </FormLabel>\n                              </FormItem>\n                            </TooltipTrigger>\n                            <TooltipContent>\n                              Monitor type cannot be changed after creation.\n                            </TooltipContent>\n                          </Tooltip>\n                        );\n                      })}\n                      <div\n                        className={cn(\n                          \"col-span-1 self-end text-muted-foreground text-xs sm:place-self-end\",\n                        )}\n                      >\n                        Missing a type?{\" \"}\n                        <a href=\"mailto:ping@openstatus.dev\">Contact us</a>\n                      </div>\n                    </RadioGroup>\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          {watchType ? <FormCardSeparator /> : null}\n          {watchType === \"http\" && (\n            <>\n              <FormCardContent className=\"grid grid-cols-4 gap-4\">\n                <div className=\"col-span-1\">\n                  <FormField\n                    control={form.control}\n                    name=\"method\"\n                    render={({ field }) => (\n                      <FormItem>\n                        <FormLabel>Method</FormLabel>\n                        <Select\n                          onValueChange={field.onChange}\n                          defaultValue={field.value}\n                        >\n                          <FormControl>\n                            <SelectTrigger className=\"w-full\">\n                              <SelectValue placeholder=\"Select a method\" />\n                            </SelectTrigger>\n                          </FormControl>\n                          <SelectContent>\n                            {monitorMethods.map((method) => (\n                              <SelectItem key={method} value={method}>\n                                {method}\n                              </SelectItem>\n                            ))}\n                          </SelectContent>\n                        </Select>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n                </div>\n                <div className=\"col-span-3\">\n                  <FormField\n                    control={form.control}\n                    name=\"url\"\n                    render={({ field }) => (\n                      <FormItem>\n                        <FormLabel>URL</FormLabel>\n                        <FormControl>\n                          <Input\n                            placeholder=\"https://openstatus.dev\"\n                            type=\"url\"\n                            {...field}\n                          />\n                        </FormControl>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n                </div>\n                <FormField\n                  control={form.control}\n                  name=\"headers\"\n                  render={({ field }) => (\n                    <FormItem className=\"col-span-full\">\n                      <FormLabel>Request Headers</FormLabel>\n                      {field.value.map((header, index) => (\n                        <div key={index} className=\"grid gap-2 sm:grid-cols-5\">\n                          <Input\n                            placeholder=\"Key\"\n                            className=\"col-span-2\"\n                            value={header.key}\n                            onChange={(e) => {\n                              const newHeaders = [...field.value];\n                              newHeaders[index] = {\n                                ...newHeaders[index],\n                                key: e.target.value,\n                              };\n                              field.onChange(newHeaders);\n                            }}\n                          />\n                          <Input\n                            placeholder=\"Value\"\n                            className=\"col-span-2\"\n                            value={header.value}\n                            onChange={(e) => {\n                              const newHeaders = [...field.value];\n                              newHeaders[index] = {\n                                ...newHeaders[index],\n                                value: e.target.value,\n                              };\n                              field.onChange(newHeaders);\n                            }}\n                          />\n                          <Button\n                            size=\"icon\"\n                            variant=\"ghost\"\n                            onClick={() => {\n                              const newHeaders = field.value.filter(\n                                (_, i) => i !== index,\n                              );\n                              field.onChange(newHeaders);\n                            }}\n                          >\n                            <X />\n                          </Button>\n                        </div>\n                      ))}\n                      <div>\n                        <Button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          type=\"button\"\n                          onClick={() => {\n                            field.onChange([\n                              ...field.value,\n                              { key: \"\", value: \"\" },\n                            ]);\n                          }}\n                        >\n                          <Plus />\n                          Add Header\n                        </Button>\n                      </div>\n                      <FormMessage />\n                    </FormItem>\n                  )}\n                />\n                {[\"POST\", \"PUT\", \"PATCH\", \"DELETE\"].includes(watchMethod) && (\n                  <FormField\n                    control={form.control}\n                    name=\"body\"\n                    render={({ field }) => (\n                      <FormItem className=\"col-span-full\">\n                        <FormLabel>Body</FormLabel>\n                        <FormControl>\n                          <Textarea {...field} />\n                        </FormControl>\n                        <FormDescription>Write your payload</FormDescription>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n                )}\n              </FormCardContent>\n              <FormCardSeparator />\n              <FormCardContent>\n                <FormField\n                  control={form.control}\n                  name=\"assertions\"\n                  render={({ field }) => (\n                    <FormItem className=\"col-span-full\">\n                      <FormLabel>Assertions</FormLabel>\n                      <FormDescription>\n                        Validate the response to ensure your service is working\n                        as expected. <br />\n                        Add body, header, or status assertions.\n                      </FormDescription>\n                      {field.value.map((assertion, index) => (\n                        <div key={index} className=\"grid gap-2 sm:grid-cols-6\">\n                          <FormField\n                            control={form.control}\n                            name={`assertions.${index}.type`}\n                            render={({ field }) => (\n                              <FormItem>\n                                <Select\n                                  value={field.value}\n                                  onValueChange={field.onChange}\n                                  disabled={true}\n                                >\n                                  <SelectTrigger\n                                    aria-invalid={\n                                      !!form.formState.errors.assertions?.[\n                                        index\n                                      ]?.type\n                                    }\n                                    className=\"w-full\"\n                                  >\n                                    <SelectValue placeholder=\"Select type\" />\n                                  </SelectTrigger>\n                                  <SelectContent>\n                                    {HTTP_ASSERTION_TYPES.map((type) => (\n                                      <SelectItem key={type} value={type}>\n                                        {type}\n                                      </SelectItem>\n                                    ))}\n                                  </SelectContent>\n                                </Select>\n                              </FormItem>\n                            )}\n                          />\n                          <FormField\n                            control={form.control}\n                            name={`assertions.${index}.compare`}\n                            render={({ field }) => (\n                              <FormItem>\n                                <Select\n                                  value={field.value}\n                                  onValueChange={field.onChange}\n                                >\n                                  <SelectTrigger className=\"w-full min-w-16\">\n                                    <span className=\"truncate\">\n                                      <SelectValue placeholder=\"Select compare\" />\n                                    </span>\n                                  </SelectTrigger>\n                                  <SelectContent>\n                                    {assertion.type === \"status\"\n                                      ? Object.entries(\n                                          numberCompareDictionary,\n                                        ).map(([key, value]) => (\n                                          <SelectItem key={key} value={key}>\n                                            {value}\n                                          </SelectItem>\n                                        ))\n                                      : Object.entries(\n                                          stringCompareDictionary,\n                                        ).map(([key, value]) => (\n                                          <SelectItem key={key} value={key}>\n                                            {value}\n                                          </SelectItem>\n                                        ))}\n                                  </SelectContent>\n                                </Select>\n                                <FormMessage />\n                              </FormItem>\n                            )}\n                          />\n                          {assertion.type === \"header\" && (\n                            <FormField\n                              control={form.control}\n                              name={`assertions.${index}.key`}\n                              render={({ field }) => (\n                                <FormItem>\n                                  <Input\n                                    placeholder=\"Header key\"\n                                    className=\"w-full\"\n                                    {...field}\n                                    value={field.value as string}\n                                  />\n                                  <FormMessage />\n                                </FormItem>\n                              )}\n                            />\n                          )}\n                          <FormField\n                            control={form.control}\n                            name={`assertions.${index}.target`}\n                            render={({ field }) => (\n                              <FormItem>\n                                <Input\n                                  placeholder=\"Target value\"\n                                  className=\"w-full\"\n                                  type={\n                                    assertion.type === \"status\"\n                                      ? \"number\"\n                                      : \"text\"\n                                  }\n                                  {...field}\n                                  value={field.value?.toString() || \"\"}\n                                  onChange={(e) => {\n                                    const value =\n                                      assertion.type === \"status\"\n                                        ? Number.parseInt(e.target.value) || 0\n                                        : e.target.value;\n                                    field.onChange(value);\n                                  }}\n                                />\n                                <FormMessage />\n                              </FormItem>\n                            )}\n                          />\n                          <Button\n                            size=\"icon\"\n                            variant=\"ghost\"\n                            type=\"button\"\n                            onClick={() => {\n                              const newAssertions = field.value.filter(\n                                (_, i) => i !== index,\n                              );\n                              field.onChange(newAssertions);\n                            }}\n                          >\n                            <X />\n                          </Button>\n                        </div>\n                      ))}\n                      <div className=\"flex flex-wrap gap-2\">\n                        <Button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          type=\"button\"\n                          onClick={() => {\n                            const currentAssertions =\n                              form.getValues(\"assertions\");\n                            field.onChange([\n                              ...currentAssertions,\n                              {\n                                type: \"status\",\n                                version: \"v1\",\n                                compare: \"eq\",\n                                target: 200,\n                              },\n                            ]);\n                          }}\n                        >\n                          <Plus />\n                          Add Status Assertion\n                        </Button>\n                        <Button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          type=\"button\"\n                          onClick={() => {\n                            const currentAssertions =\n                              form.getValues(\"assertions\");\n                            field.onChange([\n                              ...currentAssertions,\n                              {\n                                type: \"header\",\n                                version: \"v1\",\n                                compare: \"eq\",\n                                key: \"\",\n                                target: \"\",\n                              },\n                            ]);\n                          }}\n                        >\n                          <Plus />\n                          Add Header Assertion\n                        </Button>\n                        <Button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          type=\"button\"\n                          onClick={() => {\n                            const currentAssertions =\n                              form.getValues(\"assertions\");\n                            field.onChange([\n                              ...currentAssertions,\n                              {\n                                type: \"textBody\",\n                                version: \"v1\",\n                                compare: \"eq\",\n                                target: \"\",\n                              },\n                            ]);\n                          }}\n                        >\n                          <Plus />\n                          Add Body Assertion\n                        </Button>\n                      </div>\n                      <FormMessage />\n                    </FormItem>\n                  )}\n                />\n              </FormCardContent>\n            </>\n          )}\n          {watchType === \"tcp\" && (\n            <FormCardContent className=\"grid gap-4 sm:grid-cols-3\">\n              <FormField\n                control={form.control}\n                name=\"url\"\n                render={({ field }) => (\n                  <FormItem className=\"sm:col-span-2\">\n                    <FormLabel>Host:Port</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"127.0.0.0.1:8080\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                    <FormDescription>\n                      The input supports both IPv4 addresses and IPv6 addresses.\n                    </FormDescription>\n                  </FormItem>\n                )}\n              />\n              <div className=\"col-span-full text-muted-foreground text-sm\">\n                Examples:\n                <ul className=\"list-inside list-disc\">\n                  <li>\n                    Domain:{\" \"}\n                    <span className=\"font-mono text-foreground\">\n                      openstatus.dev:443\n                    </span>\n                  </li>\n                  <li>\n                    IPv4:{\" \"}\n                    <span className=\"font-mono text-foreground\">\n                      192.168.1.1:443\n                    </span>\n                  </li>\n                  <li>\n                    IPv6:{\" \"}\n                    <span className=\"font-mono text-foreground\">\n                      [2001:db8:85a3:8d3:1319:8a2e:370:7348]:443\n                    </span>\n                  </li>\n                </ul>\n              </div>\n            </FormCardContent>\n          )}\n          {watchType === \"dns\" && (\n            <>\n              <FormCardContent className=\"grid gap-4 sm:grid-cols-3\">\n                <FormField\n                  control={form.control}\n                  name=\"url\"\n                  render={({ field }) => (\n                    <FormItem className=\"sm:col-span-2\">\n                      <FormLabel>URI</FormLabel>\n                      <FormControl>\n                        <Input placeholder=\"openstatus.dev\" {...field} />\n                      </FormControl>\n                      <FormMessage />\n                      <FormDescription>\n                        The input supports both domain names and URIs.\n                      </FormDescription>\n                    </FormItem>\n                  )}\n                />\n              </FormCardContent>\n              <FormCardSeparator />\n              <FormCardContent>\n                <FormField\n                  control={form.control}\n                  name=\"assertions\"\n                  render={({ field }) => (\n                    <FormItem className=\"col-span-full\">\n                      <FormLabel>Assertions</FormLabel>\n                      <FormDescription>\n                        Validate the response to ensure your service is working\n                        as expected. <br />\n                        Add DNS record assertions.\n                      </FormDescription>\n                      {field.value.map((assertion, index) => (\n                        <div key={index} className=\"grid gap-2 sm:grid-cols-6\">\n                          <FormField\n                            control={form.control}\n                            name={`assertions.${index}.type`}\n                            defaultValue={\"dnsRecord\"}\n                            render={({ field }) => (\n                              <FormItem className=\"hidden\">\n                                <Select\n                                  value={field.value}\n                                  onValueChange={field.onChange}\n                                  disabled\n                                >\n                                  <SelectTrigger className=\"w-full\">\n                                    <SelectValue placeholder=\"Select type\" />\n                                  </SelectTrigger>\n                                </Select>\n                              </FormItem>\n                            )}\n                          />\n                          <FormField\n                            control={form.control}\n                            name={`assertions.${index}.key`}\n                            render={({ field }) => (\n                              <FormItem>\n                                <Select\n                                  value={field.value as string}\n                                  onValueChange={field.onChange}\n                                >\n                                  <SelectTrigger\n                                    aria-invalid={\n                                      !!form.formState.errors.assertions?.[\n                                        index\n                                      ]?.type\n                                    }\n                                    className=\"w-full\"\n                                  >\n                                    <SelectValue placeholder=\"Select type\" />\n                                  </SelectTrigger>\n                                  <SelectContent>\n                                    {DNS_ASSERTION_TYPES.map((type) => (\n                                      <SelectItem key={type} value={type}>\n                                        {type}\n                                      </SelectItem>\n                                    ))}\n                                  </SelectContent>\n                                </Select>\n                              </FormItem>\n                            )}\n                          />\n                          <FormField\n                            control={form.control}\n                            name={`assertions.${index}.compare`}\n                            render={({ field }) => (\n                              <FormItem>\n                                <Select\n                                  value={field.value}\n                                  onValueChange={field.onChange}\n                                >\n                                  <SelectTrigger className=\"w-full min-w-16\">\n                                    <span className=\"truncate\">\n                                      <SelectValue placeholder=\"Select compare\" />\n                                    </span>\n                                  </SelectTrigger>\n                                  <SelectContent>\n                                    {Object.entries(\n                                      recordCompareDictionary,\n                                    ).map(([key, value]) => (\n                                      <SelectItem key={key} value={key}>\n                                        {value}\n                                      </SelectItem>\n                                    ))}\n                                  </SelectContent>\n                                </Select>\n                                <FormMessage />\n                              </FormItem>\n                            )}\n                          />\n                          {assertion.type === \"header\" && (\n                            <FormField\n                              control={form.control}\n                              name={`assertions.${index}.key`}\n                              render={({ field }) => (\n                                <FormItem>\n                                  <Input\n                                    placeholder=\"Header key\"\n                                    className=\"w-full\"\n                                    {...field}\n                                    value={field.value as string}\n                                  />\n                                  <FormMessage />\n                                </FormItem>\n                              )}\n                            />\n                          )}\n                          <FormField\n                            control={form.control}\n                            name={`assertions.${index}.target`}\n                            render={({ field }) => (\n                              <FormItem>\n                                <Input\n                                  placeholder=\"Target value\"\n                                  className=\"w-full\"\n                                  type={\n                                    assertion.type === \"status\"\n                                      ? \"number\"\n                                      : \"text\"\n                                  }\n                                  {...field}\n                                  value={field.value?.toString() || \"\"}\n                                  onChange={(e) => {\n                                    const value =\n                                      assertion.type === \"status\"\n                                        ? Number.parseInt(e.target.value) || 0\n                                        : e.target.value;\n                                    field.onChange(value);\n                                  }}\n                                />\n                                <FormMessage />\n                              </FormItem>\n                            )}\n                          />\n                          <Button\n                            size=\"icon\"\n                            variant=\"ghost\"\n                            type=\"button\"\n                            onClick={() => {\n                              const newAssertions = field.value.filter(\n                                (_, i) => i !== index,\n                              );\n                              field.onChange(newAssertions);\n                            }}\n                          >\n                            <X />\n                          </Button>\n                        </div>\n                      ))}\n                      <div className=\"flex flex-wrap gap-2\">\n                        <Button\n                          size=\"sm\"\n                          variant=\"outline\"\n                          type=\"button\"\n                          onClick={() => {\n                            const currentAssertions =\n                              form.getValues(\"assertions\");\n                            field.onChange([\n                              ...currentAssertions,\n                              {\n                                type: \"dnsRecord\",\n                                version: \"v1\",\n                                compare: \"eq\",\n                                key: \"A\",\n                                target: \"\",\n                              },\n                            ]);\n                          }}\n                        >\n                          <Plus />\n                          Add DNS Record Assertion\n                        </Button>\n                      </div>\n                      <FormMessage />\n                    </FormItem>\n                  )}\n                />\n              </FormCardContent>\n            </>\n          )}\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/tutorial/how-to-create-monitor/\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                Monitor Type\n              </Link>{\" \"}\n              and{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/tutorial/how-to-create-monitor/\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                Assertions\n              </Link>\n              . We test your endpoint before saving the monitor.\n            </FormCardFooterInfo>\n            <Button type=\"submit\" disabled={isPending || disabled}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n        <AlertDialog open={!!error} onOpenChange={() => setError(null)}>\n          <AlertDialogContent>\n            <AlertDialogHeader>\n              <AlertDialogTitle>Still save?</AlertDialogTitle>\n              <AlertDialogDescription>\n                It seems like the endpoint is not reachable or the assertions\n                failed. Do you want to save the monitor anyway?\n              </AlertDialogDescription>\n            </AlertDialogHeader>\n            <div className=\"max-h-48 overflow-auto whitespace-pre rounded-md border border-destructive/20 bg-destructive/10 p-2\">\n              <p className=\"font-mono text-destructive text-sm\">{error}</p>\n            </div>\n            <AlertDialogFooter>\n              <AlertDialogCancel type=\"button\">Cancel</AlertDialogCancel>\n              <AlertDialogAction\n                type=\"button\"\n                onClick={async (e) => {\n                  e.preventDefault();\n                  form.setValue(\"skipCheck\", true);\n                  form.handleSubmit(submitAction)();\n                  form.setValue(\"skipCheck\", false);\n                  setError(null);\n                }}\n                disabled={isPending}\n              >\n                Save\n              </AlertDialogAction>\n            </AlertDialogFooter>\n          </AlertDialogContent>\n        </AlertDialog>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-notifiers.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { config } from \"@/data/notifications.client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport type { NotificationProvider } from \"@openstatus/db/src/schema\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  notifiers: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormNotifiers({\n  defaultValues,\n  onSubmit,\n  notifiers,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  notifiers: { id: number; name: string; provider: NotificationProvider }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      notifiers: [],\n    },\n  });\n  const watchNotifiers = form.watch(\"notifiers\");\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Notifications</FormCardTitle>\n            <FormCardDescription>\n              Get notified when your monitor is degraded or down.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent>\n            {notifiers.length > 0 ? (\n              <FormField\n                control={form.control}\n                name=\"notifiers\"\n                render={() => (\n                  <FormItem>\n                    <div className=\"flex items-center justify-between\">\n                      <FormLabel className=\"text-base\">\n                        List of Notifications\n                      </FormLabel>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        type=\"button\"\n                        className={cn(\n                          watchNotifiers.length === notifiers.length &&\n                            \"text-muted-foreground\",\n                        )}\n                        onClick={() => {\n                          const allSelected = notifiers.every((item) =>\n                            watchNotifiers.includes(item.id),\n                          );\n\n                          if (!allSelected) {\n                            form.setValue(\n                              \"notifiers\",\n                              notifiers.map((item) => item.id),\n                            );\n                          } else {\n                            form.setValue(\"notifiers\", []);\n                          }\n                        }}\n                      >\n                        Select all\n                      </Button>\n                    </div>\n                    {notifiers.map((item) => (\n                      <FormField\n                        key={item.id}\n                        control={form.control}\n                        name=\"notifiers\"\n                        render={({ field }) => {\n                          const Icon = config[item.provider].icon;\n                          const label = config[item.provider].label;\n                          return (\n                            <FormItem\n                              key={item.id}\n                              className=\"flex items-center\"\n                            >\n                              <FormControl>\n                                <Checkbox\n                                  checked={\n                                    field.value?.includes(item.id) || false\n                                  }\n                                  onCheckedChange={(checked) => {\n                                    return checked\n                                      ? field.onChange([\n                                          ...field.value,\n                                          item.id,\n                                        ])\n                                      : field.onChange(\n                                          field.value?.filter(\n                                            (value) => value !== item.id,\n                                          ),\n                                        );\n                                  }}\n                                />\n                              </FormControl>\n                              <FormLabel className=\"font-normal text-sm\">\n                                {item.name}{\" \"}\n                                <Badge\n                                  variant=\"secondary\"\n                                  className=\"px-1.5 py-px font-mono text-[10px]\"\n                                >\n                                  <Icon className=\"size-2.5\" />\n                                  {label}\n                                </Badge>\n                              </FormLabel>\n                            </FormItem>\n                          );\n                        }}\n                      />\n                    ))}\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            ) : (\n              <EmptyStateContainer>\n                <EmptyStateTitle>No notifications</EmptyStateTitle>\n              </EmptyStateContainer>\n            )}\n          </FormCardContent>\n          <FormCardFooter>\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-otel.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n  FormCardUpgrade,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Lock, Plus, X } from \"lucide-react\";\nimport NextLink from \"next/link\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\n// TODO: add headers\n\nconst schema = z.object({\n  endpoint: z.url(\"Please enter a valid URL\"),\n  headers: z\n    .array(z.object({ key: z.string(), value: z.string() }))\n    .prefault([]),\n});\n\ntype FormValues = z.input<typeof schema>;\n\nexport function FormOtel({\n  locked,\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  locked?: boolean;\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? { endpoint: \"\", headers: [] },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: \"Failed to save\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          {locked ? <FormCardUpgrade /> : null}\n          <FormCardHeader>\n            <FormCardTitle>OpenTelemetry</FormCardTitle>\n            <FormCardDescription>\n              Configure your OpenTelemetry Exporter.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid grid-cols-4 gap-4\">\n            <FormField\n              control={form.control}\n              name=\"endpoint\"\n              render={({ field }) => (\n                <FormItem className=\"col-span-full\">\n                  <FormLabel>Endpoint</FormLabel>\n                  <FormControl>\n                    <Input\n                      placeholder=\"https://otel.openstatus.dev/api/v1/metrics\"\n                      disabled={locked}\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"headers\"\n              disabled={locked}\n              render={({ field }) => (\n                <FormItem className=\"col-span-full\">\n                  <FormLabel>Request Headers</FormLabel>\n                  {field.value?.map((header, index) => (\n                    <div key={index} className=\"grid gap-2 sm:grid-cols-5\">\n                      <Input\n                        placeholder=\"Key\"\n                        className=\"col-span-2\"\n                        value={header.key}\n                        disabled={locked}\n                        onChange={(e) => {\n                          const newHeaders = [...(field.value ?? [])];\n                          newHeaders[index] = {\n                            ...newHeaders[index],\n                            key: e.target.value,\n                          };\n                          field.onChange(newHeaders);\n                        }}\n                      />\n                      <Input\n                        placeholder=\"Value\"\n                        className=\"col-span-2\"\n                        value={header.value}\n                        disabled={locked}\n                        onChange={(e) => {\n                          const newHeaders = [...(field.value ?? [])];\n                          newHeaders[index] = {\n                            ...newHeaders[index],\n                            value: e.target.value,\n                          };\n                          field.onChange(newHeaders);\n                        }}\n                      />\n                      <Button\n                        size=\"icon\"\n                        variant=\"ghost\"\n                        onClick={() => {\n                          const newHeaders = field.value?.filter(\n                            (_, i) => i !== index,\n                          );\n                          field.onChange(newHeaders);\n                        }}\n                      >\n                        <X />\n                      </Button>\n                    </div>\n                  ))}\n                  <div>\n                    <Button\n                      size=\"sm\"\n                      variant=\"outline\"\n                      type=\"button\"\n                      disabled={locked}\n                      onClick={() => {\n                        field.onChange([\n                          ...(field.value ?? []),\n                          { key: \"\", value: \"\" },\n                        ]);\n                      }}\n                    >\n                      <Plus />\n                      Add Header\n                    </Button>\n                  </div>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/reference/http-monitor/#opentelemetry\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                OTel\n              </Link>\n              .\n            </FormCardFooterInfo>\n            {locked ? (\n              <Button asChild>\n                <NextLink href=\"/settings/billing\">\n                  <Lock className=\"size-4\" />\n                  Upgrade\n                </NextLink>\n              </Button>\n            ) : (\n              <Button type=\"submit\" disabled={isPending}>\n                {isPending ? \"Submitting...\" : \"Submit\"}\n              </Button>\n            )}\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-response-time.tsx",
    "content": "\"use client\";\n\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst DEGRADED = 30_000;\nconst TIMEOUT = 45_000;\n\nconst schema = z.object({\n  degradedAfter: z.coerce.number<number>().optional(),\n  timeout: z.coerce.number<number>(),\n});\n\ntype FormValues = z.input<typeof schema>;\n\nexport function FormResponseTime({\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      degradedAfter: DEGRADED,\n      timeout: TIMEOUT,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: \"Failed to save\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Response Time Thresholds</FormCardTitle>\n            <FormCardDescription>\n              Configure your degraded and timeout thresholds.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid gap-4 sm:grid-cols-2\">\n            <FormField\n              control={form.control}\n              name=\"degradedAfter\"\n              render={({ field }) => (\n                <FormItem className=\"self-start\">\n                  <FormLabel>Degraded (in ms.)</FormLabel>\n                  <FormControl>\n                    <Input placeholder=\"30000\" type=\"number\" {...field} />\n                  </FormControl>\n                  <FormDescription>\n                    Time after which the endpoint is considered degraded.\n                  </FormDescription>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"timeout\"\n              render={({ field }) => (\n                <FormItem className=\"self-start\">\n                  <FormLabel>Timeout (in ms.)</FormLabel>\n                  <FormControl>\n                    <Input placeholder=\"45000\" type=\"number\" {...field} />\n                  </FormControl>\n                  <FormDescription>\n                    Max. time allowed for request to complete.\n                  </FormDescription>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-retry.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst RETRY_MIN = 1;\nconst RETRY_MAX = 10;\nexport const RETRY_DEFAULT = 3;\n\nconst schema = z.object({\n  retry: z.coerce\n    .number<number>()\n    .min(RETRY_MIN)\n    .max(RETRY_MAX)\n    .prefault(RETRY_DEFAULT),\n});\n\ntype FormValues = z.input<typeof schema>;\n\nexport function FormRetry({\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      retry: RETRY_DEFAULT,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: \"Failed to save\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Retry Policy</FormCardTitle>\n            <FormCardDescription>\n              Configure the retry policy for your monitor.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid gap-4 sm:grid-cols-2\">\n            <FormField\n              control={form.control}\n              name=\"retry\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Retry</FormLabel>\n                  <FormControl>\n                    <Input\n                      min={RETRY_MIN}\n                      max={RETRY_MAX}\n                      step={1}\n                      type=\"number\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                  <FormDescription>\n                    The retry policy is exponential backoff.\n                  </FormDescription>\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/reference/http-monitor/#retry\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                retries\n              </Link>\n              .\n            </FormCardFooterInfo>\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-scheduling-regions.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  type Region,\n  monitorPeriodicity,\n} from \"@openstatus/db/src/schema/constants\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Slider } from \"@openstatus/ui/components/ui/slider\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useState, useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nimport { IconCloudProviderTooltip } from \"@/components/common/icon-cloud-provider\";\nimport { Note, NoteButton } from \"@/components/common/note\";\nimport { UpgradeDialog } from \"@/components/dialogs/upgrade\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  formatRegionCode,\n  groupByContinent,\n  regionDict,\n} from \"@openstatus/regions\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { CircleX, Globe, Info } from \"lucide-react\";\n\nconst DEFAULT_PERIODICITY = \"10m\";\nconst DEFAULT_REGIONS = [\"ams\", \"fra\", \"iad\", \"syd\", \"jnb\", \"gru\"];\nconst PERIODICITY = monitorPeriodicity.filter((p) => p !== \"other\");\nconst DEFAULT_PRIVATE_LOCATIONS = [] satisfies { id: number; name: string }[];\n\nconst schema = z.object({\n  regions: z.array(z.string()),\n  periodicity: z.enum(monitorPeriodicity),\n  privateLocations: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormSchedulingRegions({\n  defaultValues,\n  onSubmit,\n  privateLocations,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  privateLocations: { id: number; name: string }[];\n}) {\n  const trpc = useTRPC();\n  const [openDialog, setOpenDialog] = useState(false);\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      regions: DEFAULT_REGIONS,\n      periodicity: DEFAULT_PERIODICITY,\n      privateLocations: DEFAULT_PRIVATE_LOCATIONS,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const watchPeriodicity = form.watch(\"periodicity\");\n  const watchRegions = form.watch(\"regions\");\n  const watchPrivateLocations = form.watch(\"privateLocations\");\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            console.error(error);\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  if (!workspace) return null;\n\n  const allowedRegions = workspace.limits.regions.filter(\n    (r) => !regionDict[r as keyof typeof regionDict].deprecated,\n  );\n  const maxRegions = workspace.limits[\"max-regions\"];\n  const periodicity = workspace.limits.periodicity;\n\n  const isMaxed = watchRegions.length >= maxRegions;\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Scheduling & Regions</FormCardTitle>\n            <FormCardDescription>\n              Configure the scheduling and regions for your monitor.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid gap-4\">\n            <FormField\n              control={form.control}\n              name=\"periodicity\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Periodicity</FormLabel>\n                  <FormControl>\n                    <div>\n                      <Slider\n                        value={[monitorPeriodicity.indexOf(field.value)]}\n                        max={PERIODICITY.length - 1}\n                        aria-label=\"Slider with ticks\"\n                        onValueChange={(value) => {\n                          field.onChange(PERIODICITY[value[0]]);\n                        }}\n                        className={cn(\n                          !periodicity.includes(watchPeriodicity) &&\n                            \"[&_[data-slot=slider-range]]:bg-destructive\",\n                        )}\n                      />\n                      <span\n                        className=\"mt-3 flex w-full items-center justify-between gap-1 px-2.5 font-medium text-muted-foreground text-xs\"\n                        aria-hidden=\"true\"\n                      >\n                        {PERIODICITY.map((period) => (\n                          <span\n                            key={period}\n                            className=\"flex w-0 flex-col items-center justify-center gap-2\"\n                          >\n                            <span\n                              className={cn(\"h-1 w-px bg-muted-foreground/70\")}\n                            />\n                            {period}\n                          </span>\n                        ))}\n                      </span>\n                    </div>\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n            {!periodicity.includes(watchPeriodicity) ? (\n              <Note color=\"error\">\n                <CircleX />\n                The periodicity you are selecting is not allowed for your plan.\n                <NoteButton type=\"button\" onClick={() => setOpenDialog(true)}>\n                  Upgrade your plan\n                </NoteButton>\n              </Note>\n            ) : null}\n          </FormCardContent>\n          <FormCardSeparator />\n          <FormCardContent className=\"grid gap-4\">\n            <Note color=\"warning\">\n              <Info />\n              To minimize false positives, we recommend monitoring your endpoint\n              in at least 3 regions.\n            </Note>\n            <FormField\n              control={form.control}\n              name=\"regions\"\n              render={() => (\n                <FormItem>\n                  <FormControl>\n                    <div className=\"grid gap-4\">\n                      {Object.entries(groupByContinent).map(\n                        ([continent, r]) => {\n                          const selected = r\n                            .filter((r) => allowedRegions.includes(r.code))\n                            .reduce((prev, curr) => {\n                              return (\n                                prev +\n                                (watchRegions.includes(curr.code) ? 1 : 0)\n                              );\n                            }, 0);\n                          const isAllSelected =\n                            selected ===\n                            r.filter((r) => allowedRegions.includes(r.code))\n                              .length;\n\n                          const disabled =\n                            r.length + watchRegions.length - selected >\n                            maxRegions;\n\n                          return (\n                            <div key={continent} className=\"space-y-2\">\n                              <div className=\"flex items-center justify-between\">\n                                <FormLabel>\n                                  {continent}{\" \"}\n                                  <span className=\"align-baseline font-mono font-normal text-muted-foreground/70 text-xs tabular-nums\">\n                                    ({selected}/{r.length})\n                                  </span>\n                                </FormLabel>\n                                <Button\n                                  variant=\"ghost\"\n                                  size=\"sm\"\n                                  type=\"button\"\n                                  className={cn(\n                                    isAllSelected && \"text-muted-foreground\",\n                                  )}\n                                  disabled={disabled}\n                                  onClick={() => {\n                                    if (!isAllSelected) {\n                                      // Add all regions from this continent\n                                      const newRegions = [...watchRegions];\n                                      r.filter((r) =>\n                                        allowedRegions.includes(r.code),\n                                      ).forEach((region) => {\n                                        if (!newRegions.includes(region.code)) {\n                                          newRegions.push(region.code);\n                                        }\n                                      });\n                                      form.setValue(\"regions\", newRegions);\n                                    } else {\n                                      // Remove all regions from this continent\n                                      form.setValue(\n                                        \"regions\",\n                                        watchRegions?.filter(\n                                          (region) =>\n                                            !r\n                                              .map(({ code }) => code)\n                                              .includes(region as Region),\n                                        ),\n                                      );\n                                    }\n                                  }}\n                                >\n                                  Select all\n                                </Button>\n                              </div>\n                              <div className=\"grid grid-cols-2 gap-2\">\n                                {r.map((region) => {\n                                  return (\n                                    <FormField\n                                      key={region.code}\n                                      control={form.control}\n                                      name=\"regions\"\n                                      render={({ field }) => {\n                                        const checked = field.value?.includes(\n                                          region.code,\n                                        );\n                                        const disabled = checked\n                                          ? false\n                                          : !allowedRegions.includes(\n                                              region.code,\n                                            ) || isMaxed;\n                                        const deprecated = region.deprecated;\n                                        return (\n                                          <FormItem\n                                            key={region.code}\n                                            className=\"flex items-center\"\n                                          >\n                                            <Checkbox\n                                              id={region.code}\n                                              checked={checked || false}\n                                              disabled={\n                                                disabled ||\n                                                (deprecated && !checked)\n                                              }\n                                              onCheckedChange={(checked) => {\n                                                if (checked) {\n                                                  field.onChange([\n                                                    ...field.value,\n                                                    region.code,\n                                                  ]);\n                                                } else {\n                                                  field.onChange(\n                                                    field.value?.filter(\n                                                      (r) => r !== region.code,\n                                                    ),\n                                                  );\n                                                }\n                                              }}\n                                            />\n                                            <FormLabel\n                                              htmlFor={region.code}\n                                              className=\"w-full truncate font-mono font-normal text-sm\"\n                                            >\n                                              <span className=\"text-nowrap\">\n                                                {formatRegionCode(region.code)}{\" \"}\n                                                {region.flag}\n                                              </span>\n                                              <span className=\"truncate font-normal text-muted-foreground text-xs leading-[inherit]\">\n                                                {region.location}\n                                              </span>\n                                              <IconCloudProviderTooltip\n                                                provider={region.provider}\n                                                className=\"size-3\"\n                                              />\n                                            </FormLabel>\n                                          </FormItem>\n                                        );\n                                      }}\n                                    />\n                                  );\n                                })}\n                              </div>\n                            </div>\n                          );\n                        },\n                      )}\n                    </div>\n                  </FormControl>\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardSeparator />\n          <FormCardContent className=\"grid gap-4\">\n            {privateLocations.length === 0 ? (\n              <Note>\n                <Globe />\n                Monitor your endpoints from private locations.\n                <NoteButton variant=\"outline\" asChild>\n                  <Link href=\"/private-locations\">Learn more</Link>\n                </NoteButton>\n              </Note>\n            ) : (\n              <FormField\n                control={form.control}\n                name=\"privateLocations\"\n                render={() => (\n                  <FormItem>\n                    <div className=\"flex items-center justify-between\">\n                      <FormLabel>\n                        Private Locations{\" \"}\n                        <span className=\"align-baseline font-mono font-normal text-muted-foreground/70 text-xs tabular-nums\">\n                          ({watchPrivateLocations.length}/\n                          {privateLocations.length})\n                        </span>\n                      </FormLabel>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        type=\"button\"\n                        className={cn(\n                          watchPrivateLocations.length ===\n                            privateLocations.length && \"text-muted-foreground\",\n                        )}\n                        onClick={() => {\n                          const allSelected = privateLocations.every((item) =>\n                            watchPrivateLocations.includes(item.id),\n                          );\n\n                          if (!allSelected) {\n                            form.setValue(\n                              \"privateLocations\",\n                              privateLocations.map((item) => item.id),\n                            );\n                          } else {\n                            form.setValue(\"privateLocations\", []);\n                          }\n                        }}\n                      >\n                        Select all\n                      </Button>\n                    </div>\n                    <div className=\"grid grid-cols-2 gap-2\">\n                      {privateLocations.map((item) => (\n                        <FormField\n                          key={item.id}\n                          control={form.control}\n                          name=\"privateLocations\"\n                          render={({ field }) => {\n                            return (\n                              <FormItem\n                                key={item.id}\n                                className=\"flex items-center\"\n                              >\n                                <FormControl>\n                                  <Checkbox\n                                    checked={\n                                      field.value?.includes(item.id) || false\n                                    }\n                                    onCheckedChange={(checked) => {\n                                      return checked\n                                        ? field.onChange([\n                                            ...field.value,\n                                            item.id,\n                                          ])\n                                        : field.onChange(\n                                            field.value?.filter(\n                                              (value) => value !== item.id,\n                                            ),\n                                          );\n                                    }}\n                                  />\n                                </FormControl>\n                                <FormLabel className=\"w-full truncate font-mono font-normal text-sm\">\n                                  {item.name}\n                                  <Globe className=\"size-3\" />\n                                </FormLabel>\n                              </FormItem>\n                            );\n                          }}\n                        />\n                      ))}\n                    </div>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            )}\n          </FormCardContent>\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Your plan allows you to run{\" \"}\n              <span className=\"font-medium text-foreground\">{maxRegions}</span>{\" \"}\n              out of{\" \"}\n              <span className=\"font-medium text-foreground\">\n                {allowedRegions.length}\n              </span>{\" \"}\n              regions. Learn more about{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/reference/http-monitor/#regions\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                Regions\n              </Link>{\" \"}\n              and{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/reference/http-monitor/#frequency\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                Periodicity\n              </Link>\n              .\n            </FormCardFooterInfo>\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n      <UpgradeDialog open={openDialog} onOpenChange={setOpenDialog} />\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-status-pages.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  description: z.string().optional(),\n  externalName: z.string().optional(),\n  statusPages: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormStatusPages({\n  defaultValues,\n  onSubmit,\n  statusPages,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  statusPages: { id: number; title: string; slug: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      description: \"\",\n      externalName: \"\",\n      statusPages: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const watchStatusPages = form.watch(\"statusPages\");\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Status Pages</FormCardTitle>\n            <FormCardDescription>\n              Add status pages to your monitor and configure the external name\n              and description to be shown on the status page.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid gap-4 sm:grid-cols-3\">\n            <FormField\n              control={form.control}\n              name=\"externalName\"\n              render={({ field }) => (\n                <FormItem className=\"sm:col-span-2\">\n                  <FormLabel>External Name</FormLabel>\n                  <FormControl>\n                    <Input placeholder=\"OpenStatus API\" {...field} />\n                  </FormControl>\n                  <FormDescription>\n                    External name on the status page, the feed or subscribers\n                    notifications. If not provided, monitor&apos;s name will be\n                    used.\n                  </FormDescription>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"description\"\n              render={({ field }) => (\n                <FormItem className=\"sm:col-span-full\">\n                  <FormLabel>Description</FormLabel>\n                  <FormControl>\n                    <Input placeholder=\"My Status Page\" {...field} />\n                  </FormControl>\n                  <FormDescription>\n                    A tooltip with extra information about the monitor will be\n                    displayed.\n                  </FormDescription>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardSeparator />\n          <FormCardContent>\n            {statusPages.length > 0 ? (\n              <FormField\n                control={form.control}\n                name=\"statusPages\"\n                render={() => (\n                  <FormItem>\n                    <div className=\"flex items-center justify-between\">\n                      <FormLabel className=\"text-base\">\n                        List of Status Pages\n                      </FormLabel>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        type=\"button\"\n                        className={cn(\n                          watchStatusPages.length === statusPages.length &&\n                            \"text-muted-foreground\",\n                        )}\n                        onClick={() => {\n                          const allSelected = statusPages.every((item) =>\n                            watchStatusPages.includes(item.id),\n                          );\n\n                          if (!allSelected) {\n                            form.setValue(\n                              \"statusPages\",\n                              statusPages.map((item) => item.id),\n                            );\n                          } else {\n                            form.setValue(\"statusPages\", []);\n                          }\n                        }}\n                      >\n                        Select all\n                      </Button>\n                    </div>\n                    {statusPages.map((item) => (\n                      <FormField\n                        key={item.id}\n                        control={form.control}\n                        name=\"statusPages\"\n                        render={({ field }) => {\n                          return (\n                            <FormItem\n                              key={item.id}\n                              className=\"flex items-center\"\n                            >\n                              <FormControl>\n                                <Checkbox\n                                  checked={\n                                    field.value?.includes(item.id) || false\n                                  }\n                                  onCheckedChange={(checked) => {\n                                    return checked\n                                      ? field.onChange([\n                                          ...field.value,\n                                          item.id,\n                                        ])\n                                      : field.onChange(\n                                          field.value?.filter(\n                                            (value) => value !== item.id,\n                                          ),\n                                        );\n                                  }}\n                                />\n                              </FormControl>\n                              <FormLabel className=\"font-normal text-sm\">\n                                {item.title}{\" \"}\n                                <Badge\n                                  variant=\"secondary\"\n                                  className=\"px-1.5 py-px font-mono text-[10px]\"\n                                >\n                                  {item.slug}\n                                </Badge>\n                              </FormLabel>\n                            </FormItem>\n                          );\n                        }}\n                      />\n                    ))}\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            ) : (\n              <EmptyStateContainer>\n                <EmptyStateTitle>No status pages</EmptyStateTitle>\n              </EmptyStateContainer>\n            )}\n          </FormCardContent>\n          <FormCardFooter>\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-tags.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@openstatus/ui/components/ui/command\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport { useEffect, useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\nimport { FormSheetMonitorTag } from \"../monitor-tag/sheet\";\n\nconst schema = z.object({\n  tags: z.array(\n    z.object({\n      id: z.number(),\n      name: z.string(),\n      color: z.string(),\n    }),\n  ),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormTags({\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const trpc = useTRPC();\n  const { data: tags, refetch } = useQuery(trpc.monitorTag.list.queryOptions());\n  const syncTagsMutation = useMutation(\n    trpc.monitorTag.syncTags.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      tags: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: \"Failed to save\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  useEffect(() => {\n    // if tags name/color changed, update the form\n    if (!tags) {\n      form.setValue(\"tags\", []);\n    } else {\n      const formTags = form.getValues(\"tags\");\n      form.setValue(\n        \"tags\",\n        tags\n          ?.filter((tag) => formTags.map((t) => t.id).includes(tag.id))\n          .map((tag) => ({\n            id: tag.id,\n            name: tag.name,\n            color: tag.color,\n          })) ?? [],\n      );\n    }\n  }, [tags, form]);\n\n  if (!tags) return null;\n\n  return (\n    <Form {...form}>\n      <form\n        onSubmit={form.handleSubmit(submitAction)}\n        id=\"monitor-tags-form\"\n        {...props}\n      >\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Tags</FormCardTitle>\n            <FormCardDescription>\n              Add tags to categorize and organize your monitor.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid gap-4 md:grid-cols-2\">\n            <FormField\n              control={form.control}\n              name=\"tags\"\n              render={({ field }) => (\n                <FormItem className=\"flex flex-col md:col-span-1\">\n                  <FormLabel>Tags</FormLabel>\n                  <Popover>\n                    <PopoverTrigger asChild>\n                      <FormControl>\n                        <Button\n                          variant=\"outline\"\n                          role=\"combobox\"\n                          className={cn(\n                            \"h-auto min-h-9 w-full justify-between\",\n                            !field.value?.length && \"text-muted-foreground\",\n                          )}\n                        >\n                          <div className=\"group/badges -space-x-2 flex flex-wrap\">\n                            {field.value.length ? (\n                              field.value.map((tag) => (\n                                <Badge\n                                  key={tag.id}\n                                  variant=\"outline\"\n                                  className=\"relative flex translate-x-0 items-center gap-1.5 rounded-full bg-background transition-transform hover:z-10 hover:translate-x-1\"\n                                >\n                                  <div\n                                    className={cn(\"size-2.5 rounded-full\")}\n                                    style={{ backgroundColor: tag.color }}\n                                  />\n                                  {tag.name}\n                                </Badge>\n                              ))\n                            ) : (\n                              <span className=\"text-muted-foreground\">\n                                No tags selected\n                              </span>\n                            )}\n                          </div>\n                          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n                        </Button>\n                      </FormControl>\n                    </PopoverTrigger>\n                    <PopoverContent className=\"w-[var(--radix-popover-trigger-width)] p-0\">\n                      <Command>\n                        <CommandInput placeholder=\"Search tags...\" />\n                        <CommandList className=\"w-full\">\n                          <CommandEmpty>No tag found.</CommandEmpty>\n                          <CommandGroup>\n                            {tags?.map((tag) => (\n                              <CommandItem\n                                value={tag.name}\n                                key={tag.id}\n                                onSelect={() => {\n                                  if (\n                                    field.value\n                                      .map((tag) => tag.id)\n                                      ?.includes(tag.id)\n                                  ) {\n                                    form.setValue(\n                                      \"tags\",\n                                      field.value.filter(\n                                        (value) => value.id !== tag.id,\n                                      ),\n                                    );\n                                  } else {\n                                    form.setValue(\"tags\", [\n                                      ...(field.value ?? []),\n                                      {\n                                        id: tag.id,\n                                        name: tag.name,\n                                        color: tag.color,\n                                      },\n                                    ]);\n                                  }\n                                }}\n                              >\n                                <div\n                                  className={cn(\"mr-2 h-4 w-4 rounded-full\")}\n                                  style={{ backgroundColor: tag.color }}\n                                />\n                                {tag.name}\n                                <Check\n                                  className={cn(\n                                    \"ml-auto h-4 w-4\",\n                                    field.value\n                                      ?.map((tag) => tag.id)\n                                      ?.includes(tag.id)\n                                      ? \"opacity-100\"\n                                      : \"opacity-0\",\n                                  )}\n                                />\n                              </CommandItem>\n                            ))}\n                          </CommandGroup>\n                        </CommandList>\n                      </Command>\n                    </PopoverContent>\n                  </Popover>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n            <div className=\"flex items-end\">\n              <FormSheetMonitorTag\n                onSubmit={async (values) => {\n                  await syncTagsMutation.mutateAsync(values.tags);\n                }}\n                defaultValues={{\n                  tags: tags.map((tag) => ({\n                    id: tag.id,\n                    name: tag.name,\n                    color: tag.color,\n                  })),\n                }}\n              >\n                <Button variant=\"outline\" size=\"sm\">\n                  Edit Tags\n                </Button>\n              </FormSheetMonitorTag>\n            </div>\n          </FormCardContent>\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about{\" \"}\n              <Link\n                href=\"https://www.openstatus.dev/changelog/monitor-tags\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                tags\n              </Link>{\" \"}\n              and how to use them.\n            </FormCardFooterInfo>\n            <Button type=\"submit\" form=\"monitor-tags-form\" disabled={isPending}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/form-visibility.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n  FormCardUpgrade,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Switch } from \"@openstatus/ui/components/ui/switch\";\nimport { Lock } from \"lucide-react\";\nimport NextLink from \"next/link\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  visibility: z.boolean(),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormVisibility({\n  locked,\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  locked?: boolean;\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      visibility: false,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: \"Failed to save\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          {locked ? <FormCardUpgrade /> : null}\n          <FormCardHeader>\n            <FormCardTitle>Visibility</FormCardTitle>\n            <FormCardDescription>\n              Share your monitor stats with the public.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent>\n            <FormField\n              control={form.control}\n              name=\"visibility\"\n              disabled={locked}\n              render={({ field }) => (\n                <FormItem className=\"flex flex-row items-center justify-between\">\n                  <div className=\"space-y-0.5\">\n                    <FormLabel>Allow public access</FormLabel>\n                    <FormDescription>\n                      Change monitor visibility. The monitor stats will be\n                      attached to the status page the monitor is connected to.\n                    </FormDescription>\n                  </div>\n                  <FormControl>\n                    <Switch\n                      checked={field.value}\n                      onCheckedChange={field.onChange}\n                      disabled={locked}\n                    />\n                  </FormControl>\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/reference/http-monitor/#public\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                monitor visibility\n              </Link>\n              .\n            </FormCardFooterInfo>\n            {locked ? (\n              <Button asChild>\n                <NextLink href=\"/settings/billing\">\n                  <Lock className=\"size-4\" />\n                  Upgrade\n                </NextLink>\n              </Button>\n            ) : (\n              <Button type=\"submit\" disabled={isPending}>\n                {isPending ? \"Submitting...\" : \"Submit\"}\n              </Button>\n            )}\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor/update.tsx",
    "content": "\"use client\";\n\nimport { FormCardGroup } from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { deserialize } from \"@openstatus/assertions\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useParams, useRouter } from \"next/navigation\";\nimport { FormDangerZone } from \"./form-danger-zone\";\nimport {\n  FOLLOW_REDIRECTS_DEFAULT,\n  FormFollowRedirect,\n} from \"./form-follow-redirect\";\nimport { FormGeneral } from \"./form-general\";\nimport { FormNotifiers } from \"./form-notifiers\";\nimport { FormOtel } from \"./form-otel\";\nimport { FormResponseTime } from \"./form-response-time\";\nimport { FormRetry, RETRY_DEFAULT } from \"./form-retry\";\nimport { FormSchedulingRegions } from \"./form-scheduling-regions\";\nimport { FormTags } from \"./form-tags\";\nimport { FormVisibility } from \"./form-visibility\";\n\nexport function FormMonitorUpdate() {\n  const { id } = useParams<{ id: string }>();\n  const trpc = useTRPC();\n  const router = useRouter();\n  const queryClient = useQueryClient();\n  const { data: monitor, refetch } = useQuery(\n    trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  const { data: statusPages } = useQuery(trpc.page.list.queryOptions());\n  const { data: privateLocations } = useQuery(\n    trpc.privateLocation.list.queryOptions(),\n  );\n  const { data: notifications } = useQuery(\n    trpc.notification.list.queryOptions(),\n  );\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const updateRetryMutation = useMutation(\n    trpc.monitor.updateRetry.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n  const updateOtelMutation = useMutation(\n    trpc.monitor.updateOtel.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n  const updatePublicMutation = useMutation(\n    trpc.monitor.updatePublic.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n  const updateSchedulingRegionsMutation = useMutation(\n    trpc.monitor.updateSchedulingRegions.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n  const updateResponseTimeMutation = useMutation(\n    trpc.monitor.updateResponseTime.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n  const updateTagsMutation = useMutation(\n    trpc.monitor.updateTags.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n  const updateFollowRedirectsMutation = useMutation(\n    trpc.monitor.updateFollowRedirects.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  const updateGeneralMutation = useMutation(\n    trpc.monitor.updateGeneral.mutationOptions({\n      onSuccess: () => {\n        // NOTE: invalidate the list query to update the monitor in the list (especially the name)\n        queryClient.invalidateQueries({\n          queryKey: trpc.monitor.list.queryKey(),\n        });\n        refetch();\n      },\n      onError: (err) => {\n        // TODO: open dialog\n        console.error(err);\n      },\n    }),\n  );\n\n  const updateNotifiersMutation = useMutation(\n    trpc.monitor.updateNotifiers.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  const deleteMonitorMutation = useMutation(\n    trpc.monitor.delete.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: trpc.monitor.list.queryKey(),\n        });\n        router.push(\"/monitors\");\n      },\n    }),\n  );\n\n  if (\n    !monitor ||\n    !statusPages ||\n    !notifications ||\n    !workspace ||\n    !privateLocations\n  )\n    return null;\n\n  return (\n    <FormCardGroup>\n      <FormGeneral\n        defaultValues={{\n          type: monitor.jobType as \"http\" | \"tcp\",\n          url: monitor.url,\n          name: monitor.name,\n          method: monitor.method as \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\",\n          headers: monitor.headers ?? [],\n          body: monitor.body,\n          active: monitor.active ?? true,\n          // TODO: move to server after migration\n          assertions: monitor?.assertions\n            ? deserialize(monitor?.assertions).map((a) => a.schema)\n            : [],\n          skipCheck: false,\n          saveCheck: false,\n        }}\n        onSubmit={async (values) => {\n          await updateGeneralMutation.mutateAsync({\n            id: Number.parseInt(id),\n            name: values.name,\n            jobType: values.type,\n            url: values.url,\n            method: values.method,\n            headers: values.headers,\n            body: values.body,\n            assertions: values.assertions,\n            skipCheck: values.skipCheck,\n            saveCheck: values.saveCheck,\n            active: values.active,\n          });\n        }}\n      />\n      <FormResponseTime\n        defaultValues={{\n          timeout: monitor.timeout,\n          degradedAfter: monitor.degradedAfter ?? undefined,\n        }}\n        onSubmit={async (values) => {\n          await updateResponseTimeMutation.mutateAsync({\n            id: Number.parseInt(id),\n            timeout: values.timeout,\n            degradedAfter: values.degradedAfter ?? undefined,\n          });\n        }}\n      />\n      <FormTags\n        defaultValues={{\n          tags: monitor.tags,\n        }}\n        onSubmit={async (values) => {\n          await updateTagsMutation.mutateAsync({\n            id: Number.parseInt(id),\n            tags: values.tags.map((tag) => tag.id),\n          });\n        }}\n      />\n      <FormSchedulingRegions\n        privateLocations={privateLocations}\n        defaultValues={{\n          regions: monitor.regions,\n          periodicity: monitor.periodicity,\n          privateLocations: monitor.privateLocations.map(({ id }) => id),\n        }}\n        onSubmit={async (values) => {\n          await updateSchedulingRegionsMutation.mutateAsync({\n            id: Number.parseInt(id),\n            regions: values.regions,\n            periodicity: values.periodicity,\n            privateLocations: values.privateLocations,\n          });\n        }}\n      />\n      <FormNotifiers\n        notifiers={notifications}\n        defaultValues={{\n          notifiers: monitor.notifications.map(({ id }) => id),\n        }}\n        onSubmit={async (values) => {\n          await updateNotifiersMutation.mutateAsync({\n            id: Number.parseInt(id),\n            notifiers: values.notifiers,\n          });\n        }}\n      />\n      <FormRetry\n        defaultValues={{\n          retry: monitor.retry ?? RETRY_DEFAULT,\n        }}\n        onSubmit={async (values) =>\n          await updateRetryMutation.mutateAsync({\n            id: Number.parseInt(id),\n            retry: values.retry ?? RETRY_DEFAULT,\n          })\n        }\n      />\n      <FormFollowRedirect\n        defaultValues={{\n          followRedirects: monitor.followRedirects ?? FOLLOW_REDIRECTS_DEFAULT,\n        }}\n        onSubmit={async (values) =>\n          await updateFollowRedirectsMutation.mutateAsync({\n            id: Number.parseInt(id),\n            followRedirects: values.followRedirects ?? FOLLOW_REDIRECTS_DEFAULT,\n          })\n        }\n      />\n      <FormOtel\n        locked={workspace.limits.otel === false}\n        defaultValues={{\n          endpoint: monitor.otelEndpoint ?? \"\",\n          headers: monitor.otelHeaders ?? [],\n        }}\n        onSubmit={async (values) => {\n          await updateOtelMutation.mutateAsync({\n            id: Number.parseInt(id),\n            otelEndpoint: values.endpoint,\n            otelHeaders: values.headers,\n          });\n        }}\n      />\n      <FormVisibility\n        defaultValues={{\n          visibility: monitor.public ?? false,\n        }}\n        onSubmit={async (values) => {\n          await updatePublicMutation.mutateAsync({\n            id: Number.parseInt(id),\n            public: values.visibility,\n          });\n        }}\n      />\n      <FormDangerZone\n        title={monitor.name}\n        onSubmit={async () => {\n          await deleteMonitorMutation.mutateAsync({ id: Number.parseInt(id) });\n        }}\n      />\n    </FormCardGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor-tag/form-monitor-tag.tsx",
    "content": "\"use client\";\n\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Plus, Trash2 } from \"lucide-react\";\nimport React, { useTransition } from \"react\";\nimport { useFieldArray, useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst tagSchema = z.object({\n  id: z.number().optional(),\n  name: z.string().min(1, \"Name is required\"),\n  color: z.string().regex(/^#[0-9A-Fa-f]{6}$/, \"Invalid color format\"),\n});\n\nconst schema = z.object({\n  tags: z.array(tagSchema),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\n// FIXME: rename, its not monitor specfic, its all the tags\nexport function FormMonitorTag({\n  defaultValues,\n  className,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const trpc = useTRPC();\n  const { data: tags } = useQuery(trpc.monitorTag.list.queryOptions());\n\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      tags: [],\n    },\n  });\n\n  const { fields, append, remove } = useFieldArray({\n    control: form.control,\n    name: \"tags\",\n  });\n\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving tags...\",\n          success: \"Tags saved successfully\",\n          error: \"Failed to save tags\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  if (!tags) return null;\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={(e) => {\n          // NOTE: we use the form nested within another form, so we need to prevent the default behavior\n          // and stop the propagation to avoid double submission\n          e.preventDefault();\n          e.stopPropagation();\n          form.handleSubmit(submitAction)(e);\n        }}\n        {...props}\n      >\n        <div className=\"space-y-4\">\n          <div className=\"flex items-center justify-between\">\n            <FormLabel>Tags</FormLabel>\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"sm\"\n              onClick={() => append({ name: \"\", color: \"#00008B\" })}\n            >\n              <Plus className=\"mr-2 h-4 w-4\" />\n              Add Tag\n            </Button>\n          </div>\n\n          {fields.map((field, index) => (\n            <div key={field.id} className=\"flex items-start gap-4\">\n              <FormField\n                control={form.control}\n                name={`tags.${index}.color`}\n                render={({ field }) => (\n                  <FormItem className=\"p-1\">\n                    <FormControl>\n                      <Input\n                        type=\"color\"\n                        className=\"size-7 overflow-hidden rounded-full p-0\"\n                        style={{ backgroundColor: field.value }}\n                        {...field}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name={`tags.${index}.name`}\n                render={({ field }) => (\n                  <FormItem className=\"flex-1\">\n                    <FormControl>\n                      <Input placeholder=\"Tag name\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon\"\n                onClick={() => remove(index)}\n              >\n                <Trash2 className=\"h-4 w-4\" />\n              </Button>\n            </div>\n          ))}\n        </div>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/monitor-tag/sheet.tsx",
    "content": "\"use client\";\n\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardGroup,\n} from \"@/components/forms/form-card\";\nimport {\n  FormSheetContent,\n  FormSheetDescription,\n  FormSheetFooter,\n  FormSheetHeader,\n  FormSheetTitle,\n  FormSheetTrigger,\n  FormSheetWithDirtyProtection,\n} from \"@/components/forms/form-sheet\";\nimport {\n  FormMonitorTag,\n  type FormValues,\n} from \"@/components/forms/monitor-tag/form-monitor-tag\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useState } from \"react\";\n\nexport function FormSheetMonitorTag({\n  children,\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<typeof FormSheetTrigger>, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const [open, setOpen] = useState(false);\n\n  return (\n    <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}>\n      <FormSheetTrigger {...props} asChild>\n        {children}\n      </FormSheetTrigger>\n      <FormSheetContent>\n        <FormSheetHeader>\n          <FormSheetTitle>Monitor Tag</FormSheetTitle>\n          <FormSheetDescription>\n            Configure and update the monitor tag.\n          </FormSheetDescription>\n        </FormSheetHeader>\n        <FormCardGroup className=\"flex-1 overflow-y-auto\">\n          <FormCard className=\"flex-1 overflow-auto rounded-none border-none\">\n            <FormCardContent>\n              <FormMonitorTag\n                onSubmit={onSubmit}\n                defaultValues={defaultValues}\n                id=\"tags-form\"\n                className=\"my-4\"\n              />\n            </FormCardContent>\n          </FormCard>\n        </FormCardGroup>\n        <FormSheetFooter>\n          <Button type=\"submit\" form=\"tags-form\">\n            Submit\n          </Button>\n        </FormSheetFooter>\n      </FormSheetContent>\n    </FormSheetWithDirtyProtection>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-discord.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { config } from \"@/data/notifications.client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"discord\"),\n  data: z.url(\"Please enter a valid URL\"),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormDiscord({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"discord\",\n      data: \"\",\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data\");\n        const promise = config[provider].sendTest(data);\n        toast.promise(promise, {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: (error) => {\n            if (error instanceof Error) {\n              return error.message;\n            }\n            return \"Failed to send test\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Webhook URL</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"https://example.com/webhook\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter the webhook URL to your Discord channel.{\" \"}\n                  <Link\n                    href=\"https://docs.openstatus.dev/reference/notification/#discord\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                  >\n                    Read more\n                  </Link>\n                  .\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              Send Test\n            </Button>\n          </div>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-email.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"email\"),\n  data: z.email(),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormEmail({\n  monitors,\n  defaultValues,\n  onSubmit,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"email\",\n      data: \"\",\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Email</FormLabel>\n                <FormControl>\n                  <Input\n                    placeholder=\"max@openstatus.dev\"\n                    type=\"email\"\n                    {...field}\n                  />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter the email address to send notifications to.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-google-chat.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"google-chat\"),\n  data: z.url(\"Please enter a valid URL\"),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormGoogleChat({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"google-chat\",\n      data: \"\",\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n  const trpc = useTRPC();\n\n  const sendTestMutation = useMutation(\n    trpc.notification.sendTest.mutationOptions(),\n  );\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data\");\n        const promise = sendTestMutation.mutateAsync({\n          provider,\n          data: {\n            \"google-chat\": data,\n          },\n        });\n        toast.promise(promise, {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: (error) => {\n            if (error instanceof Error) {\n              return error.message;\n            }\n            return \"Failed to send test\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Google Chat Webhook</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"https://...\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter the phone number to send notifications to.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              Send Test\n            </Button>\n          </div>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-grafana-oncall.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport React from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\nimport { useFormSheetDirty } from \"../form-sheet\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"grafana-oncall\"),\n  data: z.record(z.string(), z.string()),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormGrafanaOncall({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"grafana-oncall\",\n      data: {\n        webhookUrl: \"\",\n      },\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const trpc = useTRPC();\n\n  const sendTestMutation = useMutation(\n    trpc.notification.sendTest.mutationOptions(),\n  );\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data\");\n        const promise = sendTestMutation.mutateAsync({\n          provider,\n          data: {\n            \"grafana-oncall\": data,\n          },\n        });\n        toast.promise(promise, {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: (error) => {\n            if (error instanceof Error) {\n              return error.message;\n            }\n            return \"Failed to send test\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data.webhookUrl\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Webhook URL</FormLabel>\n                <FormControl>\n                  <Input\n                    placeholder=\"https://oncall-prod-us-central-0.grafana.net/oncall/integrations/v1/webhook/...\"\n                    {...field}\n                  />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter your Grafana OnCall incoming webhook URL. You can find\n                  this in your Grafana OnCall integration settings.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              Send Test\n            </Button>\n          </div>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-ntfy.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { config } from \"@/data/notifications.client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"ntfy\"),\n  data: z.record(z.string(), z.string()),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormNtfy({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"ntfy\",\n      data: {\n        topic: \"\",\n        serverUrl: \"\",\n        token: \"\",\n      },\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data\");\n        const promise = config[provider].sendTest(\n          data as unknown as {\n            topic: string;\n            serverUrl?: string;\n            token?: string;\n          },\n        );\n        toast.promise(promise, {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: (error) => {\n            if (error instanceof Error) {\n              return error.message;\n            }\n            return \"Failed to send test\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data.topic\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Topic</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"your-topic\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter the topic for your ntfy notifications.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data.serverUrl\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Server URL</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"https://ntfy.sh\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter the ntfy server URL. Leave empty for default.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data.token\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Bearer Token</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"tk_iloveopenstatus\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter the bearer token for authentication.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              Send Test\n            </Button>\n          </div>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-opsgenie.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { config } from \"@/data/notifications.client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"opsgenie\"),\n  data: z.record(z.string(), z.string()),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormOpsGenie({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"opsgenie\",\n      data: {\n        apiKey: \"\",\n        region: undefined,\n      },\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data\");\n        const promise = config[provider].sendTest(\n          data as unknown as {\n            apiKey: string;\n            region: \"eu\" | \"us\";\n          },\n        );\n        toast.promise(promise, {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: (error) => {\n            if (error instanceof Error) {\n              return error.message;\n            }\n            return \"Failed to send test\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data.apiKey\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>API Key</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"your-api-key\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>Enter your OpsGenie API key.</FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data.region\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Region</FormLabel>\n                <Select\n                  onValueChange={field.onChange}\n                  defaultValue={field.value}\n                >\n                  <FormControl>\n                    <SelectTrigger>\n                      <SelectValue placeholder=\"Select a region\" />\n                    </SelectTrigger>\n                  </FormControl>\n                  <SelectContent>\n                    <SelectItem value=\"us\">US</SelectItem>\n                    <SelectItem value=\"eu\">EU</SelectItem>\n                  </SelectContent>\n                </Select>\n                <FormMessage />\n                <FormDescription>Select your OpsGenie region.</FormDescription>\n              </FormItem>\n            )}\n          />\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              Send Test\n            </Button>\n          </div>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-pagerduty.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { config } from \"@/data/notifications.client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { PagerDutySchema } from \"@openstatus/notification-pagerduty\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { parseAsString, useQueryState } from \"nuqs\";\nimport React, { useEffect, useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"pagerduty\"),\n  data: z.string(),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormPagerDuty({\n  monitors,\n  defaultValues,\n  onSubmit,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const [searchConfig] = useQueryState(\"config\", parseAsString);\n  console.log(searchConfig);\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"pagerduty\",\n      data: \"\",\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  useEffect(() => {\n    if (searchConfig) {\n      const data = PagerDutySchema.safeParse(JSON.parse(searchConfig));\n      if (data.success) {\n        form.setValue(\"data\", JSON.stringify(data.data));\n      } else {\n        toast.error(\"Invalid PagerDuty configuration\");\n      }\n    }\n  }, [searchConfig, form]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data\");\n        if (!data) {\n          toast.error(\"No PagerDuty configuration found\");\n          return;\n        }\n        const validation = PagerDutySchema.safeParse(JSON.parse(data));\n        if (!validation.success) {\n          toast.error(\"Invalid PagerDuty configuration\");\n          return;\n        }\n        const promise = config[provider].sendTest({\n          integrationKey: validation.data.integration_keys[0].integration_key,\n        });\n        toast.promise(promise, {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: (error) => {\n            if (error instanceof Error) {\n              return error.message;\n            }\n            return \"Failed to send test\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Config</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"...\" disabled {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  The PagerDuty configuration that is being used.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              Send Test\n            </Button>\n          </div>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-slack.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { config } from \"@/data/notifications.client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"slack\"),\n  data: z.url(\"Please enter a valid URL\"),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormSlack({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"slack\",\n      data: \"\",\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data\");\n        const promise = config[provider].sendTest(data);\n        toast.promise(promise, {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: (error) => {\n            if (error instanceof Error) {\n              return error.message;\n            }\n            return \"Failed to send test\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Webhook URL</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"https://example.com/webhook\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter the webhook URL to your Slack channel.{\" \"}\n                  <Link\n                    href=\"https://docs.openstatus.dev/reference/notification/#slack\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                  >\n                    Read more\n                  </Link>\n                  .\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              Send Test\n            </Button>\n          </div>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-sms.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"sms\"),\n  data: z.string(),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormSms({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"sms\",\n      data: \"\",\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => JSON.stringify(values),\n          error: \"Failed to save\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>SMS</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"+1234567890\" type=\"tel\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter the phone number to send notifications to.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-telegram.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\nimport { TelegramConnectionFlow } from \"../components/telegram-connection-flow\";\nimport { TelegramFormActions } from \"../components/telegram-form-actions\";\nimport { TelegramManualInput } from \"../components/telegram-manual-input\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"telegram\"),\n  data: z.object({\n    chatId: z.string(),\n  }),\n  monitors: z.array(z.number()),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function FormTelegram({\n  monitors,\n  defaultValues,\n  onSubmit,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"telegram\",\n      data: {\n        chatId: \"\",\n      },\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  // Check if we're editing an existing notification (has chatID) or creating a new one\n  const isEditMode = React.useMemo(() => {\n    return Boolean(defaultValues?.data?.chatId);\n  }, [defaultValues]);\n\n  const [mode, setMode] = React.useState<\"qr\" | \"manual\" | null>(\n    isEditMode ? null : \"qr\",\n  );\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n\n        // Reset UI state after successful submission\n        setMode(null);\n        form.reset();\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        id=\"notifier-form-telegram\"\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <div className=\"flex flex-col gap-4\">\n            {isEditMode ? (\n              // Edit mode: Show editable chatID input only\n              <TelegramManualInput form={form} />\n            ) : (\n              // Create mode: Show QR/manual connection flow\n              <TelegramConnectionFlow\n                form={form}\n                mode={mode}\n                onModeChange={setMode}\n              />\n            )}\n          </div>\n          <TelegramFormActions form={form} isPending={isPending} />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-webhook.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { config } from \"@/data/notifications.client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"webhook\"),\n  data: z.record(z.string(), z.string()),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormWebhook({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"webhook\",\n      data: {\n        endpoint: \"\",\n        // headers: []\n      },\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data.endpoint\");\n        toast.promise(config[provider].sendTest({ url: data }), {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: \"Failed to send test\",\n        });\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data.endpoint\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Webhook URL</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"https://example.com/webhook\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Send notifications to a custom webhook URL.{\" \"}\n                  <Link\n                    href=\"https://docs.openstatus.dev/reference/notification/#webhook\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                  >\n                    Read more\n                  </Link>\n                  .\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              Send Test\n            </Button>\n          </div>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form-whatsapp.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.literal(\"whatsapp\"),\n  data: z.string(),\n  monitors: z.array(z.number()),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormWhatsApp({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      provider: \"whatsapp\",\n      data: \"\",\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n  const trpc = useTRPC();\n\n  const sendTestMutation = useMutation(\n    trpc.notification.sendTest.mutationOptions(),\n  );\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  function testAction() {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const provider = form.getValues(\"provider\");\n        const data = form.getValues(\"data\");\n        const promise = sendTestMutation.mutateAsync({\n          provider,\n          data: {\n            whatsapp: data,\n          },\n        });\n        toast.promise(promise, {\n          loading: \"Sending test...\",\n          success: \"Test sent\",\n          error: (error) => {\n            if (error instanceof Error) {\n              return error.message;\n            }\n            return \"Failed to send test\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent className=\"grid gap-4\">\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Notifier\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter a descriptive name for your notifier.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"data\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>WhatsApp</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"+1234567890\" type=\"tel\" {...field} />\n                </FormControl>\n                <FormMessage />\n                <FormDescription>\n                  Enter the phone number to send notifications to.\n                </FormDescription>\n              </FormItem>\n            )}\n          />\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={testAction}\n            >\n              Send Test\n            </Button>\n          </div>\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Select the monitors you want to notify.\n                </FormDescription>\n                <div className=\"grid gap-3\">\n                  <div className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id=\"all\"\n                        checked={field.value?.length === monitors.length}\n                        onCheckedChange={(checked) => {\n                          field.onChange(\n                            checked ? monitors.map((m) => m.id) : [],\n                          );\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor=\"all\">Select all</Label>\n                  </div>\n                  {monitors.map((item) => (\n                    <div key={item.id} className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id={String(item.id)}\n                          checked={field.value?.includes(item.id)}\n                          onCheckedChange={(checked) => {\n                            const newValue = checked\n                              ? [...(field.value || []), item.id]\n                              : field.value?.filter((id) => id !== item.id);\n                            field.onChange(newValue);\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor={String(item.id)}>{item.name}</Label>\n                    </div>\n                  ))}\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/form.tsx",
    "content": "\"use client\";\n\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n  provider: z.enum([\n    \"slack\",\n    \"discord\",\n    \"email\",\n    \"sms\",\n    \"webhook\",\n    \"opsgenie\",\n    \"pagerduty\",\n    \"ntfy\",\n    \"telegram\",\n    \"whatsapp\",\n    \"google-chat\",\n    \"grafana-oncall\",\n  ]),\n  data: z.record(z.string(), z.string()).or(z.string()),\n  monitors: z.array(z.number()),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function NotifierForm({\n  defaultValues,\n  className,\n  onSubmit,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit?: (values: FormValues) => Promise<void> | void;\n  monitors: { id: number; name: string }[];\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      data: {\n        webhook: \"\",\n      },\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = new Promise((resolve) => setTimeout(resolve, 1000));\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => JSON.stringify(values),\n          error: \"Failed to save\",\n        });\n        await promise;\n        onSubmit?.(values);\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        id=\"notifier-form\"\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormField\n          control={form.control}\n          name=\"name\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Name</FormLabel>\n              <FormControl>\n                <Input placeholder=\"My Notifier\" {...field} />\n              </FormControl>\n              <FormMessage />\n              <FormDescription>\n                Enter a descriptive name for your notifier.\n              </FormDescription>\n            </FormItem>\n          )}\n        />\n        <FormField\n          control={form.control}\n          name=\"data.webhook\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Webhook URL</FormLabel>\n              <FormControl>\n                <Input placeholder=\"https://example.com/webhook\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n        <FormField\n          control={form.control}\n          name=\"monitors\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Monitors</FormLabel>\n              <FormDescription>\n                Select the monitors you want to notify.\n              </FormDescription>\n              <div className=\"grid gap-3\">\n                <div className=\"flex items-center gap-2\">\n                  <FormControl>\n                    <Checkbox\n                      id=\"all\"\n                      checked={field.value?.length === monitors.length}\n                      onCheckedChange={(checked) => {\n                        field.onChange(\n                          checked ? monitors.map((m) => m.id) : [],\n                        );\n                      }}\n                    />\n                  </FormControl>\n                  <Label htmlFor=\"all\">Select all</Label>\n                </div>\n                {monitors.map((item) => (\n                  <div key={item.id} className=\"flex items-center gap-2\">\n                    <FormControl>\n                      <Checkbox\n                        id={String(item.id)}\n                        checked={field.value?.includes(item.id)}\n                        onCheckedChange={(checked) => {\n                          const newValue = checked\n                            ? [...(field.value || []), item.id]\n                            : field.value?.filter((id) => id !== item.id);\n                          field.onChange(newValue);\n                        }}\n                      />\n                    </FormControl>\n                    <Label htmlFor={String(item.id)}>{item.name}</Label>\n                  </div>\n                ))}\n              </div>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/notifications/sheet.tsx",
    "content": "\"use client\";\n\nimport { FormCard, FormCardGroup } from \"@/components/forms/form-card\";\nimport {\n  FormSheetContent,\n  FormSheetDescription,\n  FormSheetFooter,\n  FormSheetHeader,\n  FormSheetTitle,\n  FormSheetTrigger,\n  FormSheetWithDirtyProtection,\n} from \"@/components/forms/form-sheet\";\nimport { config } from \"@/data/notifications.client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useState } from \"react\";\nimport type { FormValues } from \"./form\";\n\nexport function FormSheetNotifier({\n  children,\n  defaultValues,\n  provider,\n  onSubmit,\n  monitors,\n  defaultOpen,\n  ...props\n}: Omit<React.ComponentProps<typeof FormSheetTrigger>, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  provider: FormValues[\"provider\"];\n  onSubmit?: (values: FormValues) => Promise<void>;\n  monitors: { id: number; name: string }[];\n  defaultOpen?: boolean;\n}) {\n  const [open, setOpen] = useState(defaultOpen ?? false);\n  const Form = provider ? config[provider].form : undefined;\n\n  return (\n    <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}>\n      <FormSheetTrigger {...props} asChild>\n        {children}\n      </FormSheetTrigger>\n      <FormSheetContent className=\"sm:max-w-lg\">\n        <FormSheetHeader>\n          <FormSheetTitle>Notifier</FormSheetTitle>\n          <FormSheetDescription>\n            Configure and update the notifier.\n          </FormSheetDescription>\n        </FormSheetHeader>\n        <FormCardGroup className=\"overflow-y-auto\">\n          <FormCard className=\"overflow-auto rounded-none border-none\">\n            {Form && (\n              <Form\n                id={`notifier-form-${provider}`}\n                className=\"my-4\"\n                onSubmit={async (values) => {\n                  await onSubmit?.(values);\n                  setOpen(false);\n                }}\n                // @ts-expect-error - defaultValues is not defined in the form component\n                defaultValues={\n                  defaultValues\n                    ? {\n                        ...defaultValues,\n                        data:\n                          typeof defaultValues?.data === \"string\"\n                            ? defaultValues?.data\n                            : defaultValues?.data &&\n                                typeof defaultValues.data === \"object\" &&\n                                provider in defaultValues.data\n                              ? defaultValues.data[provider]\n                              : defaultValues?.data,\n                      }\n                    : undefined\n                }\n                monitors={monitors}\n              />\n            )}\n          </FormCard>\n        </FormCardGroup>\n        <FormSheetFooter>\n          <Button type=\"submit\" form={`notifier-form-${provider}`}>\n            Submit\n          </Button>\n        </FormSheetFooter>\n      </FormSheetContent>\n    </FormSheetWithDirtyProtection>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/onboarding/create-monitor.tsx",
    "content": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  url: z.url(),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function CreateMonitorForm({\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      url: \"\",\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            console.error(error);\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormField\n          control={form.control}\n          name=\"url\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>URL</FormLabel>\n              <FormControl>\n                <Input placeholder=\"https://api.openstatus.dev\" {...field} />\n              </FormControl>\n              <FormMessage />\n              <FormDescription>\n                Enter the URL of your API or website.\n              </FormDescription>\n            </FormItem>\n          )}\n        />\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/onboarding/create-page.tsx",
    "content": "\"use client\";\n\n// FIXME: use input-group instead\nimport { InputWithAddons } from \"@/components/common/input-with-addons\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { useDebounce } from \"@openstatus/ui/hooks/use-debounce\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useEffect, useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst SLUG_UNIQUE_ERROR_MESSAGE =\n  \"This slug is already taken. Please choose another one.\";\n\nconst schema = z.object({\n  slug: z.string().min(3),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function CreatePageForm({\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const trpc = useTRPC();\n  const form = useForm({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? { slug: \"\" },\n  });\n  const [isPending, startTransition] = useTransition();\n  const watchSlug = form.watch(\"slug\");\n  const debouncedSlug = useDebounce(watchSlug, 500);\n  const { data: isUnique } = useQuery(\n    trpc.page.getSlugUniqueness.queryOptions(\n      { slug: debouncedSlug },\n      { enabled: debouncedSlug.length > 0 },\n    ),\n  );\n\n  useEffect(() => {\n    if (isUnique === false) {\n      form.setError(\"slug\", { message: SLUG_UNIQUE_ERROR_MESSAGE });\n    } else {\n      form.clearErrors(\"slug\");\n    }\n  }, [isUnique, form]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        if (isUnique === false) {\n          toast.error(SLUG_UNIQUE_ERROR_MESSAGE);\n          form.setError(\"slug\", { message: SLUG_UNIQUE_ERROR_MESSAGE });\n          return;\n        }\n\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            console.error(error);\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormField\n          control={form.control}\n          name=\"slug\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Slug</FormLabel>\n              <FormControl>\n                <InputWithAddons\n                  placeholder=\"status\"\n                  trailing=\".openstatus.dev\"\n                  {...field}\n                />\n              </FormControl>\n              <FormMessage />\n              <FormDescription>\n                Choose a unique subdomain for your status page (minimum 3\n                characters).\n              </FormDescription>\n            </FormItem>\n          )}\n        />\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/onboarding/learn-from.tsx",
    "content": "\"use client\";\n\nimport { Note } from \"@/components/common/note\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  RadioGroup,\n  RadioGroupItem,\n} from \"@openstatus/ui/components/ui/radio-group\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Check } from \"lucide-react\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst wantFrom = [\n  {\n    id: \"statuspage\",\n    title: \"Status Page\",\n  },\n  {\n    id: \"uptime-monitoring\",\n    title: \"Uptime Monitoring\",\n  },\n  {\n    id: \"both\",\n    title: \"Both\",\n  },\n  {\n    id: \"other\",\n    title: \"Other\",\n  },\n] as const;\n\nconst schema = z.object({\n  from: z.string(),\n  other: z.string().optional(),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function LearnFromForm({\n  onSubmit,\n  defaultValues,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const [isPending, startTransition] = useTransition();\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      from: \"\",\n      other: \"\",\n    },\n  });\n  const watchFrom = form.watch(\"from\");\n\n  function handleSubmit(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Submitting...\",\n          success: () => \"Submitted\",\n          error: \"Failed to submit\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  if (!isPending && form.formState.isSubmitSuccessful) {\n    return (\n      <Note color=\"success\">\n        <Check />\n        Thank you for your feedback!\n      </Note>\n    );\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        onSubmit={form.handleSubmit(handleSubmit)}\n        className={cn(\"space-y-3\", className)}\n        {...props}\n      >\n        <FormField\n          control={form.control}\n          name=\"from\"\n          render={({ field }) => (\n            <FormItem>\n              <FormControl>\n                <RadioGroup\n                  onValueChange={field.onChange}\n                  defaultValue={field.value}\n                  className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\"\n                >\n                  {wantFrom.map((item) => (\n                    <FormItem key={item.id} className=\"flex items-center gap-3\">\n                      <FormControl>\n                        <RadioGroupItem value={item.id} id={item.id} />\n                      </FormControl>\n                      <FormLabel\n                        htmlFor={item.id}\n                        className=\"w-full font-normal\"\n                      >\n                        {item.title}\n                      </FormLabel>\n                    </FormItem>\n                  ))}\n                </RadioGroup>\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n        {watchFrom === \"other\" && (\n          <FormField\n            control={form.control}\n            name=\"other\"\n            render={({ field }) => (\n              <FormItem>\n                <FormControl>\n                  <Input\n                    placeholder=\"Please specify\"\n                    className=\"sm:w-1/2\"\n                    {...field}\n                  />\n                </FormControl>\n              </FormItem>\n            )}\n          />\n        )}\n        <Button size=\"sm\" type=\"submit\" disabled={isPending}>\n          {isPending ? \"Submitting...\" : \"Submit\"}\n        </Button>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/private-location/form.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"@openstatus/ui/components/ui/input-group\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { Check, Copy } from \"lucide-react\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string().min(1, \"Name is required\"),\n  token: z.string(),\n  monitors: z.array(z.number()),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function FormPrivateLocation({\n  defaultValues,\n  onSubmit,\n  className,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  monitors: { id: number; name: string; url: string }[];\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n      token: crypto.randomUUID(),\n      monitors: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { copy, isCopied } = useCopyToClipboard();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"name\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Name</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"My Raspberry Pi\" {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"token\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Token</FormLabel>\n                <FormControl>\n                  <InputGroup>\n                    <InputGroupInput\n                      placeholder=\"Private Location Token\"\n                      readOnly\n                      value={field.value}\n                    />\n                    <InputGroupAddon align=\"inline-end\">\n                      <InputGroupButton\n                        aria-label=\"Copy\"\n                        title=\"Copy\"\n                        size=\"icon-xs\"\n                        onClick={() => {\n                          copy(field.value, {\n                            successMessage: \"Token copied to clipboard\",\n                          });\n                        }}\n                      >\n                        {isCopied ? <Check /> : <Copy />}\n                      </InputGroupButton>\n                    </InputGroupAddon>\n                  </InputGroup>\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"monitors\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Monitors</FormLabel>\n                <FormDescription>\n                  Connected monitors will be automatically activated for the\n                  private location.\n                </FormDescription>\n                {monitors.length ? (\n                  <div className=\"grid gap-3\">\n                    <div className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id=\"all\"\n                          checked={field.value?.length === monitors.length}\n                          onCheckedChange={(checked) => {\n                            field.onChange(\n                              checked ? monitors.map((m) => m.id) : [],\n                            );\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor=\"all\">Select all</Label>\n                    </div>\n                    {monitors.map((item) => (\n                      <div key={item.id} className=\"flex items-center gap-2\">\n                        <FormControl>\n                          <Checkbox\n                            id={String(item.id)}\n                            checked={field.value?.includes(item.id)}\n                            onCheckedChange={(checked) => {\n                              const newValue = checked\n                                ? [...(field.value || []), item.id]\n                                : field.value?.filter((id) => id !== item.id);\n                              field.onChange(newValue);\n                            }}\n                          />\n                        </FormControl>\n                        <Label htmlFor={String(item.id)}>{item.name}</Label>\n                      </div>\n                    ))}\n                  </div>\n                ) : (\n                  <EmptyStateContainer>\n                    <EmptyStateTitle>No monitors found</EmptyStateTitle>\n                  </EmptyStateContainer>\n                )}\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/private-location/sheet.tsx",
    "content": "\"use client\";\n\nimport { FormCard, FormCardGroup } from \"@/components/forms/form-card\";\nimport {\n  FormSheetContent,\n  FormSheetDescription,\n  FormSheetFooter,\n  FormSheetHeader,\n  FormSheetTitle,\n  FormSheetTrigger,\n  FormSheetWithDirtyProtection,\n} from \"@/components/forms/form-sheet\";\nimport {\n  FormPrivateLocation,\n  type FormValues,\n} from \"@/components/forms/private-location/form\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useState } from \"react\";\n\nexport function FormSheetPrivateLocation({\n  children,\n  defaultValues,\n  onSubmit,\n  monitors,\n  ...props\n}: Omit<React.ComponentProps<typeof FormSheetTrigger>, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  monitors: { id: number; name: string; url: string }[];\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const [open, setOpen] = useState(false);\n\n  return (\n    <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}>\n      <FormSheetTrigger {...props} asChild>\n        {children}\n      </FormSheetTrigger>\n      <FormSheetContent>\n        <FormSheetHeader>\n          <FormSheetTitle>Private Location</FormSheetTitle>\n          <FormSheetDescription>\n            Configure and update the private location.\n          </FormSheetDescription>\n        </FormSheetHeader>\n        <FormCardGroup className=\"overflow-y-auto\">\n          <FormCard className=\"overflow-auto rounded-none border-none\">\n            <FormPrivateLocation\n              monitors={monitors}\n              onSubmit={async (values) => {\n                await onSubmit(values);\n                setOpen(false);\n              }}\n              defaultValues={defaultValues}\n              id=\"private-location-form\"\n              className=\"my-4\"\n            />\n          </FormCard>\n        </FormCardGroup>\n        <FormSheetFooter>\n          <Button type=\"submit\" form=\"private-location-form\">\n            Submit\n          </Button>\n        </FormSheetFooter>\n      </FormSheetContent>\n    </FormSheetWithDirtyProtection>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/settings/form-api-key.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  EmptyStateDescription,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport { EmptyStateContainer } from \"@/components/content/empty-state\";\nimport { DataTable } from \"@/components/data-table/settings/api-key/data-table\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  AlertDialog,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Calendar } from \"@openstatus/ui/components/ui/calendar\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@openstatus/ui/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { format } from \"date-fns\";\nimport { CalendarIcon, Check, Copy } from \"lucide-react\";\nimport { useState, useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\n// we should prefetch the api key on the server (layout)\n\nconst schema = z.object({\n  name: z.string().min(1, \"Name is required\"),\n  description: z.string().optional(),\n  expiresAt: z.string().optional(),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormApiKey() {\n  const trpc = useTRPC();\n  const [isPending, startTransition] = useTransition();\n  const { copy, isCopied } = useCopyToClipboard();\n  const [result, setResult] = useState<{\n    token: string;\n    key: string;\n  } | null>(null);\n  const [createDialogOpen, setCreateDialogOpen] = useState(false);\n\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      name: \"\",\n      description: \"\",\n      expiresAt: \"\",\n    },\n  });\n\n  const { data: workspace } = useQuery(\n    trpc.workspace.getWorkspace.queryOptions(),\n  );\n  const { data: apiKeys = [], refetch } = useQuery(\n    trpc.apiKeyRouter.getAll.queryOptions(),\n  );\n  const createApiKeyMutation = useMutation(\n    trpc.apiKeyRouter.create.mutationOptions({\n      onSuccess: (data) => {\n        if (data) {\n          refetch();\n          setResult({ token: data.token, key: data.key.name });\n          setCreateDialogOpen(false);\n          form.reset();\n        } else {\n          throw new Error(\"Failed to create API key\");\n        }\n      },\n    }),\n  );\n\n  function createAction(values: FormValues) {\n    if (isPending || !workspace) {\n      return;\n    }\n\n    startTransition(async () => {\n      try {\n        const promise = createApiKeyMutation.mutateAsync({\n          name: values.name.trim(),\n          description: values.description?.trim() || undefined,\n          expiresAt: values.expiresAt ? new Date(values.expiresAt) : undefined,\n        });\n        toast.promise(promise, {\n          loading: \"Creating...\",\n          success: () => \"Created\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to create API key\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <FormCard>\n      <FormCardHeader>\n        <FormCardTitle>API Keys</FormCardTitle>\n        <FormCardDescription>\n          Create and manage your API keys.\n        </FormCardDescription>\n      </FormCardHeader>\n      <FormCardContent>\n        {apiKeys.length === 0 ? (\n          <EmptyStateContainer>\n            <EmptyStateTitle>No API keys</EmptyStateTitle>\n            <EmptyStateDescription>\n              Access your data via API.\n            </EmptyStateDescription>\n          </EmptyStateContainer>\n        ) : (\n          <DataTable apiKeys={apiKeys} refetch={refetch} />\n        )}\n      </FormCardContent>\n      <FormCardFooter>\n        <FormCardFooterInfo>\n          Trigger monitors via CLI, CI/CD or create your own status page.{\" \"}\n          <Link\n            href=\"https://api.openstatus.dev/v1\"\n            rel=\"noreferrer\"\n            target=\"_blank\"\n          >\n            Learn more\n          </Link>\n          .\n        </FormCardFooterInfo>\n        <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>\n          <DialogTrigger asChild>\n            <Button size=\"sm\">Create</Button>\n          </DialogTrigger>\n          <DialogContent>\n            <Form {...form}>\n              <form onSubmit={form.handleSubmit(createAction)}>\n                <DialogHeader>\n                  <DialogTitle>Create API Key</DialogTitle>\n                  <DialogDescription>\n                    Create a new API key to access your workspace data.\n                  </DialogDescription>\n                </DialogHeader>\n                <div className=\"space-y-4\">\n                  <FormField\n                    control={form.control}\n                    name=\"name\"\n                    render={({ field }) => (\n                      <FormItem>\n                        <FormLabel>Name</FormLabel>\n                        <FormControl>\n                          <Input placeholder=\"Production API\" {...field} />\n                        </FormControl>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n                  <FormField\n                    control={form.control}\n                    name=\"description\"\n                    render={({ field }) => (\n                      <FormItem>\n                        <FormLabel>Description</FormLabel>\n                        <FormControl>\n                          <Textarea\n                            placeholder=\"Used for production deployment\"\n                            rows={3}\n                            {...field}\n                          />\n                        </FormControl>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n                  <FormField\n                    control={form.control}\n                    name=\"expiresAt\"\n                    render={({ field }) => (\n                      <FormItem className=\"flex flex-col\">\n                        <FormLabel>Expiration Date</FormLabel>\n                        <Popover modal>\n                          <FormControl>\n                            <PopoverTrigger asChild>\n                              <Button\n                                type=\"button\"\n                                variant=\"outline\"\n                                size=\"sm\"\n                                className={cn(\n                                  \"w-[240px] pl-3 text-left font-normal\",\n                                  !field.value && \"text-muted-foreground\",\n                                )}\n                              >\n                                {field.value ? (\n                                  format(new Date(field.value), \"PPP\")\n                                ) : (\n                                  <span>Pick a date</span>\n                                )}\n                                <CalendarIcon className=\"ml-auto h-4 w-4 opacity-50\" />\n                              </Button>\n                            </PopoverTrigger>\n                          </FormControl>\n                          <PopoverContent\n                            className=\"pointer-events-auto w-auto p-0\"\n                            align=\"start\"\n                          >\n                            <Calendar\n                              mode=\"single\"\n                              selected={\n                                field.value ? new Date(field.value) : undefined\n                              }\n                              onSelect={(date) => {\n                                if (!date) {\n                                  field.onChange(\"\");\n                                  return;\n                                }\n                                // Convert to ISO string and take only the date part (YYYY-MM-DD)\n                                const dateString = date\n                                  .toISOString()\n                                  .split(\"T\")[0];\n                                field.onChange(dateString);\n                              }}\n                              disabled={(date) => {\n                                const today = new Date();\n                                today.setHours(0, 0, 0, 0);\n                                const compareDate = new Date(date);\n                                compareDate.setHours(0, 0, 0, 0);\n                                return compareDate < today;\n                              }}\n                              initialFocus\n                            />\n                          </PopoverContent>\n                        </Popover>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n                </div>\n                <DialogFooter className=\"mt-4\">\n                  <Button\n                    variant=\"outline\"\n                    type=\"button\"\n                    onClick={() => setCreateDialogOpen(false)}\n                  >\n                    Cancel\n                  </Button>\n                  <Button type=\"submit\" disabled={isPending}>\n                    Create\n                  </Button>\n                </DialogFooter>\n              </form>\n            </Form>\n          </DialogContent>\n        </Dialog>\n      </FormCardFooter>\n      <AlertDialog open={!!result} onOpenChange={() => setResult(null)}>\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>API Key Created</AlertDialogTitle>\n            <AlertDialogDescription>\n              Ensure you copy your API key before closing this dialog. You will\n              not see it again.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={() => {\n                copy(result?.token || \"\", {\n                  successMessage: \"Copied API key to clipboard\",\n                });\n              }}\n            >\n              <code>{result?.token}</code>\n              {isCopied ? (\n                <Check size={16} className=\"text-muted-foreground\" />\n              ) : (\n                <Copy size={16} className=\"text-muted-foreground\" />\n              )}\n            </Button>\n          </div>\n          <AlertDialogFooter>\n            <Button onClick={() => setResult(null)}>Done</Button>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/settings/form-members.tsx",
    "content": "\"use client\";\n\nimport {\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\n\nimport {\n  FormCardContent,\n  FormCardDescription,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n  FormCardUpgrade,\n} from \"@/components/forms/form-card\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { FormCardFooter, FormCardFooterInfo } from \"../form-card\";\n\nimport { Link } from \"@/components/common/link\";\nimport { FormCard } from \"@/components/forms/form-card\";\nimport { Tabs } from \"@openstatus/ui/components/ui/tabs\";\nimport { Lock } from \"lucide-react\";\n\nimport { DataTable as InvitationsDataTable } from \"@/components/data-table/settings/invitations/data-table\";\nimport { DataTable as MembersDataTable } from \"@/components/data-table/settings/members/data-table\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  email: z.email(),\n  role: z.enum([\"member\"]),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormMembers({\n  locked,\n  onCreate,\n}: {\n  locked?: boolean;\n  onCreate: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      email: \"\",\n      role: \"member\",\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onCreate(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: \"Failed to save\",\n        });\n        await promise;\n        form.reset();\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)}>\n        <FormCard>\n          {locked ? <FormCardUpgrade /> : null}\n          <FormCardHeader>\n            <FormCardTitle>Team</FormCardTitle>\n            <FormCardDescription>Manage your team members.</FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent>\n            <Tabs defaultValue=\"members\">\n              <TabsList>\n                <TabsTrigger value=\"members\">Members</TabsTrigger>\n                <TabsTrigger value=\"pending\">Pending</TabsTrigger>\n              </TabsList>\n              <TabsContent value=\"members\">\n                <MembersDataTable />\n              </TabsContent>\n              <TabsContent value=\"pending\">\n                <InvitationsDataTable />\n              </TabsContent>\n            </Tabs>\n          </FormCardContent>\n          <FormCardSeparator />\n          <FormCardContent>\n            <FormField\n              control={form.control}\n              disabled={locked}\n              name=\"email\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Add member</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"email\"\n                      placeholder=\"Email\"\n                      disabled={locked}\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                  <FormCardDescription>\n                    Send an invitation to join the team.\n                  </FormCardDescription>\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            {locked ? (\n              <>\n                <FormCardFooterInfo>\n                  This feature is available on the{\" \"}\n                  <Link\n                    href=\"https://www.openstatus.dev/changelog/team-invites\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                  >\n                    Pro plan\n                  </Link>\n                  .\n                </FormCardFooterInfo>\n                <Button type=\"button\" size=\"sm\" asChild>\n                  <Link href=\"/settings/billing\">\n                    <Lock />\n                    Upgrade\n                  </Link>\n                </Button>\n              </>\n            ) : (\n              <Button type=\"submit\" size=\"sm\" disabled={isPending}>\n                {isPending ? \"Submitting...\" : \"Submit\"}\n              </Button>\n            )}\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/settings/form-slug.tsx",
    "content": "\"use client\";\n\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\n\nimport { FormDialogSupportContact } from \"@/components/forms/support-contact/dialog\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { Check, Copy } from \"lucide-react\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  slug: z.string().min(1),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormSlug({ defaultValues }: { defaultValues?: FormValues }) {\n  const { copy, isCopied } = useCopyToClipboard();\n  console.log({ defaultValues, schema });\n\n  return (\n    <FormCard>\n      <FormCardHeader>\n        <FormCardTitle>Slug</FormCardTitle>\n        <FormCardDescription>\n          The unique slug for your workspace.\n        </FormCardDescription>\n      </FormCardHeader>\n      <FormCardContent>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={() =>\n            copy(defaultValues?.slug ?? \"unknown slug\", {\n              successMessage: \"Copied slug to clipboard\",\n            })\n          }\n        >\n          {defaultValues?.slug ?? \"unknown slug\"}\n          {isCopied ? (\n            <Check size={16} className=\"text-muted-foreground\" />\n          ) : (\n            <Copy size={16} className=\"text-muted-foreground\" />\n          )}\n        </Button>\n      </FormCardContent>\n      <FormCardFooter className=\"[&>:last-child]:ml-0\">\n        <FormCardFooterInfo>\n          Used when interacting with the API or for help on Discord.{\" \"}\n          <FormDialogSupportContact>\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              className=\"px-0 py-0 text-accent-foreground hover:bg-transparent dark:hover:bg-transparent\"\n            >\n              Let us know\n            </Button>\n          </FormDialogSupportContact>{\" \"}\n          if you&apos;d like to change it.\n        </FormCardFooterInfo>\n      </FormCardFooter>\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/settings/form-workspace.tsx",
    "content": "\"use client\";\n\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  name: z.string(),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormWorkspace({\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      name: \"\",\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: \"Failed to save\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Workspace</FormCardTitle>\n            <FormCardDescription>\n              Manage your workspace name.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent>\n            <FormField\n              control={form.control}\n              name=\"name\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Name</FormLabel>\n                  <FormControl>\n                    <Input {...field} />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            <Button type=\"submit\" disabled={isPending} size=\"sm\">\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-page/form-appearance.tsx",
    "content": "import { useTransition } from \"react\";\nimport { z } from \"zod\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { THEME_KEYS } from \"@openstatus/theme-store\";\nimport { THEMES } from \"@openstatus/theme-store\";\nimport type { ThemeKey } from \"@openstatus/theme-store\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@openstatus/ui/components/ui/command\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { ArrowUpRight, Laptop, Moon, Sun } from \"lucide-react\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\n\nconst schema = z.object({\n  forceTheme: z.enum([\"light\", \"dark\", \"system\"]),\n  configuration: z.object({\n    theme: z.string(),\n  }),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormAppearance({\n  defaultValues,\n  onSubmit,\n}: {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const [isPending, startTransition] = useTransition();\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      forceTheme: \"system\",\n    },\n  });\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Appearance</FormCardTitle>\n            <FormCardDescription>\n              Forced theme will override the user&apos;s preference.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid gap-4 sm:grid-cols-3\">\n            <FormField\n              control={form.control}\n              name=\"forceTheme\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Mode</FormLabel>\n                  <Select\n                    onValueChange={field.onChange}\n                    defaultValue={field.value}\n                  >\n                    <FormControl>\n                      <SelectTrigger className=\"w-full\">\n                        <SelectValue placeholder=\"Select a theme\" />\n                      </SelectTrigger>\n                    </FormControl>\n                    <SelectContent>\n                      <SelectItem value=\"light\">\n                        <div className=\"flex items-center gap-2\">\n                          <Sun className=\"h-4 w-4\" />\n                          <span>Light</span>\n                        </div>\n                      </SelectItem>\n                      <SelectItem value=\"dark\">\n                        <div className=\"flex items-center gap-2\">\n                          <Moon className=\"h-4 w-4\" />\n                          <span>Dark</span>\n                        </div>\n                      </SelectItem>\n                      <SelectItem value=\"system\">\n                        <div className=\"flex items-center gap-2\">\n                          <Laptop className=\"h-4 w-4\" />\n                          <span>System</span>\n                        </div>\n                      </SelectItem>\n                    </SelectContent>\n                  </Select>\n                  <FormMessage />\n                  <FormDescription>\n                    Override the user&apos;s preference.\n                  </FormDescription>\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"configuration.theme\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Style</FormLabel>\n                  <Popover>\n                    <PopoverTrigger asChild>\n                      <FormControl>\n                        <Button\n                          id=\"community-theme\"\n                          variant=\"outline\"\n                          role=\"combobox\"\n                          className={cn(\n                            \"w-full justify-between\",\n                            !field.value && \"text-muted-foreground\",\n                          )}\n                        >\n                          <span className=\"truncate\">\n                            {THEMES[field.value as ThemeKey]?.name ||\n                              \"Select a theme\"}\n                          </span>\n                          <ChevronsUpDown className=\"opacity-50\" />\n                        </Button>\n                      </FormControl>\n                    </PopoverTrigger>\n                    <PopoverContent className=\"p-0\" align=\"start\">\n                      <Command>\n                        <CommandInput\n                          placeholder=\"Search themes...\"\n                          className=\"h-9\"\n                        />\n                        <CommandList>\n                          <CommandEmpty>No themes found.</CommandEmpty>\n                          <CommandGroup>\n                            {THEME_KEYS.map((theme) => {\n                              const { name, author } = THEMES[theme];\n                              return (\n                                <CommandItem\n                                  value={theme}\n                                  key={theme}\n                                  keywords={[theme, name, author.name]}\n                                  onSelect={(v) => field.onChange(v)}\n                                >\n                                  <span className=\"truncate\">{name}</span>\n                                  <span className=\"truncate font-commit-mono text-muted-foreground text-xs\">\n                                    by {author.name}\n                                  </span>\n                                  <Check\n                                    className={cn(\n                                      \"ml-auto\",\n                                      theme === field.value\n                                        ? \"opacity-100\"\n                                        : \"opacity-0\",\n                                    )}\n                                  />\n                                </CommandItem>\n                              );\n                            })}\n                          </CommandGroup>\n                        </CommandList>\n                      </Command>\n                    </PopoverContent>\n                  </Popover>\n                  <FormMessage />\n                  <FormDescription>Choose a theme to apply.</FormDescription>\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Your user will still be able to change the mode via the theme\n              toggle.\n            </FormCardFooterInfo>\n            <div className=\"flex items-center gap-2\">\n              <Button type=\"button\" variant=\"ghost\" asChild>\n                <Link\n                  href=\"https://themes.openstatus.dev\"\n                  rel=\"noreferrer\"\n                  target=\"_blank\"\n                >\n                  View Theme Explorer <ArrowUpRight className=\"h-4 w-4\" />\n                </Link>\n              </Button>\n              <Button type=\"submit\" disabled={isPending}>\n                {isPending ? \"Submitting...\" : \"Submit\"}\n              </Button>\n            </div>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-page/form-configuration.tsx",
    "content": "import { useEffect, useState, useTransition } from \"react\";\nimport { z } from \"zod\";\n\nimport { Link } from \"@/components/common/link\";\nimport { Note } from \"@/components/common/note\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { THEMES, THEME_KEYS } from \"@openstatus/theme-store\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@openstatus/ui/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { ArrowUpRight } from \"lucide-react\";\nimport { parseAsStringLiteral, useQueryStates } from \"nuqs\";\nimport { type UseFormReturn, useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\n\nconst schema = z.object({\n  configuration: z.record(\n    z.string(),\n    z.string().or(z.boolean().nullish()).optional(),\n  ),\n});\n\nconst configurationSchema = z\n  .object({\n    type: z.enum([\"manual\", \"absolute\"]),\n    value: z.enum([\"duration\", \"requests\", \"manual\"]).nullish(),\n    uptime: z.boolean().or(z.literal(\"true\").or(z.literal(\"false\"))),\n    theme: z.enum(THEME_KEYS as [string, ...string[]]),\n  })\n  .refine(\n    (data) => {\n      // If type is \"manual\", value must be \"manual\"\n      if (data.type === \"manual\") return data.value === \"manual\";\n      return true;\n    },\n    {\n      error: \"Value must be manual when type is manual\",\n      path: [\"value\"],\n    },\n  );\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormConfiguration({\n  defaultValues,\n  onSubmit,\n  configLink,\n}: {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  configLink: string;\n}) {\n  const [isPending, startTransition] = useTransition();\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      configuration: {},\n    },\n  });\n  const watchConfigurationType = form.watch(\"configuration.type\") as\n    | \"manual\"\n    | \"absolute\";\n  const watchConfigurationValue = form.watch(\"configuration.value\") as\n    | \"duration\"\n    | \"requests\";\n  const watchConfigurationUptime = form.watch(\"configuration.uptime\") as\n    | \"true\"\n    | \"false\";\n\n  useEffect(() => {\n    if (watchConfigurationType === \"manual\") {\n      form.setValue(\"configuration.value\", \"manual\");\n    } else {\n      form.setValue(\"configuration.value\", \"duration\");\n      form.setValue(\"configuration.type\", \"absolute\");\n      if (!watchConfigurationUptime) {\n        form.setValue(\"configuration.uptime\", \"true\");\n      }\n    }\n  }, [watchConfigurationType, watchConfigurationUptime, form]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <>\n      <Form {...form}>\n        <form id=\"redesign\" onSubmit={form.handleSubmit(submitAction)}>\n          <FormCard>\n            <FormCardHeader>\n              <FormCardTitle>Components Configuration</FormCardTitle>\n              <FormCardDescription>\n                Configure which data should be shown for your components.\n              </FormCardDescription>\n            </FormCardHeader>\n            <FormCardSeparator />\n            <FormCardContent className=\"grid gap-4 sm:grid-cols-3\">\n              <FormField\n                control={form.control}\n                name=\"configuration.type\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Bar Type*</FormLabel>\n                    <Select\n                      onValueChange={field.onChange}\n                      defaultValue={String(field.value) ?? \"absolute\"}\n                    >\n                      <FormControl>\n                        <SelectTrigger className=\"w-full capitalize\">\n                          <SelectValue placeholder=\"Select a type\" />\n                        </SelectTrigger>\n                      </FormControl>\n                      <SelectContent>\n                        {[\"absolute\", \"manual\"].map((type) => (\n                          <SelectItem\n                            key={type}\n                            value={type}\n                            className=\"capitalize\"\n                          >\n                            {type}\n                          </SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"configuration.value\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Card Value*</FormLabel>\n                    <Select\n                      onValueChange={field.onChange}\n                      defaultValue={String(field.value) ?? \"duration\"}\n                      disabled={watchConfigurationType === \"manual\"}\n                    >\n                      <FormControl>\n                        <SelectTrigger className=\"w-full capitalize\">\n                          <SelectValue placeholder=\"Select a type\" />\n                        </SelectTrigger>\n                      </FormControl>\n                      <SelectContent>\n                        {[\"duration\", \"requests\"].map((type) => (\n                          <SelectItem\n                            key={type}\n                            value={type}\n                            className=\"capitalize\"\n                          >\n                            {type}\n                          </SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"configuration.uptime\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Show Uptime</FormLabel>\n                    <Select\n                      onValueChange={field.onChange}\n                      defaultValue={String(field.value) ?? \"true\"}\n                    >\n                      <FormControl>\n                        <SelectTrigger className=\"w-full capitalize\">\n                          <SelectValue placeholder=\"Select a type\" />\n                        </SelectTrigger>\n                      </FormControl>\n                      <SelectContent>\n                        {[\"true\", \"false\"].map((type) => (\n                          <SelectItem\n                            key={type}\n                            value={type}\n                            className=\"capitalize\"\n                          >\n                            {type}\n                          </SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <p className=\"col-span-full text-foreground/70 text-sm\">\n                *Configuration settings only apply to monitor components.\n              </p>\n              <Note className=\"col-span-full\">\n                <ul className=\"list-inside list-disc\">\n                  <li>\n                    <span>Bar Type </span>\n                    <span className=\"font-medium\">\n                      {watchConfigurationType}\n                    </span>\n                    : <span>{message.type[watchConfigurationType]}</span>\n                  </li>\n                  <li>\n                    <span>Card Value </span>\n                    <span className=\"font-medium\">\n                      {watchConfigurationValue}\n                    </span>\n                    :{\" \"}\n                    <span>\n                      {message.value[watchConfigurationValue] ??\n                        message.value.default}\n                    </span>\n                  </li>\n                  <li>\n                    <span>Show Uptime </span>\n                    <span className=\"font-medium capitalize\">\n                      {String(watchConfigurationUptime)}\n                    </span>\n                    : <span>{message.uptime[watchConfigurationUptime]}</span>\n                  </li>\n                </ul>\n              </Note>\n            </FormCardContent>\n            <FormCardFooter>\n              <FormCardFooterInfo>\n                Learn more about{\" \"}\n                <Link\n                  href=\"https://docs.openstatus.dev/tutorial/how-to-configure-status-page\"\n                  rel=\"noreferrer\"\n                  target=\"_blank\"\n                >\n                  Configuration\n                </Link>\n                .\n              </FormCardFooterInfo>\n              <div className=\"flex items-center gap-2\">\n                <Button type=\"button\" variant=\"ghost\" asChild>\n                  <Link\n                    href={configLink}\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                    className=\"inline-flex items-center gap-1\"\n                  >\n                    View and configure status page{\" \"}\n                    <ArrowUpRight className=\"h-4 w-4\" />\n                  </Link>\n                </Button>\n                <Button type=\"submit\" disabled={isPending}>\n                  {isPending ? \"Submitting...\" : \"Submit\"}\n                </Button>\n              </div>\n            </FormCardFooter>\n          </FormCard>\n        </form>\n      </Form>\n      <FormConfigurationDialog\n        defaultValues={defaultValues}\n        form={form}\n        onSubmit={async (e) => {\n          await onSubmit(e);\n          // NOTE: make sure to sync the form with the new values\n          form.reset(e);\n        }}\n      />\n    </>\n  );\n}\n\n// TODO:\nconst message = {\n  type: {\n    manual:\n      \"only shares the duration of reports and maintenaces you are setting up - nothing else.\",\n    absolute:\n      \"shares the status of your endpoint for the duration of the different statuses.\",\n  },\n  value: {\n    duration: \"shares the duration of the different statuses.\",\n    requests:\n      \"shares the number of requests received (success, degraded, error).\",\n    default: \"shares only the worse status of the day\",\n  },\n  uptime: {\n    true: \"shares the uptime percentage and current status of your endpoint.\",\n    false: \"shares only the current status.\",\n  },\n} as const;\n\n// ?type=manual&value=manual&uptime=true&theme=default\n\nconst searchParams = {\n  type: parseAsStringLiteral([\"manual\", \"absolute\"]),\n  value: parseAsStringLiteral([\"duration\", \"requests\", \"manual\"]),\n  uptime: parseAsStringLiteral([\"true\", \"false\"]),\n  theme: parseAsStringLiteral(Object.keys(THEMES)),\n};\n\nfunction FormConfigurationDialog({\n  defaultValues,\n  onSubmit,\n}: {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  form: UseFormReturn<FormValues>;\n}) {\n  const [open, setOpen] = useState(false);\n  const [isPending, startTransition] = useTransition();\n  const [{ type, value, uptime, theme }, setSearchParams] =\n    useQueryStates(searchParams);\n\n  useEffect(() => {\n    if (type) setOpen(true);\n  }, [type]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    const data = configurationSchema.safeParse(values.configuration);\n    if (!data.success) {\n      toast.error(data.error.message);\n      return;\n    }\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n        await setSearchParams({\n          type: null,\n          value: null,\n          uptime: null,\n          theme: null,\n        });\n        setOpen(false);\n      } catch (error) {\n        console.error(error);\n      } finally {\n        if (typeof window !== \"undefined\") {\n          window.location.reload();\n        }\n      }\n    });\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Status Page Configuration</DialogTitle>\n          <DialogDescription>\n            Do you want to update the status page based on the configured\n            settings? You can always change the settings later.\n          </DialogDescription>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-2\">\n          <pre className=\"rounded-md border bg-muted/50 px-3 py-2 font-commit-mono text-sm\">\n            {JSON.stringify({ type, value, uptime, theme }, null, 2)}\n          </pre>\n        </div>\n        <DialogFooter>\n          <DialogClose asChild>\n            <Button variant=\"outline\">Cancel</Button>\n          </DialogClose>\n          <Button\n            type=\"button\"\n            onClick={() =>\n              submitAction({\n                ...defaultValues,\n                configuration: {\n                  type: type ?? undefined,\n                  value: value ?? undefined,\n                  uptime: uptime ?? undefined,\n                  theme: theme ?? undefined,\n                },\n              })\n            }\n            disabled={isPending}\n          >\n            {isPending ? \"Saving...\" : \"Save\"}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-page/form-custom-domain.tsx",
    "content": "\"use client\";\n\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n  FormCardUpgrade,\n} from \"@/components/forms/form-card\";\n\nimport { Label } from \"@openstatus/ui/components/ui/label\";\n\n// FIXME: use input-group instead\nimport { InputWithAddons } from \"@/components/common/input-with-addons\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Lock } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { Link } from \"@/components/common/link\";\nimport DomainConfiguration from \"@/components/domains/domain-configuration\";\nimport { useDomainStatus } from \"@/components/domains/use-domain-status\";\nimport {\n  Form,\n  FormField,\n  FormItem,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport type React from \"react\";\nimport { useEffect, useTransition } from \"react\";\nimport { toast } from \"sonner\";\n\nconst schema = z.object({\n  domain: z.string(),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormCustomDomain({\n  locked,\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  locked?: boolean;\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      domain: undefined,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const { refresh, isLoading } = useDomainStatus(defaultValues?.domain);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  // NOTE: poll every 30 seconds to check for the status\n  useEffect(() => {\n    const interval = setInterval(() => refresh(), 30_000);\n    return () => clearInterval(interval);\n  }, [refresh]);\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          {locked ? <FormCardUpgrade /> : null}\n          <FormCardHeader>\n            <FormCardTitle>Custom Domain</FormCardTitle>\n            <FormCardDescription>\n              Use your own domain for your status page.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent>\n            <FormField\n              control={form.control}\n              name=\"domain\"\n              render={({ field }) => (\n                <FormItem>\n                  <Label>Domain</Label>\n                  <InputWithAddons\n                    placeholder=\"status.openstatus.dev\"\n                    leading=\"https://\"\n                    disabled={locked}\n                    {...field}\n                  />\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          {defaultValues?.domain ? (\n            <>\n              <FormCardSeparator />\n              <FormCardContent>\n                <DomainConfiguration domain={defaultValues?.domain} />\n              </FormCardContent>\n            </>\n          ) : null}\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/reference/status-page/#custom-domain\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                Custom Domain\n              </Link>\n              .\n            </FormCardFooterInfo>\n            {locked ? (\n              <Button type=\"button\" asChild>\n                <Link href=\"/settings/billing\">\n                  <Lock />\n                  Upgrade\n                </Link>\n              </Button>\n            ) : (\n              <div className=\"flex items-center gap-2\">\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  disabled={isPending || isLoading}\n                  onClick={refresh}\n                  className=\"hidden sm:block\"\n                >\n                  {isLoading ? \"Refreshing...\" : \"Refresh Configuration\"}\n                </Button>\n                <Button type=\"submit\" disabled={isPending}>\n                  {isPending ? \"Submitting...\" : \"Submit\"}\n                </Button>\n              </div>\n            )}\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-page/form-danger-zone.tsx",
    "content": "\"use client\";\n\nimport { FormAlertDialog } from \"@/components/forms/form-alert-dialog\";\nimport {\n  FormCard,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\n\nexport function FormDangerZone({\n  onSubmit,\n  title,\n}: {\n  onSubmit: () => Promise<void>;\n  title: string;\n}) {\n  return (\n    <FormCard variant=\"destructive\">\n      <FormCardHeader>\n        <FormCardTitle>Danger Zone</FormCardTitle>\n        <FormCardDescription>This action cannot be undone.</FormCardDescription>\n      </FormCardHeader>\n      <FormCardFooter variant=\"destructive\" className=\"justify-end\">\n        <FormAlertDialog confirmationValue={title} submitAction={onSubmit} />\n      </FormCardFooter>\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-page/form-general.tsx",
    "content": "\"use client\";\n\n// FIXME: use input-group instead\nimport { InputWithAddons } from \"@/components/common/input-with-addons\";\nimport {\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { useDebounce } from \"@openstatus/ui/hooks/use-debounce\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport Image from \"next/image\";\nimport { useEffect, useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst SLUG_UNIQUE_ERROR_MESSAGE =\n  \"This slug is already taken. Please choose another one.\";\n\nfunction formatSlug(title: string) {\n  return title\n    .toLowerCase()\n    .replace(/[^a-z0-9]+/g, \"-\")\n    .replace(/^-+|-+$/g, \"\");\n}\n\nconst schema = z.object({\n  title: z.string().min(1, \"Title is required\"),\n  slug: z.string().min(3, \"Slug is required\"),\n  icon: z.string().optional(),\n  description: z.string().optional(),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\n/** Convert a File to a base64 string without the data: prefix */\nasync function fileToBase64(file: File): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      const result = reader.result as string;\n      // result is like \"data:image/png;base64,XXXX\" – we only need the part after the comma\n      resolve(result.split(\",\")[1] || \"\");\n    };\n    reader.onerror = reject;\n    reader.readAsDataURL(file);\n  });\n}\n\nexport function FormGeneral({\n  disabled,\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  disabled?: boolean;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      title: \"\",\n      slug: \"\",\n      icon: undefined,\n      description: \"\",\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const trpc = useTRPC();\n  const uploadMutation = useMutation(trpc.blob.upload.mutationOptions());\n  const watchSlug = form.watch(\"slug\");\n  const watchTitle = form.watch(\"title\");\n  const watchIcon = form.watch(\"icon\");\n  const debouncedSlug = useDebounce(watchSlug, 500);\n  const { data: isUnique } = useQuery(\n    trpc.page.getSlugUniqueness.queryOptions(\n      { slug: debouncedSlug },\n      { enabled: debouncedSlug.length > 0 },\n    ),\n  );\n\n  useEffect(() => {\n    if (!defaultValues?.title) {\n      const formattedSlug = formatSlug(watchTitle);\n      form.setValue(\"slug\", formattedSlug);\n    }\n  }, [form, defaultValues?.title, watchTitle]);\n\n  useEffect(() => {\n    if (isUnique === undefined) return;\n    if (defaultValues?.slug === debouncedSlug) return;\n\n    if (!isUnique) {\n      form.setError(\"slug\", { message: SLUG_UNIQUE_ERROR_MESSAGE });\n    } else {\n      form.clearErrors(\"slug\");\n    }\n  }, [isUnique, form, debouncedSlug, defaultValues?.slug]);\n\n  function submitAction(values: FormValues) {\n    if (isPending || disabled) return;\n\n    startTransition(async () => {\n      try {\n        if (isUnique === false && defaultValues?.slug !== values.slug) {\n          toast.error(SLUG_UNIQUE_ERROR_MESSAGE);\n          form.setError(\"slug\", { message: SLUG_UNIQUE_ERROR_MESSAGE });\n          return;\n        }\n\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>General</FormCardTitle>\n            <FormCardDescription>\n              Configure the essential details for your status page.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardSeparator />\n          <FormCardContent className=\"grid gap-4\">\n            <FormField\n              control={form.control}\n              name=\"title\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Title</FormLabel>\n                  <FormControl>\n                    <Input placeholder=\"My Status Page\" {...field} />\n                  </FormControl>\n                  <FormMessage />\n                  <FormDescription>\n                    Enter a descriptive name for your status page.\n                  </FormDescription>\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"slug\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Slug</FormLabel>\n                  <FormControl>\n                    <InputWithAddons\n                      placeholder=\"status\"\n                      trailing=\".openstatus.dev\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                  <FormDescription>\n                    Choose a unique subdomain for your status page (minimum 3\n                    characters).\n                  </FormDescription>\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"icon\"\n              render={() => (\n                <FormItem>\n                  <FormLabel>Icon</FormLabel>\n                  <FormControl>\n                    <div className=\"flex items-center space-x-2\">\n                      {watchIcon ? (\n                        <>\n                          <div className=\"size-[36px] overflow-hidden rounded-md border bg-muted\">\n                            <Image\n                              src={watchIcon}\n                              width={36}\n                              height={36}\n                              alt=\"Icon preview\"\n                            />\n                          </div>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            type=\"button\"\n                            onClick={() => form.setValue(\"icon\", undefined)}\n                          >\n                            Remove\n                          </Button>\n                        </>\n                      ) : (\n                        <Input\n                          type=\"file\"\n                          accept=\"image/png,image/x-icon\"\n                          onChange={async (e) => {\n                            const file = e.target.files?.[0];\n                            if (!file) return;\n                            const base64String = await fileToBase64(file);\n                            try {\n                              const blob = await uploadMutation.mutateAsync({\n                                filename: file.name,\n                                file: base64String,\n                              });\n                              if (blob?.url) {\n                                form.setValue(\"icon\", blob.url as string);\n                              }\n                            } catch (err) {\n                              console.error(err);\n                              toast.error(\"Upload failed\");\n                            }\n                          }}\n                        />\n                      )}\n                    </div>\n                  </FormControl>\n                  <FormMessage />\n                  <FormDescription>\n                    Select an icon for your status page. Ideally sized\n                    512x512px. Will be used as favicon.\n                  </FormDescription>\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"description\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Description</FormLabel>\n                  <FormControl>\n                    <Textarea {...field} />\n                  </FormControl>\n                  <FormMessage />\n                  <FormDescription>\n                    Provide a brief overview of your status page purpose.\n                  </FormDescription>\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            <Button type=\"submit\" disabled={isPending || disabled}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-page/form-links.tsx",
    "content": "import { useTransition } from \"react\";\nimport { z } from \"zod\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\n\nconst schema = z.object({\n  homepageUrl: z.string().optional(),\n  contactUrl: z.string().optional(),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormLinks({\n  defaultValues,\n  onSubmit,\n}: {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const [isPending, startTransition] = useTransition();\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      homepageUrl: \"\",\n      contactUrl: \"\",\n    },\n  });\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Links</FormCardTitle>\n            <FormCardDescription>\n              Configure the links for the status page.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid gap-4 sm:grid-cols-3\">\n            <FormField\n              control={form.control}\n              name=\"homepageUrl\"\n              render={({ field }) => (\n                <FormItem className=\"sm:col-span-full\">\n                  <FormLabel>Homepage URL</FormLabel>\n                  <FormControl>\n                    <Input placeholder=\"https://acme.com\" {...field} />\n                  </FormControl>\n                  <FormMessage />\n                  <FormDescription>\n                    What URL should the logo link to? Leave empty to hide.\n                  </FormDescription>\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"contactUrl\"\n              render={({ field }) => (\n                <FormItem className=\"sm:col-span-full\">\n                  <FormLabel>Contact URL</FormLabel>\n                  <FormControl>\n                    <Input placeholder=\"https://acme.com/contact\" {...field} />\n                  </FormControl>\n                  <FormMessage />\n                  <FormDescription>\n                    Enter the URL for your contact page. Or start with{\" \"}\n                    <code className=\"rounded-md bg-muted px-1 py-0.5\">\n                      mailto:\n                    </code>{\" \"}\n                    to open the email client. Leave empty to hide.\n                  </FormDescription>\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/tutorial/how-to-configure-status-page/#3-links\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                links\n              </Link>\n              .\n            </FormCardFooterInfo>\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-page/form-monitors.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport {\n  Sortable,\n  SortableContent,\n  SortableItem,\n  SortableItemHandle,\n  SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport type { UniqueIdentifier } from \"@dnd-kit/core\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@openstatus/ui/components/ui/command\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { PopoverContent } from \"@openstatus/ui/components/ui/popover\";\nimport { Popover, PopoverTrigger } from \"@openstatus/ui/components/ui/popover\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport {\n  Check,\n  ChevronsUpDown,\n  GripVertical,\n  Plus,\n  Trash2,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useState, useTransition } from \"react\";\nimport { type UseFormReturn, useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\ntype Monitor = {\n  id: number;\n  name: string;\n  externalName: string | null;\n  url: string;\n  active: boolean | null;\n};\n\ntype MonitorGroup = {\n  id: number;\n  name: string;\n  monitors: Monitor[];\n};\n\nconst monitorSchema = z.object({\n  id: z.number(),\n  order: z.number(),\n  active: z.boolean().nullable(),\n});\n\nconst schema = z.object({\n  monitors: z.array(monitorSchema),\n  groups: z.array(\n    z.object({\n      id: z.number(),\n      order: z.number(),\n      name: z.string(),\n      monitors: z.array(monitorSchema).min(1, {\n        error: \"At least one monitor is required\",\n      }),\n    }),\n  ),\n});\n\nconst getSortedMonitors = (\n  monitors: Monitor[],\n  monitorData: { id: number; order: number }[],\n) => {\n  const orderMap = new Map(monitorData?.map((m) => [m.id, m.order]) ?? []);\n\n  return monitors\n    .filter((monitor) => orderMap.has(monitor.id))\n    .sort((a, b) => {\n      const aOrder = orderMap.get(a.id) ?? 0;\n      const bOrder = orderMap.get(b.id) ?? 0;\n      return aOrder - bOrder;\n    });\n};\n\nconst getSortedItems = (\n  monitors: Monitor[],\n  monitorData: { id: number; order: number }[],\n  groups: Array<{\n    id: number;\n    order: number;\n    name: string;\n    monitors: Array<{ id: number; order: number; active: boolean | null }>;\n  }>,\n): (Monitor | MonitorGroup)[] => {\n  // Create map of monitor orders\n  const monitorOrderMap = new Map(monitorData.map((m) => [m.id, m.order]));\n\n  // Create array of monitors with their orders\n  const monitorsWithOrder = monitors\n    .filter((monitor) => monitorOrderMap.has(monitor.id))\n    .map((monitor) => ({\n      item: monitor,\n      order: monitorOrderMap.get(monitor.id) ?? 0,\n    }));\n\n  // Create array of groups with their orders\n  const groupsWithOrder = groups.map((group) => ({\n    item: {\n      id: group.id,\n      name: group.name,\n      monitors: getSortedMonitors(monitors, group.monitors),\n    } as MonitorGroup,\n    order: group.order,\n  }));\n\n  // Combine and sort by order\n  return [...monitorsWithOrder, ...groupsWithOrder]\n    .sort((a, b) => a.order - b.order)\n    .map((entry) => entry.item);\n};\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormMonitors({\n  defaultValues,\n  onSubmit,\n  monitors,\n  legacy,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  monitors: Monitor[];\n  /**\n   * Whether the status page is legacy or new\n   */\n  legacy: boolean;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {},\n  });\n  const [isPending, startTransition] = useTransition();\n  const watchMonitors = form.watch(\"monitors\");\n  const watchGroups = form.watch(\"groups\");\n  const [data, setData] = useState<(Monitor | MonitorGroup)[]>(\n    getSortedItems(\n      monitors,\n      defaultValues?.monitors ?? [],\n      defaultValues?.groups ?? [],\n    ),\n  );\n\n  // Get all monitor IDs that are already used in groups\n  const monitorsInGroups = new Set(\n    (watchGroups ?? []).flatMap((g) => g.monitors.map((m) => m.id)),\n  );\n\n  useEffect(() => {\n    const sortedItems = getSortedItems(\n      monitors,\n      watchMonitors,\n      watchGroups ?? [],\n    );\n    setData(sortedItems);\n  }, [watchMonitors, watchGroups, monitors]);\n\n  const onValueChange = useCallback(\n    (newItems: (Monitor | MonitorGroup)[]) => {\n      setData(newItems);\n\n      // Update monitors with their position in the overall list\n      const monitors = newItems\n        .map((item, index) => ({ item, index }))\n        .filter(\n          (entry): entry is { item: Monitor; index: number } =>\n            \"url\" in entry.item,\n        )\n        .map(({ item, index }) => ({\n          id: item.id,\n          order: index,\n          active: item.active,\n        }));\n      form.setValue(\"monitors\", monitors);\n\n      // Update groups with their position in the overall list\n      const existingGroups = form.getValues(\"groups\") ?? [];\n      const groups = newItems\n        .map((item, index) => ({ item, index }))\n        .filter(\n          (entry): entry is { item: MonitorGroup; index: number } =>\n            \"monitors\" in entry.item && !(\"url\" in entry.item),\n        )\n        .map(({ item, index }) => {\n          const existingGroup = existingGroups.find((g) => g.id === item.id);\n          return existingGroup\n            ? {\n                ...existingGroup,\n                order: index,\n              }\n            : {\n                id: item.id,\n                order: index,\n                name: item.name,\n                monitors: [],\n              };\n        });\n      form.setValue(\"groups\", groups);\n    },\n    [form],\n  );\n\n  const getItemValue = useCallback(\n    (item: Monitor | MonitorGroup) => item.id,\n    [],\n  );\n\n  const handleAddGroup = useCallback(() => {\n    const newGroupId = Date.now();\n    const existingGroups = form.getValues(\"groups\") ?? [];\n    const existingMonitors = form.getValues(\"monitors\") ?? [];\n    const order = existingGroups.length + existingMonitors.length;\n    const newGroups = [\n      ...existingGroups,\n      { id: newGroupId, order, name: \"\", monitors: [] },\n    ];\n    form.setValue(\"groups\", newGroups);\n    setData((prev) => [\n      ...prev,\n      { id: newGroupId, order, name: \"\", monitors: [] },\n    ]);\n  }, [form]);\n\n  const handleDeleteGroup = useCallback(\n    (groupId: number) => {\n      const existingGroups = form.getValues(\"groups\") ?? [];\n      form.setValue(\n        \"groups\",\n        existingGroups.filter((g) => g.id !== groupId),\n      );\n      setData((prev) => prev.filter((item) => item.id !== groupId));\n    },\n    [form],\n  );\n\n  const renderOverlay = useCallback(\n    ({ value }: { value: UniqueIdentifier }) => {\n      const monitor = data.find((item) => item.id === value);\n      if (!monitor) return null;\n\n      if (\"url\" in monitor) {\n        return (\n          <MonitorRow\n            monitor={monitor}\n            form={form}\n            className=\"border-transparent border-x px-2\"\n          />\n        );\n      }\n\n      const groups = form.getValues(\"groups\") ?? [];\n      const groupIndex = groups.findIndex((g) => g.id === monitor.id);\n      return (\n        <MonitorGroup\n          group={monitor}\n          groupIndex={groupIndex}\n          onDeleteGroup={handleDeleteGroup}\n          form={form}\n          monitors={monitors}\n        />\n      );\n    },\n    [data, handleDeleteGroup, form, monitors],\n  );\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Monitors</FormCardTitle>\n            <FormCardDescription>\n              Connect your monitors to your status page.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n            <FormField\n              control={form.control}\n              name=\"monitors\"\n              render={({ field }) => (\n                <FormItem className=\"flex flex-col\">\n                  <FormLabel className=\"sr-only\">Monitors</FormLabel>\n                  <Popover>\n                    <PopoverTrigger asChild>\n                      <FormControl>\n                        <Button\n                          variant=\"outline\"\n                          role=\"combobox\"\n                          className={cn(\n                            \"w-full justify-between\",\n                            !field.value && \"text-muted-foreground\",\n                          )}\n                        >\n                          {field.value.length > 0\n                            ? `${field.value.length} monitors selected`\n                            : \"Select monitors\"}\n                          <ChevronsUpDown className=\"opacity-50\" />\n                        </Button>\n                      </FormControl>\n                    </PopoverTrigger>\n                    <PopoverContent className=\"p-0\">\n                      <Command>\n                        <CommandInput\n                          placeholder=\"Search monitors...\"\n                          className=\"h-9\"\n                        />\n                        <CommandList>\n                          <CommandEmpty>No monitors found.</CommandEmpty>\n                          <CommandGroup>\n                            {monitors.map((monitor) => {\n                              const isInGroup = monitorsInGroups.has(\n                                monitor.id,\n                              );\n                              const isSelected = field.value.some(\n                                (m) => m.id === monitor.id,\n                              );\n                              return (\n                                <CommandItem\n                                  value={monitor.name}\n                                  key={monitor.id}\n                                  disabled={isInGroup}\n                                  onSelect={() => {\n                                    if (isSelected) {\n                                      form.setValue(\n                                        \"monitors\",\n                                        field.value.filter(\n                                          (m) => m.id !== monitor.id,\n                                        ),\n                                      );\n                                    } else {\n                                      form.setValue(\"monitors\", [\n                                        ...field.value,\n                                        {\n                                          id: monitor.id,\n                                          order: watchMonitors.length,\n                                          active: monitor.active,\n                                        },\n                                      ]);\n                                    }\n                                  }}\n                                >\n                                  {monitor.name}\n                                  <Check\n                                    className={cn(\n                                      \"ml-auto\",\n                                      isSelected ? \"opacity-100\" : \"opacity-0\",\n                                    )}\n                                  />\n                                </CommandItem>\n                              );\n                            })}\n                          </CommandGroup>\n                        </CommandList>\n                      </Command>\n                    </PopoverContent>\n                  </Popover>\n                  <FormDescription>Choose monitors to display.</FormDescription>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n            {legacy ? (\n              <TooltipProvider>\n                <Tooltip>\n                  <TooltipTrigger asChild>\n                    <span className=\"w-full\">\n                      <Button\n                        variant=\"outline\"\n                        type=\"button\"\n                        className=\"w-full\"\n                        disabled={legacy}\n                      >\n                        <Plus />\n                        Add Group\n                      </Button>\n                    </span>\n                  </TooltipTrigger>\n                  <TooltipContent>\n                    <p>\n                      Enable the new redesign to add groups to your status page.\n                    </p>\n                  </TooltipContent>\n                </Tooltip>\n              </TooltipProvider>\n            ) : (\n              <Button\n                variant=\"outline\"\n                type=\"button\"\n                className=\"w-full\"\n                onClick={handleAddGroup}\n              >\n                <Plus />\n                Add Group\n              </Button>\n            )}\n          </FormCardContent>\n          <FormCardSeparator />\n          <FormCardContent>\n            <Sortable\n              value={data}\n              onValueChange={onValueChange}\n              getItemValue={getItemValue}\n              orientation=\"vertical\"\n            >\n              {data.length ? (\n                <SortableContent className=\"grid gap-2\">\n                  {data.map((item) => {\n                    if (\"url\" in item) {\n                      return (\n                        <MonitorRow\n                          key={`${item.id}-monitor`}\n                          className=\"border-transparent border-x px-2\"\n                          monitor={item}\n                          form={form}\n                        />\n                      );\n                    }\n                    const groups = form.getValues(\"groups\") ?? [];\n                    const groupIndex = groups.findIndex(\n                      (g) => g.id === item.id,\n                    );\n                    return (\n                      <MonitorGroup\n                        key={`${item.id}-group`}\n                        group={item}\n                        groupIndex={groupIndex}\n                        onDeleteGroup={handleDeleteGroup}\n                        form={form}\n                        monitors={monitors}\n                      />\n                    );\n                  })}\n                  <SortableOverlay>{renderOverlay}</SortableOverlay>\n                </SortableContent>\n              ) : (\n                <EmptyStateContainer>\n                  <EmptyStateTitle>No monitors selected</EmptyStateTitle>\n                </EmptyStateContainer>\n              )}\n            </Sortable>\n          </FormCardContent>\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about monitor <Link href=\"#\">display options</Link>.\n            </FormCardFooterInfo>\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending ? \"Submitting...\" : \"Submit\"}\n            </Button>\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n\ninterface MonitorRowProps\n  extends Omit<React.ComponentPropsWithoutRef<typeof SortableItem>, \"value\"> {\n  monitor: Monitor;\n  form: UseFormReturn<FormValues>;\n}\n\nfunction MonitorRow({ monitor, className, ...props }: MonitorRowProps) {\n  return (\n    <SortableItem\n      value={monitor.id}\n      asChild\n      className={cn(\"rounded-md\", className)}\n      {...props}\n    >\n      <div className=\"grid h-9 grid-cols-3 gap-2\">\n        <div className=\"flex flex-row items-center gap-4 self-center\">\n          <SortableItemHandle>\n            <GripVertical\n              size={16}\n              aria-hidden=\"true\"\n              className=\"text-muted-foreground\"\n            />\n          </SortableItemHandle>\n          <span className=\"truncate text-sm\">\n            {monitor.name}{\" \"}\n            <span className=\"text-muted-foreground\">\n              {monitor.externalName ? `(${monitor.externalName})` : \"\"}\n            </span>\n          </span>\n        </div>\n        <div className=\"self-center truncate text-muted-foreground text-sm\">\n          {monitor.url}\n        </div>\n        <div className=\"self-center truncate text-muted-foreground text-sm\">\n          {monitor.active ? \"Active\" : \"Inactive\"}\n        </div>\n      </div>\n    </SortableItem>\n  );\n}\n\ninterface MonitorGroupProps\n  extends Omit<React.ComponentPropsWithoutRef<typeof SortableItem>, \"value\"> {\n  group: MonitorGroup;\n  groupIndex: number;\n  onDeleteGroup: (groupId: number) => void;\n  form: UseFormReturn<FormValues>;\n  monitors: Monitor[];\n}\n\nfunction MonitorGroup({\n  group,\n  groupIndex,\n  onDeleteGroup,\n  form,\n  monitors,\n}: MonitorGroupProps) {\n  const watchGroup = form.watch(`groups.${groupIndex}`);\n  const watchMonitors = form.watch(\"monitors\");\n  const watchGroups = form.watch(\"groups\");\n  const [data, setData] = useState<Monitor[]>(group.monitors);\n\n  // Calculate taken monitors (in main list or other groups)\n  const takenMonitorIds = new Set([\n    ...watchMonitors.map((m) => m.id),\n    ...watchGroups\n      .filter((g) => g.id !== group.id)\n      .flatMap((g) => g.monitors.map((m) => m.id)),\n  ]);\n\n  const onValueChange = useCallback(\n    (newMonitors: Monitor[]) => {\n      setData(newMonitors);\n      // Update the form with the new monitor order\n      form.setValue(\n        `groups.${groupIndex}.monitors`,\n        newMonitors.map((m, index) => ({\n          id: m.id,\n          order: index,\n          active: m.active,\n        })),\n      );\n    },\n    [form, groupIndex],\n  );\n\n  useEffect(() => {\n    setData(getSortedMonitors(monitors, watchGroup.monitors));\n  }, [watchGroup.monitors, monitors]);\n\n  const getItemValue = useCallback((item: Monitor) => item.id, []);\n\n  const renderOverlay = useCallback(\n    ({ value }: { value: UniqueIdentifier }) => {\n      const monitor = data.find((item) => item.id === value);\n      if (!monitor) return null;\n\n      return <MonitorRow monitor={monitor} form={form} />;\n    },\n    [data, form],\n  );\n\n  return (\n    <SortableItem value={group.id} className=\"rounded-md border bg-muted\">\n      <div className=\"grid grid-cols-3 gap-2 px-2 pt-2\">\n        <div className=\"flex flex-row items-center gap-1 self-center\">\n          <SortableItemHandle>\n            <GripVertical\n              size={16}\n              aria-hidden=\"true\"\n              className=\"text-muted-foreground\"\n            />\n          </SortableItemHandle>\n          <FormField\n            key={`${group.id}-name-${groupIndex}`}\n            control={form.control}\n            name={`groups.${groupIndex}.name` as const}\n            render={({ field }) => (\n              <FormItem className=\"w-full\">\n                <FormLabel className=\"sr-only\">Group name</FormLabel>\n                <FormControl>\n                  <Input\n                    placeholder=\"Group Name\"\n                    className=\"w-full bg-background\"\n                    {...field}\n                  />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </div>\n        <FormField\n          key={`${group.id}-monitors-${groupIndex}`}\n          control={form.control}\n          name={`groups.${groupIndex}.monitors` as const}\n          render={({ field }) => (\n            <FormItem className=\"flex w-full flex-col\">\n              <FormLabel className=\"sr-only\">Monitors</FormLabel>\n              <Popover>\n                <PopoverTrigger asChild>\n                  <FormControl>\n                    <Button\n                      variant=\"outline\"\n                      role=\"combobox\"\n                      className={cn(\n                        \"w-full justify-between\",\n                        !field.value && \"text-muted-foreground\",\n                      )}\n                    >\n                      {Array.isArray(field.value) && field.value.length > 0\n                        ? `${field.value.length} monitors selected`\n                        : \"Select monitors\"}\n                      <ChevronsUpDown className=\"opacity-50\" />\n                    </Button>\n                  </FormControl>\n                </PopoverTrigger>\n                <PopoverContent className=\"p-0\">\n                  <Command>\n                    <CommandInput\n                      placeholder=\"Search monitors...\"\n                      className=\"h-9\"\n                    />\n                    <CommandList>\n                      <CommandEmpty>No monitors found.</CommandEmpty>\n                      <CommandGroup>\n                        {monitors.map((monitor) => {\n                          const current = field.value ?? [];\n                          const isSelected = current.some(\n                            (m) => m.id === monitor.id,\n                          );\n                          const isTaken = takenMonitorIds.has(monitor.id);\n                          return (\n                            <CommandItem\n                              value={monitor.name}\n                              key={monitor.id}\n                              disabled={isTaken}\n                              onSelect={() => {\n                                if (isSelected) {\n                                  form.setValue(\n                                    `groups.${groupIndex}.monitors`,\n                                    current.filter((m) => m.id !== monitor.id),\n                                  );\n                                } else {\n                                  form.setValue(\n                                    `groups.${groupIndex}.monitors`,\n                                    [\n                                      ...current,\n                                      {\n                                        id: monitor.id,\n                                        order: 0,\n                                        active: monitor.active,\n                                      },\n                                    ],\n                                  );\n                                }\n                              }}\n                            >\n                              {monitor.name}\n                              <Check\n                                className={cn(\n                                  \"ml-auto\",\n                                  isSelected ? \"opacity-100\" : \"opacity-0\",\n                                )}\n                              />\n                            </CommandItem>\n                          );\n                        })}\n                      </CommandGroup>\n                    </CommandList>\n                  </Command>\n                </PopoverContent>\n              </Popover>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n        <div className=\"flex justify-end\">\n          <AlertDialog>\n            <AlertDialogTrigger asChild>\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"text-destructive hover:bg-destructive/10 hover:text-destructive dark:hover:bg-destructive/20 [&_svg]:size-4 [&_svg]:text-destructive\"\n                // NOTE: delete directly if no monitors are in the group\n                {...(data.length === 0\n                  ? { onClick: () => onDeleteGroup(group.id) }\n                  : {})}\n              >\n                <Trash2 />\n              </Button>\n            </AlertDialogTrigger>\n            <AlertDialogContent>\n              <AlertDialogHeader>\n                <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n                <AlertDialogDescription>\n                  You are about to delete this group and all its monitors.\n                </AlertDialogDescription>\n              </AlertDialogHeader>\n              <AlertDialogFooter>\n                <AlertDialogCancel>Cancel</AlertDialogCancel>\n                <AlertDialogAction\n                  className=\"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\"\n                  onClick={() => onDeleteGroup(group.id)}\n                >\n                  Delete\n                </AlertDialogAction>\n              </AlertDialogFooter>\n            </AlertDialogContent>\n          </AlertDialog>\n        </div>\n      </div>\n      <div className=\"mt-2 border-t px-2 pt-2 pb-2\">\n        <Sortable\n          value={data}\n          onValueChange={onValueChange}\n          getItemValue={getItemValue}\n          orientation=\"vertical\"\n        >\n          {data.length ? (\n            <SortableContent className=\"grid gap-2\">\n              {data.map((item) => {\n                return (\n                  <MonitorRow\n                    key={`${item.id}-monitor`}\n                    monitor={item}\n                    form={form}\n                  />\n                );\n              })}\n              <SortableOverlay>{renderOverlay}</SortableOverlay>\n            </SortableContent>\n          ) : (\n            <EmptyStateContainer>\n              <EmptyStateTitle>No monitors selected</EmptyStateTitle>\n            </EmptyStateContainer>\n          )}\n        </Sortable>\n      </div>\n    </SortableItem>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-page/form-page-access.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardContentUpgrade,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardSeparator,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  RadioGroup,\n  RadioGroupItem,\n} from \"@openstatus/ui/components/ui/radio-group\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { Key, Lock, LockOpen, ShieldUser } from \"lucide-react\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst accessTypeSchema = z.enum([\"public\", \"password\", \"email-domain\"]);\n\nconst schema = z.object({\n  accessType: accessTypeSchema,\n  password: z.string().optional(),\n  authEmailDomains: z\n    .preprocess(\n      (val: string[] | undefined) =>\n        val\n          ? String(val)\n              .split(\",\")\n              .map((domain) => domain.trim())\n              .filter((domain) => domain.length > 0)\n          : [],\n      z.array(z.string()).optional(),\n    )\n    .optional(),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormPageAccess({\n  lockedMap,\n  defaultValues,\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  lockedMap?: Map<z.infer<typeof accessTypeSchema>, boolean>;\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const [isPending, startTransition] = useTransition();\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      accessType: \"public\",\n      password: \"\",\n      authEmailDomains: [],\n    },\n  });\n  const watchAccessType = form.watch(\"accessType\");\n  const locked = lockedMap?.get(watchAccessType);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        console.log(values);\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormCard>\n          <FormCardHeader>\n            <FormCardTitle>Page Access</FormCardTitle>\n            <FormCardDescription>\n              Enable protection for your status page. Choose between simple\n              password or email domain authentication via magic link.\n            </FormCardDescription>\n          </FormCardHeader>\n          <FormCardContent>\n            <FormField\n              control={form.control}\n              name=\"accessType\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Protection Type</FormLabel>\n                  <FormControl>\n                    <RadioGroup\n                      onValueChange={field.onChange}\n                      defaultValue={field.value}\n                      className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\"\n                    >\n                      {[\n                        { value: \"public\", icon: LockOpen, label: \"Public\" },\n                        { value: \"password\", icon: Key, label: \"Password\" },\n                        {\n                          value: \"email-domain\",\n                          icon: ShieldUser,\n                          label: \"Magic Link (Auth)\",\n                        },\n                      ].map((type) => {\n                        return (\n                          <FormItem\n                            key={type.value}\n                            className={cn(\n                              \"relative flex cursor-pointer flex-row items-center gap-3 rounded-md border border-input px-2 py-3 text-center shadow-xs outline-none transition-[color,box-shadow] has-aria-[invalid=true]:border-destructive has-data-[state=checked]:border-primary/50 has-focus-visible:border-ring has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50\",\n                            )}\n                          >\n                            <FormControl>\n                              <RadioGroupItem\n                                value={type.value}\n                                className=\"sr-only\"\n                              />\n                            </FormControl>\n                            <type.icon\n                              className=\"shrink-0 text-muted-foreground\"\n                              size={16}\n                              aria-hidden=\"true\"\n                            />\n                            <FormLabel className=\"cursor-pointer font-medium text-foreground text-xs leading-none after:absolute after:inset-0\">\n                              {type.label}\n                            </FormLabel>\n                          </FormItem>\n                        );\n                      })}\n                    </RadioGroup>\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          {watchAccessType && watchAccessType !== \"public\" ? (\n            <FormCardSeparator />\n          ) : null}\n          {watchAccessType === \"password\" ? (\n            <FormCardContent className=\"grid gap-4\">\n              {locked ? <FormCardContentUpgrade /> : null}\n              <FormField\n                control={form.control}\n                name=\"password\"\n                disabled={locked}\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Password</FormLabel>\n                    <FormControl>\n                      <Input {...field} />\n                    </FormControl>\n                    <FormMessage />\n                    <FormDescription>\n                      Set a password to your status page to have a very basic\n                      protection.\n                    </FormDescription>\n                  </FormItem>\n                )}\n              />\n            </FormCardContent>\n          ) : null}\n          {watchAccessType === \"email-domain\" ? (\n            <FormCardContent className=\"grid gap-4\">\n              {locked ? <FormCardContentUpgrade /> : null}\n              <FormField\n                control={form.control}\n                name=\"authEmailDomains\"\n                disabled={locked}\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Email Domains</FormLabel>\n                    <FormControl>\n                      <Input {...field} />\n                    </FormControl>\n                    <FormMessage />\n                    <FormDescription>\n                      Comma-separated list of email domains. Only emails from\n                      these domains will be authenticated to access the status\n                      page.\n                    </FormDescription>\n                  </FormItem>\n                )}\n              />\n            </FormCardContent>\n          ) : null}\n          <FormCardFooter>\n            <FormCardFooterInfo>\n              Learn more about{\" \"}\n              <Link\n                href=\"https://docs.openstatus.dev/reference/status-page/#password\"\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                Protection\n              </Link>\n              .\n            </FormCardFooterInfo>\n            {locked ? (\n              <Button type=\"button\" asChild>\n                <Link href=\"/settings/billing\">\n                  <Lock />\n                  Upgrade\n                </Link>\n              </Button>\n            ) : (\n              <Button type=\"submit\" disabled={isPending}>\n                {isPending ? \"Submitting...\" : \"Submit\"}\n              </Button>\n            )}\n          </FormCardFooter>\n        </FormCard>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-page/update.tsx",
    "content": "import { Link } from \"@/components/common/link\";\nimport { Note, NoteButton } from \"@/components/common/note\";\nimport { FormCardGroup } from \"@/components/forms/form-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { Info } from \"lucide-react\";\nimport { useParams, useRouter } from \"next/navigation\";\nimport { FormAppearance } from \"./form-appearance\";\nimport { FormCustomDomain } from \"./form-custom-domain\";\nimport { FormDangerZone } from \"./form-danger-zone\";\nimport { FormGeneral } from \"./form-general\";\nimport { FormLinks } from \"./form-links\";\nimport { FormPageAccess } from \"./form-page-access\";\n\nexport function FormStatusPageUpdate() {\n  const { id } = useParams<{ id: string }>();\n  const router = useRouter();\n  const trpc = useTRPC();\n  const { data: statusPage, refetch } = useQuery(\n    trpc.page.get.queryOptions({ id: Number.parseInt(id) }),\n  );\n  const queryClient = useQueryClient();\n  const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const updateStatusPageMutation = useMutation(\n    trpc.page.updateGeneral.mutationOptions({\n      onSuccess: () => {\n        refetch();\n        // NOTE: invalidate status page list to update name\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n      },\n    }),\n  );\n\n  const updatePasswordProtectionMutation = useMutation(\n    trpc.page.updatePasswordProtection.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  const updateCustomDomainMutation = useMutation(\n    trpc.page.updateCustomDomain.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  const updatePageAppearanceMutation = useMutation(\n    trpc.page.updateAppearance.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  const deleteStatusPageMutation = useMutation(\n    trpc.page.delete.mutationOptions({\n      onSuccess: () => {\n        router.push(\"/status-pages\");\n        // NOTE: invalidate workspace to update the usage\n        queryClient.invalidateQueries({\n          queryKey: trpc.workspace.get.queryKey(),\n        });\n        // NOTE: invalidate status page list to update the usage\n        queryClient.invalidateQueries({\n          queryKey: trpc.page.list.queryKey(),\n        });\n      },\n    }),\n  );\n\n  const updateLinksMutation = useMutation(\n    trpc.page.updateLinks.mutationOptions({\n      onSuccess: () => refetch(),\n    }),\n  );\n\n  if (!statusPage || !monitors || !workspace) return null;\n\n  return (\n    <FormCardGroup>\n      <Note color=\"warning\">\n        <Info />\n        <p className=\"text-sm\">\n          Looking to connect monitors to your status page? The setup now has a\n          separate page{\" \"}\n          <Link href={`/status-pages/${id}/components`}>components</Link>.\n        </p>\n        <NoteButton variant=\"default\" asChild>\n          <Link href=\"https://openstatus.dev/blog/status-page-components\">\n            Learn more\n          </Link>\n        </NoteButton>\n      </Note>\n      <FormGeneral\n        defaultValues={{\n          title: statusPage.title,\n          slug: statusPage.slug,\n          description: statusPage.description,\n          icon: statusPage.icon ?? undefined,\n        }}\n        onSubmit={async (values) => {\n          await updateStatusPageMutation.mutateAsync({\n            id: Number.parseInt(id),\n            title: values.title,\n            slug: values.slug,\n            description: values.description ?? \"\",\n            icon: values.icon ?? \"\",\n          });\n        }}\n      />\n      <FormCustomDomain\n        locked={workspace.limits[\"custom-domain\"] === false}\n        defaultValues={{\n          domain: statusPage.customDomain ?? undefined,\n        }}\n        onSubmit={async (values) => {\n          await updateCustomDomainMutation.mutateAsync({\n            id: Number.parseInt(id),\n            customDomain: values.domain,\n          });\n        }}\n      />\n      <FormLinks\n        defaultValues={{\n          homepageUrl: statusPage.homepageUrl ?? \"\",\n          contactUrl: statusPage.contactUrl ?? \"\",\n        }}\n        onSubmit={async (values) => {\n          await updateLinksMutation.mutateAsync({\n            id: Number.parseInt(id),\n            homepageUrl: values.homepageUrl ?? undefined,\n            contactUrl: values.contactUrl ?? undefined,\n          });\n        }}\n      />\n      <FormAppearance\n        defaultValues={{\n          forceTheme: statusPage.forceTheme ?? \"system\",\n          configuration: {\n            theme: statusPage.configuration?.theme ?? \"default\",\n          },\n        }}\n        onSubmit={async (values) => {\n          await updatePageAppearanceMutation.mutateAsync({\n            id: Number.parseInt(id),\n            forceTheme: values.forceTheme,\n            configuration: values.configuration,\n          });\n        }}\n      />\n      <FormPageAccess\n        lockedMap={\n          new Map([\n            [\"public\", false],\n            [\"password\", workspace.limits[\"password-protection\"] === false],\n            [\n              \"email-domain\",\n              workspace.limits[\"email-domain-protection\"] === false,\n            ],\n          ])\n        }\n        defaultValues={{\n          accessType: statusPage.accessType,\n          password: statusPage.password ?? undefined,\n          authEmailDomains: statusPage.authEmailDomains ?? [],\n        }}\n        onSubmit={async (values) => {\n          await updatePasswordProtectionMutation.mutateAsync({\n            id: Number.parseInt(id),\n            accessType: values.accessType,\n            password: values.password,\n            authEmailDomains: values.authEmailDomains,\n          });\n        }}\n      />\n      <FormDangerZone\n        title={statusPage.title}\n        onSubmit={async () => {\n          await deleteStatusPageMutation.mutateAsync({\n            id: Number.parseInt(id),\n          });\n        }}\n      />\n    </FormCardGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-report/form.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport { ProcessMessage } from \"@/components/content/process-message\";\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { colors } from \"@/data/status-report-updates.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  type PageComponent,\n  statusReportStatus,\n} from \"@openstatus/db/src/schema\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Calendar } from \"@openstatus/ui/components/ui/calendar\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { TabsContent } from \"@openstatus/ui/components/ui/tabs\";\nimport { TabsList, TabsTrigger } from \"@openstatus/ui/components/ui/tabs\";\nimport { Tabs } from \"@openstatus/ui/components/ui/tabs\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { format } from \"date-fns\";\nimport { CalendarIcon, ClockIcon } from \"lucide-react\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  status: z.enum(statusReportStatus),\n  title: z.string(),\n  message: z.string(),\n  date: z.date(),\n  pageComponents: z.array(z.number()),\n  notifySubscribers: z.boolean().optional(),\n});\n\nconst updateSchema = schema.omit({\n  message: true,\n  date: true,\n  notifySubscribers: true,\n});\n\nexport type FormValues = z.infer<typeof schema> | z.infer<typeof updateSchema>;\n\nexport function FormStatusReport({\n  defaultValues,\n  onSubmit,\n  className,\n  pageComponents,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  pageComponents: Pick<PageComponent, \"id\" | \"name\" | \"type\">[];\n}) {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(\n    trpc.workspace.getWorkspace.queryOptions(),\n  );\n  const mobile = useIsMobile();\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const form = useForm<FormValues>({\n    resolver: zodResolver(defaultValues ? updateSchema : schema),\n    defaultValues: defaultValues ?? {\n      status: \"investigating\",\n      title: \"\",\n      message: \"\",\n      date: new Date(),\n      pageComponents: [],\n      notifySubscribers: true,\n    },\n  });\n  const watchMessage = form.watch(\"message\");\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"title\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Title</FormLabel>\n                <FormControl>\n                  <Input {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"status\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Status</FormLabel>\n                <FormControl>\n                  <Select\n                    defaultValue={field.value}\n                    onValueChange={field.onChange}\n                  >\n                    <SelectTrigger\n                      className={cn(\n                        colors[field.value],\n                        \"font-mono capitalize\",\n                      )}\n                    >\n                      <SelectValue placeholder=\"Select a status\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      {statusReportStatus.map((status) => (\n                        <SelectItem\n                          key={status}\n                          value={status}\n                          className={cn(\"font-mono capitalize\", colors[status])}\n                        >\n                          {status}\n                        </SelectItem>\n                      ))}\n                    </SelectContent>\n                  </Select>\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        {!defaultValues ? (\n          <>\n            <FormCardSeparator />\n            <FormCardContent>\n              <FormField\n                control={form.control}\n                name=\"date\"\n                render={({ field }) => (\n                  <FormItem className=\"flex flex-col\">\n                    <FormLabel>Date</FormLabel>\n                    <Popover modal>\n                      <FormControl>\n                        <PopoverTrigger asChild>\n                          <Button\n                            type=\"button\"\n                            variant=\"outline\"\n                            size=\"sm\"\n                            className={cn(\n                              \"w-[240px] pl-3 text-left font-normal\",\n                              !field.value && \"text-muted-foreground\",\n                            )}\n                          >\n                            {field.value ? (\n                              format(field.value, \"PPP 'at' h:mm a\")\n                            ) : (\n                              <span>Pick a date</span>\n                            )}\n                            <CalendarIcon className=\"ml-auto h-4 w-4 opacity-50\" />\n                          </Button>\n                        </PopoverTrigger>\n                      </FormControl>\n                      <PopoverContent\n                        className=\"pointer-events-auto w-auto p-0\"\n                        align=\"start\"\n                        side={mobile ? \"bottom\" : \"left\"}\n                      >\n                        <Calendar\n                          mode=\"single\"\n                          selected={field.value}\n                          onSelect={(selectedDate) => {\n                            if (!selectedDate) return;\n                            const newDate = new Date(selectedDate);\n                            newDate.setHours(\n                              field.value.getHours(),\n                              field.value.getMinutes(),\n                              field.value.getSeconds(),\n                              field.value.getMilliseconds(),\n                            );\n                            field.onChange(newDate);\n                          }}\n                          disabled={(date) =>\n                            date > new Date() || date < new Date(\"1900-01-01\")\n                          }\n                          initialFocus\n                        />\n                        <div className=\"border-t p-3\">\n                          <div className=\"flex items-center gap-3\">\n                            <Label htmlFor=\"time\" className=\"text-xs\">\n                              Enter time\n                            </Label>\n                            <div className=\"relative grow\">\n                              <Input\n                                id=\"time\"\n                                type=\"time\"\n                                step=\"1\"\n                                defaultValue={new Date()\n                                  .toTimeString()\n                                  .slice(0, 8)}\n                                className=\"peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n                                onChange={(e) => {\n                                  try {\n                                    const timeValue = e.target.value;\n                                    if (!timeValue || !field.value) return;\n\n                                    const [hours, minutes, seconds] = timeValue\n                                      .split(\":\")\n                                      .map(Number);\n\n                                    const newDate = new Date(field.value);\n                                    newDate.setHours(\n                                      hours,\n                                      minutes,\n                                      seconds || 0,\n                                      0,\n                                    );\n\n                                    field.onChange(newDate);\n                                  } catch (error) {\n                                    console.error(error);\n                                  }\n                                }}\n                              />\n                              <div className=\"pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80 peer-disabled:opacity-50\">\n                                <ClockIcon size={16} aria-hidden=\"true\" />\n                              </div>\n                            </div>\n                          </div>\n                        </div>\n                      </PopoverContent>\n                    </Popover>\n                    <FormDescription>\n                      When the status report was created. Shown in your timezone\n                      (\n                      <code className=\"font-commit-mono text-foreground/70\">\n                        {timezone}\n                      </code>\n                      ) and saved as Unix time (\n                      <code className=\"font-commit-mono text-foreground/70\">\n                        UTC\n                      </code>\n                      ).\n                    </FormDescription>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            </FormCardContent>\n            <FormCardSeparator />\n            <FormCardContent>\n              <Tabs defaultValue=\"tab-1\">\n                <TabsList>\n                  <TabsTrigger value=\"tab-1\">Writing</TabsTrigger>\n                  <TabsTrigger value=\"tab-2\">Preview</TabsTrigger>\n                </TabsList>\n                <TabsContent value=\"tab-1\">\n                  <FormField\n                    control={form.control}\n                    name=\"message\"\n                    render={({ field }) => (\n                      <FormItem>\n                        <FormLabel>Message</FormLabel>\n                        <FormControl>\n                          <Textarea rows={6} {...field} />\n                        </FormControl>\n                        <FormMessage />\n                        <FormDescription>Markdown support</FormDescription>\n                      </FormItem>\n                    )}\n                  />\n                </TabsContent>\n                <TabsContent value=\"tab-2\">\n                  <div className=\"grid gap-2\">\n                    <Label>Preview</Label>\n                    <div className=\"prose dark:prose-invert prose-sm rounded-md border px-3 py-2 text-foreground text-sm\">\n                      <ProcessMessage value={watchMessage} />\n                    </div>\n                  </div>\n                </TabsContent>\n              </Tabs>\n            </FormCardContent>\n          </>\n        ) : null}\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"pageComponents\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Page Components</FormLabel>\n                <FormDescription>\n                  Select the page components you want to notify.\n                </FormDescription>\n                {pageComponents.length ? (\n                  <div className=\"grid gap-3\">\n                    <div className=\"flex items-center gap-2\">\n                      <FormControl>\n                        <Checkbox\n                          id=\"all\"\n                          checked={\n                            field.value?.length === pageComponents.length\n                          }\n                          onCheckedChange={(checked) => {\n                            field.onChange(\n                              checked ? pageComponents.map((c) => c.id) : [],\n                            );\n                          }}\n                        />\n                      </FormControl>\n                      <Label htmlFor=\"all\">Select all</Label>\n                    </div>\n                    {pageComponents.map((item) => (\n                      <div key={item.id} className=\"flex items-center gap-2\">\n                        <FormControl>\n                          <Checkbox\n                            id={String(item.id)}\n                            checked={field.value?.includes(item.id)}\n                            onCheckedChange={(checked) => {\n                              const newValue = checked\n                                ? [...(field.value || []), item.id]\n                                : field.value?.filter((id) => id !== item.id);\n                              field.onChange(newValue);\n                            }}\n                          />\n                        </FormControl>\n                        <Label htmlFor={String(item.id)}>{item.name}</Label>\n                      </div>\n                    ))}\n                  </div>\n                ) : (\n                  <EmptyStateContainer>\n                    <EmptyStateTitle>No page components found</EmptyStateTitle>\n                  </EmptyStateContainer>\n                )}\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        {!defaultValues && workspace?.limits[\"status-subscribers\"] ? (\n          <>\n            <FormCardSeparator />\n            <FormCardContent>\n              <FormField\n                control={form.control}\n                name=\"notifySubscribers\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Notify Subscribers</FormLabel>\n                    <FormControl>\n                      <div className=\"flex items-center gap-2\">\n                        <Checkbox\n                          id=\"notifySubscribers\"\n                          checked={field.value}\n                          onCheckedChange={field.onChange}\n                        />\n                        <Label htmlFor=\"notifySubscribers\">\n                          Send email notification to subscribers\n                        </Label>\n                      </div>\n                    </FormControl>\n                    <FormMessage />\n                    <FormDescription>\n                      Subscribers will receive an email when creating a status\n                      report.\n                    </FormDescription>\n                  </FormItem>\n                )}\n              />\n            </FormCardContent>\n          </>\n        ) : null}\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-report/sheet.tsx",
    "content": "\"use client\";\n\nimport { FormCard, FormCardGroup } from \"@/components/forms/form-card\";\nimport {\n  FormSheetContent,\n  FormSheetDescription,\n  FormSheetFooter,\n  FormSheetHeader,\n  FormSheetTitle,\n  FormSheetTrigger,\n  FormSheetWithDirtyProtection,\n} from \"@/components/forms/form-sheet\";\nimport {\n  FormStatusReport,\n  type FormValues,\n} from \"@/components/forms/status-report/form\";\nimport type { PageComponent } from \"@openstatus/db/src/schema\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { useState } from \"react\";\n\nexport function FormSheetStatusReport({\n  children,\n  defaultValues,\n  onSubmit,\n  pageComponents,\n  warning,\n}: Omit<React.ComponentProps<typeof FormSheetTrigger>, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  pageComponents: Pick<PageComponent, \"id\" | \"name\" | \"type\">[];\n  warning?: React.ReactNode;\n}) {\n  const [open, setOpen] = useState(false);\n\n  return (\n    <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}>\n      <FormSheetTrigger asChild>{children}</FormSheetTrigger>\n      <FormSheetContent className=\"sm:max-w-lg\">\n        <FormSheetHeader>\n          <FormSheetTitle>Status Report</FormSheetTitle>\n          <FormSheetDescription>\n            Configure and update the status of your report.\n          </FormSheetDescription>\n        </FormSheetHeader>\n        {warning ? (\n          <>\n            <p className=\"px-4 py-4 text-sm text-warning\">{warning}</p>\n            <Separator />\n          </>\n        ) : null}\n        <FormCardGroup className=\"overflow-y-scroll\">\n          <FormCard className=\"overflow-auto rounded-none border-none\">\n            <FormStatusReport\n              id=\"status-report-form\"\n              className=\"my-4\"\n              onSubmit={async (values) => {\n                await onSubmit(values);\n                setOpen(false);\n              }}\n              defaultValues={defaultValues}\n              pageComponents={pageComponents}\n            />\n          </FormCard>\n        </FormCardGroup>\n        <FormSheetFooter>\n          <Button type=\"submit\" form=\"status-report-form\">\n            Submit\n          </Button>\n        </FormSheetFooter>\n      </FormSheetContent>\n    </FormSheetWithDirtyProtection>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-report-update/form-status-report.tsx",
    "content": "\"use client\";\n\nimport { ProcessMessage } from \"@/components/content/process-message\";\nimport { FormAlertDialog } from \"@/components/forms/form-alert-dialog\";\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport {\n  FormCard,\n  FormCardFooter,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { colors } from \"@/data/status-report-updates.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  type StatusReportUpdate,\n  statusReportStatus,\n} from \"@openstatus/db/src/schema\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Calendar } from \"@openstatus/ui/components/ui/calendar\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { TabsContent } from \"@openstatus/ui/components/ui/tabs\";\nimport { TabsList, TabsTrigger } from \"@openstatus/ui/components/ui/tabs\";\nimport { Tabs } from \"@openstatus/ui/components/ui/tabs\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { format } from \"date-fns\";\nimport { CalendarIcon, ClockIcon } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  status: z.enum(statusReportStatus),\n  message: z.string(),\n  date: z.date(),\n  notifySubscribers: z.boolean().optional(),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function FormStatusReportUpdateCard({\n  defaultValues,\n  onSubmit,\n  className,\n  index,\n  update,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: FormValues;\n  onSubmit: (values: FormValues) => Promise<void>;\n  index: number;\n  update: StatusReportUpdate;\n}) {\n  const { reportId } = useParams<{ id: string; reportId: string }>();\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(\n    trpc.workspace.getWorkspace.queryOptions(),\n  );\n  const mobile = useIsMobile();\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: defaultValues ?? {\n      status: \"identified\",\n      message: \"\",\n      date: new Date(),\n      notifySubscribers: true,\n    },\n  });\n  const watchMessage = form.watch(\"message\");\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  const { data: statusReport, refetch } = useQuery(\n    trpc.statusReport.get.queryOptions({ id: Number.parseInt(reportId) }),\n  );\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  const deleteStatusReportUpdateMutation = useMutation(\n    trpc.statusReport.deleteUpdate.mutationOptions({\n      onSuccess: () => {\n        refetch();\n      },\n    }),\n  );\n\n  const updates = [...(statusReport?.updates ?? [])].sort(\n    (a, b) => b.date.getTime() - a.date.getTime(),\n  );\n\n  return (\n    <FormCard>\n      <FormCardHeader>\n        <FormCardTitle>\n          Status Report Update #{updates.length - index}\n        </FormCardTitle>\n      </FormCardHeader>\n\n      <Form {...form}>\n        <form\n          className={cn(\"grid gap-4\", className)}\n          onSubmit={form.handleSubmit(submitAction)}\n          {...props}\n        >\n          <FormCardContent className=\"grid gap-4 sm:grid-cols-3\">\n            <FormField\n              control={form.control}\n              name=\"status\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Status</FormLabel>\n                  <FormControl>\n                    <Select\n                      defaultValue={field.value}\n                      onValueChange={field.onChange}\n                    >\n                      <SelectTrigger\n                        className={cn(\n                          colors[field.value],\n                          \"w-full font-mono capitalize\",\n                        )}\n                      >\n                        <SelectValue placeholder=\"Select a status\" />\n                      </SelectTrigger>\n                      <SelectContent>\n                        {statusReportStatus.map((status) => (\n                          <SelectItem\n                            key={status}\n                            value={status}\n                            className={cn(\n                              colors[status],\n                              \"font-mono capitalize\",\n                            )}\n                          >\n                            {status}\n                          </SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"date\"\n              render={({ field }) => (\n                <FormItem className=\"flex flex-col\">\n                  <FormLabel>Date</FormLabel>\n                  <Popover modal>\n                    <FormControl>\n                      <PopoverTrigger asChild>\n                        <Button\n                          type=\"button\"\n                          variant=\"outline\"\n                          size=\"sm\"\n                          className={cn(\n                            \"h-9 w-full pl-3 text-left font-normal sm:w-[240px]\",\n                            !field.value && \"text-muted-foreground\",\n                          )}\n                        >\n                          {field.value ? (\n                            format(field.value, \"PPP 'at' h:mm a\")\n                          ) : (\n                            <span>Pick a date</span>\n                          )}\n                          <CalendarIcon className=\"ml-auto h-4 w-4 opacity-50\" />\n                        </Button>\n                      </PopoverTrigger>\n                    </FormControl>\n                    <PopoverContent\n                      className=\"pointer-events-auto w-auto p-0\"\n                      align=\"start\"\n                      side={mobile ? \"bottom\" : \"left\"}\n                    >\n                      <Calendar\n                        mode=\"single\"\n                        selected={field.value}\n                        onSelect={(selectedDate) => {\n                          if (!selectedDate) return;\n                          const newDate = new Date(selectedDate);\n                          newDate.setHours(\n                            field.value.getHours(),\n                            field.value.getMinutes(),\n                            field.value.getSeconds(),\n                            field.value.getMilliseconds(),\n                          );\n                          field.onChange(newDate);\n                        }}\n                        disabled={(date) =>\n                          date > new Date() || date < new Date(\"1900-01-01\")\n                        }\n                        initialFocus\n                      />\n                      <div className=\"border-t p-3\">\n                        <div className=\"flex items-center gap-3\">\n                          <Label htmlFor=\"time\" className=\"text-xs\">\n                            Enter time\n                          </Label>\n                          <div className=\"relative grow\">\n                            <Input\n                              id=\"time\"\n                              type=\"time\"\n                              step=\"1\"\n                              defaultValue={new Date()\n                                .toTimeString()\n                                .slice(0, 8)}\n                              className=\"peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n                              onChange={(e) => {\n                                try {\n                                  const timeValue = e.target.value;\n                                  if (!timeValue || !field.value) return;\n                                  const [hours, minutes, seconds] = timeValue\n                                    .split(\":\")\n                                    .map(Number);\n                                  const newDate = new Date(field.value);\n                                  newDate.setHours(\n                                    hours,\n                                    minutes,\n                                    seconds || 0,\n                                    0,\n                                  );\n                                  field.onChange(newDate);\n                                } catch (error) {\n                                  console.error(error);\n                                }\n                              }}\n                            />\n                            <div className=\"pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80 peer-disabled:opacity-50\">\n                              <ClockIcon size={16} aria-hidden=\"true\" />\n                            </div>\n                          </div>\n                        </div>\n                      </div>\n                    </PopoverContent>\n                  </Popover>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </FormCardContent>\n          <FormCardContent>\n            <FormDescription>\n              When the status report was created. Shown in your timezone (\n              <code className=\"font-commit-mono text-foreground/70\">\n                {timezone}\n              </code>\n              ) and saved as Unix time (\n              <code className=\"font-commit-mono text-foreground/70\">UTC</code>\n              ).\n            </FormDescription>\n          </FormCardContent>\n          <FormCardSeparator />\n          <FormCardContent>\n            <Tabs defaultValue=\"tab-1\">\n              <TabsList>\n                <TabsTrigger value=\"tab-1\">Writing</TabsTrigger>\n                <TabsTrigger value=\"tab-2\">Preview</TabsTrigger>\n              </TabsList>\n              <TabsContent value=\"tab-1\">\n                <FormField\n                  control={form.control}\n                  name=\"message\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormLabel>Message</FormLabel>\n                      <FormControl>\n                        <Textarea rows={6} {...field} />\n                      </FormControl>\n                      <FormMessage />\n                      <FormDescription>Markdown support</FormDescription>\n                    </FormItem>\n                  )}\n                />\n              </TabsContent>\n              <TabsContent value=\"tab-2\">\n                <div className=\"grid gap-2\">\n                  <Label>Preview</Label>\n                  <div className=\"prose prose-sm dark:prose-invert rounded-md border px-3 py-2 text-foreground text-sm\">\n                    <ProcessMessage value={watchMessage} />\n                  </div>\n                </div>\n              </TabsContent>\n            </Tabs>\n          </FormCardContent>\n          {!defaultValues && workspace?.limits[\"status-subscribers\"] ? (\n            <>\n              <FormCardSeparator />\n              <FormCardContent>\n                <FormField\n                  control={form.control}\n                  name=\"notifySubscribers\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormLabel>Notify Subscribers</FormLabel>\n                      <FormControl>\n                        <div className=\"flex items-center gap-2\">\n                          <Checkbox\n                            id=\"notifySubscribers\"\n                            checked={field.value}\n                            onCheckedChange={field.onChange}\n                          />\n                          <Label htmlFor=\"notifySubscribers\">\n                            Send email notification to subscribers\n                          </Label>\n                        </div>\n                      </FormControl>\n                      <FormMessage />\n                      <FormDescription>\n                        Subscribers will receive an email when creating a status\n                        report.\n                      </FormDescription>\n                    </FormItem>\n                  )}\n                />\n              </FormCardContent>\n            </>\n          ) : null}\n        </form>\n      </Form>\n      <FormCardFooter className=\"flex items-center justify-end gap-2 [&>:last-child]:ml-0\">\n        <FormAlertDialog\n          confirmationValue={update.status}\n          submitAction={async () => {\n            await deleteStatusReportUpdateMutation.mutateAsync({\n              id: update.id,\n            });\n          }}\n        >\n          <Button\n            variant=\"outline\"\n            className=\"text-destructive hover:bg-destructive/10 hover:text-destructive\"\n          >\n            Delete\n          </Button>\n        </FormAlertDialog>\n        <Button type=\"submit\" form={`update-form-${update.id}`}>\n          {isPending ? \"Submitting...\" : \"Submit\"}\n        </Button>\n      </FormCardFooter>\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-report-update/form.tsx",
    "content": "\"use client\";\n\nimport { ProcessMessage } from \"@/components/content/process-message\";\nimport {\n  FormCardContent,\n  FormCardSeparator,\n} from \"@/components/forms/form-card\";\nimport { useFormSheetDirty } from \"@/components/forms/form-sheet\";\nimport { colors } from \"@/data/status-report-updates.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { statusReportStatus } from \"@openstatus/db/src/schema\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Calendar } from \"@openstatus/ui/components/ui/calendar\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport {\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { Popover } from \"@openstatus/ui/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { TabsContent } from \"@openstatus/ui/components/ui/tabs\";\nimport { TabsList, TabsTrigger } from \"@openstatus/ui/components/ui/tabs\";\nimport { Tabs } from \"@openstatus/ui/components/ui/tabs\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { format } from \"date-fns\";\nimport { CalendarIcon, ClockIcon } from \"lucide-react\";\nimport React, { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  status: z.enum(statusReportStatus),\n  message: z.string(),\n  date: z.date(),\n  notifySubscribers: z.boolean().optional(),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function FormStatusReportUpdate({\n  defaultValues,\n  onSubmit,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  defaultValues?: Partial<FormValues>;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(\n    trpc.workspace.getWorkspace.queryOptions(),\n  );\n  const mobile = useIsMobile();\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      status: defaultValues?.status ?? \"identified\",\n      message: defaultValues?.message ?? \"\",\n      date: defaultValues?.date ?? new Date(),\n      notifySubscribers: defaultValues?.notifySubscribers ?? true,\n    },\n  });\n  const watchMessage = form.watch(\"message\");\n  const [isPending, startTransition] = useTransition();\n  const { setIsDirty } = useFormSheetDirty();\n\n  const formIsDirty = form.formState.isDirty;\n  React.useEffect(() => {\n    setIsDirty(formIsDirty);\n  }, [formIsDirty, setIsDirty]);\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Saving...\",\n          success: () => \"Saved\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to save\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\"grid gap-4\", className)}\n        onSubmit={form.handleSubmit(submitAction)}\n        {...props}\n      >\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"status\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Status</FormLabel>\n                <FormControl>\n                  <Select\n                    defaultValue={field.value}\n                    onValueChange={field.onChange}\n                  >\n                    <SelectTrigger\n                      className={cn(\n                        colors[field.value],\n                        \"font-mono capitalize\",\n                      )}\n                    >\n                      <SelectValue placeholder=\"Select a status\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      {statusReportStatus.map((status) => (\n                        <SelectItem\n                          key={status}\n                          value={status}\n                          className={cn(colors[status], \"font-mono capitalize\")}\n                        >\n                          {status}\n                        </SelectItem>\n                      ))}\n                    </SelectContent>\n                  </Select>\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <FormField\n            control={form.control}\n            name=\"date\"\n            render={({ field }) => (\n              <FormItem className=\"flex flex-col\">\n                <FormLabel>Date</FormLabel>\n                <Popover modal>\n                  <FormControl>\n                    <PopoverTrigger asChild>\n                      <Button\n                        type=\"button\"\n                        variant=\"outline\"\n                        size=\"sm\"\n                        className={cn(\n                          \"w-[240px] pl-3 text-left font-normal\",\n                          !field.value && \"text-muted-foreground\",\n                        )}\n                      >\n                        {field.value ? (\n                          format(field.value, \"PPP 'at' h:mm a\")\n                        ) : (\n                          <span>Pick a date</span>\n                        )}\n                        <CalendarIcon className=\"ml-auto h-4 w-4 opacity-50\" />\n                      </Button>\n                    </PopoverTrigger>\n                  </FormControl>\n                  <PopoverContent\n                    className=\"pointer-events-auto w-auto p-0\"\n                    align=\"start\"\n                    side={mobile ? \"bottom\" : \"left\"}\n                  >\n                    <Calendar\n                      mode=\"single\"\n                      selected={field.value}\n                      onSelect={(selectedDate) => {\n                        if (!selectedDate) return;\n                        const newDate = new Date(selectedDate);\n                        newDate.setHours(\n                          field.value.getHours(),\n                          field.value.getMinutes(),\n                          field.value.getSeconds(),\n                          field.value.getMilliseconds(),\n                        );\n                        field.onChange(newDate);\n                      }}\n                      disabled={(date) =>\n                        date > new Date() || date < new Date(\"1900-01-01\")\n                      }\n                      initialFocus\n                    />\n                    <div className=\"border-t p-3\">\n                      <div className=\"flex items-center gap-3\">\n                        <Label htmlFor=\"time\" className=\"text-xs\">\n                          Enter time\n                        </Label>\n                        <div className=\"relative grow\">\n                          <Input\n                            id=\"time\"\n                            type=\"time\"\n                            step=\"1\"\n                            defaultValue={new Date().toTimeString().slice(0, 8)}\n                            className=\"peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n                            onChange={(e) => {\n                              try {\n                                const timeValue = e.target.value;\n                                if (!timeValue || !field.value) return;\n\n                                const [hours, minutes, seconds] = timeValue\n                                  .split(\":\")\n                                  .map(Number);\n\n                                const newDate = new Date(field.value);\n                                newDate.setHours(\n                                  hours,\n                                  minutes,\n                                  seconds || 0,\n                                  0,\n                                );\n\n                                field.onChange(newDate);\n                              } catch (error) {\n                                console.error(error);\n                              }\n                            }}\n                          />\n                          <div className=\"pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80 peer-disabled:opacity-50\">\n                            <ClockIcon size={16} aria-hidden=\"true\" />\n                          </div>\n                        </div>\n                      </div>\n                    </div>\n                  </PopoverContent>\n                </Popover>\n                <FormDescription>\n                  When the status report was created. Shown in your timezone (\n                  <code className=\"font-commit-mono text-foreground/70\">\n                    {timezone}\n                  </code>\n                  ) and saved as Unix time (\n                  <code className=\"font-commit-mono text-foreground/70\">\n                    UTC\n                  </code>\n                  ).\n                </FormDescription>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </FormCardContent>\n        <FormCardSeparator />\n        <FormCardContent>\n          <Tabs defaultValue=\"tab-1\">\n            <TabsList>\n              <TabsTrigger value=\"tab-1\">Writing</TabsTrigger>\n              <TabsTrigger value=\"tab-2\">Preview</TabsTrigger>\n            </TabsList>\n            <TabsContent value=\"tab-1\">\n              <FormField\n                control={form.control}\n                name=\"message\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Message</FormLabel>\n                    <FormControl>\n                      <Textarea rows={6} {...field} />\n                    </FormControl>\n                    <FormMessage />\n                    <FormDescription>Markdown support</FormDescription>\n                  </FormItem>\n                )}\n              />\n            </TabsContent>\n            <TabsContent value=\"tab-2\">\n              <div className=\"grid gap-2\">\n                <Label>Preview</Label>\n                <div className=\"prose prose-sm dark:prose-invert rounded-md border px-3 py-2 text-foreground text-sm\">\n                  <ProcessMessage value={watchMessage} />\n                </div>\n              </div>\n            </TabsContent>\n          </Tabs>\n        </FormCardContent>\n        {!defaultValues?.date && workspace?.limits[\"status-subscribers\"] ? (\n          <>\n            <FormCardSeparator />\n            <FormCardContent>\n              <FormField\n                control={form.control}\n                name=\"notifySubscribers\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Notify Subscribers</FormLabel>\n                    <FormControl>\n                      <div className=\"flex items-center gap-2\">\n                        <Checkbox\n                          id=\"notifySubscribers\"\n                          checked={field.value}\n                          onCheckedChange={field.onChange}\n                        />\n                        <Label htmlFor=\"notifySubscribers\">\n                          Send email notification to subscribers\n                        </Label>\n                      </div>\n                    </FormControl>\n                    <FormMessage />\n                    <FormDescription>\n                      Subscribers will receive an email when creating a status\n                      report.\n                    </FormDescription>\n                  </FormItem>\n                )}\n              />\n            </FormCardContent>\n          </>\n        ) : null}\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/status-report-update/sheet.tsx",
    "content": "\"use client\";\n\nimport { FormCard, FormCardGroup } from \"@/components/forms/form-card\";\nimport {\n  FormSheetContent,\n  FormSheetDescription,\n  FormSheetFooter,\n  FormSheetHeader,\n  FormSheetTitle,\n  FormSheetTrigger,\n  FormSheetWithDirtyProtection,\n} from \"@/components/forms/form-sheet\";\nimport {\n  FormStatusReportUpdate,\n  type FormValues,\n} from \"@/components/forms/status-report-update/form\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useState } from \"react\";\n\nexport function FormSheetStatusReportUpdate({\n  children,\n  defaultValues,\n  onSubmit,\n}: Omit<React.ComponentProps<typeof FormSheetTrigger>, \"onSubmit\"> & {\n  defaultValues?: Partial<FormValues>;\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const [open, setOpen] = useState(false);\n  return (\n    <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}>\n      <FormSheetTrigger asChild>{children}</FormSheetTrigger>\n      <FormSheetContent className=\"sm:max-w-lg\">\n        <FormSheetHeader>\n          <FormSheetTitle>Status Report Update</FormSheetTitle>\n          <FormSheetDescription>\n            Configure and update the status of your report.\n          </FormSheetDescription>\n        </FormSheetHeader>\n        <FormCardGroup className=\"overflow-y-scroll\">\n          <FormCard className=\"overflow-auto rounded-none border-none\">\n            <FormStatusReportUpdate\n              id=\"status-report-update-form\"\n              className=\"my-4\"\n              onSubmit={async (values) => {\n                await onSubmit(values);\n                setOpen(false);\n              }}\n              defaultValues={defaultValues}\n            />\n          </FormCard>\n        </FormCardGroup>\n        <FormSheetFooter>\n          <Button type=\"submit\" form=\"status-report-update-form\">\n            Submit\n          </Button>\n        </FormSheetFooter>\n      </FormSheetContent>\n    </FormSheetWithDirtyProtection>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/support-contact/dialog.tsx",
    "content": "import { Link } from \"@/components/common/link\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@openstatus/ui/components/ui/dialog\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\nimport { ContactForm, type FormValues } from \"./form\";\n\nexport function FormDialogSupportContact({\n  children,\n  defaultValues,\n  ...props\n}: React.ComponentProps<typeof DialogTrigger> & {\n  defaultValues?: FormValues;\n}) {\n  const [open, setOpen] = useState(false);\n  const isMobile = useIsMobile();\n  const trpc = useTRPC();\n  const { data: user } = useQuery(trpc.user.get.queryOptions());\n  const feedbackMutation = useMutation(trpc.feedback.submit.mutationOptions());\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger {...props} asChild>\n        {children}\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Support</DialogTitle>\n          <DialogDescription>\n            Please fill out the form below to get in touch with us. Or send us\n            an email to{\" \"}\n            <Link href=\"mailto:ping@openstatus.dev\">ping@openstatus.dev</Link>.\n          </DialogDescription>\n        </DialogHeader>\n        <ContactForm\n          defaultValues={{\n            name: defaultValues?.name ?? user?.name ?? undefined,\n            email: defaultValues?.email ?? user?.email ?? undefined,\n            type: defaultValues?.type,\n            message: defaultValues?.message,\n            blocker: defaultValues?.blocker,\n          }}\n          onSubmit={async (data) => {\n            await feedbackMutation.mutateAsync({\n              name: data.name,\n              email: data.email,\n              type: data.type,\n              message: data.message,\n              blocker: data.blocker,\n              path: window.location.pathname,\n              isMobile,\n            });\n            setOpen(false);\n          }}\n        />\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/forms/support-contact/form.tsx",
    "content": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@openstatus/ui/components/ui/form\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { SelectItem } from \"@openstatus/ui/components/ui/select\";\nimport {\n  SelectContent,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { SelectTrigger } from \"@openstatus/ui/components/ui/select\";\nimport { Select } from \"@openstatus/ui/components/ui/select\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { toast } from \"sonner\";\n\nexport const types = [\n  {\n    label: \"Report a bug\",\n    value: \"bug\" as const,\n  },\n  {\n    label: \"Book a demo\",\n    value: \"demo\" as const,\n  },\n  {\n    label: \"Suggest a feature\",\n    value: \"feature\" as const,\n  },\n  {\n    label: \"Report a security issue\",\n    value: \"security\" as const,\n  },\n  {\n    label: \"Something else\",\n    value: \"question\" as const,\n  },\n];\n\nexport const schema = z.object({\n  name: z.string().min(1, {\n    error: \"Name is required\",\n  }),\n  type: z.enum([\"bug\", \"demo\", \"feature\", \"security\", \"question\"]),\n  email: z.email({\n    error: \"Invalid email address\",\n  }),\n  message: z.string().min(1, {\n    error: \"Message is required\",\n  }),\n  blocker: z.boolean(),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\ninterface ContactFormProps {\n  defaultValues?: Partial<FormValues>;\n  onSubmit: (data: FormValues) => Promise<void>;\n  className?: string;\n}\n\nexport function ContactForm({\n  defaultValues,\n  onSubmit,\n  className,\n}: ContactFormProps) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      name: defaultValues?.name ?? \"\",\n      email: defaultValues?.email ?? \"\",\n      type: defaultValues?.type ?? undefined,\n      message: defaultValues?.message ?? \"\",\n      blocker: defaultValues?.blocker ?? false,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n  const watchType = form.watch(\"type\");\n\n  async function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Sending message...\",\n          success: \"Message sent. We'll get back to you soon.\",\n          error: \"Failed to send message. Please try again.\",\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        onSubmit={form.handleSubmit(submitAction)}\n        className={cn(\"grid gap-4 sm:grid-cols-2\", className)}\n      >\n        <FormField\n          control={form.control}\n          name=\"name\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Name</FormLabel>\n              <FormControl>\n                <Input placeholder=\"Max\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n        <FormField\n          control={form.control}\n          name=\"email\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Email</FormLabel>\n              <FormControl>\n                <Input placeholder=\"max@openstatus.dev\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n        <FormField\n          control={form.control}\n          name=\"type\"\n          render={({ field }) => (\n            <FormItem className=\"sm:col-span-full\">\n              <FormLabel>Type</FormLabel>\n              <Select onValueChange={field.onChange} defaultValue={field.value}>\n                <FormControl>\n                  <SelectTrigger className=\"w-full\">\n                    <SelectValue placeholder=\"What you need help with\" />\n                  </SelectTrigger>\n                </FormControl>\n                <SelectContent>\n                  {types.map((type) => (\n                    <SelectItem key={type.value} value={type.value}>\n                      {type.label}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n        {watchType ? (\n          <FormField\n            control={form.control}\n            name=\"message\"\n            render={({ field }) => (\n              <FormItem className=\"sm:col-span-full\">\n                <FormLabel>Message</FormLabel>\n                <FormControl>\n                  <Textarea placeholder=\"Tell us about it...\" {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        ) : null}\n        {watchType === \"bug\" ? (\n          <FormField\n            control={form.control}\n            name=\"blocker\"\n            render={({ field }) => (\n              <FormItem className=\"flex flex-row items-start sm:col-span-full\">\n                <FormControl>\n                  <Checkbox\n                    checked={field.value}\n                    onCheckedChange={field.onChange}\n                  />\n                </FormControl>\n                <FormLabel className=\"font-normal leading-none\">\n                  This bug prevents me from using the product.\n                </FormLabel>\n              </FormItem>\n            )}\n          />\n        ) : null}\n        <Button\n          type=\"submit\"\n          className=\"w-full sm:col-span-full\"\n          disabled={isPending}\n        >\n          {isPending ? \"Submitting...\" : \"Submit\"}\n        </Button>\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/layout/auth-layout.tsx",
    "content": "import Image from \"next/image\";\n\nexport function AuthLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <div className=\"grid min-h-screen grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-5\">\n      <aside className=\"col-span-1 flex w-full flex-col gap-4 border border-border bg-sidebar p-4 backdrop-blur-[2px] md:p-8 xl:col-span-2\">\n        <a href=\"https://openstatus.dev\" className=\"relative h-8 w-8\">\n          <Image\n            src=\"https://openstatus.dev/icon.png\"\n            alt=\"OpenStatus\"\n            height={32}\n            width={32}\n            className=\"rounded-full border border-border\"\n          />\n        </a>\n        <div className=\"mx-auto flex w-full max-w-lg flex-1 flex-col justify-center gap-8 text-center md:text-left\">\n          <div className=\"mx-auto grid gap-3\">\n            <h1 className=\"font-cal text-3xl text-foreground\">\n              Open Source Monitoring Service\n            </h1>\n            <p className=\"text-muted-foreground text-sm\">\n              Monitor your website or API and create your own status page within\n              a couple of minutes. Want to know how it works? <br />\n              <br />\n              Check out{\" \"}\n              <a\n                href=\"https://github.com/openstatushq/openstatus\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"text-foreground underline underline-offset-4 hover:no-underline\"\n              >\n                GitHub\n              </a>{\" \"}\n              and let us know your use case!\n            </p>\n          </div>\n        </div>\n        <div className=\"md:h-8\" />\n      </aside>\n      <main className=\"container col-span-1 mx-auto flex items-center justify-center md:col-span-1 xl:col-span-3\">\n        {children}\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/metric/global-uptime/section.tsx",
    "content": "\"use client\";\n\nimport {\n  MetricCard,\n  MetricCardBadge,\n  MetricCardGroup,\n  MetricCardHeader,\n  MetricCardSkeleton,\n  MetricCardTitle,\n  MetricCardValue,\n} from \"@/components/metric/metric-card\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { mapMetrics, metricsCards } from \"@/data/metrics.client\";\nimport {\n  formatMilliseconds,\n  formatNumber,\n  formatPercentage,\n} from \"@/lib/formatter\";\nimport { formatDistanceToNow } from \"date-fns\";\n\ntype Metric = {\n  label: string;\n  value: string;\n  trend?: number | null;\n  variant: React.ComponentProps<typeof MetricCard>[\"variant\"];\n};\n\n// TODO: move the fetch to the parent component\n// TODO: missing dynamic degraded\n\nexport function GlobalUptimeSection({\n  monitorId,\n  jobType,\n  period = \"7d\",\n  regions,\n}: {\n  monitorId: string;\n  jobType: \"http\" | \"tcp\";\n  period: \"1d\" | \"7d\" | \"14d\";\n  regions: string[] | undefined;\n}) {\n  const trpc = useTRPC();\n\n  const { data: metrics, isLoading } = useQuery(\n    trpc.tinybird.metrics.queryOptions({\n      monitorId,\n      period,\n      type: jobType,\n      regions,\n    }),\n  );\n\n  // Helper to transform the data the same way it used to be in the page\n  function defineMetrics() {\n    if (!metrics) return null;\n    const _metrics = mapMetrics(metrics);\n\n    if (_metrics.length !== 2) return null;\n\n    return _metrics.reverse().reduce(\n      (acc, metric) => {\n        Object.entries(metric).forEach(([key, value]) => {\n          const k = key as keyof typeof acc;\n          const v = (() => {\n            if (k === \"lastTimestamp\") {\n              if (!value) return \"N/A\";\n              return formatDistanceToNow(new Date(value ?? 0), {\n                addSuffix: true,\n              });\n            }\n            if (k === \"uptime\") {\n              return formatPercentage(value ?? 0);\n            }\n            if (k.startsWith(\"p\")) {\n              return formatMilliseconds(value ?? 0);\n            }\n            return formatNumber(value ?? 0);\n          })();\n\n          if (k in acc) {\n            const trend = acc[k]?.raw\n              ? k === \"uptime\"\n                ? acc[k]?.raw / (value ?? 0)\n                : (value ?? 0) / acc[k]?.raw\n              : 1;\n            const hasTrend =\n              !Number.isNaN(trend) &&\n              trend !== Number.POSITIVE_INFINITY &&\n              k !== \"total\" &&\n              k !== \"lastTimestamp\";\n            acc[k] = {\n              label: metricsCards[k].label,\n              variant: metricsCards[k].variant,\n              value: v ?? \"0\",\n              trend: hasTrend ? trend : null,\n              raw: value ?? 0,\n            } as (typeof acc)[typeof k & keyof typeof acc];\n          } else {\n            acc[k] = {\n              label: metricsCards[k].label,\n              variant: metricsCards[k].variant,\n              value: v ?? \"0\",\n              trend: 1,\n              raw: value ?? 0,\n            } as (typeof acc)[typeof k & keyof typeof acc];\n          }\n        });\n        return acc;\n      },\n      {} as Record<\n        keyof ReturnType<typeof mapMetrics>[number],\n        Metric & { raw: number }\n      >,\n    );\n  }\n\n  const refinedMetrics: (Metric | null)[] = (\n    [\n      \"uptime\",\n      \"degraded\",\n      \"error\",\n      \"total\",\n      \"lastTimestamp\",\n      \"p50\",\n      \"p75\",\n      \"p90\",\n      \"p95\",\n      \"p99\",\n    ] as const\n  ).map((key) => {\n    if (!key) return null;\n    const metric =\n      defineMetrics()?.[key as keyof ReturnType<typeof mapMetrics>[number]];\n    return {\n      label: metricsCards[key].label,\n      value: metric?.value ?? \"0\",\n      trend: metric?.trend ?? null,\n      variant: metricsCards[key].variant,\n    } as Metric;\n  });\n\n  // TODO: rework, might be removed and simply add a condition on the other return\n  if (isLoading) {\n    return (\n      <MetricCardGroup>\n        {refinedMetrics.map((metric) => {\n          if (metric === null)\n            return <div key={metric} className=\"hidden lg:block\" />;\n          return (\n            <MetricCard key={metric.label} variant={metric.variant}>\n              <MetricCardHeader>\n                <MetricCardTitle className=\"truncate\">\n                  {metric.label}\n                </MetricCardTitle>\n              </MetricCardHeader>\n              <MetricCardSkeleton className=\"h-6 w-12\" />\n            </MetricCard>\n          );\n        })}\n      </MetricCardGroup>\n    );\n  }\n\n  return (\n    <MetricCardGroup>\n      {refinedMetrics.map((metric) => {\n        if (metric === null)\n          return <div key={metric} className=\"hidden lg:block\" />;\n        return (\n          <MetricCard key={metric.label} variant={metric.variant}>\n            <MetricCardHeader>\n              <MetricCardTitle className=\"truncate\">\n                {metric.label}\n              </MetricCardTitle>\n            </MetricCardHeader>\n            <div className=\"flex flex-row flex-wrap items-center gap-1.5\">\n              <MetricCardValue>{metric.value}</MetricCardValue>\n              {metric.trend ? <MetricCardBadge value={metric.trend} /> : null}\n            </div>\n          </MetricCard>\n        );\n      })}\n    </MetricCardGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/metric/metric-card.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport { cva } from \"class-variance-authority\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport type React from \"react\";\n\nconst metricCardVariants = cva(\n  \"flex flex-col gap-1 border rounded-lg px-3 py-2 text-card-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"border-input bg-card\",\n        ghost: \"border-transparent\",\n        destructive: \"border-destructive/80 bg-destructive/10\",\n        success: \"border-success/80 bg-success/10\",\n        warning: \"border-warning/80 bg-warning/10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport function MetricCard({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof metricCardVariants>) {\n  return (\n    <div\n      data-variant={variant}\n      className={cn(metricCardVariants({ variant, className }), \"group\")}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function MetricCardTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      className={cn(\n        \"font-commit-mono font-medium text-sm tracking-tight \",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </p>\n  );\n}\n\nexport function MetricCardHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"text-muted-foreground\",\n        \"group-data-[variant=destructive]:text-destructive\",\n        \"group-data-[variant=success]:text-success\",\n        \"group-data-[variant=warning]:text-warning\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function MetricCardValue({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"font-medium text-foreground\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function MetricCardGroup({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nconst badgeVariants = cva(\"px-1.5 font-mono text-[10px]\", {\n  variants: {\n    variant: {\n      default: \"border-border\",\n      increase:\n        \"border-destructive/20 bg-destructive/10 hover:bg-destructive/10 text-destructive\",\n      decrease:\n        \"border-success/20 bg-success/10 hover:bg-success/10 text-success\",\n    },\n  },\n  defaultVariants: {\n    variant: \"default\",\n  },\n});\n\nexport function MetricCardBadge({\n  value,\n  decimal = 1,\n  className,\n  ...props\n}: React.ComponentProps<typeof Badge> & {\n  value: number;\n  decimal?: number;\n}) {\n  const round = 10 ** decimal; // 10^1 = 10 (1 decimal), 10^2 = 100 (2 decimals), etc.\n  const percentage = Math.round((value - 1) * 100 * round) / round;\n\n  const variant: VariantProps<typeof badgeVariants>[\"variant\"] =\n    percentage > 0 ? \"increase\" : percentage < 0 ? \"decrease\" : \"default\";\n\n  return (\n    <Badge\n      variant=\"secondary\"\n      className={badgeVariants({ variant, className })}\n      {...props}\n    >\n      {percentage !== 0 ? (\n        <span>\n          {percentage > 0 ? <ChevronUp className=\"mr-px size-2.5\" /> : null}\n          {percentage < 0 ? <ChevronDown className=\"mr-px size-2.5\" /> : null}\n        </span>\n      ) : null}\n      {Math.abs(percentage)}%\n    </Badge>\n  );\n}\n\nconst metricCardButtonVariants = cva(\n  \"group w-full text-left transition-all rounded-md outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 cursor-pointer\",\n  // TODO: discuss if we want rings\n);\n\nexport function MetricCardButton({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"button\"> & VariantProps<typeof metricCardVariants>) {\n  return (\n    <button\n      type=\"button\"\n      data-variant={variant}\n      className={cn(\n        metricCardVariants({ variant, className }),\n        metricCardButtonVariants(),\n      )}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n}\n\nexport function MetricCardSkeleton({\n  className,\n  ...props\n}: React.ComponentProps<typeof Skeleton>) {\n  return (\n    <Skeleton\n      className={cn(\n        \"group-data-[variant=destructive]:bg-destructive/50\",\n        \"group-data-[variant=success]:bg-success/50\",\n        \"group-data-[variant=warning]:bg-warning/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/app-header.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport function AppHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"header\">) {\n  return (\n    <header\n      className={cn(\n        \"sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 border-b bg-background px-2\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </header>\n  );\n}\n\nexport function AppHeaderContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"flex flex-1 items-center gap-2 px-3\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function AppHeaderActions({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"ml-auto px-3\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/app-sidebar.tsx",
    "content": "\"use client\";\n\nimport {\n  Activity,\n  Bell,\n  Bot,\n  Cog,\n  Globe,\n  LayoutGrid,\n  PanelTop,\n  Terminal,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Kbd } from \"@/components/common/kbd\";\nimport { NavMonitors } from \"@/components/nav/nav-monitors\";\nimport { NavOverview } from \"@/components/nav/nav-overview\";\nimport { NavStatusPages } from \"@/components/nav/nav-status-pages\";\nimport { NavUser } from \"@/components/nav/nav-user\";\nimport { WorkspaceSwitcher } from \"@/components/nav/workspace-switcher\";\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarHeader,\n  SidebarRail,\n  SidebarTrigger,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { NavBanner } from \"./nav-banner\";\nimport { NavHelp } from \"./nav-help\";\n\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"[\";\n\n// This is sample data.\nconst data = {\n  user: {\n    name: \"mxkaske\",\n    email: \"max@openstatus.dev\",\n    avatar: \"/avatars/shadcn.jpg\",\n  },\n  overview: [\n    {\n      name: \"Overview\",\n      url: \"/overview\",\n      icon: LayoutGrid,\n    },\n    {\n      name: \"Monitors\",\n      url: \"/monitors\",\n      icon: Activity,\n    },\n    {\n      name: \"Status Pages\",\n      url: \"/status-pages\",\n      icon: PanelTop,\n    },\n    {\n      name: \"Notifications\",\n      url: \"/notifications\",\n      icon: Bell,\n    },\n    {\n      name: \"Settings\",\n      url: \"/settings/general\",\n      icon: Cog,\n    },\n    {\n      name: \"Private Locations\",\n      url: \"/private-locations\",\n      icon: Globe,\n    },\n    {\n      name: \"Agents\",\n      url: \"/agents\",\n      icon: Bot,\n    },\n    {\n      name: \"CLI\",\n      url: \"/cli\",\n      icon: Terminal,\n    },\n  ],\n};\n\nexport function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {\n  return (\n    <Sidebar collapsible=\"icon\" {...props}>\n      <SidebarHeader className=\"flex h-14 justify-center gap-0 border-b p-0\">\n        <WorkspaceSwitcher />\n      </SidebarHeader>\n      <SidebarContent>\n        <NavOverview items={data.overview} />\n        <NavStatusPages />\n        <NavMonitors />\n        <div className=\"mt-auto px-2\">\n          <NavBanner />\n        </div>\n        <NavHelp />\n      </SidebarContent>\n      <SidebarFooter className=\"flex h-14 flex-col justify-center gap-0 border-t p-0\">\n        <NavUser />\n      </SidebarFooter>\n      <SidebarRail />\n    </Sidebar>\n  );\n}\n\nexport function AppSidebarTrigger() {\n  const { toggleSidebar } = useSidebar();\n\n  // Adds a keyboard shortcut to toggle the sidebar.\n  React.useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (\n        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n        (event.metaKey || event.ctrlKey)\n      ) {\n        event.preventDefault();\n        toggleSidebar();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [toggleSidebar]);\n\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <SidebarTrigger />\n        </TooltipTrigger>\n        <TooltipContent side=\"right\">\n          <p className=\"mr-px inline-flex items-center\">\n            Toggle Sidebar{\" \"}\n            <Kbd className=\"border-muted-foreground bg-primary font-mono text-background\">\n              ⌘\n            </Kbd>\n            <Kbd className=\"border-muted-foreground bg-primary font-mono text-background\">\n              {SIDEBAR_KEYBOARD_SHORTCUT}\n            </Kbd>\n          </p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-banner-checklist.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  SidebarGroup,\n  SidebarGroupLabel,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuItem,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { CircleCheck, CircleDashed, X } from \"lucide-react\";\n\nexport function NavBannerChecklist({\n  handleClose,\n}: {\n  handleClose: () => void;\n}) {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n\n  if (!workspace) return null;\n\n  const hasMonitors = (workspace.usage?.monitors ?? 0) > 0;\n  const hasStatusPages = (workspace.usage?.pages ?? 0) > 0;\n  const hastNotifications = (workspace.usage?.notifications ?? 0) > 0;\n\n  if (hasMonitors && hasStatusPages && hastNotifications) return null;\n\n  const items = [\n    {\n      title: \"Create Monitor\",\n      checked: hasMonitors,\n      href: \"/monitors/create\",\n    },\n    {\n      title: \"Create Status Page\",\n      checked: hasStatusPages,\n      href: \"/status-pages/create\",\n    },\n    {\n      title: \"Create Notification\",\n      checked: hastNotifications,\n      href: \"/notifications\",\n    },\n  ];\n\n  return (\n    <SidebarGroup className=\"rounded-lg border bg-background group-data-[collapsible=icon]:hidden\">\n      <SidebarGroupLabel className=\"flex items-center justify-between pr-1\">\n        <span>Getting Started</span>\n        <SidebarMenuAction\n          className=\"relative top-0 right-0\"\n          onClick={handleClose}\n        >\n          <X className=\"text-muted-foreground\" size={16} />\n        </SidebarMenuAction>\n      </SidebarGroupLabel>\n      <SidebarMenu>\n        {items.map((item) => (\n          <SidebarMenuItem\n            key={item.title}\n            className=\"flex items-center gap-2 text-sm\"\n          >\n            {item.checked ? (\n              <>\n                <CircleCheck className=\"shrink-0 text-success\" size={12} />\n                <span>{item.title}</span>\n              </>\n            ) : (\n              <>\n                <CircleDashed\n                  className=\"shrink-0 text-muted-foreground/50\"\n                  size={12}\n                />\n                <Link href={item.href}>{item.title}</Link>\n              </>\n            )}\n          </SidebarMenuItem>\n        ))}\n      </SidebarMenu>\n    </SidebarGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-banner-upgrade.tsx",
    "content": "\"use client\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  SidebarGroup,\n  SidebarGroupLabel,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuButton,\n  SidebarMenuItem,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Rocket, X } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { UpgradeDialog } from \"../dialogs/upgrade\";\n\nexport function NavBannerUpgrade({ handleClose }: { handleClose: () => void }) {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const [open, setOpen] = useState(false);\n\n  if (!workspace) return null;\n\n  return (\n    <SidebarGroup className=\"rounded-lg border bg-background group-data-[collapsible=icon]:hidden\">\n      <SidebarGroupLabel className=\"flex items-center justify-between pr-1\">\n        <span>OpenStatus Pro</span>\n        <SidebarMenuAction\n          onClick={handleClose}\n          className=\"relative top-0 right-0\"\n        >\n          <X className=\"text-muted-foreground\" size={16} />\n        </SidebarMenuAction>\n      </SidebarGroupLabel>\n      <SidebarMenu>\n        <SidebarMenuItem className=\"flex items-center gap-2 text-sm\">\n          <Rocket className=\"shrink-0 text-info\" size={12} />\n          <span>\n            Unlock custom domains, teams, 1 min. checks, subscriptions and more.\n          </span>\n        </SidebarMenuItem>\n        <SidebarMenuItem>\n          <SidebarMenuButton\n            className=\"justify-center border\"\n            data-active=\"true\"\n            onClick={() => setOpen(true)}\n          >\n            Upgrade\n          </SidebarMenuButton>\n        </SidebarMenuItem>\n      </SidebarMenu>\n      <UpgradeDialog open={open} onOpenChange={setOpen} />\n    </SidebarGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-banner.tsx",
    "content": "\"use client\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useCookieState } from \"@openstatus/ui/hooks/use-cookie-state\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { NavBannerChecklist } from \"./nav-banner-checklist\";\nimport { NavBannerUpgrade } from \"./nav-banner-upgrade\";\n\nconst EXPIRES_IN = 7 * 24 * 60 * 60 * 1000; // in 7 days\n\nexport function NavBanner() {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const [openChecklist, setOpenChecklist] = useCookieState<\"true\" | \"false\">(\n    \"sidebar_banner_checklist\",\n    \"true\",\n    { expires: EXPIRES_IN },\n  );\n  const [openUpgrade, setOpenUpgrade] = useCookieState<\"true\" | \"false\">(\n    \"sidebar_banner_upgrade\",\n    \"true\",\n    { expires: EXPIRES_IN },\n  );\n\n  if (!workspace) return null;\n\n  if (openChecklist === \"true\") {\n    return <NavBannerChecklist handleClose={() => setOpenChecklist(\"false\")} />;\n  }\n\n  if (openUpgrade === \"true\" && workspace.plan === \"free\") {\n    return <NavBannerUpgrade handleClose={() => setOpenUpgrade(\"false\")} />;\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-breadcrumb.tsx",
    "content": "\"use client\";\n\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@openstatus/ui/components/ui/breadcrumb\";\nimport type { LucideIcon } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { Fragment } from \"react\";\n\ninterface NavBreadcrumbProps {\n  items: (\n    | {\n        type: \"link\";\n        label: string;\n        href: string;\n        icon?: LucideIcon;\n      }\n    | {\n        type: \"page\";\n        label: string;\n        icon?: LucideIcon;\n      }\n  )[];\n}\n\nexport function NavBreadcrumb({ items }: NavBreadcrumbProps) {\n  return (\n    <Breadcrumb>\n      <BreadcrumbList>\n        <BreadcrumbSeparator className=\"hidden md:block\" />\n        {items.map((item, i) => (\n          <Fragment key={item.type === \"link\" ? item.href : item.label}>\n            <BreadcrumbItem>\n              {item.type === \"link\" ? (\n                <BreadcrumbLink\n                  className=\"hidden flex-nowrap items-center gap-1.5 md:flex\"\n                  asChild\n                >\n                  <Link\n                    href={item.href}\n                    className=\"font-commit-mono tracking-tight\"\n                  >\n                    {item.icon && (\n                      <item.icon\n                        size={16}\n                        aria-hidden=\"true\"\n                        className=\"shrink-0\"\n                      />\n                    )}\n                    {item.label}\n                  </Link>\n                </BreadcrumbLink>\n              ) : null}\n              {item.type === \"page\" ? (\n                <BreadcrumbPage className=\" hidden max-w-[120px] truncate font-commit-mono tracking-tight md:block lg:max-w-[200px] \">\n                  <span className=\"flex items-center gap-1.5\">\n                    {item.icon && (\n                      <item.icon\n                        size={16}\n                        aria-hidden=\"true\"\n                        className=\"shrink-0\"\n                      />\n                    )}\n\n                    {item.label}\n                  </span>\n                </BreadcrumbPage>\n              ) : null}\n            </BreadcrumbItem>\n            {i < items.length - 1 && (\n              <BreadcrumbSeparator className=\"hidden md:block\" />\n            )}\n          </Fragment>\n        ))}\n      </BreadcrumbList>\n    </Breadcrumb>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-feedback.tsx",
    "content": "\"use client\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Kbd } from \"@openstatus/ui/components/ui/kbd\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { AudioLines, Inbox, LoaderCircle, Mic } from \"lucide-react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  message: z.string().min(1),\n});\n\nexport function NavFeedback() {\n  const [open, setOpen] = useState(false);\n  const isMobile = useIsMobile();\n  const form = useForm<z.infer<typeof schema>>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      message: \"\",\n    },\n  });\n  const trpc = useTRPC();\n  const feedbackMutation = useMutation(trpc.feedback.submit.mutationOptions());\n  const [isListening, setIsListening] = useState(false);\n  const recognitionRef = useRef<SpeechRecognition | null>(null);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return;\n\n    if (\"SpeechRecognition\" in window || \"webkitSpeechRecognition\" in window) {\n      console.log(\"speech recognition API supported\");\n    } else {\n      console.log(\"speech recognition API not supported\");\n    }\n\n    const SpeechRecognitionCtor =\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      (window as any).webkitSpeechRecognition ||\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      (window as any).SpeechRecognition;\n\n    // Browser not supported\n    if (!SpeechRecognitionCtor) return;\n\n    const recognition: SpeechRecognition = new SpeechRecognitionCtor();\n    recognition.lang = \"en-US\";\n    recognition.continuous = false;\n    recognition.interimResults = false;\n\n    recognition.onresult = (event: SpeechRecognitionEvent) => {\n      const transcript = Array.from(event.results)\n        .map((r) => r[0].transcript)\n        .join(\" \");\n      form.setValue(\n        \"message\",\n        `${form.getValues(\"message\") ?? \"\"}${transcript} `,\n      );\n    };\n\n    recognition.onend = () => {\n      setIsListening(false);\n    };\n\n    recognitionRef.current = recognition;\n  }, [form]);\n\n  const toggleListening = () => {\n    const recognition = recognitionRef.current;\n    if (!recognition) return;\n    if (isListening) {\n      recognition.stop();\n    } else {\n      try {\n        recognition.start();\n        setIsListening(true);\n      } catch {\n        // recognition already started, ignore\n      }\n    }\n  };\n\n  const onSubmit = useCallback(\n    async (values: z.infer<typeof schema>) => {\n      const promise = feedbackMutation.mutateAsync({\n        ...values,\n        path: window.location.pathname,\n        isMobile,\n      });\n      toast.promise(promise, {\n        loading: \"Sending feedback...\",\n        success: \"Feedback sent\",\n        error: \"Failed to send feedback\",\n      });\n      await promise;\n    },\n    [feedbackMutation, isMobile],\n  );\n\n  useEffect(() => {\n    if (!open && feedbackMutation.isSuccess) {\n      // NOTE: the popover takes 300ms to close, so we need to wait for that\n      setTimeout(() => feedbackMutation.reset(), 300);\n    }\n  }, [open, feedbackMutation]);\n\n  useEffect(() => {\n    const down = async (e: KeyboardEvent) => {\n      if (open && (e.metaKey || e.ctrlKey) && e.key === \"Enter\") {\n        await form.handleSubmit(onSubmit)();\n      }\n\n      const target = e.target as HTMLElement;\n      const isTyping =\n        target.tagName === \"INPUT\" ||\n        target.tagName === \"TEXTAREA\" ||\n        target.isContentEditable;\n\n      if (isTyping) return;\n\n      if (!open) {\n        if (e.key === \"f\") {\n          e.preventDefault();\n          setOpen(true);\n        }\n        return;\n      }\n    };\n    document.addEventListener(\"keydown\", down);\n    return () => document.removeEventListener(\"keydown\", down);\n  }, [open, form, onSubmit]);\n\n  useEffect(() => {\n    if (!open) {\n      form.reset();\n    }\n  }, [open, form]);\n\n  if (isMobile) {\n    return null;\n  }\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          className=\"group gap-0 px-2 text-muted-foreground text-sm hover:bg-transparent hover:text-foreground data-[state=open]:text-foreground\"\n        >\n          Feedback{\" \"}\n          <Kbd className=\"ml-1 font-mono group-hover:text-foreground group-data-[state=open]:text-foreground\">\n            F\n          </Kbd>\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align=\"end\" className=\"relative border-none p-0\">\n        {feedbackMutation.isSuccess ? (\n          <div className=\"flex h-[110px] flex-col items-center justify-center gap-1 rounded-md border border-input p-3 text-base shadow-xs\">\n            <Inbox className=\"size-4 shrink-0\" />\n            <p className=\"text-center font-medium\">Thanks for sharing!</p>\n            <p className=\"text-center text-muted-foreground text-sm\">\n              We&apos;ll get in touch if there&apos;s a follow-up.\n            </p>\n          </div>\n        ) : (\n          <Form {...form}>\n            <form onSubmit={form.handleSubmit(onSubmit)}>\n              <FormField\n                control={form.control}\n                name=\"message\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel className=\"sr-only\">Feedback</FormLabel>\n                    <FormControl>\n                      <Textarea\n                        placeholder=\"Ideas, bugs, or anything else...\"\n                        className=\"field-sizing-fixed h-[110px] resize-none p-3\"\n                        rows={4}\n                        {...field}\n                      />\n                    </FormControl>\n                  </FormItem>\n                )}\n              />\n              {recognitionRef.current && (\n                <Button\n                  type=\"button\"\n                  size=\"sm\"\n                  variant=\"ghost\"\n                  className=\"group absolute bottom-1.5 left-1.5 gap-0\"\n                  onClick={toggleListening}\n                >\n                  {isListening ? (\n                    <AudioLines className=\"size-4 animate-pulse\" />\n                  ) : (\n                    <Mic className=\"size-4\" />\n                  )}\n                </Button>\n              )}\n              <Button\n                size=\"sm\"\n                variant=\"ghost\"\n                className=\"group absolute right-1.5 bottom-1.5 gap-0\"\n                type=\"submit\"\n                disabled={feedbackMutation.isPending}\n              >\n                {feedbackMutation.isPending ? (\n                  <LoaderCircle className=\"size-4 animate-spin\" />\n                ) : (\n                  <>\n                    Send\n                    <Kbd className=\"ml-1 font-mono group-hover:text-foreground\">\n                      ⌘\n                    </Kbd>\n                    <Kbd className=\"ml-1 font-mono group-hover:text-foreground\">\n                      ↵\n                    </Kbd>\n                  </>\n                )}\n              </Button>\n            </form>\n          </Form>\n        )}\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-help.tsx",
    "content": "\"use client\";\n\nimport { FormDialogSupportContact } from \"@/components/forms/support-contact/dialog\";\nimport { DiscordIcon } from \"@openstatus/icons\";\nimport { GitHubIcon } from \"@openstatus/icons\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport {\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport {\n  Book,\n  Braces,\n  CalendarClock,\n  HelpCircle,\n  LifeBuoy,\n} from \"lucide-react\";\nimport Link from \"next/link\";\n\nexport function NavHelp() {\n  const { isMobile } = useSidebar();\n  return (\n    <SidebarGroup>\n      <SidebarGroupContent>\n        <SidebarMenu>\n          <SidebarMenuItem>\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <SidebarMenuButton\n                  className=\"font-commit-mono tracking-tight\"\n                  tooltip=\"Get Help\"\n                >\n                  <HelpCircle />\n                  <span>Get Help</span>\n                </SidebarMenuButton>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent\n                className=\"w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg\"\n                side={isMobile ? \"bottom\" : \"right\"}\n                align=\"end\"\n                sideOffset={4}\n              >\n                <DropdownMenuLabel className=\"text-muted-foreground text-xs\">\n                  Get Help\n                </DropdownMenuLabel>\n                <FormDialogSupportContact>\n                  <DropdownMenuItem onSelect={(e) => e.preventDefault()}>\n                    <LifeBuoy />\n                    Support\n                  </DropdownMenuItem>\n                </FormDialogSupportContact>\n                <DropdownMenuItem asChild>\n                  <Link\n                    href=\"https://docs.openstatus.dev\"\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    <Book /> Docs\n                  </Link>\n                </DropdownMenuItem>\n                <DropdownMenuItem asChild>\n                  <Link\n                    href=\"https://api.openstatus.dev/openapi\"\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    <Braces /> API Reference\n                  </Link>\n                </DropdownMenuItem>\n                <DropdownMenuItem asChild>\n                  <Link\n                    href=\"https://openstatus.dev/cal\"\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    <CalendarClock /> Book a Call\n                  </Link>\n                </DropdownMenuItem>\n                <DropdownMenuItem asChild>\n                  <Link\n                    href=\"https://openstatus.dev/discord\"\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    <DiscordIcon />\n                    Community\n                  </Link>\n                </DropdownMenuItem>\n                <DropdownMenuItem asChild>\n                  <Link\n                    href=\"https://openstatus.dev/github\"\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    <GitHubIcon />\n                    GitHub\n                  </Link>\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </SidebarMenuItem>\n        </SidebarMenu>\n      </SidebarGroupContent>\n    </SidebarGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-main.tsx",
    "content": "\"use client\";\n\nimport { ChevronRight, type LucideIcon } from \"lucide-react\";\n\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@openstatus/ui/components/ui/collapsible\";\nimport {\n  SidebarGroup,\n  SidebarGroupLabel,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarMenuSub,\n  SidebarMenuSubButton,\n  SidebarMenuSubItem,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport Link from \"next/link\";\nexport function NavMain({\n  items,\n}: {\n  items: {\n    title: string;\n    url: string;\n    icon?: LucideIcon;\n    isActive?: boolean;\n    items?: {\n      title: string;\n      url: string;\n    }[];\n  }[];\n}) {\n  const { setOpenMobile } = useSidebar();\n  return (\n    <SidebarGroup>\n      <SidebarGroupLabel>Workspace Data</SidebarGroupLabel>\n      <SidebarMenu>\n        {items.map((item) => (\n          <Collapsible\n            key={item.title}\n            asChild\n            defaultOpen={item.isActive}\n            className=\"group/collapsible\"\n          >\n            <SidebarMenuItem>\n              <CollapsibleTrigger asChild>\n                <SidebarMenuButton tooltip={item.title}>\n                  {item.icon && <item.icon />}\n                  <span>{item.title}</span>\n                  <ChevronRight className=\"ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90\" />\n                </SidebarMenuButton>\n              </CollapsibleTrigger>\n              <CollapsibleContent>\n                <SidebarMenuSub>\n                  {item.items?.map((subItem) => (\n                    <SidebarMenuSubItem key={subItem.title}>\n                      <SidebarMenuSubButton asChild>\n                        <Link\n                          href={subItem.url}\n                          onClick={() => setOpenMobile(false)}\n                        >\n                          <span>{subItem.title}</span>\n                        </Link>\n                      </SidebarMenuSubButton>\n                    </SidebarMenuSubItem>\n                  ))}\n                </SidebarMenuSub>\n              </CollapsibleContent>\n            </SidebarMenuItem>\n          </Collapsible>\n        ))}\n      </SidebarMenu>\n    </SidebarGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-monitors.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\n\nimport { MoreHorizontal, Plus } from \"lucide-react\";\n\nimport { ExportCodeDialog } from \"@/components/dialogs/export-code\";\nimport { UpgradeDialog } from \"@/components/dialogs/upgrade\";\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { getActions } from \"@/data/monitors.client\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  SidebarGroup,\n  SidebarGroupLabel,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarMenuSkeleton,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { toast } from \"sonner\";\n\nexport const STATUS = {\n  degraded: \"bg-warning border border-warning\",\n  error: \"bg-destructive border border-destructive\",\n  inactive: \"bg-muted-foreground/70 border border-muted-foreground/70\",\n  active: \"bg-success border border-success\",\n};\n\nexport function NavMonitors() {\n  const [openDialog, setOpenDialog] = useState(false);\n  const [openUpgradeDialog, setOpenUpgradeDialog] = useState(false);\n  const { isMobile, setOpenMobile } = useSidebar();\n  const trpc = useTRPC();\n  const router = useRouter();\n  const pathname = usePathname();\n  const {\n    data: monitors,\n    isLoading,\n    refetch,\n  } = useQuery(trpc.monitor.list.queryOptions());\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const queryClient = useQueryClient();\n  const deleteMonitorMutation = useMutation(\n    trpc.monitor.delete.mutationOptions({\n      onSuccess: () => {\n        refetch();\n        queryClient.invalidateQueries({\n          queryKey: trpc.workspace.get.queryKey(),\n        });\n      },\n    }),\n  );\n  const cloneMonitorMutation = useMutation(\n    trpc.monitor.clone.mutationOptions({\n      onSuccess: (newMonitor) => {\n        refetch();\n        queryClient.invalidateQueries({\n          queryKey: trpc.workspace.get.queryKey(),\n        });\n        router.push(`/monitors/${newMonitor.id}`);\n      },\n    }),\n  );\n\n  if (!workspace || !monitors) return null;\n\n  const limitReached = monitors.length >= workspace.limits.monitors;\n\n  return (\n    <SidebarGroup className=\"group-data-[collapsible=icon]:hidden\">\n      <SidebarGroupLabel\n        className=\"flex items-center justify-between pr-1\"\n        style={{ paddingRight: 4 }}\n      >\n        <div className=\"flex items-center gap-1\">\n          <span>Monitors</span>\n          {isLoading ? (\n            <Skeleton className=\"h-4 w-5 shrink-0\" />\n          ) : (\n            <code className=\"text-muted-foreground\">({monitors.length})</code>\n          )}\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <TooltipProvider>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <SidebarMenuAction\n                  data-limited={limitReached}\n                  className=\"relative top-0 right-0 border data-[limited=true]:opacity-80\"\n                  onClick={() => {\n                    if (limitReached) {\n                      setOpenUpgradeDialog(true);\n                      return;\n                    }\n                    router.push(\"/monitors/create\");\n                    setOpenMobile(false);\n                  }}\n                >\n                  <Plus className=\"text-muted-foreground\" />\n                  <span className=\"sr-only\">Create Monitor</span>\n                </SidebarMenuAction>\n              </TooltipTrigger>\n              <TooltipContent side=\"right\" align=\"center\">\n                {limitReached ? \"Upgrade\" : \"Create Monitor\"}\n              </TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n        </div>\n      </SidebarGroupLabel>\n      <SidebarMenu>\n        {isLoading ? (\n          <SidebarMenuItem>\n            <SidebarMenuSkeleton />\n          </SidebarMenuItem>\n        ) : monitors && monitors.length > 0 ? (\n          monitors.map((item) => {\n            const isActive = pathname.startsWith(`/monitors/${item.id}/`);\n            const actions = getActions({\n              edit: () => router.push(`/monitors/${item.id}/edit`),\n              \"copy-id\": () => {\n                navigator.clipboard.writeText(item.id.toString());\n                toast.success(\"Monitor ID copied to clipboard\");\n              },\n              clone: () => {\n                const promise = cloneMonitorMutation.mutateAsync({\n                  id: item.id,\n                });\n                toast.promise(promise, {\n                  loading: \"Cloning monitor...\",\n                  success: \"Monitor cloned\",\n                  error: (error) => {\n                    if (isTRPCClientError(error)) {\n                      return error.message;\n                    }\n                    return \"Failed to clone monitor\";\n                  },\n                });\n              },\n              // export: () => setOpenDialog(true),\n            });\n            return (\n              <SidebarMenuItem key={item.id}>\n                <SidebarMenuButton\n                  className=\"group-has-data-[sidebar=menu-dot]/menu-item:pr-11\"\n                  isActive={isActive}\n                  asChild\n                >\n                  <Link\n                    href={`/monitors/${item.id}/overview`}\n                    onClick={() => setOpenMobile(false)}\n                    className=\"font-commit-mono tracking-tight\"\n                  >\n                    <span>{item.name}</span>\n                  </Link>\n                </SidebarMenuButton>\n                <div\n                  data-sidebar=\"menu-dot\"\n                  className={cn(\n                    \"absolute top-1.5 right-1 flex h-2.5 items-center justify-center p-2.5 transition-all duration-200 group-focus-within/menu-item:right-6 group-hover/menu-action:right-6 group-hover/menu-item:right-6 group-data-[state=open]/menu-action:right-6 [&:has(+[data-sidebar=menu-action][data-state=open])]:right-6\",\n                    isMobile && \"right-6\",\n                  )}\n                >\n                  <div className=\"relative flex items-center justify-center\">\n                    <div\n                      className={cn(\n                        \"-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 h-2 w-2 rounded-full\",\n                        STATUS[item.active ? item.status : \"inactive\"],\n                      )}\n                    >\n                      <span className=\"sr-only\">{item.status}</span>\n                    </div>\n                  </div>\n                </div>\n                <QuickActions\n                  actions={actions}\n                  deleteAction={{\n                    confirmationValue: item.name ?? \"monitor\",\n                    submitAction: async () => {\n                      await deleteMonitorMutation.mutateAsync({\n                        id: item.id,\n                      });\n                      if (pathname.startsWith(`/monitors/${item.id}`)) {\n                        router.push(\"/monitors\");\n                      }\n                    },\n                  }}\n                  side={isMobile ? \"bottom\" : \"right\"}\n                  align={isMobile ? \"end\" : \"start\"}\n                >\n                  <SidebarMenuAction showOnHover>\n                    <MoreHorizontal />\n                    <span className=\"sr-only\">More</span>\n                  </SidebarMenuAction>\n                </QuickActions>\n              </SidebarMenuItem>\n            );\n          })\n        ) : (\n          <SidebarMenuItem>\n            <SidebarMenuButton disabled>\n              <span>No monitors found</span>\n            </SidebarMenuButton>\n          </SidebarMenuItem>\n        )}\n      </SidebarMenu>\n      <ExportCodeDialog open={openDialog} onOpenChange={setOpenDialog} />\n      <UpgradeDialog\n        open={openUpgradeDialog}\n        onOpenChange={setOpenUpgradeDialog}\n      />\n    </SidebarGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-overview.tsx",
    "content": "\"use client\";\n\nimport type { LucideIcon } from \"lucide-react\";\n\nimport {\n  SidebarGroup,\n  SidebarGroupLabel,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\n\nexport function NavOverview({\n  items,\n}: {\n  items: {\n    name: string;\n    url: string;\n    icon: LucideIcon;\n  }[];\n}) {\n  const pathname = usePathname();\n  const { setOpenMobile } = useSidebar();\n  return (\n    <SidebarGroup>\n      <SidebarGroupLabel>Workspace</SidebarGroupLabel>\n      <SidebarMenu>\n        {items.map((item) => (\n          <SidebarMenuItem key={item.name}>\n            <SidebarMenuButton\n              // FIXME: check with settings as exception (as it includes subpages)\n              isActive={pathname === item.url}\n              asChild\n              tooltip={item.name}\n            >\n              <Link\n                href={item.url}\n                onClick={() => setOpenMobile(false)}\n                className=\"font-commit-mono tracking-tight\"\n              >\n                <item.icon />\n                <span>{item.name}</span>\n              </Link>\n            </SidebarMenuButton>\n          </SidebarMenuItem>\n        ))}\n      </SidebarMenu>\n    </SidebarGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-status-pages.tsx",
    "content": "\"use client\";\n\nimport { MoreHorizontal, Plus } from \"lucide-react\";\n\nimport { QuickActions } from \"@/components/dropdowns/quick-actions\";\nimport { getActions } from \"@/data/status-pages.client\";\nimport {\n  SidebarGroup,\n  SidebarGroupLabel,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarMenuSkeleton,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { toast } from \"sonner\";\n\nimport { UpgradeDialog } from \"@/components/dialogs/upgrade\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\n\nconst STATUS = {\n  operational: \"bg-success border border-success\",\n  degraded: \"bg-warning border border-warning\",\n  outage: \"bg-destructive border border-destructive\",\n};\n\nexport function NavStatusPages() {\n  const { isMobile, setOpenMobile } = useSidebar();\n  const [openUpgradeDialog, setOpenUpgradeDialog] = useState(false);\n  const pathname = usePathname();\n  const trpc = useTRPC();\n  const router = useRouter();\n  const {\n    data: statusPages,\n    refetch,\n    isLoading,\n  } = useQuery(trpc.page.list.queryOptions());\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const queryClient = useQueryClient();\n  const deleteStatusPage = useMutation(\n    trpc.page.delete.mutationOptions({\n      onSuccess: () => {\n        refetch();\n        queryClient.invalidateQueries({\n          queryKey: trpc.workspace.get.queryKey(),\n        });\n      },\n    }),\n  );\n\n  if (!workspace || !statusPages) return null;\n\n  const limitReached = statusPages.length >= workspace.limits[\"status-pages\"];\n\n  return (\n    <SidebarGroup className=\"group-data-[collapsible=icon]:hidden\">\n      <SidebarGroupLabel className=\"flex items-center justify-between pr-1\">\n        <div className=\"flex items-center gap-1\">\n          <span>Status Pages</span>\n          {isLoading ? (\n            <Skeleton className=\"h-4 w-5 shrink-0\" />\n          ) : (\n            <code className=\"text-muted-foreground\">\n              ({statusPages?.length})\n            </code>\n          )}\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <TooltipProvider>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <SidebarMenuAction\n                  data-limited={limitReached}\n                  className=\"relative top-0 right-0 border data-[limited=true]:opacity-80\"\n                  onClick={() => {\n                    if (limitReached) {\n                      setOpenUpgradeDialog(true);\n                      return;\n                    }\n                    router.push(\"/status-pages/create\");\n                    setOpenMobile(false);\n                  }}\n                >\n                  <Plus className=\"text-muted-foreground\" />\n                  <span className=\"sr-only\">Create Status Page</span>\n                </SidebarMenuAction>\n              </TooltipTrigger>\n              <TooltipContent side=\"right\" align=\"center\">\n                {limitReached ? \"Upgrade\" : \"Create Status Page\"}\n              </TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n        </div>\n      </SidebarGroupLabel>\n      <SidebarMenu>\n        {isLoading ? (\n          <SidebarMenuItem>\n            <SidebarMenuSkeleton />\n          </SidebarMenuItem>\n        ) : statusPages && statusPages.length > 0 ? (\n          statusPages.map((item) => {\n            const isActive = pathname.startsWith(`/status-pages/${item.id}/`);\n            const actions = getActions({\n              edit: () => router.push(`/status-pages/${item.id}/edit`),\n              \"copy-id\": async () => {\n                await navigator.clipboard.writeText(item.id.toString());\n                toast.success(\"Status Page ID copied to clipboard\");\n              },\n            });\n            const hasActiveStatusReport = item.statusReports.some(\n              (report) => report.status !== \"resolved\",\n            );\n\n            return (\n              <SidebarMenuItem key={item.id}>\n                <SidebarMenuButton\n                  className=\"group-has-data-[sidebar=menu-dot]/menu-item:pr-11\"\n                  isActive={isActive}\n                  asChild\n                >\n                  <Link\n                    href={`/status-pages/${item.id}/status-reports`}\n                    onClick={() => setOpenMobile(false)}\n                    className=\"font-commit-mono tracking-tight\"\n                  >\n                    <span>{item.title}</span>\n                  </Link>\n                </SidebarMenuButton>\n                <div\n                  data-sidebar=\"menu-dot\"\n                  className={cn(\n                    \"absolute top-1.5 right-1 flex h-2.5 items-center justify-center p-2.5 transition-all duration-200 group-focus-within/menu-item:right-6 group-hover/menu-action:right-6 group-hover/menu-item:right-6 group-data-[state=open]/menu-action:right-6 [&:has(+[data-sidebar=menu-action][data-state=open])]:right-6\",\n                    isMobile && \"right-6\",\n                  )}\n                >\n                  <div className=\"relative flex items-center justify-center\">\n                    <div\n                      className={cn(\n                        \"-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 h-2 w-2 rounded-full\",\n                        STATUS[\n                          hasActiveStatusReport ? \"degraded\" : \"operational\"\n                        ],\n                      )}\n                    />\n                  </div>\n                </div>\n                <QuickActions\n                  actions={actions}\n                  deleteAction={{\n                    confirmationValue: item.title ?? \"status page\",\n                    submitAction: async () => {\n                      await deleteStatusPage.mutateAsync({ id: item.id });\n                      if (pathname.includes(`/status-pages/${item.id}`)) {\n                        router.push(\"/status-pages\");\n                      }\n                    },\n                  }}\n                  side={isMobile ? \"bottom\" : \"right\"}\n                  align={isMobile ? \"end\" : \"start\"}\n                >\n                  <SidebarMenuAction showOnHover>\n                    <MoreHorizontal />\n                    <span className=\"sr-only\">More</span>\n                  </SidebarMenuAction>\n                </QuickActions>\n              </SidebarMenuItem>\n            );\n          })\n        ) : (\n          <SidebarMenuItem>\n            <SidebarMenuButton disabled>\n              <span>No status pages found</span>\n            </SidebarMenuButton>\n          </SidebarMenuItem>\n        )}\n      </SidebarMenu>\n      <UpgradeDialog\n        open={openUpgradeDialog}\n        onOpenChange={setOpenUpgradeDialog}\n        limit=\"status-pages\"\n      />\n    </SidebarGroup>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-tabs.tsx",
    "content": "\"use client\";\n\nimport type { LucideIcon } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface NavTabsProps {\n  items: {\n    value: string;\n    label: string;\n    icon: LucideIcon;\n    href: string;\n  }[];\n}\n\nexport function NavTabs({ items }: NavTabsProps) {\n  const pathname = usePathname();\n  const normalizedPath = pathname.replace(/\\/+$/, \"\") || \"/\";\n\n  return (\n    <nav className=\"sticky top-14 z-10 h-[41px] w-full overflow-x-auto border-b bg-background px-2\">\n      <ul className=\"inline-flex h-full items-center gap-1 px-3 text-sm\">\n        {items.map((item) => {\n          const normalizedHref = item.href.replace(/\\/+$/, \"\") || \"/\";\n          const isActive =\n            normalizedPath === normalizedHref ||\n            normalizedPath.startsWith(`${normalizedHref}/`);\n          return (\n            <li\n              key={item.value}\n              className={cn(\n                \"relative flex h-full items-center\",\n                isActive &&\n                  \"after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-foreground\",\n              )}\n            >\n              <Link\n                href={item.href}\n                aria-current={isActive ? \"page\" : undefined}\n                className={cn(\n                  \"relative inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-md px-2 py-1 font-commit-mono tracking-tight focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                  isActive\n                    ? \"text-foreground\"\n                    : \"text-muted-foreground hover:text-foreground\",\n                )}\n              >\n                <item.icon size={16} aria-hidden=\"true\" className=\"shrink-0\" />\n                {item.label}\n              </Link>\n            </li>\n          );\n        })}\n      </ul>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/nav-user.tsx",
    "content": "\"use client\";\n\nimport {\n  ChevronsUpDown,\n  CreditCard,\n  Laptop,\n  LogOut,\n  Moon,\n  Sparkles,\n  Sun,\n  User,\n} from \"lucide-react\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@openstatus/ui/components/ui/avatar\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuPortal,\n  DropdownMenuSeparator,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport {\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { signOut } from \"next-auth/react\";\nimport { useTheme } from \"next-themes\";\nimport Link from \"next/link\";\n\nexport function NavUser() {\n  const { isMobile, setOpenMobile } = useSidebar();\n  const { theme, setTheme } = useTheme();\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const { data: user } = useQuery(trpc.user.get.queryOptions());\n\n  if (!user || !workspace) return null;\n\n  const userName = user?.name ?? `${user?.firstName} ${user?.lastName}`.trim();\n\n  return (\n    <SidebarMenu>\n      <SidebarMenuItem>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <SidebarMenuButton\n              size=\"lg\"\n              className=\"h-14 rounded-none px-4 ring-inset data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:mx-2! group-data-[collapsible=icon]:rounded-lg! group-data-[collapsible=icon]:px-0!\"\n            >\n              <Avatar className=\"h-8 w-8 rounded-lg\">\n                <AvatarImage src={user?.photoUrl ?? undefined} alt={userName} />\n                <AvatarFallback className=\"rounded-lg uppercase\">\n                  {userName.slice(0, 2)}\n                </AvatarFallback>\n                {/*                   <img\n                    src={`https://api.dicebear.com/9.x/glass/svg?seed=${workspace.slug}`}\n                    alt=\"avatar\"\n                  />\n                   */}\n              </Avatar>\n              <div className=\"grid flex-1 text-left text-sm leading-tight\">\n                <span className=\"truncate font-medium\">{userName}</span>\n                <span className=\"truncate font-commit-mono text-xs tracking-tight\">\n                  {user?.email}\n                </span>\n              </div>\n              <ChevronsUpDown className=\"ml-auto size-4\" />\n            </SidebarMenuButton>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent\n            className=\"w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg\"\n            side={isMobile ? \"bottom\" : \"right\"}\n            align=\"end\"\n            sideOffset={4}\n          >\n            <DropdownMenuLabel className=\"p-0 font-normal\">\n              <div className=\"flex items-center gap-2 px-1 py-1.5 text-left text-sm\">\n                <Avatar className=\"h-8 w-8 rounded-lg\">\n                  <AvatarImage\n                    src={user?.photoUrl ?? undefined}\n                    alt={userName}\n                  />\n                  <AvatarFallback className=\"rounded-lg\">\n                    {userName.slice(0, 2)}\n                  </AvatarFallback>\n                </Avatar>\n                <div className=\"grid flex-1 text-left text-sm leading-tight\">\n                  <span className=\"truncate font-medium\">{userName}</span>\n                  <span className=\"truncate font-commit-mono text-xs tracking-tight\">\n                    {user?.email}\n                  </span>\n                </div>\n              </div>\n            </DropdownMenuLabel>\n            <DropdownMenuSeparator />\n            {workspace.plan === \"free\" ? (\n              <>\n                <DropdownMenuItem asChild>\n                  <Link\n                    href=\"/settings/billing\"\n                    onClick={() => setOpenMobile(false)}\n                    className=\"font-commit-mono tracking-tight\"\n                  >\n                    <Sparkles />\n                    Upgrade Workspace\n                  </Link>\n                </DropdownMenuItem>\n                <DropdownMenuSeparator />\n              </>\n            ) : null}\n            <DropdownMenuGroup className=\"font-commit-mono tracking-tight\">\n              <DropdownMenuItem asChild>\n                <Link\n                  href=\"/settings/account\"\n                  onClick={() => setOpenMobile(false)}\n                >\n                  <User />\n                  Account\n                </Link>\n              </DropdownMenuItem>\n              <DropdownMenuSub>\n                <DropdownMenuSubTrigger className=\"gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0\">\n                  {theme === \"dark\" ? (\n                    <Moon />\n                  ) : theme === \"light\" ? (\n                    <Sun />\n                  ) : (\n                    <Laptop />\n                  )}\n                  Theme\n                </DropdownMenuSubTrigger>\n                <DropdownMenuPortal>\n                  <DropdownMenuSubContent className=\"font-commit-mono tracking-tight\">\n                    <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n                      <Sun /> Light\n                    </DropdownMenuItem>\n                    <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n                      <Moon /> Dark\n                    </DropdownMenuItem>\n                    <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n                      <Laptop /> System\n                    </DropdownMenuItem>\n                  </DropdownMenuSubContent>\n                </DropdownMenuPortal>\n              </DropdownMenuSub>\n              <DropdownMenuItem asChild>\n                <Link\n                  href=\"/settings/billing\"\n                  onClick={() => setOpenMobile(false)}\n                >\n                  <CreditCard />\n                  Billing\n                </Link>\n              </DropdownMenuItem>\n            </DropdownMenuGroup>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem\n              onClick={() => signOut()}\n              className=\"font-commit-mono tracking-tight\"\n            >\n              <LogOut />\n              Log out\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </SidebarMenuItem>\n    </SidebarMenu>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/sidebar-metadata.tsx",
    "content": "import { ChevronRight } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateDescription,\n} from \"@/components/content/empty-state\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@openstatus/ui/components/ui/collapsible\";\nimport {\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { TooltipProvider } from \"@radix-ui/react-tooltip\";\n\nexport type SidebarMetadataProps = {\n  label: string;\n  items?: {\n    label: string;\n    value: React.ReactNode;\n    isNested?: boolean;\n  }[];\n};\n\nexport function SidebarMetadata({ label, items }: SidebarMetadataProps) {\n  return (\n    <SidebarGroup className=\"p-0\">\n      <Collapsible defaultOpen className=\"group/collapsible border-b\">\n        <SidebarGroupLabel\n          asChild\n          className=\"group/label h-9 w-full rounded-none text-sidebar-foreground text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\"\n        >\n          <CollapsibleTrigger>\n            {label}{\" \"}\n            <ChevronRight className=\"ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90\" />\n          </CollapsibleTrigger>\n        </SidebarGroupLabel>\n        <CollapsibleContent>\n          <SidebarGroupContent className=\"border-t\">\n            {items && items.length > 0 ? (\n              <SidebarMetadataTable items={items} />\n            ) : (\n              <EmptyStateContainer className=\"m-2\">\n                <EmptyStateDescription>No {label}</EmptyStateDescription>\n              </EmptyStateContainer>\n            )}\n          </SidebarGroupContent>\n        </CollapsibleContent>\n      </Collapsible>\n    </SidebarGroup>\n  );\n}\n\nfunction SidebarMetadataTable({\n  items,\n}: {\n  items: {\n    label: string;\n    value: React.ReactNode;\n    isNested?: boolean;\n    tooltip?: string;\n  }[];\n}) {\n  return (\n    <Table>\n      <TableHeader className=\"sr-only\">\n        <TableRow>\n          <TableHead className=\"w-26\">Label</TableHead>\n          <TableHead>Value</TableHead>\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        {items.map((item, index) => (\n          <TableRow key={`${item.label}-${index}`}>\n            <TableCell className=\"w-26 border-r text-muted-foreground\">\n              <div className=\"min-w-[90px] max-w-[90px] truncate\">\n                {item.isNested ? \"└ \" : \"\"}\n                {item.label}\n              </div>\n            </TableCell>\n            <SidebarMetadataTableCell className=\"max-w-0 truncate font-mono\">\n              {item.value}\n            </SidebarMetadataTableCell>\n          </TableRow>\n        ))}\n      </TableBody>\n    </Table>\n  );\n}\n\nfunction SidebarMetadataTableCell({\n  className,\n  ...props\n}: React.ComponentProps<typeof TableCell>) {\n  const ref = React.useRef<HTMLTableCellElement>(null);\n  const [isTruncated, setIsTruncated] = React.useState(false);\n  const { copy, isCopied } = useCopyToClipboard();\n  const [open, setOpen] = React.useState(false);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  React.useEffect(() => {\n    if (ref.current) {\n      setIsTruncated(ref.current.scrollWidth > ref.current.clientWidth);\n    }\n  }, [ref]);\n\n  const handleClick = () => {\n    if (typeof props.children === \"string\") {\n      copy(props.children, { withToast: false, timeout: 1000 });\n    }\n  };\n\n  React.useEffect(() => {\n    if (isCopied) setOpen(true);\n  }, [isCopied]);\n\n  return (\n    <TableCell\n      {...props}\n      ref={ref}\n      className={cn(\n        typeof props.children === \"string\" && \"cursor-pointer\",\n        className,\n      )}\n      onClick={handleClick}\n    >\n      <TooltipProvider>\n        {isTruncated || isCopied ? (\n          <Tooltip open={open} onOpenChange={setOpen}>\n            <TooltipTrigger\n              // NOTE: all the prevent default events avoid the tooltip to hide and show again\n              onClick={(event) => event.preventDefault()}\n              onPointerDown={(event) => event.preventDefault()}\n              asChild\n            >\n              <span className=\"block truncate\">{props.children}</span>\n            </TooltipTrigger>\n            <TooltipContent\n              onPointerDownOutside={(event) => event.preventDefault()}\n              side=\"left\"\n            >\n              {isCopied ? \"Copied\" : props.children}\n            </TooltipContent>\n          </Tooltip>\n        ) : (\n          props.children\n        )}\n      </TooltipProvider>\n    </TableCell>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/sidebar-right.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarHeader,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarProvider,\n  SidebarSeparator,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useMediaQuery } from \"@openstatus/ui/hooks/use-media-query\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { PanelRight } from \"lucide-react\";\nimport { Kbd } from \"../common/kbd\";\nimport { SidebarMetadata, type SidebarMetadataProps } from \"./sidebar-metadata\";\n\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"]\";\nconst SIDEBAR_WIDTH = \"18rem\";\nconst SIDEBAR_WIDTH_2XL = \"24rem\";\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\";\n\ntype SidebarRightProps = React.ComponentProps<typeof Sidebar> & {\n  header: string;\n  metadata: SidebarMetadataProps[];\n  footerButton?: React.ComponentProps<typeof SidebarMenuButton>;\n};\n\nexport function SidebarRight({\n  header,\n  metadata,\n  footerButton,\n  ...props\n}: SidebarRightProps) {\n  const isMobile = useIsMobile();\n  const is2XL = useMediaQuery(\"(min-width: 1536px)\");\n  return (\n    <SidebarProvider\n      style={\n        {\n          \"--sidebar-width\": isMobile\n            ? SIDEBAR_WIDTH_MOBILE\n            : is2XL\n              ? SIDEBAR_WIDTH_2XL\n              : SIDEBAR_WIDTH,\n        } as React.CSSProperties\n      }\n      defaultOpen={false}\n      cookieName=\"sidebar_state_right\"\n    >\n      <Sidebar\n        collapsible=\"offcanvas\"\n        side=\"right\"\n        className=\"top-14 flex h-[calc(100svh_-_56px)]\"\n        {...props}\n      >\n        <SidebarHeader className=\"relative border-sidebar-border border-b\">\n          {header}\n          <div className=\"-left-9 absolute inset-y-0 z-10 flex items-center justify-center\">\n            <TooltipProvider>\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <SidebarTrigger />\n                </TooltipTrigger>\n                <TooltipContent side=\"left\">\n                  <p className=\"mr-px inline-flex items-center\">\n                    Toggle Sidebar{\" \"}\n                    <Kbd className=\"border-muted-foreground bg-primary font-mono text-background\">\n                      ⌘\n                    </Kbd>\n                    <Kbd className=\"border-muted-foreground bg-primary font-mono text-background\">\n                      {SIDEBAR_KEYBOARD_SHORTCUT}\n                    </Kbd>\n                  </p>\n                </TooltipContent>\n              </Tooltip>\n            </TooltipProvider>\n          </div>\n        </SidebarHeader>\n        <SidebarContent className=\"flex flex-col gap-0\">\n          {metadata.map((item) => (\n            <SidebarMetadata key={item.label} {...item} />\n          ))}\n        </SidebarContent>\n        <SidebarSeparator className=\"mx-0\" />\n        {footerButton ? (\n          <SidebarFooter>\n            <SidebarMenu>\n              <SidebarMenuItem>\n                <SidebarMenuButton {...footerButton} />\n              </SidebarMenuItem>\n            </SidebarMenu>\n          </SidebarFooter>\n        ) : null}\n      </Sidebar>\n    </SidebarProvider>\n  );\n}\n\nexport function SidebarTrigger({\n  className,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { toggleSidebar } = useSidebar();\n\n  // Adds a keyboard shortcut to toggle the sidebar.\n  React.useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (\n        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n        (event.metaKey || event.ctrlKey)\n      ) {\n        event.preventDefault();\n        toggleSidebar();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [toggleSidebar]);\n\n  return (\n    <Button\n      data-sidebar=\"trigger\"\n      data-slot=\"sidebar-trigger\"\n      variant=\"ghost\"\n      size=\"icon\"\n      className={cn(\"size-7\", className)}\n      onClick={(event) => {\n        onClick?.(event);\n        toggleSidebar();\n      }}\n      {...props}\n    >\n      <PanelRight />\n      <span className=\"sr-only\">Toggle Sidebar</span>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/nav/workspace-switcher.tsx",
    "content": "\"use client\";\n\nimport { ChevronsUpDown, Plus } from \"lucide-react\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport {\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Link } from \"../common/link\";\n\nexport function WorkspaceSwitcher() {\n  const { isMobile, setOpenMobile } = useSidebar();\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n  const { data: workspaces } = useQuery(trpc.workspace.list.queryOptions());\n\n  if (!workspace) return null;\n\n  function handleClick(slug: string) {\n    document.cookie = `workspace-slug=${slug}; path=/;`;\n    window.location.href = \"/overview\";\n  }\n\n  return (\n    <SidebarMenu>\n      <SidebarMenuItem>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <SidebarMenuButton\n              size=\"lg\"\n              className=\"h-14 rounded-none px-4 ring-inset data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:mx-2! group-data-[collapsible=icon]:rounded-lg! group-data-[collapsible=icon]:px-0!\"\n            >\n              <div className=\"flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary\">\n                <div className=\"size-8 overflow-hidden rounded-lg\">\n                  <img\n                    src={`https://api.dicebear.com/9.x/glass/svg?seed=${workspace.slug}`}\n                    alt=\"avatar\"\n                  />\n                </div>\n              </div>\n              <div className=\"grid flex-1 text-left text-sm leading-tight\">\n                <div className=\"truncate font-medium\">\n                  {workspace.name || \"Untitled Workspace\"}\n                </div>\n                <div className=\"truncate text-xs\">\n                  <span className=\"font-commit-mono tracking-tight\">\n                    {workspace.slug}\n                  </span>{\" \"}\n                  <span className=\"text-muted-foreground\">\n                    {workspace.plan === \"team\" ? \"pro\" : workspace.plan}\n                  </span>\n                </div>\n              </div>\n              <ChevronsUpDown className=\"ml-auto\" />\n            </SidebarMenuButton>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent\n            className=\"w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg\"\n            align=\"start\"\n            side={isMobile ? \"bottom\" : \"right\"}\n            sideOffset={4}\n          >\n            <DropdownMenuLabel className=\"text-muted-foreground text-xs\">\n              Workspaces\n            </DropdownMenuLabel>\n            {workspaces?.map((workspace) => (\n              <DropdownMenuItem\n                key={workspace.id}\n                onClick={() => {\n                  handleClick(workspace.slug);\n                  setOpenMobile(false);\n                }}\n                className=\"gap-2 p-2\"\n              >\n                <span className=\"truncate\">\n                  {workspace.name || \"Untitled Workspace\"}\n                </span>\n                <span className=\"truncate font-mono text-muted-foreground text-xs\">\n                  {workspace.slug}\n                </span>\n              </DropdownMenuItem>\n            ))}\n            <DropdownMenuSeparator />\n            <DropdownMenuItem className=\"gap-2 p-2\" asChild>\n              <Link href=\"/settings/general\">\n                <Plus />\n                <div className=\"font-commit-mono text-muted-foreground tracking-tight\">\n                  Add team member\n                </div>\n              </Link>\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </SidebarMenuItem>\n    </SidebarMenu>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/popovers/popover-quantile.tsx",
    "content": "import {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function PopoverQuantile({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof PopoverTrigger>) {\n  return (\n    <Popover>\n      <PopoverTrigger\n        className={cn(\n          \"shrink-0 rounded-md p-0 underline decoration-muted-foreground/70 decoration-dotted underline-offset-2 outline-none transition-all hover:decoration-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=open]:decoration-foreground dark:aria-invalid:ring-destructive/40\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </PopoverTrigger>\n      <PopoverContent side=\"top\" className=\"p-0 text-sm\">\n        <p className=\"px-3 py-2 font-medium\">\n          A quantile represents a specific percentile in your dataset.\n        </p>\n        <Separator />\n        <p className=\"px-3 py-2 text-muted-foreground\">\n          For example, p50 is the 50th percentile - the point below which 50% of\n          data falls. Higher percentiles include more data and highlight the\n          upper range.\n        </p>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/popovers/popover-resolution.tsx",
    "content": "import {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function PopoverResolution({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof PopoverTrigger>) {\n  return (\n    <Popover>\n      <PopoverTrigger\n        className={cn(\n          \"shrink-0 rounded-md p-0 underline decoration-muted-foreground/70 decoration-dotted underline-offset-2 outline-none transition-all hover:decoration-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=open]:decoration-foreground dark:aria-invalid:ring-destructive/40\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </PopoverTrigger>\n      <PopoverContent side=\"top\" className=\"p-0 text-sm\">\n        <p className=\"px-3 py-2 font-medium\">\n          Run data aggregation on fixed time boundaries.\n        </p>\n        <Separator />\n        <p className=\"px-3 py-2 text-muted-foreground\">\n          A 30-minute resolution aligns to the top or bottom of the hour (e.g.,\n          00:00, 00:30) so all intervals are consistent for analysis.\n        </p>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/tailwind-indicator.tsx",
    "content": "export function TailwindIndicator() {\n  if (process.env.NODE_ENV === \"production\") return null;\n\n  return (\n    <div className=\"fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-foreground p-3 font-mono text-background text-xs\">\n      <div className=\"block sm:hidden\">xs</div>\n      <div className=\"hidden sm:block md:hidden\">sm</div>\n      <div className=\"hidden md:block lg:hidden\">md</div>\n      <div className=\"hidden lg:block xl:hidden\">lg</div>\n      <div className=\"hidden xl:block 2xl:hidden\">xl</div>\n      <div className=\"hidden 2xl:block\">2xl</div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport type * as React from \"react\";\n\nexport function ThemeProvider({\n  children,\n  ...props\n}: React.ComponentProps<typeof NextThemesProvider>) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport type * as React from \"react\";\n\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Laptop, Moon, Sun } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { useEffect } from \"react\";\n\nexport function ThemeToggle({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectTrigger>) {\n  const { setTheme, theme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  // NOTE: hydration error if we don't do this\n  if (!mounted) {\n    return (\n      <Select>\n        <SelectTrigger className={cn(\"w-[180px]\", className)} {...props}>\n          <SelectValue placeholder=\"Select theme\" />\n        </SelectTrigger>\n      </Select>\n    );\n  }\n\n  return (\n    <Select value={theme} onValueChange={setTheme}>\n      <SelectTrigger className={cn(\"w-[180px]\", className)} {...props}>\n        <SelectValue defaultValue={theme} placeholder=\"Select theme\" />\n      </SelectTrigger>\n      <SelectContent>\n        <SelectItem value=\"light\">\n          <div className=\"flex items-center gap-2\">\n            <Sun className=\"h-4 w-4\" />\n            <span>Light</span>\n          </div>\n        </SelectItem>\n        <SelectItem value=\"dark\">\n          <div className=\"flex items-center gap-2\">\n            <Moon className=\"h-4 w-4\" />\n            <span>Dark</span>\n          </div>\n        </SelectItem>\n        <SelectItem value=\"system\">\n          <div className=\"flex items-center gap-2\">\n            <Laptop className=\"h-4 w-4\" />\n            <span>System</span>\n          </div>\n        </SelectItem>\n      </SelectContent>\n    </Select>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/ui/data-table/data-table-action-bar.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport type { Table } from \"@tanstack/react-table\";\nimport { Loader, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\nexport interface DataTableActionBarProps<TData>\n  extends React.ComponentProps<\"div\"> {\n  table: Table<TData>;\n  visible?: boolean;\n  container?: Element | DocumentFragment | null;\n}\n\nfunction DataTableActionBar<TData>({\n  table,\n  visible: visibleProp,\n  container: containerProp,\n  children,\n  className,\n  ...props\n}: DataTableActionBarProps<TData>) {\n  const [mounted, setMounted] = React.useState(false);\n\n  React.useLayoutEffect(() => {\n    setMounted(true);\n  }, []);\n\n  React.useEffect(() => {\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key === \"Escape\") {\n        table.toggleAllRowsSelected(false);\n      }\n    }\n\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => window.removeEventListener(\"keydown\", onKeyDown);\n  }, [table]);\n\n  const container =\n    containerProp ?? (mounted ? globalThis.document?.body : null);\n\n  if (!container) return null;\n\n  const visible =\n    visibleProp ?? table.getFilteredSelectedRowModel().rows.length > 0;\n\n  return ReactDOM.createPortal(\n    <div>\n      {visible && (\n        <div\n          role=\"toolbar\"\n          aria-orientation=\"horizontal\"\n          className={cn(\n            \"fixed inset-x-0 bottom-6 z-50 mx-auto flex w-fit flex-wrap items-center justify-center gap-2 rounded-md border bg-background p-2 text-foreground shadow-sm\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </div>\n      )}\n    </div>,\n    container,\n  );\n}\n\ninterface DataTableActionBarActionProps\n  extends React.ComponentProps<typeof Button> {\n  tooltip?: string;\n  isPending?: boolean;\n}\n\nfunction DataTableActionBarAction({\n  size = \"sm\",\n  tooltip,\n  isPending,\n  disabled,\n  className,\n  children,\n  ...props\n}: DataTableActionBarActionProps) {\n  const trigger = (\n    <Button\n      variant=\"secondary\"\n      size={size}\n      className={cn(\n        \"gap-1.5 border border-secondary bg-secondary/50 hover:bg-secondary/70 [&>svg]:size-3.5\",\n        size === \"icon\" ? \"size-7\" : \"h-7\",\n        className,\n      )}\n      disabled={disabled || isPending}\n      {...props}\n    >\n      {isPending ? <Loader className=\"animate-spin\" /> : children}\n    </Button>\n  );\n\n  if (!tooltip) return trigger;\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{trigger}</TooltipTrigger>\n      <TooltipContent\n        sideOffset={6}\n        className=\"border bg-accent font-semibold text-foreground dark:bg-zinc-900 [&>span]:hidden\"\n      >\n        <p>{tooltip}</p>\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n\ninterface DataTableActionBarSelectionProps<TData> {\n  table: Table<TData>;\n}\n\nfunction DataTableActionBarSelection<TData>({\n  table,\n}: DataTableActionBarSelectionProps<TData>) {\n  const onClearSelection = React.useCallback(() => {\n    table.toggleAllRowsSelected(false);\n  }, [table]);\n\n  return (\n    <div className=\"flex h-7 items-center rounded-md border pr-1 pl-2.5\">\n      <span className=\"whitespace-nowrap text-xs\">\n        {table.getFilteredSelectedRowModel().rows.length} selected\n      </span>\n      <Separator\n        orientation=\"vertical\"\n        className=\"mr-1 ml-2 data-[orientation=vertical]:h-4\"\n      />\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            className=\"size-5\"\n            onClick={onClearSelection}\n          >\n            <X className=\"size-3.5\" />\n          </Button>\n        </TooltipTrigger>\n        <TooltipContent\n          sideOffset={10}\n          className=\"flex items-center gap-2 border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-zinc-900 [&>span]:hidden\"\n        >\n          <p>Clear selection</p>\n          <kbd className=\"select-none rounded border bg-background px-1.5 py-px font-mono font-normal text-[0.7rem] text-foreground shadow-xs\">\n            <abbr title=\"Escape\" className=\"no-underline\">\n              Esc\n            </abbr>\n          </kbd>\n        </TooltipContent>\n      </Tooltip>\n    </div>\n  );\n}\n\nexport {\n  DataTableActionBar,\n  DataTableActionBarAction,\n  DataTableActionBarSelection,\n};\n"
  },
  {
    "path": "apps/dashboard/src/components/ui/data-table/data-table-column-header.tsx",
    "content": "import type { Column } from \"@tanstack/react-table\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\ninterface DataTableColumnHeaderProps<TData, TValue>\n  extends React.ComponentProps<\"button\"> {\n  column: Column<TData, TValue>;\n  title: string;\n}\n\nexport function DataTableColumnHeader<TData, TValue>({\n  column,\n  title,\n  className,\n  ...props\n}: DataTableColumnHeaderProps<TData, TValue>) {\n  if (!column.getCanSort()) {\n    return <div className={cn(className)}>{title}</div>;\n  }\n\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"sm\"\n      onClick={() => {\n        column.toggleSorting(undefined);\n      }}\n      className={cn(\n        \"flex h-7 w-full items-center justify-between gap-2 px-0 py-0 hover:bg-transparent dark:hover:bg-transparent\",\n        className,\n      )}\n      {...props}\n    >\n      <span>{title}</span>\n      <span className=\"flex flex-col\">\n        <ChevronUp\n          className={cn(\n            \"-mb-0.5 size-3\",\n            column.getIsSorted() === \"asc\"\n              ? \"text-accent-foreground\"\n              : \"text-muted-foreground\",\n          )}\n        />\n        <ChevronDown\n          className={cn(\n            \"-mt-0.5 size-3\",\n            column.getIsSorted() === \"desc\"\n              ? \"text-accent-foreground\"\n              : \"text-muted-foreground\",\n          )}\n        />\n      </span>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/ui/data-table/data-table-faceted-filter.tsx",
    "content": "import type { Column } from \"@tanstack/react-table\";\nimport { Check, PlusCircle } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from \"@openstatus/ui/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\ninterface DataTableFacetedFilterProps<TData, TValue> {\n  column?: Column<TData, TValue>;\n  title?: string;\n  options: {\n    label: string;\n    value: string | number;\n    icon?: React.ComponentType<{ className?: string }>;\n  }[];\n}\n\nexport function DataTableFacetedFilter<TData, TValue>({\n  column,\n  title,\n  options,\n}: DataTableFacetedFilterProps<TData, TValue>) {\n  const facets = column?.getFacetedUniqueValues();\n  const selectedValues = new Set(\n    column?.getFilterValue() as (string | number)[],\n  );\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\" className=\"h-8 border-dashed\">\n          <PlusCircle />\n          {title}\n          {selectedValues?.size > 0 && (\n            <>\n              <Separator orientation=\"vertical\" className=\"mx-2 h-4\" />\n              <Badge\n                variant=\"secondary\"\n                className=\"rounded-sm px-1 font-normal lg:hidden\"\n              >\n                {selectedValues.size}\n              </Badge>\n              <div className=\"hidden space-x-1 lg:flex\">\n                {selectedValues.size > 2 ? (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"rounded-sm px-1 font-normal\"\n                  >\n                    {selectedValues.size} selected\n                  </Badge>\n                ) : (\n                  options\n                    .filter((option) => selectedValues.has(option.value))\n                    .map((option) => (\n                      <Badge\n                        variant=\"secondary\"\n                        key={option.value}\n                        className=\"rounded-sm px-1 font-normal\"\n                      >\n                        {option.label}\n                      </Badge>\n                    ))\n                )}\n              </div>\n            </>\n          )}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[200px] p-0\" align=\"start\">\n        <Command>\n          <CommandInput placeholder={title} />\n          <CommandList>\n            <CommandEmpty>No results found.</CommandEmpty>\n            <CommandGroup>\n              {options.map((option) => {\n                const isSelected = selectedValues.has(option.value);\n                return (\n                  <CommandItem\n                    key={option.value}\n                    onSelect={() => {\n                      if (isSelected) {\n                        selectedValues.delete(option.value);\n                      } else {\n                        selectedValues.add(option.value);\n                      }\n                      const filterValues = Array.from(selectedValues);\n                      column?.setFilterValue(\n                        filterValues.length ? filterValues : undefined,\n                      );\n                    }}\n                  >\n                    <div\n                      className={cn(\n                        \"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary\",\n                        isSelected\n                          ? \"bg-primary text-primary-foreground\"\n                          : \"opacity-50 [&_svg]:invisible\",\n                      )}\n                    >\n                      <Check />\n                    </div>\n                    {option.icon && (\n                      <option.icon className=\"mr-2 h-4 w-4 text-muted-foreground\" />\n                    )}\n                    <span>{option.label}</span>\n                    {facets?.get(option.value) && (\n                      <span className=\"ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs\">\n                        {facets.get(option.value)}\n                      </span>\n                    )}\n                  </CommandItem>\n                );\n              })}\n            </CommandGroup>\n            {selectedValues.size > 0 && (\n              <>\n                <CommandSeparator />\n                <CommandGroup>\n                  <CommandItem\n                    onSelect={() => column?.setFilterValue(undefined)}\n                    className=\"justify-center text-center\"\n                  >\n                    Clear filters\n                  </CommandItem>\n                </CommandGroup>\n              </>\n            )}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/ui/data-table/data-table-pagination.tsx",
    "content": "import type { Table } from \"@tanstack/react-table\";\nimport {\n  ChevronLeft,\n  ChevronRight,\n  ChevronsLeft,\n  ChevronsRight,\n} from \"lucide-react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\n\nexport interface DataTablePaginationProps<TData> {\n  table: Table<TData>;\n}\n\nexport function DataTablePagination<TData>({\n  table,\n}: DataTablePaginationProps<TData>) {\n  return (\n    <div className=\"flex flex-wrap items-center justify-between gap-2\">\n      <div className=\"flex-1 text-muted-foreground text-sm\">\n        {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n        {table.getFilteredRowModel().rows.length} row(s) selected.\n      </div>\n      <div className=\"flex items-center space-x-6 lg:space-x-8\">\n        <div className=\"flex items-center space-x-2\">\n          <p className=\"font-medium text-sm\">Rows per page</p>\n          <Select\n            value={`${table.getState().pagination.pageSize}`}\n            onValueChange={(value) => {\n              table.setPageSize(Number(value));\n            }}\n          >\n            <SelectTrigger className=\"h-8 w-[70px]\">\n              <SelectValue placeholder={table.getState().pagination.pageSize} />\n            </SelectTrigger>\n            <SelectContent side=\"top\">\n              {[10, 20, 30, 40, 50].map((pageSize) => (\n                <SelectItem key={pageSize} value={`${pageSize}`}>\n                  {pageSize}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n        <div className=\"flex items-center justify-center font-medium text-sm\">\n          Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n          {table.getPageCount()}\n        </div>\n        <div className=\"flex items-center space-x-2\">\n          <Button\n            variant=\"outline\"\n            className=\"hidden h-8 w-8 p-0 lg:flex\"\n            onClick={() => table.setPageIndex(0)}\n            disabled={!table.getCanPreviousPage()}\n          >\n            <span className=\"sr-only\">Go to first page</span>\n            <ChevronsLeft />\n          </Button>\n          <Button\n            variant=\"outline\"\n            className=\"h-8 w-8 p-0\"\n            onClick={() => table.previousPage()}\n            disabled={!table.getCanPreviousPage()}\n          >\n            <span className=\"sr-only\">Go to previous page</span>\n            <ChevronLeft />\n          </Button>\n          <Button\n            variant=\"outline\"\n            className=\"h-8 w-8 p-0\"\n            onClick={() => table.nextPage()}\n            disabled={!table.getCanNextPage()}\n          >\n            <span className=\"sr-only\">Go to next page</span>\n            <ChevronRight />\n          </Button>\n          <Button\n            variant=\"outline\"\n            className=\"hidden h-8 w-8 p-0 lg:flex\"\n            onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n            disabled={!table.getCanNextPage()}\n          >\n            <span className=\"sr-only\">Go to last page</span>\n            <ChevronsRight />\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function DataTablePaginationSimple<TData>({\n  table,\n}: DataTablePaginationProps<TData>) {\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex-1 text-muted-foreground text-sm\">\n        {table.getFilteredRowModel().rows.length} of{\" \"}\n        {table.getPreFilteredRowModel().rows.length} row(s) filtered.\n      </div>\n      <div className=\"flex items-center space-x-2\">\n        <Button\n          variant=\"outline\"\n          className=\"hidden h-8 w-8 p-0 lg:flex\"\n          onClick={() => table.setPageIndex(0)}\n          disabled={!table.getCanPreviousPage()}\n        >\n          <span className=\"sr-only\">Go to first page</span>\n          <ChevronsLeft />\n        </Button>\n        <Button\n          variant=\"outline\"\n          className=\"h-8 w-8 p-0\"\n          onClick={() => table.previousPage()}\n          disabled={!table.getCanPreviousPage()}\n        >\n          <span className=\"sr-only\">Go to previous page</span>\n          <ChevronLeft />\n        </Button>\n        <Button\n          variant=\"outline\"\n          className=\"h-8 w-8 p-0\"\n          onClick={() => table.nextPage()}\n          disabled={!table.getCanNextPage()}\n        >\n          <span className=\"sr-only\">Go to next page</span>\n          <ChevronRight />\n        </Button>\n        <Button\n          variant=\"outline\"\n          className=\"hidden h-8 w-8 p-0 lg:flex\"\n          onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n          disabled={!table.getCanNextPage()}\n        >\n          <span className=\"sr-only\">Go to last page</span>\n          <ChevronsRight />\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/ui/data-table/data-table-skeleton.tsx",
    "content": "import { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\n\ninterface DataTableSkeletonProps {\n  /**\n   * Number of rows to render\n   * @default 10\n   */\n  rows?: number;\n}\n\n// TODO: add checkbox skeleton (for MonitorTable e.g.)\n\nexport function DataTableSkeleton({ rows = 3 }: DataTableSkeletonProps) {\n  return (\n    <Table>\n      <TableHeader className=\"bg-muted/50\">\n        <TableRow className=\"hover:bg-transparent\">\n          <TableHead>\n            <Skeleton className=\"my-1.5 h-4 w-24\" />\n          </TableHead>\n          <TableHead className=\"hidden sm:table-cell\">\n            <Skeleton className=\"my-1.5 h-4 w-32\" />\n          </TableHead>\n          <TableHead className=\"hidden md:table-cell\">\n            <Skeleton className=\"my-1.5 h-4 w-16\" />\n          </TableHead>\n          <TableHead>\n            <Skeleton className=\"my-1.5 h-4 w-20\" />\n          </TableHead>\n          <TableHead className=\"flex items-center justify-end\" />\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        {[rows].fill(0).map((_, i) => (\n          <TableRow key={i} className=\"hover:bg-transparent\">\n            <TableCell>\n              <Skeleton className=\"my-1.5 h-4 w-full max-w-40\" />\n            </TableCell>\n            <TableCell className=\"hidden sm:table-cell\">\n              <Skeleton className=\"my-1.5 h-4 w-full max-w-52\" />\n            </TableCell>\n            <TableCell className=\"hidden md:table-cell\">\n              <Skeleton className=\"my-1.5 h-4 w-24\" />\n            </TableCell>\n            <TableCell>\n              <Skeleton className=\"my-1.5 h-4 w-full max-w-40\" />\n            </TableCell>\n            <TableCell className=\"flex justify-end\">\n              <Skeleton className=\"my-1.5 h-5 w-5\" />\n            </TableCell>\n          </TableRow>\n        ))}\n      </TableBody>\n    </Table>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/ui/data-table/data-table-toobar.tsx",
    "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\n\nimport { DataTableViewOptions } from \"@/components/ui/data-table/data-table-view-options\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\n\nimport { DataTableFacetedFilter } from \"@/components/ui/data-table/data-table-faceted-filter\";\n\nexport interface DataTableToolbarProps<TData> {\n  table: Table<TData>;\n}\n\nexport function DataTableToolbar<TData>({\n  table,\n}: DataTableToolbarProps<TData>) {\n  const isFiltered = table.getState().columnFilters.length > 0;\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex flex-1 items-center space-x-2\">\n        <Input\n          placeholder=\"Filter entries...\"\n          value={(table.getColumn(\"title\")?.getFilterValue() as string) ?? \"\"}\n          onChange={(event) =>\n            table.getColumn(\"title\")?.setFilterValue(event.target.value)\n          }\n          className=\"h-8 w-[150px] lg:w-[250px]\"\n        />\n        {table.getColumn(\"status\") && (\n          <DataTableFacetedFilter\n            column={table.getColumn(\"status\")}\n            title=\"Status\"\n            options={[]}\n          />\n        )}\n        {table.getColumn(\"tags\") && (\n          <DataTableFacetedFilter\n            column={table.getColumn(\"tags\")}\n            title=\"Tags\"\n            options={[]}\n          />\n        )}\n        {isFiltered && (\n          <Button\n            variant=\"ghost\"\n            onClick={() => table.resetColumnFilters()}\n            className=\"h-8 px-2 lg:px-3\"\n          >\n            Reset\n            <X />\n          </Button>\n        )}\n      </div>\n      <DataTableViewOptions table={table} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/ui/data-table/data-table-view-options.tsx",
    "content": "\"use client\";\n\nimport { DropdownMenuTrigger } from \"@radix-ui/react-dropdown-menu\";\nimport type { Table } from \"@tanstack/react-table\";\nimport { Settings2 } from \"lucide-react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\n\ninterface DataTableViewOptionsProps<TData> {\n  table: Table<TData>;\n}\n\nexport function DataTableViewOptions<TData>({\n  table,\n}: DataTableViewOptionsProps<TData>) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          className=\"ml-auto hidden h-8 lg:flex\"\n        >\n          <Settings2 />\n          View\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"w-[150px]\">\n        <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        {table\n          .getAllColumns()\n          .filter(\n            (column) =>\n              typeof column.accessorFn !== \"undefined\" && column.getCanHide(),\n          )\n          .map((column) => {\n            return (\n              <DropdownMenuCheckboxItem\n                key={column.id}\n                className=\"capitalize\"\n                checked={column.getIsVisible()}\n                onCheckedChange={(value) => column.toggleVisibility(!!value)}\n              >\n                {column.id}\n              </DropdownMenuCheckboxItem>\n            );\n          })}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/ui/data-table/data-table.tsx",
    "content": "\"use client\";\n\nimport {\n  type ColumnDef,\n  type ColumnFiltersState,\n  type PaginationState,\n  type Row,\n  type SortingState,\n  type VisibilityState,\n  flexRender,\n  getCoreRowModel,\n  getExpandedRowModel,\n  getFacetedRowModel,\n  getFacetedUniqueValues,\n  getFilteredRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  useReactTable,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\n\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\nimport { Fragment } from \"react\";\nimport type { DataTableActionBarProps } from \"./data-table-action-bar\";\nimport type { DataTablePaginationProps } from \"./data-table-pagination\";\nimport type { DataTableToolbarProps } from \"./data-table-toobar\";\n\nexport interface DataTableProps<TData, TValue> {\n  columns: ColumnDef<TData, TValue>[];\n  data: TData[];\n  rowComponent?: React.ComponentType<{ row: Row<TData> }>;\n  toolbarComponent?: React.ComponentType<DataTableToolbarProps<TData>>;\n  actionBar?: React.ComponentType<DataTableActionBarProps<TData>>;\n  paginationComponent?: React.ComponentType<DataTablePaginationProps<TData>>;\n  onRowClick?: (row: Row<TData>) => void;\n  defaultSorting?: SortingState;\n  defaultColumnVisibility?: VisibilityState;\n  defaultColumnFilters?: ColumnFiltersState;\n  defaultPagination?: PaginationState;\n  autoResetPageIndex?: boolean;\n\n  /** access the state from the parent component */\n  columnFilters?: ColumnFiltersState;\n  setColumnFilters?: React.Dispatch<React.SetStateAction<ColumnFiltersState>>;\n  sorting?: SortingState;\n  setSorting?: React.Dispatch<React.SetStateAction<SortingState>>;\n  pagination?: PaginationState;\n  setPagination?: React.Dispatch<React.SetStateAction<PaginationState>>;\n}\n\nexport function DataTable<TData, TValue>({\n  columns,\n  data,\n  rowComponent,\n  toolbarComponent,\n  actionBar,\n  paginationComponent,\n  onRowClick,\n  defaultSorting = [],\n  defaultColumnVisibility = {},\n  defaultColumnFilters = [],\n  defaultPagination = { pageIndex: 0, pageSize: 20 },\n  autoResetPageIndex = true,\n  columnFilters,\n  setColumnFilters,\n  sorting,\n  setSorting,\n  pagination,\n  setPagination,\n}: DataTableProps<TData, TValue>) {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  const [globalFilter, setGlobalFilter] = React.useState<any>();\n  const [rowSelection, setRowSelection] = React.useState({});\n  const [columnVisibility, setColumnVisibility] =\n    React.useState<VisibilityState>(defaultColumnVisibility);\n  const [internalPagination, setInternalPagination] =\n    React.useState<PaginationState>(defaultPagination);\n  const [internalColumnFilters, setInternalColumnFilters] =\n    React.useState<ColumnFiltersState>(defaultColumnFilters);\n  const [internalSorting, setInternalSorting] =\n    React.useState<SortingState>(defaultSorting);\n\n  // Use controlled or uncontrolled column filters\n  const columnFiltersState = columnFilters ?? internalColumnFilters;\n  const setColumnFiltersState = setColumnFilters ?? setInternalColumnFilters;\n  const sortingState = sorting ?? internalSorting;\n  const setSortingState = setSorting ?? setInternalSorting;\n  const paginationState = pagination ?? internalPagination;\n  const setPaginationState = setPagination ?? setInternalPagination;\n\n  const table = useReactTable({\n    data,\n    columns,\n    state: {\n      sorting: sortingState,\n      columnVisibility,\n      rowSelection,\n      pagination: paginationState,\n      columnFilters: columnFiltersState,\n      globalFilter,\n    },\n    enableRowSelection: true,\n    onRowSelectionChange: setRowSelection,\n    onSortingChange: setSortingState,\n    onColumnFiltersChange: setColumnFiltersState,\n    onColumnVisibilityChange: setColumnVisibility,\n    onPaginationChange: setPaginationState,\n    onGlobalFilterChange: setGlobalFilter,\n    getCoreRowModel: getCoreRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getFacetedRowModel: getFacetedRowModel(),\n    getFacetedUniqueValues: getFacetedUniqueValues(),\n    getExpandedRowModel: getExpandedRowModel(),\n    autoResetPageIndex,\n    // @ts-expect-error as we have an id in the data\n    getRowCanExpand: (row) => Boolean(row.original.id),\n  });\n\n  return (\n    <div className=\"grid gap-2\">\n      {toolbarComponent\n        ? React.createElement(toolbarComponent, { table })\n        : null}\n      <Table>\n        <TableHeader>\n          {table.getHeaderGroups().map((headerGroup) => (\n            <TableRow key={headerGroup.id}>\n              {headerGroup.headers.map((header) => {\n                return (\n                  <TableHead\n                    key={header.id}\n                    colSpan={header.colSpan}\n                    className={header.column.columnDef.meta?.headerClassName}\n                  >\n                    {header.isPlaceholder\n                      ? null\n                      : flexRender(\n                          header.column.columnDef.header,\n                          header.getContext(),\n                        )}\n                  </TableHead>\n                );\n              })}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {table.getRowModel().rows?.length ? (\n            table.getRowModel().rows.map((row) => (\n              <Fragment key={row.id}>\n                <TableRow\n                  data-state={\n                    (row.getIsSelected() || row.getIsExpanded()) && \"selected\"\n                  }\n                  onClick={() => onRowClick?.(row)}\n                  className=\"data-[state=selected]:bg-muted/50\"\n                >\n                  {row.getVisibleCells().map((cell) => (\n                    <TableCell\n                      key={cell.id}\n                      className={cell.column.columnDef.meta?.cellClassName}\n                    >\n                      {flexRender(\n                        cell.column.columnDef.cell,\n                        cell.getContext(),\n                      )}\n                    </TableCell>\n                  ))}\n                </TableRow>\n                {row.getIsExpanded() && (\n                  <TableRow className=\"hover:bg-background\">\n                    <TableCell\n                      className=\"p-0\"\n                      colSpan={row.getVisibleCells().length}\n                    >\n                      {rowComponent\n                        ? React.createElement(rowComponent, { row })\n                        : null}\n                    </TableCell>\n                  </TableRow>\n                )}\n              </Fragment>\n            ))\n          ) : (\n            <TableRow>\n              <TableCell colSpan={columns.length} className=\"h-24 text-center\">\n                No results.\n              </TableCell>\n            </TableRow>\n          )}\n        </TableBody>\n        {actionBar ? React.createElement(actionBar, { table }) : null}\n      </Table>\n      {paginationComponent\n        ? React.createElement(paginationComponent, { table })\n        : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/components/ui/sortable.tsx",
    "content": "\"use client\";\n\nimport {\n  type Announcements,\n  DndContext,\n  type DndContextProps,\n  type DragEndEvent,\n  DragOverlay,\n  type DraggableSyntheticListeners,\n  type DropAnimation,\n  KeyboardSensor,\n  MouseSensor,\n  type ScreenReaderInstructions,\n  TouchSensor,\n  type UniqueIdentifier,\n  closestCenter,\n  closestCorners,\n  defaultDropAnimationSideEffects,\n  useSensor,\n  useSensors,\n} from \"@dnd-kit/core\";\nimport {\n  restrictToHorizontalAxis,\n  restrictToParentElement,\n  restrictToVerticalAxis,\n} from \"@dnd-kit/modifiers\";\nimport {\n  SortableContext,\n  type SortableContextProps,\n  arrayMove,\n  horizontalListSortingStrategy,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\nimport { composeEventHandlers, useComposedRefs } from \"@/lib/composition\";\nimport { cn } from \"@/lib/utils\";\n\nconst orientationConfig = {\n  vertical: {\n    modifiers: [restrictToVerticalAxis, restrictToParentElement],\n    strategy: verticalListSortingStrategy,\n    collisionDetection: closestCenter,\n  },\n  horizontal: {\n    modifiers: [restrictToHorizontalAxis, restrictToParentElement],\n    strategy: horizontalListSortingStrategy,\n    collisionDetection: closestCenter,\n  },\n  mixed: {\n    modifiers: [restrictToParentElement],\n    strategy: undefined,\n    collisionDetection: closestCorners,\n  },\n};\n\nconst ROOT_NAME = \"Sortable\";\nconst CONTENT_NAME = \"SortableContent\";\nconst ITEM_NAME = \"SortableItem\";\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\nconst OVERLAY_NAME = \"SortableOverlay\";\n\nconst SORTABLE_ERRORS = {\n  [ROOT_NAME]: `\\`${ROOT_NAME}\\` components must be within \\`${ROOT_NAME}\\``,\n  [CONTENT_NAME]: `\\`${CONTENT_NAME}\\` must be within \\`${ROOT_NAME}\\``,\n  [ITEM_NAME]: `\\`${ITEM_NAME}\\` must be within \\`${CONTENT_NAME}\\``,\n  [ITEM_HANDLE_NAME]: `\\`${ITEM_HANDLE_NAME}\\` must be within \\`${ITEM_NAME}\\``,\n  [OVERLAY_NAME]: `\\`${OVERLAY_NAME}\\` must be within \\`${ROOT_NAME}\\``,\n} as const;\n\ninterface SortableRootContextValue<T> {\n  id: string;\n  items: UniqueIdentifier[];\n  modifiers: DndContextProps[\"modifiers\"];\n  strategy: SortableContextProps[\"strategy\"];\n  activeId: UniqueIdentifier | null;\n  setActiveId: (id: UniqueIdentifier | null) => void;\n  getItemValue: (item: T) => UniqueIdentifier;\n  flatCursor: boolean;\n}\n\nconst SortableRootContext =\n  React.createContext<SortableRootContextValue<unknown> | null>(null);\nSortableRootContext.displayName = ROOT_NAME;\n\nfunction useSortableContext(name: keyof typeof SORTABLE_ERRORS) {\n  const context = React.useContext(SortableRootContext);\n  if (!context) {\n    throw new Error(SORTABLE_ERRORS[name]);\n  }\n  return context;\n}\n\ninterface GetItemValue<T> {\n  /**\n   * Callback that returns a unique identifier for each sortable item. Required for array of objects.\n   * @example getItemValue={(item) => item.id}\n   */\n  getItemValue: (item: T) => UniqueIdentifier;\n}\n\ntype SortableProps<T> = DndContextProps & {\n  value: T[];\n  onValueChange?: (items: T[]) => void;\n  onMove?: (\n    event: DragEndEvent & { activeIndex: number; overIndex: number },\n  ) => void;\n  strategy?: SortableContextProps[\"strategy\"];\n  orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\n  flatCursor?: boolean;\n} & (T extends object ? GetItemValue<T> : Partial<GetItemValue<T>>);\n\nfunction Sortable<T>(props: SortableProps<T>) {\n  const {\n    value,\n    onValueChange,\n    collisionDetection,\n    modifiers,\n    strategy,\n    onMove,\n    orientation = \"vertical\",\n    flatCursor = false,\n    getItemValue: getItemValueProp,\n    accessibility,\n    ...sortableProps\n  } = props;\n  const id = React.useId();\n  const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);\n\n  const sensors = useSensors(\n    useSensor(MouseSensor),\n    useSensor(TouchSensor),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    }),\n  );\n  const config = React.useMemo(\n    () => orientationConfig[orientation],\n    [orientation],\n  );\n\n  const getItemValue = React.useCallback(\n    (item: T): UniqueIdentifier => {\n      if (typeof item === \"object\" && !getItemValueProp) {\n        throw new Error(\n          \"getItemValue is required when using array of objects.\",\n        );\n      }\n      return getItemValueProp\n        ? getItemValueProp(item)\n        : (item as UniqueIdentifier);\n    },\n    [getItemValueProp],\n  );\n\n  const items = React.useMemo(() => {\n    return value.map((item) => getItemValue(item));\n  }, [value, getItemValue]);\n\n  const onDragEnd = React.useCallback(\n    (event: DragEndEvent) => {\n      const { active, over } = event;\n      if (over && active.id !== over?.id) {\n        const activeIndex = value.findIndex(\n          (item) => getItemValue(item) === active.id,\n        );\n        const overIndex = value.findIndex(\n          (item) => getItemValue(item) === over.id,\n        );\n\n        if (onMove) {\n          onMove({ ...event, activeIndex, overIndex });\n        } else {\n          onValueChange?.(arrayMove(value, activeIndex, overIndex));\n        }\n      }\n      setActiveId(null);\n    },\n    [value, onValueChange, onMove, getItemValue],\n  );\n\n  const announcements: Announcements = React.useMemo(\n    () => ({\n      onDragStart({ active }) {\n        const activeValue = active.id.toString();\n        return `Grabbed sortable item \"${activeValue}\". Current position is ${\n          active.data.current?.sortable.index + 1\n        } of ${value.length}. Use arrow keys to move, space to drop.`;\n      },\n      onDragOver({ active, over }) {\n        if (over) {\n          const overIndex = over.data.current?.sortable.index ?? 0;\n          const activeIndex = active.data.current?.sortable.index ?? 0;\n          const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n          const activeValue = active.id.toString();\n          return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${\n            overIndex + 1\n          } of ${value.length}.`;\n        }\n        return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n      },\n      onDragEnd({ active, over }) {\n        const activeValue = active.id.toString();\n        if (over) {\n          const overIndex = over.data.current?.sortable.index ?? 0;\n          return `Sortable item \"${activeValue}\" dropped at position ${\n            overIndex + 1\n          } of ${value.length}.`;\n        }\n        return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\n      },\n      onDragCancel({ active }) {\n        const activeIndex = active.data.current?.sortable.index ?? 0;\n        const activeValue = active.id.toString();\n        return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${\n          activeIndex + 1\n        } of ${value.length}.`;\n      },\n      onDragMove({ active, over }) {\n        if (over) {\n          const overIndex = over.data.current?.sortable.index ?? 0;\n          const activeIndex = active.data.current?.sortable.index ?? 0;\n          const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n          const activeValue = active.id.toString();\n          return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${\n            overIndex + 1\n          } of ${value.length}.`;\n        }\n        return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n      },\n    }),\n    [value],\n  );\n\n  const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\n    () => ({\n      draggable: `\n        To pick up a sortable item, press space or enter.\n        While dragging, use the ${\n          orientation === \"vertical\"\n            ? \"up and down\"\n            : orientation === \"horizontal\"\n              ? \"left and right\"\n              : \"arrow\"\n        } keys to move the item.\n        Press space or enter again to drop the item in its new position, or press escape to cancel.\n      `,\n    }),\n    [orientation],\n  );\n\n  const contextValue = React.useMemo(\n    () => ({\n      id,\n      items,\n      modifiers: modifiers ?? config.modifiers,\n      strategy: strategy ?? config.strategy,\n      activeId,\n      setActiveId,\n      getItemValue,\n      flatCursor,\n    }),\n    [\n      id,\n      items,\n      modifiers,\n      strategy,\n      config.modifiers,\n      config.strategy,\n      activeId,\n      getItemValue,\n      flatCursor,\n    ],\n  );\n\n  return (\n    <SortableRootContext.Provider\n      value={contextValue as SortableRootContextValue<unknown>}\n    >\n      <DndContext\n        collisionDetection={collisionDetection ?? config.collisionDetection}\n        modifiers={modifiers ?? config.modifiers}\n        sensors={sensors}\n        {...sortableProps}\n        id={id}\n        onDragStart={composeEventHandlers(\n          sortableProps.onDragStart,\n          ({ active }) => setActiveId(active.id),\n        )}\n        onDragEnd={composeEventHandlers(sortableProps.onDragEnd, onDragEnd)}\n        onDragCancel={composeEventHandlers(sortableProps.onDragCancel, () =>\n          setActiveId(null),\n        )}\n        accessibility={{\n          announcements,\n          screenReaderInstructions,\n          ...accessibility,\n        }}\n      />\n    </SortableRootContext.Provider>\n  );\n}\n\nconst SortableContentContext = React.createContext<boolean>(false);\nSortableContentContext.displayName = CONTENT_NAME;\n\ninterface SortableContentProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  strategy?: SortableContextProps[\"strategy\"];\n  children: React.ReactNode;\n  asChild?: boolean;\n  withoutSlot?: boolean;\n}\n\nconst SortableContent = React.forwardRef<HTMLDivElement, SortableContentProps>(\n  (props, forwardedRef) => {\n    const {\n      strategy: strategyProp,\n      asChild,\n      withoutSlot,\n      children,\n      ...contentProps\n    } = props;\n    const context = useSortableContext(CONTENT_NAME);\n\n    const ContentPrimitive = asChild ? Slot : \"div\";\n\n    return (\n      <SortableContentContext.Provider value={true}>\n        <SortableContext\n          items={context.items}\n          strategy={strategyProp ?? context.strategy}\n        >\n          {withoutSlot ? (\n            children\n          ) : (\n            <ContentPrimitive {...contentProps} ref={forwardedRef}>\n              {children}\n            </ContentPrimitive>\n          )}\n        </SortableContext>\n      </SortableContentContext.Provider>\n    );\n  },\n);\nSortableContent.displayName = CONTENT_NAME;\n\ninterface SortableItemContextValue {\n  id: string;\n  attributes: React.HTMLAttributes<HTMLElement>;\n  listeners: DraggableSyntheticListeners | undefined;\n  setActivatorNodeRef: (node: HTMLElement | null) => void;\n  isDragging?: boolean;\n  disabled?: boolean;\n}\n\nconst SortableItemContext =\n  React.createContext<SortableItemContextValue | null>(null);\nSortableItemContext.displayName = ITEM_NAME;\n\ninterface SortableItemProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  value: UniqueIdentifier;\n  asHandle?: boolean;\n  asChild?: boolean;\n  disabled?: boolean;\n}\n\nconst SortableItem = React.forwardRef<HTMLDivElement, SortableItemProps>(\n  (props, forwardedRef) => {\n    const {\n      value,\n      style,\n      asHandle,\n      asChild,\n      disabled,\n      className,\n      ...itemProps\n    } = props;\n    const inSortableContent = React.useContext(SortableContentContext);\n    const inSortableOverlay = React.useContext(SortableOverlayContext);\n\n    if (!inSortableContent && !inSortableOverlay) {\n      throw new Error(SORTABLE_ERRORS[ITEM_NAME]);\n    }\n\n    if (value === \"\") {\n      throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\n    }\n\n    const context = useSortableContext(ITEM_NAME);\n    const id = React.useId();\n    const {\n      attributes,\n      listeners,\n      setNodeRef,\n      setActivatorNodeRef,\n      transform,\n      transition,\n      isDragging,\n    } = useSortable({ id: value, disabled });\n\n    const composedRef = useComposedRefs(forwardedRef, (node) => {\n      if (disabled) return;\n      setNodeRef(node);\n      if (asHandle) setActivatorNodeRef(node);\n    });\n\n    const composedStyle = React.useMemo<React.CSSProperties>(() => {\n      return {\n        transform: CSS.Translate.toString(transform),\n        transition,\n        ...style,\n      };\n    }, [transform, transition, style]);\n\n    const itemContext = React.useMemo<SortableItemContextValue>(\n      () => ({\n        id,\n        attributes,\n        listeners,\n        setActivatorNodeRef,\n        isDragging,\n        disabled,\n      }),\n      [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\n    );\n\n    const ItemPrimitive = asChild ? Slot : \"div\";\n\n    return (\n      <SortableItemContext.Provider value={itemContext}>\n        <ItemPrimitive\n          id={id}\n          data-dragging={isDragging ? \"\" : undefined}\n          {...itemProps}\n          {...(asHandle ? attributes : {})}\n          {...(asHandle ? listeners : {})}\n          tabIndex={disabled ? undefined : 0}\n          ref={composedRef}\n          style={composedStyle}\n          className={cn(\n            \"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1\",\n            {\n              \"touch-none select-none\": asHandle,\n              \"cursor-default\": context.flatCursor,\n              \"data-dragging:cursor-grabbing\": !context.flatCursor,\n              \"cursor-grab\": !isDragging && asHandle && !context.flatCursor,\n              \"opacity-50\": isDragging,\n              \"pointer-events-none opacity-50\": disabled,\n            },\n            className,\n          )}\n        />\n      </SortableItemContext.Provider>\n    );\n  },\n);\nSortableItem.displayName = ITEM_NAME;\n\ninterface SortableItemHandleProps\n  extends React.ComponentPropsWithoutRef<\"button\"> {\n  asChild?: boolean;\n}\n\nconst SortableItemHandle = React.forwardRef<\n  HTMLButtonElement,\n  SortableItemHandleProps\n>((props, forwardedRef) => {\n  const { asChild, disabled, className, ...itemHandleProps } = props;\n  const itemContext = React.useContext(SortableItemContext);\n  if (!itemContext) {\n    throw new Error(SORTABLE_ERRORS[ITEM_HANDLE_NAME]);\n  }\n  const context = useSortableContext(ITEM_HANDLE_NAME);\n\n  const isDisabled = disabled ?? itemContext.disabled;\n\n  const composedRef = useComposedRefs(forwardedRef, (node) => {\n    if (!isDisabled) return;\n    itemContext.setActivatorNodeRef(node);\n  });\n\n  const HandlePrimitive = asChild ? Slot : \"button\";\n\n  return (\n    <HandlePrimitive\n      type=\"button\"\n      aria-controls={itemContext.id}\n      data-dragging={itemContext.isDragging ? \"\" : undefined}\n      {...itemHandleProps}\n      {...itemContext.attributes}\n      {...itemContext.listeners}\n      ref={composedRef}\n      className={cn(\n        \"select-none disabled:pointer-events-none disabled:opacity-50\",\n        context.flatCursor\n          ? \"cursor-default\"\n          : \"cursor-grab data-dragging:cursor-grabbing\",\n        className,\n      )}\n      disabled={isDisabled}\n    />\n  );\n});\nSortableItemHandle.displayName = ITEM_HANDLE_NAME;\n\nconst SortableOverlayContext = React.createContext(false);\nSortableOverlayContext.displayName = OVERLAY_NAME;\n\nconst dropAnimation: DropAnimation = {\n  sideEffects: defaultDropAnimationSideEffects({\n    styles: {\n      active: {\n        opacity: \"0.4\",\n      },\n    },\n  }),\n};\n\ninterface SortableOverlayProps\n  extends Omit<React.ComponentPropsWithoutRef<typeof DragOverlay>, \"children\"> {\n  container?: Element | DocumentFragment | null;\n  children?:\n    | ((params: { value: UniqueIdentifier }) => React.ReactNode)\n    | React.ReactNode;\n}\n\nfunction SortableOverlay(props: SortableOverlayProps) {\n  const { container: containerProp, children, ...overlayProps } = props;\n  const context = useSortableContext(OVERLAY_NAME);\n\n  const [mounted, setMounted] = React.useState(false);\n  React.useLayoutEffect(() => setMounted(true), []);\n\n  const container =\n    containerProp ?? (mounted ? globalThis.document?.body : null);\n\n  if (!container) return null;\n\n  return ReactDOM.createPortal(\n    <DragOverlay\n      dropAnimation={dropAnimation}\n      modifiers={context.modifiers}\n      className={cn(!context.flatCursor && \"cursor-grabbing\")}\n      {...overlayProps}\n    >\n      <SortableOverlayContext.Provider value={true}>\n        {context.activeId\n          ? typeof children === \"function\"\n            ? children({ value: context.activeId })\n            : children\n          : null}\n      </SortableOverlayContext.Provider>\n    </DragOverlay>,\n    container,\n  );\n}\n\nconst Root = Sortable;\nconst Content = SortableContent;\nconst Item = SortableItem;\nconst ItemHandle = SortableItemHandle;\nconst Overlay = SortableOverlay;\n\nexport {\n  Root,\n  Content,\n  Item,\n  ItemHandle,\n  Overlay,\n  //\n  Sortable,\n  SortableContent,\n  SortableItem,\n  SortableItemHandle,\n  SortableOverlay,\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/audit-logs.client.ts",
    "content": "import { formatMilliseconds } from \"@/lib/formatter\";\nimport type { PrivateLocation } from \"@openstatus/db/src/schema\";\nimport { getRegionInfo } from \"@openstatus/regions\";\nimport {\n  CircleAlert,\n  CircleCheck,\n  CircleMinus,\n  Send,\n  Siren,\n} from \"lucide-react\";\n\nexport const config = {\n  \"incident.created\": {\n    icon: Siren,\n    color: \"text-destructive\",\n    title: \"Incident Created\",\n  },\n  \"incident.resolved\": {\n    icon: CircleCheck,\n    color: \"text-success\",\n    title: \"Incident Resolved\",\n  },\n  \"monitor.failed\": {\n    icon: CircleMinus,\n    color: \"text-destructive\",\n    title: \"Monitor Failed\",\n  },\n  \"notification.sent\": {\n    icon: Send,\n    color: \"text-info\",\n    title: \"Notification Sent\",\n  },\n  \"monitor.recovered\": {\n    icon: CircleCheck,\n    color: \"text-success\",\n    title: \"Monitor Recovered\",\n  },\n  \"monitor.degraded\": {\n    icon: CircleAlert,\n    color: \"text-warning\",\n    title: \"Monitor Degraded\",\n  },\n} as const;\n\nexport const getMetadata = (privateLocations?: PrivateLocation[]) => {\n  return {\n    region: {\n      label: \"Region\",\n      key: \"region\",\n      unit: undefined,\n      visible: () => true,\n      format: (value) => {\n        const regionInfo = getRegionInfo(`${value}`, {\n          location: privateLocations?.find(\n            (location) => String(location.id) === String(value),\n          )?.name,\n        });\n        return `${regionInfo.location} (${regionInfo.provider})`;\n      },\n    },\n    cronTimestamp: {\n      label: \"Timestamp\",\n      key: \"timestamp\",\n      unit: undefined,\n      visible: () => false,\n      format: (value) => String(value),\n    },\n    statusCode: {\n      label: \"Status Code\",\n      key: \"status\",\n      unit: undefined,\n      visible: (_value) => typeof _value === \"number\" && _value !== -1,\n      format: (value) => String(value),\n    },\n    latency: {\n      label: \"Latency\",\n      key: \"latency\",\n      unit: \"ms\",\n      visible: () => true,\n      format: (value) => formatMilliseconds(Number(value)),\n    },\n    provider: {\n      label: \"Provider\",\n      key: \"provider\",\n      unit: undefined,\n      visible: () => true,\n      format: (value) => String(value),\n    },\n  } as const satisfies Record<\n    string,\n    {\n      label: string;\n      key: string;\n      unit?: string | undefined;\n      visible: (value: string | number) => boolean;\n      format: (value: string | number) => string;\n    }\n  >;\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/audit-logs.ts",
    "content": "export const auditLogs = [\n  {\n    id: 3,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"incident.created\" as const,\n  },\n  {\n    id: 2,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"monitor.failed\" as const,\n    metadata: {\n      region: \"ams\",\n      status: 500,\n      latency: 1400,\n    } as const,\n  },\n  {\n    id: 1,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"notification.sent\" as const,\n    metadata: {\n      provider: \"slack\",\n    } as const,\n  },\n  {\n    id: 0,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"monitor.recovered\" as const,\n    metadata: {\n      region: \"ams\",\n      latency: 140,\n    } as const,\n  },\n  {\n    id: -1,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"monitor.degraded\" as const,\n    metadata: {\n      region: \"ams\",\n      latency: 30_000,\n    } as const,\n  },\n  {\n    id: -2,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"incident.resolved\" as const,\n  },\n  {\n    id: -3,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"incident.created\" as const,\n  },\n  {\n    id: -4,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"monitor.degraded\" as const,\n    metadata: {\n      region: \"ams\",\n      latency: 30_000,\n    } as const,\n  },\n  {\n    id: -5,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"monitor.degraded\" as const,\n    metadata: {\n      region: \"ams\",\n      latency: 32_000,\n    } as const,\n  },\n  {\n    id: -6,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"monitor.degraded\" as const,\n    metadata: {\n      region: \"ams\",\n      latency: 33_000,\n    } as const,\n  },\n  {\n    id: -7,\n    timestamp: new Date(\"2025-05-05 12:00:00\"),\n    action: \"monitor.degraded\" as const,\n    metadata: {\n      region: \"ams\",\n      latency: 34_000,\n    } as const,\n  },\n];\nexport type AuditLog = (typeof auditLogs)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/icons.ts",
    "content": "\"use client\";\n\nimport { Activity, AlertCircle, Search, SearchCheck } from \"lucide-react\";\n\nexport const status = {\n  resolved: SearchCheck,\n  investigating: AlertCircle,\n  identified: Search,\n  monitoring: Activity,\n} as const;\n\nexport const icons = {\n  status,\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/incidents.client.ts",
    "content": "import { Bookmark, Check, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"acknowledge\",\n    label: \"Acknowledge\",\n    icon: Bookmark,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"resolve\",\n    label: \"Resolve\",\n    icon: Check,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type IncidentAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<IncidentAction[\"id\"], () => Promise<void> | void>>,\n): (IncidentAction & {\n  onClick?: () => Promise<void> | void;\n})[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/incidents.ts",
    "content": "export const incidents = [\n  {\n    id: 1,\n    startedAt: new Date(\"2025-05-05 12:00:00\"),\n    acknowledged: null,\n    resolvedAt: new Date(\"2025-05-05 14:00:00\"),\n    monitor: \"OpenStatus API\",\n  },\n];\n\nexport type Incident = (typeof incidents)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/invitations.ts",
    "content": "export const invitations = [\n  {\n    id: 1,\n    email: \"thibault@openstatus.dev\",\n    role: \"member\",\n    createdAt: \"2021-01-01\",\n    expiresAt: \"2021-01-07\",\n    acceptedAt: \"2021-01-02\",\n  },\n];\n\nexport type Invitation = (typeof invitations)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/maintenances.client.ts",
    "content": "import { Cog, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Settings\",\n    icon: Cog,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type MaintenanceAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<MaintenanceAction[\"id\"], () => Promise<void> | void>>,\n): (MaintenanceAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/maintenances.ts",
    "content": "export const maintenances = [\n  {\n    id: 1,\n    title: \"DB Migration\",\n    message:\n      \"We are currently performing a db migration on our system and will be down for a few hours.\",\n    startDate: new Date(\"2025-04-01\"),\n    endDate: new Date(\"2025-04-02\"),\n    affected: [\"OpenStatus API\"],\n  },\n];\n\nexport type Maintenance = (typeof maintenances)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/members.ts",
    "content": "export const members = [\n  {\n    id: 1,\n    name: \"Maximilian Kaske\",\n    email: \"max@openstatus.dev\",\n    role: \"admin\",\n    createdAt: \"2021-01-01\",\n  },\n];\n\nexport type Member = (typeof members)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/metrics.client.ts",
    "content": "\"use client\";\n\nimport type { MetricCard } from \"@/components/metric/metric-card\";\nimport { formatDateTime, formatMilliseconds } from \"@/lib/formatter\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { monitorRegions } from \"@openstatus/db/src/schema/constants\";\nimport { startOfDay, subDays } from \"date-fns\";\nimport type { RegionMetric } from \"./region-metrics\";\n\nexport const STATUS = [\"success\", \"error\", \"degraded\"] as const;\nexport const PERIODS = [\"1d\", \"7d\", \"14d\"] as const;\nexport const REGIONS =\n  monitorRegions as unknown as (typeof monitorRegions)[number][];\nexport const PERCENTILES = [\"p50\", \"p75\", \"p90\", \"p95\", \"p99\"] as const;\nexport const INTERVALS = [5, 15, 30, 60, 120, 240, 480, 1440] as const;\nexport const TRIGGER = [\"api\", \"cron\"] as const;\n\nconst PERCENTILE_MAP = {\n  p50: \"p50Latency\",\n  p75: \"p75Latency\",\n  p90: \"p90Latency\",\n  p95: \"p95Latency\",\n  p99: \"p99Latency\",\n} as const;\n\n// FIXME: rename pipe return values\n\nexport function mapMetrics(metrics: RouterOutputs[\"tinybird\"][\"metrics\"]) {\n  return metrics.data?.map((metric) => {\n    return {\n      p50: metric.p50Latency,\n      p75: metric.p75Latency,\n      p90: metric.p90Latency,\n      p95: metric.p95Latency,\n      p99: metric.p99Latency,\n      total: metric.count,\n      uptime: (metric.success + metric.degraded) / metric.count,\n      degraded: metric.degraded,\n      error: metric.error,\n      lastTimestamp: metric.lastTimestamp,\n    };\n  });\n}\n\nexport const metricsCards = {\n  uptime: {\n    label: \"UPTIME\",\n    variant: \"success\",\n  },\n  degraded: {\n    label: \"DEGRADED\",\n    variant: \"warning\",\n  },\n  error: {\n    label: \"FAILING\",\n    variant: \"destructive\",\n  },\n  total: {\n    label: \"REQUESTS\",\n    variant: \"default\",\n  },\n  lastTimestamp: {\n    label: \"LAST CHECKED\",\n    variant: \"ghost\",\n  },\n  p50: {\n    label: \"P50\",\n    variant: \"default\",\n  },\n  p75: {\n    label: \"P75\",\n    variant: \"default\",\n  },\n  p90: {\n    label: \"P90\",\n    variant: \"default\",\n  },\n  p95: {\n    label: \"P95\",\n    variant: \"default\",\n  },\n  p99: {\n    label: \"P99\",\n    variant: \"default\",\n  },\n} as const satisfies Record<\n  keyof ReturnType<typeof mapMetrics>[number],\n  {\n    label: string;\n    variant: React.ComponentProps<typeof MetricCard>[\"variant\"];\n  }\n>;\n\nexport function mapUptime(status: RouterOutputs[\"tinybird\"][\"uptime\"]) {\n  return status.data\n    .map((status) => {\n      return {\n        ...status,\n        ok: status.success,\n        interval: formatDateTime(status.interval),\n        total: status.success + status.error + status.degraded,\n      };\n    })\n    .reverse();\n}\n\n/**\n * Transform Tinybird `metricsRegions` response into RegionMetric[] for UI.\n */\nexport function mapRegionMetrics(\n  timeline: RouterOutputs[\"tinybird\"][\"metricsRegions\"] | undefined,\n  regions: string[],\n  percentile: (typeof PERCENTILES)[number],\n): RegionMetric[] {\n  if (!timeline)\n    return (regions\n      .sort((a, b) => a.localeCompare(b))\n      .map((region) => ({\n        region,\n        p50: 0,\n        p90: 0,\n        p99: 0,\n        trend: [] as {\n          latency: number;\n          timestamp: number;\n          [key: string]: number;\n        }[],\n      })) ?? []) satisfies RegionMetric[];\n\n  type TimelineRow = (typeof timeline.data)[number];\n\n  const map = new Map<\n    string,\n    {\n      region: string;\n      p50: number;\n      p90: number;\n      p99: number;\n      trend: {\n        latency: number;\n        timestamp: number;\n        [key: string]: number;\n      }[];\n    }\n  >();\n\n  (timeline.data as TimelineRow[])\n    .filter((row) => regions.includes(row.region))\n    .sort((a, b) => a.region.localeCompare(b.region))\n    .forEach((row) => {\n      const region = row.region;\n      const entry = map.get(region) ?? {\n        region,\n        p50: 0,\n        p90: 0,\n        p99: 0,\n        trend: [],\n      };\n\n      entry.trend.push({\n        latency: row[PERCENTILE_MAP[percentile]] ?? 0,\n        timestamp: row.timestamp,\n        [region]: row[PERCENTILE_MAP[percentile]] ?? 0,\n      });\n\n      entry.p50 += row.p50Latency ?? 0;\n      entry.p90 += row.p90Latency ?? 0;\n      entry.p99 += row.p99Latency ?? 0;\n\n      map.set(region, entry);\n    });\n\n  map.forEach((entry) => {\n    const count = entry.trend.length || 1;\n    entry.trend.reverse();\n    entry.p50 = Math.round(entry.p50 / count);\n    entry.p90 = Math.round(entry.p90 / count);\n    entry.p99 = Math.round(entry.p99 / count);\n  });\n\n  return Array.from(map.values()) as RegionMetric[];\n}\n\nexport function mapGlobalMetrics(\n  metrics: RouterOutputs[\"tinybird\"][\"globalMetrics\"],\n) {\n  return metrics.data?.map((metric) => {\n    return {\n      p50: metric.p50Latency,\n      p75: metric.p75Latency,\n      p90: metric.p90Latency,\n      p95: metric.p95Latency,\n      p99: metric.p99Latency,\n      total: metric.count,\n      monitorId: metric.monitorId,\n    };\n  });\n}\n\nexport type MonitorListMetric = {\n  title: string;\n  key: \"degraded\" | \"error\" | \"active\" | \"inactive\" | \"p95\";\n  value: number | string | undefined;\n  variant: React.ComponentProps<typeof MetricCard>[\"variant\"];\n};\n\nexport const globalCards = [\n  \"active\",\n  \"degraded\",\n  \"error\",\n  \"inactive\",\n  \"p95\",\n] as const;\n\nexport const metricsGlobalCards: Record<\n  (typeof globalCards)[number],\n  {\n    title: string;\n    key: (typeof globalCards)[number];\n  }\n> = {\n  active: {\n    title: \"Normal\",\n    key: \"active\" as const,\n  },\n  degraded: {\n    title: \"Degraded\",\n    key: \"degraded\" as const,\n  },\n  error: {\n    title: \"Failing\",\n    key: \"error\" as const,\n  },\n  inactive: {\n    title: \"Inactive\",\n    key: \"inactive\" as const,\n  },\n  p95: {\n    title: \"Slowest P95\",\n    key: \"p95\" as const,\n  },\n};\n\n/**\n * Build the metric cards data that is shown on the monitors list page.\n */\nexport function getMonitorListMetrics(\n  monitors: RouterOutputs[\"monitor\"][\"list\"] = [],\n  data: {\n    p95Latency: number;\n    monitorId: string;\n  }[] = [],\n): readonly MonitorListMetric[] {\n  const variantMap: Record<\n    (typeof globalCards)[number],\n    React.ComponentProps<typeof MetricCard>[\"variant\"]\n  > = {\n    active: \"success\",\n    degraded: \"warning\",\n    error: \"destructive\",\n    inactive: \"default\",\n    p95: \"ghost\",\n  } as const;\n\n  return globalCards.map((key) => {\n    let value: number | string | undefined;\n    switch (key) {\n      case \"active\":\n        value = monitors.filter(\n          (m) => m.status === \"active\" && m.active,\n        ).length;\n        break;\n      case \"degraded\":\n        value = monitors.filter(\n          (m) => m.status === \"degraded\" && m.active,\n        ).length;\n        break;\n      case \"error\":\n        value = monitors.filter((m) => m.status === \"error\" && m.active).length;\n        break;\n      case \"inactive\":\n        value = monitors.filter((m) => m.active === false).length;\n        break;\n      case \"p95\":\n        const p95 = data.sort((a, b) => b.p95Latency - a.p95Latency)[0]\n          ?.p95Latency;\n        value = p95 ? formatMilliseconds(p95) : \"N/A\";\n        break;\n    }\n\n    return {\n      title: metricsGlobalCards[key].title,\n      key,\n      value,\n      variant: variantMap[key],\n    } as const;\n  }) as readonly MonitorListMetric[];\n}\n\nexport function mapLatency(\n  latency: RouterOutputs[\"tinybird\"][\"metricsLatency\"],\n  percentile: (typeof PERCENTILES)[number],\n) {\n  return latency.data?.map((metric) => {\n    return {\n      timestamp: formatDateTime(new Date(metric.timestamp)),\n      latency: metric[PERCENTILE_MAP[percentile]],\n    };\n  });\n}\n\nexport function mapTimingPhases(\n  timingPhases: RouterOutputs[\"tinybird\"][\"metricsTimingPhases\"],\n  percentile: (typeof PERCENTILES)[number],\n) {\n  return timingPhases.data?.map((metric) => {\n    return {\n      timestamp: formatDateTime(new Date(metric.timestamp)),\n      dns: metric[`${percentile}Dns`],\n      ttfb: metric[`${percentile}Ttfb`],\n      transfer: metric[`${percentile}Transfer`],\n      connect: metric[`${percentile}Connect`],\n      tls: metric[`${percentile}Tls`],\n    };\n  });\n}\n\nexport const periodToInterval = {\n  \"1d\": 60,\n  \"7d\": 240,\n  \"14d\": 480,\n} satisfies Record<(typeof PERIODS)[number], number>;\n\nexport const periodToFromDate = {\n  \"1d\": startOfDay(subDays(new Date(), 1)),\n  \"7d\": startOfDay(subDays(new Date(), 7)),\n  \"14d\": startOfDay(subDays(new Date(), 14)),\n} satisfies Record<(typeof PERIODS)[number], Date>;\n"
  },
  {
    "path": "apps/dashboard/src/data/monitor-tags.ts",
    "content": "export const monitorTags = [\n  {\n    value: \"production\",\n    label: \"Production\",\n    color: \"bg-green-500\",\n  },\n  {\n    value: \"development\",\n    label: \"Development\",\n    color: \"bg-blue-500\",\n  },\n  {\n    value: \"staging\",\n    label: \"Staging\",\n    color: \"bg-yellow-500\",\n  },\n  {\n    value: \"testing\",\n    label: \"Testing\",\n    color: \"bg-purple-500\",\n  },\n  {\n    value: \"api\",\n    label: \"API\",\n    color: \"bg-red-500\",\n  },\n  {\n    value: \"database\",\n    label: \"Database\",\n    color: \"bg-orange-500\",\n  },\n];\n\nexport type MonitorTag = (typeof monitorTags)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/monitors.client.ts",
    "content": "import {\n  Cog,\n  Copy,\n  CopyPlus,\n  Globe,\n  Network,\n  Server,\n  Trash2,\n} from \"lucide-react\";\n\nexport const monitorTypes = [\n  {\n    id: \"http\",\n    label: \"HTTP\",\n    icon: Globe,\n  },\n  {\n    id: \"tcp\",\n    label: \"TCP\",\n    icon: Network,\n  },\n  {\n    id: \"dns\",\n    label: \"DNS\",\n    icon: Server,\n  },\n] as const;\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Settings\",\n    icon: Cog,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"copy-id\",\n    label: \"Copy ID\",\n    icon: Copy,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"clone\",\n    label: \"Clone\",\n    icon: CopyPlus,\n    variant: \"default\" as const,\n  },\n  // {\n  //   id: \"export\",\n  //   label: \"Export Code\",\n  //   icon: Code,\n  //   variant: \"default\" as const,\n  // },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type MonitorAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<MonitorAction[\"id\"], () => Promise<void> | void>>,\n): (MonitorAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/monitors.ts",
    "content": "export const monitors = [\n  {\n    id: 1,\n    name: \"OpenStatus Marketing\",\n    description: \"Marketing website for OpenStatus\",\n    public: false,\n    active: true,\n    status: \"Normal\" as const,\n    url: \"https://openstatus.dev\",\n    tags: [\"Production\"],\n    lastIncident: undefined,\n    p50: 110,\n    p90: 200,\n    p99: 250,\n  },\n  {\n    id: 2,\n    name: \"OpenStatus API\",\n    description: \"API for OpenStatus\",\n    public: false,\n    active: true,\n    status: \"Normal\" as const,\n    url: \"https://api.openstatus.dev/v1/ping\",\n    tags: [\"Production\", \"API\"],\n    lastIncident: undefined,\n    p50: 34,\n    p90: 201,\n    p99: 530,\n  },\n  {\n    id: 3,\n    name: \"OpenStatus App\",\n    description: \"Dashboard for OpenStatus\",\n    public: false,\n    active: true,\n    status: \"Failing\" as const,\n    url: \"https://openstatus.dev/app\",\n    tags: [\"Production\"],\n    lastIncident: \"10 minutes ago\",\n    p50: 130,\n    p90: 200,\n    p99: 250,\n  },\n  {\n    id: 4,\n    name: \"Lightweight OS\",\n    description: \"Lightweight Operations System\",\n    public: false,\n    active: false,\n    status: \"Inactive\" as const,\n    url: \"https://data-table.openstatus.dev/light\",\n    tags: [\"Development\"],\n    lastIncident: undefined,\n    p50: undefined,\n    p90: undefined,\n    p99: undefined,\n  },\n  {\n    id: 5,\n    name: \"Astro Status Page\",\n    description: \"Status page for Astro\",\n    public: false,\n    active: true,\n    status: \"Degraded\" as const,\n    url: \"https://status.openstat.us\",\n    tags: [\"Development\"],\n    lastIncident: undefined,\n    p50: 130,\n    p90: 201,\n    p99: 250,\n  },\n  {\n    id: 6,\n    name: \"Vercel Edge Ping\",\n    description: \"Ping for Vercel Edge\",\n    public: false,\n    active: true,\n    status: \"Normal\" as const,\n    url: \"https://light.openstatus.dev\",\n    tags: [\"Staging\"],\n    lastIncident: \"15 days ago\",\n    p50: 30,\n    p90: 240,\n    p99: 400,\n  },\n];\n\nexport type Monitor = (typeof monitors)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/notifications.client.ts",
    "content": "import { FormDiscord } from \"@/components/forms/notifications/form-discord\";\nimport { FormEmail } from \"@/components/forms/notifications/form-email\";\nimport { FormGoogleChat } from \"@/components/forms/notifications/form-google-chat\";\nimport { FormGrafanaOncall } from \"@/components/forms/notifications/form-grafana-oncall\";\nimport { FormNtfy } from \"@/components/forms/notifications/form-ntfy\";\nimport { FormOpsGenie } from \"@/components/forms/notifications/form-opsgenie\";\nimport { FormPagerDuty } from \"@/components/forms/notifications/form-pagerduty\";\nimport { FormSlack } from \"@/components/forms/notifications/form-slack\";\nimport { FormSms } from \"@/components/forms/notifications/form-sms\";\nimport { FormTelegram } from \"@/components/forms/notifications/form-telegram\";\nimport { FormWebhook } from \"@/components/forms/notifications/form-webhook\";\nimport { FormWhatsApp } from \"@/components/forms/notifications/form-whatsapp\";\nimport {\n  DiscordIcon,\n  GoogleIcon,\n  GrafanaIcon,\n  TelegramIcon,\n  WhatsappIcon,\n} from \"@openstatus/icons\";\nimport { OpsGenieIcon } from \"@openstatus/icons\";\nimport { PagerDutyIcon } from \"@openstatus/icons\";\nimport { SlackIcon } from \"@openstatus/icons\";\nimport { sendTestDiscordMessage as sendTestDiscord } from \"@openstatus/notification-discord\";\nimport { sendTest as sendTestGrafanaOncall } from \"@openstatus/notification-grafana-oncall\";\nimport { sendTest as sendTestNtfy } from \"@openstatus/notification-ntfy\";\nimport { sendTest as sendTestOpsGenie } from \"@openstatus/notification-opsgenie\";\nimport { sendTest as sendTestPagerDuty } from \"@openstatus/notification-pagerduty\";\nimport { sendTestSlackMessage as sendTestSlack } from \"@openstatus/notification-slack\";\nimport { sendTest as sendTestTelegram } from \"@openstatus/notification-telegram\";\nimport { sendTest as sendWhatsAppTest } from \"@openstatus/notification-twillio-whatsapp\";\nimport { sendTest as sendTestWebhook } from \"@openstatus/notification-webhook\";\nimport {\n  BellIcon,\n  Cog,\n  Mail,\n  MessageCircle,\n  Trash2,\n  Webhook,\n} from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Settings\",\n    icon: Cog,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type NotifierAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<NotifierAction[\"id\"], () => Promise<void> | void>>,\n): (NotifierAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n\n// List of the notifiers\n\nexport const config = {\n  slack: {\n    icon: SlackIcon,\n    label: \"Slack\",\n    form: FormSlack,\n    sendTest: sendTestSlack,\n  },\n  discord: {\n    icon: DiscordIcon,\n    label: \"Discord\",\n    form: FormDiscord,\n    sendTest: sendTestDiscord,\n  },\n  email: {\n    icon: Mail,\n    label: \"Email\",\n    form: FormEmail,\n    // TODO: add sendTest\n    sendTest: undefined,\n  },\n  sms: {\n    icon: MessageCircle,\n    label: \"SMS\",\n    form: FormSms,\n    // TODO: add sendTest\n    sendTest: undefined,\n  },\n  webhook: {\n    icon: Webhook,\n    label: \"Webhook\",\n    form: FormWebhook,\n    sendTest: sendTestWebhook,\n  },\n  opsgenie: {\n    icon: OpsGenieIcon,\n    label: \"OpsGenie\",\n    form: FormOpsGenie,\n    sendTest: sendTestOpsGenie,\n  },\n  \"google-chat\": {\n    icon: GoogleIcon,\n    label: \"Google Chat\",\n    form: FormGoogleChat,\n    sendTest: sendTestWebhook,\n  },\n  \"grafana-oncall\": {\n    icon: GrafanaIcon,\n    label: \"Grafana OnCall\",\n    form: FormGrafanaOncall,\n    sendTest: sendTestGrafanaOncall,\n  },\n  pagerduty: {\n    icon: PagerDutyIcon,\n    label: \"PagerDuty\",\n    form: FormPagerDuty,\n    sendTest: sendTestPagerDuty,\n  },\n  ntfy: {\n    icon: BellIcon, // TODO: add svg icon\n    label: \"Ntfy\",\n    form: FormNtfy,\n    sendTest: sendTestNtfy,\n  },\n  telegram: {\n    icon: TelegramIcon,\n    label: \"Telegram\",\n    form: FormTelegram,\n    sendTest: sendTestTelegram,\n  },\n  whatsapp: {\n    icon: WhatsappIcon,\n    label: \"WhatsApp\",\n    form: FormWhatsApp,\n    sendTest: sendWhatsAppTest,\n  },\n};\n\nexport type NotifierProvider = keyof typeof config;\n"
  },
  {
    "path": "apps/dashboard/src/data/notifications.ts",
    "content": "export const notifications = [\n  {\n    id: 1,\n    name: \"Email\",\n    provider: \"email\",\n    value: \"max@openstatus.dev\",\n  },\n];\n\nexport type Notification = (typeof notifications)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/page-components.client.ts",
    "content": "import { Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type PageComponentAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<PageComponentAction[\"id\"], () => Promise<void> | void>>,\n): (PageComponentAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/plans.ts",
    "content": "import { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport type React from \"react\";\n\nexport const plans = allPlans;\n\nexport const config: Record<\n  string,\n  {\n    label: string;\n    features: {\n      value: keyof Limits;\n      label: string;\n      description?: React.ReactNode; // tooltip informations\n      badge?: string;\n      monthly?: boolean;\n    }[];\n  }\n> = {\n  \"status-pages\": {\n    label: \"Status Pages\",\n    features: [\n      {\n        value: \"status-pages\",\n        label: \"Number of status pages\",\n      },\n      {\n        value: \"page-components\",\n        label: \"Number of components\",\n      },\n      {\n        value: \"maintenance\",\n        label: \"Maintenance status\",\n      },\n      {\n        value: \"slack-agent\",\n        label: \"Slack Agent\",\n      },\n      {\n        value: \"monitor-values-visibility\",\n        label: \"Toggle numbers visibility\",\n      },\n      {\n        value: \"status-subscribers\",\n        label: \"Subscribers\",\n      },\n      {\n        value: \"custom-domain\",\n        label: \"Custom domain\",\n      },\n      {\n        value: \"white-label\",\n        label: \"White Label\",\n      },\n    ],\n  },\n  \"status-page-audience\": {\n    label: \"Status Page Audience\",\n    features: [\n      {\n        value: \"password-protection\",\n        label: \"Password Protection (Basic)\",\n      },\n      {\n        value: \"email-domain-protection\",\n        label: \"Magic Link (Auth)\",\n      },\n    ],\n  },\n  monitors: {\n    label: \"Monitors\",\n    features: [\n      {\n        value: \"periodicity\",\n        label: \"Frequency\",\n      },\n      {\n        value: \"monitors\",\n        label: \"Number of monitors\",\n      },\n      {\n        value: \"multi-region\",\n        label: \"Multi-region monitoring\",\n      },\n      { value: \"regions\", label: \"Total regions\" },\n      { value: \"max-regions\", label: \"Regions per monitor\" },\n      { value: \"data-retention\", label: \"Data retention\" },\n      { value: \"response-logs\", label: \"Response Logs\" },\n      { value: \"otel\", label: \"OTel Exporter\" },\n      {\n        value: \"synthetic-checks\",\n        label: \"Synthetic API Checks\",\n        monthly: true,\n      },\n    ],\n  },\n  notifications: {\n    label: \"Notifications\",\n    features: [\n      {\n        value: \"notifications\",\n        label: \"Slack, Discord, Email, Webhook, ntfy.sh\",\n      },\n      {\n        value: \"sms\",\n        label: \"SMS\",\n      },\n      {\n        value: \"pagerduty\",\n        label: \"PagerDuty\",\n      },\n      {\n        value: \"opsgenie\",\n        label: \"OpsGenie\",\n      },\n      {\n        value: \"grafana-oncall\",\n        label: \"Grafana OnCall\",\n      },\n      {\n        value: \"whatsapp\",\n        label: \"WhatsApp\",\n      },\n      {\n        value: \"notification-channels\",\n        label: \"Number of notification channels\",\n      },\n    ],\n  },\n  collaboration: {\n    label: \"Collaboration\",\n    features: [\n      {\n        value: \"members\",\n        label: \"Team members\",\n      },\n      {\n        value: \"audit-log\",\n        label: \"Audit log\",\n        badge: \"Planned\",\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/region-metrics.client.ts",
    "content": "import { Filter } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"filter\",\n    label: \"Filter\",\n    icon: Filter,\n    variant: \"default\" as const,\n  },\n  // {\n  //   id: \"trigger\",\n  //   label: \"Trigger\",\n  //   icon: Zap,\n  //   variant: \"default\" as const,\n  // },\n] as const;\n\nexport type RegionMetricAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<RegionMetricAction[\"id\"], () => Promise<void> | void>>,\n): (RegionMetricAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/region-metrics.ts",
    "content": "export const regionMetrics = [\n  {\n    region: \"ams\",\n    p50: 100,\n    p90: 150,\n    p99: 200,\n    trend: [{ ams: 100, timestamp: 1716729600, latency: 100 }] as {\n      [key: string]: number;\n      timestamp: number;\n      latency: number;\n    }[],\n  },\n  {\n    region: \"fra\",\n    p50: 110,\n    p90: 155,\n    p99: 220,\n    trend: [{ fra: 100, timestamp: 1716729600, latency: 100 }] as {\n      [key: string]: number;\n      timestamp: number;\n      latency: number;\n    }[],\n  },\n  {\n    region: \"gru\",\n    p50: 120,\n    p90: 160,\n    p99: 230,\n    trend: [{ gru: 100, timestamp: 1716729600, latency: 100 }] as {\n      [key: string]: number;\n      timestamp: number;\n      latency: number;\n    }[],\n  },\n];\n\nexport type RegionMetric = (typeof regionMetrics)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/regions.ts",
    "content": "export const regions = [\n  {\n    code: \"ams\",\n    location: \"Amsterdam, Netherlands\",\n    flag: \"🇳🇱\",\n    continent: \"Europe\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"arn\",\n    location: \"Stockholm, Sweden\",\n    flag: \"🇸🇪\",\n    continent: \"Europe\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"atl\",\n    location: \"Atlanta, Georgia, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"bog\",\n    location: \"Bogotá, Colombia\",\n    flag: \"🇨🇴\",\n    continent: \"South America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"bom\",\n    location: \"Mumbai, India\",\n    flag: \"🇮🇳\",\n    continent: \"Asia\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"bos\",\n    location: \"Boston, Massachusetts, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"cdg\",\n    location: \"Paris, France\",\n    flag: \"🇫🇷\",\n    continent: \"Europe\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"den\",\n    location: \"Denver, Colorado, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"dfw\",\n    location: \"Dallas, Texas, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"ewr\",\n    location: \"Secaucus, New Jersey, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"eze\",\n    location: \"Ezeiza, Argentina\",\n    flag: \"🇦🇷\",\n    continent: \"South America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"fra\",\n    location: \"Frankfurt, Germany\",\n    flag: \"🇩🇪\",\n    continent: \"Europe\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"gdl\",\n    location: \"Guadalajara, Mexico\",\n    flag: \"🇲🇽\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"gig\",\n    location: \"Rio de Janeiro, Brazil\",\n    flag: \"🇧🇷\",\n    continent: \"South America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"gru\",\n    location: \"Sao Paulo, Brazil\",\n    flag: \"🇧🇷\",\n    continent: \"South America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"hkg\",\n    location: \"Hong Kong, Hong Kong\",\n    flag: \"🇭🇰\",\n    continent: \"Asia\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"iad\",\n    location: \"Ashburn, Virginia, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"jnb\",\n    location: \"Johannesburg, South Africa\",\n    flag: \"🇿🇦\",\n    continent: \"Africa\",\n  },\n  {\n    code: \"lax\",\n    location: \"Los Angeles, California, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"lhr\",\n    location: \"London, United Kingdom\",\n    flag: \"🇬🇧\",\n    continent: \"Europe\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"mad\",\n    location: \"Madrid, Spain\",\n    flag: \"🇪🇸\",\n    continent: \"Europe\",\n  },\n  {\n    code: \"mia\",\n    location: \"Miami, Florida, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"nrt\",\n    location: \"Tokyo, Japan\",\n    flag: \"🇯🇵\",\n    continent: \"Asia\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"ord\",\n    location: \"Chicago, Illinois, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"otp\",\n    location: \"Bucharest, Romania\",\n    flag: \"🇷🇴\",\n    continent: \"Europe\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"phx\",\n    location: \"Phoenix, Arizona, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"qro\",\n    location: \"Querétaro, Mexico\",\n    flag: \"🇲🇽\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"scl\",\n    location: \"Santiago, Chile\",\n    flag: \"🇨🇱\",\n    continent: \"South America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"sjc\",\n    location: \"San Jose, California, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"sea\",\n    location: \"Seattle, Washington, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"sin\",\n    location: \"Singapore, Singapore\",\n    flag: \"🇸🇬\",\n    continent: \"Asia\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"syd\",\n    location: \"Sydney, Australia\",\n    flag: \"🇦🇺\",\n    continent: \"Oceania\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"waw\",\n    location: \"Warsaw, Poland\",\n    flag: \"🇵🇱\",\n    continent: \"Europe\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"yul\",\n    location: \"Montreal, Canada\",\n    flag: \"🇨🇦\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"yyz\",\n    location: \"Toronto, Canada\",\n    flag: \"🇨🇦\",\n    continent: \"North America\",\n    provider: \"Fly\",\n  },\n  {\n    code: \"koyeb_fra\",\n    location: \"Frankfurt, Germany\",\n    flag: \"🇩🇪\",\n    continent: \"Europe\",\n    provider: \"koyeb\",\n  },\n  {\n    code: \"koyeb_par\",\n    location: \"Paris, France\",\n    flag: \"🇫🇷\",\n    continent: \"Europe\",\n    provider: \"koyeb\",\n  },\n  {\n    code: \"koyeb_sfo\",\n    location: \"San Francisco, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"koyeb\",\n  },\n  {\n    code: \"koyeb_sin\",\n    location: \"Singapore, Singapore\",\n    flag: \"🇸🇬\",\n    continent: \"Asia\",\n    provider: \"koyeb\",\n  },\n  {\n    code: \"koyeb_tyo\",\n    location: \"Tokyo, Japan\",\n    flag: \"🇯🇵\",\n    continent: \"Asia\",\n    provider: \"koyeb\",\n  },\n  {\n    code: \"koyeb_was\",\n    location: \"Washington, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"koyeb\",\n  },\n  {\n    code: \"railway_us-west2\",\n    location: \"California, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"railway\",\n  },\n  {\n    code: \"railway_us-east4-eqdc4a\",\n    location: \"Virginia, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    provider: \"railway\",\n  },\n  {\n    code: \"railway_europe-west4-drams3a\",\n    location: \"Amsterdam, Netherlands\",\n    flag: \"🇳🇱\",\n    continent: \"Europe\",\n    provider: \"railway\",\n  },\n  {\n    code: \"railway_asia-southeast1-eqsg3a\",\n    location: \"Singapore, Singapore\",\n    flag: \"🇸🇬\",\n    continent: \"Asia\",\n    provider: \"railway\",\n  },\n] as const;\n\nexport type Region = (typeof regions)[number][\"code\"];\n\nexport const groupedRegions = regions.reduce(\n  (acc, region) => {\n    const continent = region.continent;\n    if (!acc[continent]) {\n      acc[continent] = [];\n    }\n    acc[continent].push(region.code);\n    return acc;\n  },\n  {} as Record<string, Region[]>,\n);\n\nexport const regionColors = {\n  ams: \"hsl(217.2 91.2% 59.8%)\",\n  arn: \"hsl(238.7 83.5% 66.7%)\",\n  atl: \"hsl(258.3 89.5% 66.3%)\",\n  bog: \"hsl(270.7 91% 65.1%)\",\n  bom: \"hsl(292.2 84.1% 60.6%)\",\n  bos: \"hsl(330.4 81.2% 60.4%)\",\n  cdg: \"hsl(349.7 89.2% 60.2%)\",\n  den: \"hsl(215.4 16.3% 46.9%)\",\n  dfw: \"hsl(220 8.9% 46.1%)\",\n  ewr: \"hsl(240 3.8% 46.1%)\",\n  eze: \"hsl(0 0% 45.1%)\",\n  fra: \"hsl(25 5.3% 44.7%)\",\n  gdl: \"hsl(0 84.2% 60.2%)\",\n  gig: \"hsl(24.6 95% 53.1%)\",\n  gru: \"hsl(37.7 92.1% 50.2%)\",\n  hkg: \"hsl(45.4 93.4% 47.5%)\",\n  iad: \"hsl(83.7 80.5% 44.3%)\",\n  jnb: \"hsl(142.1 70.6% 45.3%)\",\n  lax: \"hsl(160.1 84.1% 39.4%)\",\n  lhr: \"hsl(173.4 80.4% 40%)\",\n  mad: \"hsl(188.7 94.5% 42.7%)\",\n  mia: \"hsl(198.6 88.7% 48.4%)\",\n  nrt: \"hsl(217.2 91.2% 59.8%)\",\n  ord: \"hsl(238.7 83.5% 66.7%)\",\n  otp: \"hsl(258.3 89.5% 66.3%)\",\n  phx: \"hsl(270.7 91% 65.1%)\",\n  qro: \"hsl(292.2 84.1% 60.6%)\",\n  scl: \"hsl(330.4 81.2% 60.4%)\",\n  sjc: \"hsl(349.7 89.2% 60.2%)\",\n  sea: \"hsl(215.4 16.3% 46.9%)\",\n  sin: \"hsl(220 8.9% 46.1%)\",\n  syd: \"hsl(240 3.8% 46.1%)\",\n  waw: \"hsl(0 0% 45.1%)\",\n  yul: \"hsl(25 5.3% 44.7%)\",\n  yyz: \"hsl(0 84.2% 60.2%)\",\n\n  koyeb_fra: \"hsl(25 5.3% 44.7%)\",\n  koyeb_par: \"hsl(25 5.3% 44.7%)\",\n  koyeb_sin: \"hsl(25 5.3% 44.7%)\",\n  koyeb_sfo: \"hsl(0 0% 45.1%)\",\n  koyeb_tyo: \"hsl(0 0% 45.1%)\",\n  koyeb_was: \"hsl(0 0% 45.1%)\",\n\n  \"railway_asia-southeast1-eqsg3a\": \"hsl(0 0% 45.1%)\",\n  \"railway_europe-west4-drams3a\": \"hsl(0 0% 45.1%)\",\n  \"railway_us-east4-eqdc4a\": \"hsl(0 0% 45.1%)\",\n  \"railway_us-west2\": \"hsl(0 0% 45.1%)\",\n} satisfies Record<Region, string>;\n\nexport function getRegionColor(region: string) {\n  if (region in regionColors) {\n    return regionColors[region as keyof typeof regionColors];\n  }\n  return \"hsl(0 0% 45.1%)\";\n}\n"
  },
  {
    "path": "apps/dashboard/src/data/response-logs.ts",
    "content": "import type { RouterOutputs } from \"@openstatus/api\";\nimport { monitorRegions } from \"@openstatus/db/src/schema/constants\";\nimport { startOfDay } from \"date-fns\";\n\ntype ResponseLog = RouterOutputs[\"tinybird\"][\"list\"][\"data\"][number];\n\nconst today = startOfDay(new Date());\n\nexport const exampleLogs: ResponseLog[] = Array.from({ length: 10 }).map(\n  (_, i) => ({\n    id: i.toString(),\n    type: \"http\",\n    url: \"https://api.openstatus.dev\",\n    method: \"GET\",\n    statusCode: 200,\n    requestStatus: \"success\" as const,\n    latency: 150,\n    timing: {\n      dns: 10,\n      connect: 20,\n      tls: 30,\n      ttfb: 40,\n      transfer: 50,\n    },\n    assertions: [],\n    region: monitorRegions[i],\n    error: false,\n    timestamp: today.getTime() + i * 1000 * 60,\n    headers: {\n      \"Cache-Control\":\n        \"private, no-cache, no-store, max-age=0, must-revalidate\",\n      \"Content-Type\": \"text/html; charset=utf-8\",\n      Date: \"Sun, 28 Jan 2024 08:50:13 GMT\",\n      Server: \"Vercel\",\n    },\n    workspaceId: \"1\",\n    monitorId: \"1\",\n    cronTimestamp: today.getTime() + i * 1000 * 60,\n    trigger: \"cron\" as const satisfies \"cron\" | \"api\",\n  }),\n);\n"
  },
  {
    "path": "apps/dashboard/src/data/status-codes.ts",
    "content": "export const statusCodes = [\n  {\n    code: 200 as const,\n    bg: \"bg-success\",\n    text: \"text-success\",\n    name: \"OK\",\n  },\n  {\n    code: 500 as const,\n    bg: \"bg-destructive\",\n    text: \"text-destructive\",\n    name: \"Internal Server Error\",\n  },\n];\n\nexport type StatusCode = (typeof statusCodes)[number][\"code\"];\n\nexport const getStatusCodeVariant = (code?: number | null) => {\n  if (!code) return \"muted\";\n  if (code.toString().startsWith(\"2\")) return \"success\";\n  if (code.toString().startsWith(\"3\")) return \"info\";\n  if (code.toString().startsWith(\"4\")) return \"warning\";\n  if (code.toString().startsWith(\"5\")) return \"destructive\";\n  return \"muted\";\n};\n\nexport const bgColors = {\n  success: \"bg-success\",\n  info: \"bg-info\",\n  warning: \"bg-warning\",\n  destructive: \"bg-destructive\",\n  muted: \"bg-muted\",\n};\n\nexport const textColors = {\n  success: \"text-success\",\n  info: \"text-info\",\n  warning: \"text-warning\",\n  destructive: \"text-destructive\",\n  muted: \"text-muted-foreground\",\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/status-pages.client.ts",
    "content": "import { Cog, Copy, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Settings\",\n    icon: Cog,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"copy-id\",\n    label: \"Copy ID\",\n    icon: Copy,\n    variant: \"default\" as const,\n  },\n  // {\n  //   id: \"create-badge\",\n  //   label: \"Create Badge\",\n  //   icon: Tag,\n  //   variant: \"default\" as const,\n  // },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type StatusPageAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<StatusPageAction[\"id\"], () => Promise<void> | void>>,\n): (StatusPageAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/status-pages.ts",
    "content": "export const statusPages = [\n  {\n    id: 1,\n    name: \"OpenStatus Status\",\n    description: \"See our uptime history and status reports.\",\n    slug: \"status\",\n    favicon: \"https://openstatus.dev/favicon.ico\",\n    domain: \"status.openstatus.dev\",\n    protected: true,\n    showValues: false,\n    // NOTE: the worst status of a report\n    status: \"degraded\" as const,\n    monitors: [],\n  },\n];\n\nexport type StatusPage = (typeof statusPages)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/status-report-updates.client.ts",
    "content": "import type { StatusReportStatus } from \"@openstatus/db/src/schema\";\nimport { Cog, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Settings\",\n    icon: Cog,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type StatusReportUpdateAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<\n    Record<StatusReportUpdateAction[\"id\"], () => Promise<void> | void>\n  >,\n): (StatusReportUpdateAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n\nexport const colors = {\n  resolved:\n    \"text-success/80 data-[state=selected]:bg-success/10 data-[state=selected]:text-success\",\n  investigating:\n    \"text-destructive/80 data-[state=selected]:bg-destructive/10 data-[state=selected]:text-destructive\",\n  monitoring:\n    \"text-info/80 data-[state=selected]:bg-info/10 data-[state=selected]:text-info\",\n  identified:\n    \"text-warning/80 data-[state=selected]:bg-warning/10 data-[state=selected]:text-warning\",\n} as const satisfies Record<StatusReportStatus, string>;\n\n/**\n * Get the next status in the progression:\n * investigating → identified → monitoring → resolved\n *\n * @param currentStatus - The current status\n * @returns The next status in the progression, or 'resolved' if already at the end, or 'investigating' for invalid statuses\n */\nexport function getNextStatus(currentStatus: string): StatusReportStatus {\n  const statusProgression: Record<StatusReportStatus, StatusReportStatus> = {\n    investigating: \"identified\",\n    identified: \"monitoring\",\n    monitoring: \"resolved\",\n    resolved: \"resolved\",\n  };\n\n  return (\n    statusProgression[currentStatus as StatusReportStatus] ?? \"investigating\"\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/data/status-reports.client.ts",
    "content": "import { Cog, Eye, Plus, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Settings\",\n    icon: Cog,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"create-update\",\n    label: \"Create Update\",\n    icon: Plus,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"view-report\",\n    label: \"View Report\",\n    icon: Eye,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type StatusReportUpdateAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<\n    Record<StatusReportUpdateAction[\"id\"], () => Promise<void> | void>\n  >,\n): (StatusReportUpdateAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/dashboard/src/data/status-reports.ts",
    "content": "export const statusReports = [\n  {\n    id: 1,\n    name: \"Downtime API\",\n    startedAt: new Date(\"2025-06-07 12:00:00\"),\n    updatedAt: new Date(\"2025-06-07 12:30:00\"),\n    status: \"operational\",\n    updates: [\n      {\n        id: 2,\n        status: \"operational\" as const,\n        message:\n          \"Everything is under control, we continue to monitor the situation.\",\n        date: new Date(\"2025-06-07 12:30:00\"),\n        updatedAt: new Date(\"2025-06-07 12:30:00\"),\n        monitors: [1],\n      },\n      {\n        id: 1,\n        status: \"investigating\" as const,\n        message:\n          \"Our hosting provider is having an increase of 400 errors. We are aware of the dependency and will be working on a solution to reduce the risk.\",\n        date: new Date(\"2025-06-07 12:00:00\"),\n        updatedAt: new Date(\"2025-06-07 12:00:00\"),\n        monitors: [1],\n      },\n    ],\n    affected: [\"OpenStatus API\"],\n  },\n  {\n    id: 2,\n    name: \"Downtime API\",\n    startedAt: new Date(\"2025-06-04 12:10:00\"),\n    updatedAt: new Date(\"2025-06-04 12:30:00\"),\n    status: \"operational\",\n    updates: [\n      {\n        id: 2,\n        status: \"operational\" as const,\n        message:\n          \"Everything is under control, we continue to monitor the situation.\",\n        date: new Date(\"2025-06-04 12:30:00\"),\n        updatedAt: new Date(\"2025-06-04 12:30:00\"),\n        monitors: [1],\n      },\n      {\n        id: 1,\n        status: \"investigating\" as const,\n        message:\n          \"Our hosting provider is having an increase of 400 errors. We are working on a solution to reduce the risk.\",\n        date: new Date(\"2025-06-04 12:00:00\"),\n        updatedAt: new Date(\"2025-06-04 12:00:00\"),\n        monitors: [1],\n      },\n    ],\n    affected: [\"OpenStatus API\"],\n  },\n];\n\nexport type StatusReport = (typeof statusReports)[number];\n"
  },
  {
    "path": "apps/dashboard/src/data/subscribers.ts",
    "content": "export const subscribers = [\n  {\n    id: \"1\",\n    email: \"max@openstatus.dev\",\n    createdAt: \"2025-05-20\",\n    validatedAt: \"2025-05-20\",\n  },\n  {\n    id: \"2\",\n    email: \"thibault@openstatus.dev\",\n    createdAt: \"2025-05-20\",\n    validatedAt: \"2025-05-20\",\n  },\n];\n\nexport type Subscriber = (typeof subscribers)[number];\n"
  },
  {
    "path": "apps/dashboard/src/hooks/use-feature.ts",
    "content": "import { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\n\n/**\n * Record<feature, [workspaceId, ...]>\n */\nconst features = {\n  \"slack-agent\": [1, 6850],\n};\n\nexport function useFeature(feature: keyof typeof features) {\n  const trpc = useTRPC();\n  const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());\n\n  if (!workspace) return false;\n\n  return features[feature]?.includes(workspace.id) ?? false;\n}\n"
  },
  {
    "path": "apps/dashboard/src/hooks/use-telegram-connection.ts",
    "content": "\"use client\";\n\nimport type { FormValues } from \"@/components/forms/notifications/form-telegram\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport React, { useReducer, useTransition } from \"react\";\nimport type { UseFormReturn } from \"react-hook-form\";\nimport { toast } from \"sonner\";\n\ninterface UseTelegramConnectionProps {\n  form: UseFormReturn<FormValues>;\n  mode: \"qr\" | \"manual\" | null;\n}\n\ninterface TelegramConnectionState {\n  flowStep: \"private\" | \"group\";\n  privateChatId: string | null;\n  userName: string | null;\n  groupTitle: string | null;\n  sessionStartTime: number | null;\n}\n\ntype TelegramConnectionAction =\n  | { type: \"SET_SESSION_START_TIME\"; payload: number | null }\n  | { type: \"RESET_STATE\" }\n  | { type: \"RESET_GROUP_CONNECTION\" }\n  | {\n      type: \"SET_PRIVATE_CONNECTION_DATA\";\n      payload: {\n        privateChatId: string;\n        userName: string;\n      };\n    }\n  | {\n      type: \"SET_GROUP_CONNECTION_DATA\";\n      payload: {\n        groupTitle: string;\n        chatId: string;\n      };\n    };\n\nconst initialState: TelegramConnectionState = {\n  flowStep: \"private\",\n  privateChatId: null,\n  userName: null,\n  groupTitle: null,\n  sessionStartTime: null,\n};\n\nfunction telegramConnectionReducer(\n  state: TelegramConnectionState,\n  action: TelegramConnectionAction,\n): TelegramConnectionState {\n  switch (action.type) {\n    case \"SET_SESSION_START_TIME\":\n      return { ...state, sessionStartTime: action.payload };\n    case \"RESET_STATE\":\n      return initialState;\n    case \"RESET_GROUP_CONNECTION\":\n      return {\n        ...state,\n        groupTitle: null,\n        sessionStartTime: Math.floor(Date.now() / 1000),\n        flowStep: state.privateChatId ? \"group\" : \"private\",\n      };\n    case \"SET_PRIVATE_CONNECTION_DATA\":\n      return {\n        ...state,\n        privateChatId: action.payload.privateChatId,\n        userName: action.payload.userName,\n        flowStep: \"group\",\n      };\n    case \"SET_GROUP_CONNECTION_DATA\":\n      return {\n        ...state,\n        groupTitle: action.payload.groupTitle,\n      };\n    default:\n      return state;\n  }\n}\n\nexport function useTelegramConnection({\n  form,\n  mode,\n}: UseTelegramConnectionProps) {\n  const [isPending, startTransition] = useTransition();\n  const trpc = useTRPC();\n  const [state, dispatch] = useReducer(telegramConnectionReducer, initialState);\n\n  // Create Telegram Token\n  const { data: tokenData, isLoading: isTokenLoading } = useQuery({\n    ...trpc.notification.createTelegramToken.queryOptions(),\n    refetchOnWindowFocus: false,\n  });\n\n  // Set session start time when entering QR mode\n  React.useEffect(() => {\n    if (mode === \"qr\") {\n      dispatch({\n        type: \"SET_SESSION_START_TIME\",\n        payload: Math.floor(Date.now() / 1000),\n      });\n    } else if (mode === null) {\n      dispatch({ type: \"SET_SESSION_START_TIME\", payload: null });\n    }\n  }, [mode]);\n\n  // Cleanup: Reset UI state when component unmounts (e.g., on discard)\n  React.useEffect(() => {\n    return () => {\n      // This runs when component unmounts\n      dispatch({ type: \"RESET_STATE\" });\n    };\n  }, []);\n\n  // Start polling for updates\n  const { data: updates } = useQuery({\n    ...trpc.notification.getTelegramUpdates.queryOptions({\n      privateChatId:\n        state.flowStep === \"group\"\n          ? state.privateChatId ?? undefined\n          : undefined,\n      since: state.sessionStartTime ?? undefined,\n    }),\n    enabled:\n      !!tokenData?.token && !form.getValues(\"data.chatId\") && mode === \"qr\",\n    refetchInterval: 5000,\n  });\n\n  React.useEffect(() => {\n    if (updates && updates.length > 0) {\n      const lastUpdate = updates[updates.length - 1];\n\n      // Phase 1: Private chat ID received\n      if (lastUpdate.chatType === \"private\" && state.flowStep === \"private\") {\n        dispatch({\n          type: \"SET_PRIVATE_CONNECTION_DATA\",\n          payload: {\n            privateChatId: lastUpdate.chatId,\n            userName: lastUpdate.user?.first_name || \"Unknown\",\n          },\n        });\n        toast.success(\n          `Connected to ${lastUpdate.user?.first_name || \"Unknown\"}'s account. Now add the bot to your group.`,\n        );\n      }\n      // Phase 2: Group chat ID received\n      else if (lastUpdate.chatType === \"group\" && state.flowStep === \"group\") {\n        dispatch({\n          type: \"SET_GROUP_CONNECTION_DATA\",\n          payload: {\n            groupTitle: lastUpdate.chatTitle || \"Unknown\",\n            chatId: lastUpdate.chatId,\n          },\n        });\n        startTransition(() => {\n          form.setValue(\"data.chatId\", lastUpdate.chatId, {\n            shouldDirty: true,\n          });\n          toast.success(\n            `Connected to group \"${lastUpdate.chatTitle || \"Unknown\"}\"`,\n          );\n        });\n      }\n    }\n  }, [updates, form, state.flowStep]);\n\n  const resetConnection = React.useCallback(() => {\n    form.setValue(\"data.chatId\", \"\", { shouldDirty: true });\n    dispatch({ type: \"RESET_GROUP_CONNECTION\" });\n  }, [form]);\n\n  const confirmPrivateChat = React.useCallback(() => {\n    if (state.privateChatId) {\n      startTransition(() => {\n        form.setValue(\"data.chatId\", state.privateChatId ?? \"\", {\n          shouldDirty: true,\n        });\n        toast.success(\n          `Connected to ${state.userName || \"Unknown\"}'s private chat`,\n        );\n      });\n    }\n  }, [form, state.privateChatId, state.userName]);\n\n  return {\n    tokenData,\n    isTokenLoading,\n    flowStep: state.flowStep,\n    privateChatId: state.privateChatId,\n    userName: state.userName,\n    groupTitle: state.groupTitle,\n    isPolling:\n      !!tokenData?.token && !form.watch(\"data.chatId\") && mode === \"qr\",\n    resetConnection,\n    confirmPrivateChat,\n    isPending,\n  };\n}\n"
  },
  {
    "path": "apps/dashboard/src/instrumentation.ts",
    "content": "import * as Sentry from \"@sentry/nextjs\";\n\nexport async function register() {\n  if (process.env.NEXT_RUNTIME === \"nodejs\") {\n    await import(\"../sentry.server.config\");\n  }\n\n  if (process.env.NEXT_RUNTIME === \"edge\") {\n    await import(\"../sentry.edge.config\");\n  }\n}\n\nexport const onRequestError = Sentry.captureRequestError;\n"
  },
  {
    "path": "apps/dashboard/src/lib/auth/adapter.ts",
    "content": "import { DrizzleAdapter } from \"@auth/drizzle-adapter\";\nimport type { Adapter } from \"next-auth/adapters\";\n\nimport { db } from \"@openstatus/db\";\nimport {\n  account,\n  session,\n  user,\n  verificationToken,\n} from \"@openstatus/db/src/schema\";\n\nimport { createUser, getUser } from \"./helpers\";\n\nexport const adapter: Adapter = {\n  ...DrizzleAdapter(db, {\n    // @ts-expect-error: problem with type\n    usersTable: user,\n    // @ts-expect-error: problem with type\n    accountsTable: account,\n    // @ts-expect-error: problem with type\n    sessionsTable: session,\n    verificationTokensTable: verificationToken,\n  }),\n  createUser: async (data) => {\n    const user = await createUser(data);\n    return {\n      ...user,\n      id: user.id.toString(),\n      email: user.email || \"\",\n    };\n  },\n  getUser: async (id) => {\n    const user = await getUser(id);\n    if (!user) return null;\n    return {\n      ...user,\n      id: user.id.toString(),\n      email: user.email || \"\",\n    };\n  },\n};\n"
  },
  {
    "path": "apps/dashboard/src/lib/auth/helpers.ts",
    "content": "import type { AdapterUser } from \"next-auth/adapters\";\nimport * as randomWordSlugs from \"random-word-slugs\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { user, usersToWorkspaces, workspace } from \"@openstatus/db/src/schema\";\n\nexport async function createUser(data: AdapterUser) {\n  const newUser = await db\n    .insert(user)\n    .values({\n      email: data.email,\n      photoUrl: data.image,\n      name: data.name,\n      firstName: data.firstName,\n      lastName: data.lastName,\n    })\n    .returning()\n    .get();\n\n  let slug: string | undefined = undefined;\n\n  while (!slug) {\n    slug = randomWordSlugs.generateSlug(2);\n    const slugAlreadyExists = await db\n      .select()\n      .from(workspace)\n      .where(eq(workspace.slug, slug))\n      .get();\n\n    if (slugAlreadyExists) {\n      console.warn(`slug already exists: '${slug} - recreating new one'`);\n      slug = undefined;\n    }\n  }\n\n  const newWorkspace = await db\n    .insert(workspace)\n    .values({ slug, name: \"\" })\n    .returning({ id: workspace.id })\n    .get();\n\n  await db\n    .insert(usersToWorkspaces)\n    .values({\n      userId: newUser.id,\n      workspaceId: newWorkspace.id,\n      role: \"owner\",\n    })\n    .returning()\n    .get();\n\n  return newUser;\n}\n\nexport async function getUser(id: string) {\n  const _user = await db\n    .select()\n    .from(user)\n    .where(eq(user.id, Number(id)))\n    .get();\n\n  return _user || null;\n}\n"
  },
  {
    "path": "apps/dashboard/src/lib/auth/index.ts",
    "content": "import type { DefaultSession } from \"next-auth\";\nimport NextAuth from \"next-auth\";\n\nimport { Events, setupAnalytics } from \"@openstatus/analytics\";\nimport { db, eq } from \"@openstatus/db\";\nimport { user } from \"@openstatus/db/src/schema\";\n\nimport { WelcomeEmail, sendEmail } from \"@openstatus/emails\";\nimport { headers } from \"next/headers\";\nimport { adapter } from \"./adapter\";\nimport { GitHubProvider, GoogleProvider, ResendProvider } from \"./providers\";\n\nexport type { DefaultSession };\n\nexport const { handlers, signIn, signOut, auth } = NextAuth({\n  // debug: true,\n  adapter,\n  providers:\n    process.env.NODE_ENV === \"development\" || process.env.SELF_HOST === \"true\"\n      ? [GitHubProvider, GoogleProvider, ResendProvider]\n      : [GitHubProvider, GoogleProvider],\n  callbacks: {\n    async signIn(params) {\n      // We keep updating the user info when we loggin in\n\n      if (params.account?.provider === \"google\") {\n        if (!params.profile) return true;\n        if (Number.isNaN(Number(params.user.id))) return true;\n\n        await db\n          .update(user)\n          .set({\n            firstName: params.profile.given_name,\n            lastName: params.profile.family_name || \"\",\n            photoUrl: params.profile.picture,\n            // keep the name in sync\n            name: `${params.profile.given_name} ${\n              params.profile.family_name || \"\"\n            }`.trim(),\n            updatedAt: new Date(),\n          })\n          .where(eq(user.id, Number(params.user.id)))\n          .run();\n      }\n      if (params.account?.provider === \"github\") {\n        if (!params.profile) return true;\n        if (Number.isNaN(Number(params.user.id))) return true;\n\n        await db\n          .update(user)\n          .set({\n            name: params.profile.name,\n            photoUrl: String(params.profile.avatar_url),\n            updatedAt: new Date(),\n          })\n          .where(eq(user.id, Number(params.user.id)))\n          .run();\n      }\n\n      // REMINDER: only used in dev mode\n      if (params.account?.provider === \"resend\") {\n        if (Number.isNaN(Number(params.user.id))) return true;\n        await db\n          .update(user)\n          .set({ updatedAt: new Date() })\n          .where(eq(user.id, Number(params.user.id)))\n          .run();\n      }\n\n      return true;\n    },\n    async session(params) {\n      return params.session;\n    },\n  },\n  events: {\n    // That should probably done in the callback method instead\n    async createUser(params) {\n      if (!params.user.id || !params.user.email) {\n        throw new Error(\"User id & email is required\");\n      }\n\n      // this means the user has already been created with clerk\n      if (params.user.tenantId) return;\n\n      await sendEmail({\n        from: \"Thibault from OpenStatus <thibault@openstatus.dev>\",\n        subject: \"Welcome to OpenStatus.\",\n        to: [params.user.email],\n        react: WelcomeEmail(),\n      });\n\n      const analytics = await setupAnalytics({\n        userId: `usr_${params.user.id}`,\n        email: params.user.email,\n        location: (await headers()).get(\"x-forwarded-for\") ?? undefined,\n        userAgent: (await headers()).get(\"user-agent\") ?? undefined,\n      });\n\n      await analytics.track(Events.CreateUser);\n    },\n\n    async signIn(params) {\n      if (params.isNewUser) return;\n      if (!params.user.id || !params.user.email) return;\n\n      const analytics = await setupAnalytics({\n        userId: `usr_${params.user.id}`,\n        email: params.user.email,\n        location: (await headers()).get(\"x-forwarded-for\") ?? undefined,\n        userAgent: (await headers()).get(\"user-agent\") ?? undefined,\n      });\n\n      await analytics.track(Events.SignInUser);\n    },\n  },\n  pages: {\n    signIn: \"/login\",\n    newUser: \"/onboarding\",\n  },\n  // basePath: \"/api/auth\", // default is `/api/auth`\n  // secret: process.env.AUTH_SECRET, // default is `AUTH_SECRET`\n  debug: process.env.NODE_ENV === \"development\",\n});\n"
  },
  {
    "path": "apps/dashboard/src/lib/auth/providers.ts",
    "content": "import GitHub from \"next-auth/providers/github\";\nimport Google from \"next-auth/providers/google\";\nimport Resend from \"next-auth/providers/resend\";\n\nexport const GitHubProvider = GitHub({\n  allowDangerousEmailAccountLinking: true,\n});\n\nexport const GoogleProvider = Google({\n  allowDangerousEmailAccountLinking: true,\n  authorization: {\n    params: {\n      // See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest\n      prompt: \"select_account\",\n      // scope:\n      //   \"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email\",\n    },\n  },\n});\n\nexport const ResendProvider = Resend({\n  apiKey: undefined, // REMINDER: keep undefined to avoid sending emails\n  async sendVerificationRequest(params) {\n    console.log(\"\");\n    console.log(`>>> Magic Link: ${params.url}`);\n    console.log(\"\");\n  },\n});\n"
  },
  {
    "path": "apps/dashboard/src/lib/composition.ts",
    "content": "import * as React from \"react\";\n\n/**\n * A utility to compose multiple event handlers into a single event handler.\n * Run originalEventHandler first, then ourEventHandler unless prevented.\n */\nfunction composeEventHandlers<E>(\n  originalEventHandler?: (event: E) => void,\n  ourEventHandler?: (event: E) => void,\n  { checkForDefaultPrevented = true } = {},\n) {\n  return function handleEvent(event: E) {\n    originalEventHandler?.(event);\n\n    if (\n      checkForDefaultPrevented === false ||\n      !(event as unknown as Event).defaultPrevented\n    ) {\n      return ourEventHandler?.(event);\n    }\n  };\n}\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value.\n * This utility takes care of different types of refs: callback refs and RefObject(s).\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together.\n * Accepts callback refs and RefObject(s).\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs.\n * Accepts callback refs and RefObject(s).\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeEventHandlers, composeRefs, useComposedRefs };\n"
  },
  {
    "path": "apps/dashboard/src/lib/domains.ts",
    "content": "export const getSubdomain = (name: string, apexName: string) => {\n  if (name === apexName) return null;\n  return name.slice(0, name.length - apexName.length - 1);\n};\n\nexport const getApexDomain = (url: string) => {\n  let domain: string;\n  try {\n    domain = new URL(url).hostname;\n  } catch (e) {\n    console.error(e);\n    return \"\";\n  }\n  const parts = domain.split(\".\");\n  if (parts.length > 2) {\n    // if it's a subdomain (e.g. dub.vercel.app), return the last 2 parts\n    return parts.slice(-2).join(\".\");\n  }\n  // if it's a normal domain (e.g. dub.sh), we return the domain\n  return domain;\n};\n\nexport function extractDomain(url: string) {\n  // Use URL constructor to parse\n  try {\n    if (url.trim() === \"\") return \"\";\n\n    const hostname = new URL(url).hostname; // e.g. \"craft.mxkaske.dev\"\n\n    const parts = hostname.split(\".\"); // [\"craft\", \"mxkaske\", \"dev\"]\n\n    if (parts.length === 2) {\n      // no subdomain\n      return parts[0]; // \"mxkaske\"\n    }\n    if (parts.length > 2) {\n      // has subdomain(s)\n      return `${parts.slice(0, -2).join(\"-\")}-${parts[parts.length - 2]}`;\n      // \"craft-mxkaske\"\n    }\n    return \"\";\n  } catch (e) {\n    console.error(e);\n    return \"\";\n  }\n}\n"
  },
  {
    "path": "apps/dashboard/src/lib/formatter.ts",
    "content": "import { endOfDay, isSameDay, startOfDay } from \"date-fns\";\n\nexport function formatMilliseconds(ms: number) {\n  if (ms > 1000) {\n    return `${Intl.NumberFormat(\"en-US\", {\n      style: \"unit\",\n      unit: \"second\",\n      maximumFractionDigits: 2,\n    }).format(ms / 1000)}`;\n  }\n\n  return `${Intl.NumberFormat(\"en-US\", {\n    style: \"unit\",\n    unit: \"millisecond\",\n  }).format(ms)}`;\n}\n\nexport function formatPercentage(value: number) {\n  if (Number.isNaN(value)) return \"100%\";\n  return `${Intl.NumberFormat(\"en-US\", {\n    style: \"percent\",\n    minimumFractionDigits: 2,\n    maximumFractionDigits: 2,\n  }).format(value)}`;\n}\n\nexport function formatNumber(value: number) {\n  return `${Intl.NumberFormat(\"en-US\").format(value)}`;\n}\n\n// TODO: think of supporting custom formats\n\nexport function formatDate(date: Date) {\n  return date.toLocaleDateString(\"en-US\", {\n    year: \"numeric\",\n    month: \"long\",\n    day: \"numeric\",\n  });\n}\n\nexport function formatDateTime(date: Date) {\n  return date.toLocaleDateString(\"en-US\", {\n    month: \"long\",\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"numeric\",\n  });\n}\n\nexport function formatTime(date: Date) {\n  return date.toLocaleTimeString(\"en-US\", {\n    hour: \"numeric\",\n    minute: \"numeric\",\n  });\n}\n\nexport function formatDateRange(from?: Date, to?: Date) {\n  const sameDay = from && to && isSameDay(from, to);\n  const isFromStartDay = from && startOfDay(from).getTime() === from.getTime();\n  const isToEndDay = to && endOfDay(to).getTime() === to.getTime();\n\n  if (sameDay) {\n    if (from && to) {\n      return `${formatDateTime(from)} - ${formatTime(to)}`;\n    }\n  }\n\n  if (from && to) {\n    if (isFromStartDay && isToEndDay) {\n      return `${formatDate(from)} - ${formatDate(to)}`;\n    }\n    return `${formatDateTime(from)} - ${formatDateTime(to)}`;\n  }\n\n  if (to) {\n    return `Until ${formatDateTime(to)}`;\n  }\n\n  if (from) {\n    return `Since ${formatDateTime(from)}`;\n  }\n\n  return \"All time\";\n}\n\nexport function formatDateForInput(date: Date): string {\n  const year = date.getFullYear();\n  const month = String(date.getMonth() + 1).padStart(2, \"0\");\n  const day = String(date.getDate()).padStart(2, \"0\");\n  const hours = String(date.getHours()).padStart(2, \"0\");\n  const minutes = String(date.getMinutes()).padStart(2, \"0\");\n\n  return `${year}-${month}-${day}T${hours}:${minutes}`;\n}\n"
  },
  {
    "path": "apps/dashboard/src/lib/middleware/with-invitation.ts",
    "content": ""
  },
  {
    "path": "apps/dashboard/src/lib/stripe.ts",
    "content": "import type { Stripe as StripeProps } from \"@stripe/stripe-js\";\nimport { loadStripe } from \"@stripe/stripe-js\";\n\nlet stripePromise: Promise<StripeProps | null>;\n\nexport const getStripe = () => {\n  if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {\n    throw new Error(\"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set\");\n  }\n\n  if (!stripePromise) {\n    stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);\n  }\n\n  return stripePromise;\n};\n"
  },
  {
    "path": "apps/dashboard/src/lib/trpc/client.tsx",
    "content": "\"use client\";\n\nimport { endingLink } from \"@/lib/trpc/shared\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { createTRPCClient, loggerLink } from \"@trpc/client\";\nimport { createTRPCContext } from \"@trpc/tanstack-react-query\";\nimport { useState } from \"react\";\n\nimport type { AppRouter } from \"@openstatus/api\";\n\nexport const { TRPCProvider, useTRPC, useTRPCClient } =\n  createTRPCContext<AppRouter>();\n\nfunction makeQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        // With SSR, we usually want to set some default staleTime\n        // above 0 to avoid refetching immediately on the client\n        staleTime: 60 * 1000,\n      },\n    },\n  });\n}\nlet browserQueryClient: QueryClient | undefined = undefined;\nfunction getQueryClient() {\n  if (typeof window === \"undefined\") {\n    // Server: always make a new query client\n    return makeQueryClient();\n  }\n  // Browser: make a new query client if we don't already have one\n  // This is very important, so we don't re-make a new client if React\n  // suspends during the initial render. This may not be needed if we\n  // have a suspense boundary BELOW the creation of the query client\n  if (!browserQueryClient) browserQueryClient = makeQueryClient();\n  return browserQueryClient;\n}\n\nexport function TRPCReactProvider({ children }: { children: React.ReactNode }) {\n  const queryClient = getQueryClient();\n  const [trpcClient] = useState(() =>\n    createTRPCClient<AppRouter>({\n      links: [\n        loggerLink({\n          enabled: (opts) =>\n            process.env.NODE_ENV === \"development\" ||\n            (opts.direction === \"down\" && opts.result instanceof Error),\n        }),\n        endingLink({\n          headers: {\n            \"x-trpc-source\": \"client\",\n          },\n        }),\n      ],\n    }),\n  );\n\n  return (\n    <QueryClientProvider client={queryClient}>\n      <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>\n        {children}\n      </TRPCProvider>\n    </QueryClientProvider>\n  );\n}\n"
  },
  {
    "path": "apps/dashboard/src/lib/trpc/query-client.ts",
    "content": "import {\n  QueryClient,\n  defaultShouldDehydrateQuery,\n} from \"@tanstack/react-query\";\n\nexport function makeQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        staleTime: 60 * 1000,\n      },\n      dehydrate: {\n        shouldDehydrateQuery: (query) =>\n          defaultShouldDehydrateQuery(query) ||\n          query.state.status === \"pending\",\n      },\n      hydrate: {},\n    },\n  });\n}\n"
  },
  {
    "path": "apps/dashboard/src/lib/trpc/server.tsx",
    "content": "import \"server-only\";\n\nimport type { AppRouter } from \"@openstatus/api\";\n\nimport { HydrationBoundary } from \"@tanstack/react-query\";\nimport { dehydrate } from \"@tanstack/react-query\";\nimport { createTRPCClient, loggerLink } from \"@trpc/client\";\nimport {\n  type TRPCQueryOptions,\n  createTRPCOptionsProxy,\n} from \"@trpc/tanstack-react-query\";\nimport { cookies } from \"next/headers\";\nimport { cache } from \"react\";\nimport { makeQueryClient } from \"./query-client\";\nimport { endingLink } from \"./shared\";\n\n// IMPORTANT: Create a stable getter for the query client that\n//            will return the same client during the same request.\nexport const getQueryClient = cache(makeQueryClient);\n\nexport const trpc = createTRPCOptionsProxy<AppRouter>({\n  queryClient: getQueryClient,\n  client: createTRPCClient({\n    links: [\n      loggerLink({\n        enabled: (opts) =>\n          process.env.NODE_ENV === \"development\" ||\n          (opts.direction === \"down\" && opts.result instanceof Error),\n      }),\n      endingLink({\n        headers: {\n          \"x-trpc-source\": \"server\",\n        },\n        fetch: async (url, options) => {\n          const cookieStore = await cookies();\n          return fetch(url, {\n            ...options,\n            credentials: \"include\",\n            headers: {\n              ...options?.headers,\n              cookie: cookieStore.toString(),\n            },\n          });\n        },\n      }),\n    ],\n  }),\n});\n\nexport function HydrateClient(props: { children: React.ReactNode }) {\n  const queryClient = getQueryClient();\n\n  return (\n    <HydrationBoundary state={dehydrate(queryClient)}>\n      {props.children}\n    </HydrationBoundary>\n  );\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\nexport function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(\n  queryOptions: T,\n) {\n  const queryClient = getQueryClient();\n\n  if (queryOptions.queryKey[1]?.type === \"infinite\") {\n    // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\n    void queryClient.prefetchInfiniteQuery(queryOptions as any);\n  } else {\n    void queryClient.prefetchQuery(queryOptions);\n  }\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\nexport function batchPrefetch<T extends ReturnType<TRPCQueryOptions<any>>>(\n  queryOptionsArray: T[],\n) {\n  const queryClient = getQueryClient();\n\n  for (const queryOptions of queryOptionsArray) {\n    if (queryOptions.queryKey[1]?.type === \"infinite\") {\n      // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\n      void queryClient.prefetchInfiniteQuery(queryOptions as any);\n    } else {\n      void queryClient.prefetchQuery(queryOptions);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/dashboard/src/lib/trpc/shared.ts",
    "content": "import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from \"@trpc/client\";\nimport { httpBatchLink } from \"@trpc/client\";\n\nimport type { AppRouter } from \"@openstatus/api\";\nimport superjson from \"superjson\";\n\nconst getBaseUrl = () => {\n  if (typeof window !== \"undefined\") return \"\";\n  // Note: dashboard has its own tRPC API routes\n  if (process.env.VERCEL_URL) return \"https://app.openstatus.dev\"; // Vercel\n  return \"http://localhost:3000\"; // Local dev and Docker (internal calls)\n};\n\nconst lambdas = [\n  \"stripeRouter\",\n  \"emailRouter\",\n  \"apiKeyRouter\",\n  \"integrationRouter\",\n];\n\nexport const endingLink = (opts?: {\n  fetch?: typeof fetch;\n  headers?: HTTPHeaders | (() => HTTPHeaders | Promise<HTTPHeaders>);\n}) =>\n  ((runtime) => {\n    const sharedOpts = {\n      headers: opts?.headers,\n      fetch: opts?.fetch,\n      transformer: superjson,\n      // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\n    } satisfies Partial<HTTPBatchLinkOptions<any>>;\n\n    const edgeLink = httpBatchLink({\n      ...sharedOpts,\n      url: `${getBaseUrl()}/api/trpc/edge`,\n    })(runtime);\n    const lambdaLink = httpBatchLink({\n      ...sharedOpts,\n      url: `${getBaseUrl()}/api/trpc/lambda`,\n    })(runtime);\n\n    return (ctx) => {\n      const path = ctx.op.path.split(\".\") as [string, ...string[]];\n      const endpoint = lambdas.includes(path[0]) ? \"lambda\" : \"edge\";\n\n      const newCtx = {\n        ...ctx,\n        op: { ...ctx.op, path: path.join(\".\") },\n      };\n      return endpoint === \"edge\" ? edgeLink(newCtx) : lambdaLink(newCtx);\n    };\n  }) satisfies TRPCLink<AppRouter>;\n"
  },
  {
    "path": "apps/dashboard/src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/dashboard/src/next-auth.d.ts",
    "content": "import type { User as DefaultUserSchema } from \"@openstatus/db/src/schema\";\n\ndeclare module \"next-auth\" {\n  // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n  interface User extends DefaultUserSchema {}\n}\n"
  },
  {
    "path": "apps/dashboard/src/proxy.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport { auth } from \"@/lib/auth\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { user, usersToWorkspaces, workspace } from \"@openstatus/db/src/schema\";\nimport { getCurrency } from \"@openstatus/db/src/schema/plan/utils\";\n\nexport default auth(async (req) => {\n  const url = req.nextUrl.clone();\n  const response = NextResponse.next();\n\n  const continent = req.headers.get(\"x-vercel-ip-continent\") || \"NA\";\n  const country = req.headers.get(\"x-vercel-ip-country\") || \"US\";\n  const currency = getCurrency({ continent, country });\n\n  // NOTE: used in the pricing table to display the currency based on user's location\n  response.cookies.set(\"x-currency\", currency);\n\n  if (url.pathname.includes(\"api/trpc\")) {\n    return response;\n  }\n\n  if (!req.auth && url.pathname !== \"/login\") {\n    console.log(\"User not authenticated, redirecting to login\");\n    const newURL = new URL(\"/login\", req.url);\n    const encodedSearchParams = `${url.pathname}${url.search}`;\n\n    if (encodedSearchParams) {\n      newURL.searchParams.append(\"redirectTo\", encodedSearchParams);\n    }\n\n    return NextResponse.redirect(newURL);\n  }\n\n  if (req.auth && url.pathname === \"/login\") {\n    const redirectTo = url.searchParams.get(\"redirectTo\");\n    console.log(\"User authenticated, redirecting to\", redirectTo);\n    if (redirectTo) {\n      const redirectToUrl = new URL(redirectTo, req.url);\n      return NextResponse.redirect(redirectToUrl);\n    }\n  }\n\n  const hasWorkspaceSlug = req.cookies.has(\"workspace-slug\");\n\n  if (req.auth?.user?.id && !hasWorkspaceSlug) {\n    const [query] = await db\n      .select()\n      .from(usersToWorkspaces)\n      .innerJoin(user, eq(user.id, usersToWorkspaces.userId))\n      .innerJoin(workspace, eq(workspace.id, usersToWorkspaces.workspaceId))\n      .where(eq(user.id, Number.parseInt(req.auth.user.id)))\n      .all();\n\n    if (!query) {\n      console.error(\">> Should not happen, no workspace found for user\");\n    }\n\n    response.cookies.set(\"workspace-slug\", query.workspace.slug);\n  }\n\n  if (!req.auth && hasWorkspaceSlug) {\n    response.cookies.delete(\"workspace-slug\");\n  }\n\n  return response;\n});\n\nexport const config = {\n  matcher: [\n    \"/((?!api|assets|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)\",\n  ],\n};\n"
  },
  {
    "path": "apps/dashboard/src/scripts/README.md",
    "content": "# Export Blog Post Metrics Script\n\nThis script exports monitor metrics data from OpenStatus for use in blog posts and documentation.\n\n## Overview\n\nThe script fetches monitor data directly from the database and Tinybird analytics, then exports it to a JSON file that can be used for visualizations in blog posts.\n\n**Features:**\n- Fetches metrics from both regular regions and private locations\n- Automatically combines public regions with private location data\n- Supports both HTTP and TCP monitors\n\n## Configuration\n\nEdit the constants at the top of `export-blog-post-metrics.ts`:\n\n```typescript\nconst MONITOR_ID = \"1\";        // The ID of the monitor to export\nconst PERIOD = \"7d\";           // Time period: \"1d\", \"7d\", or \"14d\"\nconst INTERVAL = 60;           // Interval in minutes for data points\nconst TYPE = \"http\";           // Fallback monitor type: \"http\" or \"tcp\" (auto-detected from monitor)\nconst OUTPUT_FILE = \"blog-post-metrics.json\"; // Output filename\n```\n\n**Note:** The script automatically detects the monitor type from the database, but you can set a fallback with the `TYPE` constant.\n\n## Prerequisites\n\n1. Make sure you have the `TINY_BIRD_API_KEY` environment variable set in your `.env` file\n2. The database should be accessible (local or remote)\n3. Install dependencies: `pnpm install`\n\n## Usage\n\n> [!IMPORTANT]\n> Go to the `/tinybird/src/client.ts` file and make sure tb is **not using the NoopClient**.\n\nFrom the `apps/dashboard` directory:\n\n```bash\n# Using the npm script\npnpm export-metrics\n\n# Or directly with bun\nbun src/scripts/export-blog-post-metrics.ts\n```\n\n## Output Format\n\nThe script generates a JSON file with the following structure:\n\n```json\n{\n  \"regions\": [\"ams\", \"fra\", \"lhr\", ...],\n  \"data\": {\n    \"regions\": [\"ams\", \"fra\", \"lhr\", ...],\n    \"data\": [\n      {\n        \"timestamp\": \"2025-08-18T16:00:00.000Z\",\n        \"ams\": 207,\n        \"fra\": 142,\n        \"lhr\": 327,\n        ...\n      }\n    ]\n  },\n  \"metricsByRegions\": [\n    {\n      \"region\": \"ams\",\n      \"count\": 1000,\n      \"ok\": 995,\n      \"p50Latency\": 150,\n      \"p75Latency\": 200,\n      \"p90Latency\": 250,\n      \"p95Latency\": 300,\n      \"p99Latency\": 400\n    }\n  ]\n}\n```\n\n## Data Fields\n\n- **regions**: Array of region codes and private location names for the monitor\n- **data.data**: Timeline data with latency values per region/location at each timestamp\n- **metricsByRegions**: Summary statistics per region/location including:\n  - `count`: Total number of checks\n  - `ok`: Number of successful checks\n  - `p50Latency`, `p75Latency`, `p90Latency`, `p95Latency`, `p99Latency`: Latency percentiles in milliseconds\n\n**Note:** The script automatically includes both public Fly.io regions and any private locations connected to the monitor.\n\n## Example: Moving to Web Assets\n\nTo use the exported data in the web app (like the existing `hono-cold.json`):\n\n```bash\n# After running the script\ncp blog-post-metrics.json ../web/public/assets/posts/your-blog-post/data.json\n```\n\n## Troubleshooting\n\n**Error: \"TINY_BIRD_API_KEY environment variable is required\"**\n- Make sure you have the `TINY_BIRD_API_KEY` set in your `.env` file\n\n**Error: \"Monitor with ID X not found\"**\n- Verify the monitor ID exists in your database\n- Check that you're connected to the correct database\n\n**No data returned**\n- Ensure the monitor has been running and collecting data for the specified period\n- Try a different time period (e.g., \"7d\" instead of \"1d\")\n\n"
  },
  {
    "path": "apps/dashboard/src/scripts/export-blog-post-metrics.ts",
    "content": "import { writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { db, eq } from \"@openstatus/db\";\nimport { monitor, selectMonitorSchema } from \"@openstatus/db/src/schema\";\nimport { OSTinybird } from \"@openstatus/tinybird\";\n\n// WARNING: make sure to enable the Tinybird client in the env you are running this script in\n\n// Configuration\nconst MONITOR_ID = \"7002\";\nconst PERIOD = \"7d\" as const;\nconst INTERVAL = 60;\nconst TYPE = \"http\" as const;\nconst OUTPUT_FILE = \"blog-post-metrics.json\";\nconst PERCENTILE = \"p50\"; // p50, p75, p90, p95, p99\n\nasync function main() {\n  // Get Tinybird API key from environment\n  const tinybirdApiKey = process.env.TINY_BIRD_API_KEY;\n  if (!tinybirdApiKey) {\n    throw new Error(\"TINY_BIRD_API_KEY environment variable is required\");\n  }\n\n  const tb = new OSTinybird(tinybirdApiKey);\n\n  console.log(`Fetching data for monitor ID: ${MONITOR_ID}`);\n\n  // 1. Fetch monitor from database with private locations\n  const monitorDataRaw = await db.query.monitor.findFirst({\n    where: eq(monitor.id, Number.parseInt(MONITOR_ID)),\n    with: {\n      privateLocationToMonitors: {\n        with: {\n          privateLocation: true,\n        },\n      },\n    },\n  });\n\n  if (!monitorDataRaw) {\n    throw new Error(`Monitor with ID ${MONITOR_ID} not found`);\n  }\n\n  // Parse the monitor data using the schema to convert regions string to array\n  const monitorData = selectMonitorSchema.parse(monitorDataRaw);\n\n  // Get private location names\n  const privateLocationNames =\n    monitorDataRaw.privateLocationToMonitors\n      ?.map((pl) => pl.privateLocation?.name)\n      .filter((name): name is string => Boolean(name)) || [];\n\n  // Combine regular regions with private locations\n  const allRegions = [...monitorData.regions, ...privateLocationNames];\n\n  console.log(`\\nMonitor Details:`);\n  console.log(`  ID: ${MONITOR_ID}`);\n  console.log(`  Name: ${monitorData.name || \"Unnamed\"}`);\n  console.log(`  Type: ${monitorData.jobType}`);\n  console.log(`  Active: ${monitorData.active}`);\n  console.log(`  Created: ${monitorData.createdAt}`);\n  console.log(`  Regular regions: ${monitorData.regions.join(\", \")}`);\n  console.log(\n    `  Private locations: ${privateLocationNames.join(\", \") || \"None\"}`\n  );\n  console.log(`  Total regions: ${allRegions.length}`);\n  console.log(`\\nQuery Parameters:`);\n  console.log(`  Period: ${PERIOD}`);\n  console.log(`  Interval: ${INTERVAL} minutes`);\n\n  // Use the monitor's actual type, or fall back to the configured TYPE\n  const monitorType = (monitorData.jobType || TYPE) as \"http\" | \"tcp\";\n\n  // 2. Fetch metricsRegions (timeline data with region, timestamp, and quantiles)\n  const metricsRegionsResult =\n    monitorType === \"http\"\n      ? PERIOD === \"7d\"\n        ? await tb.httpMetricsRegionsWeekly({\n            monitorId: MONITOR_ID,\n            interval: INTERVAL,\n          })\n        : await tb.httpMetricsRegionsDaily({\n            monitorId: MONITOR_ID,\n            interval: INTERVAL,\n          })\n      : PERIOD === \"7d\"\n      ? await tb.tcpMetricsByIntervalWeekly({\n          monitorId: MONITOR_ID,\n          interval: INTERVAL,\n        })\n      : await tb.tcpMetricsByIntervalDaily({\n          monitorId: MONITOR_ID,\n          interval: INTERVAL,\n        });\n\n  console.log(\n    `\\nFetched ${metricsRegionsResult.data.length} metrics regions data points`\n  );\n  if (metricsRegionsResult.data.length > 0) {\n    console.log(\n      `  First data point:`,\n      JSON.stringify(metricsRegionsResult.data[0], null, 2)\n    );\n    console.log(\n      `  Last data point:`,\n      JSON.stringify(\n        metricsRegionsResult.data[metricsRegionsResult.data.length - 1],\n        null,\n        2\n      )\n    );\n  } else {\n    console.log(`  ⚠️  No data returned. This could mean:`);\n    console.log(`     - The monitor hasn't collected any data yet`);\n    console.log(`     - The monitor is inactive or was just created`);\n    console.log(\n      `     - There's no data in the selected time period (${PERIOD})`\n    );\n    console.log(\n      `\\n  💡 Tip: Try querying without the interval parameter or using PERIOD=\"1d\"`\n    );\n\n    // Try without interval to see if that helps\n    console.log(`\\n  Trying without interval parameter...`);\n    const retryResult =\n      monitorType === \"http\"\n        ? PERIOD === \"7d\"\n          ? await tb.httpMetricsRegionsWeekly({\n              monitorId: MONITOR_ID,\n            })\n          : await tb.httpMetricsRegionsDaily({\n              monitorId: MONITOR_ID,\n            })\n        : PERIOD === \"7d\"\n        ? await tb.tcpMetricsByIntervalWeekly({\n            monitorId: MONITOR_ID,\n          })\n        : await tb.tcpMetricsByIntervalDaily({\n            monitorId: MONITOR_ID,\n          });\n    console.log(`  Retry returned ${retryResult.data.length} data points`);\n    if (retryResult.data.length > 0) {\n      console.log(`  ✅ Success! The interval parameter might be the issue.`);\n      console.log(\n        `     First data point:`,\n        JSON.stringify(retryResult.data[0], null, 2)\n      );\n    }\n  }\n\n  // 3. Fetch metricsByRegion (summary data by region)\n  const metricsByRegionProcedure =\n    monitorType === \"http\"\n      ? PERIOD === \"7d\"\n        ? tb.httpMetricsByRegionWeekly\n        : tb.httpMetricsByRegionDaily\n      : PERIOD === \"7d\"\n      ? tb.tcpMetricsByRegionWeekly\n      : tb.tcpMetricsByRegionDaily;\n\n  const metricsByRegionsResult = await metricsByRegionProcedure({\n    monitorId: MONITOR_ID,\n  });\n\n  console.log(\n    `\\nFetched ${metricsByRegionsResult.data.length} metrics by region data points`\n  );\n  if (metricsByRegionsResult.data.length > 0) {\n    console.log(\n      `  Sample:`,\n      JSON.stringify(metricsByRegionsResult.data.slice(0, 3), null, 2)\n    );\n  }\n\n  // 4. Transform metricsRegions data to match expected format\n  // Group by timestamp and pivot regions as columns\n  const timelineMap = new Map<number, Record<string, number | string>>();\n\n  for (const row of metricsRegionsResult.data) {\n    const timestamp = row.timestamp;\n    const region = row.region;\n    const latency = row[`${PERCENTILE}Latency`] ?? 0;\n\n    if (!timelineMap.has(timestamp)) {\n      timelineMap.set(timestamp, {\n        timestamp: new Date(timestamp).toISOString(),\n      });\n    }\n\n    const entry = timelineMap.get(timestamp)!;\n    entry[region] = latency;\n  }\n\n  // Convert map to sorted array\n  const timelineData = Array.from(timelineMap.values()).sort((a, b) => {\n    const timeA = new Date(a.timestamp as string).getTime();\n    const timeB = new Date(b.timestamp as string).getTime();\n    return timeA - timeB;\n  });\n\n  // 5. Build final output structure\n  const output = {\n    regions: allRegions,\n    data: {\n      regions: allRegions,\n      data: timelineData,\n    },\n    metricsByRegions: metricsByRegionsResult.data,\n  };\n\n  // 6. Write to file\n  const outputPath = resolve(process.cwd(), OUTPUT_FILE);\n  writeFileSync(outputPath, JSON.stringify(output, null, 2));\n\n  console.log(`\\n✅ Data exported successfully to: ${outputPath}`);\n  console.log(`Total timeline entries: ${timelineData.length}`);\n  console.log(\n    `Total regions (including private locations): ${allRegions.length}`\n  );\n}\n\n// Run the script\nmain().catch((error) => {\n  console.error(\"Error:\", error);\n  process.exit(1);\n});\n"
  },
  {
    "path": "apps/dashboard/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"strictNullChecks\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\", \"env.ts\"]\n}\n"
  },
  {
    "path": "apps/docs/.gitignore",
    "content": "# build output\ndist/\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# environment variables\n.env\n.env.production\n\n# macOS-specific files\n.DS_Store\n"
  },
  {
    "path": "apps/docs/README.md",
    "content": "# Starlight Starter Kit: Basics\n\n[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)\n\n```\nnpm create astro@latest -- --template starlight\n```\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)\n[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)\n[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)\n\n> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!\n\n## 🚀 Project Structure\n\nInside of your Astro + Starlight project, you'll see the following folders and files:\n\n```\n.\n├── public/\n├── src/\n│   ├── assets/\n│   ├── content/\n│   │   ├── docs/\n│   │   └── config.ts\n│   └── env.d.ts\n├── astro.config.mjs\n├── package.json\n└── tsconfig.json\n```\n\nStarlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.\n\nImages can be added to `src/assets/` and embedded in Markdown with a relative link.\n\nStatic assets, like favicons, can be placed in the `public/` directory.\n\n## 🧞 Commands\n\nAll commands are run from the root of the project, from a terminal:\n\n| Command                   | Action                                           |\n| :------------------------ | :----------------------------------------------- |\n| `npm install`             | Installs dependencies                            |\n| `npm run dev`             | Starts local dev server at `localhost:4321`      |\n| `npm run build`           | Build your production site to `./dist/`          |\n| `npm run preview`         | Preview your build locally, before deploying     |\n| `npm run astro ...`       | Run CLI commands like `astro add`, `astro check` |\n| `npm run astro -- --help` | Get help using the Astro CLI                     |\n\n## 👀 Want to learn more?\n\nCheck out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).\n"
  },
  {
    "path": "apps/docs/astro.config.mjs",
    "content": "import sitemap from \"@astrojs/sitemap\";\nimport starlight from \"@astrojs/starlight\";\nimport tailwindcss from \"@tailwindcss/vite\";\nimport { defineConfig, envField } from \"astro/config\";\nimport starlightImageZoom from \"starlight-image-zoom\";\nimport starlightLinksValidator from \"starlight-links-validator\";\nimport starlightLlmsTxt from \"starlight-llms-txt\";\n\nimport Icons from \"unplugin-icons/vite\";\n\n// https://astro.build/config\nexport default defineConfig({\n  site: \"https://docs.openstatus.dev\",\n  vite: {\n    plugins: [Icons({ compiler: \"astro\" }), tailwindcss()],\n    ssr: {\n      noExternal: [\"zod\"],\n    },\n  },\n  env: {\n    schema: {\n      NEXT_PUBLIC_OPENPANEL_CLIENT_ID: envField.string({\n        access: \"public\",\n        context: \"client\",\n      }),\n    },\n  },\n  integrations: [\n    sitemap(),\n    starlight({\n      title: \"openstatus docs\",\n      favicon: \"/favicon.ico\",\n      social: [\n        {\n          icon: \"github\",\n          label: \"GitHub\",\n          href: \"https://github.com/openstatusHQ/openstatus\",\n        },\n        {\n          icon: \"discord\",\n          label: \"Discord\",\n          href: \"https://www.openstatus.dev/discord\",\n        },\n        {\n          icon: \"blueSky\",\n          label: \"BlueSky\",\n          href: \"https://bsky.app/profile/openstatus.dev\",\n        },\n      ],\n      components: {\n        SiteTitle: \"./src/components/SiteTitle.astro\",\n        Head: \"./src/components/Head.astro\",\n        Hero: \"./src/components/Hero.astro\",\n        Footer: \"./src/components/Footer.astro\",\n      },\n      editLink: {\n        baseUrl: \"https://github.com/openstatusHQ/openstatus/app/docs\",\n      },\n      customCss: [\n        // Path to your Tailwind base styles:\n        \"./src/global.css\",\n        \"./src/custom.css\",\n        \"@fontsource-variable/inter\",\n      ],\n      sidebar: [\n        {\n          label: \"Concepts\",\n          items: [\n            {\n              label: \"About Uptime monitoring\",\n              slug: \"concept/uptime-monitoring\",\n            },\n            {\n              label: \"Best Practices for Status Pages\",\n              slug: \"concept/best-practices-status-page\",\n            },\n            {\n              label: \"Uptime Calculation and Values\",\n              slug: \"concept/uptime-calculation-and-values\",\n            },\n            {\n              label: \"Uptime Monitoring as Code\",\n              slug: \"concept/uptime-monitoring-as-code\",\n            },\n            {\n              label: \"Latency vs Response Time\",\n              slug: \"concept/latency-vs-response-time\",\n            },\n          ],\n        },\n        {\n          label: \"Tutorials\",\n          items: [\n            {\n              label: \"How to create a monitor\",\n              slug: \"tutorial/how-to-create-monitor\",\n            },\n            {\n              label: \"How to create a status page\",\n              slug: \"tutorial/how-to-create-status-page\",\n            },\n            {\n              label: \"How to configure a status page\",\n              slug: \"tutorial/how-to-configure-status-page\",\n            },\n            {\n              label: \"How to create a private location (beta)\",\n              slug: \"tutorial/how-to-create-private-location\",\n            },\n            {\n              label: \"Get Started with OpenStatus CLI\",\n              slug: \"tutorial/get-started-with-openstatus-cli\",\n            },\n            {\n              label: \"How to set up the Slack Agent\",\n              slug: \"tutorial/how-to-setup-slack-agent\",\n            },\n          ],\n        },\n\n        {\n          label: \"Guides\",\n          items: [\n            {\n              label: \"Monitor your MCP Server\",\n              slug: \"guides/how-to-monitor-mcp-server\",\n            },\n            {\n              label: \"Run check in GitHub Actions\",\n              slug: \"guides/how-to-run-synthetic-test-github-action\",\n            },\n            {\n              label: \"Export Metrics to your OTLP Endpoint\",\n              slug: \"guides/how-to-export-metrics-to-otlp-endpoint\",\n            },\n            {\n              label: \"How to Add an SVG Status Badge to your GitHub README\",\n              slug: \"guides/how-to-add-svg-status-badge\",\n            },\n            {\n              label: \"How to use React Status Widget\",\n              slug: \"guides/how-to-use-react-widget\",\n            },\n            {\n              label: \"How to deploy probes on Cloudflare Containers \",\n              slug: \"guides/how-to-deploy-probes-cloudflare-containers\",\n            },\n            {\n              label: \"How to self-host openstatus\",\n              slug: \"guides/self-hosting-openstatus\",\n            },\n            {\n              label: \"Self host Status Page only\",\n              slug: \"guides/self-host-status-page-only\",\n            },\n          ],\n        },\n        {\n          label: \"SDK\",\n          items: [\n            {\n              label: \"Node SDK\",\n              autogenerate: { directory: \"sdk/nodejs\" },\n              collapsed: true,\n            },\n          ],\n        },\n        {\n          label: \"Reference\",\n          items: [\n            {\n              label: \"CLI Reference\",\n              slug: \"reference/cli-reference\",\n            },\n            {\n              label: \"API Reference V1 - Deprecated\",\n              link: \"https://api.openstatus.dev/v1\",\n              // badge: { text: 'External' },\n              attrs: {\n                target: \"_blank\",\n              },\n            },\n            {\n              label: \"API Reference V2\",\n              link: \"https://api.openstatus.dev/openapi\",\n              // badge: { text: 'External' },\n              attrs: {\n                target: \"_blank\",\n              },\n            },\n            {\n              label: \"DNS Monitor\",\n              slug: \"reference/dns-monitor\",\n            },\n            {\n              label: \"HTTP Monitor\",\n              slug: \"reference/http-monitor\",\n            },\n            {\n              label: \"Incident\",\n              slug: \"reference/incident\",\n            },\n            {\n              label: \"TCP Monitor\",\n              slug: \"reference/tcp-monitor\",\n            },\n            {\n              label: \"Notification\",\n              slug: \"reference/notification\",\n            },\n            {\n              label: \"Location\",\n              slug: \"reference/location\",\n            },\n            {\n              label: \"Private location\",\n              slug: \"reference/private-location\",\n            },\n            {\n              label: \"Status Page\",\n              slug: \"reference/status-page\",\n            },\n            {\n              label: \"Page Components\",\n              slug: \"reference/page-components\",\n            },\n            {\n              label: \"Status Report\",\n              slug: \"reference/status-report\",\n            },\n            {\n              label: \"Subscriber\",\n              slug: \"reference/subscriber\",\n            },\n            {\n              label: \"Terraform Provider\",\n              slug: \"reference/terraform\",\n            },\n          ],\n        },\n      ],\n      plugins: [\n        starlightLinksValidator({\n          errorOnLocalLinks: false,\n        }),\n        starlightLlmsTxt({\n          projectName: \"openstatus docs\",\n          description:\n            \"openstatus is an open-source status page platform with global monitoring (HTTP, TCP, DNS).\",\n        }),\n        starlightImageZoom(),\n      ],\n    }),\n  ],\n});\n"
  },
  {
    "path": "apps/docs/package.json",
    "content": "{\n  \"name\": \"@openstatus/docs\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"start\": \"astro dev\",\n    \"build\": \"astro check && astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\"\n  },\n  \"dependencies\": {\n    \"@astrojs/check\": \"0.9.6\",\n    \"@astrojs/react\": \"4.4.2\",\n    \"@astrojs/sitemap\": \"3.6.0\",\n    \"@astrojs/starlight\": \"0.37.1\",\n    \"@astrojs/starlight-tailwind\": \"4.0.2\",\n    \"@fontsource-variable/inter\": \"5.2.8\",\n    \"@openpanel/astro\": \"1.0.1\",\n    \"@tailwindcss/vite\": \"4.1.8\",\n    \"astro\": \"5.16.6\",\n    \"shiki\": \"3.23.0\",\n    \"sharp\": \"0.33.5\",\n    \"starlight-image-zoom\": \"0.13.2\",\n    \"starlight-links-validator\": \"0.19.2\",\n    \"starlight-llms-txt\": \"0.7.0\",\n    \"starlight-showcases\": \"0.3.1\",\n    \"starlight-sidebar-topics\": \"0.6.2\",\n    \"tailwindcss\": \"4.1.11\",\n    \"unplugin-icons\": \"22.1.0\"\n  },\n  \"devDependencies\": {\n    \"@iconify-json/lucide\": \"1.2.26\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "apps/docs/public/robots.txt",
    "content": "# Allow all crawlers\nUser-agent: *\nAllow: /\n\n# Sitemap location\nSitemap: https://docs.openstatus.dev/sitemap-index.xml\n\n"
  },
  {
    "path": "apps/docs/src/components/Footer.astro",
    "content": "---\nimport EditLink from \"@astrojs/starlight/components/EditLink.astro\";\nimport LastUpdated from \"@astrojs/starlight/components/LastUpdated.astro\";\nimport Pagination from \"@astrojs/starlight/components/Pagination.astro\";\n---\n\n<footer class=\"sl-flex\">\n\t<div class=\"meta sl-flex\">\n\t\t<EditLink {...Astro.props} />\n\t\t<LastUpdated {...Astro.props} />\n\t</div>\n\t<Pagination {...Astro.props} />\n\n    <div class=\"github font-mono\">\n        <span>Show your support! Star us on GitHub ⭐️</span>\n        <a class=\"github-button\" href=\"https://github.com/openstatusHQ/openstatus\" data-color-scheme=\"no-preference: light; light: light; dark: light;\" data-icon=\"octicon-star\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Star openstatusHQ/openstatus on GitHub\">Star</a>\n    </div>\n\t<div class=\"flex items-center justify-center\">\n\t<a href=\"https://status.openstatus.dev\" target=\"_blank\" >\n\t\t<img src='https://status.openstatus.dev/badge/v2?variant=outline'>\n\t\t</a>\n\t</div>\n</footer>\n\n<style>\n\tfooter {\n\t\tflex-direction: column;\n\t\tgap: 1.5rem;\n\t}\n\t.meta {\n\t\tgap: 0.75rem 3rem;\n\t\tjustify-content: space-between;\n\t\tflex-wrap: wrap;\n\t\tmargin-top: 3rem;\n\t\tfont-size: var(--sl-text-sm);\n\t\tcolor: var(--sl-color-gray-3);\n\t}\n\t.meta > :global(p:only-child) {\n\t\tmargin-inline-start: auto;\n\t}\n\n\t.github {\n\t\talign-items: center;\n        justify-content: center;\n\t\tgap: 0.5em;\n\t\tmargin: 2rem auto;\n\t\tfont-size: var(--sl-text-sm);\n\t\ttext-decoration: none;\n        display: flex;\n        flex-direction: column;\n\t}\n    .github span {\n        font-size: var(--sl-text-sm);\n        cursor: default;\n    }\n</style>\n"
  },
  {
    "path": "apps/docs/src/components/Head.astro",
    "content": "---\nimport { NEXT_PUBLIC_OPENPANEL_CLIENT_ID } from \"astro:env/client\";\nimport Default from \"@astrojs/starlight/components/Head.astro\";\nimport { OpenPanelComponent } from \"@openpanel/astro\";\n\nconst title = Astro.locals.starlightRoute.entry.data.title;\nconst { siteTitle } = Astro.locals.starlightRoute;\n\nconst url = `https://openstatus.dev/api/og?title=${encodeURIComponent(siteTitle)}&description=${encodeURIComponent(title)}`;\n---\n<Default><slot /></Default>\n\n<meta property=\"og:image\" content={url} />\n<meta name=\"twitter:image\" content={url} />\n\n\n<script is:inline defer data-domain=\"docs.openstatus.dev\" src=\"https://plausible.io/js/script.js\" />\n\n<!-- REMINDER: prevent unexpected font flashes for our 'OpenStatus' logo on each page load -->\n<link rel=\"preload\" href=\"/fonts/CalSans-SemiBold.ttf\" as=\"font\" type=\"font/ttf\" crossorigin>\n<link rel=\"preload\" href=\"/fonts/CommitMono-400-Regular.otf\" as=\"font\" type=\"font/otf\" crossorigin>\n<link rel=\"preload\" href=\"/fonts/CommitMono-700-Regular.otf\" as=\"font\" type=\"font/otf\" crossorigin>\n\n<script is:inline defer async src=\"https://buttons.github.io/buttons.js\" />\n\n<OpenPanelComponent clientId={NEXT_PUBLIC_OPENPANEL_CLIENT_ID!} trackScreenViews trackOutgoingLinks trackAttributes  />\n"
  },
  {
    "path": "apps/docs/src/components/Hero.astro",
    "content": "---\nconst { data } = Astro.locals.starlightRoute.entry;\nconst { title = data.title, tagline, actions = [] } = data.hero || {};\n\nimport { LinkButton } from \"@astrojs/starlight/components\";\n---\n\n<div\n  class=\"flex w-full flex-col justify-center gap-1 px-3 py-4 text-center md:p-6\"\n>\n  <div class=\"flex flex-col gap-6\">\n    <h1\n      class=\"font-bold text-4xl md:text-6xl\"\n      data-page-title\n      set:html={title}\n    />\n    {\n      tagline && (\n        <h2\n          class=\"mx-auto font-normal max-w-md text-lg text-muted-foreground md:max-w-xl md:text-xl\"\n          set:html={tagline}\n        />\n      )\n    }\n  </div>\n  <div class=\"my-4 grid gap-2 sm:grid-cols-2\">\n\n    <div class=\"text-center sm:block sm:text-left\">\n\t\t{\n\t\t\tactions.length > 0 && (\n\t\t\t\t<div class=\"sl-flex actions\">\n\t\t\t\t\t{actions.map(\n\t\t\t\t\t\t({ attrs: { class: className, ...attrs } = {}, icon, link: href, text, variant }) => (\n\t\t\t\t\t\t\t<LinkButton {href} {variant} icon={icon?.name} class:list={[className]} {...attrs}>\n\t\t\t\t\t\t\t\t{text}\n\t\t\t\t\t\t\t\t{icon?.html && <Fragment set:html={icon.html} />}\n\t\t\t\t\t\t\t</LinkButton>\n\t\t\t\t\t\t)\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)\n\t\t}\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "apps/docs/src/components/SiteTitle.astro",
    "content": "---\nimport { Image } from \"astro:assets\";\nimport logo from \"../assets/icon.png\"; // Image is 1600x900\n---\n\n\n<a href=\"/\" class=\"flex items-center gap-2 font-bold no-underline text-black dark:text-white text-lg font-cal\">\n    <Image\n      src={logo}\n      alt=\"OpenStatus\"\n      height={30}\n      width={30}\n      class=\"rounded-full border border-border bg-transparent \"\n    />\n    openstatus\n</a>\n"
  },
  {
    "path": "apps/docs/src/components/Status.astro",
    "content": "---\nimport { getStatus, statusDictionary } from \"./utils\";\nconst { status } = await getStatus(\"openstatus\");\nconst { label, color } = statusDictionary[status];\n---\n\n<a\nclass=\"inline-flex max-w-fit items-center gap-2 rounded-md border border-gray-200 px-3 py-1 text-gray-700 text-sm hover:bg-gray-100 hover:text-black dark:border-gray-800 dark:text-gray-300 dark:hover:bg-gray-900 dark:hover:text-white no-underline\"\nhref=\"https://status.openstatus.dev\"\ntarget=\"_blank\"\nrel=\"noreferrer\"\n>\n{label}\n<span class=\"relative flex h-2 w-2\">\n  {status === \"operational\" ? (\n    <span\n      class={`absolute inline-flex h-full w-full animate-ping rounded-full ${color} opacity-75 duration-1000`}\n    />\n  ) : null}\n  <span\n    class={`relative inline-flex h-2 w-2 rounded-full ${color}`}\n  />\n</span>\n</a>\n"
  },
  {
    "path": "apps/docs/src/components/utils.ts",
    "content": "export type Status =\n  | \"operational\"\n  | \"degraded_performance\"\n  | \"partial_outage\"\n  | \"major_outage\"\n  | \"under_maintenance\"\n  | \"unknown\"\n  | \"incident\";\n\ntype StatusResponse = { status: Status };\n\nexport const statusDictionary: Record<\n  Status,\n  { label: string; color: string }\n> = {\n  operational: {\n    label: \"Operational\",\n    color: \"bg-green-500\",\n  },\n  degraded_performance: {\n    label: \"Degraded Performance\",\n    color: \"bg-yellow-500\",\n  },\n  partial_outage: {\n    label: \"Partial Outage\",\n    color: \"bg-yellow-500\",\n  },\n  major_outage: {\n    label: \"Major Outage\",\n    color: \"bg-red-500\",\n  },\n  unknown: {\n    label: \"Unknown\",\n    color: \"bg-gray-500\",\n  },\n  incident: {\n    label: \"Incident\",\n    color: \"bg-yellow-500\",\n  },\n  under_maintenance: {\n    label: \"Under Maintenance\",\n    color: \"bg-blue-500\",\n  },\n} as const;\n\nexport async function getStatus(slug: string): Promise<StatusResponse> {\n  const res = await fetch(`https://api.openstatus.dev/public/status/${slug}`);\n\n  if (res.ok) {\n    const data = (await res.json()) as StatusResponse;\n    return data;\n  }\n\n  return { status: \"unknown\" };\n}\n"
  },
  {
    "path": "apps/docs/src/content/config.ts",
    "content": "import { defineCollection } from \"astro:content\";\nimport { docsLoader } from \"@astrojs/starlight/loaders\";\nimport { docsSchema } from \"@astrojs/starlight/schema\";\n// import { glob } from \"astro/loaders\";\n\nexport const collections = {\n  docs: defineCollection({\n    loader: docsLoader(),\n    // loader: glob({ pattern: \"**/*.mdx\", base: \"./src/content/docs\" }),\n    schema: docsSchema(),\n  }),\n};\n"
  },
  {
    "path": "apps/docs/src/content/docs/404.md",
    "content": "---\ntitle: Not Found\ntemplate: splash\neditUrl: false\nhero:\n  tagline: This page could not be found.\n  actions: []\npagefind: false\nsidebar:\n  hidden: true\ndraft: false\n---\n"
  },
  {
    "path": "apps/docs/src/content/docs/concept/best-practices-status-page.mdx",
    "content": "---\ntitle: Building Trust with Status Pages\ndescription: \"Understanding how to communicate effectively during incidents and build user trust through transparency.\"\n---\n\n\n```\n+------------------------------------------------+\n|               openstatus Status Page           |\n+------------------------------------------------+\n| Service Name      | Status | Uptime            |\n+-------------------+--------+-------------------+\n| Web Server        | ✅ OK       | 99.9%        |\n| Database          | ✅ OK       | 99.8%        |\n| API Gateway       | ⚠️ Degraded | 99.5%        |\n| Monitoring        | ✅ OK       | 100%         |\n| Payment Processing| ✅ OK       | 99.7%        |\n+-------------------+--------+-------------------+\n| Incidents:                                     |\n|                                                |\n|   - Degraded performance on API Gateway due    |\n|     to high traffic. Our team is investigating.|\n+------------------------------------------------+\n```\n\n## The purpose of a status page\n\nA status page is more than just a dashboard of green lights. It's a critical tool for communication and a cornerstone of building trust with your users. Its primary purpose is to provide a single, authoritative source of truth about your service's health and any ongoing incidents.\n\nWhen done right, a status page:\n- **Reduces support burden**: Users can self-serve information about outages instead of contacting your team.\n- **Builds trust**: Proactive transparency, even when things go wrong, demonstrates accountability.\n- **Improves communication**: It provides a central and consistent channel for incident updates.\n- **Demonstrates professionalism**: It shows that you take reliability and user experience seriously.\n\nThis article explores the principles that make a status page an effective tool for building trust.\n\n## Principles of Effective Status Pages\n\n### Maintain Transparency and Honesty\n\nA status page's effectiveness hinges on being a reliable source of truth. Be upfront about issues, even minor ones. Hiding problems erodes user trust and can lead to frustration and a higher support load.\n\n- **Communicate Clearly:** Use simple, non-technical language. Your users shouldn't need a technical dictionary to understand the impact of an issue.\n\n- **Be Timely:** Update the page as soon as an incident is confirmed. Provide regular, predictable updates throughout the resolution process, even if the only update is \"we're still working on it.\"\n\n### Automate Where Possible\n\nManual updates during a high-stress outage are prone to error and can be slow. Automation ensures that your status page reflects reality quickly and accurately.\n\n- **Integrate Monitoring Tools:** Your status page should be directly connected to your internal monitoring and alerting systems. When a metric crosses a threshold (e.g., a high error rate), the status page can be updated automatically to reflect a degraded state.\n\n- **Use an API:** We provide APIs that allow you to programmatically update component statuses and post new incidents, integrating your status page into your incident response workflows.\n\n\n### Provide Context-Rich Incident Communication\n\nWhen an incident occurs, a structured narrative helps users understand the situation.\n\n- **Start with the Impact:** Clearly and concisely state what the problem is from the user's perspective. For example, \"Users are currently unable to log in.\"\n\n- **Explain the Cause (When Known):** Briefly explain the root cause if you've identified it. Transparency here is key.\n\n- **Outline Next Steps and ETA:** Explain what is being done to resolve the issue and provide an estimated time to resolution if possible. It's better to give a conservative estimate or no estimate than to give one you can't meet.\n\nA typical incident communication lifecycle looks like this:\n\n- **Investigating:** \"We're currently investigating an issue affecting user logins.\"\n\n- **Identified:** \"We've identified the root cause as a database connection issue and are working on a fix.\"\n\n- **Monitoring:** \"A fix has been deployed, and we're monitoring the system to ensure stability.\"\n\n- **Resolved:** \"The issue has been resolved. We will publish a post-mortem within 48 hours.\"\n\n\n### Ensure Easy Accessibility\n\nYour status page is useless if no one can find it.\n\n- **Prominent Link:** Link to your status page from your application's footer, your main website, and your support documentation.\n\n- **Custom Domain:** Use a simple, memorable URL like `status.yourcompany.com`.\n\n## Advanced Considerations for Deeper Trust\n\n### Scheduled Maintenance\n\nCommunicating planned downtime is just as important as communicating unexpected incidents.\n- Announce maintenance well in advance (e.g., at least 72 hours).\n- Display upcoming maintenance windows clearly on the status page.\n- Send reminders to subscribers before maintenance begins.\n\n### Historical Data and Post-Mortems\n\nDemonstrate your commitment to reliability by being open about your track record.\n- Display historical uptime percentages (e.g., over the last 30/60/90 days).\n- Link to past incidents and their post-mortems. Being honest about past failures and what you've learned from them is a powerful trust-builder.\n\n### Subscriber Notifications\n\nAllow users to opt-in to the level of communication they want.\n- Email notifications for new incidents and resolutions.\n- SMS for critical alerts (if applicable).\n- RSS/Atom feeds for users who want to integrate your status into their own monitoring.\n\n## Common Pitfalls to Avoid\n\n1. **Claiming unrealistic uptime**: Don't claim 100% uptime unless you can back it up. Honesty is better than perfection.\n2. **Hiding or downplaying incidents**: Users will find out anyway. It's better they hear it from you.\n3. **Using technical jargon**: Write for a broad audience, not just other engineers.\n4. **Leaving users in the dark**: During an incident, regular updates are crucial, even if there's no new information. A simple \"still investigating\" is better than silence.\n5. **Hosting your status page on the same infrastructure**: Your status page must be available even when your main service is down.\n\n## Implementing with openstatus\n\nopenstatus is designed to make implementing these principles straightforward:\n\n- **[Create a status page](/tutorial/how-to-create-status-page)** - Get set up in minutes.\n- **[Configure your page](/tutorial/how-to-configure-status-page)** - Customize its appearance to match your brand.\n- **[Understand uptime calculations](/concept/uptime-calculation-and-values)** - Be transparent about how you measure uptime.\n\n## Next steps\n\n\n- **[Understanding uptime monitoring](/concept/uptime-monitoring)** - Learn more about monitoring what you communicate.\n- **[Status page reference](/reference/status-page)** - Dive into technical configuration options.\n"
  },
  {
    "path": "apps/docs/src/content/docs/concept/getting-started.mdx",
    "content": "---\ntitle: Foundational Concepts\ndescription: \"Understand uptime monitoring, latency vs response time, SLA calculations, and the design decisions behind OpenStatus.\"\nsidebar:\n  label: Concepts Overview\n  order: 1\n---\n\n## Building a solid foundation\n\nThis section of our documentation is dedicated to explanation. It's not about quick fixes or step-by-step instructions. Instead, it's here to help you build a deep understanding of uptime monitoring and the principles behind OpenStatus.\n\nA solid mental model will empower you to use our tools more effectively, make better decisions for your own systems, and communicate with your team and stakeholders with clarity and confidence.\n\nWe'll explore the \"why\" behind the \"what,\" covering core concepts, best practices, and the design philosophy that guides our development.\n\n### Core Concepts\n\nStart here to grasp the fundamental building blocks of uptime monitoring.\n\n- **[Uptime Monitoring](/concept/uptime-monitoring)**: What is it, why does it matter, and how does it work?\n- **[Uptime Calculation and Values](/concept/uptime-calculation-and-values)**: A look under the hood at how uptime percentages are calculated and what they truly represent.\n- **[Latency vs Response Time](/concept/latency-vs-response-time)**: Untangle the difference between these two critical performance metrics.\n\n### Best Practices & Philosophy\n\nLearn from experience and understand our approach to modern monitoring.\n\n- **[Building Trust with Status Pages](/concept/best-practices-status-page)**: How to communicate effectively during incidents and maintain user trust.\n- **[Uptime Monitoring as Code](/concept/uptime-monitoring-as-code)**: The why and how of managing your monitoring configuration in a GitOps workflow.\n\nHave questions or want to discuss these concepts? [Join our community](/help/support/) and share your thoughts!\n"
  },
  {
    "path": "apps/docs/src/content/docs/concept/latency-vs-response-time.mdx",
    "content": "---\ntitle: Understanding Latency vs Response Time\ndescription: \"Deep dive into the difference between latency and response time, and why both matter for monitoring\"\n---\n\n## The confusion\n\nLatency and response time are often used interchangeably, but they measure different things. Understanding the distinction is crucial for effective monitoring and performance optimization.\n\n**The key difference:**\n- **Latency** measures network travel time\n- **Response time** measures total time including server processing\n\nBoth metrics matter, but for different reasons.\n\n## What is latency?\n\nLatency and response time are two different metrics used in uptime monitoring. Latency measures the time it takes for a request to travel from the probes to the server and back. Response time is the time it takes for the server to process the request and send back a response, plus the latency.\n\n```\nopenstatus                  Network                 Server (Website)\n  |                           |                          |\n  |------- Request ---------->|                          |\n  | (Timestamp A: Send)       |                          |\n  |                           |------- Process --------->|\n  |                           | (Server processing time) |\n  |                           |<------- Response --------|\n  |                           | (Timestamp B: Receive)   |\n  |                           |                          |\nLatency = Timestamp B - Timestamp A\n```\n\nLatency is the time it takes for data to travel from its source to its destination. Think of it as the round-trip time (RTT) for a network packet. This delay is influenced by several factors:\n\n- **Distance:** The physical distance between the client and the server. Data traveling across continents will have higher latency than data traveling within the same city.\n\n- **Network Congestion:** When too much data is on the network, it can slow down transmission, similar to a traffic jam on a highway.\n\nTo measure latency, you can monitor endpoints like `/ping` or `/healthcheck` with minimum server processing time.\n\n## What Is Response Time?\n\n```\n    openstatus                 Network                Server\n        |                         |                     |\n(Start) |------- Request -------->|                     |\n(T1)    |                         |                     |\n        |                         |--- Processing ----->|\n        |                         |   (Server's work)   |\n        |                         |<-- Response Data ---|\n        |                         |                     |\n(End)   |<--- (Received) ---------|                     |\n(T2)    |                         |                     |\n\n Response Time = T2 - T1\n```\n\n\nResponse time is the total time from the moment a user's request is sent until the moment the first byte of the server's response is received. It includes both the network latency and the server's processing time.\n\nResponse time = Network Latency + Server Processing Time\n\nThe server processing time is the duration the server spends on tasks like:\n\n- Executing database queries.\n- Running application logic.\n- Generating the HTML or JSON response.\n\nA high response time often indicates a problem with the server-side application itself. For example, slow database queries or inefficient can dramatically increase the response time, even if the network latency is low.\n\n\n##  Why the distinction matters for uptime monitoring\n\nUnderstanding the difference between these two metrics is crucial for diagnosing performance issues.\n\n- If your monitoring shows a **high response time but low latency**, the problem is likely with your server's performance. You should investigate your application's code, database queries, and server resources.\n\n- If both your **latency and response time** are high, the issue is likely network-related. This could be due to a poor connection between the monitoring location and your server, or a broader network issue.\n\n- **Response time is the ultimate measure of user experience** because it reflects the full journey of a request. Users don't just care how fast a packet can get to the server; they care how long it takes to see the results.\n\nBy monitoring both metrics, you can quickly pinpoint whether a performance slowdown is caused by your application or by the network.\n\n## Practical implications\n\n### For monitoring strategy\n\n- **Monitor both metrics**: Don't rely on just one\n- **Set appropriate thresholds**: Latency thresholds should be lower than response time thresholds\n- **Consider geographic factors**: Latency varies by monitoring location\n- **Track trends**: Sudden changes in either metric indicate issues\n\n### For optimization\n\n- **Reduce latency**: Use CDNs, optimize routing, choose closer hosting\n- **Improve response time**: Optimize code, database queries, caching\n- **User location matters**: Users far from your server will always see higher latency\n\n### Common scenarios\n\n**Scenario 1: Consistent latency, variable response time**\n- Indicates server-side performance issues\n- Look at: Database queries, API calls, resource utilization\n\n**Scenario 2: High latency from specific regions**\n- Indicates geographic network issues\n- Solution: Add regional monitoring points or CDN\n\n**Scenario 3: Both metrics degrading**\n- Could be network saturation or DDoS attack\n- Check: Network bandwidth, traffic patterns, security\n\n## What openstatus tracks\n\nopenstatus monitors and displays:\n- **Total response time**: The complete user experience\n- **Detailed timing breakdown**: DNS, TCP, TLS, request, response\n- **Regional differences**: Compare performance across locations\n- **Historical trends**: Identify patterns over time\n\n## Next steps\n\n- **[Create your first monitor](/tutorial/how-to-create-monitor)** - Start tracking these metrics\n- **[Understanding uptime monitoring](/concept/uptime-monitoring)** - Broader monitoring concepts\n- **[HTTP monitor reference](/reference/http-monitor)** - Technical specifications\n"
  },
  {
    "path": "apps/docs/src/content/docs/concept/uptime-calculation-and-values.mdx",
    "content": "---\ntitle: Uptime Calculation and Shared Values\n---\n\nLet’s face it - uptime values can be a complete lie if they’re not properly connected to monitoring.\n\nWe want to make uptime transparent and configurable. You decide how your uptime is calculated and which values you want to share on your status page.\nWhen monitoring an endpoint, a check can end up in one of three states:\n\n- ✅ Success – everything’s fine\n- ⚠️ Degraded – slow or partially failing\n- ❌ Down – no response or full failure\n\nWe now offer multiple types of uptime calculation:\n- **Absolute** (default): derived directly from your monitoring data\n    - **Duration**: aggregated from the incidents duration\n    - **Requests** (default): aggregated from the request values\n- **Manual**: for teams that prefer full control over what’s shown\n\n**TL;DR**\n\n| Type        | Source of Truth      | What Users See                      | Best For                        |\n|-------------|----------------------|-------------------------------------|----------------------------------|\n| **Duration** (Absolute) | Incident duration    | Time based uptime, proportional colors | Accurate long-term view         |\n| **Request** (Absolute)  | Every ping result    | Request-based uptime %              | Real-time reflection            |\n| **Manual**   | Manually set status  | Controlled, narrative updates       | Transparency without monitoring |\n\n<br />\n\nLet’s break them down!\n\n## Absolute Type\n\nThe absolute type calculates uptime based on actual monitoring results. It’s the most accurate reflection of what’s really happening, and comes in two variants: _Duration_ and _Request_.\nBoth of these share real data with your users - incidents, degraded states, and historical uptime - but they differ in how they aggregate and display that data.\n\n---\n\n### Duration\n\nThe duration value is calculated from the **total monitoring time and the duration of incidents**.\n\nIn simple terms: `uptime = (total time - incident duration) / total time`\n\nThis means uptime is based on how long something was down, not how many checks failed. Only incident durations are included in the calculation. \n\nTemporary single-region ping failures (e.g., one location failing once, sometimes this just happens) are not propagated to users - because these often don’t represent a real outage. That’s also why we recommend at least three locations per monitor for redundancy. \n\nThe proportional colors in the status bar are drawn from these duration values. Hovering over a day shows both incidents and status reports, so users can explore what happened.\n\n---\n\n### Request\n\nThe request value is more straightforward - it looks at each ping result individually. **Every check we run contributes to your uptime score**.\n\nIn simple terms: `uptime = (success + degraded - error) / total requests`\n\nThis is the current default mode for most openstatus users. It’s simple, data-driven, and updates immediately as new results come in. \n\nLike with duration, hover cards display incidents and status reports, giving your users a quick overview of recent events.\n\n---\n\n## Manual Type\n\nThe manual type is for teams who want to **fully control what’s shown** on your status page, without relying on automatic checks.\nBy default, your monitor is marked operational. You can then manually create status reports whenever you want to reflect changes - independent of any monitoring data.\n\nThis is ideal if:\n- you don’t have synthetic monitoring set up yet,\n- or you’re sharing updates that aren’t tied to uptime (e.g., service degradation due to external dependencies).\n\nIn this mode, all displayed uptime values and statuses come from your shared report data, not from active pings.\n\nIn simple terms: `uptime = (total duration - status report duration) / total duration`\n\n> **Note**: the values you are defining are attached to a status page. You cannot change them per monitor (for now).\n"
  },
  {
    "path": "apps/docs/src/content/docs/concept/uptime-monitoring-as-code.mdx",
    "content": "---\ntitle: Understanding Monitoring as Code\ndescription: \"Why and how to manage monitoring configuration as code for GitOps workflows\"\n---\nimport { Code } from '@astrojs/starlight/components';\n\n## The traditional approach (and its problems)\n\nTraditionally, monitoring is configured through web dashboards:\n1. Log into a web interface\n2. Click through forms to create monitors\n3. Manually replicate configuration across environments\n4. No audit trail of who changed what\n5. Difficult to review changes before they go live\n\nThis works for small teams with few monitors, but doesn't scale.\n\n## What is monitoring as code?\n\n**Monitoring as Code** treats your monitoring configuration the same way you treat your application code: as text files that can be versioned, reviewed, and deployed through automated pipelines.\n\nInstead of clicking buttons, you define monitors in YAML:\n\nexport const code = `\n# yaml-language-server: $schema=https://www.openstatus.dev/schema.json\nuptime-monitor:\n  name: \"Uptime Monitor\"\n  description: \"Uptime monitoring example\"\n  frequency: \"10m\"\n  active: true\n  regions:\n    - iad\n    - ams\n    - syd\n    - jnb\n    - gru\n  retry: 3\n  kind: http\n  request:\n    url: https://openstat.us\n    method: GET\n    headers:\n      User-Agent: openstatus\n  assertions:\n    - kind: statusCode\n      compare: eq\n      target: 200\n\ngraphql-monitor:\n  name: \"Graphql\"\n  description: \"GitHub GraphQL API\"\n  frequency: \"10m\"\n  active: true\n  regions:\n    - iad\n    - ams\n    - syd\n    - jnb\n    - gru\n  retry: 3\n  kind: http\n  request:\n    url: https://api.github.com/graphql\n    method: POST\n    headers:\n      User-Agent: openstatus\n      Authorization: Bearer YOUR_TOKEN_HERE\n    body: |\n      {\n        \"query\": \"query { viewer { login }}\"\n      }\n`\n\nUptime monitoring is a vital part of any robust system, ensuring your services are online and available to users. Historically, this has involved manually configuring monitors through a web interface, which can be tedious and prone to human error. Uptime Monitoring as Code changes this by treating your monitoring configurations like any other part of your application-as code.\n\n\n## Why Use Uptime Monitoring as Code?\n\nThis approach offers significant advantages:\n\n- **Version Control:** By defining your monitors in a YAML file, you can track every change, rollback to previous versions, and see who made which modifications using tools like Git. This is crucial for auditing and troubleshooting.\n\n- **Automation and Consistency:** Your monitoring setup can be part of your automated deployment pipeline. When you deploy a new service, its monitors are created automatically, ensuring consistency across your entire infrastructure. This eliminates the risk of forgetting to set up monitoring for a new service.\n\n- **Collaboration:** A code-based approach simplifies collaboration among teams. A developer can create a new monitor definition in the YAML file and submit it for peer review, just as they would with any other code change. This promotes a shared understanding of your system's health.\n\n- **Scalability:** Manually setting up hundreds of monitors is a nightmare. With a code-based approach, you can programmatically generate configurations for a large number of services, making it easy to scale your monitoring as your infrastructure grows.\n\n- **Simplified Auditing:** Since the entire configuration is in a file, it's easy to see the current state of your monitors at a glance. You don't have to navigate through multiple screens in a web UI.\n\n\n## How It Works with openstatus\n\n We offer the use ofa simple, human-readable YAML file to define all uptime monitors. This file serves as the single source of truth for your monitoring setup. You define each monitor with its URL, expected status code, and other parameters.\n\nHere’s an example of what your `openstatus.yaml` file might look like:\n\n<Code code={code} lang=\"yaml\" title='openstatus.yaml' />\n\n### Making Changes with the CLI\n\nOnce your `openstatus.yaml` file is ready, you use our [command-line interface (CLI)](/tutorial/get-started-with-openstatus-cli) to apply the changes. The CLI compares your local configuration with the current state of your monitors and applies only the necessary changes creating new monitors, updating existing ones, or deleting those no longer defined.\n\n**Common CLI Commands:**\n\n- `openstatus monitors apply`: Applies the changes defined in your `openstatus.yaml` file.\n- `openstatus monitors import`: Import the monitors from your dashboard to a new `openstatus.yaml` file.\n\nBy integrating this **Uptime Monitoring as Code** workflow into your development lifecycle, you can achieve a more reliable, consistent, and scalable system. It's about moving from manual clicks to automated, version-controlled operations.\n\n## Best practices\n\n1. **Start simple**: Begin with a few monitors, expand as you learn\n2. **Use templates**: Create reusable patterns for common monitor types\n3. **Environment variables**: Use secrets management for tokens and sensitive data\n4. **Review changes**: Always review diffs before applying\n5. **Document decisions**: Use commit messages to explain \"why\"\n\n## Next steps\n\nReady to implement monitoring as code?\n\n- **[Get Started with CLI](/tutorial/get-started-with-openstatus-cli)** - Install and configure the CLI\n- **[Monitor Your MCP Server](/guides/how-to-monitor-mcp-server/)** - Real-world example\n- **[CLI Reference](/reference/cli-reference)** - Complete command documentation\n- **[YAML Examples](https://github.com/openstatusHQ/cli-template)** - Sample configurations\n"
  },
  {
    "path": "apps/docs/src/content/docs/concept/uptime-monitoring.mdx",
    "content": "---\ntitle: Understanding Uptime Monitoring\ndescription: A deep dive into uptime monitoring concepts, architecture, and best practices\n---\n\n## What is uptime monitoring?\n\nUptime monitoring is the practice of continuously asking a simple question: \"Is our service working correctly for our users?\" It's a systematic, automated process for checking the availability, performance, and correctness of a website, server, or application.\n\nInstead of waiting for users to report a problem, uptime monitoring acts as a proactive defense, alerting you the moment an issue arises. This helps you minimize downtime and protect your reputation.\n\nAt its core, uptime monitoring answers three critical questions:\n1.  **Is my service available?** Can users reach it?\n2.  **Is it performing well?** Are response times fast enough?\n3.  **Is it functioning correctly?** Is it returning the expected data and behaving as intended?\n\n\n```\n+----------------+\n| Service to be  |\n| Monitored      |\n+----------------+\n      ▲\n      |\n      |   (Network Latency)\n      |\n+-----+-------+   +-----+-------+   +-----+-------+\n| Monitoring  |   | Monitoring  |   | Monitoring  |\n| Node (USA)  |   | Node (EU)   |   | Node (Asia) |\n+-----+-------+   +-----+-------+   +-----+-------+\n      |               |                 |\n      |---------------|-----------------|\n      ▼               ▼                 ▼\n+------------------------------------------------+\n|       Global Uptime Monitoring Service         |\n|                                                |\n| - Sends automated requests (e.g., pings or     |\n|   HTTP checks) from all nodes at set intervals |\n| - Records response time and success/failure    |\n| - Compares results from different nodes        |\n| - If a failure or a slow response is detected, |\n|   it triggers an alert.                        |\n+------------------------------------------------+\n      |\n      | (Alerts: Email, SMS, Slack, etc.) 🔔\n      |\n+-----+-----+\n| Your Team |\n+-----------+\n```\n\n\n## Key Concepts\n\n- **Downtime**: The period when a service is unavailable or not functioning as expected. It can be caused by server failures, network issues, software bugs, or even cyberattacks.\n\n- **Uptime**: The percentage of time a service is available and operational. A high uptime percentage (e.g., 99.9% or \"three nines\") indicates reliability.\n\n- **Alerting**: The system of notifying a team or individual when downtime is detected. Alerts can be sent via email, SMS, Slack, or other communication channels.\n\n## Why Uptime Monitoring is Crucial\n\n- **Business Continuity**: Downtime can lead to significant financial losses, damage to reputation, and loss of customer trust. Uptime monitoring helps you address issues quickly, ensuring your services are always available to your users.\n\n- **Performance Insight**: Monitoring tools often provide data on latency and response times, giving you insights into your service's performance beyond just availability. This can help you optimize your infrastructure and user experience.\n\n- **Proactive Problem Solving**: Instead of waiting for a customer to report an issue, uptime monitoring allows you to be the first to know about it. This enables you to troubleshoot and resolve problems before they escalate.\n\n## How it Works\n\nUptime monitoring typically involves a monitoring agent that periodically sends a request (like an HTTP GET request) to your service.\n\n- If the service responds with a successful status code (e.g., 200 OK), it's considered up.\n\n- If the service returns an error code, a timeout, or no response, the agent will perform a re-check from a different location to confirm the outage. This helps prevent false alarms caused by temporary network glitches.\n\n- Upon confirmation, the system triggers an alert, notifying the relevant team members. The monitoring system will continue to check the service until it's back online, at which point a recovery alert is often sent.\n\nCommon types of checks include:\n\n- **HTTP/HTTPS checks:** Verify a website is accessible and returns a valid response.\n- **TCP checks:** Confirm a server is reachable on the network.\n\n\n\n## Planning an Uptime Monitoring System\n\nWhen planning your own uptime monitoring system, consider the following:\n\n1.  Define What to Monitor: Identify all critical services, websites, APIs, and servers that need to be monitored. Prioritize based on business impact.\n\n2. Select a Monitoring Tool: Choose a tool that fits your needs. Options range from simple free services to complex enterprise-level platforms. Look for features like:\n\n    - **Multiple locations:** Checks from various geographic regions to ensure global availability.\n\n    - **Customizable alerting:** Set up different alert thresholds and notification methods.\n\n    - **Reporting and dashboards:** Visualize uptime history, performance metrics, and incident reports.\n\n    - **Integrations:** Connect with your existing tools like Slack, PagerDuty, or email.\n\n3. **Establish Alerting Rules:** Determine who should be notified and when. Set up an escalation policy, for example, if a primary on-call engineer doesn't respond within 15 minutes, the alert is sent to a manager.\n\n4. **Regularly Review and Optimize:** Monitor your monitoring system itself. Review historical data to identify recurring issues, fine-tune alert thresholds, and update your list of monitored services as your infrastructure evolves.\n\n## The human factor\n\nWhile uptime monitoring is largely automated, it's important to remember the human aspects:\n\n- **Alert fatigue**: Too many false positives can lead teams to ignore alerts. Fine-tune your monitoring to reduce noise.\n- **On-call burden**: Distribute monitoring responsibilities fairly and ensure adequate coverage.\n- **Communication**: During incidents, clear communication with users is as important as technical fixes.\n- **Post-mortems**: Learn from downtime by conducting blameless post-mortems.\n\n## Monitoring philosophy\n\nDifferent approaches to monitoring reflect different philosophies:\n\n- **Optimistic monitoring**: Assume everything is working unless proven otherwise. Alert on failures.\n- **Pessimistic monitoring**: Assume nothing works unless actively verified. Alert on missing data.\n- **SLI/SLO based**: Monitor Service Level Indicators against defined Service Level Objectives.\n\nopenstatus supports all these approaches, letting you choose what works best for your team.\n\n## Beyond basic availability\n\nModern uptime monitoring goes beyond simple \"up or down\" checks:\n\n- **Performance monitoring**: Track response times and identify degradation before outages.\n- **Geographic monitoring**: Verify availability from multiple regions to catch regional issues.\n- **Synthetic monitoring**: Simulate user journeys to catch functional issues.\n- **Real user monitoring (RUM)**: Complement synthetic checks with actual user experience data.\n\n## Next steps\n\nNow that you understand uptime monitoring concepts:\n\n- **[Get started with a monitor](/tutorial/how-to-create-monitor)** - Apply these concepts in practice\n- **[Learn about uptime calculations](/concept/uptime-calculation-and-values)** - Understand how uptime percentages work\n- **[Building Trust with Status Pages](/concept/best-practices-status-page)** - Communicate effectively during incidents\n- **[Monitoring as Code](/concept/uptime-monitoring-as-code)** - Manage monitoring configuration programmatically\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/getting-started.mdx",
    "content": "---\ntitle: How-to Guides\ndescription: \"Practical guides for integrating OpenStatus with GitHub Actions, OTLP endpoints, Cloudflare, and more.\"\nsidebar:\n    label: Guides Overview\n    order: 1\n---\n\n## 🛠️ How-to Guides\n\n### What you'll find here\n\nOur how-to guides are designed to help you:\n- Solve specific problems with step-by-step instructions\n- Implement advanced features and integrations\n- Customize and extend your openstatus setup\n\n### Monitoring & Integration\n\nExtend your monitoring capabilities:\n\n- **[Monitor Your MCP Server](/guides/how-to-monitor-mcp-server/)** - Set up monitoring for Model Context Protocol servers\n- **[Export Metrics to OTLP Endpoint](/guides/how-to-export-metrics-to-otlp-endpoint)** - Send monitoring data to your observability platform\n- **[Run Synthetic Tests in GitHub Actions](/guides/how-to-run-synthetic-test-github-action/)** - Automate testing in your CI/CD pipeline\n\n### Status Pages & Widgets\n\nShare your status with users:\n\n- **[Add SVG Status Badge to GitHub README](/guides/how-to-add-svg-status-badge)** - Display real-time status in your repository\n- **[Use React Status Widget](/guides/how-to-use-react-widget)** - Embed live status updates in your React application\n- **[Deploy Status Page on Cloudflare Pages](/guides/how-deploy-status-page-cf-pages)** - Host your status page on Cloudflare's edge network\n\n### Infrastructure & Deployment\n\nSelf-host and customize your setup:\n\n- **[Deploy Private Locations on Cloudflare Containers](/guides/how-to-deploy-probes-cloudflare-containers)** - Run monitoring agents in your own infrastructure\n- **[Self-Host openstatus](/guides/self-hosting-openstatus)** - Deploy openstatus on your own servers\n- **[Self-Host Status Page Only](/guides/self-host-status-page-only)** - Deploy only the status page without monitoring\n\n### Related sections\n\n- **[Tutorials](/tutorial/getting-started/)** - If you need step-by-step learning instead\n- **[Explanations](/concept/getting-started/)** - To understand the concepts behind these guides\n- **[Reference](/reference)** - For detailed technical specifications\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/how-deploy-status-page-cf-pages.mdx",
    "content": "---\ntitle: How to Deploy a Status Page to Cloudflare Pages\ndescription: Learn how to use openstatus monitoring data to deploy a status page on Cloudflare Pages.\nsidebar:\n  label: Host your status page on Cloudflare Pages\n---\nimport { Image } from 'astro:assets';\nimport statusPage from '../../../assets/guides/how-deploy-status-page-cf-pages/status-page.png';\n\n## Problem\n\nYou need a fast, reliable, and automated status page, but you don't want to manage the hosting infrastructure. Manually updating a status page during an incident is inefficient and error-prone.\n\n## Solution\n\nDeploy a custom status page to Cloudflare's global edge network using openstatus as the data source. This guide shows you how to use our Astro-based template to create a status page that automatically reflects your monitoring data.\n\nThe code for the template is available on [GitHub](https://github.com/openstatusHQ/astro-status-page).\n\n<Image\n  src={statusPage}\n  alt=\"Astro Status Page\"\n/>\n\n## Prerequisites\n\n- A Cloudflare account\n- An [openstatus account](https://www.openstatus.dev) with at least one monitor configured.\n\n## Step-by-step guide\n\n### 1. Get your openstatus API key\n\nFirst, you need an API key to fetch your monitoring data.\n\n1.  Navigate to your openstatus dashboard.\n2.  Go to **Settings** → **API Token**.\n3.  Click **Create API Key** and copy the key.\n\n### 2. Set up the Astro project\n\nClone our status page template and install the dependencies.\n\n```bash\ngit clone https://github.com/openstatusHQ/astro-status-page.git\ncd astro-status-page\nnpm install\n```\n\n### 3. Customize the status page\n\nYou need to specify which monitors to display on your page.\n\n1.  Open the `src/pages/index.astro` file.\n2.  Find the following line of code:\n    ```javascript\n    const monitorIds = [1]\n    ```\n3.  Replace the `1` with the ID of the monitor you want to display. You can find the monitor ID in the URL when you view a monitor in the openstatus dashboard (`/monitors/[ID]`). You can also add multiple IDs: `[1, 2, 5]`.\n\n### 4. Configure your Cloudflare environment variable\n\nBefore deploying, you must provide your openstatus API key to Cloudflare.\n\n1.  Go to your Cloudflare dashboard and click on **Workers & Pages**.\n2.  Select your site and go to the **Settings** tab.\n3.  Navigate to **Environment variables** and add a new variable:\n    -   **Variable name**: `API_KEY`\n    -   **Value**: Paste your openstatus API key here.\n\n### 5. Deploy to Cloudflare Pages\n\nNow you can deploy your status page.\n\n```bash\nnpm run pages:deploy\n```\n\nAfter the command completes, your status page will be live on Cloudflare Pages. 🎉\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/how-to-add-svg-status-badge.mdx",
    "content": "---\ntitle: How to Add a Status Badge to a GitHub README\ndescription: A step-by-step guide to adding a real-time SVG or PNG status badge to your GitHub repository.\n---\n\n## Problem\n\nYou want to display the real-time status of your service directly in your GitHub repository's README file. This provides immediate visibility to your users and team members about the health of your application.\n\n## Solution\n\nopenstatus provides embeddable status badges that you can add to any Markdown file, including your `README.md`. You can choose between a modern SVG badge or a legacy PNG badge.\n\n## Prerequisites\n\n-   An openstatus account with a configured status page.\n-   The \"slug\" of your status page (the unique name in its URL, e.g., `https://[slug].openstatus.dev`).\n\n## Step-by-step guide\n\n### Step 1: Choose your badge type\n\nWe recommend using the modern SVG badge (v2) as it offers more customization options.\n\n-   **SVG Badge (v2)**: More flexible, better styling, and uses a monospaced font.\n-   **PNG Badge (Legacy)**: Simpler, but with fewer customization options.\n\n### Step 2: Add the badge to your README\n\nCopy the Markdown snippet for your chosen badge type and paste it into your `README.md` file. **Remember to replace `[slug]` with your status page slug.**\n\n---\n\n#### Option A: Modern SVG Badge (Recommended)\n\nThis is the recommended badge for most use cases.\n\n##### Base URL\n\n```\nhttps://[slug].openstatus.dev/badge/v2\n```\n\n##### Markdown Snippet\n\n```\n![Status](https://[slug].openstatus.dev/badge/v2)\n```\n\n**Example:**\n\n![Status](https://status.openstatus.dev/badge/v2)\n\n##### Customization\n\nYou can customize the badge by adding query parameters to the URL.\n\n-   **Theme**: `?theme=dark` (default is `light`)\n-   **Size**: `?size=md` (options: `sm`, `md`, `lg`, `xl`; default is `sm`)\n-   **Variant**: `?variant=outline` (adds a border; default has no border)\n\n**Example with all options:**\n\n```\n![Status](https://[slug].openstatus.dev/badge/v2?theme=dark&size=lg&variant=outline)\n```\n\n---\n\n#### Option B: Legacy PNG Badge\n\nUse this badge if you prefer the older style.\n\n##### Base URL\n\n```\nhttps://[slug].openstatus.dev/badge\n```\n\n##### Markdown Snippet\n\n```markdown\n![Status](https://[slug].openstatus.dev/badge)\n```\n\n**Example:**\n\n![Status](https://status.openstatus.dev/badge)\n\n##### Customization\n\n-   **Theme**: `?theme=dark` (default is `light`)\n-   **Size**: `?size=lg` (options: `sm`, `md`, `lg`, `xl`; default is `sm`)\n\n**Example with all options:**\n\n```\n![Status](https://[slug].openstatus.dev/badge?theme=dark&size=lg)\n```\n\n### Step 3: Commit your changes\n\nSave your `README.md` file and commit it to your repository. The status badge will now be visible to anyone visiting your repository. It will automatically update to reflect the current status of your services.\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/how-to-deploy-probes-cloudflare-containers.mdx",
    "content": "---\ntitle: How to Deploy a Private Probe on Cloudflare Containers\ndescription: A step-by-step guide to deploying an openstatus private monitoring probe on Cloudflare Containers.\nsidebar:\n  label: Deploy private probes on Cloudflare Containers\n---\nimport { Image } from 'astro:assets';\nimport log from '../../../assets/guides/how-to-deploy-probes-cf-containters/cloudflare-log.png';\nimport os from '../../../assets/guides/how-to-deploy-probes-cf-containters/private-location.jpg';\n\n\n## Problem\n\nYou need to monitor internal services that are not accessible from the public internet, or you want to run checks from your own infrastructure for compliance or performance reasons. You need a lightweight, serverless way to run these monitoring probes without managing virtual machines.\n\n## Solution\n\nDeploy the openstatus private location probe as a serverless container using Cloudflare Containers. This allows you to run monitoring checks from within your own network infrastructure, managed by Cloudflare. This guide will walk you through the entire process, from creating the private location in openstatus to deploying the container.\n\nThe code for the Cloudflare Worker template is available on [GitHub](https://github.com/openstatusHQ/private-location-cloudflare-container).\n\n## Prerequisites\n- A Cloudflare account\n- An [openstatus account](https://www.openstatus.dev)\n- `pnpm` and `docker` installed on your local machine\n\n## Step-by-step guide\n\n### 1. Create a private location in openstatus\n\nFirst, you need to create a private location in your openstatus workspace to get an access key.\n\n1.  Go to the openstatus dashboard.\n2.  Click on **Private locations** in the sidebar.\n3.  Click **Create Private Location**.\n4.  Give it a human-readable name (e.g., \"Cloudflare-EU\").\n5.  Copy the generated token and save it somewhere secure.\n6.  Click **Submit** to save the new private location.\n\n### 2. Set up the Cloudflare project\n\nNext, create a new Cloudflare project using the containers template.\n\n```bash\npnpm create cloudflare@latest --template=cloudflare/templates/containers-template\n```\n\n### 3. Pull and tag the probe Docker image\n\nPull the official openstatus private location image from Docker Hub. You must specify the `linux/amd64` platform, as this is what Cloudflare Containers supports.\n\n```bash\n# Pull the image\ndocker pull --platform linux/amd64 ghcr.io/openstatushq/private-location:latest\n\n# Tag the image for Cloudflare (you cannot use the 'latest' tag)\ndocker tag ghcr.io/openstatushq/private-location:latest openstatus-private-location:v1\n```\n\n### 4. Push the image to Cloudflare Container Registry\n\nPush the tagged image to your Cloudflare account's container registry.\n\n```bash\npnpm wrangler containers push openstatus-private-location:v1\n```\n\n### 5. Configure `wrangler.toml`\n\nNow, configure your Cloudflare project to use the container and run it on a schedule.\n\n1.  Open the `wrangler.toml` file.\n2.  Add a `[containers]` section to link the image you pushed. Replace `GENERATED_ID` with the actual ID from the previous step's output.\n    ```toml\n    [containers]\n    image = \"registry.cloudflare.com/GENERATED_ID/openstatus-private-location:v1\"\n    ```\n3.  Add a `triggers` section to run the worker on a cron schedule. This keeps the container alive, as Cloudflare Containers automatically scales to zero.\n    ```toml\n    triggers = { cron = [\"*/2 * * * *\"] } # Runs every 2 minutes\n    ```\n\n### 6. Configure the worker\n\nUpdate the worker script (`index.ts`) to start the container with the correct environment variables.\n\n1.  Set the `sleepAfter` value to control how long the container runs after being invoked.\n    ```typescript\n    sleepAfter = \"150s\";\n    ```\n2.  Update the `scheduled` function to pass your openstatus key to the container.\n    ```typescript\n    async scheduled(_controller: any, env: Env) {\n      try {\n        const container = getContainer(env.MY_CONTAINER);\n        await container.start({\n          envVars: {\n            OPENSTATUS_KEY: env.OPENSTATUS_KEY,\n          },\n        });\n      } catch (e) {\n        console.error(\"Error in scheduled task:\", e);\n      }\n    \n      return new Response(\"ok\");\n    },\n    ```\n\n### 7. Add your openstatus key as a secret\n\nSecurely provide your private location token to the Cloudflare worker.\n\n```bash\n# Paste the token you saved in Step 1 when prompted\npnpm wrangler secret put OPENSTATUS_KEY\n```\n\n### 8. Deploy the application\n\nFinally, deploy the worker and container to Cloudflare.\n\n```bash\npnpm wrangler deploy\n```\n\nYour private location probe is now running on Cloudflare Containers and will start picking up monitoring jobs from your openstatus workspace.\n\n## Verify the deployment\n\nYou can check the logs of your Cloudflare Worker to see the probe in action. In your openstatus dashboard, the private location should now show as connected.\n\n<Image\n    src={log}\n    alt=\"Cloudflare Workers Logs showing openstatus Private Location running\"\n    caption=\"Cloudflare Workers logs showing the private probe running.\"\n  />\n\n<Image\n    src={os}\n    alt=\"openstatus Private Location connected\"\n    caption=\"The private location showing as 'Connected' in the openstatus dashboard.\"\n  />\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/how-to-export-metrics-to-otlp-endpoint.mdx",
    "content": "---\ntitle: How to Export Metrics to an OTLP Endpoint\ndescription: A step-by-step guide to sending openstatus metrics to your observability platform via OTLP.\n---\nimport { Image } from 'astro:assets';\n\nimport grafana  from '../../../assets/guides/how-to-export-metrics-via-otel/grafana.png';\nimport newrelic  from '../../../assets/guides/how-to-export-metrics-via-otel/newrelic.jpg';\nimport signoz  from '../../../assets/guides/how-to-export-metrics-via-otel/signoz.jpg';\nimport honeycomb    from '../../../assets/guides/how-to-export-metrics-via-otel/honeycomb.jpg';\n\n## Problem\n\nYou want to analyze your openstatus monitoring data alongside other telemetry data in your existing observability platform (like Grafana, New Relic, or Honeycomb). You need a standardized way to export these metrics without building a custom integration.\n\n## Solution\n\nopenstatus can export monitoring metrics to any OTLP (OpenTelemetry Protocol) compatible endpoint. By adding a simple configuration to your `openstatus.yaml` file, you can have metrics from every check sent directly to your monitoring stack.\n\n## Prerequisites\n\n- An observability platform that supports OTLP metric ingestion over HTTP.\n- An `openstatus.yaml` file to configure your monitors.\n- The [openstatus CLI](/tutorial/get-started-with-openstatus-cli) to apply your configuration.\n\n## Step-by-step guide\n\n### 1. Locate your OTLP endpoint URL and headers\n\nFirst, you need to find the specific URL and any required authentication headers from your observability platform. This is usually found in the documentation under \"OTLP,\" \"OpenTelemetry,\" or \"Metrics Export.\"\n\n-   **Endpoint URL**: Look for an HTTP endpoint for OTLP metrics. It will typically end in `/v1/metrics`. For example: `https://otlp.your-provider.com/v1/metrics`.\n-   **Headers**: You will likely need an authentication header, such as `Authorization: Bearer YOUR_API_KEY` or `X-API-Key: YOUR_API_KEY`.\n\n### 2. Configure your `openstatus.yaml` file\n\nOpen your `openstatus.yaml` file and add the `openTelemetry` block at the top level.\n\n```yaml\n# yaml-language-server: $schema=https://www.openstatus.dev/schema.json\n\nopenTelemetry:\n  endpoint: <YOUR_OTLP_ENDPOINT_URL>\n  headers:\n    Authorization: Bearer <YOUR_TOKEN>\n    # Add any other required headers here\n\n# Your monitors are defined below\nmy-first-monitor:\n  # ...\n```\n\nReplace `<YOUR_OTLP_ENDPOINT_URL>` and `<YOUR_TOKEN>` with the values you found in step 1.\n\n**Note**: Currently, we only support OTLP over HTTP.\n\n### 3. Apply the configuration\n\nUse the openstatus CLI to apply the changes to your account.\n\n```bash\nopenstatus apply\n```\n\nAfter applying the configuration, openstatus will send metrics to your specified endpoint after every check is completed.\n\n### 4. Verify in your observability platform\n\nGo to your observability platform and look for the new metrics coming from openstatus. You should be able to build dashboards and alerts based on this data.\n\nHere are some examples of what it can look like:\n\n#### Grafana\n\n<Image\n  src={grafana}\n  alt=\"openstatus metrics in grafana\"\n/>\n\n#### Honeycomb\n\n<Image\n  src={honeycomb}\n  alt=\"openstatus metrics in honeycomb\"\n/>\n\n#### New Relic\n\n<Image\n  src={newrelic}\n  alt=\"openstatus metrics in new-relic\"\n/>\n\n#### SigNoz\n\n<Image\n  src={signoz}\n  alt=\"openstatus metrics in signoz\"\n/>\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/how-to-monitor-mcp-server.mdx",
    "content": "---\ntitle: How to Monitor Your Model Context Provider (MCP) Server\ndescription: Learn how to monitor your MCP server with openstatus using JSON-RPC ping checks\nsidebar:\n  label: Monitor your MCP Server\n---\n\n## Problem: Ensuring Your MCP Server is Always Responsive\n\nRunning a Model Context Provider (MCP) server is critical for your AI applications, but traditional HTTP monitoring often falls short. MCP servers communicate using the JSON-RPC 2.0 protocol, requiring specific request/response patterns that standard health checks don't cover. How can you confidently ensure your MCP server is healthy and responsive at all times, without custom scripts or complex setups?\n\n## Solution: JSON-RPC Ping Monitoring with openstatus\n\nopenstatus offers a robust solution for monitoring your MCP servers. By sending precise JSON-RPC `ping` requests to your endpoint from multiple global locations, openstatus verifies not only network reachability but also the correct functioning of your server's JSON-RPC interface. This guide will walk you through setting up comprehensive monitoring for any MCP server using the openstatus CLI.\n\n## Prerequisites\n\nBefore you begin, ensure you have:\n\n-   An [openstatus account](https://www.openstatus.dev/app/login).\n-   The [openstatus CLI installed](/tutorial/get-started-with-openstatus-cli). (If you haven't installed it yet, follow this guide first).\n-   An MCP server with a publicly accessible endpoint.\n-   A basic understanding of the [JSON-RPC 2.0 protocol](https://www.jsonrpc.org/specification).\n\n## Background: Understanding MCP for Monitoring\n\nA Model Context Provider (MCP) is a crucial component that extends AI models with external context, data, or capabilities via the **Model Context Protocol (MCP)**. Essentially, it acts as a bridge, allowing AI models to interact with resources like databases, APIs, or file systems that aren't part of their core training.\n\n### The MCP Server and JSON-RPC 2.0\n\n## Step-by-step guide\n\n### 1. Create your `openstatus.yaml` file\n\nopenstatus allows you to define and manage your monitors using a YAML configuration file, which is ideal for GitOps workflows. This approach ensures your monitoring setup is version-controlled, auditable, and easily deployable.\n\nCreate a file named `openstatus.yaml` and add the following configuration, adapting it for your own MCP endpoint. This example targets a Hugging Face MCP server.\n\n```yaml\n# yaml-language-server: $schema=https://www.openstatus.dev/schema.json\n\nmcp-server:\n  name: \"HF MCP Server\"\n  description: \"Hugging Face MCP server monitoring\"\n  frequency: \"1m\"\n  active: true\n  regions: [\"iad\", \"ams\", \"lax\"]\n  retry: 3\n  kind: http\n  request:\n    url: https://hf.co/mcp\n    method: POST\n    body: >\n      {\n        \"jsonrpc\": \"2.0\",\n        \"id\": \"openstatus\",\n        \"method\": \"ping\"\n      }\n    headers:\n      User-Agent: openstatus\n      Accept: application/json, text/event-stream\n      Content-Type: application/json\n  assertions:\n    - kind: statusCode\n      compare: eq\n      target: 200\n    - kind: textBody\n      compare: eq\n      target: '{\"result\":{},\"jsonrpc\":\"2.0\",\"id\":\"openstatus\"}'\n```\n\n### 2. Understand the configuration\n\nLet's break down the key fields in this YAML configuration:\n\n-   `name` & `description`: A human-readable name and explanation for your monitor.\n-   `frequency`: How often openstatus will run the check (e.g., `1m`, `5m`, `10m`).\n-   `regions`: An array of geographic regions from which to perform checks (e.g., `[\"iad\", \"ams\", \"lax\"]`). Monitoring from multiple regions helps detect localized issues.\n-   `retry`: The number of times to retry a failed check before marking it as down.\n-   `kind`: Must be `http` for MCP servers.\n-   `request`:\n    -   `url`: The full URL of your MCP server's JSON-RPC endpoint.\n    -   `method`: Must be `POST` for JSON-RPC requests.\n    -   `body`: The JSON-RPC `ping` request payload.\n    -   `headers`: Standard HTTP headers for JSON-RPC communication.\n-   `assertions`: Rules to validate the server's response.\n    -   `statusCode`: Ensures the HTTP response is `200 OK`.\n    -   `textBody`: Verifies that the response payload exactly matches the expected JSON-RPC `ping` result.\n\n### 3. Test your MCP server (Optional)\n\nBefore deploying your monitor, you can manually test your MCP server's `ping` endpoint with `curl` to confirm it responds as expected. This helps verify the `target` value for your `textBody` assertion.\n\n```bash\ncurl -X POST \\\\\n  -H \"Content-Type: application/json\" \\\\\n  -d '{\"jsonrpc\": \"2.0\", \"id\": \"openstatus\", \"method\": \"ping\"}' \\\\\n  https://hf.co/mcp # Replace with your MCP server URL\n```\n\nA healthy server should return a JSON response like `{\"result\":{},\"jsonrpc\":\"2.0\",\"id\":\"openstatus\"}`.\n\n### 4. Deploy your monitor\n\nOnce your `openstatus.yaml` file is ready, use the openstatus CLI to create the monitor:\n\n```bash\nopenstatus create openstatus.yaml\n```\n\nThis command uploads your configuration, and monitoring will begin immediately.\n\n## Conclusion: Comprehensive MCP Server Monitoring Achieved\n\nThis guide has equipped you with the knowledge to effectively monitor your MCP server using openstatus. By leveraging YAML configuration and the openstatus CLI, you can ensure your critical AI infrastructure remains healthy and responsive.\n\n## What You've Accomplished\n\n-   ✅ Successfully configured a JSON-RPC based monitor for your MCP server.\n-   ✅ Implemented precise assertions to validate `ping` responses.\n-   ✅ Set up global monitoring to detect localized or widespread issues.\n-   ✅ Automated monitor deployment using a version-controlled YAML configuration.\n\n## Next Steps\n\nNow that your MCP server is under robust monitoring, consider further enhancing your setup:\n\n\n-   **[Export Metrics to OTLP](/guides/how-to-export-metrics-to-otlp-endpoint)**: Integrate your MCP monitoring data with your existing observability platform for centralized analytics.\n-   **[Run Synthetic Tests in GitHub Actions](/guides/how-to-run-synthetic-test-github-action/)**: Incorporate these synthetic checks into your CI/CD pipeline for pre-deployment validation.\n\n## Related Resources\n\n-   **[JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)**: Deep dive into the JSON-RPC protocol.\n-   **[MCP Official Documentation](https://modelcontextprotocol.io/docs/concepts/architecture#debugging-and-monitoring)**: Official insights into MCP health checks.\n-   **[HTTP Monitor Reference](/reference/http-monitor)**: Comprehensive API reference for HTTP monitors.\n-   **[CLI Reference](/reference/cli-reference)**: Full documentation for the openstatus CLI.\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/how-to-run-synthetic-test-github-action.mdx",
    "content": "---\ntitle: How to Run Synthetic Tests in GitHub Actions\ndescription: Integrate openstatus synthetic monitoring into your CI/CD pipeline\nsidebar:\n  label: GitHub Action for Synthetics\n---\n\n## Problem\n\nYou want to validate that your application's critical endpoints are working before deploying to production. Running synthetic tests in your CI/CD pipeline catches issues early and prevents broken deployments.\n\n## Solution\n\nopenstatus provides a GitHub Action that runs your configured monitors as part of your CI/CD workflow. This guide shows you how to set it up.\n\n## Prerequisites\n\n- An [openstatus](https://www.openstatus.dev) account\n- A GitHub repository\n- At least one monitor configured in openstatus\n- Admin access to your GitHub repository (for secrets)\n\n## Step-by-step guide\n\n### 1. Create a configuration file\n\nCreate a file named `openstatus.config.yaml` in your repository root:\n\n```yaml\ntests:\n  ids:\n    - 1\n    - 2\n```\n\n**Finding monitor IDs:**\n1. Go to your openstatus dashboard\n2. Click on a monitor\n3. The ID is in the URL: `https://www.openstatus.dev/app/[workspace]/monitors/[ID]`\n\n**Tip:** Start with your most critical monitors and expand from there.\n\n### 2. Get your openstatus API key\n\n1. Go to your openstatus workspace settings\n2. Navigate to the API section\n3. Create a new API key or copy an existing one\n4. Store it securely - you'll need it for the next step\n\n### 3. Add your API key to GitHub Secrets\n\nSecure your API key as a GitHub secret:\n\n1. Go to your GitHub repository\n2. Click **Settings** → **Secrets and variables** → **Actions**\n3. Click **New repository secret**\n4. Name: `OPENSTATUS_API_KEY`\n5. Value: Your openstatus API key\n6. Click **Add secret**\n\n### 4. Create the GitHub Action workflow\n\nCreate `.github/workflows/openstatus.yml`:\n\n```yaml\nname: Run openstatus Synthetics CI\n\non:\n  workflow_dispatch:  # Manual trigger\n  push:\n    branches: [ main ]  # Trigger on push to main\n  pull_request:        # Run on PRs (optional)\n\njobs:\n  synthetic_ci:\n    runs-on: ubuntu-latest\n    name: Run openstatus Synthetics CI\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        \n      - name: Run openstatus Synthetics CI\n        uses: openstatushq/openstatus-github-action@v1\n        with:\n          api_key: ${{ secrets.OPENSTATUS_API_KEY }}\n```\n\n### 5. Commit and push\n\n```bash\ngit add openstatus.config.yaml .github/workflows/openstatus.yml\ngit commit -m \"Add openstatus synthetic tests to CI\"\ngit push origin main\n```\n\nThe GitHub Action will run automatically on the next push to `main`.\n\n## What you've accomplished\n\nGreat work! You've successfully:\n- ✅ Integrated openstatus into your CI/CD pipeline\n- ✅ Automated synthetic testing on every deployment\n- ✅ Added a safety check before production releases\n- ✅ Set up continuous validation of critical endpoints\n\n## Customization options\n\n### Run on different branches\n\n```yaml\non:\n  push:\n    branches: [ main, staging, develop ]\n```\n\n### Run on pull requests\n\n```yaml\non:\n  pull_request:\n    types: [opened, synchronize, reopened]\n```\n\n### Run on a schedule\n\n```yaml\non:\n  schedule:\n    - cron: '0 */4 * * *'  # Every 4 hours\n```\n\n### Multiple configuration files\n\n```yaml\n- name: Run openstatus Synthetics CI\n  uses: openstatushq/openstatus-github-action@v1\n  with:\n    api_key: ${{ secrets.OPENSTATUS_API_KEY }}\n    config_file: .openstatus/production.yaml\n```\n\n## Best practices\n\n1. **Start small**: Begin with 2-3 critical monitors\n2. **Fail fast**: Run synthetic tests early in your pipeline\n3. **Monitor the monitors**: Track your synthetic test success rate\n4. **Environment-specific**: Use different monitors for staging vs production\n5. **Document failures**: Investigate and document any CI failures\n\n## Troubleshooting\n\n**Action fails with authentication error:**\n- Verify `OPENSTATUS_API_KEY` secret is set correctly\n- Check that your API key hasn't expired\n\n**Monitors not found:**\n- Confirm monitor IDs are correct in `openstatus.config.yaml`\n- Ensure monitors are active in your openstatus dashboard\n\n**Tests timing out:**\n- Check that your endpoints are accessible from GitHub's runners\n- Consider increasing timeouts in monitor configuration\n\n## Next steps\n\n- **[Monitor Your MCP Server](/guides/how-to-monitor-mcp-server/)** - Advanced monitoring examples\n- **[Monitoring as Code](/concept/uptime-monitoring-as-code)** - Manage monitors with YAML\n- **[CLI Reference](/reference/cli-reference)** - Automate monitor management\n- **[Export Metrics](/guides/how-to-export-metrics-to-otlp-endpoint)** - Send data to your observability platform\n\n## Related resources\n\n- **[GitHub Action on Marketplace](https://github.com/marketplace/actions/openstatus-synthetics-ci)** - Official action\n- **[Example Repository](https://github.com/openstatusHQ/github-action-example)** - Working examples\n- **[Join Discord](https://www.openstatus.dev/discord)** - Get help from the community\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/how-to-use-react-widget.mdx",
    "content": "---\ntitle: How to Use openstatus React Widget\n---\n\nInstall the [npm](https://www.npmjs.com/package/@openstatus/react) package:\n\n```bash\nnpm install @openstatus/react\n```\n\n## React Server Component\n\n```tsx\nimport { StatusWidget } from \"@openstatus/react\";\n\nexport function Page() {\n  return <StatusWidget slug=\"status\" />;\n}\n```\n\nIt will automatically attach the slug to the href to allow the user to open a\nnew tab on click to `https://slug.openstatus.dev`. If you want to redirect him\nto a specific page, use the `href` property, like so:\n\n```tsx\n<StatusWidget slug=\"documenso\" href=\"https://status.documenso.com\" />\n```\n\n> `StatusWidget` is an **async function** and will only work with RSC. Using it\n> within a dead simple React App will not work.\n\n### Styling\n\n#### With tailwindcss\n\n```ts\n// tailwind.config.js\nmodule.exports = {\n  content: [\n    \"./app/**/*.{tsx,ts,mdx,md}\",\n    \"./node_modules/@openstatus/react/**/*.{js,ts,jsx,tsx}\",\n  ],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n};\n```\n\n#### Without tailwindcss\n\n```tsx\n// app/layout.tsx\nimport \"@openstatus/react/dist/styles.css\";\n```\n\n## Typed fetch function\n\n```tsx\nimport { getStatus } from \"@openstatus/react\";\n\n// React Server Component\nasync function CustomStatusWidget() {\n  const res = await getStatus(\"slug\");\n  // ^StatusResponse = { status: Status }\n\n  const { status } = res;\n  // ^Status = \"unknown\" | \"operational\" | \"degraded_performance\" | \"partial_outage\" | \"major_outage\" | \"under_maintenance\" | \"incident\"\n\n  return <div>{/* customize */}</div>;\n}\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/self-host-status-page-only.mdx",
    "content": "---\ntitle: Self-Host the OpenStatus Status Page (Lightweight)\ndescription: Deploy only the openstatus status page and dashboard on your own infrastructure, without monitoring, analytics, or background services.\n---\n\n## Problem\n\nYou want a status page to communicate incidents and maintenance to your users, but you don't need automated monitoring, analytics, or alerting. You may already have your own monitoring tools, or you simply want a lightweight way to manage your public-facing status page.\n\n## Solution\n\nOpenStatus provides a lightweight Docker Compose setup that runs only 4 services: a database, a one-shot migration runner, the dashboard, and the status page. This is ideal for teams who only want to self-host the status page without monitoring, or for teams that manage incidents manually using external monitoring tools.\n\n## Lightweight vs Full\n\nThe lightweight stack strips away all monitoring infrastructure. Here's what each version includes:\n\n| Feature | Full | Lightweight |\n|---------|------|-------------|\n| Status page | Yes | Yes |\n| Dashboard | Yes | Yes |\n| Database (libSQL) | Yes | Yes |\n| Automated monitoring | Yes | **No** |\n| Analytics & charts (Tinybird) | Yes | **No** |\n| API server | Yes | **No** |\n| Private location probes | Yes | **No** |\n\nIf you need automated monitoring, follow the [full self-hosting guide](/guides/self-hosting-openstatus) instead.\n\n## Prerequisites\n\n- Docker and Docker Compose installed\n- Git installed\n- Command line experience\n\n## Step-by-step guide\n\n### Part 1: Initial Setup and Service Launch\n\n1.  **Clone the Repository**\n\n    Get the latest version of openstatus:\n    ```bash\n    git clone https://github.com/openstatushq/openstatus\n    cd openstatus\n    ```\n\n2.  **Configure Your Environment**\n\n    Copy the example environment file. This is a simplified version of the full configuration, with only the variables relevant to the status page and dashboard.\n    ```bash\n    cp .env.docker-lightweight.example .env.docker\n    ```\n    Open `.env.docker` in a text editor. You **must** set values for the following variables:\n\n    - `AUTH_SECRET` — required for authentication. Generate a value with:\n      ```bash\n      openssl rand -base64 32\n      ```\n    - `RESEND_API_KEY` — required for magic link login emails. Get a key from [resend.com](https://resend.com).\n\n    Optionally, you can configure GitHub or Google OAuth providers by filling in the `AUTH_GITHUB_*` or `AUTH_GOOGLE_*` variables in the same file.\n\n3.  **Build and Start Services**\n\n    Use Docker Compose to build and run all services in the background:\n    ```bash\n    docker compose -f docker-compose-lightweight.yaml up -d\n    ```\n\n    The first build takes several minutes as it compiles the Next.js applications. Subsequent starts are much faster.\n\n    Check the status of the services:\n    ```bash\n    docker compose -f docker-compose-lightweight.yaml ps\n    ```\n\n    Wait until all services show as `healthy` before proceeding. The `db-migrate` service will show as exited — this is expected, as it runs once and stops.\n\n### Part 2: Application Configuration\n\n4.  **Access the Applications**\n\n    -   **Dashboard:** `http://localhost:3000`\n    -   **Status Page:** `http://localhost:3001`\n\n    Log in to the dashboard using email authentication (magic link). This will create your account and workspace.\n\n5.  **Set Workspace Limits**\n\n    Because this is a self-hosted instance, you need to manually set the feature limits for your workspace directly in the database. The following command updates the limits for the workspace with `id = 1`:\n\n    ```bash\n    curl -X POST http://localhost:8080/ -H \"Content-Type: application/json\" \\\n      -d '{\"statements\":[\"UPDATE workspace SET limits = '\\''{\\\\\"monitors\\\\\":100,\\\\\"periodicity\\\\\":[\\\\\"30s\\\\\",\\\\\"1m\\\\\",\\\\\"5m\\\\\",\\\\\"10m\\\\\",\\\\\"30m\\\\\",\\\\\"1h\\\\\"],\\\\\"multi-region\\\\\":true,\\\\\"data-retention\\\\\":\\\\\"24 months\\\\\",\\\\\"status-pages\\\\\":20,\\\\\"maintenance\\\\\":true,\\\\\"status-subscribers\\\\\":true,\\\\\"custom-domain\\\\\":true,\\\\\"password-protection\\\\\":true,\\\\\"white-label\\\\\":true,\\\\\"notifications\\\\\":true,\\\\\"sms\\\\\":true,\\\\\"pagerduty\\\\\":true,\\\\\"notification-channels\\\\\":50,\\\\\"members\\\\\":\\\\\"Unlimited\\\\\",\\\\\"audit-log\\\\\":true,\\\\\"private-locations\\\\\":true}'\\'' WHERE id = 1\"]}'\n    ```\n\n    You can find your workspace ID by querying the database:\n    ```bash\n    curl -X POST http://localhost:8080/ -H \"Content-Type: application/json\" \\\n      -d '{\"statements\":[\"SELECT id, name FROM workspace\"]}'\n    ```\n\n6.  **Create Your Status Page**\n\n    In the dashboard, create a new status page, add components for the services you want to display, and publish it. Your status page will be available at `http://localhost:3001`.\n\n## Service architecture\n\n| Service | Container | Host Port | Purpose |\n|---------|-----------|-----------|---------|\n| libsql | openstatus-libsql | 8080 | Database (HTTP API) |\n| db-migrate | openstatus-db-migrate | — | One-shot database migration (exits after completion) |\n| dashboard | openstatus-dashboard | 3000 | Admin interface |\n| status-page | openstatus-status-page | 3001 | Public status page |\n\n## Data persistence\n\nAll application data is stored in the `openstatus-libsql-data` Docker volume.\n\n- `docker compose down` **preserves** your data.\n- `docker compose down -v` **destroys** the volume and all data.\n\nFor production use, back up this volume regularly.\n\n## Troubleshooting\n\n**Containers won't start:** Check the logs for the failing service:\n```bash\ndocker compose -f docker-compose-lightweight.yaml logs <service-name>\n```\n\n**Magic link emails not arriving:** Verify that `RESEND_API_KEY` is set correctly in `.env.docker`.\n\n**Dashboard shows errors on first load:** The `db-migrate` service may still be running. Check its status:\n```bash\ndocker compose -f docker-compose-lightweight.yaml ps\n```\n\n**Port conflicts:** If ports 3000, 3001, or 8080 are already in use on your machine, update the host port mappings in `docker-compose-lightweight.yaml`. For example, change `\"3000:3000\"` to `\"4000:3000\"` to use port 4000 instead.\n\n## Next steps\n\n- **[Self-Host openstatus (Full)](/guides/self-hosting-openstatus)** — Add automated monitoring, analytics, and alerting\n- **[Join our Discord](https://www.openstatus.dev/discord)** — Get help from the community\n"
  },
  {
    "path": "apps/docs/src/content/docs/guides/self-hosting-openstatus.mdx",
    "content": "---\ntitle: How to Self-Host openstatus\ndescription: Complete guide to deploying openstatus on your own infrastructure\n---\n\nimport { Image } from 'astro:assets';\n\nimport localOpenstatus from '../../../assets/guides/self-hosting-openstatus/local-openstatus.png';\n\n## Problem\n\nYou want to run openstatus on your own infrastructure instead of using the hosted service. This gives you full control over your data, customization options, and the ability to monitor internal services not accessible from the public internet.\n\n## Solution\n\nopenstatus provides a Docker Compose setup that makes self-hosting straightforward. This guide walks you through deploying all necessary services and configuring your self-hosted instance.\n\n## Prerequisites\n\n- Docker and Docker Compose installed\n- Basic understanding of Docker and containerization\n- Command line experience\n- Git installed\n\n## Known limitations\n\nSelf-hosting openstatus currently has these constraints:\n\n- It only works with private locations. You have to deploy our probes to the cloud provider of your choice.\n\n## Step-by-step guide\n\nThis guide is divided into three parts: launching the services, setting up the database and analytics, and configuring the application through the UI.\n\n### Part 1: Initial Setup and Service Launch\n\n1.  **Clone the Repository**\n\n    Get the latest version of openstatus:\n    ```bash\n    git clone https://github.com/openstatushq/openstatus\n    cd openstatus\n    ```\n\n2.  **Configure Your Environment**\n\n    Copy the example environment file. This file will hold all your configuration variables.\n    ```bash\n    cp .env.docker.example .env.docker\n    ```\n    Open `.env.docker` in a text editor. At a minimum, you **must** set a value for `AUTH_SECRET` for authentication to work. For a complete setup, review the file for other variables like OAuth providers or email services.\n\n3.  **Build and Start Services**\n\n    Use Docker Compose to build and run all openstatus services in the background.\n    ```bash\n    export DOCKER_BUILDKIT=1\n    docker compose up -d\n    ```\n    You can check the status of the services with `docker compose ps`. It might take a few minutes for all services to be healthy.\n\n### Part 2: Database and Analytics Setup\n\n4.  **Run Database Migrations**\n\n    The database container starts with an empty database. You must run migrations to set up the required schema.\n    ```bash\n    # Make sure you are in the root of the openstatus project\n    cd packages/db\n    pnpm install\n    pnpm migrate\n    cd ../.. # Return to the project root\n    ```\n\n\tIf you do not have or want to avoid installing the necessary tools on the \thost, you can run this command to create a one-shot container that will remove itself after completion.\n\t```\n\t# Make sure you are in the root of the openstatus project\n\t# For RHEL derivatives, make sure to end /work with :Z for SELinux, \"$PWD\":/work:Z\n\tsudo docker run --rm -it \\\n\t  --network openstatus \\\n\t  --env-file .env.docker \\\n\t  -v \"$PWD\":/work \\\n\t  -w /work/packages/db \\\n\t  node:22-trixie \\\n\t  bash -lc '\n\t    set -euo pipefail\n\t    export DEBIAN_FRONTEND=noninteractive\n\t    apt-get update -qq\n\t    apt-get install -y -qq curl ca-certificates unzip\n\t    curl -fsSL https://bun.sh/install -o /tmp/bun-install.sh\n\t    bash /tmp/bun-install.sh\n\t    export PATH=\"$HOME/.bun/bin:$PATH\"\n\t    npm i -g pnpm\n\t    pnpm install\n\t    bun src/migrate.mts\n\t  '\n\t```\n\n5.  **Deploy Local Tinybird Analytics**\n\n    Tinybird is used for analytics. Deploy the local datasources, pipes, and endpoints.\n    ```bash\n    # Make sure you are in the root of the openstatus project\n    cd packages/tinybird\n    pnpm install\n    tb --local deploy\n    cd ../.. # Return to the project root\n    ```\n\n6.  **Configure Tinybird API Key**\n\n    You need to get your local Tinybird admin token and add it to your environment file.\n    ```bash\n    cd packages/tinybird\n    tb --local open # This opens the Tinybird UI in your browser\n    ```\n    In the Tinybird UI, find and copy your admin token. Then, add it to your `.env.docker` file in the root of the project:\n    ```env\n    TINY_BIRD_API_KEY=\"your-tinybird-admin-token\"\n    ```\n    After adding the token, restart your services for the changes to take effect:\n    ```bash\n    # Make sure you are in the root of the openstatus project\n    docker compose restart\n    ```\n\n### Part 3: Application Configuration\n\nNow that the services are running, you can access the dashboard and perform the final setup steps.\n\n-   **Dashboard:** `http://localhost:3002`\n-   **Status Pages:** `http://localhost:3003`\n\n7.  **Create a Workspace and Set Limits**\n\n    -   Navigate to the dashboard at `http://localhost:3002`.\n    -   Sign up and create a new workspace.\n    -   Because this is a self-hosted instance, you need to manually set the feature limits for your workspace directly in the database.\n\n    The following command updates the limits for the workspace with `id = 1`. If your workspace has a different ID, change the `WHERE id = 1` part of the command.\n    ```bash\n    curl -X POST http://localhost:8080/ -H \"Content-Type: application/json\" \\\n      -d '{\"statements\":[\"UPDATE workspace SET limits = '\\''{\\\\\"monitors\\\\\":100,\\\\\"periodicity\\\\\":[\\\\\"30s\\\\\",\\\\\"1m\\\\\",\\\\\"5m\\\\\",\\\\\"10m\\\\\",\\\\\"30m\\\\\",\\\\\"1h\\\\\"],\\\\\"multi-region\\\\\":true,\\\\\"data-retention\\\\\":\\\\\"24 months\\\\\",\\\\\"status-pages\\\\\":20,\\\\\"maintenance\\\\\":true,\\\\\"status-subscribers\\\\\":true,\\\\\"custom-domain\\\\\":true,\\\\\"password-protection\\\\\":true,\\\\\"white-label\\\\\":true,\\\\\"notifications\\\\\":true,\\\\\"sms\\\\\":true,\\\\\"pagerduty\\\\\":true,\\\\\"notification-channels\\\\\":50,\\\\\"members\\\\\":\\\\\"Unlimited\\\\\",\\\\\"audit-log\\\\\":true,\\\\\"private-locations\\\\\":true}'\\'' WHERE id = 1\"]}'\n    ```\n    You can find your workspace ID by inspecting the database with a command like `curl -X POST http://localhost:8080/ -H \"Content-Type: application/json\" -d '{\"statements\":[\"SELECT id, name FROM workspace\"]}'`.\n    \n    If you want to unlock the paid features, you need to upgrade your workspace inside the database. The following command assumes that you want to change the payment plan for the workspace with the ID of 1, and that you want to change it to a \"Pro\" instance indefinitely.\n    ```\n\tcurl -sS -X POST \"http://localhost:8080/\" \\\n\t  -H \"Content-Type: application/json\" \\\n\t  -d \"{\\\"statements\\\":[\n\t    \\\"UPDATE workspace SET plan='team', paid_until=strftime('%s','now') + 315360000, ends_at=NULL WHERE id=1;\\\",\n\t    \\\"SELECT id, plan, paid_until, ends_at FROM workspace WHERE id=1;\\\"\n\t  ]}\"\n    ```   \n\n8.  **Deploy a Private Location**\n\n    The self-hosted version relies on private locations to perform checks.\n    -   In the dashboard, navigate to **Settings -> Private Locations** and create a new one.\n    -   Copy the generated `OPENSTATUS_KEY`.\n    -   Deploy the private location probe to your infrastructure using the Docker image `ghcr.io/openstatushq/private-location:latest`.\n    -   When deploying, you must provide two environment variables to the container:\n        -   `OPENSTATUS_KEY`: The key you just copied.\n        -   `OPENSTATUS_INGEST_URL`: The URL of your self-hosted server's API endpoint (e.g., `http://<your-server-ip-or-domain>:3001`).\n    -   For a detailed guide on deploying a private location, see **[Deploy Private Locations on Cloudflare Containers](/guides/how-to-deploy-probes-cloudflare-containers)**.\n\n9.  **Create Monitors**\n\n    You're all set! You can now create monitors in the dashboard. They will be checked by the private location you deployed.\n\n\n  <Image\n    src={localOpenstatus}\n    alt=\"openstatus running locally with self-hosted services\"\n  />\n  \n\n## Service architecture\n\nopenstatus consists of multiple services running together:\n\n| Service | Port | Purpose |\n|---------|------|---------|\n| workflows | 3000 | Background jobs and scheduled tasks |\n| server | 3001 | API backend (tRPC) |\n| dashboard | 3002 | Admin interface for configuration |\n| status-page | 3003 | Public status pages |\n| private-location | 8081 | Monitoring agent for checks |\n| libsql | 8080 | Database (HTTP) |\n| libsql | 5001 | Database (gRPC) |\n| tinybird-local | 7181 | Analytics and metrics |\n\n## What you've accomplished\n\nCongratulations! You've successfully:\n- ✅ Deployed openstatus on your own infrastructure\n- ✅ Configured all required services\n- ✅ Set up a private location for monitoring\n- ✅ Created your first self-hosted monitor\n\n## Troubleshooting\n\n**Containers won't start**: Check Docker logs with `docker compose logs [service-name]`\n\n**Database migrations fail**: Ensure you're in the correct directory and have pnpm installed\n\n**Private location not connecting**: Verify the `OPENSTATUS_KEY` and `OPENSTATUS_INGEST_URL` are correct\n\n**Tinybird issues**: Make sure the Tinybird token is correctly set in `.env.docker`\n\n## Next steps\n\n- **[Deploy Private Locations](/guides/how-to-deploy-probes-cloudflare-containers)** - Set up monitoring from multiple regions\n- **[Create Monitors](/tutorial/how-to-create-monitor)** - Start monitoring your services\n\n## Additional resources\n\n- **[Docker Compose file](https://github.com/openstatusHQ/openstatus/blob/main/docker-compose.yaml)** - Review the complete configuration\n- **[Private Location Reference](/reference/private-location)** - Technical specifications\n- **[Join our Discord](https://www.openstatus.dev/discord)** - Get help from the community\n"
  },
  {
    "path": "apps/docs/src/content/docs/help/support.mdx",
    "content": "---\ntitle: Need help?\ndescription: \"We're always here to help.\"\n---\n\nIf you have any questions, feedback, or need help you can:\n- Schedule a [call](https://cal.com/team/openstatus/30min) with us.\n- Join our [Discord](https://www.openstatus.dev/discord) community.\n- Send us an email at [ping@openstatus.dev](mailto:ping@openstatus.dev)\n- Open an issue on our [GitHub](https://www.github.com/openstatushq/openstatus) repository."
  },
  {
    "path": "apps/docs/src/content/docs/index.mdx",
    "content": "---\ntitle: \"OpenStatus Docs — Open Source Status Page & Uptime Monitoring\"\ndescription: \"OpenStatus documentation — learn how to create your open-source status page, monitor your websites and APIs from 35+ global locations, and configure alerts.\"\ntemplate: doc\ntopic: docs\nnext: false\nhero:\n  title: OpenStatus Documentation\n  tagline: Learn how to create your status page, monitor your services, and keep your users informed — all open source.\n---\n\nimport { Card, CardGrid, LinkCard } from \"@astrojs/starlight/components\";\nimport \"../../custom.css\";\n\n### What is openstatus?\n\n[openstatus](https://www.openstatus.dev) is an open-source status page platform with uptime monitoring. Monitor your websites, APIs, and services from multiple global locations and share real-time status updates with your users.\n\n### Why OpenStatus?\n\n- **Open source** — fully open-source, self-hostable, and transparent\n- **Beautiful status pages** — keep your users informed with public or audience specific status pages\n- **30+ global locations** — monitor from your users' perspective, not just your own\n- **HTTP, TCP & DNS monitors** — check APIs, servers, and DNS resolution\n- **12 notification channels** — Slack, Discord, PagerDuty, OpsGenie, email, SMS, and more\n- **Monitoring as code** — define monitors in YAML, manage with CLI or Terraform\n- **OpenTelemetry export** — send metrics to Grafana, Datadog, Honeycomb, or any OTLP endpoint\n- **Private locations** — deploy lightweight probes inside your own infrastructure\n\n### How to use this documentation\n\nOur documentation follows the [Diátaxis framework](https://diataxis.fr/), organizing content into four distinct categories to help you find what you need:\n\n<CardGrid>\n  <LinkCard\n    title=\"📚 Tutorials\"\n    href=\"/tutorial/getting-started/\"\n    description=\"Step-by-step lessons to learn openstatus from scratch. Start here if you're new!\"\n  />\n  <LinkCard\n    title=\"🛠️ How-to Guides\"\n    href=\"/guides/getting-started/\"\n    description=\"Practical guides to solve specific problems and accomplish particular tasks.\"\n  />\n  <LinkCard\n    title=\"💡 Concepts\"\n    href=\"/concept/getting-started/\"\n    description=\"In-depth explanations of key concepts, design decisions, and best practices.\"\n  />\n  <LinkCard\n    title=\"📖 Reference\"\n    href=\"/reference\"\n    description=\"Technical specifications, API documentation, and configuration references.\"\n  />\n</CardGrid>\n\n**Not sure where to start?**\n\n- **New users**: Begin with our [tutorials](/tutorial/getting-started/) to create your first monitor\n- **Experienced users**: Check out [how-to guides](/guides/getting-started/) for specific tasks\n- **Need help?** Visit our [help section](/help/support/) or join our community\n\n### Join the community\n\nJoin the community to get help, share your ideas or just to say hi.\n\n<CardGrid>\n<LinkCard href=\"https://bsky.app/profile/openstatus.dev\" title=\"Bluesky\" />\n<LinkCard href=\"https://www.openstatus.dev/discord\" title=\"Discord\"/>\n<LinkCard href=\"https://www.openstatus.dev/github\" title=\"GitHub\"/>\n\n</CardGrid>\n"
  },
  {
    "path": "apps/docs/src/content/docs/monitoring/overview.mdx",
    "content": "---\ntitle: Monitoring Overview\ndescription: \"Introduction to synthetic monitoring with openstatus\"\n---\n\nimport { CardGrid, LinkCard } from '@astrojs/starlight/components';\nimport { Image } from 'astro:assets';\nimport overview from '../../../assets/monitor/overview/dashboard.png';\n\n## What is synthetic monitoring?\n\nWith openstatus, you can simulate user requests to check the availability and performance of your website, API, or server from different locations around the globe. This proactive approach helps you find issues before your users do.\n\nSynthetic monitoring complements real user monitoring (RUM) by providing:\n- **Consistent baseline**: Predictable checks at regular intervals\n- **Early warning system**: Detect issues before they affect many users\n- **Global perspective**: Monitor from multiple regions simultaneously\n- **24/7 coverage**: Continuous monitoring, even when you have no traffic\n\n<Image\n    src={overview}\n    alt=\"openstatus dashboard showing status codes and response time charts\"\n/>\n\n\n\n\n## How it works\n\nWe send a request to your specified endpoint on a regular schedule and record the response. If your website or API is down, timing out, or doesn't return the expected response, we'll alert you right away.\n\n## What is a monitor?\n\nA **monitor** is a job that runs periodically to check the status of a service. This could be a website, an API, or any other service that can be automatically checked. Each monitor you create runs a request to your endpoint and records the results for you to review.\n\n## Creating a Monitor\n\nYou can create a new monitor in one of four ways:\n\n- Dashboard: Use our intuitive dashboard to quickly set up and manage your monitors.\n- API: Integrate monitor creation into your workflow using our [API](https://api.openstatus.dev/v1#tag/monitor/POST/monitor).\n- CLI: Use our command-line interface to create and manage monitors with [YAML configuration files](https://github.com/openstatusHQ/cli-template).\n- Terraform: Automate the process with our [Terraform provider](/reference/terraform/).\n\n### Monitor types\n\n- **HTTP**: Check the availability and performance of your web services by sending HTTP requests and analyzing the responses.\n- **TCP**: Verify that your servers are accepting connections on specific ports, ensuring that critical\n- **DNS**: Monitor the health of your DNS records by performing lookups and validating responses.\n\n## Getting started\n\nReady to start monitoring? Follow these guides:\n\n1. **[Create Your First Monitor](/tutorial/how-to-create-monitor)** - Step-by-step tutorial\n\n## Learn more\n\n- **[Understanding Uptime Monitoring](/concept/uptime-monitoring)** - Core concepts explained\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/cli-reference.mdx",
    "content": "---\ntitle: CLI Reference\n---\n\n## CLI interface - openstatus\n\nopenstatus is a command line interface for managing your monitors and triggering your synthetics tests.\n\nThis is openstatus Command Line Interface, the openstatus.dev CLI.\n\nUsage:\n\n```bash\n$ openstatus [COMMAND] [COMMAND FLAGS] [ARGUMENTS...]\n```\n\n### `monitors` command\n\nManage your monitors.\n\nUsage:\n\n```bash\n$ openstatus [GLOBAL FLAGS] monitors [ARGUMENTS...]\n```\n\n### `monitors apply` subcommand\n\nCreate or update monitors.\n\n> openstatus monitors apply [options]\n\nCreates or updates monitors according to the openstatus configuration file.\n\nUsage:\n\n```bash\n$ openstatus [GLOBAL FLAGS] monitors apply [COMMAND FLAGS] [ARGUMENTS...]\n```\n\nThe following flags are supported:\n\n| Name                        | Description                                           |   Default value   |  Environment variables |\n|-----------------------------|-------------------------------------------------------|:-----------------:|:----------------------:|\n| `--config=\"…\"` (`-c`)       | The configuration file containing monitor information | `openstatus.yaml` |         *none*         |\n| `--access-token=\"…\"` (`-t`) | openstatus API Access Token                           |                   | `OPENSTATUS_API_TOKEN` |\n| `--auto-accept` (`-y`)      | Automatically accept the prompt                       |      `false`      |         *none*         |\n\n\n### `monitors import` subcommand\n\nImport all your monitors.\n\n> openstatus monitors import [options]\n\nImport all your monitors from your workspace to a YAML file; it will also create a lock file to manage your monitors with 'apply'.\n\nUsage:\n\n```bash\n$ openstatus [GLOBAL FLAGS] monitors import [COMMAND FLAGS] [ARGUMENTS...]\n```\n\nThe following flags are supported:\n\n| Name                        | Description                 |   Default value   |  Environment variables |\n|-----------------------------|-----------------------------|:-----------------:|:----------------------:|\n| `--access-token=\"…\"` (`-t`) | openstatus API Access Token |                   | `OPENSTATUS_API_TOKEN` |\n| `--output=\"…\"` (`-o`)       | The output file name        | `openstatus.yaml` |         *none*         |\n\n### `monitors info` subcommand\n\nGet a monitor information.\n\n> openstatus monitors info [MonitorID]\n\nFetch the monitor information. The monitor information includes details such as name, description, endpoint, method, frequency, locations, active status, public status, timeout, degraded after, and body. The body is truncated to 40 characters.\n\nUsage:\n\n```bash\n$ openstatus [GLOBAL FLAGS] monitors info [COMMAND FLAGS] [ARGUMENTS...]\n```\n\nThe following flags are supported:\n\n| Name                        | Description                 | Default value |  Environment variables |\n|-----------------------------|-----------------------------|:-------------:|:----------------------:|\n| `--access-token=\"…\"` (`-t`) | openstatus API Access Token |               | `OPENSTATUS_API_TOKEN` |\n\n### `monitors list` subcommand\n\nList all monitors.\n\n> openstatus monitors list [options]\n\nList all monitors. The list shows all your monitors attached to your workspace. It displays the ID, name, and URL of each monitor.\n\nUsage:\n\n```bash\n$ openstatus [GLOBAL FLAGS] monitors list [COMMAND FLAGS] [ARGUMENTS...]\n```\n\nThe following flags are supported:\n\n| Name                        | Description                               | Default value |  Environment variables |\n|-----------------------------|-------------------------------------------|:-------------:|:----------------------:|\n| `--all`                     | List all monitors including inactive ones |    `false`    |         *none*         |\n| `--access-token=\"…\"` (`-t`) | openstatus API Access Token               |               | `OPENSTATUS_API_TOKEN` |\n\n### `monitors trigger` subcommand\n\nTrigger a monitor execution.\n\n> openstatus monitors trigger [MonitorId] [options]\n\nTrigger a monitor execution on demand. This command allows you to launch your tests on demand.\n\nUsage:\n\n```bash\n$ openstatus [GLOBAL FLAGS] monitors trigger [COMMAND FLAGS] [ARGUMENTS...]\n```\n\nThe following flags are supported:\n\n| Name                        | Description                 | Default value |  Environment variables |\n|-----------------------------|-----------------------------|:-------------:|:----------------------:|\n| `--access-token=\"…\"` (`-t`) | openstatus API Access Token |               | `OPENSTATUS_API_TOKEN` |\n\n### `run` command (aliases: `r`)\n\nRun your synthetics tests.\n\n> openstatus run [options]\n\nRun the synthetic tests defined in the config.openstatus.yaml.\n\nUsage:\n\n```bash\n$ openstatus [GLOBAL FLAGS] run [COMMAND FLAGS] [ARGUMENTS...]\n```\n\nThe following flags are supported:\n\n| Name                        | Description                 |      Default value       |  Environment variables |\n|-----------------------------|-----------------------------|:------------------------:|:----------------------:|\n| `--config=\"…\"`              | The configuration file      | `config.openstatus.yaml` |         *none*         |\n| `--access-token=\"…\"` (`-t`) | openstatus API Access Token |                          | `OPENSTATUS_API_TOKEN` |\n\n### `whoami` command (aliases: `w`)\n\nGet your workspace information.\n\n> openstatus whoami [options]\n\nGet your current workspace information, display the workspace name, slug, and plan.\n\nUsage:\n\n```bash\n$ openstatus [GLOBAL FLAGS] whoami [COMMAND FLAGS] [ARGUMENTS...]\n```\n\nThe following flags are supported:\n\n| Name                        | Description                 | Default value |  Environment variables |\n|-----------------------------|-----------------------------|:-------------:|:----------------------:|\n| `--access-token=\"…\"` (`-t`) | openstatus API Access Token |               | `OPENSTATUS_API_TOKEN` |\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/dns-monitor.mdx",
    "content": "---\ntitle: DNS Monitor Reference\ndescription: Complete technical specification for DNS record monitoring\n---\n\n## Overview\n\nA DNS Monitor is a component designed to verify the availability and correctness of DNS records. It performs periodic lookups for specified DNS record types against a target domain or subdomain from various geographical locations.\n\n**Use cases:**\n- Validating domain name resolution\n- Monitoring changes to critical DNS records (e.g., A, CNAME, MX)\n- Ensuring proper load balancing via DNS (when combined with multi-region checks)\n- Detecting unauthorized DNS alterations\n\n## Configuration\n\n### URI\n\n**Type:** String (required)\n**Format:** Domain name or subdomain\n\nThe fully qualified domain name or subdomain to be monitored.\n\n**Examples:**\n- `openstat.us`\n- `api.example.com`\n- `mail.example.org`\n\n### Record Types\n\nThe monitor supports fetching and validating the following DNS record types:\n\n-   `A` (Address Record): Maps a domain name to an IPv4 address.\n-   `AAAA` (IPv6 Address Record): Maps a domain name to an IPv6 address.\n-   `CNAME` (Canonical Name Record): Maps an alias domain name to another canonical domain name.\n-   `MX` (Mail Exchange Record): Specifies the mail servers responsible for accepting email messages on behalf of a domain name.\n-   `NS` (Name Server Record): Delegates a domain or subdomain to a set of authoritative name servers.\n-   `TXT` (Text Record): Carries arbitrary human-readable text and is also used for various purposes like SPF, DKIM, DMARC, and site verification.\n\n### Regions\n\nThe geographical locations from which the DNS monitoring checks are performed. This allows for verification of DNS propagation and performance across different networks.\n\n__Africa__\n\n- Johannesburg, South Africa 🇿🇦 (free)\n\n__Asia__\n\n- Hong Kong, Hong Kong 🇭🇰 (free)\n- Mumbai, India 🇮🇳\n- Singapore, Singapore 🇸🇬\n- Tokyo, Japan 🇯🇵\n\n__Europe__\n\n- Amsterdam, Netherlands 🇳🇱 (free)\n- Bucharest, Romania 🇷🇴\n- Frankfurt, Germany 🇩🇪\n- London, United Kingdom 🇬🇧\n- Madrid, Spain 🇪🇸\n- Paris, France 🇫🇷\n- Stockholm, Sweden 🇸🇪\n- Warsaw, Poland 🇵🇱\n\n__North America__\n\n- Ashburn, Virginia, USA 🇺🇸 (free)\n- Atlanta, Georgia, USA 🇺🇸\n- Boston, Massachusetts, USA 🇺🇸\n- Chicago, Illinois, USA 🇺🇸\n- Dallas, Texas, USA 🇺🇸\n- Denver, Colorado, USA 🇺🇸\n- Guadalajara, Mexico 🇲🇽\n- Los Angeles, California, USA 🇺🇸\n- Miami, Florida, USA 🇺🇸\n- Montreal, Canada 🇨🇦\n- Phoenix, Arizona, USA 🇺🇸\n- Queretaro, Mexico 🇲🇽\n- Seattle, Washington, USA 🇺🇸\n- San Jose, California, USA 🇺🇸\n- Toronto, Canada 🇨🇦\n\n__South America__\n\n- Bogota, Colombia 🇨🇴\n- Buenos Aires, Argentina 🇦🇷\n- Rio de Janeiro, Brazil 🇧🇷\n- Sao Paulo, Brazil 🇧🇷 (free)\n- Santiago, Chile 🇨🇱\n\n__Oceania__\n\n- Sydney, Australia 🇦🇺 (free)\n\n### Frequency\n\nThe interval at which the DNS checks are performed. Supported frequencies:\n- 30 seconds\n- 1 minute\n- 5 minutes\n- 10 minutes\n- 30 minutes\n- 1 hour\n\n### Response Time Thresholds\n\n#### Timeout\n\n**Type:** Duration (optional)\n**Default:** `45 seconds`\n\nThe maximum duration to wait for a DNS response. If the lookup exceeds this time, the check is considered failed.\n\n#### Degraded\n\n**Type:** Duration (optional)\n\nThe duration after which a DNS response is considered degraded. This indicates a performance issue without being a complete failure.\n\n### Retry\n\n**Type:** Integer (optional)\n**Default:** `3`\n\nThe number of times the monitor will retry a failed DNS lookup before reporting a definitive failure. For example: `3`\n\n## Related resources\n\n- **[CLI Reference](/reference/cli-reference)** - Manage monitors as code using the OpenStatus CLI.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/http-monitor.mdx",
    "content": "---\ntitle: HTTP Monitor Reference\ndescription: Complete technical specification for HTTP/HTTPS endpoint monitoring\n---\n\n## Overview\n\nAn HTTP Monitor is a component that allows you to monitor the status of HTTP and HTTPS endpoints. It can be used to monitor websites, APIs, webhooks, or any other HTTP-accessible service.\n\n**Use cases:**\n- Website uptime monitoring\n- API health checks\n- Webhook endpoint validation\n- CDN performance monitoring\n- Authentication endpoint testing\n\n## Configuration\n\n### URL\n\n**Type:** String (required)  \n**Format:** Full URL including protocol\n\nThe URL of the HTTP endpoint you want to monitor.\n\n**Examples:**\n- `https://openstat.us`\n- `https://api.example.com/health`\n- `http://internal-service.local:8080/status`\n\n**Note:** We recommend using HTTPS for better security.\n\n### Methods\n\n**Type:** String (required)  \n**Default:** `GET`\n\nThe HTTP method to use when making the request to the endpoint.\n\n**Available methods:**\n- `GET` - Retrieve data (most common for health checks)\n- `POST` - Send data to create/trigger actions\n- `PUT` - Update existing resources\n- `DELETE` - Remove resources\n- `HEAD` - Like GET but without response body\n- `OPTIONS` - Query supported methods\n- `PATCH` - Partial resource updates\n- `TRACE` - Echo request for debugging\n\n**Common usage:**\n- Health checks: `GET`\n- API testing: `POST`, `PUT`, `DELETE`\n- Webhook testing: `POST`\n\n### Body\n\n**Type:** String (optional)  \n**Available for:** `POST`, `PUT`, `PATCH` methods\n\nThe request body to send with the HTTP request. Supports both text and binary data.\n\n**Text body examples:**\n```json\n{ \"key\": \"value\" }\n```\n\n**Binary data:**\nFor binary content (e.g., images), use base64 encoding with data URI:\n```\ndata:image/jpeg;base64,/9j...\n```\n\n**Content type:** Set the appropriate `Content-Type` header (e.g., `application/json`, `application/octet-stream`).\n### Headers\n\n**Type:** Key-value pairs (optional)\n\nCustom HTTP headers to include with your request.\n\n**Common examples:**\n```\nContent-Type: application/json\nAuthorization: Bearer your_token_here\nAccept: application/json\nUser-Agent: Custom-Agent/1.0\n```\n\n**Use cases:**\n- **Authentication:** Send API tokens or credentials\n- **Content negotiation:** Specify accepted response formats\n- **Custom identification:** Add tracking or debugging headers\n\n**Note:** openstatus automatically adds `User-Agent: openstatus/1.0` to all requests.\n\n### Regions\n\n**Type:** Array of Strings (required)\n**Format:** Region identifiers (e.g., `iad`, `jnb`)\n\nThe geographical regions from which the HTTP request will be triggered. This allows for monitoring global availability and performance.\n\n__Africa__\n\n- Johannesburg, South Africa 🇿🇦 (free)\n\n__Asia__\n\n- Hong Kong, Hong Kong 🇭🇰 (free)\n- Mumbai, India 🇮🇳\n- Singapore, Singapore 🇸🇬\n- Tokyo, Japan 🇯🇵\n\n__Europe__\n\n- Amsterdam, Netherlands 🇳🇱 (free)\n- Bucharest, Romania 🇷🇴\n- Frankfurt, Germany 🇩🇪\n- London, United Kingdom 🇬🇧\n- Madrid, Spain 🇪🇸\n- Paris, France 🇫🇷\n- Stockholm, Sweden 🇸🇪\n- Warsaw, Poland 🇵🇱\n\n__North America__\n\n- Ashburn, Virginia, USA 🇺🇸 (free)\n- Atlanta, Georgia, USA 🇺🇸\n- Boston, Massachusetts, USA 🇺🇸\n- Chicago, Illinois, USA 🇺🇸\n- Dallas, Texas, USA 🇺🇸\n- Denver, Colorado, USA 🇺🇸\n- Guadalajara, Mexico 🇲🇽\n- Los Angeles, California, USA 🇺🇸\n- Miami, Florida, USA 🇺🇸\n- Montreal, Canada 🇨🇦\n- Phoenix, Arizona, USA 🇺🇸\n- Queretaro, Mexico 🇲🇽\n- Seattle, Washington, USA 🇺🇸\n- San Jose, California, USA 🇺🇸\n- Toronto, Canada 🇨🇦\n\n__South America__\n\n- Bogota, Colombia 🇨🇴\n- Buenos Aires, Argentina 🇦🇷\n- Rio de Janeiro, Brazil 🇧🇷\n- Sao Paulo, Brazil 🇧🇷 (free)\n- Santiago, Chile 🇨🇱\n\n__Oceania__\n\n- Sydney, Australia 🇦🇺 (free)\n\n### Frequency\n\n**Type:** String (required)\n**Format:** Duration string (e.g., `30s`, `1m`, `1h`)\n\nThe interval at which the HTTP monitor will perform checks. Supported frequencies:\n- `30 seconds`\n- `1 minute`\n- `5 minutes`\n- `10 minutes`\n- `30 minutes`\n- `1 hour`\n\n### Response Time Thresholds\n\n#### Timeout\n\n**Type:** Duration (optional)\n**Default:** `45 seconds`\n\nThe maximum duration to wait for the HTTP request to complete. If the request exceeds this time, it is considered a failure.\n\n#### Degraded\n\n**Type:** Duration (optional)\n\nThe duration after which the HTTP request is considered to be performing in a degraded state. This threshold allows for proactive alerting on performance issues before a complete outage.\n\n### Retry\n\n**Type:** Integer (optional)\n**Default:** `3`\n\nThe number of times the monitor will automatically retry the HTTP request upon failure before reporting a definitive error. For example: `3`\n\n### Assertions\n\nAssertions allow you to validate specific aspects of the HTTP response.\n\n#### Body Assertions\n\nValidate the content of the HTTP response body.\n\n**Comparisons:**\n- `Contains`: The response body must include the specified string.\n- `Not Contains`: The response body must not include the specified string.\n- `Equal`: The response body must exactly match the specified string.\n- `Not Equal`: The response body must not exactly match the specified string.\n- `Empty`: The response body must be empty.\n\n#### Status Code Assertions\n\nValidate the HTTP status code of the response.\n\n**Comparisons:**\n- `Equal`: The status code must be exactly the specified value.\n- `Not Equal`: The status code must not be the specified value.\n- `Greater Than`: The status code must be greater than the specified value.\n- `Greater Than or Equal`: The status code must be greater than or equal to the specified value.\n- `Less Than`: The status code must be less than the specified value.\n- `Less Than or Equal`: The status code must be less than or equal to the specified value.\n\n#### Headers Assertions\n\nValidate the presence or content of specific HTTP response headers.\n\n**Purpose:** Verify cache headers, check security headers (e.g., `X-Frame-Options`), validate content-type.\n\n**Comparisons:**\n- `Contains`: A header's value must include the specified string.\n- `Not Contains`: A header's value must not include the specified string.\n- `Equal`: A header's value must exactly match the specified string.\n- `Not Equal`: A header's value must not exactly match the specified string.\n- `Empty`: A header's value must be empty or the header must not be present.\n\n**Example Use Cases:**\n- Verify `Cache-Control` headers are present and correct.\n- Check for the existence of security-related headers like `Strict-Transport-Security`.\n- Validate the `Content-Type` header in API responses.\n\n### OpenTelemetry\n\nConfigures the export of monitoring metrics to an OpenTelemetry-compatible observability platform.\n\n#### OTLP Endpoint\n\n**Type:** String (optional)  \n**Protocol:** HTTP only\n\nThe OTLP (OpenTelemetry Protocol) endpoint URL where collected metrics should be exported.\n\n**Example:** `https://otlp.example.com/v1/metrics`\n\n#### OTLP Headers\n\n**Type:** Key-value pairs (optional)\n\nCustom headers to include when sending metrics to your OTLP endpoint. Commonly used for authentication or tenant identification.\n\n**Common example:**\n```\nAuthorization: Bearer <your_token>\n```\n\n### Public\n\n**Type:** Boolean  \n**Default:** `false`\n\nControls the visibility of monitor data on your public status page.\n\n- `true`: Monitor metrics and status are visible to all visitors of your status page.\n- `false`: Monitor data remains private, accessible only within your OpenStatus dashboard.\n\n**Use cases for public visibility:**\n- Enhancing transparency with users regarding service health.\n- Providing public API status pages.\n- Displaying SaaS service availability to customers.\n\n## Related resources\n\n- **[Create Your First Monitor](/tutorial/how-to-create-monitor)** - Step-by-step tutorial on setting up a monitor.\n- **[CLI Reference](/reference/cli-reference)** - Guide to managing OpenStatus monitors programmatically using the command-line interface.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/incident.mdx",
    "content": "---\ntitle: Incident Reference\ndescription: Technical specification for incident management and lifecycle\n---\n\n## Overview\n\nAn incident in OpenStatus represents a detected problem or service disruption related to a monitored resource. Incidents are automatically generated when a monitor reports a failure condition that meets predefined criteria. They serve as a central point for tracking, managing, and resolving service impairments.\n\n**Key characteristics:**\n-   Automatically triggered by monitor failures.\n-   Aggregates related failure events for a single monitor.\n-   Provides a clear status of service health.\n\n## Incident Triggering\n\nAn incident is triggered when a significant percentage of recent monitoring checks for a given monitor report a failed status. This mechanism prevents false positives from transient network issues.\n\n**Trigger Condition:**\n-   **Failure Threshold:** An incident is initiated when at least 50% of the checks within a defined window (e.g., the last `N` checks or within a `T` duration) have reported a `failure` or `degraded` status.\n\n## Incident Lifecycle and States\n\nIncidents progress through several states reflecting their current resolution status. These states are managed through status reports (see [Status Report Reference](/reference/status-report)).\n\n**Primary States:**\n-   `investigating`: The incident has been detected, and the team is actively looking into the root cause.\n-   `identified`: The root cause of the incident has been identified.\n-   `monitoring`: A fix has been deployed or a mitigation is in place, and the service is being monitored to confirm resolution.\n-   `resolved`: The incident has been fully resolved, and the service is operating normally.\n\n## Properties\n\nWhile an incident is active, it collects and displays key information related to the service disruption.\n\n-   **Monitor Association:** Each incident is directly linked to the monitor that triggered it, providing immediate context to the affected service.\n-   **Start Time:** Timestamp indicating when the incident was first detected and created.\n-   **Status Reports:** A chronological log of all updates and state changes applied to the incident.\n-   **Impacted Locations:** Details on the geographical regions from which the monitor reported failures.\n\n## Related resources\n\n- **[Status Report Reference](/reference/status-report)** - Details on how incident statuses are managed and reported.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/location.mdx",
    "content": "---\ntitle: Location Reference\ndescription: Complete technical specification for Location monitoring\n---\n\n## Overview\n\nOpenStatus monitors your endpoints from multiple global locations to ensure accurate uptime and latency reporting. Each monitoring location corresponds to a Fly.io region, with both IPv4 and IPv6 addresses available for each.\n\nYou can use these locations to:\n- Configure region-specific checks\n- Allowlist monitoring IPs in your firewall\n- Understand where requests originate during synthetic monitoring\n\n### Fly.io Regions & Monitoring IPs\n\nBelow is the complete list of regions used for monitoring, along with their associated IPv4 and IPv6 addresses.\n\n| Region Code | Location Name      | IPv4 Address      | IPv6 Address                   |\n|-------------|-------------------|-------------------|-------------------------------|\n| ams         | Amsterdam         | 209.71.64.1       | 2a09:8280:e601:1:0:22:b79b:0  |\n| arn         | Stockholm         | 209.71.98.189     | 2a09:8280:e602:1:0:22:b79b:0  |\n| bom         | Mumbai            | 209.71.68.172     | 2a09:8280:e605:1:0:22:b79b:0  |\n| cdg         | Paris             | 209.71.86.183     | 2a09:8280:e607:1:0:22:b79b:0  |\n| dfw         | Dallas            | 209.71.71.89      | 2a09:8280:e609:1:0:22:b79b:0  |\n| ewr         | Newark            | 209.71.69.221     | 2a09:8280:e610:1:0:22:b79b:0  |\n| fra         | Frankfurt         | 209.71.90.204     | 2a09:8280:e612:1:0:22:b79b:0  |\n| gru         | São Paulo         | 209.71.94.28      | 2a09:8280:e615:1:0:22:b79b:0  |\n| iad         | Washington, D.C.  | 209.71.81.6       | 2a09:8280:e618:1:0:22:b79b:0  |\n| jnb         | Johannesburg      | 209.71.83.120     | 2a09:8280:e620:1:0:22:b79b:0  |\n| lax         | Los Angeles       | 209.71.91.96      | 2a09:8280:e621:1:0:22:b79b:0  |\n| lhr         | London            | 209.71.85.82      | 2a09:8280:e622:1:0:22:b79b:0  |\n| nrt         | Tokyo             | 209.71.88.150     | 2a09:8280:e625:1:0:22:b79b:0  |\n| ord         | Chicago           | 209.71.89.1       | 2a09:8280:e626:1:0:22:b79b:0  |\n| sin         | Singapore         | 209.71.80.112     | 2a09:8280:e632:1:0:22:b79b:0  |\n| sjc         | San Jose          | 209.71.101.37     | 2a09:8280:e633:1:0:22:b79b:0  |\n| syd         | Sydney            | 209.71.97.108     | 2a09:8280:e634:1:0:22:b79b:0  |\n| yyz         | Toronto           | 209.71.99.51      | 2a09:8280:e637:1:0:22:b79b:0  |\n\n\n---\n\n## Railway Regions & Monitoring IPs\n\nBelow is the complete list of Railway regions used for monitoring, along with their associated IPv4 addresses.\n\n| Region Code              | Location Name     | IPv4 Address      |\n|--------------------------|------------------|-------------------|\n| europe-west4-drams3a     | Europe West      | 208.77.244.15     |\n| asia-southeast1-eqsg3a   | Asia Southeast   | 208.77.246.15     |\n| us-east4-eqdc4a          | US East          | 162.220.234.15    |\n| us-west2                 | US West          | 162.220.232.99    |\n\n---\n\n## Koyeb Regions & Monitoring IPs\n\nKoyeb does not provide static IP addresses for their regions. For more information, please refer to Koyeb's [documentation](https://www.koyeb.com/docs/faqs/general#i-want-to-restrict-access-to-a-database-or-other-application-by-ip-address-what-ip-addresses-does-koyeb-use).\n\n---\n\n**Note:**  \n\n- Both IPv4 and IPv6 addresses are provided for allowlisting and diagnostics.\n- Location names are for reference and may be used in the dashboard UI.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/notification.mdx",
    "content": "---\ntitle: Notification Channels Reference\ndescription: Technical specification for OpenStatus notification channels and alert payloads.\n---\n\n## Overview\n\nNotifications in OpenStatus provide real-time alerts regarding changes in monitor status, such as recovery from an outage or detection of a new failure. By default, no notification channels are configured in a new workspace. Users must configure and enable specific channels to receive alerts.\n\n## Notification Channels\n\nEach notification channel requires specific configuration parameters to enable alert delivery.\n\n### Slack\n\nIntegrates with Slack to send alerts to a designated channel.\n\n**Configuration:**\n-   **Incoming Webhook URL:** (Required) A [Slack incoming webhook URL](https://api.slack.com/incoming-webhooks) where notifications will be posted.\n    **Example**: `https://hooks.slack.com/services/XXX/YYY/ZZZ`\n\nYou can [download the openstatus logo](https://www.openstatus.dev/assets/logos/openstatus.jpeg) to add a custom logo.\n\n### Email\n\nSends alerts directly to a specified email address.\n\n**Configuration:**\n-   **Email Address:** (Required) The recipient's email address.\n\n### Discord\n\nDelivers alerts to a Discord channel via a webhook.\n\n**Configuration:**\n-   **Webhook URL:** (Required) A [Discord webhook URL](https://support.discord.com/hc/en-us/articles/228383668) for the target channel.\n    **Example:** `https://discordapp.com/api/webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyz1234567890`\n\nYou can [download the openstatus logo](https://www.openstatus.dev/assets/logos/openstatus.jpeg) to add a custom logo.\n\n### Grafana OnCall IRM \n\nSends notifications to a Grafana OnCall IRM.\n\n**Configuration:**\n-   **Webhook URL:** (Required) A [Grafana OnCall IRM webhook URL](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/configure/integrations/webhooks/incoming-webhooks/oncall-webhooks/).\n\n\n### Google Chat\n\nSends notifications to a Google Chat space.\n\n**Configuration:**\n-   **Webhook URL:** (Required) A [Google Chat webhook URL](https://developers.google.com/workspace/chat/quickstart/webhooks) for the target space.\n\n### SMS\n\nSends alerts as SMS messages to a mobile phone number.\n\n**Configuration:**\n-   **Phone Number:** (Required) The recipient's phone number in international format (e.g., `+14155552671`).\n\n**Note:** SMS delivery can vary by country due to provider routing. Contact support if delivery issues are encountered. WhatsApp notifications may be an alternative.\n\n### WhatsApp\n\nSends alerts as WhatsApp messages to a mobile phone number.\n\n**Configuration:**\n-   **Phone Number:** (Required) The recipient's phone number in international format (e.g., `+14155552671`).\n\n### Telegram\n\nDelivers alerts to a specified Telegram chat.\n\n**Configuration:**\n-   **Chat ID:** (Required) The unique identifier for the Telegram chat. This typically requires manual retrieval; users can ask `@raw_info_bot` for their chat ID.\n\n**Bot ID:** The official OpenStatus Telegram bot ID is `@openstatushq_bot`.\n\n### Webhook\n\nSends HTTP POST requests to a custom endpoint with a JSON payload.\n\n**Configuration:**\n-   **URL:** (Required) The endpoint URL to which the webhook payload will be sent.\n-   **Headers:** (Optional) Custom HTTP headers to include with the webhook request (key-value pairs).\n\n#### Notification Payloads\n\nWebhook notifications utilize specific JSON payloads for different monitor status changes.\n\n##### Monitor Recovery Payload\n\nSent when a monitor recovers from a `degraded` or `error` state.\n\n```json\n{\n  \"monitor\": {\n    \"id\": 1,\n    \"name\": \"test\",\n    \"url\": \"http://openstat.us\"\n  },\n  \"cronTimestamp\": 1744023705307,\n  \"status\": \"recovered\",\n  \"statusCode\": 200,\n  \"latency\": 1337\n}\n```\n\n**Payload Fields:**\n\n| Field         | Type     | Description                                                          |\n| :------------ | :------- | :------------------------------------------------------------------- |\n| `monitor.id`  | `number` | Unique identifier of the monitor.                                    |\n| `monitor.name`| `string` | Name of the monitor.                                                 |\n| `monitor.url` | `string` | The URL or URI being monitored.                                      |\n| `cronTimestamp`| `number` | Timestamp of the check execution in milliseconds since epoch.        |\n| `status`      | `string` | Indicates the monitor status: `\"recovered\"`.                        |\n| `statusCode`  | `number` | (Optional) HTTP status code returned by the monitored service.        |\n| `latency`     | `number` | (Optional) Time taken to complete the check in milliseconds.         |\n\n##### Monitor Failure Payload\n\nSent when a monitor enters an `error` or `degraded` state.\n\n```json\n{\n  \"monitor\": {\n    \"id\": 1,\n    \"name\": \"test\",\n    \"url\": \"http://openstat.us\"\n  },\n  \"cronTimestamp\": 1744023705307,\n  \"status\": \"error\",\n  \"errorMessage\": \"Connection refused\"\n}\n```\n\n**Payload Fields:**\n\n| Field         | Type     | Description                                                          |\n| :------------ | :------- | :------------------------------------------------------------------- |\n| `monitor.id`  | `number` | Unique identifier of the monitor.                                    |\n| `monitor.name`| `string` | Name of the monitor.                                                 |\n| `monitor.url` | `string` | The URL or URI being monitored.                                      |\n| `cronTimestamp`| `number` | Timestamp of the check execution in milliseconds since epoch.        |\n| `status`      | `string` | Indicates the monitor status: `\"degraded\"` or `\"error\"`.         |\n| `errorMessage`| `string` | (Optional) A description of the error encountered during the check.  |\n\n#### Zod Schema\n\nThe validation schema for webhook payloads:\n\n```ts\nimport { z } from \"zod\";\n\nexport const PayloadSchema = z.object({\n  monitor: z.object({\n    id: z.number(),\n    name: z.string(),\n    url: z.string(),\n  }),\n  cronTimestamp: z.number(),\n  status: z.enum([\"degraded\", \"error\", \"recovered\"]),\n  statusCode: z.number().optional(),\n  latency: z.number().optional(),\n  errorMessage: z.string().optional(),\n});\n```\n\n### OpsGenie\n\nIntegrates with OpsGenie for incident management.\n\n**Configuration:**\n-   **API Key:** (Required) An API key obtained from your OpsGenie account.\n\n### PagerDuty\n\nIntegrates with PagerDuty for incident alerting.\n\n**Configuration:**\n-   **Integration Steps:** (Required) Follow the specific integration steps provided within the PagerDuty workflow to set up this channel.\n\n### Ntfy\n\nSends notifications to an Ntfy topic.\n\n**Configuration:**\n-   **Ntfy Topic:** (Required) The topic name to which notifications will be published.\n-   **Custom Server URL:** (Optional) The URL of a custom Ntfy server if not using the default.\n-   **Bearer Token:** (Optional) An authentication token for accessing the Ntfy server.\n\n## Related resources\n\n- **[Incident Reference](/reference/incident)** - Information about incident creation and management.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/page-components.mdx",
    "content": "---\ntitle: Page Components Reference\ndescription: Complete specification for OpenStatus page components on status pages.\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n## Overview\n\nPage components are the individual elements displayed on your status page that show the operational status of your services. They provide a flexible way to organize and present both monitored services and static content on your status page.\n\n**Key features:**\n- Support for both monitor-linked and static components.\n- Organize components into logical groups.\n- Custom ordering and arrangement.\n- Individual status tracking with incidents, reports, and maintenances.\n- Granular control over what appears on your status page.\n\n## Component Types\n\nPage components come in two distinct types, each serving different purposes on your status page.\n\n### Monitor Components\n\n**Type:** `monitor`\n\nMonitor components are linked to an active monitor in your workspace. They automatically inherit the monitor's status and display real-time health information.\n\n**Characteristics:**\n- Display live monitor status (up, degraded, down).\n- Show active incidents from the linked monitor.\n- Include historical uptime data.\n- Reflect the monitor's current operational state.\n- Automatically update when the monitor changes.\n\n**Use cases:**\n- Displaying API endpoint health.\n- Showing website availability.\n- Tracking critical service dependencies.\n- Monitoring infrastructure components.\n\n#### Configuring Monitor Components\n\nWhen you create a monitor component, you link it to an existing monitor in your workspace. This connection provides several benefits:\n\n**Automatic incident tracking:**\nWhen your monitor detects a failure (connection timeout, HTTP error, assertion failure), an incident is automatically created and displayed on the status page. The component will show an **error** status until the monitor recovers.\n\n**Real-time status updates:**\nThe component reflects the current operational state of your monitor. If the monitor is actively checking and healthy, your visitors see a **success** status. If checks fail, they immediately see the issue.\n\n**Historical data visualization:**\nMonitor components display historical uptime data through status trackers. Depending on your [tracker configuration](/tutorial/how-to-configure-status-page#1-tracker-configuration), you can show:\n- **Absolute bar with duration card**: Shows the time spent in each status (success, error, degraded, maintenance).\n- **Absolute bar with request card**: Shows the number of successful vs. failed requests.\n- **Manual bar**: Shows only the most significant status of each day.\n\n**Uptime calculations:**\nMonitor components calculate uptime percentages based on:\n- Duration of successful vs. failed checks (for duration-based tracking).\n- Number of successful vs. failed requests (for request-based tracking).\n- Includes incidents and status reports in the calculation.\n\n**Monitor selection:**\nWhen adding a monitor component, the dashboard shows you available monitors with indicators for:\n- **Public/Private status**: Whether the monitor is already public.\n- **Active status**: Only active monitors can be linked.\n- **Already linked**: Monitors already used on this status page are unavailable.\n\n<Aside type=\"tip\">\nYou can customize the component name to be different from the monitor name. For example, your monitor might be named \"prod-api-health-check\" internally, but the component can display as \"API Server\" for your visitors.\n</Aside>\n\n**Status hierarchy:**\n1. **Error** - Active incidents from the linked monitor.\n2. **Degraded** - Unresolved status reports affecting this component.\n3. **Info** - Ongoing scheduled maintenance.\n4. **Success** - Healthy and operational.\n\n**What affects monitor components:**\n- ✅ Automatic incidents (from monitor failures)\n- ✅ Manual status reports\n- ✅ Scheduled maintenances\n\n### Static Components\n\n**Type:** `static`\n\nStatic components are independent elements not linked to any monitor. They allow you to display services or systems that you manually manage through status reports and maintenance windows only.\n\n**Characteristics:**\n- No automatic status updates.\n- Status controlled exclusively by manual status reports and scheduled maintenances.\n- Useful for third-party services or manual tracking.\n- Do not display incidents (no automatic incident creation).\n- Provide flexibility for non-monitored services.\n\n**Use cases:**\n- Third-party service dependencies (e.g., payment providers like Stripe, email services like SendGrid).\n- Manual status tracking for systems without monitors.\n- Services monitored through external tools.\n- Components that only need maintenance window communication.\n- Legacy systems without API endpoints to monitor.\n\n<Aside type=\"caution\">\nStatic components **only** respond to manual status reports and scheduled maintenances. They never automatically detect issues or create incidents. If you need automatic failure detection, use a monitor component instead.\n</Aside>\n\n#### Managing Static Components\n\nStatic components give you full manual control over what your visitors see:\n\n**Status reports:**\nCreate status reports to indicate issues or degraded performance for static components. For example:\n- \"Stripe payment processing experiencing delays\" (degraded status).\n- \"Email delivery service partially unavailable\" (degraded status).\n\nOnce you resolve the issue and mark the status report as resolved, the component returns to a success status.\n\n**Maintenance windows:**\nSchedule maintenance windows to inform visitors about planned downtime:\n- \"Scheduled database backup - Sunday 2:00 AM - 4:00 AM\" (info status).\n- \"Third-party CDN maintenance window\" (info status).\n\nDuring the maintenance window, the component shows an info status. After the window ends, it returns to its previous status.\n\n**No automatic monitoring:**\nStatic components do not perform any health checks or generate incidents. You are responsible for:\n- Monitoring the service through other means.\n- Creating status reports when issues occur.\n- Updating reports when issues are resolved.\n- Communicating maintenance windows in advance.\n\n**Status hierarchy:**\n1. **Degraded** - Unresolved status reports affecting this component.\n2. **Info** - Ongoing scheduled maintenance.\n3. **Success** - No active reports or maintenances.\n\n**What affects static components:**\n- ✅ Manual status reports\n- ✅ Scheduled maintenances\n- ❌ Automatic incidents (not supported)\n\n## Component Groups\n\nComponent groups allow you to organize related page components into logical sections on your status page. Groups improve readability and help visitors understand your service architecture.\n\n**Benefits:**\n- Visual organization of related services.\n- Collapsible sections for better page structure.\n- Independent ordering within groups.\n- Clear service categorization.\n\n**Examples of grouping strategies:**\n\n| Group Name | Components |\n|------------|------------|\n| **API Services** | Authentication API, Data API, WebSocket API |\n| **Infrastructure** | Database, Cache, Message Queue |\n| **External Dependencies** | Payment Provider, Email Service, CDN |\n| **Regional Services** | US Region, EU Region, APAC Region |\n\n**Group configuration:**\n- **Name:** The group heading displayed on your status page.\n- **Order:** Position of the group relative to other groups and ungrouped components.\n- **Components:** The page components contained within this group.\n## Events and Status\n\nPage components can be affected by up to three types of events that influence their displayed status. The type of component determines which events apply:\n\n| Event Type | Monitor Components | Static Components |\n|------------|-------------------|-------------------|\n| **Incidents** | ✅ Automatic | ❌ Not supported |\n| **Status Reports** | ✅ Manual | ✅ Manual |\n| **Maintenances** | ✅ Manual | ✅ Manual |\n\n### Incidents\n\n**Applies to:** Monitor components only\n\nIncidents are automatically generated when a monitor detects a failure. They represent unplanned outages or degraded performance discovered through active monitoring.\n\n**How incidents are created:**\n- Monitor check fails (connection timeout, HTTP error, DNS failure).\n- Monitor assertion fails (wrong status code, unexpected response body).\n- Monitor reaches degraded threshold (response time too slow).\n\n**Status impact:** Components with active incidents show an **error** status. This takes the highest priority in the status hierarchy.\n\n**Resolution:** Incidents are automatically resolved when the monitor recovers and checks succeed again.\n\n<Aside>\nStatic components **never** generate incidents because they are not linked to monitors. Use status reports for manual issue tracking on static components.\n</Aside>\n\n### Status Reports\n\n**Applies to:** Both monitor and static components\n\nStatus reports are manually created updates about component health or issues. They provide a way to communicate problems that may not trigger automatic monitoring or to manually report issues with static components.\n\n**Status impact:** Components with unresolved status reports show a **degraded** status (unless overridden by an incident for monitor components).\n\n**Use cases for monitor components:**\n- Reporting known issues that don't cause complete outages.\n- Communicating performance degradation not captured by monitoring.\n- Providing context for intermittent issues.\n\n**Use cases for static components:**\n- Reporting third-party service issues (e.g., \"Stripe processing delays\").\n- Communicating external service degradation.\n- Announcing partial outages of non-monitored systems.\n\n**Attaching to components:** When creating a status report, you can select which components are affected. Multiple components can be attached to a single report.\n\n### Maintenances\n\n**Applies to:** Both monitor and static components\n\nMaintenances are scheduled maintenance windows that you create in advance. They inform visitors about planned downtime or service interruptions for both monitored and static components.\n\n**Status impact:** Components with ongoing maintenances show an **info** status (unless overridden by incidents or reports).\n\n**Use cases for monitor components:**\n- Scheduled system upgrades that will cause downtime.\n- Infrastructure changes that affect monitored services.\n- Planned deployments requiring service restarts.\n\n**Use cases for static components:**\n- Third-party maintenance windows (e.g., \"Payment provider scheduled maintenance\").\n- External service upgrade notifications.\n- Planned downtime for non-monitored dependencies.\n\n**Scheduling:** Maintenances have a defined start and end time. The info status automatically appears during the window and disappears when the maintenance ends.\n\n**Attaching to components:** When creating a maintenance, you select which components will be affected. This allows you to communicate maintenance impact across multiple services.\n\n## Managing Components\n\n### Adding Components\n\nYou can add components to your status page in two ways:\n\n1. **Individual components:** Add a single component outside of any group.\n2. **Components within groups:** Add a component directly into a new or existing group.\n\nWhen adding a monitor component, you can only select from monitors that:\n- Are currently active.\n- Have not been deleted.\n- Are not already linked to another component on this status page.\n\n### Reordering Components\n\nComponents and groups can be reordered using drag-and-drop functionality in the dashboard. The order determines how they appear on your status page from top to bottom.\n\n**Ordering tips:**\n- Place your most critical services at the top.\n- Group related services together.\n- Consider visitor priorities when ordering.\n\n### Editing Components\n\nYou can modify the following properties of existing components:\n- Component name and description.\n- Group assignment (move between groups or make ungrouped).\n- Display order.\n\n**Note:** You cannot change a component's type (monitor to static or vice versa) after creation. To change types, delete the component and create a new one.\n\n### Deleting Components\n\nWhen you delete a component, any associations with status reports and maintenances are automatically removed. The linked monitor (if applicable) is not deleted and remains available in your workspace.\n\n**Warning:** Deletion is permanent and cannot be undone. Ensure you want to remove the component before confirming deletion.\n\n## Deprecation Notice\n\nThe legacy monitor-only system for status pages is deprecated in favor of the more flexible page component system.\n\n**Deprecated approach:**\n- Status pages directly referenced monitors.\n- No support for static content.\n- Limited organizational flexibility.\n\n**Current approach (page components):**\n- Status pages contain page components.\n- Components can be monitors or static content.\n- Full support for grouping and custom ordering.\n- Better separation between monitoring and status page presentation.\n\n### API Compatibility\n\n**v1 API (backward compatibility):**\nThe v1 API continues to display `monitorIds` and `monitors` fields in status page responses to avoid breaking changes for existing integrations. However, these fields now only include page components that are explicitly of type `monitor`. Static components are not included in these legacy fields.\n\n**Future API versions:**\nNewer API versions will primarily use the `pageComponents` structure. The legacy `monitors` and `monitorIds` fields will be removed in future API versions. We recommend migrating your integrations to use `pageComponents` for full feature support.\n\n## Related resources\n\n- **[Status Page Reference](/reference/status-page)** - Complete status page configuration reference.\n- **[Status Report Reference](/reference/status-report)** - Details on creating and managing status reports.\n- **[Create Status Page](/tutorial/how-to-create-status-page)** - Step-by-step tutorial on creating a status page.\n- **[HTTP Monitor Reference](/reference/http-monitor)** - Technical specification for HTTP monitors that can be linked to components.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/private-location.mdx",
    "content": "---\ntitle: Private Location Reference\ndescription: Technical specification for configuring and utilizing private monitoring locations.\n---\n\n## Overview\n\nA private location in OpenStatus enables users to deploy monitoring probes within their own infrastructure or private networks. This capability is essential for monitoring internal services, APIs, or systems that are not publicly accessible from the internet, such as those behind firewalls or within a Virtual Private Cloud (VPC).\n\n**Key benefits:**\n-   **Internal Monitoring:** Monitor services running on private networks.\n-   **Security:** Keep sensitive internal endpoints protected from public exposure.\n-   **Compliance:** Meet specific regulatory or security compliance requirements by controlling data paths.\n-   **Reduced Latency:** Conduct checks closer to your services for more accurate performance metrics.\n\n## How it Works\n\nWhen a private location is configured, OpenStatus provides a mechanism (e.g., a container image or agent) that you deploy within your private environment. This deployed component acts as a local monitoring probe, executing checks on behalf of your OpenStatus account.\n\n1.  **Deployment:** You deploy the OpenStatus private probe within your chosen infrastructure (e.g., a Docker container on a server, a Kubernetes pod).\n2.  **Secure Connection:** The private probe establishes a secure, outbound-only connection to the OpenStatus platform, eliminating the need for inbound firewall rules.\n3.  **Check Execution:** OpenStatus dispatches monitoring tasks to your private probe via this secure connection. The probe then executes the configured checks against your internal services.\n4.  **Result Reporting:** The private probe securely sends the monitoring results (e.g., status, latency, response data) back to the OpenStatus platform for processing, alerting, and visualization.\n\n## Configuration\n\nDetailed steps for setting up a private location involve:\n\n1.  **Probe Deployment:** Provisioning a server or container environment within your private network.\n2.  **Agent Installation:** Deploying the OpenStatus private probe agent (e.g., Docker image) onto your infrastructure.\n3.  **Authentication:** Configuring the probe with necessary API keys or tokens to securely authenticate with your OpenStatus workspace.\n4.  **Network Access:** Ensuring the deployed probe has network access to the internal services it needs to monitor, as well as outbound access to the OpenStatus platform.\n\n**Example Use Cases:**\n-   Monitoring an internal REST API that is only accessible from within your corporate network.\n-   Checking the health of a database server running on a private subnet.\n-   Performing synthetic transactions on an internal web application before it's exposed publicly.\n\n## Related resources\n\n- **[How to Deploy Probes on Cloudflare Containers](/guides/how-to-deploy-probes-cloudflare-containers)** - A guide for deploying private probes using Cloudflare Workers/Containers. (Example deployment guide)\n- **[CLI Reference](/reference/cli-reference)** - Manage monitors as code, including those utilizing private locations.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/status-page.mdx",
    "content": "---\ntitle: Status Page Reference\ndescription: Complete technical specification for OpenStatus status page configuration.\n---\n\n## Overview\n\nA status page is a dedicated web interface provided by OpenStatus that publicly displays the operational status of your services and systems. It serves as a transparent communication tool during incidents and for showcasing overall service health.\n\n**Key features:**\n-   Real-time service status updates.\n-   Incident communication and history.\n-   Customizable branding and domain.\n-   Multiple access control options.\n\n## Configuration\n\nOpenStatus provides several configuration options to customize your status page's appearance, accessibility, and functionality.\n\n### Slug\n\n**Type:** String (required)\n**Format:** URL-friendly string (e.g., `my-service-status`)\n\nA unique identifier that forms part of your status page's default URL. For example, a slug of `status` will result in a URL like `https://status.openstatus.dev`.\n\n### Custom Domain\n\n**Type:** String (optional)\n**Format:** Valid domain name (e.g., `status.example.com`)\n\nAllows you to host your status page on a custom domain. Once configured, your status page will be accessible at `https://your-custom-domain.com`.\n\n### Password (Basic Auth)\n\n**Type:** String (optional)\n\nEnables basic password protection for your status page. If a password is set, users will be redirected to a login page (`/login`) to gain access. The password is stored in a cookie upon successful authentication.\n\n**Sharing with password:** You can provide direct access by appending the password as a URL search parameter: `https://[slug].openstatus.dev/?pw=your-secret-password`. This method is also useful for authenticating private RSS feeds.\n\n### Magic Link (Session Auth)\n\n**Type:** Boolean (add-on feature)\n\nRestricts access to your status page to users with approved email domains. Users receive a magic link via email, which, upon clicking, authenticates them via a session token. This feature is typically available as a paid add-on for specific plans.\n\n### Favicon\n\n**Type:** Image file (e.g., `.ico`, `.png`)\n\nAllows you to upload a custom favicon that will appear in browser tabs and bookmarks for your status page.\n\n### JSON Feed\n\n**Type:** Read-only endpoint\n**Format:** JSON\n\nProvides a machine-readable JSON representation of your status page data. This feed can be accessed by appending `/feed/json` to your status page URL.\n\n**Example:** `https://status.openstatus.dev/feed/json`\n\n**Deprecation Notice:**\n\nThe following fields are deprecated and will be removed in a future version:\n\n- **`monitors`** (top-level): Use `pageComponents` instead, which provides a more flexible component-based structure that supports both monitors and external services.\n- **`maintenances[].monitors`**: Use `maintenances[].pageComponents` instead, which references page component IDs rather than monitor IDs.\n- **`statusReports[].monitors`**: Use `statusReports[].pageComponents` instead, which references page component IDs rather than monitor IDs.\n\nThese deprecated fields are currently maintained for backward compatibility but may be removed in future versions.\n\n### SSH Command\n\n**Type:** Command-line utility\n\nAllows you to quickly check the current status page status directly from your terminal using an SSH command.\n\n**Usage:**\n\n```bash\nssh [slug]@ssh.openstatus.dev\n```\n\n**Example:** `ssh my-service@ssh.openstatus.dev`\n\n### White Label\n\n**Type:** Boolean (add-on feature)\n\nRemoves the \"powered by openstatus.dev\" footer from your status page, providing a fully branded experience. This feature is typically available as a paid add-on for Starter and Pro plans and is enabled via your workspace settings, affecting all status pages within that workspace.\n\n## Related resources\n\n- **[Create Status Page](/tutorial/how-to-create-status-page)** - Step-by-step tutorial on creating a status page.\n- **[How to Configure Status Page](/tutorial/how-to-configure-status-page)** - Guide on advanced status page configuration.\n- **[Status Report Reference](/reference/status-report)** - Details on how incident statuses are managed and reported.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/status-report.mdx",
    "content": "---\ntitle: Status Report Reference\ndescription: Technical specification for incident status updates within OpenStatus.\n---\n\n## Overview\n\nA status report is a chronological update or event associated with an ongoing incident in OpenStatus. These reports are crucial for communicating the progress of an incident, from initial detection to final resolution, providing transparency to stakeholders.\n\n**Purpose:**\n-   To document the lifecycle and progress of an incident.\n-   To communicate current incident status and actions taken.\n-   To provide historical context for post-incident analysis.\n\n## Relationship to Incidents\n\nEach status report is directly linked to a specific incident. As an incident progresses through its resolution process, new status reports are added to provide updates, often accompanied by a change in the incident's overall status.\n\n## Configuration and Properties\n\nA status report consists of several key properties that define its content and context.\n\n### Status\n\n**Type:** Enumerated String (required)\n\nRepresents the current stage or state of the associated incident at the time the report is issued. The available statuses are:\n\n-   `investigating`: The incident has been detected, and the team is actively looking into the root cause.\n-   `identified`: The root cause of the incident has been identified.\n-   `monitoring`: A fix has been deployed or a mitigation is in place, and the service is being monitored to confirm resolution.\n-   `resolved`: The incident has been fully resolved, and the service is operating normally.\n\n### Date\n\n**Type:** Datetime (required)\n**Format:** ISO 8601 (e.g., `2026-01-05T12:30:00Z`)\n\nThe timestamp indicating when the status report was created or when the reported status took effect. This provides a clear timeline for incident progression.\n\n### Message\n\n**Type:** String (required)\n\nA descriptive message detailing the update, actions taken, or any relevant information regarding the incident at the time of the report. This message should be clear and concise, providing context to the status change.\n\n**Example Messages:**\n-   `\"Initial detection of elevated error rates on the API. Investigating potential upstream issues.\"`\n-   `\"Root cause identified as a misconfigured caching layer. Working on a rollback.\"`\n-   `\"Fix deployed to production. Monitoring service health for full recovery.\"`\n-   `\"All services restored to normal operation. Incident resolved.\"`\n\n## Related resources\n\n- **[Incident Reference](/reference/incident)** - Detailed information on incident creation and lifecycle.\n- **[Status Page Reference](/reference/status-page)** - Information on how status reports are displayed on public status pages.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/subscriber.mdx",
    "content": "---\ntitle: Subscriber Reference\ndescription: Technical specification for managing status page subscribers and their notifications.\n---\n\n## Overview\n\nA subscriber in OpenStatus is an entity (typically a user or an integration) that opts to receive real-time notifications and updates regarding incidents and status changes on a specific status page. Subscribers play a crucial role in maintaining transparent communication during service disruptions.\n\n**Key functions of subscribers:**\n-   Receive automated alerts when monitor statuses change or incidents are updated.\n-   Stay informed about service health without actively monitoring the status page.\n-   Choose preferred notification channels for receiving updates.\n\n## Subscription Process\n\nUsers typically subscribe to a status page's updates through a dedicated interface provided on the status page itself. The process involves:\n\n1.  **Inputting Contact Information:** Providing an email address, phone number, or other contact details depending on the available notification channels.\n2.  **Opt-in Confirmation:** Confirming their subscription, often through a verification link sent to the provided contact to prevent unwanted subscriptions.\n3.  **Channel Selection (Optional):** Selecting which specific notification channels (e.g., email, SMS, Slack webhook) they wish to receive updates through, if multiple options are available.\n\n## Notification Types Received\n\nSubscribers receive notifications for key events affecting the monitored services linked to the status page:\n\n-   **Incident Creation:** When a new incident is detected and published.\n-   **Incident Updates:** When status reports are published for an ongoing incident (e.g., status changes from `investigating` to `identified`, `monitoring`, or `resolved`).\n-   **Monitor Status Changes:** Direct alerts for individual monitor status changes if configured to do so (less common for public subscribers).\n\n## Subscriber Management\n\nStatus page administrators can manage their subscriber lists, including:\n\n-   **Viewing Subscribers:** Accessing a list of all active subscribers for a status page.\n-   **Adding/Removing Subscribers:** Manually adding or removing subscribers.\n-   **Communication:** Sending ad-hoc notifications to the subscriber list (if supported by the platform).\n\n## Related resources\n\n- **[Status Page Reference](/reference/status-page)** - Detailed information on managing and configuring status pages.\n- **[Notification Channels Reference](/reference/notification)** - Technical specifications for the various notification delivery methods.\n- **[Incident Reference](/reference/incident)** - Information about incident creation and management.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/tcp-monitor.mdx",
    "content": "---\ntitle: TCP Monitor Reference\ndescription: Complete technical specification for TCP service monitoring.\n---\n\n## Overview\n\nA TCP Monitor is a component that establishes a connection to a specified TCP endpoint (IP address and port) to verify its reachability and responsiveness. This is fundamental for monitoring the availability of services that communicate over TCP, such as databases, mail servers, and custom network services.\n\n**Use cases:**\n-   Database server availability checks (e.g., PostgreSQL, MySQL).\n-   Mail server (SMTP, IMAP, POP3) reachability.\n-   Custom application service port monitoring.\n-   Validating network connectivity to specific endpoints.\n\n## Configuration\n\n### URI\n\n**Type:** String (required)\n**Format:** Hostname or IP address with port (e.g., `example.com:8080`, `192.168.1.1:22`)\n\nThe endpoint of the TCP service you want to monitor. This includes the hostname or IP address and the port number.\n\n**Examples:**\n- `openstat.us:443`\n- `db.internal:5432`\n- `10.0.0.5:3306`\n\n### Regions\n\n**Type:** Array of Strings (required)\n**Format:** Region identifiers (e.g., `iad`, `jnb`)\n\nThe geographical regions from which the TCP connection attempt will be initiated. This allows for verification of service availability and network latency across different global locations.\n\n__Africa__\n\n- Johannesburg, South Africa 🇿🇦 (free)\n\n__Asia__\n\n- Hong Kong, Hong Kong 🇭🇰 (free)\n- Mumbai, India 🇮🇳\n- Singapore, Singapore 🇸🇬\n- Tokyo, Japan 🇯🇵\n\n__Europe__\n\n- Amsterdam, Netherlands 🇳🇱 (free)\n- Bucharest, Romania 🇷🇴\n- Frankfurt, Germany 🇩🇪\n- London, United Kingdom 🇬🇧\n- Madrid, Spain 🇪🇸\n- Paris, France 🇫🇷\n- Stockholm, Sweden 🇸🇪\n- Warsaw, Poland 🇵🇱\n\n__North America__\n\n- Ashburn, Virginia, USA 🇺🇸 (free)\n- Atlanta, Georgia, USA 🇺🇸\n- Boston, Massachusetts, USA 🇺🇸\n- Chicago, Illinois, USA 🇺🇸\n- Dallas, Texas, USA 🇺🇸\n- Denver, Colorado, USA 🇺🇸\n- Guadalajara, Mexico 🇲🇽\n- Los Angeles, California, USA 🇺🇸\n- Miami, Florida, USA 🇺🇸\n- Montreal, Canada 🇨🇦\n- Phoenix, Arizona, USA 🇺🇸\n- Queretaro, Mexico 🇲🇽\n- Seattle, Washington, USA 🇺🇸\n- San Jose, California, USA 🇺🇸\n- Toronto, Canada 🇨🇦\n\n__South America__\n\n- Bogota, Colombia 🇨🇴\n- Buenos Aires, Argentina 🇦🇷\n- Rio de Janeiro, Brazil 🇧🇷\n- Sao Paulo, Brazil 🇧🇷 (free)\n- Santiago, Chile 🇨🇱\n\n__Oceania__\n\n- Sydney, Australia 🇦🇺 (free)\n\n### Frequency\n\n**Type:** String (required)\n**Format:** Duration string (e.g., `30s`, `1m`, `1h`)\n\nThe interval at which the TCP monitor will attempt to connect to the target URI. Supported frequencies:\n- `30 seconds`\n- `1 minute`\n- `5 minutes`\n- `10 minutes`\n- `30 minutes`\n- `1 hour`\n\n### Response Time Thresholds\n\n#### Timeout\n\n**Type:** Duration (optional)\n**Default:** `45 seconds`\n\nThe maximum duration to wait for a successful TCP connection. If the connection cannot be established within this time, the check is considered a failure.\n\n#### Degraded\n\n**Type:** Duration (optional)\n\nThe duration after which a TCP connection attempt is considered to be in a degraded performance state. This allows for early warning of network latency or service slowdowns.\n\n### Retry\n\n**Type:** Integer (optional)\n**Default:** `3`\n\nThe number of times the monitor will automatically retry a failed TCP connection attempt before reporting a definitive error. For example: `3`\n\n### OpenTelemetry\n\nConfigures the export of monitoring metrics to an OpenTelemetry-compatible observability platform.\n\n#### OTLP Endpoint\n\n**Type:** String (optional)\n**Protocol:** HTTP only\n\nThe OTLP (OpenTelemetry Protocol) endpoint URL where collected metrics should be exported. Only HTTP endpoints are supported for metric export.\n\n#### OTLP Headers\n\n**Type:** Key-value pairs (optional)\n\nCustom headers to include when sending metrics to your OTLP endpoint. Commonly used for authentication or tenant identification.\n\n**Common example:**\n```\nAuthorization: Bearer <your_token>\n```\n\n## Related resources\n\n- **[Create Your First Monitor](/tutorial/how-to-create-monitor)** - Step-by-step tutorial on setting up a monitor.\n- **[CLI Reference](/reference/cli-reference)** - Guide to managing OpenStatus monitors programmatically using the command-line interface.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference/terraform.mdx",
    "content": "---\ntitle: Terraform Provider Reference\ndescription: Technical specification for the OpenStatus Terraform Provider.\n---\n\n## Overview\n\nThe OpenStatus Terraform provider enables you to manage your OpenStatus monitors and status pages programmatically using HashiCorp Terraform. This allows for Infrastructure as Code (IaC) practices, version control, and automated deployment of your monitoring configurations.\n\n**Key capabilities:**\n-   Define and manage OpenStatus monitors as Terraform resources.\n-   Automate the deployment and updates of monitoring configurations.\n-   Integrate OpenStatus into your existing IaC workflows.\n\n## Installation\n\nTo use the OpenStatus Terraform provider, declare it in your Terraform configuration file (`.tf`). Terraform will automatically download and install the provider when you run `terraform init`.\n\n```terraform\nterraform {\n  required_providers {\n    openstatus = {\n      source = \"openstatusHQ/openstatus\"\n      version = \"~> 0.1\" # Use the latest version\n    }\n  }\n}\n```\n\nFor the latest provider version, refer to the [official Terraform Registry](https://registry.terraform.io/providers/openstatusHQ/openstatus/latest).\n\n## Provider Configuration\n\nThe OpenStatus Terraform provider requires authentication via an API token.\n\n### `openstatus_api_token`\n\n**Type:** String (required)\n**Description:** Your OpenStatus API Access Token. This token is used to authenticate your Terraform requests with the OpenStatus API.\n\n**Example:**\n\n```terraform\nprovider \"openstatus\" {\n  openstatus_api_token = \"YOUR_OPENSTATUS_API_TOKEN\"\n}\n```\n\n## Resources\n\nThe provider currently supports managing `openstatus_monitor` resources.\n\n### `openstatus_monitor`\n\nManages an OpenStatus monitor. This resource allows you to define and control the parameters of a synthetic monitor.\n\n**Arguments:**\n\n| Argument      | Type                  | Required | Default | Description                                                                                                                                     |\n| :------------ | :-------------------- | :------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------- |\n| `url`         | `string`              | Yes      |         | The URL or URI of the endpoint to be monitored. Format depends on the monitor type (e.g., full URL for HTTP, host:port for TCP).          |\n| `regions`     | `list(string)`        | Yes      |         | A list of region identifiers (e.g., `\"iad\"`, `\"jnb\"`) from where the monitor checks will be performed.                                        |\n| `periodicity` | `string`              | Yes      |         | The frequency at which the monitor will perform checks. Supported values: `\"30s\"`, `\"1m\"`, `\"5m\"`, `\"10m\"`, `\"30m\"`, `\"1h\"`. |\n| `name`        | `string`              | Yes      |         | A human-readable name for the monitor.                                                                                                          |\n| `active`      | `bool`                | Yes      |         | Specifies whether the monitor is active (`true`) or paused (`false`).                                                                           |\n| `description` | `string` (optional)   | No       | `\"\"`    | A detailed description of the monitor's purpose or configuration.                                                                               |\n| `monitor_type` | `string`             | Yes      |         | The type of monitor to create. Supported values: `\"HTTP\"`, `\"TCP\"`, `\"DNS\"`                                                               |\n| `method`      | `string` (optional)   | No       | `\"GET\"` | (HTTP monitors only) The HTTP method to use for the request (e.g., `\"GET\"`, `\"POST\"`).                                                     |\n| `body`        | `string` (optional)   | No       | `\"\"`    | (HTTP monitors only) The request body to send for `POST`, `PUT`, `PATCH` methods.                                                              |\n| `headers`     | `map(string)` (optional)| No       | `{}`    | (HTTP monitors only) A map of custom HTTP headers to include with the request.                                                                  |\n| `timeout`     | `string` (optional)   | No       | `\"45s\"` | The maximum duration to wait for a response. Format: duration string (e.g., `\"30s\"`, `\"1m\"`).                                              |\n| `degraded_after`| `string` (optional) | No       | `\"\"`    | The duration after which a response is considered degraded. Format: duration string.                                                            |\n| `retries`     | `number` (optional)   | No       | `3`     | The number of times the monitor will retry a failed check.                                                                                      |\n| `public`      | `bool` (optional)     | No       | `false` | Controls whether monitor data is accessible on your public status page.                                                                         |\n| `otlp_endpoint`| `string` (optional)  | No       | `\"\"`    | The OTLP (OpenTelemetry Protocol) endpoint URL for exporting metrics.                                                                           |\n| `otlp_headers`| `map(string)` (optional)| No       | `{}`    | Custom headers to include when exporting metrics to your OTLP endpoint.                                                                       |\n\n**Example Usage:**\n\n```terraform\nresource \"openstatus_monitor\" \"my_website_monitor\" {\n  name        = \"My Website Availability\"\n  description = \"Checks the main website for uptime and response time.\"\n  url         = \"https://www.example.com\"\n  monitor_type = \"HTTP\"\n  method      = \"GET\"\n  regions     = [\"us-east-1\", \"eu-west-1\"]\n  periodicity = \"1m\"\n  active      = true\n  public      = true\n  timeout     = \"60s\"\n}\n\nresource \"openstatus_monitor\" \"internal_api_monitor\" {\n  name        = \"Internal API Health Check\"\n  description = \"Monitors the health of a critical internal API.\"\n  url         = \"https://api.internal.corp:8443/health\"\n  monitor_type = \"HTTP\"\n  method      = \"GET\"\n  regions     = [\"private-location-id\"]\n  periodicity = \"5m\"\n  active      = true\n  public      = false\n  headers = {\n    \"Authorization\" = \"Bearer ${var.internal_api_token}\"\n    \"Accept\"        = \"application/json\"\n  }\n}\n\nresource \"openstatus_monitor\" \"database_port_monitor\" {\n  name        = \"Database TCP Port Check\"\n  description = \"Ensures the PostgreSQL port is open and reachable.\"\n  url         = \"db.example.com:5432\"\n  monitor_type = \"TCP\"\n  regions     = [\"us-west-2\"]\n  periodicity = \"30s\"\n  active      = true\n}\n```\n\n## Related resources\n\n- **[HTTP Monitor Reference](/reference/http-monitor)** - Detailed specification for HTTP monitor configuration.\n- **[TCP Monitor Reference](/reference/tcp-monitor)** - Detailed specification for TCP monitor configuration.\n- **[DNS Monitor Reference](/reference/dns-monitor)** - Detailed specification for DNS monitor configuration.\n- **[CLI Reference](/reference/cli-reference)** - Manage monitors using the OpenStatus command-line interface.\n"
  },
  {
    "path": "apps/docs/src/content/docs/reference.mdx",
    "content": "---\ntitle: Reference Documentation\ndescription: Technical specifications, API documentation, and configuration references\n---\n\nimport { CardGrid, LinkCard } from '@astrojs/starlight/components';\n\n## 📖 Reference\n\nReference documentation provides **technical specifications** and detailed information about openstatus components, APIs, and configuration options. This is where you look up exact parameter names, return types, and available options.\n\n### When to use reference docs\n\nReference documentation is ideal when you:\n- Need to look up specific API endpoints or parameters\n- Want to know all available configuration options\n- Are looking for technical specifications\n- Need to understand data structures and types\n\n### Monitor Types\n\nDetailed specifications for each monitor type:\n\n<CardGrid>\n  <LinkCard title=\"HTTP Monitor\" href=\"/reference/http-monitor\" description=\"Complete reference for HTTP/HTTPS endpoint monitoring\" />\n  <LinkCard title=\"TCP Monitor\" href=\"/reference/tcp-monitor\" description=\"TCP port monitoring specifications\" />\n  <LinkCard title=\"DNS Monitor\" href=\"/reference/dns-monitor\" description=\"DNS resolution monitoring reference\" />\n</CardGrid>\n\n### Components & Features\n\n<CardGrid>\n  <LinkCard title=\"Status Page\" href=\"/reference/status-page\" description=\"Status page configuration options\" />\n  <LinkCard title=\"Status Report\" href=\"/reference/status-report\" description=\"Status report specifications\" />\n  <LinkCard title=\"Subscriber\" href=\"/reference/subscriber\" description=\"Status page subscriber reference\" />\n  <LinkCard title=\"Incident\" href=\"/reference/incident\" description=\"Incident management reference\" />\n  <LinkCard title=\"Notification\" href=\"/reference/notification\" description=\"Notification channel specifications\" />\n</CardGrid>\n\n### Infrastructure & Tools\n\n<CardGrid>\n  <LinkCard title=\"CLI Reference\" href=\"/reference/cli-reference\" description=\"Complete command-line interface documentation\" />\n  <LinkCard title=\"Terraform Provider\" href=\"/reference/terraform\" description=\"Infrastructure as code with Terraform\" />\n  <LinkCard title=\"Private Location\" href=\"/reference/private-location\" description=\"Self-hosted monitoring agent reference\" />\n</CardGrid>\n\n### API Documentation\n\nFor programmatic access to openstatus:\n- **[REST API](https://api.openstatus.dev/v1)** - Full REST API reference with interactive examples\n\n### Related sections\n\n- **[Tutorials](/tutorial/getting-started/)** - Learn how to use these features step-by-step\n- **[How-to guides](/guides/getting-started/)** - Practical examples of common use cases\n- **[Explanations](/concept/getting-started/)** - Understand the concepts behind the reference material\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/authentication.mdx",
    "content": "---\ntitle: Authentication\ndescription: \"Configure API key authentication for the OpenStatus Node.js SDK\"\n---\n\n## Recommended: createOpenStatusClient\n\nCreate a client with your API key. The key is automatically included in all requests via an interceptor.\n\n```typescript\nimport { createOpenStatusClient } from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\n// No headers needed on individual calls\nconst { httpMonitors } = await client.monitor.v1.MonitorService.listMonitors({});\n```\n\n## Alternative: Manual Headers\n\nUse the default `openstatus` client and pass headers on each call.\n\n```typescript\nimport { openstatus } from \"@openstatus/sdk-node\";\n\nconst headers = {\n  \"x-openstatus-key\": process.env.OPENSTATUS_API_KEY,\n};\n\nawait openstatus.monitor.v1.MonitorService.listMonitors({}, { headers });\n```\n\n## Environment Variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `OPENSTATUS_API_KEY` | Your OpenStatus API key | Required for authenticated calls |\n| `OPENSTATUS_API_URL` | Custom API endpoint | `https://api.openstatus.dev/rpc` |\n\nGet your API key from the [OpenStatus dashboard](https://www.openstatus.dev/app).\n\n## Custom Base URL\n\nFor self-hosted instances or staging environments:\n\n```typescript\nimport { createOpenStatusClient } from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n  baseUrl: \"https://api.staging.example.com/rpc\",\n});\n```\n\nThe `baseUrl` option takes precedence over the `OPENSTATUS_API_URL` environment variable.\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/error-handling.mdx",
    "content": "---\ntitle: Error Handling\ndescription: \"Handle errors and implement retry strategies with the OpenStatus Node.js SDK\"\n---\n\nThe SDK uses ConnectRPC. Errors are thrown as `ConnectError` instances from the `@connectrpc/connect` package.\n\n```typescript\nimport { ConnectError } from \"@connectrpc/connect\";\n\ntry {\n  await client.monitor.v1.MonitorService.deleteMonitor({ id: \"invalid\" });\n} catch (error) {\n  if (error instanceof ConnectError) {\n    console.error(`Code: ${error.code}`);\n    console.error(`Message: ${error.message}`);\n  }\n}\n```\n\n## Common Error Codes\n\n| Code | Description |\n|------|-------------|\n| `unauthenticated` | Missing or invalid API key |\n| `not_found` | Resource does not exist |\n| `invalid_argument` | Validation failure (e.g., missing required field, value out of range) |\n| `permission_denied` | No access to this workspace or resource |\n| `already_exists` | Duplicate resource (e.g., slug already taken) |\n\n## Retry Strategy\n\nConnectRPC does not retry by default. For transient failures (`unavailable`, `deadline_exceeded`), implement your own retry logic:\n\n```typescript\nimport { ConnectError } from \"@connectrpc/connect\";\n\nasync function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {\n  for (let attempt = 0; attempt <= maxRetries; attempt++) {\n    try {\n      return await fn();\n    } catch (error) {\n      if (\n        error instanceof ConnectError &&\n        (error.code === \"unavailable\" || error.code === \"deadline_exceeded\") &&\n        attempt < maxRetries\n      ) {\n        await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** attempt));\n        continue;\n      }\n      throw error;\n    }\n  }\n  throw new Error(\"Unreachable\");\n}\n\nconst { httpMonitors } = await withRetry(() =>\n  client.monitor.v1.MonitorService.listMonitors({})\n);\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/getting-started.mdx",
    "content": "---\ntitle: Getting Started\ndescription: \"Install and start using the OpenStatus Node.js SDK\"\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n## Get Your API Key\n\nBefore using the SDK, you need an API key:\n\n1. Log in to the [OpenStatus dashboard](https://www.openstatus.dev/app/login)\n2. Go to **Settings** > **API Keys**\n3. Click **Create API Key** and copy it\n\n<Aside type=\"tip\">Store your API key as an environment variable (`OPENSTATUS_API_KEY`) — never commit it to source control.</Aside>\n\n## Installation\n\n### npm\n\n```bash\nnpm install @openstatus/sdk-node\n```\n\n### JSR\n\n```bash\nnpx jsr add @openstatus/sdk-node\n```\n\n### Deno\n\n```typescript\nimport { createOpenStatusClient } from \"jsr:@openstatus/sdk-node\";\n```\n\n### Bun\n\n```bash\nbun add @openstatus/sdk-node\n```\n\n## Quick Start\n\n```typescript\nimport {\n  createOpenStatusClient,\n  Periodicity,\n  Region,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\n// Create an HTTP monitor\nconst { monitor } = await client.monitor.v1.MonitorService.createHTTPMonitor({\n  monitor: {\n    name: \"My API\",\n    url: \"https://api.example.com/health\",\n    periodicity: Periodicity.PERIODICITY_1M,\n    regions: [Region.FLY_AMS, Region.FLY_IAD, Region.FLY_SYD],\n    active: true,\n  },\n});\n\nconsole.log(`Monitor created: ${monitor?.id}`);\n\n// List all monitors\nconst { httpMonitors, tcpMonitors, dnsMonitors, totalSize } =\n  await client.monitor.v1.MonitorService.listMonitors({});\n\nconsole.log(`Found ${totalSize} monitors`);\n```\n\n## Runtime Support\n\n| Runtime | Version | Module Format |\n|---------|---------|---------------|\n| Node.js | 18+     | ESM and CJS   |\n| Deno    | 2+      | ESM (native)  |\n| Bun     | Latest  | ESM           |\n\n## Full Workflow Example\n\nA complete example: create a monitor, set up a status page, add the monitor as a component, configure a Slack notification, and check overall status.\n\n```typescript\nimport {\n  createOpenStatusClient,\n  NotificationProvider,\n  Periodicity,\n  Region,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\n// 1. Check API health\nconst health = await client.health.v1.HealthService.check({});\nconsole.log(`API status: ${health.status}`);\n\n// 2. Create an HTTP monitor\nconst { monitor } = await client.monitor.v1.MonitorService.createHTTPMonitor({\n  monitor: {\n    name: \"Production API\",\n    url: \"https://api.example.com/health\",\n    periodicity: Periodicity.PERIODICITY_1M,\n    regions: [Region.FLY_AMS, Region.FLY_IAD, Region.FLY_SYD],\n    active: true,\n  },\n});\n\n// 3. Create a status page\nconst { statusPage } = await client.statusPage.v1.StatusPageService\n  .createStatusPage({\n    title: \"Example Status\",\n    slug: \"example-status\",\n    description: \"Status page for Example services\",\n  });\n\n// 4. Add the monitor as a component\nconst { component } = await client.statusPage.v1.StatusPageService\n  .addMonitorComponent({\n    pageId: statusPage!.id,\n    monitorId: monitor!.id,\n    name: \"Production API\",\n  });\n\n// 5. Set up Slack notifications\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Slack Alerts\",\n    provider: NotificationProvider.SLACK,\n    data: {\n      data: {\n        case: \"slack\",\n        value: { webhookUrl: \"https://hooks.slack.com/services/...\" },\n      },\n    },\n    monitorIds: [monitor!.id],\n  });\n\n// 6. Check overall status\nconst { overallStatus } = await client.statusPage.v1.StatusPageService\n  .getOverallStatus({\n    identifier: { case: \"id\", value: statusPage!.id },\n  });\n\nconsole.log(`Overall status: ${overallStatus}`);\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/health-service.mdx",
    "content": "---\ntitle: Health Service\ndescription: \"Check the OpenStatus API health status using the Node.js SDK\"\n---\n\nCheck API health status. No authentication required.\n\n```typescript\nimport { openstatus, ServingStatus } from \"@openstatus/sdk-node\";\n\nconst { status } = await openstatus.health.v1.HealthService.check({});\nconsole.log(ServingStatus[status]); // \"SERVING\"\n```\n\nOr with a configured client:\n\n```typescript\nimport { createOpenStatusClient, ServingStatus } from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient();\nconst { status } = await client.health.v1.HealthService.check({});\nconsole.log(ServingStatus[status]); // \"SERVING\"\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/index.mdx",
    "content": "---\ntitle: Getting Started\ndescription: \"Install and start using the OpenStatus Node.js SDK\"\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n## Get Your API Key\n\nBefore using the SDK, you need an API key:\n\n1. Log in to the [OpenStatus dashboard](https://www.openstatus.dev/app/login)\n2. Go to **Settings** > **API Keys**\n3. Click **Create API Key** and copy it\n\n<Aside type=\"tip\">Store your API key as an environment variable (`OPENSTATUS_API_KEY`) — never commit it to source control.</Aside>\n\n## Installation\n\n### npm\n\n```bash\nnpm install @openstatus/sdk-node\n```\n\n### JSR\n\n```bash\nnpx jsr add @openstatus/sdk-node\n```\n\n### Deno\n\n```typescript\nimport { createOpenStatusClient } from \"jsr:@openstatus/sdk-node\";\n```\n\n### Bun\n\n```bash\nbun add @openstatus/sdk-node\n```\n\n## Quick Start\n\n```typescript\nimport {\n  createOpenStatusClient,\n  Periodicity,\n  Region,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\n// Create an HTTP monitor\nconst { monitor } = await client.monitor.v1.MonitorService.createHTTPMonitor({\n  monitor: {\n    name: \"My API\",\n    url: \"https://api.example.com/health\",\n    periodicity: Periodicity.PERIODICITY_1M,\n    regions: [Region.FLY_AMS, Region.FLY_IAD, Region.FLY_SYD],\n    active: true,\n  },\n});\n\nconsole.log(`Monitor created: ${monitor?.id}`);\n\n// List all monitors\nconst { httpMonitors, tcpMonitors, dnsMonitors, totalSize } =\n  await client.monitor.v1.MonitorService.listMonitors({});\n\nconsole.log(`Found ${totalSize} monitors`);\n```\n\n## Runtime Support\n\n| Runtime | Version | Module Format |\n|---------|---------|---------------|\n| Node.js | 18+     | ESM and CJS   |\n| Deno    | 2+      | ESM (native)  |\n| Bun     | Latest  | ESM           |\n\n## Full Workflow Example\n\nA complete example: create a monitor, set up a status page, add the monitor as a component, configure a Slack notification, and check overall status.\n\n```typescript\nimport {\n  createOpenStatusClient,\n  NotificationProvider,\n  Periodicity,\n  Region,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\n// 1. Check API health\nconst health = await client.health.v1.HealthService.check({});\nconsole.log(`API status: ${health.status}`);\n\n// 2. Create an HTTP monitor\nconst { monitor } = await client.monitor.v1.MonitorService.createHTTPMonitor({\n  monitor: {\n    name: \"Production API\",\n    url: \"https://api.example.com/health\",\n    periodicity: Periodicity.PERIODICITY_1M,\n    regions: [Region.FLY_AMS, Region.FLY_IAD, Region.FLY_SYD],\n    active: true,\n  },\n});\n\n// 3. Create a status page\nconst { statusPage } = await client.statusPage.v1.StatusPageService\n  .createStatusPage({\n    title: \"Example Status\",\n    slug: \"example-status\",\n    description: \"Status page for Example services\",\n  });\n\n// 4. Add the monitor as a component\nconst { component } = await client.statusPage.v1.StatusPageService\n  .addMonitorComponent({\n    pageId: statusPage!.id,\n    monitorId: monitor!.id,\n    name: \"Production API\",\n  });\n\n// 5. Set up Slack notifications\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Slack Alerts\",\n    provider: NotificationProvider.SLACK,\n    data: {\n      data: {\n        case: \"slack\",\n        value: { webhookUrl: \"https://hooks.slack.com/services/...\" },\n      },\n    },\n    monitorIds: [monitor!.id],\n  });\n\n// 6. Check overall status\nconst { overallStatus } = await client.statusPage.v1.StatusPageService\n  .getOverallStatus({\n    identifier: { case: \"id\", value: statusPage!.id },\n  });\n\nconsole.log(`Overall status: ${overallStatus}`);\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/maintenance-service.mdx",
    "content": "---\ntitle: Maintenance Service\ndescription: \"Manage scheduled maintenance windows with the OpenStatus Node.js SDK\"\n---\n\nManage scheduled maintenance windows. The Maintenance Service provides 5 RPC methods.\n\n## Create Maintenance Window\n\n```typescript\nconst { maintenance } = await client.maintenance.v1.MaintenanceService\n  .createMaintenance({\n    title: \"Database Upgrade\",\n    message: \"We will be upgrading our database infrastructure.\",\n    from: \"2024-01-20T02:00:00Z\",\n    to: \"2024-01-20T04:00:00Z\",\n    pageId: \"page_123\",\n    pageComponentIds: [\"comp_456\"],\n    notify: true,\n  });\n\nconsole.log(`Maintenance created: ${maintenance?.id}`);\n```\n\nAll date fields (`from`, `to`) must be in RFC 3339 format.\n\n## List Maintenances\n\nList maintenance windows with optional page filtering.\n\n```typescript\nconst { maintenances, totalSize } = await client.maintenance.v1\n  .MaintenanceService.listMaintenances({\n    limit: 10,\n    offset: 0,\n    pageId: \"page_123\",\n  });\n\nconsole.log(`Found ${totalSize} maintenance windows`);\n```\n\n## Get / Update / Delete Maintenance Windows\n\n### Get Maintenance\n\n```typescript\nconst { maintenance } = await client.maintenance.v1.MaintenanceService\n  .getMaintenance({ id: \"maint_123\" });\n\nconsole.log(`Title: ${maintenance?.title}`);\nconsole.log(`From: ${maintenance?.from}`);\nconsole.log(`To: ${maintenance?.to}`);\n```\n\n### Update Maintenance\n\n```typescript\nconst { maintenance } = await client.maintenance.v1.MaintenanceService\n  .updateMaintenance({\n    id: \"maint_123\",\n    title: \"Extended Database Upgrade\",\n    to: \"2024-01-20T06:00:00Z\",\n  });\n```\n\n### Delete Maintenance\n\n```typescript\nconst { success } = await client.maintenance.v1.MaintenanceService\n  .deleteMaintenance({ id: \"maint_123\" });\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/monitor-service.mdx",
    "content": "---\ntitle: Monitor Service\ndescription: \"Create and manage HTTP, TCP, and DNS monitors with the OpenStatus Node.js SDK\"\n---\n\nManage HTTP, TCP, and DNS monitors. The Monitor Service provides 12 RPC methods for creating, updating, listing, triggering, deleting, and querying monitor status and metrics.\n\nAll examples assume you have created a client:\n\n```typescript\nimport { createOpenStatusClient } from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n```\n\n## HTTP Monitors\n\n### Create HTTP Monitor\n\n```typescript\nimport {\n  createOpenStatusClient,\n  HTTPMethod,\n  NumberComparator,\n  Periodicity,\n  Region,\n  StringComparator,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst { monitor } = await client.monitor.v1.MonitorService.createHTTPMonitor({\n  monitor: {\n    name: \"My API\",\n    url: \"https://api.example.com/health\",\n    periodicity: Periodicity.PERIODICITY_1M,\n    method: HTTPMethod.HTTP_METHOD_GET,\n    regions: [Region.FLY_AMS, Region.FLY_IAD, Region.FLY_SYD],\n    active: true,\n    timeout: BigInt(30000),\n    retry: BigInt(3),\n    followRedirects: true,\n    degradedAt: BigInt(5000),\n    headers: [\n      { key: \"Authorization\", value: \"Bearer my-token\" },\n    ],\n    statusCodeAssertions: [\n      { comparator: NumberComparator.EQUAL, target: BigInt(200) },\n    ],\n    bodyAssertions: [\n      { comparator: StringComparator.CONTAINS, target: '\"status\":\"ok\"' },\n    ],\n    headerAssertions: [\n      {\n        key: \"content-type\",\n        comparator: StringComparator.CONTAINS,\n        target: \"application/json\",\n      },\n    ],\n    description: \"Health check for the production API\",\n    public: false,\n    openTelemetry: {\n      endpoint: \"https://otel.example.com/v1/traces\",\n      headers: [{ key: \"Authorization\", value: \"Bearer otel-token\" }],\n    },\n  },\n});\n\nconsole.log(`Created monitor: ${monitor?.id}`);\n```\n\n### Update HTTP Monitor\n\nUpdates are partial — only include the fields you want to change.\n\n```typescript\nconst { monitor } = await client.monitor.v1.MonitorService.updateHTTPMonitor({\n  id: \"mon_123\",\n  monitor: {\n    name: \"Updated API Monitor\",\n    active: false,\n  },\n});\n```\n\n### HTTP Monitor Options\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `name` | string | Yes | Monitor name (max 256 chars) |\n| `url` | string | Yes | URL to monitor (max 2048 chars) |\n| `periodicity` | Periodicity | Yes | Check interval |\n| `method` | HTTPMethod | No | HTTP method (default: GET) |\n| `body` | string | No | Request body |\n| `headers` | Headers[] | No | Custom headers `{ key, value }[]` |\n| `timeout` | bigint | No | Timeout in ms (default: 45000, max: 120000) |\n| `retry` | bigint | No | Retry attempts (default: 3, max: 10) |\n| `followRedirects` | boolean | No | Follow redirects (default: true) |\n| `regions` | Region[] | No | Regions for checks |\n| `active` | boolean | No | Enable monitoring (default: false) |\n| `public` | boolean | No | Public visibility (default: false) |\n| `degradedAt` | bigint | No | Latency threshold (ms) for degraded status |\n| `description` | string | No | Monitor description (max 1024 chars) |\n| `statusCodeAssertions` | StatusCodeAssertion[] | No | Status code assertions |\n| `bodyAssertions` | BodyAssertion[] | No | Body assertions |\n| `headerAssertions` | HeaderAssertion[] | No | Header assertions |\n| `openTelemetry` | OpenTelemetryConfig | No | OpenTelemetry export configuration |\n\n## TCP Monitors\n\n### Create TCP Monitor\n\n```typescript\nimport {\n  createOpenStatusClient,\n  Periodicity,\n  Region,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst { monitor } = await client.monitor.v1.MonitorService.createTCPMonitor({\n  monitor: {\n    name: \"Database\",\n    uri: \"db.example.com:5432\",\n    periodicity: Periodicity.PERIODICITY_5M,\n    regions: [Region.FLY_AMS, Region.FLY_IAD],\n    active: true,\n  },\n});\n```\n\n### Update TCP Monitor\n\n```typescript\nconst { monitor } = await client.monitor.v1.MonitorService.updateTCPMonitor({\n  id: \"mon_123\",\n  monitor: {\n    name: \"Updated Database Monitor\",\n  },\n});\n```\n\n### TCP Monitor Options\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `name` | string | Yes | Monitor name (max 256 chars) |\n| `uri` | string | Yes | `host:port` to monitor (max 2048 chars) |\n| `periodicity` | Periodicity | Yes | Check interval |\n| `timeout` | bigint | No | Timeout in ms (default: 45000, max: 120000) |\n| `retry` | bigint | No | Retry attempts (default: 3, max: 10) |\n| `regions` | Region[] | No | Regions for checks |\n| `active` | boolean | No | Enable monitoring (default: false) |\n| `public` | boolean | No | Public visibility (default: false) |\n| `degradedAt` | bigint | No | Latency threshold (ms) for degraded status |\n| `description` | string | No | Monitor description (max 1024 chars) |\n| `openTelemetry` | OpenTelemetryConfig | No | OpenTelemetry export configuration |\n\n## DNS Monitors\n\n### Create DNS Monitor\n\n```typescript\nimport {\n  createOpenStatusClient,\n  Periodicity,\n  RecordComparator,\n  Region,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst { monitor } = await client.monitor.v1.MonitorService.createDNSMonitor({\n  monitor: {\n    name: \"DNS Check\",\n    uri: \"example.com\",\n    periodicity: Periodicity.PERIODICITY_10M,\n    regions: [Region.FLY_AMS],\n    active: true,\n    recordAssertions: [\n      {\n        record: \"A\",\n        comparator: RecordComparator.EQUAL,\n        target: \"93.184.216.34\",\n      },\n      {\n        record: \"CNAME\",\n        comparator: RecordComparator.CONTAINS,\n        target: \"cdn\",\n      },\n    ],\n  },\n});\n```\n\n### Update DNS Monitor\n\n```typescript\nconst { monitor } = await client.monitor.v1.MonitorService.updateDNSMonitor({\n  id: \"mon_123\",\n  monitor: {\n    name: \"Updated DNS Check\",\n  },\n});\n```\n\n### DNS Monitor Options\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `name` | string | Yes | Monitor name (max 256 chars) |\n| `uri` | string | Yes | Domain to resolve (max 2048 chars) |\n| `periodicity` | Periodicity | Yes | Check interval |\n| `timeout` | bigint | No | Timeout in ms (default: 45000, max: 120000) |\n| `retry` | bigint | No | Retry attempts (default: 3, max: 10) |\n| `regions` | Region[] | No | Regions for checks |\n| `active` | boolean | No | Enable monitoring (default: false) |\n| `public` | boolean | No | Public visibility (default: false) |\n| `degradedAt` | bigint | No | Latency threshold (ms) for degraded status |\n| `description` | string | No | Monitor description (max 1024 chars) |\n| `recordAssertions` | RecordAssertion[] | No | DNS record assertions |\n| `openTelemetry` | OpenTelemetryConfig | No | OpenTelemetry export configuration |\n\n## List Monitors\n\nList all monitors with offset-based pagination. Returns monitors grouped by type.\n\n```typescript\nconst { httpMonitors, tcpMonitors, dnsMonitors, totalSize } =\n  await client.monitor.v1.MonitorService.listMonitors({\n    limit: 10,\n    offset: 0,\n  });\n\nconsole.log(`Total: ${totalSize}`);\nconsole.log(`HTTP: ${httpMonitors.length}`);\nconsole.log(`TCP: ${tcpMonitors.length}`);\nconsole.log(`DNS: ${dnsMonitors.length}`);\n```\n\nPagination parameters:\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `limit` | number (optional) | Max results to return (1–100, default 50) |\n| `offset` | number (optional) | Number of results to skip (default 0) |\n\n## Get Monitor\n\nGet a single monitor by ID. The response uses a `MonitorConfig` oneof type that contains one of HTTP, TCP, or DNS configuration.\n\n```typescript\nconst { monitor } = await client.monitor.v1.MonitorService.getMonitor({\n  id: \"mon_123\",\n});\n\nif (monitor?.config.case === \"http\") {\n  console.log(`HTTP Monitor: ${monitor.config.value.name} — ${monitor.config.value.url}`);\n} else if (monitor?.config.case === \"tcp\") {\n  console.log(`TCP Monitor: ${monitor.config.value.name} — ${monitor.config.value.uri}`);\n} else if (monitor?.config.case === \"dns\") {\n  console.log(`DNS Monitor: ${monitor.config.value.name} — ${monitor.config.value.uri}`);\n}\n```\n\n## Trigger Monitor\n\nTrigger an immediate check for a monitor.\n\n```typescript\nconst { success } = await client.monitor.v1.MonitorService.triggerMonitor({\n  id: \"mon_123\",\n});\n\nconsole.log(`Trigger successful: ${success}`);\n```\n\n## Delete Monitor\n\n```typescript\nconst { success } = await client.monitor.v1.MonitorService.deleteMonitor({\n  id: \"mon_123\",\n});\n```\n\n## Get Monitor Status\n\nGet the current status of a monitor across all configured regions.\n\n```typescript\nimport {\n  createOpenStatusClient,\n  MonitorStatus,\n  Region,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst { id, regions } = await client.monitor.v1.MonitorService.getMonitorStatus(\n  { id: \"mon_123\" },\n);\n\nfor (const { region, status } of regions) {\n  console.log(`${Region[region]}: ${MonitorStatus[status]}`);\n}\n```\n\n## Get Monitor Summary\n\nGet aggregated metrics and latency percentiles for a monitor over a time range.\n\n```typescript\nimport { createOpenStatusClient, TimeRange } from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst summary = await client.monitor.v1.MonitorService.getMonitorSummary({\n  id: \"mon_123\",\n  timeRange: TimeRange.TIME_RANGE_7D,\n  regions: [],\n});\n\nconsole.log(`Last ping: ${summary.lastPingAt}`);\nconsole.log(`Successful: ${summary.totalSuccessful}`);\nconsole.log(`Degraded: ${summary.totalDegraded}`);\nconsole.log(`Failed: ${summary.totalFailed}`);\nconsole.log(`P50: ${summary.p50}ms`);\nconsole.log(`P75: ${summary.p75}ms`);\nconsole.log(`P90: ${summary.p90}ms`);\nconsole.log(`P95: ${summary.p95}ms`);\nconsole.log(`P99: ${summary.p99}ms`);\n```\n\nThe latency fields (`p50`, `p75`, `p90`, `p95`, `p99`) and count fields (`totalSuccessful`, `totalDegraded`, `totalFailed`) are `bigint` values. The `regions` parameter is optional — pass an empty array to get metrics across all regions.\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/notification-service.mdx",
    "content": "---\ntitle: Notification Service\ndescription: \"Manage notification channels and providers with the OpenStatus Node.js SDK\"\n---\n\nManage notification channels for monitor alerts. Supports 12 providers. The Notification Service provides 7 RPC methods.\n\n## Create Notification\n\n```typescript\nimport {\n  createOpenStatusClient,\n  NotificationProvider,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Slack Alerts\",\n    provider: NotificationProvider.SLACK,\n    data: {\n      data: {\n        case: \"slack\",\n        value: { webhookUrl: \"https://hooks.slack.com/services/...\" },\n      },\n    },\n    monitorIds: [\"mon_123\", \"mon_456\"],\n  });\n\nconsole.log(`Notification created: ${notification?.id}`);\n```\n\nThe `data` field uses a nested oneof pattern: the outer `data` is the `NotificationData` message, and `data.data` is the oneof that selects the provider-specific configuration. The `case` must match the provider type in lowercase.\n\n## Provider Configurations\n\nEach provider shown as a complete `createNotification` call.\n\n### Slack\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Slack Alerts\",\n    provider: NotificationProvider.SLACK,\n    data: {\n      data: {\n        case: \"slack\",\n        value: { webhookUrl: \"https://hooks.slack.com/services/...\" },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### Discord\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Discord Alerts\",\n    provider: NotificationProvider.DISCORD,\n    data: {\n      data: {\n        case: \"discord\",\n        value: { webhookUrl: \"https://discord.com/api/webhooks/...\" },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### Email\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Email Alerts\",\n    provider: NotificationProvider.EMAIL,\n    data: {\n      data: {\n        case: \"email\",\n        value: { email: \"alerts@example.com\" },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### PagerDuty\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"PagerDuty Alerts\",\n    provider: NotificationProvider.PAGERDUTY,\n    data: {\n      data: {\n        case: \"pagerduty\",\n        value: { integrationKey: \"your-integration-key\" },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### Opsgenie\n\n```typescript\nimport { NotificationProvider, OpsgenieRegion } from \"@openstatus/sdk-node\";\n\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Opsgenie Alerts\",\n    provider: NotificationProvider.OPSGENIE,\n    data: {\n      data: {\n        case: \"opsgenie\",\n        value: { apiKey: \"your-api-key\", region: OpsgenieRegion.US },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### Telegram\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Telegram Alerts\",\n    provider: NotificationProvider.TELEGRAM,\n    data: {\n      data: {\n        case: \"telegram\",\n        value: { chatId: \"123456789\" },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### Google Chat\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Google Chat Alerts\",\n    provider: NotificationProvider.GOOGLE_CHAT,\n    data: {\n      data: {\n        case: \"googleChat\",\n        value: { webhookUrl: \"https://chat.googleapis.com/v1/spaces/...\" },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### Grafana OnCall\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Grafana OnCall\",\n    provider: NotificationProvider.GRAFANA_ONCALL,\n    data: {\n      data: {\n        case: \"grafanaOncall\",\n        value: { webhookUrl: \"https://oncall.example.com/...\" },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### Ntfy\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Ntfy Alerts\",\n    provider: NotificationProvider.NTFY,\n    data: {\n      data: {\n        case: \"ntfy\",\n        value: {\n          topic: \"my-alerts\",\n          serverUrl: \"https://ntfy.sh\",\n          token: \"tk_...\",\n        },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### SMS\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"SMS Alerts\",\n    provider: NotificationProvider.SMS,\n    data: {\n      data: {\n        case: \"sms\",\n        value: { phoneNumber: \"+1234567890\" },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### WhatsApp\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"WhatsApp Alerts\",\n    provider: NotificationProvider.WHATSAPP,\n    data: {\n      data: {\n        case: \"whatsapp\",\n        value: { phoneNumber: \"+1234567890\" },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n### Custom Webhook\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .createNotification({\n    name: \"Custom Webhook\",\n    provider: NotificationProvider.WEBHOOK,\n    data: {\n      data: {\n        case: \"webhook\",\n        value: {\n          endpoint: \"https://api.example.com/webhook\",\n          headers: [\n            { key: \"Authorization\", value: \"Bearer token\" },\n            { key: \"X-Custom-Header\", value: \"value\" },\n          ],\n        },\n      },\n    },\n    monitorIds: [\"mon_123\"],\n  });\n```\n\n## Send Test Notification\n\nVerify a notification configuration without creating a channel.\n\n```typescript\nimport { NotificationProvider } from \"@openstatus/sdk-node\";\n\nconst { success, errorMessage } = await client.notification.v1\n  .NotificationService.sendTestNotification({\n    provider: NotificationProvider.SLACK,\n    data: {\n      data: {\n        case: \"slack\",\n        value: { webhookUrl: \"https://hooks.slack.com/services/...\" },\n      },\n    },\n  });\n\nif (success) {\n  console.log(\"Test notification sent successfully\");\n} else {\n  console.log(`Test failed: ${errorMessage}`);\n}\n```\n\n## Check Notification Limits\n\nCheck if the workspace has reached its notification channel limit.\n\n```typescript\nconst { limitReached, currentCount, maxCount } = await client.notification.v1\n  .NotificationService.checkNotificationLimit({});\n\nconsole.log(`${currentCount}/${maxCount} notification channels used`);\nif (limitReached) {\n  console.log(\"Notification limit reached — upgrade your plan\");\n}\n```\n\n## List / Get / Update / Delete Notifications\n\n### List Notifications\n\n```typescript\nconst { notifications, totalSize } = await client.notification.v1\n  .NotificationService.listNotifications({ limit: 10, offset: 0 });\n\nconsole.log(`Found ${totalSize} notification channels`);\n```\n\n### Get Notification\n\n```typescript\nimport { NotificationProvider } from \"@openstatus/sdk-node\";\n\nconst { notification } = await client.notification.v1.NotificationService\n  .getNotification({ id: \"notif_123\" });\n\nconsole.log(`Name: ${notification?.name}`);\nconsole.log(`Provider: ${NotificationProvider[notification?.provider ?? 0]}`);\n```\n\n### Update Notification\n\n```typescript\nconst { notification } = await client.notification.v1.NotificationService\n  .updateNotification({\n    id: \"notif_123\",\n    name: \"Updated Slack Alerts\",\n    monitorIds: [\"mon_123\", \"mon_456\", \"mon_789\"],\n  });\n```\n\n### Delete Notification\n\n```typescript\nconst { success } = await client.notification.v1.NotificationService\n  .deleteNotification({ id: \"notif_123\" });\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/reference.mdx",
    "content": "---\ntitle: Reference\ndescription: \"Complete reference for enums, regions, assertions, and TypeScript type exports in the OpenStatus Node.js SDK\"\n---\n\n## Enums\n\n### Periodicity\n\n| Value | Description |\n|-------|-------------|\n| `PERIODICITY_30S` | Every 30 seconds |\n| `PERIODICITY_1M` | Every 1 minute |\n| `PERIODICITY_5M` | Every 5 minutes |\n| `PERIODICITY_10M` | Every 10 minutes |\n| `PERIODICITY_30M` | Every 30 minutes |\n| `PERIODICITY_1H` | Every 1 hour |\n\n### HTTPMethod\n\n| Value | Description |\n|-------|-------------|\n| `HTTP_METHOD_GET` | GET |\n| `HTTP_METHOD_POST` | POST |\n| `HTTP_METHOD_HEAD` | HEAD |\n| `HTTP_METHOD_PUT` | PUT |\n| `HTTP_METHOD_PATCH` | PATCH |\n| `HTTP_METHOD_DELETE` | DELETE |\n| `HTTP_METHOD_TRACE` | TRACE |\n| `HTTP_METHOD_CONNECT` | CONNECT |\n| `HTTP_METHOD_OPTIONS` | OPTIONS |\n\n### MonitorStatus\n\n| Value | Description |\n|-------|-------------|\n| `ACTIVE` | Monitor is healthy |\n| `DEGRADED` | Latency threshold exceeded |\n| `ERROR` | Monitor is failing |\n\n### TimeRange\n\n| Value | Description |\n|-------|-------------|\n| `TIME_RANGE_1D` | Last 24 hours |\n| `TIME_RANGE_7D` | Last 7 days |\n| `TIME_RANGE_14D` | Last 14 days |\n\n### StatusReportStatus\n\n| Value | Description |\n|-------|-------------|\n| `INVESTIGATING` | Actively investigating the issue |\n| `IDENTIFIED` | Root cause has been identified |\n| `MONITORING` | Fix deployed, monitoring |\n| `RESOLVED` | Issue fully resolved |\n\n### OverallStatus\n\n| Value | Description |\n|-------|-------------|\n| `OPERATIONAL` | All systems operational |\n| `DEGRADED` | Performance is degraded |\n| `PARTIAL_OUTAGE` | Some systems are down |\n| `MAJOR_OUTAGE` | Major systems are down |\n| `MAINTENANCE` | Scheduled maintenance |\n| `UNKNOWN` | Status cannot be determined |\n\n### NotificationProvider\n\n| Value | Description |\n|-------|-------------|\n| `DISCORD` | Discord webhook |\n| `EMAIL` | Email notification |\n| `GOOGLE_CHAT` | Google Chat webhook |\n| `GRAFANA_ONCALL` | Grafana OnCall |\n| `NTFY` | Ntfy push service |\n| `PAGERDUTY` | PagerDuty |\n| `OPSGENIE` | Opsgenie |\n| `SLACK` | Slack webhook |\n| `SMS` | SMS notification |\n| `TELEGRAM` | Telegram bot |\n| `WEBHOOK` | Custom webhook |\n| `WHATSAPP` | WhatsApp |\n\n### OpsgenieRegion\n\n| Value | Description |\n|-------|-------------|\n| `US` | US region |\n| `EU` | EU region |\n\n### PageAccessType\n\n| Value | Description |\n|-------|-------------|\n| `PUBLIC` | Publicly accessible |\n| `PASSWORD_PROTECTED` | Requires password |\n| `AUTHENTICATED` | Requires authentication |\n\n### PageTheme\n\n| Value | Description |\n|-------|-------------|\n| `SYSTEM` | Follow system theme |\n| `LIGHT` | Light theme |\n| `DARK` | Dark theme |\n\n### PageComponentType\n\n| Value | Description |\n|-------|-------------|\n| `MONITOR` | Linked to a monitor |\n| `STATIC` | Static component (manual) |\n\n### NumberComparator\n\n| Value | Description |\n|-------|-------------|\n| `EQUAL` | Equal to target |\n| `NOT_EQUAL` | Not equal to target |\n| `GREATER_THAN` | Greater than target |\n| `GREATER_THAN_OR_EQUAL` | Greater than or equal |\n| `LESS_THAN` | Less than target |\n| `LESS_THAN_OR_EQUAL` | Less than or equal |\n\n### StringComparator\n\n| Value | Description |\n|-------|-------------|\n| `CONTAINS` | Contains target string |\n| `NOT_CONTAINS` | Does not contain target |\n| `EQUAL` | Equal to target |\n| `NOT_EQUAL` | Not equal to target |\n| `EMPTY` | Value is empty |\n| `NOT_EMPTY` | Value is not empty |\n| `GREATER_THAN` | Lexicographically greater |\n| `GREATER_THAN_OR_EQUAL` | Lexicographically greater than or equal to target |\n| `LESS_THAN` | Lexicographically less |\n| `LESS_THAN_OR_EQUAL` | Lexicographically less than or equal to target |\n\n### RecordComparator\n\n| Value | Description |\n|-------|-------------|\n| `EQUAL` | Equal to target |\n| `NOT_EQUAL` | Not equal to target |\n| `CONTAINS` | Contains target string |\n| `NOT_CONTAINS` | Does not contain target |\n\n### ServingStatus\n\n| Value | Description |\n|-------|-------------|\n| `SERVING` | Service is healthy and serving |\n| `NOT_SERVING` | Service is not healthy |\n\n## Regions\n\nMonitor from 28 global locations across multiple providers.\n\n```typescript\nimport { Region } from \"@openstatus/sdk-node\";\n\nregions: [Region.FLY_AMS, Region.FLY_IAD, Region.KOYEB_FRA];\n```\n\n### Fly.io Regions (18)\n\n| Enum Value | Location |\n|------------|----------|\n| `FLY_AMS` | Amsterdam |\n| `FLY_ARN` | Stockholm |\n| `FLY_BOM` | Mumbai |\n| `FLY_CDG` | Paris |\n| `FLY_DFW` | Dallas |\n| `FLY_EWR` | Newark |\n| `FLY_FRA` | Frankfurt |\n| `FLY_GRU` | São Paulo |\n| `FLY_IAD` | Washington D.C. |\n| `FLY_JNB` | Johannesburg |\n| `FLY_LAX` | Los Angeles |\n| `FLY_LHR` | London |\n| `FLY_NRT` | Tokyo |\n| `FLY_ORD` | Chicago |\n| `FLY_SJC` | San Jose |\n| `FLY_SIN` | Singapore |\n| `FLY_SYD` | Sydney |\n| `FLY_YYZ` | Toronto |\n\n### Koyeb Regions (6)\n\n| Enum Value | Location |\n|------------|----------|\n| `KOYEB_FRA` | Frankfurt |\n| `KOYEB_PAR` | Paris |\n| `KOYEB_SFO` | San Francisco |\n| `KOYEB_SIN` | Singapore |\n| `KOYEB_TYO` | Tokyo |\n| `KOYEB_WAS` | Washington |\n\n### Railway Regions (4)\n\n| Enum Value | Location |\n|------------|----------|\n| `RAILWAY_US_WEST2` | US West |\n| `RAILWAY_US_EAST4` | US East |\n| `RAILWAY_EUROPE_WEST4` | Europe West |\n| `RAILWAY_ASIA_SOUTHEAST1` | Asia Southeast |\n\n## Assertions\n\n### Status Code Assertions\n\nValidate HTTP response status codes using `NumberComparator`.\n\n```typescript\nimport { NumberComparator } from \"@openstatus/sdk-node\";\n\nstatusCodeAssertions: [\n  { comparator: NumberComparator.EQUAL, target: BigInt(200) },\n  { comparator: NumberComparator.LESS_THAN, target: BigInt(400) },\n];\n```\n\n### Body Assertions\n\nValidate response body content using `StringComparator`.\n\n```typescript\nimport { StringComparator } from \"@openstatus/sdk-node\";\n\nbodyAssertions: [\n  { comparator: StringComparator.CONTAINS, target: '\"status\":\"ok\"' },\n  { comparator: StringComparator.NOT_EMPTY, target: \"\" },\n];\n```\n\n### Header Assertions\n\nValidate response headers using `StringComparator` with a header `key`.\n\n```typescript\nimport { StringComparator } from \"@openstatus/sdk-node\";\n\nheaderAssertions: [\n  {\n    key: \"content-type\",\n    comparator: StringComparator.CONTAINS,\n    target: \"application/json\",\n  },\n];\n```\n\n### DNS Record Assertions\n\nValidate DNS records using `RecordComparator`. Supported record types: `A`, `AAAA`, `CNAME`, `MX`, `TXT`.\n\n```typescript\nimport { RecordComparator } from \"@openstatus/sdk-node\";\n\nrecordAssertions: [\n  {\n    record: \"A\",\n    comparator: RecordComparator.EQUAL,\n    target: \"93.184.216.34\",\n  },\n  {\n    record: \"CNAME\",\n    comparator: RecordComparator.CONTAINS,\n    target: \"cdn\",\n  },\n];\n```\n\n## TypeScript Type Exports\n\nAll types and enums exported from `@openstatus/sdk-node`:\n\n### Monitor Types\n\n- `HTTPMonitor`, `Headers`, `OpenTelemetryConfig` — HTTP monitor configuration\n- `TCPMonitor` — TCP monitor configuration\n- `DNSMonitor` — DNS monitor configuration\n- `StatusCodeAssertion`, `BodyAssertion`, `HeaderAssertion`, `RecordAssertion` — assertion types\n- `CreateHTTPMonitorRequest`, `CreateHTTPMonitorResponse` — HTTP monitor CRUD\n- `CreateTCPMonitorRequest`, `CreateTCPMonitorResponse` — TCP monitor CRUD\n- `CreateDNSMonitorRequest`, `CreateDNSMonitorResponse` — DNS monitor CRUD\n- `UpdateHTTPMonitorRequest`, `UpdateHTTPMonitorResponse`\n- `UpdateTCPMonitorRequest`, `UpdateTCPMonitorResponse`\n- `UpdateDNSMonitorRequest`, `UpdateDNSMonitorResponse`\n- `ListMonitorsRequest`, `ListMonitorsResponse`\n- `DeleteMonitorRequest`, `DeleteMonitorResponse`\n- `TriggerMonitorRequest`, `TriggerMonitorResponse`\n- `GetMonitorStatusRequest`, `GetMonitorStatusResponse`, `RegionStatus`\n- `GetMonitorSummaryRequest`, `GetMonitorSummaryResponse`\n\n### Monitor Enums\n\n- `Periodicity` — check interval\n- `Region` — monitoring region\n- `MonitorStatus` — active / degraded / error\n- `HTTPMethod` — HTTP methods\n- `TimeRange` — metrics time range\n- `NumberComparator`, `StringComparator`, `RecordComparator` — assertion comparators\n\n### Health Types\n\n- `CheckRequest`, `CheckResponse`\n- `ServingStatus` — serving / not serving\n\n### Status Report Types\n\n- `StatusReport`, `StatusReportSummary`, `StatusReportUpdate`\n- `CreateStatusReportRequest`, `CreateStatusReportResponse`\n- `GetStatusReportRequest`, `GetStatusReportResponse`\n- `ListStatusReportsRequest`, `ListStatusReportsResponse`\n- `UpdateStatusReportRequest`, `UpdateStatusReportResponse`\n- `DeleteStatusReportRequest`, `DeleteStatusReportResponse`\n- `AddStatusReportUpdateRequest`, `AddStatusReportUpdateResponse`\n- `StatusReportStatus` — investigating / identified / monitoring / resolved\n\n### Status Page Types\n\n- `StatusPage`, `StatusPageSummary`\n- `PageComponent`, `PageComponentGroup`\n- `PageSubscriber`\n- `CreateStatusPageRequest`, `CreateStatusPageResponse`\n- `GetStatusPageRequest`, `GetStatusPageResponse`\n- `ListStatusPagesRequest`, `ListStatusPagesResponse`\n- `UpdateStatusPageRequest`, `UpdateStatusPageResponse`\n- `DeleteStatusPageRequest`, `DeleteStatusPageResponse`\n- `AddMonitorComponentRequest`, `AddMonitorComponentResponse`\n- `AddStaticComponentRequest`, `AddStaticComponentResponse`\n- `RemoveComponentRequest`, `RemoveComponentResponse`\n- `UpdateComponentRequest`, `UpdateComponentResponse`\n- `CreateComponentGroupRequest`, `CreateComponentGroupResponse`\n- `DeleteComponentGroupRequest`, `DeleteComponentGroupResponse`\n- `UpdateComponentGroupRequest`, `UpdateComponentGroupResponse`\n- `SubscribeToPageRequest`, `SubscribeToPageResponse`\n- `UnsubscribeFromPageRequest`, `UnsubscribeFromPageResponse`\n- `ListSubscribersRequest`, `ListSubscribersResponse`\n- `GetStatusPageContentRequest`, `GetStatusPageContentResponse`\n- `GetOverallStatusRequest`, `GetOverallStatusResponse`, `ComponentStatus`\n- `OverallStatus`, `PageAccessType`, `PageTheme`, `PageComponentType`\n\n### Maintenance Types\n\n- `Maintenance`, `MaintenanceSummary`\n- `CreateMaintenanceRequest`, `CreateMaintenanceResponse`\n- `GetMaintenanceRequest`, `GetMaintenanceResponse`\n- `ListMaintenancesRequest`, `ListMaintenancesResponse`\n- `UpdateMaintenanceRequest`, `UpdateMaintenanceResponse`\n- `DeleteMaintenanceRequest`, `DeleteMaintenanceResponse`\n\n### Notification Types\n\n- `Notification`, `NotificationSummary`\n- `NotificationData`\n- `DiscordData`, `EmailData`, `GoogleChatData`, `GrafanaOncallData`, `NtfyData`, `OpsgenieData`, `PagerDutyData`, `SlackData`, `SmsData`, `TelegramData`, `WebhookData`, `WebhookHeader`, `WhatsappData`\n- `CreateNotificationRequest`, `CreateNotificationResponse`\n- `GetNotificationRequest`, `GetNotificationResponse`\n- `ListNotificationsRequest`, `ListNotificationsResponse`\n- `UpdateNotificationRequest`, `UpdateNotificationResponse`\n- `DeleteNotificationRequest`, `DeleteNotificationResponse`\n- `SendTestNotificationRequest`, `SendTestNotificationResponse`\n- `CheckNotificationLimitRequest`, `CheckNotificationLimitResponse`\n- `NotificationProvider`, `OpsgenieRegion`\n\n### Client Types\n\n- `OpenStatusClient` — client interface\n- `OpenStatusClientOptions` — client configuration\n- `createOpenStatusClient` — factory function\n- `openstatus` — default client instance\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/status-page-service.mdx",
    "content": "---\ntitle: Status Page Service\ndescription: \"Manage status pages, components, groups, and subscribers with the OpenStatus Node.js SDK\"\n---\n\nManage status pages, components, component groups, and subscribers. The Status Page Service provides 17 RPC methods.\n\n## Status Page CRUD\n\n### Create Status Page\n\n```typescript\nconst { statusPage } = await client.statusPage.v1.StatusPageService\n  .createStatusPage({\n    title: \"My Service Status\",\n    slug: \"my-service\",\n    description: \"Status page for My Service\",\n    homepageUrl: \"https://example.com\",\n    contactUrl: \"https://example.com/contact\",\n  });\n\nconsole.log(`Status page created: ${statusPage?.id}`);\n```\n\n### Get Status Page\n\n```typescript\nconst { statusPage } = await client.statusPage.v1.StatusPageService\n  .getStatusPage({ id: \"page_123\" });\n```\n\n### List Status Pages\n\n```typescript\nconst { statusPages, totalSize } = await client.statusPage.v1.StatusPageService\n  .listStatusPages({ limit: 10, offset: 0 });\n\nconsole.log(`Found ${totalSize} status pages`);\n```\n\n### Update Status Page\n\n```typescript\nconst { statusPage } = await client.statusPage.v1.StatusPageService\n  .updateStatusPage({\n    id: \"page_123\",\n    title: \"Updated Title\",\n    description: \"Updated description\",\n  });\n```\n\n### Delete Status Page\n\n```typescript\nconst { success } = await client.statusPage.v1.StatusPageService\n  .deleteStatusPage({ id: \"page_123\" });\n```\n\n## Components\n\nComponents represent individual services on a status page. They can be linked to a monitor (automatically reflects monitor status) or static (manually managed).\n\n### Add Monitor Component\n\n```typescript\nconst { component } = await client.statusPage.v1.StatusPageService\n  .addMonitorComponent({\n    pageId: \"page_123\",\n    monitorId: \"mon_456\",\n    name: \"API Server\",\n    description: \"Main API endpoint\",\n    order: 1,\n    groupId: \"group_789\",\n  });\n```\n\n### Add Static Component\n\n```typescript\nconst { component } = await client.statusPage.v1.StatusPageService\n  .addStaticComponent({\n    pageId: \"page_123\",\n    name: \"Third-party Service\",\n    description: \"External dependency\",\n    order: 2,\n  });\n```\n\n### Update Component\n\n```typescript\nconst { component } = await client.statusPage.v1.StatusPageService\n  .updateComponent({\n    id: \"comp_123\",\n    name: \"Updated Component Name\",\n    description: \"Updated description\",\n    order: 3,\n    groupId: \"group_789\",\n    groupOrder: 1,\n  });\n```\n\n### Remove Component\n\n```typescript\nconst { success } = await client.statusPage.v1.StatusPageService\n  .removeComponent({ id: \"comp_123\" });\n```\n\n## Component Groups\n\nGroup related components together on a status page.\n\n### Create Component Group\n\n```typescript\nconst { group } = await client.statusPage.v1.StatusPageService\n  .createComponentGroup({\n    pageId: \"page_123\",\n    name: \"Core Services\",\n  });\n```\n\n### Update Component Group\n\n```typescript\nconst { group } = await client.statusPage.v1.StatusPageService\n  .updateComponentGroup({\n    id: \"group_123\",\n    name: \"Updated Group Name\",\n  });\n```\n\n### Delete Component Group\n\n```typescript\nconst { success } = await client.statusPage.v1.StatusPageService\n  .deleteComponentGroup({ id: \"group_123\" });\n```\n\n## Subscribers\n\nManage email subscriptions to status page updates.\n\n### Subscribe to Page\n\n```typescript\nconst { subscriber } = await client.statusPage.v1.StatusPageService\n  .subscribeToPage({\n    pageId: \"page_123\",\n    email: \"user@example.com\",\n  });\n```\n\n### Unsubscribe from Page\n\nUnsubscribe by email or subscriber ID using the `identifier` oneof:\n\n```typescript\n// By email\nconst { success } = await client.statusPage.v1.StatusPageService\n  .unsubscribeFromPage({\n    pageId: \"page_123\",\n    identifier: { case: \"email\", value: \"user@example.com\" },\n  });\n\n// By subscriber ID\nconst { success: success2 } = await client.statusPage.v1.StatusPageService\n  .unsubscribeFromPage({\n    pageId: \"page_123\",\n    identifier: { case: \"id\", value: \"sub_456\" },\n  });\n```\n\n### List Subscribers\n\n```typescript\nconst { subscribers, totalSize } = await client.statusPage.v1.StatusPageService\n  .listSubscribers({\n    pageId: \"page_123\",\n    limit: 50,\n    offset: 0,\n    includeUnsubscribed: false,\n  });\n```\n\n## Get Status Page Content\n\nGet the full content of a status page including components, groups, active status reports, and maintenance windows. Identify the page by ID or slug.\n\n```typescript\nconst content = await client.statusPage.v1.StatusPageService\n  .getStatusPageContent({\n    identifier: { case: \"slug\", value: \"my-service\" },\n  });\n\nconsole.log(`Page: ${content.statusPage?.title}`);\nconsole.log(`Components: ${content.components.length}`);\nconsole.log(`Groups: ${content.groups.length}`);\nconsole.log(`Active reports: ${content.statusReports.length}`);\nconsole.log(`Maintenances: ${content.maintenances.length}`);\n```\n\n## Get Overall Status\n\nGet the aggregated status of a status page and per-component statuses.\n\n```typescript\nimport { createOpenStatusClient, OverallStatus } from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst { overallStatus, componentStatuses } = await client.statusPage.v1\n  .StatusPageService.getOverallStatus({\n    identifier: { case: \"id\", value: \"page_123\" },\n  });\n\nconsole.log(`Overall: ${OverallStatus[overallStatus]}`);\nfor (const { componentId, status } of componentStatuses) {\n  console.log(`  ${componentId}: ${OverallStatus[status]}`);\n}\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/status-report-service.mdx",
    "content": "---\ntitle: Status Report Service\ndescription: \"Manage incident reports and status updates with the OpenStatus Node.js SDK\"\n---\n\nManage incident reports with update timelines. The Status Report Service provides 6 RPC methods.\n\n## Create Status Report\n\n```typescript\nimport {\n  createOpenStatusClient,\n  StatusReportStatus,\n} from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst { statusReport } = await client.statusReport.v1.StatusReportService\n  .createStatusReport({\n    title: \"API Degradation\",\n    status: StatusReportStatus.INVESTIGATING,\n    message: \"We are investigating reports of increased latency.\",\n    date: \"2024-01-15T10:30:00Z\",\n    pageId: \"page_123\",\n    pageComponentIds: [\"comp_456\"],\n    notify: true,\n  });\n\nconsole.log(`Status report created: ${statusReport?.id}`);\n```\n\n## Add Status Report Update\n\nAdd a new entry to a status report's timeline.\n\n```typescript\nimport { StatusReportStatus } from \"@openstatus/sdk-node\";\n\nconst { statusReport } = await client.statusReport.v1.StatusReportService\n  .addStatusReportUpdate({\n    statusReportId: \"sr_123\",\n    status: StatusReportStatus.IDENTIFIED,\n    message: \"The issue has been identified as a database connection problem.\",\n    date: \"2024-01-15T11:00:00Z\",\n    notify: true,\n  });\n```\n\n## List Status Reports\n\nList status reports with optional status filtering and pagination.\n\n```typescript\nimport { StatusReportStatus } from \"@openstatus/sdk-node\";\n\nconst { statusReports, totalSize } = await client.statusReport.v1\n  .StatusReportService.listStatusReports({\n    limit: 10,\n    offset: 0,\n    statuses: [StatusReportStatus.INVESTIGATING, StatusReportStatus.IDENTIFIED],\n  });\n\nconsole.log(`Found ${totalSize} status reports`);\n```\n\n## Get / Update / Delete Status Reports\n\n### Get Status Report\n\nReturns the full report including the updates timeline.\n\n```typescript\nimport { StatusReportStatus } from \"@openstatus/sdk-node\";\n\nconst { statusReport } = await client.statusReport.v1.StatusReportService\n  .getStatusReport({ id: \"sr_123\" });\n\nconsole.log(`Title: ${statusReport?.title}`);\nconsole.log(`Status: ${StatusReportStatus[statusReport?.status ?? 0]}`);\n\nfor (const update of statusReport?.updates ?? []) {\n  console.log(`  ${update.date}: [${StatusReportStatus[update.status]}] ${update.message}`);\n}\n```\n\n### Update Status Report\n\n```typescript\nconst { statusReport } = await client.statusReport.v1.StatusReportService\n  .updateStatusReport({\n    id: \"sr_123\",\n    title: \"Updated Title\",\n    pageComponentIds: [\"comp_456\", \"comp_789\"],\n  });\n```\n\n### Delete Status Report\n\n```typescript\nconst { success } = await client.statusReport.v1.StatusReportService\n  .deleteStatusReport({ id: \"sr_123\" });\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/sdk/nodejs/typescript-tips.mdx",
    "content": "---\ntitle: TypeScript Tips\ndescription: \"Tips for working with bigint fields, oneof types, and migrating clients in the OpenStatus Node.js SDK\"\n---\n\n## Working with bigint Fields\n\nProtocol Buffers `int64` fields map to `bigint` in TypeScript. This affects:\n\n- **Monitor configuration**: `timeout`, `retry`, `degradedAt`\n- **Assertions**: `StatusCodeAssertion.target`\n- **Monitor summary**: `totalSuccessful`, `totalDegraded`, `totalFailed`, `p50`, `p75`, `p90`, `p95`, `p99`\n\nUse `BigInt()` to create values:\n\n```typescript\nconst { monitor } = await client.monitor.v1.MonitorService.createHTTPMonitor({\n  monitor: {\n    name: \"My API\",\n    url: \"https://example.com\",\n    periodicity: Periodicity.PERIODICITY_1M,\n    active: true,\n    timeout: BigInt(30000),       // 30 seconds\n    retry: BigInt(5),             // 5 retries\n    degradedAt: BigInt(3000),     // degraded after 3s\n    statusCodeAssertions: [\n      { comparator: NumberComparator.EQUAL, target: BigInt(200) },\n    ],\n  },\n});\n```\n\nReading bigint values:\n\n```typescript\nconst summary = await client.monitor.v1.MonitorService.getMonitorSummary({\n  id: \"mon_123\",\n  timeRange: TimeRange.TIME_RANGE_7D,\n  regions: [],\n});\n\n// bigint values — use Number() for display if values are safe\nconsole.log(`P95 latency: ${summary.p95}ms`);\nconsole.log(`Total checks: ${summary.totalSuccessful + summary.totalDegraded + summary.totalFailed}`);\n```\n\n## Handling oneof Types\n\nSeveral responses use protobuf `oneof` fields, which map to discriminated unions in TypeScript.\n\n### MonitorConfig (getMonitor)\n\n```typescript\nconst { monitor } = await client.monitor.v1.MonitorService.getMonitor({\n  id: \"mon_123\",\n});\n\nswitch (monitor?.config.case) {\n  case \"http\":\n    // monitor.config.value is HTTPMonitor\n    console.log(`URL: ${monitor.config.value.url}`);\n    break;\n  case \"tcp\":\n    // monitor.config.value is TCPMonitor\n    console.log(`URI: ${monitor.config.value.uri}`);\n    break;\n  case \"dns\":\n    // monitor.config.value is DNSMonitor\n    console.log(`Domain: ${monitor.config.value.uri}`);\n    break;\n}\n```\n\n### NotificationData (createNotification)\n\nThe `data.data` field selects the provider-specific configuration:\n\n```typescript\n// The case string matches the provider in camelCase\ndata: {\n  data: { case: \"slack\", value: { webhookUrl: \"...\" } }\n}\ndata: {\n  data: { case: \"googleChat\", value: { webhookUrl: \"...\" } }\n}\ndata: {\n  data: { case: \"grafanaOncall\", value: { webhookUrl: \"...\" } }\n}\n```\n\n### Status Page Identifiers\n\n`getStatusPageContent` and `getOverallStatus` accept a page identifier by ID or slug:\n\n```typescript\n// By ID\n{ identifier: { case: \"id\", value: \"page_123\" } }\n\n// By slug\n{ identifier: { case: \"slug\", value: \"my-service\" } }\n```\n\n### Unsubscribe Identifier\n\n`unsubscribeFromPage` accepts an email or subscriber ID:\n\n```typescript\n// By email\n{ identifier: { case: \"email\", value: \"user@example.com\" } }\n\n// By subscriber ID\n{ identifier: { case: \"id\", value: \"sub_456\" } }\n```\n\n## Migrating from Default Client to createOpenStatusClient\n\n**Before** — manual headers on every call:\n\n```typescript\nimport { openstatus } from \"@openstatus/sdk-node\";\n\nconst headers = { \"x-openstatus-key\": process.env.OPENSTATUS_API_KEY };\n\nconst { httpMonitors } = await openstatus.monitor.v1.MonitorService\n  .listMonitors({}, { headers });\n\nconst { monitor } = await openstatus.monitor.v1.MonitorService\n  .createHTTPMonitor({\n    monitor: { name: \"API\", url: \"https://example.com\", periodicity: 2, active: true },\n  }, { headers });\n```\n\n**After** — configure once, use everywhere:\n\n```typescript\nimport { createOpenStatusClient, Periodicity } from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst { httpMonitors } = await client.monitor.v1.MonitorService\n  .listMonitors({});\n\nconst { monitor } = await client.monitor.v1.MonitorService\n  .createHTTPMonitor({\n    monitor: {\n      name: \"API\",\n      url: \"https://example.com\",\n      periodicity: Periodicity.PERIODICITY_1M,\n      active: true,\n    },\n  });\n```\n"
  },
  {
    "path": "apps/docs/src/content/docs/tutorial/get-started-with-openstatus-cli.mdx",
    "content": "---\ntitle: Get Started with openstatus CLI\ndescription: \"Step-by-step tutorial to install and use the openstatus CLI for monitoring as code\"\n---\n\nimport { Image } from 'astro:assets';\nimport { Aside } from '@astrojs/starlight/components';\nimport CLI from '../../../assets/tutorial/get-started-with-openstatus-cli/CLI.png';\n\n## What you'll learn\n\n| | |\n|---|---|\n| **Time** | ~10 minutes |\n| **Level** | Intermediate |\n| **Prerequisites** | openstatus account, command line experience |\n\nIn this tutorial, you'll learn how to use the openstatus CLI to manage your monitors as code. This enables you to version control your monitoring configuration, automate deployments, and implement GitOps workflows.\n\n### Prerequisites\n\n- An openstatus account\n- Command line experience\n- API token from your openstatus workspace (Settings → API)\n\n### What you'll build\n\nBy the end of this tutorial, you'll have:\n- openstatus CLI installed on your system\n- Monitors exported to a YAML configuration file\n- Understanding of monitoring as code workflows\n- Ability to manage monitors programmatically\n\n<Image\n  src={CLI}\n  alt=\"openstatus CLI in action showing monitor management\"\n/>\n\n## Installation\n\nInstall the openstatus CLI to manage your monitors directly from code.\n\n### macOS\n\nUsing Homebrew (recommended):\n```bash\nbrew install openstatusHQ/cli/openstatus --cask\n```\n\nOr using the install script:\n```bash\ncurl -fsSL https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.sh | bash\n```\n\n### Linux\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.sh | bash\n```\n\n### Windows\n\n```powershell\niwr https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.ps1 | iex\n```\n\n### Verify installation\n\nRun the following command to confirm the CLI is installed:\n\n```bash\nopenstatus --version\n```\n\nYou should see output like:\n\n```\nopenstatus version x.x.x\n```\n\n## Configure API authentication\n\nCreate an API key in your workspace settings (Settings → API), then set it as an environment variable:\n\n```bash\n# macOS / Linux\nexport OPENSTATUS_API_TOKEN=<your-api-token>\n```\n\n```powershell\n# Windows PowerShell\n$env:OPENSTATUS_API_TOKEN=\"<your-api-token>\"\n```\n\n<Aside>Add this to your shell profile (`~/.bashrc`, `~/.zshrc`) to persist across sessions.</Aside>\n\n## Import existing monitors\n\nStart by importing your existing monitors from your workspace to a YAML file:\n\n```bash\nopenstatus monitors import\n```\n\nYou should see output confirming the import:\n\n```\nSuccessfully imported X monitors to openstatus.yaml\n```\n\nThis creates an `openstatus.yaml` file containing all your current monitors. This file becomes your single source of truth for monitoring configuration.\n\n**Checkpoint:** Open the `openstatus.yaml` file and verify it contains your monitors. You should see entries with your monitor names and URLs.\n\n## Manage monitors as code\n\nNow you can add, remove, or update monitors in the YAML file and apply your changes:\n\n```bash\nopenstatus monitors apply\n```\n\nThe CLI will show you a diff of changes before applying them, ensuring you're aware of what will be modified.\n\n## What you've accomplished\n\nExcellent work! You've successfully:\n- ✅ Installed the openstatus CLI\n- ✅ Configured API authentication\n- ✅ Imported monitors to a YAML file\n- ✅ Learned the monitoring as code workflow\n\n## Troubleshooting\n\n### \"command not found: openstatus\"\n\n**Cause:** The CLI binary is not in your PATH.\n\n**Fix (macOS/Homebrew):**\n```bash\nbrew reinstall openstatusHQ/cli/openstatus --cask\n```\n\n**Fix (install script):** Ensure `~/.local/bin` is in your PATH:\n```bash\nexport PATH=\"$HOME/.local/bin:$PATH\"\n```\n\n### \"unauthorized\" or \"invalid token\" error\n\n**Cause:** Your API token is missing or incorrect.\n\n**Fix:**\n1. Verify the token is set: `echo $OPENSTATUS_API_TOKEN`\n2. Regenerate the token in your workspace settings (Settings → API)\n3. Make sure there are no extra spaces or newlines in the token value\n\n### \"no monitors found\" on import\n\n**Cause:** Your workspace has no monitors, or the token belongs to a different workspace.\n\n**Fix:** Create at least one monitor in the dashboard first, then retry the import.\n\n## What's next?\n\nNow that you have the CLI set up, you can:\n\n- **[Monitor Your MCP Server](/guides/how-to-monitor-mcp-server/)** - Example of CLI-based monitor configuration\n- **[CLI Reference](/reference/cli-reference)** - Complete command documentation\n- **[Set up CI/CD](/guides/how-to-run-synthetic-test-github-action/)** - Automate monitoring in your pipeline\n\n### Advanced workflows\n\nWith the CLI, you can:\n- Version control your monitoring configuration with Git\n- Review monitoring changes in pull requests\n- Automate monitor creation for new services\n- Sync monitors across multiple environments\n- Implement GitOps for infrastructure monitoring\n\n## Learn more\n\n- **[Monitoring as Code Concept](/concept/uptime-monitoring-as-code)** - Why manage monitors as code\n- **[CLI Reference](/reference/cli-reference)** - All available commands\n- **[YAML Configuration Examples](https://github.com/openstatusHQ/cli-template)** - Sample configurations\n"
  },
  {
    "path": "apps/docs/src/content/docs/tutorial/getting-started.mdx",
    "content": "---\ntitle: Tutorials Overview\ndescription: \"Step-by-step tutorials to create monitors, status pages, and private locations with OpenStatus.\"\nsidebar:\n  label: Tutorials Overview\n  order: 1\n---\n\n## Tutorials\n\n### What you'll learn\n\nOur tutorials are designed to help you:\n- Get your first monitor up and running\n- Create and configure status pages\n- Set up monitoring infrastructure\n- Use openstatus CLI for automation\n\n### Core Tutorials\n\nStart your journey with openstatus:\n\n- **[Create Your First Monitor](/tutorial/how-to-create-monitor)** (~5 min) - Learn the fundamentals by setting up uptime monitoring for your first endpoint\n- **[Create a Status Page](/tutorial/how-to-create-status-page)** (~5 min) - Build a public status page to communicate service health to your users\n- **[Configure Your Status Page](/tutorial/how-to-configure-status-page)** (~10 min) - Customize your status page with monitors, domains, and protection\n- **[Set Up the Slack Agent](/tutorial/how-to-setup-slack-agent)** (~5 min) - Manage incidents directly from Slack\n\n### Advanced Tutorials\n\nOnce you're comfortable with the basics:\n\n- **[Create a Private Location (Beta)](/tutorial/how-to-create-private-location)** (~15 min) - Set up monitoring from your own infrastructure\n- **[Get Started with openstatus CLI](/tutorial/get-started-with-openstatus-cli)** (~10 min) - Automate monitor management with our command-line tool\n\n### What's next?\n\nAfter completing these tutorials, you'll be ready to:\n- Explore [how-to guides](/guides/getting-started) for specific tasks and advanced scenarios\n- Dive into [explanations](/concept/getting-started) to understand the concepts behind the features\n- Reference our [technical documentation](/reference/cli-reference) for detailed specifications\n"
  },
  {
    "path": "apps/docs/src/content/docs/tutorial/how-to-configure-status-page.mdx",
    "content": "---\ntitle: Configure Your Status Page\ndescription: \"A step-by-step tutorial to customize and configure your status page\"\n---\n\nimport { Image } from 'astro:assets';\nimport { Aside } from '@astrojs/starlight/components';\nimport { ShowcaseYouTube } from 'starlight-showcases'\n\nimport ConfigureStatusPage1 from '../../../assets/tutorial/configure-status-page/configure-status-page-1.png';\nimport StatusPageFloating from '../../../assets/tutorial/configure-status-page/status-page-floating.png';\nimport StatusPageBeta1 from '../../../assets/tutorial/configure-status-page/status-page-beta-1.png';\nimport StatusPageBeta2 from '../../../assets/tutorial/configure-status-page/status-page-beta-2.png';\nimport StatusPageBeta3 from '../../../assets/tutorial/configure-status-page/status-page-beta-3.png';\nimport StatusPageBeta4 from '../../../assets/tutorial/configure-status-page/status-page-beta-4.png';\n\n## What you'll learn\n\nIn this tutorial, you'll learn how to customize your status page's appearance and behavior. You'll explore different display options, themes, and configuration settings to create a status page that matches your brand and communication style.\n\n### Prerequisites\n\n- An openstatus account\n- A status page already created (see [Create a Status Page](/tutorial/how-to-create-status-page))\n- At least one monitor added to your status page\n\n### What you'll build\n\nBy the end of this tutorial, you'll have:\n- A customized status page with your preferred theme\n- Configured status trackers displaying data your way\n- Links to important resources\n- Preview and live configuration experience\n\n## Status Page Customization\n\nOpenStatus offers enhanced status page customization with multiple themes and display options.\n\nExplore available themes: [https://themes.openstatus.dev](https://themes.openstatus.dev)\n\n## Get started\n\nGo to the **Status Page Redesign** section in your status page settings and toggle `Enable New Version`. Once enabled, you'll see three subsections:\n\n1. **Tracker Configuration**\n2. **Theme Explorer**\n3. **Links**\n\n<Image\n    src={ConfigureStatusPage1}\n    alt=\"Create your status page\"\n/>\n\n### View and configure status page\n\nBefore choosing to enable the new page, we provide you with a way to check the configuration first. Click on the **View and configure status page** and you'll get forwarded to your status page and a bottom right floating button will appear. Once you're done, click on the **Dashboard** and you'll be forwarded to your page where you get asked to save the config before continuing.\n\n<Image\n    src={StatusPageFloating}\n    alt=\"Status page floating configuration popover\"\n/>\n\n\n---\n\n<Aside>Once enabled, the subdomain will have a rewrite to the new project. We are adding a `maxAge: 600` request cookie to improve the page load. **If you decide to deactivate the new version, it might take up to 10 minutes for the user to see the old page.**</Aside>\n\n### 1. Tracker Configuration\n\nWe have three new status tracker configurations to provide you with a maximum choice of displaying the collected data.\n\n**Bar Type**: How every 'day' is displayed in for a status tracker. Either **absolute** or **manual**.\n\n**Card Type**: The card type is only configurable if the bar type is **absolute**. You'll then be able to choose between **duration**, which will show the duration of \"success\", \"error\", \"degraded\" or \"maintenance\" reports or **requests** where we will share the number of request status itself. If **manual** bar type is chosen, we will only show the most significant status of the day.\n\n**Show Uptime**: The uptime is calculated by either the **duration** of the different reports _or_ the **request** values depending on what you've chosen for the **absolute** value (incl. incidents). If you've chosen **manual**, it only gets calculated by the duration of your status reports.\n\nA few examples to understand it:\n\nExample of **absolute bar** with **duration card** and **showing uptime**\n\n<Image\n    src={StatusPageBeta3}\n    alt=\"absolute bar type with duration card and showing uptime\"\n/>\n\nExample of **absolute bar** with **request card** and **hiding uptime**\n\n<Image\n    src={StatusPageBeta4}\n    alt=\"absolute bar type with request card and hiding uptime\"\n/>\n\nExample of **manual bar** with **simple card** and **hiding uptime**\n\n<Image\n    src={StatusPageBeta2}\n    alt=\"manual bar type and hiding uptime\"\n/>\n\n### 2. Theme Explorer\n\nYou can choose between different themes. We start with the following three:\n\n- `default` (openstatus)\n- `supabase`\n- `github-high-contrast`\n\nVisit [themes.openstatus.dev](https://themes.openstatus.dev) to see the list of supported themes. If you want, you can contribute your own to the list.\n\n### 3. Links\n\nLet's have a closer look at your status page header navigation:\n\n<Image\n    src={StatusPageBeta1}\n    alt=\"Header navigation of your status page\"\n/>\n\n**Homepage URL**: Your logo will support linking to your own website.\n\n**Contact URL**: If filled out, you will see a `Message` icon that users can click to forward them to a contact page. This can also be an email client by starting the input with `mailto:` (e.g. `mailto:support@openstatus.dev`).\n\n---\n\nWe are continuously adding new features. Feel free to let us know what's missing!\n\n## What you've accomplished\n\nExcellent work! You've successfully:\n- ✅ Enabled and configured the new status page design\n- ✅ Customized status tracker display options\n- ✅ Explored and applied theme settings\n- ✅ Added navigation links to your status page\n- ✅ Previewed changes before making them live\n\n## What's next?\n\nNow that your status page is configured, you can:\n\n- **[Building Trust with Status Pages](/concept/best-practices-status-page)** - Learn effective incident communication\n- **[Add Status Subscribers](/reference/subscriber)** - Let users subscribe to updates\n\n### Learn more\n\n- **[Status Page Reference](/reference/status-page)** - Complete configuration options\n- **[Uptime Calculation Values](/concept/uptime-calculation-and-values)** - How uptime percentages work\n\n\n## Video Tutorial\n\n<ShowcaseYouTube\n  entries={[\n    {\n      href: 'https://www.youtube.com/watch?v=igMbSrej6RQ',\n      title: 'View, configure and enable the new status page',\n    },\n\n  ]}\n/>\n"
  },
  {
    "path": "apps/docs/src/content/docs/tutorial/how-to-create-monitor.mdx",
    "content": "---\ntitle: Create an Uptime Monitor in 5 Minutes\ndescription: \"Set up your first uptime monitor with OpenStatus — track response time, status codes, and availability from 35+ global locations.\"\n---\n\nimport { Image } from 'astro:assets';\nimport { Aside, CardGrid, LinkCard } from '@astrojs/starlight/components';\nimport monitorOverview from '../../../assets/tutorial/create-monitor/monitor-overview.png';\nimport createMonitor from '../../../assets/tutorial/create-monitor/create-monitor-1.png';\nimport createMonitor2 from '../../../assets/tutorial/create-monitor/create-monitor-2.png';\n\nimport { ShowcaseYouTube } from 'starlight-showcases'\n\n## What you'll learn\n\nIn this tutorial, you'll learn how to create your first monitor an automated watchdog for your services. A monitor periodically checks your endpoints to ensure they are available, performant, and returning the correct data. Think of it as a `curl` command that runs 24/7, providing continuous insights into the health of your application.\n\n### Prerequisites\n\n- An openstatus account (free tier available at [openstatus.dev](https://www.openstatus.dev))\n- A URL endpoint to monitor (can be your own service or any public URL)\n\n### What you'll build\n\nBy the end of this tutorial, you'll have:\n- A working uptime monitor checking your endpoint every minute\n- Real-time metrics showing response time and status codes\n- Understanding of how to customize HTTP requests for different scenarios\n\n\n## Get Started in 1 Minute\n\nLet's get your first uptime check up and running.\n\n### 1. Create the Monitor\n\n  <Image\n    src={createMonitor}\n    alt=\"Monitors page showing the Create Monitor button in the sidebar\"\n  />\n\nNavigate to the **Monitors** page from the sidebar and click the **Create Monitor** button. This will open a new configuration screen.\n\n\n### 2. Configure the Basics\n\n  <Image\n    src={createMonitor2}\n    alt=\"Monitor creation form with name and URL fields\"\n  />\n\nTo get your monitor started, you only need to provide two essential pieces of information:\n- **Name:** A clear, descriptive name for your monitor (e.g., \"Production API Health Check\" or \"Homepage Uptime\").\n- **URL:** The full URL of the endpoint you want to test (e.g., `https://openstat.us`).\n\nAs soon as you enter the URL, our monitoring tool will automatically begin tracking key performance metrics for every check, including:\n- **Response Time:** The total time it takes for the request to complete.\n- **Status Code:** The HTTP status code returned by the server (e.g., 200, 404, 500).\n- **Response Headers:** A detailed view of the headers returned by the server.\n- **Detailed Timing Metrics:** A breakdown of the time spent on each phase of the request (DNS lookup, TCP connection, TLS handshake, etc.)\n\n\n  <Image\n    src={monitorOverview}\n    alt=\"Monitor overview dashboard showing response time and status code charts\"\n  />\n\n**Checkpoint:** After saving, you should see your monitor's overview page with response time and status code charts. If data appears within a few seconds, your monitor is running.\n\n\n### 3. Customizing the HTTP Request\n\nYour monitor doesn't just have to be a simple `GET` request. You can customize the HTTP request to simulate real-world traffic and test specific scenarios.\n\n#### HTTP Method\n\nChoose the appropriate HTTP method for your check. While `GET` is the default and most common for simple health checks, you can also select `POST`, `HEAD`, `OPTIONS`, `PUT`, `PATCH`, `DELETE`, or `TRACE`.\n\n- `GET`: Retrieve data from an endpoint. The most common choice for health checks.\n- `POST`: Send data to an endpoint, for example, to test a form submission or API creation endpoint.\n- `HEAD`: Same as `GET`, but without the response body. Useful for quickly checking if a resource exists.\n- `OPTIONS`: Retrieve the supported HTTP methods for a resource.\n- `PUT`: Update an existing resource.\n- `PATCH`: Partially update an existing resource.\n- `DELETE`: Delete a resource.\n- `TRACE`: Echo the request back to the client.\n\n#### Request Body\n\nIf you select the `POST` method, you can add a Request Body to your monitor's configuration. This is essential for testing API endpoints that require a JSON, form-encoded, or other data payload. Simply enter the data you want to send in the provided text area.\n\n#### Custom Headers\n\nYou can add any number of custom HTTP headers to your request. This is particularly useful for:\n\n- **Authentication:** Sending an Authorization token (e.g., a Bearer token) to test a protected endpoint.\n- **Content Type:** Setting a `content-type` header to specify a content type (e.g., `application/json`).\n- **Content Negotiation:** Setting an Accept header to request a specific content type from the server (e.g., `application/json`).\n- **Simulating Clients:** Adding a User-Agent header to simulate traffic from a specific browser or device.\n\n\n<Aside title=\"We've got your User-Agent covered!\">\nopenstatus automatically includes the `\"User-Agent\": \"openstatus/1.0\"` header in every request. This makes it easy to identify and filter out our monitoring traffic from your server logs or analytics.\n</Aside>\n\n\n## Important Considerations\n### Monitoring Third-Party Endpoints\n<Aside>If you're monitoring a URL you don't own (like `google.com` or a partner API), your requests might be blocked by firewalls or rate limiters (e.g., Cloudflare). This is a security measure on their end to prevent scraping or denial-of-service attacks.</Aside>\n\n## What you've accomplished\n\nCongratulations! You've successfully:\n- ✅ Created your first uptime monitor\n- ✅ Configured basic HTTP monitoring settings\n- ✅ Learned about customizing requests with methods, headers, and body\n- ✅ Understood key monitoring metrics (response time, status codes, timing breakdown)\n\n## What's next?\n\nNow that you have a monitor running, you can:\n\n- **[Create a Status Page](/tutorial/how-to-create-status-page)** - Share your service status publicly with users\n\n\n### Learn more\n\n- **[Understanding Uptime Monitoring](/concept/uptime-monitoring)** - Deep dive into monitoring concepts\n- **[HTTP Monitor Reference](/reference/http-monitor)** - Complete technical specifications\n\n\n\n## Video Tutorial\n\n<ShowcaseYouTube\n  entries={[\n    {\n      href: 'https://www.youtube.com/embed/nYti3DjHoWY?si=RBGFHzoHFmwphRf',\n      title: 'Create your first monitor',\n    },\n\n  ]}\n/>\n"
  },
  {
    "path": "apps/docs/src/content/docs/tutorial/how-to-create-private-location.mdx",
    "content": "---\ntitle: Create a Private Location\ndescription: \"Set up monitoring from your own infrastructure using Docker-based private probes\"\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n## What you'll learn\n\n| | |\n|---|---|\n| **Time** | ~15 minutes |\n| **Level** | Advanced |\n| **Prerequisites** | OpenStatus account, Docker installed |\n\nIn this tutorial, you'll set up a **private location** — a monitoring probe running on your own infrastructure. This lets you monitor internal applications, private APIs, and network resources behind your firewall without exposing them to the public internet.\n\n### Prerequisites\n\n- An OpenStatus account ([openstatus.dev](https://www.openstatus.dev))\n- Docker installed on your server or local machine (`docker --version` to verify)\n- At least one monitor already created (see [Create Your First Monitor](/tutorial/how-to-create-monitor))\n\n### What you'll build\n\nBy the end of this tutorial, you'll have:\n- A private location configured in OpenStatus\n- A Docker container running the monitoring probe on your infrastructure\n- Monitors assigned to check from your private location\n\n<Aside type=\"caution\">Private locations are currently in **beta**. Some features like incident creation and public status page support are not yet available for private locations.</Aside>\n\n## What are private locations?\n\nPrivate locations allow you to monitor internal applications from within your own infrastructure, rather than solely relying on our public cloud-based regions. By deploying monitoring probes as Docker containers on your own machines, you can:\n\n- Monitor internal APIs and services behind your firewall\n- Get detailed timing and latency data from your specific deployment location\n- Monitor from on-prem servers, Raspberry Pi devices, or any Docker-capable machine\n\n## Step 1: Create a private location\n\n1. Navigate to **Settings** > **Private Locations** in your OpenStatus dashboard\n2. Click **Create Private Location**\n3. Give your location a descriptive name (e.g., \"Office Network\", \"AWS us-east-1 VPC\")\n4. Save the configuration\n\nAfter creation, you'll receive a **token**. Copy this token — you'll need it in the next step.\n\n<Aside type=\"caution\">Keep your token secure. It authenticates your probe with the OpenStatus platform.</Aside>\n\n## Step 2: Deploy the Docker probe\n\nRun the monitoring probe on your server using Docker:\n\n```bash\ndocker run -d \\\n  --name openstatus-probe \\\n  --restart unless-stopped \\\n  -e OPENSTATUS_TOKEN=<your-token> \\\n  ghcr.io/openstatushq/probe:latest\n```\n\nReplace `<your-token>` with the token from Step 1.\n\n### Verify the container is running\n\n```bash\ndocker ps --filter name=openstatus-probe\n```\n\nYou should see the container listed with a status of `Up`:\n\n```\nCONTAINER ID   IMAGE                              STATUS         NAMES\nabc123         ghcr.io/openstatushq/probe:latest  Up 2 minutes   openstatus-probe\n```\n\n## Step 3: Assign monitors to your private location\n\n1. Go to **Settings** > **Private Locations** and select your location\n2. Choose which monitors should run from this private location\n3. Alternatively, edit an individual monitor and select your private location in its settings\n\n**Checkpoint:** Within a couple of minutes, you should see monitoring data appearing in your monitor's overview from your private location.\n\n## What you've accomplished\n\nYou've successfully:\n- ✅ Created a private location in OpenStatus\n- ✅ Deployed a monitoring probe on your own infrastructure\n- ✅ Assigned monitors to check from your private location\n\n## Current limitations\n\n- Incidents are **not yet created** for monitors running via private locations. Continue using OpenStatus public regions if you need alerting.\n- Public monitors on status pages **do not yet support** private locations.\n\n## Troubleshooting\n\n### Container exits immediately\n\nCheck the container logs for errors:\n\n```bash\ndocker logs openstatus-probe\n```\n\nCommon causes:\n- **Invalid token:** Double-check the token value — ensure no extra spaces or newlines\n- **Network issues:** Ensure the container can reach `api.openstatus.dev` on port 443\n\n### No data appearing in the dashboard\n\n1. Verify the container is running: `docker ps --filter name=openstatus-probe`\n2. Check that you've assigned at least one monitor to the private location\n3. Wait 2-3 minutes — there may be a short delay before the first check runs\n\n### Container can't reach internal services\n\nIf the probe needs to reach services on your host machine, use Docker's host networking:\n\n```bash\ndocker run -d \\\n  --name openstatus-probe \\\n  --restart unless-stopped \\\n  --network host \\\n  -e OPENSTATUS_TOKEN=<your-token> \\\n  ghcr.io/openstatushq/probe:latest\n```\n\n## What's next?\n\n- **[Read our Raspberry Pi deployment guide](https://www.openstatus.dev/blog/deploy-private-locations-raspberry-pi)** — Deploy probes on low-cost hardware\n- **[Create a Monitor](/tutorial/how-to-create-monitor)** — Set up more monitors to run from your private location\n"
  },
  {
    "path": "apps/docs/src/content/docs/tutorial/how-to-create-status-page.mdx",
    "content": "---\ntitle: Create a Status Page\ndescription: \"A step-by-step tutorial to create and publish your first status page\"\n---\n\nimport { Image } from 'astro:assets';\nimport { Aside } from '@astrojs/starlight/components';\n\nimport CreateStatusPage1 from '../../../assets/tutorial/create-status-page/create-status-page-1.png';\n\nimport CreateStatusPage2 from '../../../assets/tutorial/create-status-page/create-status-page-2.png';\n\n## What you'll learn\n\n| | |\n|---|---|\n| **Time** | ~5 minutes |\n| **Level** | Beginner |\n| **Prerequisites** | OpenStatus account, at least one monitor |\n\nIn this tutorial, you'll create a public status page to communicate your service's health to users. A status page is a transparent way to show real-time uptime information and keep your users informed during incidents.\n\n### Prerequisites\n\n- An openstatus account\n- At least one monitor created (see [Create Your First Monitor](/tutorial/how-to-create-monitor))\n\n### What you'll build\n\nBy the end of this tutorial, you'll have:\n- A public status page showing your service health\n- Monitors displayed on your status page\n- Understanding of privacy and security options\n\n## Get started\n\n### 1. Create the status page\n\nNavigate to the **Status Pages** page from the sidebar and click the **Create Status Page** button. This will open a new configuration screen.\n\n<Image\n    src={CreateStatusPage1}\n    alt=\"Status Pages sidebar with the Create Status Page button highlighted\"\n/>\n\n\n### 2. Configure the status page\n\nFill in the basic details for your status page:\n\n- **Title:** A name for your status page (e.g., \"Acme Status\" or \"API Health\")\n- **Slug:** The URL path for your status page (e.g., `acme` creates `acme.openstatus.dev`)\n\n<Image\n    src={CreateStatusPage2}\n    alt=\"Status page configuration form showing title, slug, and monitor selection\"\n/>\n\n#### Add monitors\n\nSelect the monitors you want to display on your status page. Each monitor will show as a separate service with its own uptime bar. You can add multiple monitors to give users a complete picture of your infrastructure health.\n\n#### Custom domain\n\nYou can use your own domain (e.g., `status.yourdomain.com`) instead of the default `*.openstatus.dev` subdomain. To set this up:\n\n1. Enter your custom domain in the **Custom Domain** field\n2. Add a CNAME record pointing to `status.openstatus.dev` in your DNS provider\n3. Wait for DNS propagation (usually a few minutes, up to 48 hours)\n\nSee the [Status Page Reference](/reference/status-page) for detailed DNS configuration instructions.\n\n#### Password protection\n\nEnable password protection to restrict access to your status page. This is useful for internal status pages that should only be visible to your team or specific customers. Enter a password, and visitors will be prompted to authenticate before viewing the page.\n\n**Checkpoint:** After saving, click the link to your status page (shown at the top of the settings). You should see your monitors listed with uptime bars.\n\n## What you've accomplished\n\nGreat work! You've successfully:\n- ✅ Created your first status page\n- ✅ Added monitors to display service health\n- ✅ Learned about custom domains and password protection\n\n## What's next?\n\nNow that you have a basic status page, you can:\n\n- **[Configure Your Status Page](/tutorial/how-to-configure-status-page)** - Customize appearance and add more features\n- **[Building Trust with Status Pages](/concept/best-practices-status-page)** - Learn how to communicate effectively\n\n### Learn more\n\n- **[Status Page Reference](/reference/status-page)** - Complete configuration options\n- **[Understanding Uptime Values](/concept/uptime-calculation-and-values)** - How uptime is calculated\n"
  },
  {
    "path": "apps/docs/src/content/docs/tutorial/how-to-setup-slack-agent.mdx",
    "content": "---\ntitle: Set Up the OpenStatus Slack Agent\ndescription: \"A step-by-step tutorial to install the OpenStatus Slack agent and manage incidents directly from Slack\"\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n## What you'll learn\n\n| | |\n|---|---|\n| **Time** | ~5 minutes |\n| **Level** | Beginner |\n| **Prerequisites** | OpenStatus account, Slack workspace admin access |\n\nIn this tutorial, you'll learn how to install the OpenStatus Slack agent so you can manage incidents directly from your Slack workspace — no need to switch to the dashboard.\n\n### Prerequisites\n\n- An OpenStatus account ([openstatus.dev](https://www.openstatus.dev))\n- A Slack workspace where you have permission to install apps\n\n### What you'll get\n\nBy the end of this tutorial, you'll have:\n- The OpenStatus Slack agent installed in your workspace\n- The ability to create, update, and resolve incidents from Slack\n\n## Install the Slack Agent\n\n### 1. Go to Settings\n\nNavigate to **Settings** > **Integrations** in your OpenStatus dashboard.\n\n### 2. Install the Slack Integration\n\nClick the **Install Slack** button. You'll be redirected to Slack's authorization page where you can select the workspace and channel you want to connect.\n\nGrant the requested permissions and click **Allow** to complete the installation.\n\n<Aside>Make sure you select the correct Slack workspace if you belong to multiple workspaces.</Aside>\n\n### 3. Verify the Installation\n\nAfter completing the OAuth flow, go to the Slack channel you selected and type:\n\n```\n@openstatus what's the status of my monitors?\n```\n\nThe bot should respond with a summary of your monitors. If you see a response, the installation is working.\n\n<Aside type=\"tip\">The bot only responds when mentioned with `@openstatus` — it won't interrupt your conversations.</Aside>\n\n### 4. Start Managing Incidents from Slack\n\nHere are some examples of what you can do:\n\n#### Create an incident\n\nNotify your subscribers about a new issue.\n\n```\n@openstatus create an incident for the payment API – high latency detected.\n```\n\n#### Update an incident\n\nKeep your status page updated while you investigate.\n\n```\n@openstatus keep the status page updated that we are still monitoring the issue.\n```\n\n#### Resolve an incident\n\nClose an active incident and let your subscribers know.\n\n```\n@openstatus resolve the ongoing incident on my API status page.\n```\n\n#### Schedule maintenance\n\nPlan downtime so subscribers are informed in advance.\n\n```\n@openstatus schedule a maintenance window for my database next Friday from 2–3 PM.\n```\n\n## What you've accomplished\n\nCongratulations! You've successfully:\n- ✅ Installed the OpenStatus Slack agent in your workspace\n- ✅ Connected your OpenStatus account to Slack\n- ✅ Learned how to manage incidents directly from Slack\n\n## Troubleshooting\n\n### The bot doesn't respond when mentioned\n\n1. **Check the channel:** Make sure you're mentioning `@openstatus` in the channel you selected during installation.\n2. **Check permissions:** Go to **Settings** > **Integrations** and verify the Slack integration shows as connected.\n3. **Reinstall:** If the integration appears disconnected, click **Install Slack** again to re-authorize.\n\n### \"Not authorized\" or permission errors\n\nYour Slack workspace admin may need to approve the app. Ask your workspace admin to go to **Slack Admin** > **Manage Apps** and approve the OpenStatus integration.\n\n### Bot responds but can't find monitors\n\nMake sure you have at least one monitor and one status page created in your OpenStatus workspace before using incident commands.\n\n## What's next?\n\n- **[Create a Status Page](/tutorial/how-to-create-status-page)** — share your service status publicly with users\n- **[Create a Monitor](/tutorial/how-to-create-monitor)** — set up uptime monitoring for your endpoints\n"
  },
  {
    "path": "apps/docs/src/custom.css",
    "content": "@layer my-reset, starlight;\n\n.card {\n  border-radius: 0rem;\n}\n\n.card .icon {\n    border: 1px solid var(--sl-color-gray-5);\n    background-color: var(--sl-color-black);\n    padding: 0.2em;\n    border-radius: 0.25rem;\n  }\n\n  :is(h1, h2, h3, h4, h5, h6) {\n    font-family: \"CommitMono\",  sans-serif;\n  }\n\n  sl-sidebar-state-persist summary {\n      font-family: \"CommitMono\",  sans-serif;\n  }\n\n.sl-link-card {\n    border-radius: 0;\n}\n\na {\n    border-radius: 0;\n}\n\nbutton{\n    border-radius: 0;\n}\n\n.card .title {\n    font-family: \"CommitMono\",  sans-serif;\n}\n\n.sl-link-card .title {\n    font-family: \"CommitMono\",  sans-serif;\n}\n"
  },
  {
    "path": "apps/docs/src/env.d.ts",
    "content": "/// <reference path=\"../.astro/types.d.ts\" />\n/// <reference types=\"astro/client\" />\n"
  },
  {
    "path": "apps/docs/src/global.css",
    "content": "@layer base, starlight, theme, components, utilities;\n\n@import '@astrojs/starlight-tailwind';\n@import 'tailwindcss/theme.css' layer(theme);\n@import 'tailwindcss/utilities.css' layer(utilities);\n\n@theme {\n  --font-cal: \"calsans\", \"sans-serif\";\n  --font-mono: 'CommitMono', 'sans-serif';\n  --font-sans: 'Inter', 'sans-serif';\n}\n\n@layer base {\n  @font-face {\n    font-family: 'calsans';\n    src: url('/fonts/CalSans-SemiBold.ttf') format('truetype');\n    font-weight: 600;\n    font-style: normal;\n    font-display: swap;\n  }\n  @font-face {\n    font-family: 'CommitMono';\n    src:\n        url('/fonts/CommitMono-400-Regular.otf') format('opentype');\n    font-weight: 400;\n    font-style: normal;\n    font-display: swap;\n  }\n  @font-face {\n    font-family: 'CommitMono';\n    src:\n        url('/fonts/CommitMono-700-Regular.otf') format('opentype');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Inter Variable';\n    font-style: normal;\n    font-display: swap;\n    font-weight: 100 900;\n    src: url(@fontsource-variable/inter/files/inter-latin-wght-normal.woff2) format('woff2-variations');\n    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n  }\n}\n\n@layer base {\n    h1 {\n        letter-spacing: var(--tracking-tight);\n    }\n    h2 {\n        letter-spacing: var(--tracking-tighter);\n    }\n\n\n    :root[data-theme='light'] {\n      --background: 0 0% 100%;\n      --foreground: 222.2 84% 4.9%;\n\n      --muted: 210 40% 96.1%;\n      --muted-foreground: 215.4 16.3% 46.9%;\n\n      --popover: 0 0% 100%;\n      --popover-foreground: 222.2 84% 4.9%;\n\n      --card: 0 0% 100%;\n      --card-foreground: 222.2 84% 4.9%;\n\n      --border: 214.3 31.8% 91.4%;\n      --input: 214.3 31.8% 91.4%;\n\n      --primary: 222.2 47.4% 11.2%;\n      --primary-foreground: 210 40% 98%;\n\n      --secondary: 210 40% 96.1%;\n      --secondary-foreground: 222.2 47.4% 11.2%;\n\n      --accent: 210 40% 96.1%;\n      --accent-foreground: 222.2 47.4% 11.2%;\n\n      --destructive: 0 84.2% 60.2%;\n      --destructive-foreground: 210 40% 98%;\n\n      --ring: 215 20.2% 65.1%;\n\n      --radius: 0.5rem;\n\n      /** Chart Colors */\n      --chart-1: 12 76% 61%;\n      --chart-2: 173 58% 39%;\n      --chart-3: 197 37% 24%;\n      --chart-4: 43 74% 66%;\n      --chart-5: 27 87% 67%;\n\n      /* Status Tracker Colors - Radix Color */\n      --status-degraded: 50 100% 52%; /* Amber 10 */\n      --status-operational: 131 39% 51%; /* Grass 10 */\n      --status-down: 11 82% 59%; /* Tomato 10 */\n      --status-monitoring: 210 100% 62%; /* Blue 10 */\n    }\n\n    :root {\n      --background: 222.2 84% 4.9%;\n      --foreground: 210 40% 98%;\n\n      --muted: 217.2 32.6% 17.5%;\n      --muted-foreground: 215 20.2% 65.1%;\n\n      --popover: 222.2 84% 4.9%;\n      --popover-foreground: 210 40% 98%;\n\n      --card: 222.2 84% 4.9%;\n      --card-foreground: 210 40% 98%;\n\n      --border: 217.2 32.6% 17.5%;\n      --input: 217.2 32.6% 17.5%;\n\n      --primary: 210 40% 98%;\n      --primary-foreground: 222.2 47.4% 11.2%;\n\n      --secondary: 217.2 32.6% 17.5%;\n      --secondary-foreground: 210 40% 98%;\n\n      --accent: 217.2 32.6% 17.5%;\n      --accent-foreground: 210 40% 98%;\n\n      --destructive: 0 62.8% 30.6%;\n      --destructive-foreground: 0 85.7% 97.3%;\n\n      --ring: 217.2 32.6% 17.5%;\n\n      /* Chart Colors */\n      --chart-1: 220 70% 50%;\n      --chart-2: 160 60% 45%;\n      --chart-3: 30 80% 55%;\n      --chart-4: 280 65% 60%;\n      --chart-5: 340 75% 55%;\n\n      /* Status Tracker Colors - Radix Color */\n      --status-degraded: 50 100% 52%; /* Amber 10 */\n      --status-operational: 131 39% 51%; /* Grass 10 */\n      --status-down: 11 82% 59%; /* Tomato 10 */\n      --status-monitoring: 210 100% 62%; /* Blue 10 */\n\n    }\n  }\n\n/* https://ui.shadcn.com/colors */\n\n/* Dark mode colors. */\n:root {\n\t--sl-color-accent-low: #020817;\n\t--sl-color-accent: #f8fafc;\n\t--sl-color-accent-high: #f1f5f9;\n\t--sl-color-white: #f8fafc;\n\t--sl-color-gray-1: #f1f5f9;\n\t--sl-color-gray-2: #94a3b8;\n\t--sl-color-gray-3: #64748b;\n\t--sl-color-gray-4: #475569;\n\t--sl-color-gray-5: #1e293b;\n\t--sl-color-gray-6: #0f172a;\n\t--sl-color-black: #020817;\n}\n\n/* Light mode colors. */\n:root[data-theme='light'] {\n\t--sl-color-accent-low: #f8fafc;\n\t--sl-color-accent: #020817;\n\t--sl-color-accent-high: #0f172a;\n\t--sl-color-white: #020817;\n\t--sl-color-gray-1: #0f172a;\n\t--sl-color-gray-2: #1e293b;\n\t--sl-color-gray-3: #475569;\n\t--sl-color-gray-4: #64748b;\n\t--sl-color-gray-5: #94a3b8;\n\t--sl-color-gray-6: #f1f5f9;\n\t--sl-color-gray-7: #f8fafc;\n\t--sl-color-black: #ffffff;\n}\n"
  },
  {
    "path": "apps/docs/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"compilerOptions\": {\n    \"types\": [\"unplugin-icons/types/astro\"]\n  }\n}\n"
  },
  {
    "path": "apps/private-location/.air.toml",
    "content": "root = \".\"\ntestdata_dir = \"testdata\"\ntmp_dir = \"tmp\"\n\n[build]\n    args_bin = []\n    bin = \"./tmp/main\"\n    cmd = \"go build -o ./tmp/main ./cmd/server/main.go\"\n    delay = 1000\n    exclude_dir = [\"assets\", \"tmp\", \"vendor\", \"testdata\"]\n    exclude_file = []\n    exclude_regex = [\"_test.go\"]\n    exclude_unchanged = false\n    follow_symlink = false\n    full_bin = \"\"\n    include_dir = []\n    include_ext = [\"go\", \"tpl\", \"tmpl\", \"html\"]\n    include_file = []\n    kill_delay = \"0s\"\n    log = \"build-errors.log\"\n    poll = false\n    poll_interval = 0\n    post_cmd = []\n    pre_cmd = []\n    rerun = false\n    rerun_delay = 500\n    send_interrupt = false\n    stop_on_error = false\n\n\n[color]\n  app = \"\"\n  build = \"yellow\"\n  main = \"magenta\"\n  runner = \"green\"\n  watcher = \"cyan\"\n\n[log]\n  main_only = false\n  time = false\n\n[misc]\n  clean_on_exit = false\n\n[screen]\n  clear_on_rebuild = false\n  keep_scroll = true\n"
  },
  {
    "path": "apps/private-location/.dockerignore",
    "content": "# This file is generated by Dofigen v2.6.0\n# See https://github.com/lenra-io/dofigen\n\n"
  },
  {
    "path": "apps/private-location/.gitignore",
    "content": "/tmp\nmain\n"
  },
  {
    "path": "apps/private-location/.golangci.yml",
    "content": "version: \"2\"\n\nlinters:\n  default: fast\n"
  },
  {
    "path": "apps/private-location/Dockerfile",
    "content": "# syntax=docker/dockerfile:1.19.0\n# This file is generated by Dofigen v2.6.0\n# See https://github.com/lenra-io/dofigen\n\n# builder\nFROM golang@sha256:d4c4845f5d60c6a974c6000ce58ae079328d03ab7f721a0734277e69905473e5 AS builder\nARG TARGETARCH\nARG TARGETOS\nLABEL \\\n    org.opencontainers.image.base.digest=\"sha256:d4c4845f5d60c6a974c6000ce58ae079328d03ab7f721a0734277e69905473e5\" \\\n    org.opencontainers.image.base.name=\"docker.io/golang:1.26-alpine\" \\\n    org.opencontainers.image.stage=\"builder\"\nENV \\\n    TZ=\"UTC\" \\\n    CGO_ENABLED=\"0\"\nWORKDIR /go/src/app\nCOPY \\\n    --link \\\n    \".\" \".\"\nRUN <<EOF\napk add --no-cache tzdata\ngo mod download\nGOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags \"-s -w\" -o private-location ./cmd/server\nEOF\n\n# runtime\nFROM alpine@sha256:5405e8f36ce1878720f71217d664aa3dea32e5e5df11acbf07fc78ef5661465b AS runtime\nLABEL \\\n    io.dofigen.version=\"2.6.0\" \\\n    org.opencontainers.image.authors=\"OpenStatus Team\" \\\n    org.opencontainers.image.base.digest=\"sha256:5405e8f36ce1878720f71217d664aa3dea32e5e5df11acbf07fc78ef5661465b\" \\\n    org.opencontainers.image.base.name=\"docker.io/alpine:3.21\" \\\n    org.opencontainers.image.description=\"Private location orchestrator for OpenStatus\" \\\n    org.opencontainers.image.source=\"https://github.com/openstatusHQ/openstatus\" \\\n    org.opencontainers.image.title=\"OpenStatus Private Location\" \\\n    org.opencontainers.image.vendor=\"OpenStatus\"\nENV \\\n    GIN_MODE=\"release\" \\\n    TZ=\"UTC\" \\\n    USER=\"1000\"\nWORKDIR /opt/bin\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/etc/ssl/certs/ca-certificates.crt\" \"/etc/ssl/certs/\"\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/usr/share/zoneinfo\" \"/usr/share/zoneinfo\"\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/go/src/app/private-location\" \"/opt/bin/private-location\"\nUSER 1000:1000\nEXPOSE 8080\nHEALTHCHECK \\\n    --interval=15s \\\n    --timeout=10s \\\n    --start-period=30s \\\n    --retries=3 \\\n    CMD wget --spider -q http://localhost:8080/health || exit 1\nCMD [\"/opt/bin/private-location\"]\n"
  },
  {
    "path": "apps/private-location/README.md",
    "content": "# Private Location Orchestrator\n\n\nA server that allows private regions to register and ingest data from them.\n"
  },
  {
    "path": "apps/private-location/cmd/server/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/server\"\n)\n\nfunc gracefulShutdown(apiServer *http.Server, cleanup func(context.Context), done chan bool) {\n\t// Create context that listens for the interrupt signal from the OS.\n\tctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)\n\tdefer stop()\n\n\t// Listen for the interrupt signal.\n\t<-ctx.Done()\n\n\tfmt.Println(\"shutting down gracefully, press Ctrl+C again to force\")\n\tstop() // Allow Ctrl+C to force shutdown\n\n\t// The context is used to inform the server it has 5 seconds to finish\n\t// the request it is currently handling\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tif err := apiServer.Shutdown(ctx); err != nil {\n\t\tlog.Printf(\"Server forced to shutdown with error: %v\", err)\n\t}\n\n\t// Cleanup log provider\n\tcleanup(ctx)\n\n\tlog.Println(\"Server exiting\")\n\n\t// Notify the main goroutine that the shutdown is complete\n\tdone <- true\n}\n\nfunc main() {\n\n\tserver, cleanup := server.NewServer()\n\n\t// Create a done channel to signal when the shutdown is complete\n\tdone := make(chan bool, 1)\n\n\t// Run graceful shutdown in a separate goroutine\n\tgo gracefulShutdown(server, cleanup, done)\n\n\terr := server.ListenAndServe()\n\tif err != nil && err != http.ErrServerClosed {\n\t\tpanic(fmt.Sprintf(\"http server error: %s\", err))\n\t}\n\n\t// Wait for the graceful shutdown to complete\n\t<-done\n\tlog.Println(\"Graceful shutdown complete.\")\n}\n"
  },
  {
    "path": "apps/private-location/dofigen.yml",
    "content": "builders:\n  # Stage 1: Build Go binary\n  builder:\n    fromImage: golang:1.26-alpine\n    platform: $BUILDPLATFORM\n    label:\n      org.opencontainers.image.stage: builder\n    workdir: /go/src/app\n    # Build-time arguments (overwritten by .env.docker at runtime)\n    args:\n      TARGETOS: \"\"\n      TARGETARCH: \"\"\n    env:\n      TZ: UTC\n      CGO_ENABLED: \"0\"\n    copy:\n      # Copy source code\n      - . .\n    run:\n      - apk add --no-cache tzdata\n      - go mod download\n      # Build optimized binary\n      # -trimpath: Remove file system paths from binary\n      # -ldflags \"-s -w\": Strip debug info and symbol table\n      - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags \"-s -w\" -o private-location ./cmd/server\n\n# Runtime stage\nfromImage: alpine:3.21\n\n# Metadata labels\nlabel:\n  org.opencontainers.image.title: OpenStatus Private Location\n  org.opencontainers.image.description: Private location orchestrator for OpenStatus\n  org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus\n  org.opencontainers.image.vendor: OpenStatus\n  org.opencontainers.image.authors: OpenStatus Team\nworkdir: /opt/bin\n\n# Copy artifacts from builder\ncopy:\n  - fromBuilder: builder\n    source: /etc/ssl/certs/ca-certificates.crt\n    target: /etc/ssl/certs/\n  - fromBuilder: builder\n    source: /usr/share/zoneinfo\n    target: /usr/share/zoneinfo\n  - fromBuilder: builder\n    source: /go/src/app/private-location\n    target: /opt/bin/private-location\n\nenv:\n  TZ: UTC\n  USER: \"1000\"\n  GIN_MODE: release\n\n# Security: run as non-root user\nuser: \"1000:1000\"\n\n# Expose port\nexpose: \"8080\"\n\n# Health check\nhealthcheck:\n  interval: 15s\n  timeout: 10s\n  start: 30s\n  retries: 3\n  cmd: wget --spider -q http://localhost:8080/health || exit 1\n\n# Start application\ncmd:\n  - /opt/bin/private-location\n"
  },
  {
    "path": "apps/private-location/fly.toml",
    "content": "# fly.toml app configuration file generated for openstatus-checker on 2023-11-30T20:23:20+01:00\n#\n# See https://fly.io/docs/reference/configuration/ for information about how to use this file.\n#\n\napp = \"openstatus-private-location\"\nprimary_region = \"ams\"\n\n[build]\n  dockerfile = \"./Dockerfile\"\n\n[deploy]\n  strategy = \"canary\"\n\n\n[env]\n  PORT = \"8080\"\n\n[http_service]\n  internal_port = 8080\n  force_https = true\n  auto_stop_machines = \"off\"\n  auto_start_machines = false\n  processes = [\"app\"]\n\n[[vm]]\n  cpu_kind = \"shared\"\n  cpus = 1\n  memory_mb = 256\n\n\n[[http_service.checks]]\n  grace_period = \"10s\"\n  interval = \"15s\"\n  method = \"GET\"\n  timeout = \"5s\"\n  path = \"/health\"\n\n[http_service.concurrency]\n    type = \"requests\"\n    hard_limit = 1000\n    soft_limit = 500\n"
  },
  {
    "path": "apps/private-location/go.mod",
    "content": "module github.com/openstatushq/openstatus/apps/private-location\n\ngo 1.25.2\n\nrequire (\n\tconnectrpc.com/connect v1.19.1\n\tgithub.com/go-chi/chi/v5 v5.2.3\n\tgithub.com/go-chi/render v1.0.3\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/jmoiron/sqlx v1.4.0\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/mattn/go-sqlite3 v1.14.22\n\tgithub.com/openstatushq/openstatus/apps/checker v0.0.0-20251012205355-e366f661c23e\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d\n\tgo.opentelemetry.io/contrib/bridges/otelslog v0.14.0\n\tgo.opentelemetry.io/otel v1.39.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0\n\tgo.opentelemetry.io/otel/log v0.15.0\n\tgo.opentelemetry.io/otel/sdk v1.39.0\n\tgo.opentelemetry.io/otel/sdk/log v0.15.0\n\tgoogle.golang.org/protobuf v1.36.10\n)\n\nrequire (\n\tgithub.com/ajg/form v1.5.1 // indirect\n\tgithub.com/antlr4-go/antlr/v4 v4.13.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/coder/websocket v1.8.12 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rs/zerolog v1.34.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/otel/metric v1.39.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.39.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.9.0 // indirect\n\tgolang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect\n\tgolang.org/x/net v0.47.0 // indirect\n\tgolang.org/x/sys v0.39.0 // indirect\n\tgolang.org/x/text v0.31.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/grpc v1.77.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "apps/private-location/go.sum",
    "content": "connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=\nconnectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=\ngithub.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=\ngithub.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=\ngithub.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=\ngithub.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=\ngithub.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=\ngithub.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=\ngithub.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/openstatushq/openstatus/apps/checker v0.0.0-20251012205355-e366f661c23e h1:54C0zQNHzGszQseO2QcNzM8fL7vyAYk03pRtrJIyoV0=\ngithub.com/openstatushq/openstatus/apps/checker v0.0.0-20251012205355-e366f661c23e/go.mod h1:R84xAJYFys7XOZTDk/AyjJi4Ga9ovtLhJsfTLgTsYKg=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=\ngithub.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM=\ngo.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0=\ngo.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=\ngo.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE=\ngo.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ=\ngo.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=\ngo.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngo.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=\ngo.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=\ngolang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=\ngolang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=\ngolang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=\ngolang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=\ngolang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=\ngolang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=\ngoogle.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "apps/private-location/internal/database/database.go",
    "content": "package database\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t_ \"github.com/joho/godotenv/autoload\"\n\t_ \"github.com/tursodatabase/libsql-client-go/libsql\"\n)\n\nvar (\n\tdbUrl      = os.Getenv(\"DB_URL\")\n\tauthToken  = os.Getenv(\"DB_AUTH_TOKEN\")\n\tdbInstance *sqlx.DB\n)\n\n// New returns a database connection, reusing an existing connection if available.\nfunc New() *sqlx.DB {\n\t// Reuse Connection\n\tif dbInstance != nil {\n\t\treturn dbInstance\n\t}\n\n\turl := fmt.Sprintf(\"%s?auth_token=%s\", dbUrl, authToken)\n\tc, err := sql.Open(\"libsql\", url)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"failed to open db %s: %s\", url, err)\n\t\tos.Exit(1)\n\t}\n\n\tdb := sqlx.NewDb(c, \"sqlite3\")\n\tdbInstance = db\n\n\treturn db\n}\n\n// Close closes the database connection.\nfunc Close() error {\n\tif dbInstance != nil {\n\t\terr := dbInstance.Close()\n\t\tdbInstance = nil\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "apps/private-location/internal/database/models.go",
    "content": "package database\n\nimport \"database/sql\"\n\n// JobType represents the type of job for a monitor.\ntype JobType string\n\nconst (\n\tJobTypeTCP  JobType = \"tcp\"\n\tJobTypeUDP  JobType = \"udp\"\n\tJobTypeHTTP JobType = \"http\"\n\tJobTypeDNS  JobType = \"dns\"\n)\n\ntype Monitor struct {\n\tID              int            `db:\"id\"`\n\tActive          bool           `db:\"active\"`\n\tWorkspaceID     int            `db:\"workspace_id\"`\n\tJobType         JobType        `db:\"job_type\"`\n\tPeriodicity     string         `db:\"periodicity\"`\n\tURL             string         `db:\"url\"`\n\tHeaders         string         `db:\"headers\"`\n\tBody            string         `db:\"body\"`\n\tMethod          string         `db:\"method\"`\n\tTimeout         int64          `db:\"timeout\"`\n\tDegradedAfter   sql.NullInt64  `db:\"degraded_after\"`\n\tAssertions      sql.NullString `db:\"assertions\"`\n\tRetry           int            `db:\"retry\"`\n\tFollowRedirects bool           `db:\"follow_redirects\"`\n\tOtelEndpoint    sql.NullString `db:\"otel_endpoint\" json:\"-\"`\n\tOtelHeaders     sql.NullString `db:\"otel_headers\" json:\"-\"`\n\tName            string         `db:\"name\" json:\"-\"`\n\tExternalName    sql.NullString `db:\"external_name\" json:\"-\"`\n\tDescription     string         `db:\"description\" json:\"-\"`\n\tCreatedAt       int            `db:\"created_at\" json:\"-\"`\n\tUpdatedAt       int            `db:\"updated_at\" json:\"-\"`\n\tDeletedAt       sql.NullInt64  `db:\"deleted_at\" json:\"-\"`\n\tRegions         string         `db:\"regions\" json:\"-\"`\n\tStatus          string         `db:\"status\" json:\"-\"`\n\tPublic          bool           `db:\"public\" json:\"-\"`\n}\n\ntype PrivateLocation struct {\n\tID int `db:\"id\"`\n}\n"
  },
  {
    "path": "apps/private-location/internal/logs/logs.go",
    "content": "package logs\n\nimport (\n\t\"log/slog\"\n\t\"math/rand/v2\"\n\t\"time\"\n)\n\nfunc ShouldSample(event map[string]any) bool {\n\tstatusCode, _ := event[\"status_code\"].(int)\n\tdurationMs, _ := event[\"duration_ms\"].(int)\n\t// Always capture: server errors\n\tif statusCode >= 500 {\n\t\treturn true\n\t}\n\n\t// Always capture: explicit errors\n\tif _, hasError := event[\"error\"]; hasError {\n\t\treturn true\n\t}\n\n\t// Always capture: slow requests (above p99 - 2s threshold)\n\tif durationMs > 2000 {\n\t\treturn true\n\t}\n\n\t// Higher sampling for client errors (4xx) - 100%\n\tif statusCode >= 400 && statusCode < 500 {\n\t\treturn true\n\t}\n\n\t// Random sample successful, fast requests at 20%\n\treturn rand.Float64() < 0.2\n}\n\n// MapToAttrs converts a map[string]any to a slice of slog.Attr\nfunc MapToAttrs(m map[string]any) []slog.Attr {\n\tattrs := make([]slog.Attr, 0, len(m))\n\tfor k, v := range m {\n\t\tattrs = append(attrs, toAttr(k, v))\n\t}\n\treturn attrs\n}\n\nfunc toAttr(key string, value any) slog.Attr {\n\tswitch v := value.(type) {\n\tcase string:\n\t\treturn slog.String(key, v)\n\tcase int:\n\t\treturn slog.Int(key, v)\n\tcase int64:\n\t\treturn slog.Int64(key, v)\n\tcase float64:\n\t\treturn slog.Float64(key, v)\n\tcase bool:\n\t\treturn slog.Bool(key, v)\n\tcase time.Time:\n\t\treturn slog.Time(key, v)\n\tcase time.Duration:\n\t\treturn slog.Duration(key, v)\n\tcase map[string]any:\n\t\treturn slog.Group(key, MapToAny(v)...)\n\tdefault:\n\t\treturn slog.Any(key, v)\n\t}\n}\n\nfunc MapToAny(m map[string]any) []any {\n\targs := make([]any, 0, len(m)*2)\n\tfor k, v := range m {\n\t\targs = append(args, toAttr(k, v))\n\t}\n\treturn args\n}\n"
  },
  {
    "path": "apps/private-location/internal/logs/logs_test.go",
    "content": "package logs_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/logs\"\n)\n\nfunc TestShouldSample(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tevent    map[string]any\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"server error 500 should always sample\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 500,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"server error 503 should always sample\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 503,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"server error 599 should always sample\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 599,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"explicit error should always sample\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 200,\n\t\t\t\t\"error\":       \"something went wrong\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"slow request above 2000ms should always sample\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 200,\n\t\t\t\t\"duration_ms\": 2001,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"slow request exactly 2000ms should not always sample\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 200,\n\t\t\t\t\"duration_ms\": 2000,\n\t\t\t},\n\t\t\texpected: false, // This will be randomly sampled at 20%\n\t\t},\n\t\t{\n\t\t\tname: \"client error 400 should always sample\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 400,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"client error 404 should always sample\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 404,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"client error 499 should always sample\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 499,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"status code 399 should not always sample (below client error range)\",\n\t\t\tevent: map[string]any{\n\t\t\t\t\"status_code\": 399,\n\t\t\t},\n\t\t\texpected: false, // Random 20% sampling\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := logs.ShouldSample(tt.event)\n\t\t\tif tt.expected && !result {\n\t\t\t\tt.Errorf(\"ShouldSample() = %v, expected %v (should always sample)\", result, tt.expected)\n\t\t\t}\n\t\t\t// For cases where expected is false, we can't deterministically test\n\t\t\t// because the function uses random sampling. We just verify it doesn't\n\t\t\t// always return true.\n\t\t})\n\t}\n}\n\nfunc TestShouldSample_RandomSampling(t *testing.T) {\n\t// Test that successful, fast requests are sometimes sampled\n\tevent := map[string]any{\n\t\t\"status_code\": 200,\n\t\t\"duration_ms\": 100,\n\t}\n\n\t// Run multiple times to verify random sampling works\n\tsampledCount := 0\n\titerations := 1000\n\tfor i := 0; i < iterations; i++ {\n\t\tif logs.ShouldSample(event) {\n\t\t\tsampledCount++\n\t\t}\n\t}\n\n\t// With 20% sampling, we expect roughly 200 samples out of 1000\n\t// Allow for some variance (between 10% and 30%)\n\tminExpected := iterations / 10  // 10%\n\tmaxExpected := iterations * 3 / 10 // 30%\n\n\tif sampledCount < minExpected || sampledCount > maxExpected {\n\t\tt.Errorf(\"Random sampling seems off: got %d samples out of %d (expected roughly 20%%)\", sampledCount, iterations)\n\t}\n}\n\nfunc TestShouldSample_EmptyEvent(t *testing.T) {\n\t// Empty event should fall through to random sampling\n\tevent := map[string]any{}\n\n\t// Just verify it doesn't panic\n\t_ = logs.ShouldSample(event)\n}\n\nfunc TestShouldSample_MissingFields(t *testing.T) {\n\t// Event with no status_code or duration_ms\n\tevent := map[string]any{\n\t\t\"path\":   \"/api/test\",\n\t\t\"method\": \"GET\",\n\t}\n\n\t// Should fall through to random sampling without panic\n\t_ = logs.ShouldSample(event)\n}\n\nfunc TestMapToAttrs(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    map[string]any\n\t\texpected int // expected number of attributes\n\t}{\n\t\t{\n\t\t\tname:     \"empty map\",\n\t\t\tinput:    map[string]any{},\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"single string value\",\n\t\t\tinput: map[string]any{\n\t\t\t\t\"key\": \"value\",\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple types\",\n\t\t\tinput: map[string]any{\n\t\t\t\t\"string_key\": \"value\",\n\t\t\t\t\"int_key\":    42,\n\t\t\t\t\"bool_key\":   true,\n\t\t\t},\n\t\t\texpected: 3,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tattrs := logs.MapToAttrs(tt.input)\n\t\t\tif len(attrs) != tt.expected {\n\t\t\t\tt.Errorf(\"MapToAttrs() returned %d attrs, expected %d\", len(attrs), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMapToAttrs_TypeConversions(t *testing.T) {\n\tnow := time.Now()\n\tduration := 5 * time.Second\n\n\tinput := map[string]any{\n\t\t\"string_val\":   \"hello\",\n\t\t\"int_val\":      42,\n\t\t\"int64_val\":    int64(1234567890),\n\t\t\"float64_val\":  3.14,\n\t\t\"bool_val\":     true,\n\t\t\"time_val\":     now,\n\t\t\"duration_val\": duration,\n\t}\n\n\tattrs := logs.MapToAttrs(input)\n\n\t// Verify correct number of attributes\n\tif len(attrs) != 7 {\n\t\tt.Errorf(\"Expected 7 attributes, got %d\", len(attrs))\n\t}\n\n\t// Verify each attribute type\n\tattrMap := make(map[string]slog.Attr)\n\tfor _, attr := range attrs {\n\t\tattrMap[attr.Key] = attr\n\t}\n\n\t// Check string\n\tif attr, ok := attrMap[\"string_val\"]; ok {\n\t\tif attr.Value.Kind() != slog.KindString {\n\t\t\tt.Errorf(\"string_val should be String kind, got %v\", attr.Value.Kind())\n\t\t}\n\t\tif attr.Value.String() != \"hello\" {\n\t\t\tt.Errorf(\"string_val should be 'hello', got %v\", attr.Value.String())\n\t\t}\n\t}\n\n\t// Check int\n\tif attr, ok := attrMap[\"int_val\"]; ok {\n\t\tif attr.Value.Kind() != slog.KindInt64 {\n\t\t\tt.Errorf(\"int_val should be Int64 kind, got %v\", attr.Value.Kind())\n\t\t}\n\t\tif attr.Value.Int64() != 42 {\n\t\t\tt.Errorf(\"int_val should be 42, got %v\", attr.Value.Int64())\n\t\t}\n\t}\n\n\t// Check bool\n\tif attr, ok := attrMap[\"bool_val\"]; ok {\n\t\tif attr.Value.Kind() != slog.KindBool {\n\t\t\tt.Errorf(\"bool_val should be Bool kind, got %v\", attr.Value.Kind())\n\t\t}\n\t\tif attr.Value.Bool() != true {\n\t\t\tt.Errorf(\"bool_val should be true, got %v\", attr.Value.Bool())\n\t\t}\n\t}\n}\n\nfunc TestMapToAttrs_NestedMap(t *testing.T) {\n\tinput := map[string]any{\n\t\t\"outer\": map[string]any{\n\t\t\t\"inner_string\": \"nested_value\",\n\t\t\t\"inner_int\":    123,\n\t\t},\n\t}\n\n\tattrs := logs.MapToAttrs(input)\n\n\tif len(attrs) != 1 {\n\t\tt.Errorf(\"Expected 1 attribute (group), got %d\", len(attrs))\n\t}\n\n\t// The nested map should be converted to a Group\n\tif attrs[0].Key != \"outer\" {\n\t\tt.Errorf(\"Expected key 'outer', got %s\", attrs[0].Key)\n\t}\n\tif attrs[0].Value.Kind() != slog.KindGroup {\n\t\tt.Errorf(\"Expected Group kind for nested map, got %v\", attrs[0].Value.Kind())\n\t}\n}\n\nfunc TestMapToAttrs_UnknownType(t *testing.T) {\n\ttype customType struct {\n\t\tField string\n\t}\n\n\tinput := map[string]any{\n\t\t\"custom\": customType{Field: \"test\"},\n\t}\n\n\tattrs := logs.MapToAttrs(input)\n\n\tif len(attrs) != 1 {\n\t\tt.Errorf(\"Expected 1 attribute, got %d\", len(attrs))\n\t}\n\n\t// Unknown types should be converted using slog.Any\n\tif attrs[0].Key != \"custom\" {\n\t\tt.Errorf(\"Expected key 'custom', got %s\", attrs[0].Key)\n\t}\n\tif attrs[0].Value.Kind() != slog.KindAny {\n\t\tt.Errorf(\"Expected Any kind for unknown type, got %v\", attrs[0].Value.Kind())\n\t}\n}\n\nfunc TestMapToAny(t *testing.T) {\n\tinput := map[string]any{\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": 42,\n\t}\n\n\tresult := logs.MapToAny(input)\n\n\t// MapToAny returns []any containing slog.Attr values\n\tif len(result) != 2 {\n\t\tt.Errorf(\"Expected 2 items, got %d\", len(result))\n\t}\n\n\t// Verify each item is an slog.Attr\n\tfor _, item := range result {\n\t\tif _, ok := item.(slog.Attr); !ok {\n\t\t\tt.Errorf(\"Expected slog.Attr, got %T\", item)\n\t\t}\n\t}\n}\n\nfunc TestMapToAny_EmptyMap(t *testing.T) {\n\tinput := map[string]any{}\n\n\tresult := logs.MapToAny(input)\n\n\tif len(result) != 0 {\n\t\tt.Errorf(\"Expected empty slice, got %d items\", len(result))\n\t}\n}\n"
  },
  {
    "path": "apps/private-location/internal/models/assertions.go",
    "content": "package models\n\nimport \"encoding/json\"\n\ntype AssertionType string\n\nconst (\n\tAssertionHeader    AssertionType = \"header\"\n\tAssertionTextBody  AssertionType = \"textBody\"\n\tAssertionStatus    AssertionType = \"status\"\n\tAssertionJsonBody  AssertionType = \"jsonBody\"\n\tAssertionDnsRecord AssertionType = \"dnsRecord\"\n)\n\ntype StringComparator string\n\nfunc (c StringComparator) String() string {\n\treturn string(c)\n}\n\nfunc (c NumberComparator) String() string {\n\treturn string(c)\n}\n\nconst (\n\tStringContains         StringComparator = \"contains\"\n\tStringNotContains      StringComparator = \"not_contains\"\n\tStringEquals           StringComparator = \"eq\"\n\tStringNotEquals        StringComparator = \"not_eq\"\n\tStringEmpty            StringComparator = \"empty\"\n\tStringNotEmpty         StringComparator = \"not_empty\"\n\tStringGreaterThan      StringComparator = \"gt\"\n\tStringGreaterThanEqual StringComparator = \"gte\"\n\tStringLowerThan        StringComparator = \"lt\"\n\tStringLowerThanEqual   StringComparator = \"lte\"\n)\n\ntype NumberComparator string\n\nconst (\n\tNumberEquals           NumberComparator = \"eq\"\n\tNumberNotEquals        NumberComparator = \"not_eq\"\n\tNumberGreaterThan      NumberComparator = \"gt\"\n\tNumberGreaterThanEqual NumberComparator = \"gte\"\n\tNumberLowerThan        NumberComparator = \"lt\"\n\tNumberLowerThanEqual   NumberComparator = \"lte\"\n)\n\ntype Assertion struct {\n\tAssertionType AssertionType   `json:\"type\"`\n\tComparator    json.RawMessage `json:\"compare\"`\n\tRawTarget     json.RawMessage `json:\"target\"`\n}\n\ntype StatusTarget struct {\n\tAssertionType AssertionType    `json:\"type\"`\n\tComparator    NumberComparator `json:\"compare\"`\n\tTarget        int64            `json:\"target\"`\n}\n\ntype HeaderTarget struct {\n\tAssertionType AssertionType    `json:\"type\"`\n\tComparator    StringComparator `json:\"compare\"`\n\tTarget        string           `json:\"target\"`\n\tKey           string           `json:\"key\"`\n}\n\ntype StringTargetType struct {\n\tComparator StringComparator `json:\"compare\"`\n\tTarget     string           `json:\"target\"`\n}\n\ntype BodyString struct {\n\tAssertionType AssertionType    `json:\"type\"`\n\tComparator    StringComparator `json:\"compare\"`\n\tTarget        string           `json:\"target\"`\n}\n\ntype RecordComparator string\n\nconst (\n\tRecordEquals      RecordComparator = \"eq\"\n\tRecordNotEquals   RecordComparator = \"not_eq\"\n\tRecordContains    RecordComparator = \"contains\"\n\tRecordNotContains RecordComparator = \"not_contains\"\n)\n\ntype RecordTarget struct {\n\tAssertionType AssertionType    `json:\"type\"`\n\tComparator    RecordComparator `json:\"compare\"`\n\tTarget        string           `json:\"target\"`\n\tKey           string           `json:\"key\"`\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/db_testdata",
    "content": "DROP TABLE IF EXISTS \"__drizzle_migrations\";\nCREATE TABLE \"__drizzle_migrations\" (\n\t\t\tid SERIAL PRIMARY KEY,\n\t\t\thash text NOT NULL,\n\t\t\tcreated_at numeric\n\t\t);\n\nDROP TABLE IF EXISTS \"page\";\nCREATE TABLE `page` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`title` text NOT NULL,\n\t`description` text NOT NULL,\n\t`icon` text(256),\n\t`slug` text(256) NOT NULL,\n\t`custom_domain` text(256) NOT NULL,\n\t`published` integer DEFAULT false,\n\t\"created_at\" integer DEFAULT (strftime('%s', 'now')), `updated_at` integer, `password` text(256), `password_protected` integer DEFAULT false, `show_monitor_values` integer DEFAULT true, `force_theme` text DEFAULT 'system' NOT NULL, `legacy_page` integer DEFAULT true NOT NULL, `configuration` text, `homepage_url` text(256), `contact_url` text(256),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"monitors_to_pages\";\nCREATE TABLE `monitors_to_pages` (\n\t`monitor_id` integer NOT NULL,\n\t`page_id` integer NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')), `order` integer DEFAULT 0,\n\tPRIMARY KEY(`monitor_id`, `page_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"user\";\nCREATE TABLE `user` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`tenant_id` text(256),\n\t\"created_at\" integer DEFAULT (strftime('%s', 'now'))\n, `first_name` text DEFAULT '', `last_name` text DEFAULT '', `email` text DEFAULT '', `photo_url` text DEFAULT '', `updated_at` integer, `name` text, `emailVerified` integer);\n\nDROP TABLE IF EXISTS \"users_to_workspaces\";\nCREATE TABLE `users_to_workspaces` (\n\t`user_id` integer NOT NULL,\n\t`workspace_id` integer NOT NULL, `role` text DEFAULT 'owner' NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`user_id`, `workspace_id`),\n\tFOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n\nDROP TABLE IF EXISTS \"workspace\";\nCREATE TABLE `workspace` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`slug` text NOT NULL,\n\t`stripe_id` text(256),\n\t`name` text,\n\t\"created_at\" integer DEFAULT (strftime('%s', 'now'))\n, `updated_at` integer, `subscription_id` text, `plan` text(3), `ends_at` integer, `paid_until` integer, `dsn` text, `limits` text DEFAULT '{}' NOT NULL);\n\nDROP TABLE IF EXISTS \"status_report_update\";\nCREATE TABLE \"status_report_update\" (\n  `id` integer PRIMARY KEY NOT NULL,\n  \"status\" text NOT NULL,\n  `date` integer NOT NULL,\n  `message` text NOT NULL,\n  `created_at` integer DEFAULT (strftime('%s', 'now')),\n  `updated_at` integer DEFAULT (strftime('%s', 'now')),\n  \"status_report_id\" integer NOT NULL,\n  FOREIGN KEY (\"status_report_id\") REFERENCES \"status_report\"(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"status_report_to_monitors\";\nCREATE TABLE \"status_report_to_monitors\" (\n\t`monitor_id` integer NOT NULL,\n\t\"status_report_id\" integer NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(\"status_report_id\", `monitor_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (\"status_report_id\") REFERENCES \"status_report\"(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"monitor\";\nCREATE TABLE \"monitor\" (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`job_type` text(3) DEFAULT 'other' NOT NULL,\n\t`periodicity` text(6) DEFAULT 'other' NOT NULL,\n\t`active` integer DEFAULT false,\n\t`url` text(512) NOT NULL,\n\t`name` text(256) DEFAULT '' NOT NULL,\n\t`description` text DEFAULT '' NOT NULL,\n\t`workspace_id` integer,\n\t`headers` text DEFAULT '',\n\t`body` text DEFAULT '',\n\t`method` text(5) DEFAULT 'GET',\n\t`created_at` integer DEFAULT (strftime('%s', 'now')), `regions` text DEFAULT '' NOT NULL, `updated_at` integer, `status` text(2) DEFAULT 'active' NOT NULL, `assertions` text, `deleted_at` integer, `public` integer DEFAULT false, `timeout` integer DEFAULT 45000 NOT NULL, `degraded_after` integer, `otel_endpoint` text, `otel_headers` text, `retry` integer DEFAULT 3, `follow_redirects` integer DEFAULT true,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n\nDROP TABLE IF EXISTS \"integration\";\nCREATE TABLE `integration` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text(256) NOT NULL,\n\t`workspace_id` integer,\n\t`credential` text,\n\t`external_id` text NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\t`data` text NOT NULL,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n\nDROP TABLE IF EXISTS \"notification\";\nCREATE TABLE `notification` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`provider` text NOT NULL,\n\t`data` text DEFAULT '{}',\n\t`workspace_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n\nDROP TABLE IF EXISTS \"notifications_to_monitors\";\nCREATE TABLE `notifications_to_monitors` (\n\t`monitor_id` integer NOT NULL,\n\t`notification_id` integer NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`monitor_id`, `notification_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`notification_id`) REFERENCES `notification`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"status_reports_to_pages\";\nCREATE TABLE \"status_reports_to_pages\" (\n\t`page_id` integer NOT NULL,\n\t\"status_report_id\" integer NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(\"status_report_id\", `page_id`),\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (\"status_report_id\") REFERENCES \"status_report\"(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"monitor_status\";\nCREATE TABLE `monitor_status` (\n\t`monitor_id` integer NOT NULL,\n\t`region` text DEFAULT '' NOT NULL,\n\t`status` text DEFAULT 'active' NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`monitor_id`, `region`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"invitation\";\nCREATE TABLE `invitation` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`email` text NOT NULL,\n\t`role` text DEFAULT 'member' NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`token` text NOT NULL,\n\t`expires_at` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`accepted_at` integer\n);\n\nDROP TABLE IF EXISTS \"incident\";\nCREATE TABLE \"incident\" (\n    `id` integer PRIMARY KEY NOT NULL,\n\t`title` text DEFAULT '' NOT NULL,\n\t`summary` text DEFAULT '' NOT NULL,\n\t`status` text DEFAULT 'triage' NOT NULL,\n\t`monitor_id` integer,\n\t`workspace_id` integer,\n\t`started_at` integer DEFAULT (strftime('%s', 'now')) NOT NULL,\n\t`acknowledged_at` integer,\n\t`acknowledged_by` integer,\n\t`resolved_at` integer,\n\t`resolved_by` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')), `auto_resolved` integer DEFAULT false, `incident_screenshot_url` text, `recovery_screenshot_url` text,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE set default,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`acknowledged_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`resolved_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action\n);\n\nDROP TABLE IF EXISTS \"monitor_tag\";\nCREATE TABLE `monitor_tag` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`name` text NOT NULL,\n\t`color` text NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"monitor_tag_to_monitor\";\nCREATE TABLE `monitor_tag_to_monitor` (\n\t`monitor_id` integer NOT NULL,\n\t`monitor_tag_id` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`monitor_id`, `monitor_tag_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`monitor_tag_id`) REFERENCES `monitor_tag`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"account\";\nCREATE TABLE `account` (\n\t`user_id` integer NOT NULL,\n\t`type` text NOT NULL,\n\t`provider` text NOT NULL,\n\t`provider_account_id` text NOT NULL,\n\t`refresh_token` text,\n\t`access_token` text,\n\t`expires_at` integer,\n\t`token_type` text,\n\t`scope` text,\n\t`id_token` text,\n\t`session_state` text,\n\tPRIMARY KEY(`provider`, `provider_account_id`),\n\tFOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"session\";\nCREATE TABLE `session` (\n\t`session_token` text PRIMARY KEY NOT NULL,\n\t`user_id` integer NOT NULL,\n\t`expires` integer NOT NULL,\n\tFOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"verification_token\";\nCREATE TABLE `verification_token` (\n\t`identifier` text NOT NULL,\n\t`token` text NOT NULL,\n\t`expires` integer NOT NULL,\n\tPRIMARY KEY(`identifier`, `token`)\n);\n\nDROP TABLE IF EXISTS \"application\";\nCREATE TABLE `application` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text,\n\t`dsn` text,\n\t`workspace_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n\nDROP TABLE IF EXISTS \"maintenance_to_monitor\";\nCREATE TABLE `maintenance_to_monitor` (\n\t`monitor_id` integer NOT NULL,\n\t`maintenance_id` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`maintenance_id`, `monitor_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`maintenance_id`) REFERENCES `maintenance`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"check\";\nCREATE TABLE `check` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`regions` text DEFAULT '' NOT NULL,\n\t`url` text(4096) NOT NULL,\n\t`headers` text DEFAULT '',\n\t`body` text DEFAULT '',\n\t`method` text DEFAULT 'GET',\n\t`count_requests` integer DEFAULT 1,\n\t`workspace_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n\n\n\nDROP TABLE IF EXISTS \"monitor_run\";\nCREATE TABLE `monitor_run` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer,\n\t`monitor_id` integer,\n\t`runned_at` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE no action\n);\n\nDROP TABLE IF EXISTS \"page_subscriber\";\nCREATE TABLE \"page_subscriber\" (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`email` text NOT NULL,\n\t`page_id` integer NOT NULL,\n\t`token` text,\n\t`accepted_at` integer,\n\t`expires_at` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"status_report\";\nCREATE TABLE \"status_report\" (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`status` text NOT NULL,\n\t`title` text(256) NOT NULL,\n\t`workspace_id` integer,\n\t`page_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"notification_trigger\";\nCREATE TABLE \"notification_trigger\" (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`monitor_id` integer,\n\t`notification_id` integer,\n\t`cron_timestamp` integer NOT NULL,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`notification_id`) REFERENCES `notification`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"maintenance\";\nCREATE TABLE \"maintenance\" (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`title` text(256) NOT NULL,\n\t`message` text NOT NULL,\n\t`from` integer NOT NULL,\n\t`to` integer NOT NULL,\n\t`workspace_id` integer,\n\t`page_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\nDROP TABLE IF EXISTS \"private_location\";\nCREATE TABLE `private_location` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`token` text NOT NULL,\n\t`last_seen_at` integer,\n\t`workspace_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n\nDROP TABLE IF EXISTS \"private_location_to_monitor\";\nCREATE TABLE `private_location_to_monitor` (\n\t`private_location_id` integer,\n\t`monitor_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`deleted_at` integer,\n\tFOREIGN KEY (`private_location_id`) REFERENCES `private_location`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE no action\n);\n\n\nINSERT INTO \"__drizzle_migrations\" (\"id\", \"hash\", \"created_at\") VALUES\n(NULL, 'ea497587bb639bbeae27f3f644634b7429f37df241c999e22f3acbf3cce74ec9', '1690309905039'),\n(NULL, '680e79fc5537135bcfa4da88cc0c06a7a9eaf810ab87f8ae06ef67f7b4802fad', '1690892003254'),\n(NULL, 'b8ae7d5887e4cd8416a443f193753dabc6bc1ee76a6cbcfd4b338fbc6d3d10e7', '1691573899721'),\n(NULL, '790cf15e409b0c02f0e164824289aaacacb7aee21f5e84ab4f5a48df4b46e528', '1691614487733'),\n(NULL, 'ac5db9b31935382412791565e1c11ce1c99e69bd6f826da9ff08fb4ea64acc67', '1691850907670'),\n(NULL, '4cc5cc57b2e087276e6283cbd639e2ccd27c800eec6def5bb3ff7ea3ee3d9ba9', '1691930414569'),\n(NULL, 'cc99f49622240771a187a79a7c5646ed7f1e6e86b6eefcc04e9a6be78e57a5b0', '1692646649111'),\n(NULL, '49e497500b2fdb01624090863808ab15db1e50d4bc393c1e6e820960cb070d60', '1694362217174'),\n(NULL, 'a76eb5c9a12c6828bdef95e6ca65483055e7dabdf94f0b95fdae437c811b6b55', '1695756345957'),\n(NULL, '223292dcce0a81148dfbf7338ed4fe5878503d6bf785e1a13042b7c7b7b5bf24', '1697285841283'),\n(NULL, '5cc395568f9f61ebf4b57f69fd6397da2661062db813a85c5433d56841cecc9c', '1700586221141'),\n(NULL, '4ceefb80e855b8fde6c430758d9b557cef09c42afca4575c8fe4236628112d33', '1701100570578'),\n(NULL, '4c8d34ebf56874f41a1b22cb93db50e022233ff53f763eb73a73d2e01223fa39', '1701713135829'),\n(NULL, '4f47a6efc84f60e41437134c6eec5b2a3be68aae2a0525f2109b61f7473bb5ea', '1702144660818'),\n(NULL, '6f1b9eac8a3c7cf72703171402a9f42ea7523c091e393b3972fd231ab6eac6f9', '1702227904130'),\n(NULL, '4ae3b4c679065c4ca450126e14a73dbb3d21d71b7b1de2be182c32500b5c16ea', '1705856545397'),\n(NULL, '04e2ca02dbf77bce1755dc28e624d1620d662bec94373cea08983f4801fe778d', '1706111184826'),\n(NULL, 'd3c0f8670dd8e3666b3e0bd3ef1d75aec6b593c11d35e5a75bf58abe6208c642', '1707411900987'),\n(NULL, 'a028a05328ef4480fb5d79caed96da17b68793f7c9af037dbe8bb77ce42f5e36', '1707770189561'),\n(NULL, '826d9207a73609ff0f15acb2109fed80a253beb826836a93e122157584f9ca35', '1707899175705'),\n(NULL, '0055fd4b62b8b1c7029ed978cdd2f946084acae39966cb8cf85c26923a87d1cc', '1707905605592'),\n(NULL, 'af0035a9de14837b62dddba9e40bb85bf249a0c75683867be2a2d6f70f28da6d', '1710677383007'),\n(NULL, '498de5adf9c0ea5ed4c3d57b6a0905b709a58550e79c157909f13e1269896c5a', '1711307113089'),\n(NULL, 'ce825562d32c32c28da76e2d06b5e76f4c1603d178ce82ff97e1436819f3273c', '1712311348272'),\n(NULL, '45fc9d668e1a0d69701267444ca79da8e392180635761f24e41d9a7ac30a6d8c', '1712354121499'),\n(NULL, 'e9a11163fbaa7bdb2e2350265cc3e260bdb24ae1eb42d975fbf03cba7b3c8ce7', '1713095971713'),\n(NULL, '62f1cd998d6258df5c2a89222f5b71082d575b474f6d86b75fb4bf3aefe1d063', '1713384976187'),\n(NULL, '4c0d7cd418b66257e0a49454c54e8e65c388c5a246b3f5a51df0618da1faa67d', '1714586658374'),\n(NULL, 'd785a89fd7aa7bad6dbae4db4a3daaaa26c5f14374ed045cfd22420453cc4cea', '1715173356076'),\n(NULL, '9216b9d717de25f7b615f625659b7acfbbb09adacd3b121fa4f05ec0796fbd3c', '1716215342026'),\n(NULL, 'a9ced3ac1a3f9e8de9d8240ccdeed9d9e9c80b1bb6caefea59a7dbf7cf411fa7', '1716364430118'),\n(NULL, '9d0ae1d1e4bc742555e5ae5d990b62d23bb3dcefd7f62079df0bfa908f74133b', '1717837961923'),\n(NULL, '698cdffe2cad7d5b65190d09cb8f2b2ba906b1218fedb1a727fb72049090dfb6', '1718027484219'),\n(NULL, '6255470457dc29e4e1f7b8aa84f349a68b04ba3d51b9380006ddac53d5ce90a2', '1719740057514'),\n(NULL, 'bbf2f3122e76c5cbd3b76bc36e97d8f8efdddfab25c98c16f8024154e9bb2609', '1720727898360'),\n(NULL, '812b9def5df2ffe21b91680764590eda65e5ac4674500966d921d9e8627ca2f0', '1721159796428'),\n(NULL, '56181582f13b5eb66f2598a28fc7800ec6a0b9dd31d74c99cea1299ea7321917', '1723459608109'),\n(NULL, '82e57027c88612343af361f30edb148f2fca91919d6fa44048e992462d71163d', '1729533101998'),\n(NULL, '215e2ab2354eaa50923566a10a99e79f8d8514d7e3d4ca5c5720a9f5d16714ee', '1729579461221'),\n(NULL, 'fca63d63ad18ce6864418f1597b7bb0013ab2a3e0b4c681326ee4b257de2eb66', '1739193014150'),\n(NULL, '42d3cbf03d76541be2bdd9036e2f97864aa2807fe625f3b4c2b4f6440db60bd4', '1740684132626'),\n(NULL, '1a8b01776fbc88eaad155e2d0d2c69fc41d970411193ddd97806d70e1c66248b', '1741936835660'),\n(NULL, '427ca6a7f11dce1f4c0a20344d32af5f3f146a128adce1e87c7f76c80ab080fd', '1747410497521'),\n(NULL, '150ecc57f8a96341713848b1eec1293e713289b8b87854067d543fce3ff03849', '1747908803707'),\n(NULL, '8980cf55bf7b9b7f9a2cb00e5f21be8454260dd2826644b7683aaad65ff40df6', '1753730490635'),\n(NULL, 'bd5e4767fcfcd79179bb9ac9576b58a0c51c0a3af65fc0140e53f57c40e56a9c', '1756185045968'),\n(NULL, 'c7180363acc6879b749b2e9c34d3b9ac012f13aed0c62bf7ed2104e936f72085', '1757580216081'),\n(NULL, '0b90478b428afe479c6da99669ba33dd78ca2b8b5039ca2305bd9771213f4089', '1757840904190'),\n(NULL, 'b2c7b149f424a30adbf9dbe4c1db363e85d5dfe9aca3845969b14238c1117178', '1759865914553');\n\nINSERT INTO \"page\" (\"id\", \"workspace_id\", \"title\", \"description\", \"icon\", \"slug\", \"custom_domain\", \"published\", \"created_at\", \"updated_at\", \"password\", \"password_protected\", \"show_monitor_values\", \"force_theme\", \"legacy_page\", \"configuration\", \"homepage_url\", \"contact_url\") VALUES\n('1', '1', 'Test Page', 'hello', 'https://www.openstatus.dev/favicon.ico', 'status', '', '1', '1760358329', '1760358329', NULL, '0', '1', 'system', '1', NULL, NULL, NULL);\n\nINSERT INTO \"monitors_to_pages\" (\"monitor_id\", \"page_id\", \"created_at\", \"order\") VALUES\n('1', '1', '1760358329', '0');\n\nINSERT INTO \"user\" (\"id\", \"tenant_id\", \"created_at\", \"first_name\", \"last_name\", \"email\", \"photo_url\", \"updated_at\", \"name\", \"emailVerified\") VALUES\n('1', '1', '1760358329', 'Speed', 'Matters', 'ping@openstatus.dev', '', '1760358329', NULL, NULL);\n\nINSERT INTO \"users_to_workspaces\" (\"user_id\", \"workspace_id\", \"role\", \"created_at\") VALUES\n('1', '1', 'member', '1760358329');\n\nINSERT INTO \"workspace\" (\"id\", \"slug\", \"stripe_id\", \"name\", \"created_at\", \"updated_at\", \"subscription_id\", \"plan\", \"ends_at\", \"paid_until\", \"dsn\", \"limits\") VALUES\n('1', 'love-openstatus', 'stripeId1', 'test', '1760358329', '1760358329', 'subscriptionId', 'team', NULL, NULL, NULL, '{\"monitors\":50,\"synthetic-checks\":150000,\"periodicity\":[\"30s\",\"1m\",\"5m\",\"10m\",\"30m\",\"1h\"],\"multi-region\":true,\"max-regions\":35,\"data-retention\":\"24 months\",\"status-pages\":20,\"maintenance\":true,\"status-subscribers\":true,\"custom-domain\":true,\"password-protection\":true,\"white-label\":true,\"notifications\":true,\"sms\":true,\"pagerduty\":true,\"notification-channels\":50,\"members\":\"Unlimited\",\"audit-log\":true,\"regions\":[\"ams\",\"arn\",\"atl\",\"bog\",\"bom\",\"bos\",\"cdg\",\"den\",\"dfw\",\"ewr\",\"eze\",\"fra\",\"gdl\",\"gig\",\"gru\",\"hkg\",\"iad\",\"jnb\",\"lax\",\"lhr\",\"mad\",\"mia\",\"nrt\",\"ord\",\"otp\",\"phx\",\"qro\",\"scl\",\"sea\",\"sin\",\"sjc\",\"syd\",\"waw\",\"yul\",\"yyz\"]}'),\n('2', 'test2', 'stripeId2', 'test2', '1760358329', '1760358329', 'subscriptionId2', 'free', NULL, NULL, NULL, '{}'),\n('3', 'test3', 'stripeId3', 'test3', '1760358329', '1760358329', 'subscriptionId3', 'team', NULL, NULL, NULL, '{}');\n\nINSERT INTO \"status_report_update\" (\"id\", \"status\", \"date\", \"message\", \"created_at\", \"updated_at\", \"status_report_id\") VALUES\n('1', 'investigating', '1760358329', 'Message', '1760358329', '1760358329', '1'),\n('2', 'investigating', '1760358329', 'Message', '1760358329', '1760358329', '2'),\n('3', 'monitoring', '1760358329', 'test', '1760358329', '1760358329', '1');\n\nINSERT INTO \"status_report_to_monitors\" (\"monitor_id\", \"status_report_id\", \"created_at\") VALUES\n('1', '2', '1760358329'),\n('2', '2', '1760358329');\n\nINSERT INTO \"monitor\" (\"id\", \"job_type\", \"periodicity\", \"active\", \"url\", \"name\", \"description\", \"workspace_id\", \"headers\", \"body\", \"method\", \"created_at\", \"regions\", \"updated_at\", \"status\", \"assertions\", \"deleted_at\", \"public\", \"timeout\", \"degraded_after\", \"otel_endpoint\", \"otel_headers\", \"retry\", \"follow_redirects\") VALUES\n('1', 'http', '1m', '1', 'https://www.openstatus.dev', 'OpenStatus', 'OpenStatus website', '1', '[{\"key\":\"key\", \"value\":\"value\"}]', '{\"hello\":\"world\"}', 'POST', '1760358329', 'ams', '1760358329', 'active', NULL, NULL, '0', '45000', NULL, NULL, NULL, '3', '1'),\n('2', 'http', '10m', '0', 'https://www.google.com', '', '', '1', '', '', 'GET', '1760358329', 'gru', '1760358329', 'active', NULL, NULL, '1', '45000', NULL, NULL, NULL, '3', '1'),\n('3', 'http', '1m', '1', 'https://www.openstatus.dev', 'OpenStatus', 'OpenStatus website', '1', '[{\"key\":\"key\", \"value\":\"value\"}]', '{\"hello\":\"world\"}', 'GET', '1760358329', 'ams', '1760358329', 'active', NULL, NULL, '0', '45000', NULL, NULL, NULL, '3', '1'),\n('4', 'http', '10m', '1', 'https://www.google.com', '', '', '1', '', '', 'GET', '1760358329', 'gru', '1760358329', 'active', NULL, NULL, '1', '45000', NULL, 'https://otel.com:4337', '[{\"key\":\"Authorization\",\"value\":\"Basic\"}]', '3', '1'),\n('5', 'http', '10m', '1', 'https://openstat.us', '', '', '3', '', '', 'GET', '1760358329', 'ams', '1760358329', 'active', NULL, NULL, '1', '45000', NULL, NULL, NULL, '3', '1'),\n('6', 'tcp', '5m', '1', 'tcp://db.example.com:5432', 'Database TCP', 'Database TCP check', '3', '', '', '', '1760358329', 'ams', '1760358329', 'active', NULL, NULL, '0', '30000', '5000', NULL, NULL, '2', '0'),\n('7', 'dns', '5m', '1', 'openstatus.dev', 'DNS Check', 'DNS check for openstatus.dev', '3', '', '', '', '1760358329', 'ams', '1760358329', 'active', '[{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"A\",\"compare\":\"contains\",\"target\":\"76.76.21.21\"}]', NULL, '0', '30000', '3000', NULL, NULL, '2', '0');\n\nINSERT INTO \"notification\" (\"id\", \"name\", \"provider\", \"data\", \"workspace_id\", \"created_at\", \"updated_at\") VALUES\n('1', 'sample test notification', 'email', '{\"email\":\"ping@openstatus.dev\"}', '1', '1760358329', '1760358329');\n\nINSERT INTO \"notifications_to_monitors\" (\"monitor_id\", \"notification_id\", \"created_at\") VALUES\n('1', '1', '1760358329');\n\nINSERT INTO \"incident\" (\"id\", \"title\", \"summary\", \"status\", \"monitor_id\", \"workspace_id\", \"started_at\", \"acknowledged_at\", \"acknowledged_by\", \"resolved_at\", \"resolved_by\", \"created_at\", \"updated_at\", \"auto_resolved\", \"incident_screenshot_url\", \"recovery_screenshot_url\") VALUES\n('1', '', '', 'triage', '1', '1', '1760358329', NULL, NULL, NULL, NULL, '1760358329', '1760358329', '0', NULL, NULL),\n('2', '', '', 'triage', '1', '1', '1760358330', NULL, NULL, NULL, NULL, '1760358329', '1760358329', '0', NULL, NULL);\n\nINSERT INTO \"maintenance_to_monitor\" (\"monitor_id\", \"maintenance_id\", \"created_at\") VALUES\n('1', '1', '1760358329');\n\nINSERT INTO \"status_report\" (\"id\", \"status\", \"title\", \"workspace_id\", \"page_id\", \"created_at\", \"updated_at\") VALUES\n('1', 'monitoring', 'Test Status Report', '1', '1', '1760358329', '1760358329'),\n('2', 'investigating', 'Test Status Report', '1', '1', '1760358329', '1760358329');\n\nINSERT INTO \"maintenance\" (\"id\", \"title\", \"message\", \"from\", \"to\", \"workspace_id\", \"page_id\", \"created_at\", \"updated_at\") VALUES\n('1', 'Test Maintenance', 'Test message', '1760358329', '1760358330', '1', '1', '1760358329', '1760358329');\n\nINSERT INTO \"private_location\" (\"id\", \"name\", \"token\", \"last_seen_at\", \"workspace_id\", \"created_at\", \"updated_at\") VALUES\n('1', 'My Home', 'my-secret-key', NULL, '3', '1760358329', '1760358329');\n\nINSERT INTO \"private_location_to_monitor\" (\"private_location_id\", \"monitor_id\", \"created_at\", \"deleted_at\") VALUES\n('1', '5', '1760358329', NULL),\n('1', '6', '1760358329', NULL),\n('1', '7', '1760358329', NULL);\n"
  },
  {
    "path": "apps/private-location/internal/server/errors.go",
    "content": "package server\n\nimport \"errors\"\n\n// Common errors used across the server package\nvar (\n\tErrMissingToken         = errors.New(\"missing token\")\n\tErrMonitorNotFound      = errors.New(\"monitor not found\")\n\tErrPrivateLocationNotFound = errors.New(\"private location not found\")\n)\n"
  },
  {
    "path": "apps/private-location/internal/server/ingest_common.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/database\"\n)\n\n// ingestContext holds common data needed for ingestion\ntype ingestContext struct {\n\tMonitor  database.Monitor\n\tRegion   database.PrivateLocation\n}\n\n// getIngestContext retrieves monitor and private location data for ingestion\nfunc (h *privateLocationHandler) getIngestContext(ctx context.Context, token string, monitorID string) (*ingestContext, error) {\n\tvar monitor database.Monitor\n\terr := h.db.Get(&monitor, \"SELECT monitor.id, monitor.workspace_id, monitor.url, monitor.method, monitor.assertions FROM monitor JOIN private_location_to_monitor a ON monitor.id = a.monitor_id JOIN private_location b ON a.private_location_id = b.id WHERE b.token = ? AND monitor.deleted_at IS NULL and monitor.id = ?\", token, monitorID)\n\tif err != nil {\n\t\tif holder := GetEvent(ctx); holder != nil {\n\t\t\tholder.Event[\"error\"] = map[string]any{\n\t\t\t\t\"message\": err.Error(),\n\t\t\t\t\"source\":  \"database\",\n\t\t\t\t\"type\":    \"monitor_lookup\",\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tvar region database.PrivateLocation\n\terr = h.db.Get(&region, \"SELECT private_location.id FROM private_location join private_location_to_monitor a ON private_location.id = a.private_location_id WHERE a.monitor_id = ? AND private_location.token = ?\", monitor.ID, token)\n\tif err != nil {\n\t\tif holder := GetEvent(ctx); holder != nil {\n\t\t\tholder.Event[\"error\"] = map[string]any{\n\t\t\t\t\"message\": err.Error(),\n\t\t\t\t\"source\":  \"database\",\n\t\t\t\t\"type\":    \"private_location_lookup\",\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn &ingestContext{\n\t\tMonitor: monitor,\n\t\tRegion:  region,\n\t}, nil\n}\n\n// sendEventAndUpdateLastSeen sends the event to Tinybird and updates the last_seen_at timestamp\nfunc (h *privateLocationHandler) sendEventAndUpdateLastSeen(ctx context.Context, data any, dataSourceName string, regionID int) {\n\tstart := time.Now()\n\terr := h.TbClient.SendEvent(ctx, data, dataSourceName)\n\tduration := time.Since(start).Milliseconds()\n\n\t// Enrich wide event with Tinybird operation context\n\tif holder := GetEvent(ctx); holder != nil {\n\t\tholder.Event[\"tinybird\"] = map[string]any{\n\t\t\t\"datasource\":  dataSourceName,\n\t\t\t\"duration_ms\": duration,\n\t\t\t\"success\":     err == nil,\n\t\t}\n\t\tif err != nil {\n\t\t\tholder.Event[\"error\"] = map[string]any{\n\t\t\t\t\"message\": err.Error(),\n\t\t\t\t\"source\":  \"tinybird\",\n\t\t\t}\n\t\t}\n\t}\n\n\t_, dbErr := h.db.NamedExec(\"UPDATE private_location SET last_seen_at = :last_seen_at WHERE id = :id\", map[string]any{\n\t\t\"last_seen_at\": time.Now().Unix(),\n\t\t\"id\":           regionID,\n\t})\n\tif dbErr != nil {\n\t\tif holder := GetEvent(ctx); holder != nil {\n\t\t\tholder.Event[\"db_update_error\"] = map[string]any{\n\t\t\t\t\"message\": dbErr.Error(),\n\t\t\t\t\"type\":    \"last_seen_update\",\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/ingest_dns.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/tinybird\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\ntype DNSResponse struct {\n\tID            string              `json:\"id\"`\n\tTiming        string              `json:\"timing\"`\n\tErrorMessage  string              `json:\"errorMessage\"`\n\tRegion        string              `json:\"region\"`\n\tTrigger       string              `json:\"trigger\"`\n\tURI           string              `json:\"uri\"`\n\tRequestStatus string              `json:\"requestStatus,omitempty\"`\n\tRecords       map[string][]string `json:\"records\"`\n\n\tRequestId     int64 `json:\"requestId,omitempty\"`\n\tWorkspaceID   int64 `json:\"workspaceId\"`\n\tMonitorID     int64 `json:\"monitorId\"`\n\tTimestamp     int64 `json:\"timestamp\"`\n\tLatency       int64 `json:\"latency\"`\n\tCronTimestamp int64 `json:\"cronTimestamp\"`\n\n\tError uint8 `json:\"error\"`\n}\n\nfunc (h *privateLocationHandler) IngestDNS(ctx context.Context, req *connect.Request[private_locationv1.IngestDNSRequest]) (*connect.Response[private_locationv1.IngestDNSResponse], error) {\n\ttoken := req.Header().Get(\"openstatus-token\")\n\tif token == \"\" {\n\t\treturn nil, connect.NewError(connect.CodeUnauthenticated, ErrMissingToken)\n\t}\n\n\tif err := ValidateIngestDNSRequest(req.Msg); err != nil {\n\t\treturn nil, NewValidationError(err)\n\t}\n\n\tic, err := h.getIngestContext(ctx, token, req.Msg.Id)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\n\t// Enrich wide event with business context\n\tif holder := GetEvent(ctx); holder != nil {\n\t\tholder.Event[\"private_location\"] = map[string]any{\n\t\t\t\"monitor_id\":   req.Msg.MonitorId,\n\t\t\t\"workspace_id\": ic.Monitor.WorkspaceID,\n\t\t\t\"region_id\":    ic.Region.ID,\n\t\t\t\"datasource\":   tinybird.DatasourceDNS,\n\t\t}\n\t}\n\n\trecords := make(map[string][]string)\n\tfor _, record := range req.Msg.Records {\n\t\tr := []string{}\n\t\tfor _, value := range record.GetRecord() {\n\t\t\tr = append(r, value)\n\t\t}\n\t\trecords[record.String()] = r\n\t}\n\n\tdata := DNSResponse{\n\t\tID:            req.Msg.Id,\n\t\tWorkspaceID:   int64(ic.Monitor.WorkspaceID),\n\t\tTimestamp:     req.Msg.Timestamp,\n\t\tError:         uint8(req.Msg.Error),\n\t\tRegion:        strconv.Itoa(ic.Region.ID),\n\t\tMonitorID:     int64(ic.Monitor.ID),\n\t\tTiming:        req.Msg.Timing,\n\t\tLatency:       req.Msg.Latency,\n\t\tCronTimestamp: req.Msg.CronTimestamp,\n\t\tTrigger:       \"cron\",\n\t\tURI:           req.Msg.Uri,\n\t\tRequestStatus: req.Msg.RequestStatus,\n\t\tRecords:       records,\n\t}\n\n\th.sendEventAndUpdateLastSeen(ctx, data, tinybird.DatasourceDNS, ic.Region.ID)\n\n\treturn connect.NewResponse(&private_locationv1.IngestDNSResponse{}), nil\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/ingest_dns_test.go",
    "content": "package server_test\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/server\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/tinybird\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\nfunc TestIngestDNS_Unauthenticated(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestDNSRequest{})\n\t// No token header\n\tresp, err := h.IngestDNS(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for missing token, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeUnauthenticated {\n\t\tt.Errorf(\"expected unauthenticated code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestDNS_ValidationError_EmptyID(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestDNSRequest{\n\t\tId:        \"\",\n\t\tTimestamp: 1234567890,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestDNS(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for validation failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInvalidArgument {\n\t\tt.Errorf(\"expected invalid argument code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestDNS_ValidationError_InvalidTimestamp(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestDNSRequest{\n\t\tId:        \"dns-123\",\n\t\tTimestamp: 0, // Invalid - must be positive\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestDNS(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for validation failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInvalidArgument {\n\t\tt.Errorf(\"expected invalid argument code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestDNS_ValidationError_NegativeLatency(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestDNSRequest{\n\t\tId:        \"dns-123\",\n\t\tLatency:   -100,\n\t\tTimestamp: 1234567890,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestDNS(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for validation failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInvalidArgument {\n\t\tt.Errorf(\"expected invalid argument code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestDNS_DBError(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestDNSRequest{\n\t\tId:        \"nonexistent-monitor\",\n\t\tTimestamp: 1234567890,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"invalid-token\")\n\n\tresp, err := h.IngestDNS(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for db failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInternal {\n\t\tt.Errorf(\"expected internal code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestDNS_MonitorNotExist(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestDNSRequest{\n\t\tId:        \"nonexistent-monitor\",\n\t\tTimestamp: 1234567890,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestDNS(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for monitor not found, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInternal {\n\t\tt.Errorf(\"expected internal code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestDNS_MonitorExist(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestDNSRequest{\n\t\tId:            \"5\",\n\t\tTimestamp:     1234567890,\n\t\tLatency:       50,\n\t\tCronTimestamp: 1234567800,\n\t\tUri:           \"dns://example.com\",\n\t\tTiming:        \"50ms\",\n\t\tRecords:       nil,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestDNS(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif resp == nil {\n\t\tt.Errorf(\"expected not nil response, got nil\")\n\t}\n}\n\nfunc TestIngestDNS_WithRecords(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestDNSRequest{\n\t\tId:            \"5\",\n\t\tTimestamp:     1234567890,\n\t\tLatency:       50,\n\t\tCronTimestamp: 1234567800,\n\t\tUri:           \"dns://example.com\",\n\t\tTiming:        \"50ms\",\n\t\tRecords: map[string]*private_locationv1.Records{\n\t\t\t\"A\": {\n\t\t\t\tRecord: []string{\"192.168.1.1\", \"192.168.1.2\"},\n\t\t\t},\n\t\t\t\"AAAA\": {\n\t\t\t\tRecord: []string{\"::1\"},\n\t\t\t},\n\t\t},\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestDNS(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif resp == nil {\n\t\tt.Errorf(\"expected not nil response, got nil\")\n\t}\n}\n\nfunc TestIngestDNS_WithError(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestDNSRequest{\n\t\tId:            \"5\",\n\t\tTimestamp:     1234567890,\n\t\tLatency:       0,\n\t\tCronTimestamp: 1234567800,\n\t\tUri:           \"dns://example.com\",\n\t\tError:         1, // Error occurred\n\t\tMessage:       \"DNS resolution failed\",\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestDNS(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif resp == nil {\n\t\tt.Errorf(\"expected not nil response, got nil\")\n\t}\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/ingest_http.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/tinybird\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\ntype EventHolder struct {\n\tEvent map[string]any\n}\n\ntype PingData struct {\n\tID            string `json:\"id\"`\n\tWorkspaceID   string `json:\"workspaceId\"`\n\tMonitorID     string `json:\"monitorId\"`\n\tURL           string `json:\"url\"`\n\tMethod        string `json:\"method\"`\n\tRegion        string `json:\"region\"`\n\tMessage       string `json:\"message,omitempty\"`\n\tTiming        string `json:\"timing,omitempty\"`\n\tHeaders       string `json:\"headers,omitempty\"`\n\tAssertions    string `json:\"assertions\"`\n\tBody          string `json:\"body,omitempty\"`\n\tTrigger       string `json:\"trigger,omitempty\"`\n\tRequestStatus string `json:\"requestStatus,omitempty\"`\n\tLatency       int64  `json:\"latency\"`\n\tCronTimestamp int64  `json:\"cronTimestamp\"`\n\tTimestamp     int64  `json:\"timestamp\"`\n\tStatusCode    int    `json:\"statusCode,omitempty\"`\n\tError         uint8  `json:\"error\"`\n}\n\nfunc (h *privateLocationHandler) IngestHTTP(ctx context.Context, req *connect.Request[private_locationv1.IngestHTTPRequest]) (*connect.Response[private_locationv1.IngestHTTPResponse], error) {\n\ttoken := req.Header().Get(\"openstatus-token\")\n\tif token == \"\" {\n\t\treturn nil, connect.NewError(connect.CodeUnauthenticated, ErrMissingToken)\n\t}\n\n\tif err := ValidateIngestHTTPRequest(req.Msg); err != nil {\n\t\treturn nil, NewValidationError(err)\n\t}\n\n\tic, err := h.getIngestContext(ctx, token, req.Msg.MonitorId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\n\t// Enrich wide event with business context\n\tif holder := GetEvent(ctx); holder != nil {\n\t\tholder.Event[\"private_location\"] = map[string]any{\n\t\t\t\"monitor_id\":   req.Msg.MonitorId,\n\t\t\t\"workspace_id\": ic.Monitor.WorkspaceID,\n\t\t\t\"region_id\":    ic.Region.ID,\n\t\t\t\"datasource\":   tinybird.DatasourceHTTP,\n\t\t}\n\t}\n\n\tdata := PingData{\n\t\tID:            req.Msg.Id,\n\t\tLatency:       req.Msg.Latency,\n\t\tStatusCode:    int(req.Msg.StatusCode),\n\t\tMonitorID:     req.Msg.MonitorId,\n\t\tRegion:        strconv.Itoa(ic.Region.ID),\n\t\tWorkspaceID:   strconv.Itoa(ic.Monitor.WorkspaceID),\n\t\tTimestamp:     req.Msg.Timestamp,\n\t\tCronTimestamp: req.Msg.CronTimestamp,\n\t\tURL:           ic.Monitor.URL,\n\t\tMethod:        ic.Monitor.Method,\n\t\tTiming:        req.Msg.Timing,\n\t\tHeaders:       req.Msg.Headers,\n\t\tBody:          req.Msg.Body,\n\t\tTrigger:       \"cron\",\n\t\tRequestStatus: req.Msg.RequestStatus,\n\t\tAssertions:    ic.Monitor.Assertions.String,\n\t}\n\n\th.sendEventAndUpdateLastSeen(ctx, data, tinybird.DatasourceHTTP, ic.Region.ID)\n\n\treturn connect.NewResponse(&private_locationv1.IngestHTTPResponse{}), nil\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/ingest_http_test.go",
    "content": "package server_test\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"testing\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/jmoiron/sqlx\"\n\t_ \"github.com/mattn/go-sqlite3\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/server\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/tinybird\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\nfunc testDB() *sqlx.DB {\n\n\tf, err := os.CreateTemp(\"\", \"db\")\n\tif err != nil {\n\t\tlog.Fatalln(err)\n\t}\n\tdb, err := sqlx.Connect(\"sqlite3\", f.Name())\n\tif err != nil {\n\t\tlog.Fatalln(err)\n\t}\n\tdat, err := os.ReadFile(\"./db_testdata\")\n\tdb.MustExec(string(dat))\n\n\treturn db\n}\n\ntype interceptorHTTPClient struct {\n\tf func(req *http.Request) (*http.Response, error)\n}\n\nfunc (i *interceptorHTTPClient) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn i.f(req)\n}\n\nfunc (i *interceptorHTTPClient) GetHTTPClient() *http.Client {\n\treturn &http.Client{\n\t\tTransport: i,\n\t}\n}\n\nfunc getTBClient(ctx context.Context) tinybird.Client {\n\tinterceptor := &interceptorHTTPClient{\n\t\tf: func(req *http.Request) (*http.Response, error) {\n\t\t\treturn &http.Response{\n\t\t\t\tStatusCode: http.StatusAccepted,\n\t\t\t}, nil\n\t\t},\n\t}\n\n\tclient := tinybird.NewClient(interceptor.GetHTTPClient(), \"apiKey\")\n\treturn client\n}\n\nfunc TestIngestHTTP_Unauthenticated(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestHTTPRequest{})\n\t// No token header\n\tresp, err := h.IngestHTTP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for missing token, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeUnauthenticated {\n\t\tt.Errorf(\"expected unauthenticated code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestHTTP_DBError(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestHTTPRequest{})\n\treq.Header().Set(\"openstatus-token\", \"token123\")\n\treq.Msg.Id = \"monitor1\"\n\treq.Msg.MonitorId = \"nonexistent\"\n\treq.Msg.Timestamp = 1234567890\n\tresp, err := h.IngestHTTP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for db failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInternal {\n\t\tt.Errorf(\"expected internal code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestHTTP_MonitorNotExist(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestHTTPRequest{})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\treq.Msg.Id = \"monitor1\"\n\treq.Msg.MonitorId = \"nonexistent\"\n\treq.Msg.Timestamp = 1234567890\n\tresp, err := h.IngestHTTP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for db failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInternal {\n\t\tt.Errorf(\"expected internal code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestHTTP_MonitorExist(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestHTTPRequest{})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\treq.Msg.Id = \"monitor1\"\n\treq.Msg.MonitorId = \"5\"\n\treq.Msg.Timestamp = 1234567890\n\tresp, err := h.IngestHTTP(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\n\tif resp == nil {\n\t\tt.Errorf(\"expected not nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestHTTP_ValidationError_EmptyMonitorID(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestHTTPRequest{\n\t\tMonitorId: \"\",\n\t\tTimestamp: 1234567890,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestHTTP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for validation failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInvalidArgument {\n\t\tt.Errorf(\"expected invalid argument code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestHTTP_ValidationError_InvalidTimestamp(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestHTTPRequest{\n\t\tMonitorId: \"5\",\n\t\tTimestamp: 0,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestHTTP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for validation failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInvalidArgument {\n\t\tt.Errorf(\"expected invalid argument code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestHTTP_ValidationError_NegativeLatency(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestHTTPRequest{\n\t\tMonitorId: \"5\",\n\t\tLatency:   -100,\n\t\tTimestamp: 1234567890,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestHTTP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for validation failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInvalidArgument {\n\t\tt.Errorf(\"expected invalid argument code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestHTTP_WithFullData(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestHTTPRequest{\n\t\tId:            \"request-1\",\n\t\tMonitorId:     \"5\",\n\t\tTimestamp:     1234567890,\n\t\tLatency:       150,\n\t\tCronTimestamp: 1234567800,\n\t\tUrl:           \"https://example.com/api\",\n\t\tStatusCode:    200,\n\t\tTiming:        \"150ms\",\n\t\tBody:          `{\"status\": \"ok\"}`,\n\t\tHeaders:       `{\"Content-Type\": \"application/json\"}`,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestHTTP(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif resp == nil {\n\t\tt.Errorf(\"expected not nil response, got nil\")\n\t}\n}\n\nfunc TestIngestHTTP_WithError(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestHTTPRequest{\n\t\tId:            \"request-1\",\n\t\tMonitorId:     \"5\",\n\t\tTimestamp:     1234567890,\n\t\tLatency:       0,\n\t\tCronTimestamp: 1234567800,\n\t\tUrl:           \"https://example.com/api\",\n\t\tError:         1,\n\t\tMessage:       \"Connection timeout\",\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestHTTP(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif resp == nil {\n\t\tt.Errorf(\"expected not nil response, got nil\")\n\t}\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/ingest_tcp.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/tinybird\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\ntype TCPData struct {\n\tID            string `json:\"id\"`\n\tTiming        string `json:\"timing\"`\n\tErrorMessage  string `json:\"errorMessage\"`\n\tRegion        string `json:\"region\"`\n\tTrigger       string `json:\"trigger\"`\n\tURI           string `json:\"uri\"`\n\tRequestStatus string `json:\"requestStatus,omitempty\"`\n\n\tRequestId     int64 `json:\"requestId,omitempty\"`\n\tWorkspaceID   int64 `json:\"workspaceId\"`\n\tMonitorID     int64 `json:\"monitorId\"`\n\tTimestamp     int64 `json:\"timestamp\"`\n\tLatency       int64 `json:\"latency\"`\n\tCronTimestamp int64 `json:\"cronTimestamp\"`\n\n\tError uint8 `json:\"error\"`\n}\n\nfunc (h *privateLocationHandler) IngestTCP(ctx context.Context, req *connect.Request[private_locationv1.IngestTCPRequest]) (*connect.Response[private_locationv1.IngestTCPResponse], error) {\n\ttoken := req.Header().Get(\"openstatus-token\")\n\tif token == \"\" {\n\t\treturn nil, connect.NewError(connect.CodeUnauthenticated, ErrMissingToken)\n\t}\n\n\tif err := ValidateIngestTCPRequest(req.Msg); err != nil {\n\t\treturn nil, NewValidationError(err)\n\t}\n\n\tic, err := h.getIngestContext(ctx, token, req.Msg.Id)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\n\t// Enrich wide event with business context\n\tif holder := GetEvent(ctx); holder != nil {\n\t\tholder.Event[\"private_location\"] = map[string]any{\n\t\t\t\"monitor_id\":   req.Msg.MonitorId,\n\t\t\t\"workspace_id\": ic.Monitor.WorkspaceID,\n\t\t\t\"region_id\":    ic.Region.ID,\n\t\t\t\"datasource\":   tinybird.DatasourceTCP,\n\t\t}\n\t}\n\n\tdata := TCPData{\n\t\tID:            req.Msg.Id,\n\t\tWorkspaceID:   int64(ic.Monitor.WorkspaceID),\n\t\tTimestamp:     req.Msg.Timestamp,\n\t\tError:         uint8(req.Msg.Error),\n\t\tRegion:        strconv.Itoa(ic.Region.ID),\n\t\tMonitorID:     int64(ic.Monitor.ID),\n\t\tTiming:        req.Msg.Timing,\n\t\tLatency:       req.Msg.Latency,\n\t\tCronTimestamp: req.Msg.CronTimestamp,\n\t\tTrigger:       \"cron\",\n\t\tURI:           req.Msg.Uri,\n\t\tRequestStatus: req.Msg.RequestStatus,\n\t}\n\n\th.sendEventAndUpdateLastSeen(ctx, data, tinybird.DatasourceTCP, ic.Region.ID)\n\n\treturn connect.NewResponse(&private_locationv1.IngestTCPResponse{}), nil\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/ingest_tcp_test.go",
    "content": "package server_test\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"connectrpc.com/connect\"\n\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/server\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/tinybird\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\nfunc TestIngestTCP_Unauthenticated(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestTCPRequest{})\n\t// No token header\n\tresp, err := h.IngestTCP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for missing token, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeUnauthenticated {\n\t\tt.Errorf(\"expected unauthenticated code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestTCP_DBError(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestTCPRequest{})\n\treq.Header().Set(\"openstatus-token\", \"token123\")\n\treq.Msg.Id = \"monitor1\"\n\treq.Msg.Timestamp = 1234567890\n\tresp, err := h.IngestTCP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for db failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInternal {\n\t\tt.Errorf(\"expected internal code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestTCP_ValidationError_EmptyID(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestTCPRequest{\n\t\tId:        \"\",\n\t\tTimestamp: 1234567890,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestTCP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for validation failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInvalidArgument {\n\t\tt.Errorf(\"expected invalid argument code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestTCP_ValidationError_InvalidTimestamp(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestTCPRequest{\n\t\tId:        \"tcp-123\",\n\t\tTimestamp: 0,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestTCP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for validation failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInvalidArgument {\n\t\tt.Errorf(\"expected invalid argument code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestTCP_ValidationError_NegativeLatency(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestTCPRequest{\n\t\tId:        \"tcp-123\",\n\t\tLatency:   -100,\n\t\tTimestamp: 1234567890,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestTCP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for validation failure, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInvalidArgument {\n\t\tt.Errorf(\"expected invalid argument code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestTCP_MonitorNotExist(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, \"\"))\n\n\treq := connect.NewRequest(&private_locationv1.IngestTCPRequest{\n\t\tId:        \"nonexistent-monitor\",\n\t\tTimestamp: 1234567890,\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestTCP(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for monitor not found, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeInternal {\n\t\tt.Errorf(\"expected internal code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestIngestTCP_MonitorExist(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestTCPRequest{\n\t\tId:            \"5\",\n\t\tTimestamp:     1234567890,\n\t\tLatency:       50,\n\t\tCronTimestamp: 1234567800,\n\t\tUri:           \"tcp://example.com:8080\",\n\t\tTiming:        \"50ms\",\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestTCP(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif resp == nil {\n\t\tt.Errorf(\"expected not nil response, got nil\")\n\t}\n}\n\nfunc TestIngestTCP_WithError(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.IngestTCPRequest{\n\t\tId:            \"5\",\n\t\tTimestamp:     1234567890,\n\t\tLatency:       0,\n\t\tCronTimestamp: 1234567800,\n\t\tUri:           \"tcp://example.com:8080\",\n\t\tError:         1,\n\t\tMessage:       \"Connection refused\",\n\t})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.IngestTCP(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif resp == nil {\n\t\tt.Errorf(\"expected not nil response, got nil\")\n\t}\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/monitors.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"strconv\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/database\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/models\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\n// Converts models.NumberComparator to proto NumberComparator\nfunc convertNumberComparator(m models.NumberComparator) private_locationv1.NumberComparator {\n\tswitch m {\n\tcase models.NumberNotEquals:\n\t\treturn private_locationv1.NumberComparator_NUMBER_COMPARATOR_NOT_EQUAL\n\tcase models.NumberEquals:\n\t\treturn private_locationv1.NumberComparator_NUMBER_COMPARATOR_EQUAL\n\tcase models.NumberGreaterThan:\n\t\treturn private_locationv1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN\n\tcase models.NumberGreaterThanEqual:\n\t\treturn private_locationv1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\n\tcase models.NumberLowerThan:\n\t\treturn private_locationv1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN\n\tcase models.NumberLowerThanEqual:\n\t\treturn private_locationv1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\n\tdefault:\n\t\treturn private_locationv1.NumberComparator_NUMBER_COMPARATOR_UNSPECIFIED\n\t}\n}\n\n// Converts models.StringComparator to proto StringComparator\nfunc convertStringComparator(m models.StringComparator) private_locationv1.StringComparator {\n\tswitch m {\n\tcase models.StringNotEquals:\n\t\treturn private_locationv1.StringComparator_STRING_COMPARATOR_NOT_EQUAL\n\tcase models.StringEquals:\n\t\treturn private_locationv1.StringComparator_STRING_COMPARATOR_EQUAL\n\tcase models.StringContains:\n\t\treturn private_locationv1.StringComparator_STRING_COMPARATOR_CONTAINS\n\tcase models.StringNotContains:\n\t\treturn private_locationv1.StringComparator_STRING_COMPARATOR_NOT_CONTAINS\n\tcase models.StringEmpty:\n\t\treturn private_locationv1.StringComparator_STRING_COMPARATOR_EMPTY\n\tcase models.StringNotEmpty:\n\t\treturn private_locationv1.StringComparator_STRING_COMPARATOR_NOT_EMPTY\n\tdefault:\n\t\treturn private_locationv1.StringComparator_STRING_COMPARATOR_UNSPECIFIED\n\t}\n}\n\n// Converts models.RecordComparator to proto RecordComparator\nfunc convertRecordComparator(m models.RecordComparator) private_locationv1.RecordComparator {\n\tswitch m {\n\tcase models.RecordEquals:\n\t\treturn private_locationv1.RecordComparator_RECORD_COMPARATOR_EQUAL\n\tcase models.RecordNotEquals:\n\t\treturn private_locationv1.RecordComparator_RECORD_COMPARATOR_NOT_EQUAL\n\tcase models.RecordContains:\n\t\treturn private_locationv1.RecordComparator_RECORD_COMPARATOR_CONTAINS\n\tcase models.RecordNotContains:\n\t\treturn private_locationv1.RecordComparator_RECORD_COMPARATOR_NOT_CONTAINS\n\tdefault:\n\t\treturn private_locationv1.RecordComparator_RECORD_COMPARATOR_UNSPECIFIED\n\t}\n}\n\n// addParseError records a parsing error in the wide event context\nfunc addParseError(ctx context.Context, errorType string, err error) {\n\tif holder := GetEvent(ctx); holder != nil {\n\t\terrors, ok := holder.Event[\"parse_errors\"].([]map[string]any)\n\t\tif !ok {\n\t\t\terrors = []map[string]any{}\n\t\t}\n\t\terrors = append(errors, map[string]any{\n\t\t\t\"type\":    errorType,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\tholder.Event[\"parse_errors\"] = errors\n\t}\n}\n\n// Helper to parse assertions\nfunc ParseAssertions(ctx context.Context, assertions sql.NullString) (\n\tstatusAssertions []*private_locationv1.StatusCodeAssertion,\n\theaderAssertions []*private_locationv1.HeaderAssertion,\n\tbodyAssertions []*private_locationv1.BodyAssertion,\n) {\n\tif !assertions.Valid {\n\t\treturn\n\t}\n\tvar rawAssertions []json.RawMessage\n\tif err := json.Unmarshal([]byte(assertions.String), &rawAssertions); err != nil {\n\t\taddParseError(ctx, \"assertions_unmarshal\", err)\n\t\treturn\n\t}\n\tfor _, a := range rawAssertions {\n\t\tvar assert models.Assertion\n\t\tif err := json.Unmarshal(a, &assert); err != nil {\n\t\t\taddParseError(ctx, \"assertion_unmarshal\", err)\n\t\t\tcontinue\n\t\t}\n\t\tswitch assert.AssertionType {\n\t\tcase models.AssertionStatus:\n\t\t\tvar target models.StatusTarget\n\t\t\tif err := json.Unmarshal(a, &target); err != nil {\n\t\t\t\taddParseError(ctx, \"status_target_unmarshal\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tstatusAssertions = append(statusAssertions, &private_locationv1.StatusCodeAssertion{\n\t\t\t\tTarget:     target.Target,\n\t\t\t\tComparator: convertNumberComparator(target.Comparator),\n\t\t\t})\n\t\tcase models.AssertionHeader:\n\t\t\tvar target models.HeaderTarget\n\t\t\tif err := json.Unmarshal(a, &target); err != nil {\n\t\t\t\taddParseError(ctx, \"header_target_unmarshal\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\theaderAssertions = append(headerAssertions, &private_locationv1.HeaderAssertion{\n\t\t\t\tKey:        target.Key,\n\t\t\t\tTarget:     target.Target,\n\t\t\t\tComparator: convertStringComparator(target.Comparator),\n\t\t\t})\n\t\tcase models.AssertionTextBody:\n\t\t\tvar target models.BodyString\n\t\t\tif err := json.Unmarshal(a, &target); err != nil {\n\t\t\t\taddParseError(ctx, \"body_target_unmarshal\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbodyAssertions = append(bodyAssertions, &private_locationv1.BodyAssertion{\n\t\t\t\tTarget:     target.Target,\n\t\t\t\tComparator: convertStringComparator(target.Comparator),\n\t\t\t})\n\t\t}\n\t}\n\treturn\n}\n\n// Helper to parse DNS record assertions\nfunc ParseRecordAssertions(ctx context.Context, assertions sql.NullString) []*private_locationv1.RecordAssertion {\n\tif !assertions.Valid {\n\t\treturn nil\n\t}\n\tvar rawAssertions []json.RawMessage\n\tif err := json.Unmarshal([]byte(assertions.String), &rawAssertions); err != nil {\n\t\taddParseError(ctx, \"record_assertions_unmarshal\", err)\n\t\treturn nil\n\t}\n\tvar recordAssertions []*private_locationv1.RecordAssertion\n\tfor _, a := range rawAssertions {\n\t\tvar assert models.Assertion\n\t\tif err := json.Unmarshal(a, &assert); err != nil {\n\t\t\taddParseError(ctx, \"record_assertion_unmarshal\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif assert.AssertionType == models.AssertionDnsRecord {\n\t\t\tvar target models.RecordTarget\n\t\t\tif err := json.Unmarshal(a, &target); err != nil {\n\t\t\t\taddParseError(ctx, \"record_target_unmarshal\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trecordAssertions = append(recordAssertions, &private_locationv1.RecordAssertion{\n\t\t\t\tRecord:     target.Key,\n\t\t\t\tComparator: convertRecordComparator(target.Comparator),\n\t\t\t\tTarget:     target.Target,\n\t\t\t})\n\t\t}\n\t}\n\treturn recordAssertions\n}\n\nfunc (h *privateLocationHandler) Monitors(ctx context.Context, req *connect.Request[private_locationv1.MonitorsRequest]) (*connect.Response[private_locationv1.MonitorsResponse], error) {\n\ttoken := req.Header().Get(\"openstatus-token\")\n\tif token == \"\" {\n\t\treturn nil, connect.NewError(connect.CodeUnauthenticated, ErrMissingToken)\n\t}\n\n\tvar monitors []database.Monitor\n\terr := h.db.Select(&monitors, \"SELECT monitor.id, monitor.job_type, monitor.url, monitor.periodicity, monitor.method, monitor.body, monitor.timeout, monitor.degraded_after, monitor.follow_redirects, monitor.headers, monitor.assertions, monitor.workspace_id, monitor.retry FROM monitor JOIN private_location_to_monitor a ON monitor.id = a.monitor_id JOIN private_location b ON a.private_location_id = b.id WHERE b.token = ? AND monitor.deleted_at IS NULL and monitor.active = 1\", token)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar workspaceId int\n\tvar httpMonitors []*private_locationv1.HTTPMonitor\n\tvar tcpMonitors []*private_locationv1.TCPMonitor\n\tvar dnsMonitors []*private_locationv1.DNSMonitor\n\tfor _, monitor := range monitors {\n\t\tif workspaceId == 0 {\n\t\t\tworkspaceId = monitor.WorkspaceID\n\t\t}\n\n\t\tswitch monitor.JobType {\n\t\tcase database.JobTypeHTTP:\n\t\t\tvar headers []*private_locationv1.Headers\n\t\t\tif err := json.Unmarshal([]byte(monitor.Headers), &headers); err != nil {\n\t\t\t\taddParseError(ctx, \"headers_unmarshal\", err)\n\t\t\t\theaders = nil\n\t\t\t}\n\n\t\t\tstatusAssertions, headerAssertions, bodyAssertions := ParseAssertions(ctx, monitor.Assertions)\n\n\t\t\thttpMonitors = append(httpMonitors, &private_locationv1.HTTPMonitor{\n\t\t\t\tUrl:                  monitor.URL,\n\t\t\t\tPeriodicity:          monitor.Periodicity,\n\t\t\t\tId:                   strconv.Itoa(monitor.ID),\n\t\t\t\tMethod:               monitor.Method,\n\t\t\t\tBody:                 monitor.Body,\n\t\t\t\tTimeout:              monitor.Timeout,\n\t\t\t\tDegradedAt:           &monitor.DegradedAfter.Int64,\n\t\t\t\tRetry:                int64(monitor.Retry),\n\t\t\t\tFollowRedirects:      monitor.FollowRedirects,\n\t\t\t\tHeaders:              headers,\n\t\t\t\tStatusCodeAssertions: statusAssertions,\n\t\t\t\tHeaderAssertions:     headerAssertions,\n\t\t\t\tBodyAssertions:       bodyAssertions,\n\t\t\t})\n\n\t\tcase database.JobTypeTCP:\n\t\t\ttcpMonitors = append(tcpMonitors, &private_locationv1.TCPMonitor{\n\t\t\t\tId:          strconv.Itoa(monitor.ID),\n\t\t\t\tUri:         monitor.URL,\n\t\t\t\tTimeout:     monitor.Timeout,\n\t\t\t\tDegradedAt:  &monitor.DegradedAfter.Int64,\n\t\t\t\tPeriodicity: monitor.Periodicity,\n\t\t\t\tRetry:       int64(monitor.Retry),\n\t\t\t})\n\n\t\tcase database.JobTypeDNS:\n\t\t\trecordAssertions := ParseRecordAssertions(ctx, monitor.Assertions)\n\t\t\tdnsMonitors = append(dnsMonitors, &private_locationv1.DNSMonitor{\n\t\t\t\tId:               strconv.Itoa(monitor.ID),\n\t\t\t\tUri:              monitor.URL,\n\t\t\t\tTimeout:          monitor.Timeout,\n\t\t\t\tDegradedAt:       &monitor.DegradedAfter.Int64,\n\t\t\t\tPeriodicity:      monitor.Periodicity,\n\t\t\t\tRetry:            int64(monitor.Retry),\n\t\t\t\tRecordAssertions: recordAssertions,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Enrich wide event with monitor counts\n\tif holder := GetEvent(ctx); holder != nil {\n\t\tholder.Event[\"private_location\"] = map[string]any{\n\t\t\t\"workspace_id\":   workspaceId,\n\t\t\t\"http_monitors\":  len(httpMonitors),\n\t\t\t\"tcp_monitors\":   len(tcpMonitors),\n\t\t\t\"dns_monitors\":   len(dnsMonitors),\n\t\t\t\"total_monitors\": len(monitors),\n\t\t}\n\t}\n\n\treturn connect.NewResponse(&private_locationv1.MonitorsResponse{\n\t\tHttpMonitors: httpMonitors,\n\t\tTcpMonitors:  tcpMonitors,\n\t\tDnsMonitors:  dnsMonitors,\n\t}), nil\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/monitors_test.go",
    "content": "package server_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"testing\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/server\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\nfunc TestParseAssertions_TextBodyContains(t *testing.T) {\n\t// Input JSON for the test\n\tinput := `[{\"version\":\"v1\",\"type\":\"textBody\",\"compare\":\"contains\",\"target\":\"mydata\"}]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\t_, _, bodyAssertions := server.ParseAssertions(context.Background(), assertions)\n\n\tif len(bodyAssertions) != 1 {\n\t\tt.Fatalf(\"expected 1 body assertion, got %d\", len(bodyAssertions))\n\t}\n\n\tgot := bodyAssertions[0]\n\tif got.Target != \"mydata\" {\n\t\tt.Errorf(\"expected Target to be 'mydata', got '%s'\", got.Target)\n\t}\n\n\tif got.Comparator != private_locationv1.StringComparator_STRING_COMPARATOR_CONTAINS {\n\t\tt.Errorf(\"expected Comparator to be STRING_COMPARATOR_CONTAINS, got %v\", got.Comparator)\n\t}\n}\n\nfunc TestParseAssertions_HttpStatusEquals(t *testing.T) {\n\t// Input JSON for the test\n\tinput := `[{\"version\":\"v1\",\"type\":\"status\",\"compare\":\"eq\",\"target\":200}]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\tstatusAssertion, _, _ := server.ParseAssertions(context.Background(), assertions)\n\n\tif len(statusAssertion) != 1 {\n\t\tt.Fatalf(\"expected 1 body assertion, got %d\", len(statusAssertion))\n\t}\n\n\tgot := statusAssertion[0]\n\tif got.Target != 200 {\n\t\tt.Errorf(\"expected Target to be 'mydata', got '%d'\", got.Target)\n\t}\n\n\tif got.Comparator != private_locationv1.NumberComparator_NUMBER_COMPARATOR_EQUAL {\n\t\tt.Errorf(\"expected Comparator to be STRING_COMPARATOR_CONTAINS, got %v\", got.Comparator)\n\t}\n}\n\nfunc TestParseAssertions_InvalidJSON(t *testing.T) {\n\tassertions := sql.NullString{\n\t\tString: \"not valid json\",\n\t\tValid:  true,\n\t}\n\n\tstatusAssertions, headerAssertions, bodyAssertions := server.ParseAssertions(context.Background(), assertions)\n\n\tif len(statusAssertions) != 0 || len(headerAssertions) != 0 || len(bodyAssertions) != 0 {\n\t\tt.Errorf(\"expected empty assertions for invalid JSON, got status=%d, header=%d, body=%d\",\n\t\t\tlen(statusAssertions), len(headerAssertions), len(bodyAssertions))\n\t}\n}\n\nfunc TestParseAssertions_NullString(t *testing.T) {\n\tassertions := sql.NullString{\n\t\tString: \"\",\n\t\tValid:  false,\n\t}\n\n\tstatusAssertions, headerAssertions, bodyAssertions := server.ParseAssertions(context.Background(), assertions)\n\n\tif len(statusAssertions) != 0 || len(headerAssertions) != 0 || len(bodyAssertions) != 0 {\n\t\tt.Errorf(\"expected empty assertions for null string, got status=%d, header=%d, body=%d\",\n\t\t\tlen(statusAssertions), len(headerAssertions), len(bodyAssertions))\n\t}\n}\n\nfunc TestParseAssertions_HeaderAssertion(t *testing.T) {\n\tinput := `[{\"version\":\"v1\",\"type\":\"header\",\"compare\":\"eq\",\"key\":\"Content-Type\",\"target\":\"application/json\"}]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\t_, headerAssertions, _ := server.ParseAssertions(context.Background(), assertions)\n\n\tif len(headerAssertions) != 1 {\n\t\tt.Fatalf(\"expected 1 header assertion, got %d\", len(headerAssertions))\n\t}\n\n\tgot := headerAssertions[0]\n\tif got.Key != \"Content-Type\" {\n\t\tt.Errorf(\"expected Key to be 'Content-Type', got '%s'\", got.Key)\n\t}\n\tif got.Target != \"application/json\" {\n\t\tt.Errorf(\"expected Target to be 'application/json', got '%s'\", got.Target)\n\t}\n\tif got.Comparator != private_locationv1.StringComparator_STRING_COMPARATOR_EQUAL {\n\t\tt.Errorf(\"expected Comparator to be STRING_COMPARATOR_EQUAL, got %v\", got.Comparator)\n\t}\n}\n\nfunc TestParseAssertions_MultipleAssertions(t *testing.T) {\n\tinput := `[\n\t\t{\"version\":\"v1\",\"type\":\"status\",\"compare\":\"eq\",\"target\":200},\n\t\t{\"version\":\"v1\",\"type\":\"header\",\"compare\":\"contains\",\"key\":\"X-Request-Id\",\"target\":\"req-\"},\n\t\t{\"version\":\"v1\",\"type\":\"textBody\",\"compare\":\"notContains\",\"target\":\"error\"}\n\t]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\tstatusAssertions, headerAssertions, bodyAssertions := server.ParseAssertions(context.Background(), assertions)\n\n\tif len(statusAssertions) != 1 {\n\t\tt.Errorf(\"expected 1 status assertion, got %d\", len(statusAssertions))\n\t}\n\tif len(headerAssertions) != 1 {\n\t\tt.Errorf(\"expected 1 header assertion, got %d\", len(headerAssertions))\n\t}\n\tif len(bodyAssertions) != 1 {\n\t\tt.Errorf(\"expected 1 body assertion, got %d\", len(bodyAssertions))\n\t}\n}\n\nfunc TestMonitors_Unauthenticated(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.MonitorsRequest{})\n\t// No token header\n\tresp, err := h.Monitors(context.Background(), req)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error for missing token, got nil\")\n\t}\n\tif connect.CodeOf(err) != connect.CodeUnauthenticated {\n\t\tt.Errorf(\"expected unauthenticated code, got %v\", connect.CodeOf(err))\n\t}\n\tif resp != nil {\n\t\tt.Errorf(\"expected nil response, got %v\", resp)\n\t}\n}\n\nfunc TestMonitors_InvalidToken(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.MonitorsRequest{})\n\treq.Header().Set(\"openstatus-token\", \"invalid-token\")\n\n\tresp, err := h.Monitors(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error for invalid token (just empty results), got %v\", err)\n\t}\n\tif resp == nil {\n\t\tt.Fatalf(\"expected non-nil response\")\n\t}\n\tif len(resp.Msg.HttpMonitors) != 0 {\n\t\tt.Errorf(\"expected 0 HTTP monitors for invalid token, got %d\", len(resp.Msg.HttpMonitors))\n\t}\n\tif len(resp.Msg.TcpMonitors) != 0 {\n\t\tt.Errorf(\"expected 0 TCP monitors for invalid token, got %d\", len(resp.Msg.TcpMonitors))\n\t}\n\tif len(resp.Msg.DnsMonitors) != 0 {\n\t\tt.Errorf(\"expected 0 DNS monitors for invalid token, got %d\", len(resp.Msg.DnsMonitors))\n\t}\n}\n\nfunc TestMonitors_ReturnsHTTPTCPAndDNSMonitors(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.MonitorsRequest{})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.Monitors(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\tif resp == nil {\n\t\tt.Fatalf(\"expected non-nil response\")\n\t}\n\n\t// Should have HTTP monitor (monitor ID 5)\n\tif len(resp.Msg.HttpMonitors) != 1 {\n\t\tt.Errorf(\"expected 1 HTTP monitor, got %d\", len(resp.Msg.HttpMonitors))\n\t}\n\n\t// Should have TCP monitor (monitor ID 6)\n\tif len(resp.Msg.TcpMonitors) != 1 {\n\t\tt.Errorf(\"expected 1 TCP monitor, got %d\", len(resp.Msg.TcpMonitors))\n\t}\n\n\t// Should have DNS monitor (monitor ID 7)\n\tif len(resp.Msg.DnsMonitors) != 1 {\n\t\tt.Errorf(\"expected 1 DNS monitor, got %d\", len(resp.Msg.DnsMonitors))\n\t}\n}\n\nfunc TestMonitors_HTTPMonitorFields(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.MonitorsRequest{})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.Monitors(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\tif len(resp.Msg.HttpMonitors) != 1 {\n\t\tt.Fatalf(\"expected 1 HTTP monitor, got %d\", len(resp.Msg.HttpMonitors))\n\t}\n\n\thttpMonitor := resp.Msg.HttpMonitors[0]\n\tif httpMonitor.Id != \"5\" {\n\t\tt.Errorf(\"expected ID '5', got '%s'\", httpMonitor.Id)\n\t}\n\tif httpMonitor.Url != \"https://openstat.us\" {\n\t\tt.Errorf(\"expected URL 'https://openstat.us', got '%s'\", httpMonitor.Url)\n\t}\n\tif httpMonitor.Periodicity != \"10m\" {\n\t\tt.Errorf(\"expected Periodicity '10m', got '%s'\", httpMonitor.Periodicity)\n\t}\n\tif httpMonitor.Method != \"GET\" {\n\t\tt.Errorf(\"expected Method 'GET', got '%s'\", httpMonitor.Method)\n\t}\n\tif httpMonitor.Timeout != 45000 {\n\t\tt.Errorf(\"expected Timeout 45000, got %d\", httpMonitor.Timeout)\n\t}\n}\n\nfunc TestMonitors_TCPMonitorFields(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.MonitorsRequest{})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.Monitors(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\tif len(resp.Msg.TcpMonitors) != 1 {\n\t\tt.Fatalf(\"expected 1 TCP monitor, got %d\", len(resp.Msg.TcpMonitors))\n\t}\n\n\ttcpMonitor := resp.Msg.TcpMonitors[0]\n\tif tcpMonitor.Id != \"6\" {\n\t\tt.Errorf(\"expected ID '6', got '%s'\", tcpMonitor.Id)\n\t}\n\tif tcpMonitor.Uri != \"tcp://db.example.com:5432\" {\n\t\tt.Errorf(\"expected URI 'tcp://db.example.com:5432', got '%s'\", tcpMonitor.Uri)\n\t}\n\tif tcpMonitor.Periodicity != \"5m\" {\n\t\tt.Errorf(\"expected Periodicity '5m', got '%s'\", tcpMonitor.Periodicity)\n\t}\n\tif tcpMonitor.Timeout != 30000 {\n\t\tt.Errorf(\"expected Timeout 30000, got %d\", tcpMonitor.Timeout)\n\t}\n\tif tcpMonitor.Retry != 2 {\n\t\tt.Errorf(\"expected Retry 2, got %d\", tcpMonitor.Retry)\n\t}\n\tif tcpMonitor.DegradedAt == nil || *tcpMonitor.DegradedAt != 5000 {\n\t\tt.Errorf(\"expected DegradedAt 5000, got %v\", tcpMonitor.DegradedAt)\n\t}\n}\n\nfunc TestParseRecordAssertions_DnsRecordContains(t *testing.T) {\n\tinput := `[{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"A\",\"compare\":\"contains\",\"target\":\"76.76.21.21\"}]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif len(recordAssertions) != 1 {\n\t\tt.Fatalf(\"expected 1 record assertion, got %d\", len(recordAssertions))\n\t}\n\n\tgot := recordAssertions[0]\n\tif got.Record != \"A\" {\n\t\tt.Errorf(\"expected Record to be 'A', got '%s'\", got.Record)\n\t}\n\tif got.Target != \"76.76.21.21\" {\n\t\tt.Errorf(\"expected Target to be '76.76.21.21', got '%s'\", got.Target)\n\t}\n\tif got.Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_CONTAINS {\n\t\tt.Errorf(\"expected Comparator to be RECORD_COMPARATOR_CONTAINS, got %v\", got.Comparator)\n\t}\n}\n\nfunc TestParseRecordAssertions_DnsRecordEquals(t *testing.T) {\n\tinput := `[{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"CNAME\",\"compare\":\"eq\",\"target\":\"openstatus.dev\"}]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif len(recordAssertions) != 1 {\n\t\tt.Fatalf(\"expected 1 record assertion, got %d\", len(recordAssertions))\n\t}\n\n\tgot := recordAssertions[0]\n\tif got.Record != \"CNAME\" {\n\t\tt.Errorf(\"expected Record to be 'CNAME', got '%s'\", got.Record)\n\t}\n\tif got.Target != \"openstatus.dev\" {\n\t\tt.Errorf(\"expected Target to be 'openstatus.dev', got '%s'\", got.Target)\n\t}\n\tif got.Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_EQUAL {\n\t\tt.Errorf(\"expected Comparator to be RECORD_COMPARATOR_EQUAL, got %v\", got.Comparator)\n\t}\n}\n\nfunc TestParseRecordAssertions_MultipleRecordTypes(t *testing.T) {\n\tinput := `[\n\t\t{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"A\",\"compare\":\"eq\",\"target\":\"192.168.1.1\"},\n\t\t{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"AAAA\",\"compare\":\"not_eq\",\"target\":\"::1\"},\n\t\t{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"MX\",\"compare\":\"not_contains\",\"target\":\"spam\"}\n\t]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif len(recordAssertions) != 3 {\n\t\tt.Fatalf(\"expected 3 record assertions, got %d\", len(recordAssertions))\n\t}\n\n\t// Check A record\n\tif recordAssertions[0].Record != \"A\" {\n\t\tt.Errorf(\"expected first Record to be 'A', got '%s'\", recordAssertions[0].Record)\n\t}\n\tif recordAssertions[0].Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_EQUAL {\n\t\tt.Errorf(\"expected first Comparator to be RECORD_COMPARATOR_EQUAL, got %v\", recordAssertions[0].Comparator)\n\t}\n\n\t// Check AAAA record\n\tif recordAssertions[1].Record != \"AAAA\" {\n\t\tt.Errorf(\"expected second Record to be 'AAAA', got '%s'\", recordAssertions[1].Record)\n\t}\n\tif recordAssertions[1].Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_NOT_EQUAL {\n\t\tt.Errorf(\"expected second Comparator to be RECORD_COMPARATOR_NOT_EQUAL, got %v\", recordAssertions[1].Comparator)\n\t}\n\n\t// Check MX record\n\tif recordAssertions[2].Record != \"MX\" {\n\t\tt.Errorf(\"expected third Record to be 'MX', got '%s'\", recordAssertions[2].Record)\n\t}\n\tif recordAssertions[2].Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_NOT_CONTAINS {\n\t\tt.Errorf(\"expected third Comparator to be RECORD_COMPARATOR_NOT_CONTAINS, got %v\", recordAssertions[2].Comparator)\n\t}\n}\n\nfunc TestParseRecordAssertions_InvalidJSON(t *testing.T) {\n\tassertions := sql.NullString{\n\t\tString: \"not valid json\",\n\t\tValid:  true,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif len(recordAssertions) != 0 {\n\t\tt.Errorf(\"expected empty assertions for invalid JSON, got %d\", len(recordAssertions))\n\t}\n}\n\nfunc TestParseRecordAssertions_NullString(t *testing.T) {\n\tassertions := sql.NullString{\n\t\tString: \"\",\n\t\tValid:  false,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif recordAssertions != nil {\n\t\tt.Errorf(\"expected nil for null string, got %v\", recordAssertions)\n\t}\n}\n\nfunc TestParseRecordAssertions_MixedAssertionTypes(t *testing.T) {\n\t// Test that only dnsRecord assertions are parsed, not other types\n\tinput := `[\n\t\t{\"version\":\"v1\",\"type\":\"status\",\"compare\":\"eq\",\"target\":200},\n\t\t{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"A\",\"compare\":\"contains\",\"target\":\"192.168.1.1\"},\n\t\t{\"version\":\"v1\",\"type\":\"header\",\"compare\":\"eq\",\"key\":\"Content-Type\",\"target\":\"application/json\"}\n\t]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif len(recordAssertions) != 1 {\n\t\tt.Fatalf(\"expected 1 record assertion (only dnsRecord), got %d\", len(recordAssertions))\n\t}\n\n\tif recordAssertions[0].Record != \"A\" {\n\t\tt.Errorf(\"expected Record to be 'A', got '%s'\", recordAssertions[0].Record)\n\t}\n}\n\nfunc TestMonitors_DNSMonitorFields(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.MonitorsRequest{})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.Monitors(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\tif len(resp.Msg.DnsMonitors) != 1 {\n\t\tt.Fatalf(\"expected 1 DNS monitor, got %d\", len(resp.Msg.DnsMonitors))\n\t}\n\n\tdnsMonitor := resp.Msg.DnsMonitors[0]\n\tif dnsMonitor.Id != \"7\" {\n\t\tt.Errorf(\"expected ID '7', got '%s'\", dnsMonitor.Id)\n\t}\n\tif dnsMonitor.Uri != \"openstatus.dev\" {\n\t\tt.Errorf(\"expected URI 'openstatus.dev', got '%s'\", dnsMonitor.Uri)\n\t}\n\tif dnsMonitor.Periodicity != \"5m\" {\n\t\tt.Errorf(\"expected Periodicity '5m', got '%s'\", dnsMonitor.Periodicity)\n\t}\n\tif dnsMonitor.Timeout != 30000 {\n\t\tt.Errorf(\"expected Timeout 30000, got %d\", dnsMonitor.Timeout)\n\t}\n\tif dnsMonitor.Retry != 2 {\n\t\tt.Errorf(\"expected Retry 2, got %d\", dnsMonitor.Retry)\n\t}\n\tif dnsMonitor.DegradedAt == nil || *dnsMonitor.DegradedAt != 3000 {\n\t\tt.Errorf(\"expected DegradedAt 3000, got %v\", dnsMonitor.DegradedAt)\n\t}\n}\n\nfunc TestParseRecordAssertions_EmptyArray(t *testing.T) {\n\tassertions := sql.NullString{\n\t\tString: \"[]\",\n\t\tValid:  true,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif len(recordAssertions) != 0 {\n\t\tt.Errorf(\"expected empty slice for empty JSON array, got %d\", len(recordAssertions))\n\t}\n}\n\nfunc TestParseRecordAssertions_UnknownComparator(t *testing.T) {\n\tinput := `[{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"A\",\"compare\":\"unknown_comparator\",\"target\":\"192.168.1.1\"}]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif len(recordAssertions) != 1 {\n\t\tt.Fatalf(\"expected 1 record assertion, got %d\", len(recordAssertions))\n\t}\n\n\tgot := recordAssertions[0]\n\tif got.Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_UNSPECIFIED {\n\t\tt.Errorf(\"expected Comparator to be RECORD_COMPARATOR_UNSPECIFIED for unknown comparator, got %v\", got.Comparator)\n\t}\n}\n\nfunc TestParseRecordAssertions_UnknownRecordType(t *testing.T) {\n\tinput := `[{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"INVALID_RECORD_TYPE\",\"compare\":\"eq\",\"target\":\"test\"}]`\n\tassertions := sql.NullString{\n\t\tString: input,\n\t\tValid:  true,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif len(recordAssertions) != 1 {\n\t\tt.Fatalf(\"expected 1 record assertion, got %d\", len(recordAssertions))\n\t}\n\n\tgot := recordAssertions[0]\n\t// The record type is passed through as-is, even if invalid\n\tif got.Record != \"INVALID_RECORD_TYPE\" {\n\t\tt.Errorf(\"expected Record to be 'INVALID_RECORD_TYPE', got '%s'\", got.Record)\n\t}\n}\n\nfunc TestParseRecordAssertions_MissingRequiredFields(t *testing.T) {\n\t// Missing \"key\" field\n\tinputMissingKey := `[{\"version\":\"v1\",\"type\":\"dnsRecord\",\"compare\":\"eq\",\"target\":\"test\"}]`\n\tassertions := sql.NullString{\n\t\tString: inputMissingKey,\n\t\tValid:  true,\n\t}\n\n\trecordAssertions := server.ParseRecordAssertions(context.Background(), assertions)\n\n\tif len(recordAssertions) != 1 {\n\t\tt.Fatalf(\"expected 1 record assertion, got %d\", len(recordAssertions))\n\t}\n\n\t// Missing key results in empty string\n\tif recordAssertions[0].Record != \"\" {\n\t\tt.Errorf(\"expected empty Record for missing key, got '%s'\", recordAssertions[0].Record)\n\t}\n\n\t// Missing \"compare\" field\n\tinputMissingCompare := `[{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"A\",\"target\":\"test\"}]`\n\tassertions2 := sql.NullString{\n\t\tString: inputMissingCompare,\n\t\tValid:  true,\n\t}\n\n\trecordAssertions2 := server.ParseRecordAssertions(context.Background(), assertions2)\n\n\tif len(recordAssertions2) != 1 {\n\t\tt.Fatalf(\"expected 1 record assertion, got %d\", len(recordAssertions2))\n\t}\n\n\t// Missing compare results in unspecified comparator\n\tif recordAssertions2[0].Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_UNSPECIFIED {\n\t\tt.Errorf(\"expected RECORD_COMPARATOR_UNSPECIFIED for missing compare, got %v\", recordAssertions2[0].Comparator)\n\t}\n\n\t// Missing \"target\" field\n\tinputMissingTarget := `[{\"version\":\"v1\",\"type\":\"dnsRecord\",\"key\":\"A\",\"compare\":\"eq\"}]`\n\tassertions3 := sql.NullString{\n\t\tString: inputMissingTarget,\n\t\tValid:  true,\n\t}\n\n\trecordAssertions3 := server.ParseRecordAssertions(context.Background(), assertions3)\n\n\tif len(recordAssertions3) != 1 {\n\t\tt.Fatalf(\"expected 1 record assertion, got %d\", len(recordAssertions3))\n\t}\n\n\t// Missing target results in empty string\n\tif recordAssertions3[0].Target != \"\" {\n\t\tt.Errorf(\"expected empty Target for missing target, got '%s'\", recordAssertions3[0].Target)\n\t}\n}\n\nfunc TestMonitors_DNSMonitorWithRecordAssertions(t *testing.T) {\n\th := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))\n\n\treq := connect.NewRequest(&private_locationv1.MonitorsRequest{})\n\treq.Header().Set(\"openstatus-token\", \"my-secret-key\")\n\n\tresp, err := h.Monitors(context.Background(), req)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\tif len(resp.Msg.DnsMonitors) != 1 {\n\t\tt.Fatalf(\"expected 1 DNS monitor, got %d\", len(resp.Msg.DnsMonitors))\n\t}\n\n\tdnsMonitor := resp.Msg.DnsMonitors[0]\n\tif len(dnsMonitor.RecordAssertions) != 1 {\n\t\tt.Fatalf(\"expected 1 record assertion, got %d\", len(dnsMonitor.RecordAssertions))\n\t}\n\n\tassertion := dnsMonitor.RecordAssertions[0]\n\tif assertion.Record != \"A\" {\n\t\tt.Errorf(\"expected Record 'A', got '%s'\", assertion.Record)\n\t}\n\tif assertion.Target != \"76.76.21.21\" {\n\t\tt.Errorf(\"expected Target '76.76.21.21', got '%s'\", assertion.Target)\n\t}\n\tif assertion.Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_CONTAINS {\n\t\tt.Errorf(\"expected Comparator RECORD_COMPARATOR_CONTAINS, got %v\", assertion.Comparator)\n\t}\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/routes.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/render\"\n\t\"github.com/google/uuid\"\n\t\"github.com/jmoiron/sqlx\"\n\t_ \"github.com/joho/godotenv/autoload\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/logs\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/tinybird\"\n\tv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\ntype contextKey string\n\nconst (\n\trequestIDKey contextKey = \"request_id\"\n\teventKey     contextKey = \"event\"\n)\n\n// responseWriter wraps http.ResponseWriter to capture the status code\ntype responseWriter struct {\n\thttp.ResponseWriter\n\tstatus int\n}\n\nfunc (rw *responseWriter) WriteHeader(code int) {\n\trw.status = code\n\trw.ResponseWriter.WriteHeader(code)\n}\n\nfunc (rw *responseWriter) Write(b []byte) (int, error) {\n\tif rw.status == 0 {\n\t\trw.status = http.StatusOK\n\t}\n\treturn rw.ResponseWriter.Write(b)\n}\n\n// Logger returns a Chi middleware that logs request details\nfunc Logger() func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tstartTime := time.Now()\n\n\t\t\t// Generate or get request ID\n\t\t\trequestID := r.Header.Get(\"X-Request-ID\")\n\t\t\tif requestID == \"\" {\n\t\t\t\trequestID = uuid.New().String()\n\t\t\t}\n\n\t\t\t// Build wide event context at request start\n\t\t\tscheme := \"http\"\n\t\t\tif r.TLS != nil {\n\t\t\t\tscheme = \"https\"\n\t\t\t}\n\t\t\tfullURL := scheme + \"://\" + r.Host + r.RequestURI\n\n\t\t\tholder := &EventHolder{\n\t\t\t\tEvent: map[string]any{\n\t\t\t\t\t\"timestamp\":    startTime.Format(time.RFC3339),\n\t\t\t\t\t\"request_id\":   requestID,\n\t\t\t\t\t\"method\":       r.Method,\n\t\t\t\t\t\"path\":         r.URL.Path,\n\t\t\t\t\t\"url\":          fullURL,\n\t\t\t\t\t\"user_agent\":   r.Header.Get(\"User-Agent\"),\n\t\t\t\t\t\"content_type\": r.Header.Get(\"Content-Type\"),\n\t\t\t\t\t\"service\": map[string]any{\n\t\t\t\t\t\t\"name\":        \"openstatus-private-location\",\n\t\t\t\t\t\t\"instance_id\": instanceID,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t// Store in context\n\t\t\tctx := context.WithValue(r.Context(), requestIDKey, requestID)\n\t\t\tctx = context.WithValue(ctx, eventKey, holder)\n\t\t\tr = r.WithContext(ctx)\n\n\t\t\t// Wrap response writer to capture status code\n\t\t\twrapped := &responseWriter{ResponseWriter: w, status: 0}\n\n\t\t\t// Process request\n\t\t\tnext.ServeHTTP(wrapped, r)\n\n\t\t\t// After request - capture response details\n\t\t\tduration := time.Since(startTime).Milliseconds()\n\t\t\tstatus := wrapped.status\n\t\t\tif status == 0 {\n\t\t\t\tstatus = http.StatusOK\n\t\t\t}\n\n\t\t\tholder.Event[\"status_code\"] = status\n\t\t\tholder.Event[\"duration_ms\"] = duration\n\n\t\t\tif status >= 400 {\n\t\t\t\tholder.Event[\"outcome\"] = \"error\"\n\t\t\t} else {\n\t\t\t\tholder.Event[\"outcome\"] = \"success\"\n\t\t\t}\n\n\t\t\tif logs.ShouldSample(holder.Event) {\n\t\t\t\tattrs := logs.MapToAttrs(holder.Event)\n\t\t\t\tslog.LogAttrs(r.Context(), slog.LevelInfo, \"request done\", attrs...)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// GetRequestID retrieves the request ID from context\nfunc GetRequestID(ctx context.Context) string {\n\tif id, ok := ctx.Value(requestIDKey).(string); ok {\n\t\treturn id\n\t}\n\treturn \"\"\n}\n\n// GetEvent retrieves the event holder from context\nfunc GetEvent(ctx context.Context) *EventHolder {\n\tif holder, ok := ctx.Value(eventKey).(*EventHolder); ok {\n\t\treturn holder\n\t}\n\treturn nil\n}\n\ntype privateLocationHandler struct {\n\tdb       *sqlx.DB\n\tTbClient tinybird.Client\n}\n\nfunc NewPrivateLocationServer(db *sqlx.DB, tbClient tinybird.Client) *privateLocationHandler {\n\treturn &privateLocationHandler{\n\t\tdb:       db,\n\t\tTbClient: tbClient,\n\t}\n}\n\n// RegisterRoutes sets up the HTTP routes for the server.\nfunc (s *Server) RegisterRoutes() http.Handler {\n\tr := chi.NewRouter()\n\n\tr.Use(Logger())\n\n\tr.Get(\"/health\", s.healthHandler)\n\n\ttinyBirdToken := os.Getenv(\"TINYBIRD_TOKEN\")\n\n\thttpClient := &http.Client{\n\t\tTimeout: 45 * time.Second,\n\t}\n\n\ttinybirdClient := tinybird.NewClient(httpClient, tinyBirdToken)\n\n\tprivateLocationServer := NewPrivateLocationServer(s.db, tinybirdClient)\n\tpath, handler := v1.NewPrivateLocationServiceHandler(privateLocationServer)\n\n\tr.Group(func(r chi.Router) {\n\t\tr.Mount(path, handler)\n\t})\n\treturn r\n}\n\n// healthHandler responds with the health status of the server.\nfunc (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {\n\tstatus := \"ok\"\n\thttpStatus := http.StatusOK\n\tdbOK := true\n\n\t// Check database connection\n\tif err := s.db.PingContext(r.Context()); err != nil {\n\t\tstatus = \"degraded\"\n\t\thttpStatus = http.StatusServiceUnavailable\n\t\tdbOK = false\n\t}\n\n\t// Enrich wide event with health check context\n\tif holder := GetEvent(r.Context()); holder != nil {\n\t\tholder.Event[\"health_check\"] = map[string]any{\n\t\t\t\"status\":  status,\n\t\t\t\"db_ping\": dbOK,\n\t\t}\n\t}\n\n\trender.Status(r, httpStatus)\n\trender.JSON(w, r, map[string]any{\n\t\t\"status\": status,\n\t})\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/server.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jmoiron/sqlx\"\n\t_ \"github.com/joho/godotenv/autoload\"\n\t\"go.opentelemetry.io/contrib/bridges/otelslog\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp\"\n\t\"go.opentelemetry.io/otel/log/global\"\n\tsdklog \"go.opentelemetry.io/otel/sdk/log\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.21.0\"\n\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/database\"\n)\n\n\ntype Server struct {\n\tport        int\n\tdb          *sqlx.DB\n\tlogger      *slog.Logger\n\tlogProvider *sdklog.LoggerProvider\n}\n\n// instanceID is generated once at startup\nvar instanceID = uuid.New().String()\n\n// NewServer returns an HTTP server and a cleanup function to shutdown the log provider.\nfunc NewServer() (*http.Server, func(context.Context)) {\n\tportStr := os.Getenv(\"PORT\")\n\tif portStr == \"\" {\n\t\tportStr = \"8080\"\n\t}\n\tport, err := strconv.Atoi(portStr)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"invalid PORT value %q: %v\\n\", portStr, err)\n\t\tos.Exit(1)\n\t}\n\n\tlogger, logProvider := setupLogger()\n\n\tnewServer := &Server{\n\t\tport:        port,\n\t\tdb:          database.New(),\n\t\tlogger:      logger,\n\t\tlogProvider: logProvider,\n\t}\n\n\t// Declare Server config\n\tserver := &http.Server{\n\t\tAddr:         fmt.Sprintf(\":%d\", newServer.port),\n\t\tHandler:      newServer.RegisterRoutes(),\n\t\tIdleTimeout:  time.Minute,\n\t\tReadTimeout:  10 * time.Second,\n\t\tWriteTimeout: 30 * time.Second,\n\t}\n\n\t// Startup logging\n\tslog.Info(\"server starting\",\n\t\t\"port\", port,\n\t\t\"instance_id\", instanceID,\n\t\t\"axiom_configured\", os.Getenv(\"AXIOM_TOKEN\") != \"\",\n\t\t\"tinybird_configured\", os.Getenv(\"TINYBIRD_TOKEN\") != \"\",\n\t)\n\n\t// Return cleanup function for graceful shutdown\n\tcleanup := func(ctx context.Context) {\n\t\tif logProvider != nil {\n\t\t\tlogProvider.Shutdown(ctx)\n\t\t}\n\t\tdatabase.Close()\n\t}\n\n\treturn server, cleanup\n}\n\nfunc setupLogger() (*slog.Logger, *sdklog.LoggerProvider) {\n\tctx := context.Background()\n\n\taxiomToken := env(\"AXIOM_TOKEN\", \"\")\n\taxiomDataset := env(\"AXIOM_DATASET\", \"dev\")\n\n\t// If no Axiom token, return a standard logger\n\tif axiomToken == \"\" {\n\t\tlogger := slog.Default()\n\t\treturn logger, nil\n\t}\n\n\tenvironment := env(\"ENVIRONMENT\", \"production\")\n\n\tres := resource.NewWithAttributes(\n\t\tsemconv.SchemaURL,\n\t\tsemconv.ServiceNameKey.String(\"openstatus-private-location\"),\n\t\tattribute.String(\"environment\", environment),\n\n\t\tattribute.String(\"instance_id\", instanceID),\n\t)\n\n\t// Set up OTLP log exporter for Axiom\n\texporter, err := otlploghttp.New(ctx,\n\t\totlploghttp.WithEndpointURL(\"https://eu-central-1.aws.edge.axiom.co/v1/logs\"),\n\t\totlploghttp.WithHeaders(map[string]string{\n\t\t\t\"Authorization\":   \"Bearer \" + axiomToken,\n\t\t\t\"X-Axiom-Dataset\": axiomDataset,\n\t\t}),\n\t)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"failed to create OTLP exporter: %v\\n\", err)\n\t\treturn slog.Default(), nil\n\t}\n\n\t// Create log provider with resource and batch processor\n\tlogProvider := sdklog.NewLoggerProvider(\n\t\tsdklog.WithResource(res),\n\t\tsdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)),\n\t)\n\n\tglobal.SetLoggerProvider(logProvider)\n\tlogger := otelslog.NewLogger(\"openstatus-private-location\")\n\tslog.SetDefault(logger)\n\n\treturn logger, logProvider\n}\n\nfunc env(key, fallback string) string {\n\tif value, ok := os.LookupEnv(key); ok {\n\t\treturn value\n\t}\n\treturn fallback\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/validation.go",
    "content": "package server\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"connectrpc.com/connect\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\n// Validation errors\nvar (\n\tErrEmptyMonitorID  = errors.New(\"monitor_id is required\")\n\tErrEmptyID         = errors.New(\"id is required\")\n\tErrInvalidLatency  = errors.New(\"latency must be non-negative\")\n\tErrInvalidTimestamp = errors.New(\"timestamp must be positive\")\n)\n\n// ValidateIngestHTTPRequest validates an HTTP ingest request\nfunc ValidateIngestHTTPRequest(req *private_locationv1.IngestHTTPRequest) error {\n\tif req.MonitorId == \"\" {\n\t\treturn ErrEmptyMonitorID\n\t}\n\tif req.Latency < 0 {\n\t\treturn ErrInvalidLatency\n\t}\n\tif req.Timestamp <= 0 {\n\t\treturn ErrInvalidTimestamp\n\t}\n\treturn nil\n}\n\n// ValidateIngestTCPRequest validates a TCP ingest request\nfunc ValidateIngestTCPRequest(req *private_locationv1.IngestTCPRequest) error {\n\tif req.Id == \"\" {\n\t\treturn ErrEmptyID\n\t}\n\tif req.Latency < 0 {\n\t\treturn ErrInvalidLatency\n\t}\n\tif req.Timestamp <= 0 {\n\t\treturn ErrInvalidTimestamp\n\t}\n\treturn nil\n}\n\n// ValidateIngestDNSRequest validates a DNS ingest request\nfunc ValidateIngestDNSRequest(req *private_locationv1.IngestDNSRequest) error {\n\tif req.Id == \"\" {\n\t\treturn ErrEmptyID\n\t}\n\tif req.Latency < 0 {\n\t\treturn ErrInvalidLatency\n\t}\n\tif req.Timestamp <= 0 {\n\t\treturn ErrInvalidTimestamp\n\t}\n\treturn nil\n}\n\n// NewValidationError creates a Connect error for validation failures\nfunc NewValidationError(err error) *connect.Error {\n\treturn connect.NewError(connect.CodeInvalidArgument, fmt.Errorf(\"validation error: %w\", err))\n}\n"
  },
  {
    "path": "apps/private-location/internal/server/validation_test.go",
    "content": "package server_test\n\nimport (\n\t\"testing\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/openstatushq/openstatus/apps/private-location/internal/server\"\n\tprivate_locationv1 \"github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1\"\n)\n\nfunc TestValidateIngestHTTPRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\treq     *private_locationv1.IngestHTTPRequest\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"valid request\",\n\t\t\treq: &private_locationv1.IngestHTTPRequest{\n\t\t\t\tMonitorId: \"monitor-123\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"valid request with zero latency\",\n\t\t\treq: &private_locationv1.IngestHTTPRequest{\n\t\t\t\tMonitorId: \"monitor-123\",\n\t\t\t\tLatency:   0,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"empty monitor_id\",\n\t\t\treq: &private_locationv1.IngestHTTPRequest{\n\t\t\t\tMonitorId: \"\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: server.ErrEmptyMonitorID,\n\t\t},\n\t\t{\n\t\t\tname: \"negative latency\",\n\t\t\treq: &private_locationv1.IngestHTTPRequest{\n\t\t\t\tMonitorId: \"monitor-123\",\n\t\t\t\tLatency:   -1,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: server.ErrInvalidLatency,\n\t\t},\n\t\t{\n\t\t\tname: \"zero timestamp\",\n\t\t\treq: &private_locationv1.IngestHTTPRequest{\n\t\t\t\tMonitorId: \"monitor-123\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: 0,\n\t\t\t},\n\t\t\twantErr: server.ErrInvalidTimestamp,\n\t\t},\n\t\t{\n\t\t\tname: \"negative timestamp\",\n\t\t\treq: &private_locationv1.IngestHTTPRequest{\n\t\t\t\tMonitorId: \"monitor-123\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: -1,\n\t\t\t},\n\t\t\twantErr: server.ErrInvalidTimestamp,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := server.ValidateIngestHTTPRequest(tt.req)\n\t\t\tif err != tt.wantErr {\n\t\t\t\tt.Errorf(\"ValidateIngestHTTPRequest() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateIngestTCPRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\treq     *private_locationv1.IngestTCPRequest\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"valid request\",\n\t\t\treq: &private_locationv1.IngestTCPRequest{\n\t\t\t\tId:        \"tcp-123\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"valid request with zero latency\",\n\t\t\treq: &private_locationv1.IngestTCPRequest{\n\t\t\t\tId:        \"tcp-123\",\n\t\t\t\tLatency:   0,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"empty id\",\n\t\t\treq: &private_locationv1.IngestTCPRequest{\n\t\t\t\tId:        \"\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: server.ErrEmptyID,\n\t\t},\n\t\t{\n\t\t\tname: \"negative latency\",\n\t\t\treq: &private_locationv1.IngestTCPRequest{\n\t\t\t\tId:        \"tcp-123\",\n\t\t\t\tLatency:   -1,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: server.ErrInvalidLatency,\n\t\t},\n\t\t{\n\t\t\tname: \"zero timestamp\",\n\t\t\treq: &private_locationv1.IngestTCPRequest{\n\t\t\t\tId:        \"tcp-123\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: 0,\n\t\t\t},\n\t\t\twantErr: server.ErrInvalidTimestamp,\n\t\t},\n\t\t{\n\t\t\tname: \"negative timestamp\",\n\t\t\treq: &private_locationv1.IngestTCPRequest{\n\t\t\t\tId:        \"tcp-123\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: -1,\n\t\t\t},\n\t\t\twantErr: server.ErrInvalidTimestamp,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := server.ValidateIngestTCPRequest(tt.req)\n\t\t\tif err != tt.wantErr {\n\t\t\t\tt.Errorf(\"ValidateIngestTCPRequest() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateIngestDNSRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\treq     *private_locationv1.IngestDNSRequest\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"valid request\",\n\t\t\treq: &private_locationv1.IngestDNSRequest{\n\t\t\t\tId:        \"dns-123\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"valid request with zero latency\",\n\t\t\treq: &private_locationv1.IngestDNSRequest{\n\t\t\t\tId:        \"dns-123\",\n\t\t\t\tLatency:   0,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"empty id\",\n\t\t\treq: &private_locationv1.IngestDNSRequest{\n\t\t\t\tId:        \"\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: server.ErrEmptyID,\n\t\t},\n\t\t{\n\t\t\tname: \"negative latency\",\n\t\t\treq: &private_locationv1.IngestDNSRequest{\n\t\t\t\tId:        \"dns-123\",\n\t\t\t\tLatency:   -1,\n\t\t\t\tTimestamp: 1234567890,\n\t\t\t},\n\t\t\twantErr: server.ErrInvalidLatency,\n\t\t},\n\t\t{\n\t\t\tname: \"zero timestamp\",\n\t\t\treq: &private_locationv1.IngestDNSRequest{\n\t\t\t\tId:        \"dns-123\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: 0,\n\t\t\t},\n\t\t\twantErr: server.ErrInvalidTimestamp,\n\t\t},\n\t\t{\n\t\t\tname: \"negative timestamp\",\n\t\t\treq: &private_locationv1.IngestDNSRequest{\n\t\t\t\tId:        \"dns-123\",\n\t\t\t\tLatency:   100,\n\t\t\t\tTimestamp: -1,\n\t\t\t},\n\t\t\twantErr: server.ErrInvalidTimestamp,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := server.ValidateIngestDNSRequest(tt.req)\n\t\t\tif err != tt.wantErr {\n\t\t\t\tt.Errorf(\"ValidateIngestDNSRequest() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewValidationError(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\terr          error\n\t\twantCode     connect.Code\n\t\twantContains string\n\t}{\n\t\t{\n\t\t\tname:         \"empty monitor id error\",\n\t\t\terr:          server.ErrEmptyMonitorID,\n\t\t\twantCode:     connect.CodeInvalidArgument,\n\t\t\twantContains: \"monitor_id is required\",\n\t\t},\n\t\t{\n\t\t\tname:         \"empty id error\",\n\t\t\terr:          server.ErrEmptyID,\n\t\t\twantCode:     connect.CodeInvalidArgument,\n\t\t\twantContains: \"id is required\",\n\t\t},\n\t\t{\n\t\t\tname:         \"invalid latency error\",\n\t\t\terr:          server.ErrInvalidLatency,\n\t\t\twantCode:     connect.CodeInvalidArgument,\n\t\t\twantContains: \"latency must be non-negative\",\n\t\t},\n\t\t{\n\t\t\tname:         \"invalid timestamp error\",\n\t\t\terr:          server.ErrInvalidTimestamp,\n\t\t\twantCode:     connect.CodeInvalidArgument,\n\t\t\twantContains: \"timestamp must be positive\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconnErr := server.NewValidationError(tt.err)\n\t\t\tif connErr.Code() != tt.wantCode {\n\t\t\t\tt.Errorf(\"NewValidationError() code = %v, want %v\", connErr.Code(), tt.wantCode)\n\t\t\t}\n\t\t\tif connErr.Message() == \"\" {\n\t\t\t\tt.Error(\"NewValidationError() message should not be empty\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "apps/private-location/internal/tinybird/client.go",
    "content": "package tinybird\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n)\n\n// Datasource names for Tinybird events\nconst (\n\tDatasourceHTTP = \"ping_response__v8\"\n\tDatasourceTCP  = \"tcp_response__v0\"\n\tDatasourceDNS  = \"tcp_dns__v0\"\n)\n\nfunc getBaseURL() string {\n\t// Use local Tinybird container if available (Docker/self-hosted)\n\t// https://www.tinybird.co/docs/api-reference\n\tif tinybirdURL := os.Getenv(\"TINYBIRD_URL\"); tinybirdURL != \"\" {\n\t\treturn tinybirdURL + \"/v0/events\"\n\t}\n\treturn \"https://api.tinybird.co/v0/events\"\n}\n\ntype Client interface {\n\tSendEvent(ctx context.Context, event any, dataSourceName string) error\n}\n\ntype client struct {\n\thttpClient *http.Client\n\tapiKey     string\n\tbaseURL    string\n}\n\nfunc NewClient(httpClient *http.Client, apiKey string) Client {\n\treturn client{\n\t\thttpClient: httpClient,\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    getBaseURL(),\n\t}\n}\n\nfunc (c client) SendEvent(ctx context.Context, event any, dataSourceName string) error {\n\trequestURL, err := url.Parse(c.baseURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to parse url: %w\", err)\n\t}\n\n\tq := requestURL.Query()\n\tq.Add(\"name\", dataSourceName)\n\trequestURL.RawQuery = q.Encode()\n\n\tvar payload bytes.Buffer\n\tif err := json.NewEncoder(&payload).Encode(event); err != nil {\n\t\treturn fmt.Errorf(\"unable to encode payload: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(payload.Bytes()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", c.apiKey))\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusAccepted {\n\t\treturn fmt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "apps/private-location/internal/tinybird/client_test.go",
    "content": "package tinybird_test\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype interceptorHTTPClient struct {\n\tf func(req *http.Request) (*http.Response, error)\n}\n\nfunc (i *interceptorHTTPClient) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn i.f(req)\n}\n\nfunc (i *interceptorHTTPClient) GetHTTPClient() *http.Client {\n\treturn &http.Client{\n\t\tTransport: i,\n\t}\n}\n\nfunc TestSendEvent(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\tt.Run(\"it should return an error if it can not send the event\", func(t *testing.T) {\n\t\tinterceptor := &interceptorHTTPClient{\n\t\t\tf: func(req *http.Request) (*http.Response, error) {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to send request\")\n\t\t\t},\n\t\t}\n\n\t\tclient := tinybird.NewClient(interceptor.GetHTTPClient(), \"apiKey\")\n\n\t\terr := client.SendEvent(ctx, \"event\", \"test\")\n\t\trequire.Error(t, err)\n\t})\n\n\tt.Run(\"it should return an error if the response status code is not 200\", func(t *testing.T) {\n\t\tinterceptor := &interceptorHTTPClient{\n\t\t\tf: func(req *http.Request) (*http.Response, error) {\n\t\t\t\treturn &http.Response{\n\t\t\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t}\n\n\t\tclient := tinybird.NewClient(interceptor.GetHTTPClient(), \"apiKey\")\n\n\t\terr := client.SendEvent(ctx, \"event\", \"test\")\n\t\trequire.Error(t, err)\n\t})\n\n\tt.Run(\"it should succeed and return nothing\", func(t *testing.T) {\n\t\tvar url string\n\t\tinterceptor := &interceptorHTTPClient{\n\t\t\tf: func(req *http.Request) (*http.Response, error) {\n\t\t\t\turl = req.URL.String()\n\t\t\t\treturn &http.Response{\n\t\t\t\t\tStatusCode: http.StatusAccepted,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t}\n\n\t\tclient := tinybird.NewClient(interceptor.GetHTTPClient(), \"apiKey\")\n\n\t\terr := client.SendEvent(ctx, \"event\", \"test\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"https://api.tinybird.co/v0/events?name=test\", url)\n\t})\n}\n"
  },
  {
    "path": "apps/private-location/justfile",
    "content": "# Simple Makefile for a Go project\n\n# Build the application\nall: build test\n\nbuild:\n\techo \"Building...\"\n\n\n\tgo build -o main cmd/server/main.go\ndev:\n\tair\n# Run the application\nrun:\n\tgo run cmd/server/main.go\n\n# Test the application\ntest:\n\techo \"Testing...\"\n\tgo test ./... -v\n\n# Clean the binary\nclean:\n\techo \"Cleaning...\"\n\trm -f main\n"
  },
  {
    "path": "apps/private-location/proto/private_location/v1/assertions.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/assertions.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype NumberComparator int32\n\nconst (\n\tNumberComparator_NUMBER_COMPARATOR_UNSPECIFIED           NumberComparator = 0\n\tNumberComparator_NUMBER_COMPARATOR_EQUAL                 NumberComparator = 1\n\tNumberComparator_NUMBER_COMPARATOR_NOT_EQUAL             NumberComparator = 2\n\tNumberComparator_NUMBER_COMPARATOR_GREATER_THAN          NumberComparator = 3\n\tNumberComparator_NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL NumberComparator = 4\n\tNumberComparator_NUMBER_COMPARATOR_LESS_THAN             NumberComparator = 5\n\tNumberComparator_NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL    NumberComparator = 6\n)\n\n// Enum value maps for NumberComparator.\nvar (\n\tNumberComparator_name = map[int32]string{\n\t\t0: \"NUMBER_COMPARATOR_UNSPECIFIED\",\n\t\t1: \"NUMBER_COMPARATOR_EQUAL\",\n\t\t2: \"NUMBER_COMPARATOR_NOT_EQUAL\",\n\t\t3: \"NUMBER_COMPARATOR_GREATER_THAN\",\n\t\t4: \"NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\",\n\t\t5: \"NUMBER_COMPARATOR_LESS_THAN\",\n\t\t6: \"NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\",\n\t}\n\tNumberComparator_value = map[string]int32{\n\t\t\"NUMBER_COMPARATOR_UNSPECIFIED\":           0,\n\t\t\"NUMBER_COMPARATOR_EQUAL\":                 1,\n\t\t\"NUMBER_COMPARATOR_NOT_EQUAL\":             2,\n\t\t\"NUMBER_COMPARATOR_GREATER_THAN\":          3,\n\t\t\"NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\": 4,\n\t\t\"NUMBER_COMPARATOR_LESS_THAN\":             5,\n\t\t\"NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\":    6,\n\t}\n)\n\nfunc (x NumberComparator) Enum() *NumberComparator {\n\tp := new(NumberComparator)\n\t*p = x\n\treturn p\n}\n\nfunc (x NumberComparator) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (NumberComparator) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_private_location_v1_assertions_proto_enumTypes[0].Descriptor()\n}\n\nfunc (NumberComparator) Type() protoreflect.EnumType {\n\treturn &file_private_location_v1_assertions_proto_enumTypes[0]\n}\n\nfunc (x NumberComparator) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use NumberComparator.Descriptor instead.\nfunc (NumberComparator) EnumDescriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{0}\n}\n\ntype StringComparator int32\n\nconst (\n\tStringComparator_STRING_COMPARATOR_UNSPECIFIED           StringComparator = 0\n\tStringComparator_STRING_COMPARATOR_CONTAINS              StringComparator = 1\n\tStringComparator_STRING_COMPARATOR_NOT_CONTAINS          StringComparator = 2\n\tStringComparator_STRING_COMPARATOR_EQUAL                 StringComparator = 3\n\tStringComparator_STRING_COMPARATOR_NOT_EQUAL             StringComparator = 4\n\tStringComparator_STRING_COMPARATOR_EMPTY                 StringComparator = 5\n\tStringComparator_STRING_COMPARATOR_NOT_EMPTY             StringComparator = 6\n\tStringComparator_STRING_COMPARATOR_GREATER_THAN          StringComparator = 7\n\tStringComparator_STRING_COMPARATOR_GREATER_THAN_OR_EQUAL StringComparator = 8\n\tStringComparator_STRING_COMPARATOR_LESS_THAN             StringComparator = 9\n\tStringComparator_STRING_COMPARATOR_LESS_THAN_OR_EQUAL    StringComparator = 10\n)\n\n// Enum value maps for StringComparator.\nvar (\n\tStringComparator_name = map[int32]string{\n\t\t0:  \"STRING_COMPARATOR_UNSPECIFIED\",\n\t\t1:  \"STRING_COMPARATOR_CONTAINS\",\n\t\t2:  \"STRING_COMPARATOR_NOT_CONTAINS\",\n\t\t3:  \"STRING_COMPARATOR_EQUAL\",\n\t\t4:  \"STRING_COMPARATOR_NOT_EQUAL\",\n\t\t5:  \"STRING_COMPARATOR_EMPTY\",\n\t\t6:  \"STRING_COMPARATOR_NOT_EMPTY\",\n\t\t7:  \"STRING_COMPARATOR_GREATER_THAN\",\n\t\t8:  \"STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\",\n\t\t9:  \"STRING_COMPARATOR_LESS_THAN\",\n\t\t10: \"STRING_COMPARATOR_LESS_THAN_OR_EQUAL\",\n\t}\n\tStringComparator_value = map[string]int32{\n\t\t\"STRING_COMPARATOR_UNSPECIFIED\":           0,\n\t\t\"STRING_COMPARATOR_CONTAINS\":              1,\n\t\t\"STRING_COMPARATOR_NOT_CONTAINS\":          2,\n\t\t\"STRING_COMPARATOR_EQUAL\":                 3,\n\t\t\"STRING_COMPARATOR_NOT_EQUAL\":             4,\n\t\t\"STRING_COMPARATOR_EMPTY\":                 5,\n\t\t\"STRING_COMPARATOR_NOT_EMPTY\":             6,\n\t\t\"STRING_COMPARATOR_GREATER_THAN\":          7,\n\t\t\"STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\": 8,\n\t\t\"STRING_COMPARATOR_LESS_THAN\":             9,\n\t\t\"STRING_COMPARATOR_LESS_THAN_OR_EQUAL\":    10,\n\t}\n)\n\nfunc (x StringComparator) Enum() *StringComparator {\n\tp := new(StringComparator)\n\t*p = x\n\treturn p\n}\n\nfunc (x StringComparator) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (StringComparator) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_private_location_v1_assertions_proto_enumTypes[1].Descriptor()\n}\n\nfunc (StringComparator) Type() protoreflect.EnumType {\n\treturn &file_private_location_v1_assertions_proto_enumTypes[1]\n}\n\nfunc (x StringComparator) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use StringComparator.Descriptor instead.\nfunc (StringComparator) EnumDescriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{1}\n}\n\ntype RecordComparator int32\n\nconst (\n\tRecordComparator_RECORD_COMPARATOR_UNSPECIFIED  RecordComparator = 0\n\tRecordComparator_RECORD_COMPARATOR_EQUAL        RecordComparator = 1\n\tRecordComparator_RECORD_COMPARATOR_NOT_EQUAL    RecordComparator = 2\n\tRecordComparator_RECORD_COMPARATOR_CONTAINS     RecordComparator = 3\n\tRecordComparator_RECORD_COMPARATOR_NOT_CONTAINS RecordComparator = 4\n)\n\n// Enum value maps for RecordComparator.\nvar (\n\tRecordComparator_name = map[int32]string{\n\t\t0: \"RECORD_COMPARATOR_UNSPECIFIED\",\n\t\t1: \"RECORD_COMPARATOR_EQUAL\",\n\t\t2: \"RECORD_COMPARATOR_NOT_EQUAL\",\n\t\t3: \"RECORD_COMPARATOR_CONTAINS\",\n\t\t4: \"RECORD_COMPARATOR_NOT_CONTAINS\",\n\t}\n\tRecordComparator_value = map[string]int32{\n\t\t\"RECORD_COMPARATOR_UNSPECIFIED\":  0,\n\t\t\"RECORD_COMPARATOR_EQUAL\":        1,\n\t\t\"RECORD_COMPARATOR_NOT_EQUAL\":    2,\n\t\t\"RECORD_COMPARATOR_CONTAINS\":     3,\n\t\t\"RECORD_COMPARATOR_NOT_CONTAINS\": 4,\n\t}\n)\n\nfunc (x RecordComparator) Enum() *RecordComparator {\n\tp := new(RecordComparator)\n\t*p = x\n\treturn p\n}\n\nfunc (x RecordComparator) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (RecordComparator) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_private_location_v1_assertions_proto_enumTypes[2].Descriptor()\n}\n\nfunc (RecordComparator) Type() protoreflect.EnumType {\n\treturn &file_private_location_v1_assertions_proto_enumTypes[2]\n}\n\nfunc (x RecordComparator) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use RecordComparator.Descriptor instead.\nfunc (RecordComparator) EnumDescriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{2}\n}\n\ntype StatusCodeAssertion struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTarget        int64                  `protobuf:\"varint,1,opt,name=target,proto3\" json:\"target,omitempty\"`\n\tComparator    NumberComparator       `protobuf:\"varint,2,opt,name=comparator,proto3,enum=private_location.v1.NumberComparator\" json:\"comparator,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *StatusCodeAssertion) Reset() {\n\t*x = StatusCodeAssertion{}\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *StatusCodeAssertion) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StatusCodeAssertion) ProtoMessage() {}\n\nfunc (x *StatusCodeAssertion) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StatusCodeAssertion.ProtoReflect.Descriptor instead.\nfunc (*StatusCodeAssertion) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *StatusCodeAssertion) GetTarget() int64 {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn 0\n}\n\nfunc (x *StatusCodeAssertion) GetComparator() NumberComparator {\n\tif x != nil {\n\t\treturn x.Comparator\n\t}\n\treturn NumberComparator_NUMBER_COMPARATOR_UNSPECIFIED\n}\n\ntype BodyAssertion struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTarget        string                 `protobuf:\"bytes,1,opt,name=target,proto3\" json:\"target,omitempty\"`\n\tComparator    StringComparator       `protobuf:\"varint,2,opt,name=comparator,proto3,enum=private_location.v1.StringComparator\" json:\"comparator,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BodyAssertion) Reset() {\n\t*x = BodyAssertion{}\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BodyAssertion) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BodyAssertion) ProtoMessage() {}\n\nfunc (x *BodyAssertion) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BodyAssertion.ProtoReflect.Descriptor instead.\nfunc (*BodyAssertion) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *BodyAssertion) GetTarget() string {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn \"\"\n}\n\nfunc (x *BodyAssertion) GetComparator() StringComparator {\n\tif x != nil {\n\t\treturn x.Comparator\n\t}\n\treturn StringComparator_STRING_COMPARATOR_UNSPECIFIED\n}\n\ntype HeaderAssertion struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTarget        string                 `protobuf:\"bytes,1,opt,name=target,proto3\" json:\"target,omitempty\"`\n\tComparator    StringComparator       `protobuf:\"varint,2,opt,name=comparator,proto3,enum=private_location.v1.StringComparator\" json:\"comparator,omitempty\"`\n\tKey           string                 `protobuf:\"bytes,3,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HeaderAssertion) Reset() {\n\t*x = HeaderAssertion{}\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HeaderAssertion) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HeaderAssertion) ProtoMessage() {}\n\nfunc (x *HeaderAssertion) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HeaderAssertion.ProtoReflect.Descriptor instead.\nfunc (*HeaderAssertion) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *HeaderAssertion) GetTarget() string {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn \"\"\n}\n\nfunc (x *HeaderAssertion) GetComparator() StringComparator {\n\tif x != nil {\n\t\treturn x.Comparator\n\t}\n\treturn StringComparator_STRING_COMPARATOR_UNSPECIFIED\n}\n\nfunc (x *HeaderAssertion) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\ntype RecordAssertion struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tRecord        string                 `protobuf:\"bytes,1,opt,name=record,proto3\" json:\"record,omitempty\"`\n\tComparator    RecordComparator       `protobuf:\"varint,2,opt,name=comparator,proto3,enum=private_location.v1.RecordComparator\" json:\"comparator,omitempty\"`\n\tTarget        string                 `protobuf:\"bytes,3,opt,name=target,proto3\" json:\"target,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RecordAssertion) Reset() {\n\t*x = RecordAssertion{}\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RecordAssertion) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RecordAssertion) ProtoMessage() {}\n\nfunc (x *RecordAssertion) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_assertions_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RecordAssertion.ProtoReflect.Descriptor instead.\nfunc (*RecordAssertion) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_assertions_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *RecordAssertion) GetRecord() string {\n\tif x != nil {\n\t\treturn x.Record\n\t}\n\treturn \"\"\n}\n\nfunc (x *RecordAssertion) GetComparator() RecordComparator {\n\tif x != nil {\n\t\treturn x.Comparator\n\t}\n\treturn RecordComparator_RECORD_COMPARATOR_UNSPECIFIED\n}\n\nfunc (x *RecordAssertion) GetTarget() string {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn \"\"\n}\n\nvar File_private_location_v1_assertions_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_assertions_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"$private_location/v1/assertions.proto\\x12\\x13private_location.v1\\\"t\\n\" +\n\t\"\\x13StatusCodeAssertion\\x12\\x16\\n\" +\n\t\"\\x06target\\x18\\x01 \\x01(\\x03R\\x06target\\x12E\\n\" +\n\t\"\\n\" +\n\t\"comparator\\x18\\x02 \\x01(\\x0e2%.private_location.v1.NumberComparatorR\\n\" +\n\t\"comparator\\\"n\\n\" +\n\t\"\\rBodyAssertion\\x12\\x16\\n\" +\n\t\"\\x06target\\x18\\x01 \\x01(\\tR\\x06target\\x12E\\n\" +\n\t\"\\n\" +\n\t\"comparator\\x18\\x02 \\x01(\\x0e2%.private_location.v1.StringComparatorR\\n\" +\n\t\"comparator\\\"\\x82\\x01\\n\" +\n\t\"\\x0fHeaderAssertion\\x12\\x16\\n\" +\n\t\"\\x06target\\x18\\x01 \\x01(\\tR\\x06target\\x12E\\n\" +\n\t\"\\n\" +\n\t\"comparator\\x18\\x02 \\x01(\\x0e2%.private_location.v1.StringComparatorR\\n\" +\n\t\"comparator\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x03 \\x01(\\tR\\x03key\\\"\\x88\\x01\\n\" +\n\t\"\\x0fRecordAssertion\\x12\\x16\\n\" +\n\t\"\\x06record\\x18\\x01 \\x01(\\tR\\x06record\\x12E\\n\" +\n\t\"\\n\" +\n\t\"comparator\\x18\\x02 \\x01(\\x0e2%.private_location.v1.RecordComparatorR\\n\" +\n\t\"comparator\\x12\\x16\\n\" +\n\t\"\\x06target\\x18\\x03 \\x01(\\tR\\x06target*\\x8f\\x02\\n\" +\n\t\"\\x10NumberComparator\\x12!\\n\" +\n\t\"\\x1dNUMBER_COMPARATOR_UNSPECIFIED\\x10\\x00\\x12\\x1b\\n\" +\n\t\"\\x17NUMBER_COMPARATOR_EQUAL\\x10\\x01\\x12\\x1f\\n\" +\n\t\"\\x1bNUMBER_COMPARATOR_NOT_EQUAL\\x10\\x02\\x12\\\"\\n\" +\n\t\"\\x1eNUMBER_COMPARATOR_GREATER_THAN\\x10\\x03\\x12+\\n\" +\n\t\"'NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\\x10\\x04\\x12\\x1f\\n\" +\n\t\"\\x1bNUMBER_COMPARATOR_LESS_THAN\\x10\\x05\\x12(\\n\" +\n\t\"$NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\\x10\\x06*\\x91\\x03\\n\" +\n\t\"\\x10StringComparator\\x12!\\n\" +\n\t\"\\x1dSTRING_COMPARATOR_UNSPECIFIED\\x10\\x00\\x12\\x1e\\n\" +\n\t\"\\x1aSTRING_COMPARATOR_CONTAINS\\x10\\x01\\x12\\\"\\n\" +\n\t\"\\x1eSTRING_COMPARATOR_NOT_CONTAINS\\x10\\x02\\x12\\x1b\\n\" +\n\t\"\\x17STRING_COMPARATOR_EQUAL\\x10\\x03\\x12\\x1f\\n\" +\n\t\"\\x1bSTRING_COMPARATOR_NOT_EQUAL\\x10\\x04\\x12\\x1b\\n\" +\n\t\"\\x17STRING_COMPARATOR_EMPTY\\x10\\x05\\x12\\x1f\\n\" +\n\t\"\\x1bSTRING_COMPARATOR_NOT_EMPTY\\x10\\x06\\x12\\\"\\n\" +\n\t\"\\x1eSTRING_COMPARATOR_GREATER_THAN\\x10\\a\\x12+\\n\" +\n\t\"'STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\\x10\\b\\x12\\x1f\\n\" +\n\t\"\\x1bSTRING_COMPARATOR_LESS_THAN\\x10\\t\\x12(\\n\" +\n\t\"$STRING_COMPARATOR_LESS_THAN_OR_EQUAL\\x10\\n\" +\n\t\"*\\xb7\\x01\\n\" +\n\t\"\\x10RecordComparator\\x12!\\n\" +\n\t\"\\x1dRECORD_COMPARATOR_UNSPECIFIED\\x10\\x00\\x12\\x1b\\n\" +\n\t\"\\x17RECORD_COMPARATOR_EQUAL\\x10\\x01\\x12\\x1f\\n\" +\n\t\"\\x1bRECORD_COMPARATOR_NOT_EQUAL\\x10\\x02\\x12\\x1e\\n\" +\n\t\"\\x1aRECORD_COMPARATOR_CONTAINS\\x10\\x03\\x12\\\"\\n\" +\n\t\"\\x1eRECORD_COMPARATOR_NOT_CONTAINS\\x10\\x04BJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_assertions_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_assertions_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_assertions_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_assertions_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_assertions_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_assertions_proto_rawDesc), len(file_private_location_v1_assertions_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_assertions_proto_rawDescData\n}\n\nvar file_private_location_v1_assertions_proto_enumTypes = make([]protoimpl.EnumInfo, 3)\nvar file_private_location_v1_assertions_proto_msgTypes = make([]protoimpl.MessageInfo, 4)\nvar file_private_location_v1_assertions_proto_goTypes = []any{\n\t(NumberComparator)(0),       // 0: private_location.v1.NumberComparator\n\t(StringComparator)(0),       // 1: private_location.v1.StringComparator\n\t(RecordComparator)(0),       // 2: private_location.v1.RecordComparator\n\t(*StatusCodeAssertion)(nil), // 3: private_location.v1.StatusCodeAssertion\n\t(*BodyAssertion)(nil),       // 4: private_location.v1.BodyAssertion\n\t(*HeaderAssertion)(nil),     // 5: private_location.v1.HeaderAssertion\n\t(*RecordAssertion)(nil),     // 6: private_location.v1.RecordAssertion\n}\nvar file_private_location_v1_assertions_proto_depIdxs = []int32{\n\t0, // 0: private_location.v1.StatusCodeAssertion.comparator:type_name -> private_location.v1.NumberComparator\n\t1, // 1: private_location.v1.BodyAssertion.comparator:type_name -> private_location.v1.StringComparator\n\t1, // 2: private_location.v1.HeaderAssertion.comparator:type_name -> private_location.v1.StringComparator\n\t2, // 3: private_location.v1.RecordAssertion.comparator:type_name -> private_location.v1.RecordComparator\n\t4, // [4:4] is the sub-list for method output_type\n\t4, // [4:4] is the sub-list for method input_type\n\t4, // [4:4] is the sub-list for extension type_name\n\t4, // [4:4] is the sub-list for extension extendee\n\t0, // [0:4] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_assertions_proto_init() }\nfunc file_private_location_v1_assertions_proto_init() {\n\tif File_private_location_v1_assertions_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_assertions_proto_rawDesc), len(file_private_location_v1_assertions_proto_rawDesc)),\n\t\t\tNumEnums:      3,\n\t\t\tNumMessages:   4,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_assertions_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_assertions_proto_depIdxs,\n\t\tEnumInfos:         file_private_location_v1_assertions_proto_enumTypes,\n\t\tMessageInfos:      file_private_location_v1_assertions_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_assertions_proto = out.File\n\tfile_private_location_v1_assertions_proto_goTypes = nil\n\tfile_private_location_v1_assertions_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/private-location/proto/private_location/v1/dns_monitor.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/dns_monitor.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype DNSMonitor struct {\n\tstate            protoimpl.MessageState `protogen:\"open.v1\"`\n\tId               string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tUri              string                 `protobuf:\"bytes,2,opt,name=uri,proto3\" json:\"uri,omitempty\"`\n\tTimeout          int64                  `protobuf:\"varint,3,opt,name=timeout,proto3\" json:\"timeout,omitempty\"`\n\tDegradedAt       *int64                 `protobuf:\"varint,4,opt,name=degraded_at,json=degradedAt,proto3,oneof\" json:\"degraded_at,omitempty\"`\n\tPeriodicity      string                 `protobuf:\"bytes,5,opt,name=periodicity,proto3\" json:\"periodicity,omitempty\"`\n\tRetry            int64                  `protobuf:\"varint,6,opt,name=retry,proto3\" json:\"retry,omitempty\"`\n\tRecordAssertions []*RecordAssertion     `protobuf:\"bytes,13,rep,name=record_assertions,json=recordAssertions,proto3\" json:\"record_assertions,omitempty\"`\n\tunknownFields    protoimpl.UnknownFields\n\tsizeCache        protoimpl.SizeCache\n}\n\nfunc (x *DNSMonitor) Reset() {\n\t*x = DNSMonitor{}\n\tmi := &file_private_location_v1_dns_monitor_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DNSMonitor) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DNSMonitor) ProtoMessage() {}\n\nfunc (x *DNSMonitor) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_dns_monitor_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DNSMonitor.ProtoReflect.Descriptor instead.\nfunc (*DNSMonitor) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_dns_monitor_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *DNSMonitor) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *DNSMonitor) GetUri() string {\n\tif x != nil {\n\t\treturn x.Uri\n\t}\n\treturn \"\"\n}\n\nfunc (x *DNSMonitor) GetTimeout() int64 {\n\tif x != nil {\n\t\treturn x.Timeout\n\t}\n\treturn 0\n}\n\nfunc (x *DNSMonitor) GetDegradedAt() int64 {\n\tif x != nil && x.DegradedAt != nil {\n\t\treturn *x.DegradedAt\n\t}\n\treturn 0\n}\n\nfunc (x *DNSMonitor) GetPeriodicity() string {\n\tif x != nil {\n\t\treturn x.Periodicity\n\t}\n\treturn \"\"\n}\n\nfunc (x *DNSMonitor) GetRetry() int64 {\n\tif x != nil {\n\t\treturn x.Retry\n\t}\n\treturn 0\n}\n\nfunc (x *DNSMonitor) GetRecordAssertions() []*RecordAssertion {\n\tif x != nil {\n\t\treturn x.RecordAssertions\n\t}\n\treturn nil\n}\n\nvar File_private_location_v1_dns_monitor_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_dns_monitor_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"%private_location/v1/dns_monitor.proto\\x12\\x13private_location.v1\\x1a$private_location/v1/assertions.proto\\\"\\x89\\x02\\n\" +\n\t\"\\n\" +\n\t\"DNSMonitor\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x10\\n\" +\n\t\"\\x03uri\\x18\\x02 \\x01(\\tR\\x03uri\\x12\\x18\\n\" +\n\t\"\\atimeout\\x18\\x03 \\x01(\\x03R\\atimeout\\x12$\\n\" +\n\t\"\\vdegraded_at\\x18\\x04 \\x01(\\x03H\\x00R\\n\" +\n\t\"degradedAt\\x88\\x01\\x01\\x12 \\n\" +\n\t\"\\vperiodicity\\x18\\x05 \\x01(\\tR\\vperiodicity\\x12\\x14\\n\" +\n\t\"\\x05retry\\x18\\x06 \\x01(\\x03R\\x05retry\\x12Q\\n\" +\n\t\"\\x11record_assertions\\x18\\r \\x03(\\v2$.private_location.v1.RecordAssertionR\\x10recordAssertionsB\\x0e\\n\" +\n\t\"\\f_degraded_atBJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_dns_monitor_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_dns_monitor_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_dns_monitor_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_dns_monitor_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_dns_monitor_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_dns_monitor_proto_rawDesc), len(file_private_location_v1_dns_monitor_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_dns_monitor_proto_rawDescData\n}\n\nvar file_private_location_v1_dns_monitor_proto_msgTypes = make([]protoimpl.MessageInfo, 1)\nvar file_private_location_v1_dns_monitor_proto_goTypes = []any{\n\t(*DNSMonitor)(nil),      // 0: private_location.v1.DNSMonitor\n\t(*RecordAssertion)(nil), // 1: private_location.v1.RecordAssertion\n}\nvar file_private_location_v1_dns_monitor_proto_depIdxs = []int32{\n\t1, // 0: private_location.v1.DNSMonitor.record_assertions:type_name -> private_location.v1.RecordAssertion\n\t1, // [1:1] is the sub-list for method output_type\n\t1, // [1:1] is the sub-list for method input_type\n\t1, // [1:1] is the sub-list for extension type_name\n\t1, // [1:1] is the sub-list for extension extendee\n\t0, // [0:1] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_dns_monitor_proto_init() }\nfunc file_private_location_v1_dns_monitor_proto_init() {\n\tif File_private_location_v1_dns_monitor_proto != nil {\n\t\treturn\n\t}\n\tfile_private_location_v1_assertions_proto_init()\n\tfile_private_location_v1_dns_monitor_proto_msgTypes[0].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_dns_monitor_proto_rawDesc), len(file_private_location_v1_dns_monitor_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   1,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_dns_monitor_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_dns_monitor_proto_depIdxs,\n\t\tMessageInfos:      file_private_location_v1_dns_monitor_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_dns_monitor_proto = out.File\n\tfile_private_location_v1_dns_monitor_proto_goTypes = nil\n\tfile_private_location_v1_dns_monitor_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/private-location/proto/private_location/v1/http_monitor.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/http_monitor.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Headers struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey           string                 `protobuf:\"bytes,1,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tValue         string                 `protobuf:\"bytes,2,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Headers) Reset() {\n\t*x = Headers{}\n\tmi := &file_private_location_v1_http_monitor_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Headers) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Headers) ProtoMessage() {}\n\nfunc (x *Headers) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_http_monitor_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Headers.ProtoReflect.Descriptor instead.\nfunc (*Headers) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_http_monitor_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Headers) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *Headers) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\ntype HTTPMonitor struct {\n\tstate                protoimpl.MessageState `protogen:\"open.v1\"`\n\tId                   string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tUrl                  string                 `protobuf:\"bytes,2,opt,name=url,proto3\" json:\"url,omitempty\"`\n\tPeriodicity          string                 `protobuf:\"bytes,3,opt,name=periodicity,proto3\" json:\"periodicity,omitempty\"`\n\tMethod               string                 `protobuf:\"bytes,4,opt,name=method,proto3\" json:\"method,omitempty\"`\n\tBody                 string                 `protobuf:\"bytes,5,opt,name=body,proto3\" json:\"body,omitempty\"`\n\tTimeout              int64                  `protobuf:\"varint,6,opt,name=timeout,proto3\" json:\"timeout,omitempty\"`\n\tDegradedAt           *int64                 `protobuf:\"varint,7,opt,name=degraded_at,json=degradedAt,proto3,oneof\" json:\"degraded_at,omitempty\"`\n\tRetry                int64                  `protobuf:\"varint,8,opt,name=retry,proto3\" json:\"retry,omitempty\"`\n\tFollowRedirects      bool                   `protobuf:\"varint,9,opt,name=follow_redirects,json=followRedirects,proto3\" json:\"follow_redirects,omitempty\"`\n\tHeaders              []*Headers             `protobuf:\"bytes,10,rep,name=headers,proto3\" json:\"headers,omitempty\"`\n\tStatusCodeAssertions []*StatusCodeAssertion `protobuf:\"bytes,11,rep,name=status_code_assertions,json=statusCodeAssertions,proto3\" json:\"status_code_assertions,omitempty\"`\n\tBodyAssertions       []*BodyAssertion       `protobuf:\"bytes,12,rep,name=body_assertions,json=bodyAssertions,proto3\" json:\"body_assertions,omitempty\"`\n\tHeaderAssertions     []*HeaderAssertion     `protobuf:\"bytes,13,rep,name=header_assertions,json=headerAssertions,proto3\" json:\"header_assertions,omitempty\"`\n\tunknownFields        protoimpl.UnknownFields\n\tsizeCache            protoimpl.SizeCache\n}\n\nfunc (x *HTTPMonitor) Reset() {\n\t*x = HTTPMonitor{}\n\tmi := &file_private_location_v1_http_monitor_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPMonitor) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPMonitor) ProtoMessage() {}\n\nfunc (x *HTTPMonitor) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_http_monitor_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPMonitor.ProtoReflect.Descriptor instead.\nfunc (*HTTPMonitor) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_http_monitor_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *HTTPMonitor) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetUrl() string {\n\tif x != nil {\n\t\treturn x.Url\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetPeriodicity() string {\n\tif x != nil {\n\t\treturn x.Periodicity\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetMethod() string {\n\tif x != nil {\n\t\treturn x.Method\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetBody() string {\n\tif x != nil {\n\t\treturn x.Body\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPMonitor) GetTimeout() int64 {\n\tif x != nil {\n\t\treturn x.Timeout\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPMonitor) GetDegradedAt() int64 {\n\tif x != nil && x.DegradedAt != nil {\n\t\treturn *x.DegradedAt\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPMonitor) GetRetry() int64 {\n\tif x != nil {\n\t\treturn x.Retry\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPMonitor) GetFollowRedirects() bool {\n\tif x != nil {\n\t\treturn x.FollowRedirects\n\t}\n\treturn false\n}\n\nfunc (x *HTTPMonitor) GetHeaders() []*Headers {\n\tif x != nil {\n\t\treturn x.Headers\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPMonitor) GetStatusCodeAssertions() []*StatusCodeAssertion {\n\tif x != nil {\n\t\treturn x.StatusCodeAssertions\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPMonitor) GetBodyAssertions() []*BodyAssertion {\n\tif x != nil {\n\t\treturn x.BodyAssertions\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPMonitor) GetHeaderAssertions() []*HeaderAssertion {\n\tif x != nil {\n\t\treturn x.HeaderAssertions\n\t}\n\treturn nil\n}\n\nvar File_private_location_v1_http_monitor_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_http_monitor_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"&private_location/v1/http_monitor.proto\\x12\\x13private_location.v1\\x1a$private_location/v1/assertions.proto\\\"1\\n\" +\n\t\"\\aHeaders\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value\\\"\\xc6\\x04\\n\" +\n\t\"\\vHTTPMonitor\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x10\\n\" +\n\t\"\\x03url\\x18\\x02 \\x01(\\tR\\x03url\\x12 \\n\" +\n\t\"\\vperiodicity\\x18\\x03 \\x01(\\tR\\vperiodicity\\x12\\x16\\n\" +\n\t\"\\x06method\\x18\\x04 \\x01(\\tR\\x06method\\x12\\x12\\n\" +\n\t\"\\x04body\\x18\\x05 \\x01(\\tR\\x04body\\x12\\x18\\n\" +\n\t\"\\atimeout\\x18\\x06 \\x01(\\x03R\\atimeout\\x12$\\n\" +\n\t\"\\vdegraded_at\\x18\\a \\x01(\\x03H\\x00R\\n\" +\n\t\"degradedAt\\x88\\x01\\x01\\x12\\x14\\n\" +\n\t\"\\x05retry\\x18\\b \\x01(\\x03R\\x05retry\\x12)\\n\" +\n\t\"\\x10follow_redirects\\x18\\t \\x01(\\bR\\x0ffollowRedirects\\x126\\n\" +\n\t\"\\aheaders\\x18\\n\" +\n\t\" \\x03(\\v2\\x1c.private_location.v1.HeadersR\\aheaders\\x12^\\n\" +\n\t\"\\x16status_code_assertions\\x18\\v \\x03(\\v2(.private_location.v1.StatusCodeAssertionR\\x14statusCodeAssertions\\x12K\\n\" +\n\t\"\\x0fbody_assertions\\x18\\f \\x03(\\v2\\\".private_location.v1.BodyAssertionR\\x0ebodyAssertions\\x12Q\\n\" +\n\t\"\\x11header_assertions\\x18\\r \\x03(\\v2$.private_location.v1.HeaderAssertionR\\x10headerAssertionsB\\x0e\\n\" +\n\t\"\\f_degraded_atBJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_http_monitor_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_http_monitor_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_http_monitor_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_http_monitor_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_http_monitor_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_http_monitor_proto_rawDesc), len(file_private_location_v1_http_monitor_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_http_monitor_proto_rawDescData\n}\n\nvar file_private_location_v1_http_monitor_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_private_location_v1_http_monitor_proto_goTypes = []any{\n\t(*Headers)(nil),             // 0: private_location.v1.Headers\n\t(*HTTPMonitor)(nil),         // 1: private_location.v1.HTTPMonitor\n\t(*StatusCodeAssertion)(nil), // 2: private_location.v1.StatusCodeAssertion\n\t(*BodyAssertion)(nil),       // 3: private_location.v1.BodyAssertion\n\t(*HeaderAssertion)(nil),     // 4: private_location.v1.HeaderAssertion\n}\nvar file_private_location_v1_http_monitor_proto_depIdxs = []int32{\n\t0, // 0: private_location.v1.HTTPMonitor.headers:type_name -> private_location.v1.Headers\n\t2, // 1: private_location.v1.HTTPMonitor.status_code_assertions:type_name -> private_location.v1.StatusCodeAssertion\n\t3, // 2: private_location.v1.HTTPMonitor.body_assertions:type_name -> private_location.v1.BodyAssertion\n\t4, // 3: private_location.v1.HTTPMonitor.header_assertions:type_name -> private_location.v1.HeaderAssertion\n\t4, // [4:4] is the sub-list for method output_type\n\t4, // [4:4] is the sub-list for method input_type\n\t4, // [4:4] is the sub-list for extension type_name\n\t4, // [4:4] is the sub-list for extension extendee\n\t0, // [0:4] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_http_monitor_proto_init() }\nfunc file_private_location_v1_http_monitor_proto_init() {\n\tif File_private_location_v1_http_monitor_proto != nil {\n\t\treturn\n\t}\n\tfile_private_location_v1_assertions_proto_init()\n\tfile_private_location_v1_http_monitor_proto_msgTypes[1].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_http_monitor_proto_rawDesc), len(file_private_location_v1_http_monitor_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_http_monitor_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_http_monitor_proto_depIdxs,\n\t\tMessageInfos:      file_private_location_v1_http_monitor_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_http_monitor_proto = out.File\n\tfile_private_location_v1_http_monitor_proto_goTypes = nil\n\tfile_private_location_v1_http_monitor_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/private-location/proto/private_location/v1/private_location.connect.go",
    "content": "// Code generated by protoc-gen-connect-go. DO NOT EDIT.\n//\n// Source: private_location/v1/private_location.proto\n\npackage v1\n\nimport (\n\tconnect \"connectrpc.com/connect\"\n\tcontext \"context\"\n\terrors \"errors\"\n\thttp \"net/http\"\n\tstrings \"strings\"\n)\n\n// This is a compile-time assertion to ensure that this generated file and the connect package are\n// compatible. If you get a compiler error that this constant is not defined, this code was\n// generated with a version of connect newer than the one compiled into your binary. You can fix the\n// problem by either regenerating this code with an older version of connect or updating the connect\n// version compiled into your binary.\nconst _ = connect.IsAtLeastVersion1_13_0\n\nconst (\n\t// PrivateLocationServiceName is the fully-qualified name of the PrivateLocationService service.\n\tPrivateLocationServiceName = \"private_location.v1.PrivateLocationService\"\n)\n\n// These constants are the fully-qualified names of the RPCs defined in this package. They're\n// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.\n//\n// Note that these are different from the fully-qualified method names used by\n// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to\n// reflection-formatted method names, remove the leading slash and convert the remaining slash to a\n// period.\nconst (\n\t// PrivateLocationServiceMonitorsProcedure is the fully-qualified name of the\n\t// PrivateLocationService's Monitors RPC.\n\tPrivateLocationServiceMonitorsProcedure = \"/private_location.v1.PrivateLocationService/Monitors\"\n\t// PrivateLocationServiceIngestTCPProcedure is the fully-qualified name of the\n\t// PrivateLocationService's IngestTCP RPC.\n\tPrivateLocationServiceIngestTCPProcedure = \"/private_location.v1.PrivateLocationService/IngestTCP\"\n\t// PrivateLocationServiceIngestHTTPProcedure is the fully-qualified name of the\n\t// PrivateLocationService's IngestHTTP RPC.\n\tPrivateLocationServiceIngestHTTPProcedure = \"/private_location.v1.PrivateLocationService/IngestHTTP\"\n\t// PrivateLocationServiceIngestDNSProcedure is the fully-qualified name of the\n\t// PrivateLocationService's IngestDNS RPC.\n\tPrivateLocationServiceIngestDNSProcedure = \"/private_location.v1.PrivateLocationService/IngestDNS\"\n)\n\n// PrivateLocationServiceClient is a client for the private_location.v1.PrivateLocationService\n// service.\ntype PrivateLocationServiceClient interface {\n\tMonitors(context.Context, *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error)\n\tIngestTCP(context.Context, *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error)\n\tIngestHTTP(context.Context, *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error)\n\tIngestDNS(context.Context, *connect.Request[IngestDNSRequest]) (*connect.Response[IngestDNSResponse], error)\n}\n\n// NewPrivateLocationServiceClient constructs a client for the\n// private_location.v1.PrivateLocationService service. By default, it uses the Connect protocol with\n// the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed requests. To use\n// the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or connect.WithGRPCWeb() options.\n//\n// The URL supplied here should be the base URL for the Connect or gRPC server (for example,\n// http://api.acme.com or https://acme.com/grpc).\nfunc NewPrivateLocationServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PrivateLocationServiceClient {\n\tbaseURL = strings.TrimRight(baseURL, \"/\")\n\tprivateLocationServiceMethods := File_private_location_v1_private_location_proto.Services().ByName(\"PrivateLocationService\").Methods()\n\treturn &privateLocationServiceClient{\n\t\tmonitors: connect.NewClient[MonitorsRequest, MonitorsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+PrivateLocationServiceMonitorsProcedure,\n\t\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"Monitors\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tingestTCP: connect.NewClient[IngestTCPRequest, IngestTCPResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+PrivateLocationServiceIngestTCPProcedure,\n\t\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestTCP\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tingestHTTP: connect.NewClient[IngestHTTPRequest, IngestHTTPResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+PrivateLocationServiceIngestHTTPProcedure,\n\t\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestHTTP\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tingestDNS: connect.NewClient[IngestDNSRequest, IngestDNSResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+PrivateLocationServiceIngestDNSProcedure,\n\t\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestDNS\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t}\n}\n\n// privateLocationServiceClient implements PrivateLocationServiceClient.\ntype privateLocationServiceClient struct {\n\tmonitors   *connect.Client[MonitorsRequest, MonitorsResponse]\n\tingestTCP  *connect.Client[IngestTCPRequest, IngestTCPResponse]\n\tingestHTTP *connect.Client[IngestHTTPRequest, IngestHTTPResponse]\n\tingestDNS  *connect.Client[IngestDNSRequest, IngestDNSResponse]\n}\n\n// Monitors calls private_location.v1.PrivateLocationService.Monitors.\nfunc (c *privateLocationServiceClient) Monitors(ctx context.Context, req *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error) {\n\treturn c.monitors.CallUnary(ctx, req)\n}\n\n// IngestTCP calls private_location.v1.PrivateLocationService.IngestTCP.\nfunc (c *privateLocationServiceClient) IngestTCP(ctx context.Context, req *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error) {\n\treturn c.ingestTCP.CallUnary(ctx, req)\n}\n\n// IngestHTTP calls private_location.v1.PrivateLocationService.IngestHTTP.\nfunc (c *privateLocationServiceClient) IngestHTTP(ctx context.Context, req *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error) {\n\treturn c.ingestHTTP.CallUnary(ctx, req)\n}\n\n// IngestDNS calls private_location.v1.PrivateLocationService.IngestDNS.\nfunc (c *privateLocationServiceClient) IngestDNS(ctx context.Context, req *connect.Request[IngestDNSRequest]) (*connect.Response[IngestDNSResponse], error) {\n\treturn c.ingestDNS.CallUnary(ctx, req)\n}\n\n// PrivateLocationServiceHandler is an implementation of the\n// private_location.v1.PrivateLocationService service.\ntype PrivateLocationServiceHandler interface {\n\tMonitors(context.Context, *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error)\n\tIngestTCP(context.Context, *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error)\n\tIngestHTTP(context.Context, *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error)\n\tIngestDNS(context.Context, *connect.Request[IngestDNSRequest]) (*connect.Response[IngestDNSResponse], error)\n}\n\n// NewPrivateLocationServiceHandler builds an HTTP handler from the service implementation. It\n// returns the path on which to mount the handler and the handler itself.\n//\n// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf\n// and JSON codecs. They also support gzip compression.\nfunc NewPrivateLocationServiceHandler(svc PrivateLocationServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {\n\tprivateLocationServiceMethods := File_private_location_v1_private_location_proto.Services().ByName(\"PrivateLocationService\").Methods()\n\tprivateLocationServiceMonitorsHandler := connect.NewUnaryHandler(\n\t\tPrivateLocationServiceMonitorsProcedure,\n\t\tsvc.Monitors,\n\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"Monitors\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tprivateLocationServiceIngestTCPHandler := connect.NewUnaryHandler(\n\t\tPrivateLocationServiceIngestTCPProcedure,\n\t\tsvc.IngestTCP,\n\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestTCP\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tprivateLocationServiceIngestHTTPHandler := connect.NewUnaryHandler(\n\t\tPrivateLocationServiceIngestHTTPProcedure,\n\t\tsvc.IngestHTTP,\n\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestHTTP\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tprivateLocationServiceIngestDNSHandler := connect.NewUnaryHandler(\n\t\tPrivateLocationServiceIngestDNSProcedure,\n\t\tsvc.IngestDNS,\n\t\tconnect.WithSchema(privateLocationServiceMethods.ByName(\"IngestDNS\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\treturn \"/private_location.v1.PrivateLocationService/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase PrivateLocationServiceMonitorsProcedure:\n\t\t\tprivateLocationServiceMonitorsHandler.ServeHTTP(w, r)\n\t\tcase PrivateLocationServiceIngestTCPProcedure:\n\t\t\tprivateLocationServiceIngestTCPHandler.ServeHTTP(w, r)\n\t\tcase PrivateLocationServiceIngestHTTPProcedure:\n\t\t\tprivateLocationServiceIngestHTTPHandler.ServeHTTP(w, r)\n\t\tcase PrivateLocationServiceIngestDNSProcedure:\n\t\t\tprivateLocationServiceIngestDNSHandler.ServeHTTP(w, r)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n}\n\n// UnimplementedPrivateLocationServiceHandler returns CodeUnimplemented from all methods.\ntype UnimplementedPrivateLocationServiceHandler struct{}\n\nfunc (UnimplementedPrivateLocationServiceHandler) Monitors(context.Context, *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"private_location.v1.PrivateLocationService.Monitors is not implemented\"))\n}\n\nfunc (UnimplementedPrivateLocationServiceHandler) IngestTCP(context.Context, *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"private_location.v1.PrivateLocationService.IngestTCP is not implemented\"))\n}\n\nfunc (UnimplementedPrivateLocationServiceHandler) IngestHTTP(context.Context, *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"private_location.v1.PrivateLocationService.IngestHTTP is not implemented\"))\n}\n\nfunc (UnimplementedPrivateLocationServiceHandler) IngestDNS(context.Context, *connect.Request[IngestDNSRequest]) (*connect.Response[IngestDNSResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"private_location.v1.PrivateLocationService.IngestDNS is not implemented\"))\n}\n"
  },
  {
    "path": "apps/private-location/proto/private_location/v1/private_location.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/private_location.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\t_ \"google.golang.org/protobuf/types/known/structpb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype MonitorsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MonitorsRequest) Reset() {\n\t*x = MonitorsRequest{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MonitorsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MonitorsRequest) ProtoMessage() {}\n\nfunc (x *MonitorsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MonitorsRequest.ProtoReflect.Descriptor instead.\nfunc (*MonitorsRequest) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{0}\n}\n\ntype MonitorsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tHttpMonitors  []*HTTPMonitor         `protobuf:\"bytes,1,rep,name=http_monitors,json=httpMonitors,proto3\" json:\"http_monitors,omitempty\"`\n\tTcpMonitors   []*TCPMonitor          `protobuf:\"bytes,2,rep,name=tcp_monitors,json=tcpMonitors,proto3\" json:\"tcp_monitors,omitempty\"`\n\tDnsMonitors   []*DNSMonitor          `protobuf:\"bytes,3,rep,name=dns_monitors,json=dnsMonitors,proto3\" json:\"dns_monitors,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MonitorsResponse) Reset() {\n\t*x = MonitorsResponse{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MonitorsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MonitorsResponse) ProtoMessage() {}\n\nfunc (x *MonitorsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MonitorsResponse.ProtoReflect.Descriptor instead.\nfunc (*MonitorsResponse) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *MonitorsResponse) GetHttpMonitors() []*HTTPMonitor {\n\tif x != nil {\n\t\treturn x.HttpMonitors\n\t}\n\treturn nil\n}\n\nfunc (x *MonitorsResponse) GetTcpMonitors() []*TCPMonitor {\n\tif x != nil {\n\t\treturn x.TcpMonitors\n\t}\n\treturn nil\n}\n\nfunc (x *MonitorsResponse) GetDnsMonitors() []*DNSMonitor {\n\tif x != nil {\n\t\treturn x.DnsMonitors\n\t}\n\treturn nil\n}\n\ntype IngestTCPRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tMonitorId     string                 `protobuf:\"bytes,2,opt,name=monitorId,proto3\" json:\"monitorId,omitempty\"`\n\tLatency       int64                  `protobuf:\"varint,3,opt,name=latency,proto3\" json:\"latency,omitempty\"`\n\tTimestamp     int64                  `protobuf:\"varint,4,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tCronTimestamp int64                  `protobuf:\"varint,5,opt,name=cronTimestamp,proto3\" json:\"cronTimestamp,omitempty\"`\n\tUri           string                 `protobuf:\"bytes,6,opt,name=uri,proto3\" json:\"uri,omitempty\"`\n\tMessage       string                 `protobuf:\"bytes,7,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tRequestStatus string                 `protobuf:\"bytes,8,opt,name=requestStatus,proto3\" json:\"requestStatus,omitempty\"`\n\tError         int64                  `protobuf:\"varint,9,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tTiming        string                 `protobuf:\"bytes,10,opt,name=timing,proto3\" json:\"timing,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestTCPRequest) Reset() {\n\t*x = IngestTCPRequest{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestTCPRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestTCPRequest) ProtoMessage() {}\n\nfunc (x *IngestTCPRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestTCPRequest.ProtoReflect.Descriptor instead.\nfunc (*IngestTCPRequest) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *IngestTCPRequest) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetMonitorId() string {\n\tif x != nil {\n\t\treturn x.MonitorId\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetLatency() int64 {\n\tif x != nil {\n\t\treturn x.Latency\n\t}\n\treturn 0\n}\n\nfunc (x *IngestTCPRequest) GetTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestTCPRequest) GetCronTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.CronTimestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestTCPRequest) GetUri() string {\n\tif x != nil {\n\t\treturn x.Uri\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetRequestStatus() string {\n\tif x != nil {\n\t\treturn x.RequestStatus\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestTCPRequest) GetError() int64 {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn 0\n}\n\nfunc (x *IngestTCPRequest) GetTiming() string {\n\tif x != nil {\n\t\treturn x.Timing\n\t}\n\treturn \"\"\n}\n\ntype IngestTCPResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestTCPResponse) Reset() {\n\t*x = IngestTCPResponse{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestTCPResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestTCPResponse) ProtoMessage() {}\n\nfunc (x *IngestTCPResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestTCPResponse.ProtoReflect.Descriptor instead.\nfunc (*IngestTCPResponse) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{3}\n}\n\ntype IngestHTTPRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tMonitorId     string                 `protobuf:\"bytes,2,opt,name=monitorId,proto3\" json:\"monitorId,omitempty\"`\n\tLatency       int64                  `protobuf:\"varint,3,opt,name=latency,proto3\" json:\"latency,omitempty\"`\n\tTimestamp     int64                  `protobuf:\"varint,4,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tCronTimestamp int64                  `protobuf:\"varint,5,opt,name=cronTimestamp,proto3\" json:\"cronTimestamp,omitempty\"`\n\tUrl           string                 `protobuf:\"bytes,6,opt,name=url,proto3\" json:\"url,omitempty\"`\n\tRequestStatus string                 `protobuf:\"bytes,7,opt,name=requestStatus,proto3\" json:\"requestStatus,omitempty\"`\n\tMessage       string                 `protobuf:\"bytes,8,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tBody          string                 `protobuf:\"bytes,9,opt,name=body,proto3\" json:\"body,omitempty\"`\n\tHeaders       string                 `protobuf:\"bytes,10,opt,name=headers,proto3\" json:\"headers,omitempty\"`\n\tTiming        string                 `protobuf:\"bytes,11,opt,name=timing,proto3\" json:\"timing,omitempty\"`\n\tStatusCode    int64                  `protobuf:\"varint,12,opt,name=statusCode,proto3\" json:\"statusCode,omitempty\"`\n\tError         int64                  `protobuf:\"varint,13,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestHTTPRequest) Reset() {\n\t*x = IngestHTTPRequest{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestHTTPRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestHTTPRequest) ProtoMessage() {}\n\nfunc (x *IngestHTTPRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestHTTPRequest.ProtoReflect.Descriptor instead.\nfunc (*IngestHTTPRequest) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *IngestHTTPRequest) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetMonitorId() string {\n\tif x != nil {\n\t\treturn x.MonitorId\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetLatency() int64 {\n\tif x != nil {\n\t\treturn x.Latency\n\t}\n\treturn 0\n}\n\nfunc (x *IngestHTTPRequest) GetTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestHTTPRequest) GetCronTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.CronTimestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestHTTPRequest) GetUrl() string {\n\tif x != nil {\n\t\treturn x.Url\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetRequestStatus() string {\n\tif x != nil {\n\t\treturn x.RequestStatus\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetBody() string {\n\tif x != nil {\n\t\treturn x.Body\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetHeaders() string {\n\tif x != nil {\n\t\treturn x.Headers\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetTiming() string {\n\tif x != nil {\n\t\treturn x.Timing\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestHTTPRequest) GetStatusCode() int64 {\n\tif x != nil {\n\t\treturn x.StatusCode\n\t}\n\treturn 0\n}\n\nfunc (x *IngestHTTPRequest) GetError() int64 {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn 0\n}\n\ntype IngestHTTPResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestHTTPResponse) Reset() {\n\t*x = IngestHTTPResponse{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestHTTPResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestHTTPResponse) ProtoMessage() {}\n\nfunc (x *IngestHTTPResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestHTTPResponse.ProtoReflect.Descriptor instead.\nfunc (*IngestHTTPResponse) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{5}\n}\n\ntype Records struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tRecord        []string               `protobuf:\"bytes,1,rep,name=record,proto3\" json:\"record,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Records) Reset() {\n\t*x = Records{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Records) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Records) ProtoMessage() {}\n\nfunc (x *Records) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Records.ProtoReflect.Descriptor instead.\nfunc (*Records) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *Records) GetRecord() []string {\n\tif x != nil {\n\t\treturn x.Record\n\t}\n\treturn nil\n}\n\ntype IngestDNSRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tMonitorId     string                 `protobuf:\"bytes,2,opt,name=monitorId,proto3\" json:\"monitorId,omitempty\"`\n\tLatency       int64                  `protobuf:\"varint,3,opt,name=latency,proto3\" json:\"latency,omitempty\"`\n\tTimestamp     int64                  `protobuf:\"varint,4,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tCronTimestamp int64                  `protobuf:\"varint,5,opt,name=cronTimestamp,proto3\" json:\"cronTimestamp,omitempty\"`\n\tUri           string                 `protobuf:\"bytes,6,opt,name=uri,proto3\" json:\"uri,omitempty\"`\n\tRequestStatus string                 `protobuf:\"bytes,7,opt,name=requestStatus,proto3\" json:\"requestStatus,omitempty\"`\n\tMessage       string                 `protobuf:\"bytes,8,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tRecords       map[string]*Records    `protobuf:\"bytes,9,rep,name=records,proto3\" json:\"records,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tTiming        string                 `protobuf:\"bytes,10,opt,name=timing,proto3\" json:\"timing,omitempty\"`\n\tError         int64                  `protobuf:\"varint,11,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestDNSRequest) Reset() {\n\t*x = IngestDNSRequest{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestDNSRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestDNSRequest) ProtoMessage() {}\n\nfunc (x *IngestDNSRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestDNSRequest.ProtoReflect.Descriptor instead.\nfunc (*IngestDNSRequest) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *IngestDNSRequest) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetMonitorId() string {\n\tif x != nil {\n\t\treturn x.MonitorId\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetLatency() int64 {\n\tif x != nil {\n\t\treturn x.Latency\n\t}\n\treturn 0\n}\n\nfunc (x *IngestDNSRequest) GetTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestDNSRequest) GetCronTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.CronTimestamp\n\t}\n\treturn 0\n}\n\nfunc (x *IngestDNSRequest) GetUri() string {\n\tif x != nil {\n\t\treturn x.Uri\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetRequestStatus() string {\n\tif x != nil {\n\t\treturn x.RequestStatus\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetRecords() map[string]*Records {\n\tif x != nil {\n\t\treturn x.Records\n\t}\n\treturn nil\n}\n\nfunc (x *IngestDNSRequest) GetTiming() string {\n\tif x != nil {\n\t\treturn x.Timing\n\t}\n\treturn \"\"\n}\n\nfunc (x *IngestDNSRequest) GetError() int64 {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn 0\n}\n\ntype IngestDNSResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IngestDNSResponse) Reset() {\n\t*x = IngestDNSResponse{}\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IngestDNSResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IngestDNSResponse) ProtoMessage() {}\n\nfunc (x *IngestDNSResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_private_location_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IngestDNSResponse.ProtoReflect.Descriptor instead.\nfunc (*IngestDNSResponse) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_private_location_proto_rawDescGZIP(), []int{8}\n}\n\nvar File_private_location_v1_private_location_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_private_location_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"*private_location/v1/private_location.proto\\x12\\x13private_location.v1\\x1a\\x1cgoogle/protobuf/struct.proto\\x1a%private_location/v1/dns_monitor.proto\\x1a&private_location/v1/http_monitor.proto\\x1a%private_location/v1/tcp_monitor.proto\\\"\\x11\\n\" +\n\t\"\\x0fMonitorsRequest\\\"\\xe1\\x01\\n\" +\n\t\"\\x10MonitorsResponse\\x12E\\n\" +\n\t\"\\rhttp_monitors\\x18\\x01 \\x03(\\v2 .private_location.v1.HTTPMonitorR\\fhttpMonitors\\x12B\\n\" +\n\t\"\\ftcp_monitors\\x18\\x02 \\x03(\\v2\\x1f.private_location.v1.TCPMonitorR\\vtcpMonitors\\x12B\\n\" +\n\t\"\\fdns_monitors\\x18\\x03 \\x03(\\v2\\x1f.private_location.v1.DNSMonitorR\\vdnsMonitors\\\"\\x9e\\x02\\n\" +\n\t\"\\x10IngestTCPRequest\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x1c\\n\" +\n\t\"\\tmonitorId\\x18\\x02 \\x01(\\tR\\tmonitorId\\x12\\x18\\n\" +\n\t\"\\alatency\\x18\\x03 \\x01(\\x03R\\alatency\\x12\\x1c\\n\" +\n\t\"\\ttimestamp\\x18\\x04 \\x01(\\x03R\\ttimestamp\\x12$\\n\" +\n\t\"\\rcronTimestamp\\x18\\x05 \\x01(\\x03R\\rcronTimestamp\\x12\\x10\\n\" +\n\t\"\\x03uri\\x18\\x06 \\x01(\\tR\\x03uri\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\a \\x01(\\tR\\amessage\\x12$\\n\" +\n\t\"\\rrequestStatus\\x18\\b \\x01(\\tR\\rrequestStatus\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\t \\x01(\\x03R\\x05error\\x12\\x16\\n\" +\n\t\"\\x06timing\\x18\\n\" +\n\t\" \\x01(\\tR\\x06timing\\\"\\x13\\n\" +\n\t\"\\x11IngestTCPResponse\\\"\\xed\\x02\\n\" +\n\t\"\\x11IngestHTTPRequest\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x1c\\n\" +\n\t\"\\tmonitorId\\x18\\x02 \\x01(\\tR\\tmonitorId\\x12\\x18\\n\" +\n\t\"\\alatency\\x18\\x03 \\x01(\\x03R\\alatency\\x12\\x1c\\n\" +\n\t\"\\ttimestamp\\x18\\x04 \\x01(\\x03R\\ttimestamp\\x12$\\n\" +\n\t\"\\rcronTimestamp\\x18\\x05 \\x01(\\x03R\\rcronTimestamp\\x12\\x10\\n\" +\n\t\"\\x03url\\x18\\x06 \\x01(\\tR\\x03url\\x12$\\n\" +\n\t\"\\rrequestStatus\\x18\\a \\x01(\\tR\\rrequestStatus\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\b \\x01(\\tR\\amessage\\x12\\x12\\n\" +\n\t\"\\x04body\\x18\\t \\x01(\\tR\\x04body\\x12\\x18\\n\" +\n\t\"\\aheaders\\x18\\n\" +\n\t\" \\x01(\\tR\\aheaders\\x12\\x16\\n\" +\n\t\"\\x06timing\\x18\\v \\x01(\\tR\\x06timing\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"statusCode\\x18\\f \\x01(\\x03R\\n\" +\n\t\"statusCode\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\r \\x01(\\x03R\\x05error\\\"\\x14\\n\" +\n\t\"\\x12IngestHTTPResponse\\\"!\\n\" +\n\t\"\\aRecords\\x12\\x16\\n\" +\n\t\"\\x06record\\x18\\x01 \\x03(\\tR\\x06record\\\"\\xc6\\x03\\n\" +\n\t\"\\x10IngestDNSRequest\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x1c\\n\" +\n\t\"\\tmonitorId\\x18\\x02 \\x01(\\tR\\tmonitorId\\x12\\x18\\n\" +\n\t\"\\alatency\\x18\\x03 \\x01(\\x03R\\alatency\\x12\\x1c\\n\" +\n\t\"\\ttimestamp\\x18\\x04 \\x01(\\x03R\\ttimestamp\\x12$\\n\" +\n\t\"\\rcronTimestamp\\x18\\x05 \\x01(\\x03R\\rcronTimestamp\\x12\\x10\\n\" +\n\t\"\\x03uri\\x18\\x06 \\x01(\\tR\\x03uri\\x12$\\n\" +\n\t\"\\rrequestStatus\\x18\\a \\x01(\\tR\\rrequestStatus\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\b \\x01(\\tR\\amessage\\x12L\\n\" +\n\t\"\\arecords\\x18\\t \\x03(\\v22.private_location.v1.IngestDNSRequest.RecordsEntryR\\arecords\\x12\\x16\\n\" +\n\t\"\\x06timing\\x18\\n\" +\n\t\" \\x01(\\tR\\x06timing\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\v \\x01(\\x03R\\x05error\\x1aX\\n\" +\n\t\"\\fRecordsEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x122\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\v2\\x1c.private_location.v1.RecordsR\\x05value:\\x028\\x01\\\"\\x13\\n\" +\n\t\"\\x11IngestDNSResponse2\\x90\\x03\\n\" +\n\t\"\\x16PrivateLocationService\\x12Y\\n\" +\n\t\"\\bMonitors\\x12$.private_location.v1.MonitorsRequest\\x1a%.private_location.v1.MonitorsResponse\\\"\\x00\\x12\\\\\\n\" +\n\t\"\\tIngestTCP\\x12%.private_location.v1.IngestTCPRequest\\x1a&.private_location.v1.IngestTCPResponse\\\"\\x00\\x12_\\n\" +\n\t\"\\n\" +\n\t\"IngestHTTP\\x12&.private_location.v1.IngestHTTPRequest\\x1a'.private_location.v1.IngestHTTPResponse\\\"\\x00\\x12\\\\\\n\" +\n\t\"\\tIngestDNS\\x12%.private_location.v1.IngestDNSRequest\\x1a&.private_location.v1.IngestDNSResponse\\\"\\x00BJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_private_location_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_private_location_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_private_location_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_private_location_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_private_location_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_private_location_proto_rawDesc), len(file_private_location_v1_private_location_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_private_location_proto_rawDescData\n}\n\nvar file_private_location_v1_private_location_proto_msgTypes = make([]protoimpl.MessageInfo, 10)\nvar file_private_location_v1_private_location_proto_goTypes = []any{\n\t(*MonitorsRequest)(nil),    // 0: private_location.v1.MonitorsRequest\n\t(*MonitorsResponse)(nil),   // 1: private_location.v1.MonitorsResponse\n\t(*IngestTCPRequest)(nil),   // 2: private_location.v1.IngestTCPRequest\n\t(*IngestTCPResponse)(nil),  // 3: private_location.v1.IngestTCPResponse\n\t(*IngestHTTPRequest)(nil),  // 4: private_location.v1.IngestHTTPRequest\n\t(*IngestHTTPResponse)(nil), // 5: private_location.v1.IngestHTTPResponse\n\t(*Records)(nil),            // 6: private_location.v1.Records\n\t(*IngestDNSRequest)(nil),   // 7: private_location.v1.IngestDNSRequest\n\t(*IngestDNSResponse)(nil),  // 8: private_location.v1.IngestDNSResponse\n\tnil,                        // 9: private_location.v1.IngestDNSRequest.RecordsEntry\n\t(*HTTPMonitor)(nil),        // 10: private_location.v1.HTTPMonitor\n\t(*TCPMonitor)(nil),         // 11: private_location.v1.TCPMonitor\n\t(*DNSMonitor)(nil),         // 12: private_location.v1.DNSMonitor\n}\nvar file_private_location_v1_private_location_proto_depIdxs = []int32{\n\t10, // 0: private_location.v1.MonitorsResponse.http_monitors:type_name -> private_location.v1.HTTPMonitor\n\t11, // 1: private_location.v1.MonitorsResponse.tcp_monitors:type_name -> private_location.v1.TCPMonitor\n\t12, // 2: private_location.v1.MonitorsResponse.dns_monitors:type_name -> private_location.v1.DNSMonitor\n\t9,  // 3: private_location.v1.IngestDNSRequest.records:type_name -> private_location.v1.IngestDNSRequest.RecordsEntry\n\t6,  // 4: private_location.v1.IngestDNSRequest.RecordsEntry.value:type_name -> private_location.v1.Records\n\t0,  // 5: private_location.v1.PrivateLocationService.Monitors:input_type -> private_location.v1.MonitorsRequest\n\t2,  // 6: private_location.v1.PrivateLocationService.IngestTCP:input_type -> private_location.v1.IngestTCPRequest\n\t4,  // 7: private_location.v1.PrivateLocationService.IngestHTTP:input_type -> private_location.v1.IngestHTTPRequest\n\t7,  // 8: private_location.v1.PrivateLocationService.IngestDNS:input_type -> private_location.v1.IngestDNSRequest\n\t1,  // 9: private_location.v1.PrivateLocationService.Monitors:output_type -> private_location.v1.MonitorsResponse\n\t3,  // 10: private_location.v1.PrivateLocationService.IngestTCP:output_type -> private_location.v1.IngestTCPResponse\n\t5,  // 11: private_location.v1.PrivateLocationService.IngestHTTP:output_type -> private_location.v1.IngestHTTPResponse\n\t8,  // 12: private_location.v1.PrivateLocationService.IngestDNS:output_type -> private_location.v1.IngestDNSResponse\n\t9,  // [9:13] is the sub-list for method output_type\n\t5,  // [5:9] is the sub-list for method input_type\n\t5,  // [5:5] is the sub-list for extension type_name\n\t5,  // [5:5] is the sub-list for extension extendee\n\t0,  // [0:5] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_private_location_proto_init() }\nfunc file_private_location_v1_private_location_proto_init() {\n\tif File_private_location_v1_private_location_proto != nil {\n\t\treturn\n\t}\n\tfile_private_location_v1_dns_monitor_proto_init()\n\tfile_private_location_v1_http_monitor_proto_init()\n\tfile_private_location_v1_tcp_monitor_proto_init()\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_private_location_proto_rawDesc), len(file_private_location_v1_private_location_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   10,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_private_location_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_private_location_proto_depIdxs,\n\t\tMessageInfos:      file_private_location_v1_private_location_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_private_location_proto = out.File\n\tfile_private_location_v1_private_location_proto_goTypes = nil\n\tfile_private_location_v1_private_location_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/private-location/proto/private_location/v1/tcp_monitor.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: private_location/v1/tcp_monitor.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype TCPMonitor struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tUri           string                 `protobuf:\"bytes,2,opt,name=uri,proto3\" json:\"uri,omitempty\"`\n\tTimeout       int64                  `protobuf:\"varint,3,opt,name=timeout,proto3\" json:\"timeout,omitempty\"`\n\tDegradedAt    *int64                 `protobuf:\"varint,4,opt,name=degraded_at,json=degradedAt,proto3,oneof\" json:\"degraded_at,omitempty\"`\n\tPeriodicity   string                 `protobuf:\"bytes,5,opt,name=periodicity,proto3\" json:\"periodicity,omitempty\"`\n\tRetry         int64                  `protobuf:\"varint,6,opt,name=retry,proto3\" json:\"retry,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TCPMonitor) Reset() {\n\t*x = TCPMonitor{}\n\tmi := &file_private_location_v1_tcp_monitor_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TCPMonitor) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TCPMonitor) ProtoMessage() {}\n\nfunc (x *TCPMonitor) ProtoReflect() protoreflect.Message {\n\tmi := &file_private_location_v1_tcp_monitor_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TCPMonitor.ProtoReflect.Descriptor instead.\nfunc (*TCPMonitor) Descriptor() ([]byte, []int) {\n\treturn file_private_location_v1_tcp_monitor_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *TCPMonitor) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *TCPMonitor) GetUri() string {\n\tif x != nil {\n\t\treturn x.Uri\n\t}\n\treturn \"\"\n}\n\nfunc (x *TCPMonitor) GetTimeout() int64 {\n\tif x != nil {\n\t\treturn x.Timeout\n\t}\n\treturn 0\n}\n\nfunc (x *TCPMonitor) GetDegradedAt() int64 {\n\tif x != nil && x.DegradedAt != nil {\n\t\treturn *x.DegradedAt\n\t}\n\treturn 0\n}\n\nfunc (x *TCPMonitor) GetPeriodicity() string {\n\tif x != nil {\n\t\treturn x.Periodicity\n\t}\n\treturn \"\"\n}\n\nfunc (x *TCPMonitor) GetRetry() int64 {\n\tif x != nil {\n\t\treturn x.Retry\n\t}\n\treturn 0\n}\n\nvar File_private_location_v1_tcp_monitor_proto protoreflect.FileDescriptor\n\nconst file_private_location_v1_tcp_monitor_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"%private_location/v1/tcp_monitor.proto\\x12\\x13private_location.v1\\\"\\xb6\\x01\\n\" +\n\t\"\\n\" +\n\t\"TCPMonitor\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x10\\n\" +\n\t\"\\x03uri\\x18\\x02 \\x01(\\tR\\x03uri\\x12\\x18\\n\" +\n\t\"\\atimeout\\x18\\x03 \\x01(\\x03R\\atimeout\\x12$\\n\" +\n\t\"\\vdegraded_at\\x18\\x04 \\x01(\\x03H\\x00R\\n\" +\n\t\"degradedAt\\x88\\x01\\x01\\x12 \\n\" +\n\t\"\\vperiodicity\\x18\\x05 \\x01(\\tR\\vperiodicity\\x12\\x14\\n\" +\n\t\"\\x05retry\\x18\\x06 \\x01(\\x03R\\x05retryB\\x0e\\n\" +\n\t\"\\f_degraded_atBJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\\x06proto3\"\n\nvar (\n\tfile_private_location_v1_tcp_monitor_proto_rawDescOnce sync.Once\n\tfile_private_location_v1_tcp_monitor_proto_rawDescData []byte\n)\n\nfunc file_private_location_v1_tcp_monitor_proto_rawDescGZIP() []byte {\n\tfile_private_location_v1_tcp_monitor_proto_rawDescOnce.Do(func() {\n\t\tfile_private_location_v1_tcp_monitor_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_tcp_monitor_proto_rawDesc), len(file_private_location_v1_tcp_monitor_proto_rawDesc)))\n\t})\n\treturn file_private_location_v1_tcp_monitor_proto_rawDescData\n}\n\nvar file_private_location_v1_tcp_monitor_proto_msgTypes = make([]protoimpl.MessageInfo, 1)\nvar file_private_location_v1_tcp_monitor_proto_goTypes = []any{\n\t(*TCPMonitor)(nil), // 0: private_location.v1.TCPMonitor\n}\nvar file_private_location_v1_tcp_monitor_proto_depIdxs = []int32{\n\t0, // [0:0] is the sub-list for method output_type\n\t0, // [0:0] is the sub-list for method input_type\n\t0, // [0:0] is the sub-list for extension type_name\n\t0, // [0:0] is the sub-list for extension extendee\n\t0, // [0:0] is the sub-list for field type_name\n}\n\nfunc init() { file_private_location_v1_tcp_monitor_proto_init() }\nfunc file_private_location_v1_tcp_monitor_proto_init() {\n\tif File_private_location_v1_tcp_monitor_proto != nil {\n\t\treturn\n\t}\n\tfile_private_location_v1_tcp_monitor_proto_msgTypes[0].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_tcp_monitor_proto_rawDesc), len(file_private_location_v1_tcp_monitor_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   1,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_private_location_v1_tcp_monitor_proto_goTypes,\n\t\tDependencyIndexes: file_private_location_v1_tcp_monitor_proto_depIdxs,\n\t\tMessageInfos:      file_private_location_v1_tcp_monitor_proto_msgTypes,\n\t}.Build()\n\tFile_private_location_v1_tcp_monitor_proto = out.File\n\tfile_private_location_v1_tcp_monitor_proto_goTypes = nil\n\tfile_private_location_v1_tcp_monitor_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "apps/railway-proxy/Dockerfile",
    "content": "FROM golang:1.26-alpine\n\n# Set the working directory\nWORKDIR /app\n\n# Copy go.mod and go.sum files (if they exist)\nCOPY go.mod ./\n\n# Copy the source code\nCOPY . .\n\n# Build the application\nRUN go build -o main .\n\n# Expose the port your app runs on\nEXPOSE 8080\n\n# Run the application\nCMD [\"./main\"]\n"
  },
  {
    "path": "apps/railway-proxy/go.mod",
    "content": "module github.com/openstatushq/openstatus/apps/railway-proxy\n\ngo 1.25.1\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.11.0\n\tgithub.com/rs/zerolog v1.34.0\n)\n\nrequire (\n\tgithub.com/bytedance/sonic v1.14.0 // indirect\n\tgithub.com/bytedance/sonic/loader v0.3.0 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.8 // indirect\n\tgithub.com/gin-contrib/sse v1.1.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.27.0 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/goccy/go-yaml v1.18.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/quic-go/qpack v0.5.1 // indirect\n\tgithub.com/quic-go/quic-go v0.54.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.3.0 // indirect\n\tgo.uber.org/mock v0.5.0 // indirect\n\tgolang.org/x/arch v0.20.0 // indirect\n\tgolang.org/x/crypto v0.40.0 // indirect\n\tgolang.org/x/mod v0.25.0 // indirect\n\tgolang.org/x/net v0.42.0 // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.27.0 // indirect\n\tgolang.org/x/tools v0.34.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.9 // indirect\n)\n"
  },
  {
    "path": "apps/railway-proxy/go.sum",
    "content": "github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=\ngithub.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=\ngithub.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=\ngithub.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=\ngithub.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=\ngithub.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=\ngithub.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=\ngithub.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=\ngithub.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=\ngithub.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=\ngithub.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=\ngithub.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=\ngithub.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=\ngithub.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=\ngithub.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=\ngithub.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=\ngithub.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=\ngo.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=\ngo.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=\ngolang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=\ngolang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=\ngolang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=\ngolang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=\ngolang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=\ngolang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=\ngolang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=\ngolang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=\ngolang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=\ngolang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=\ngolang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=\ngoogle.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=\ngoogle.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "apps/railway-proxy/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/rs/zerolog/log\"\n)\n\nfunc proxy(c *gin.Context) {\n\n\tvar targetUrl string\n\n\tregion := c.Request.Header.Get(\"railway-region\")\n\n\tswitch region {\n\tcase \"europe-west4-drams3a\":\n\t\ttargetUrl = \"http://openstatus-checker-eu-west.railway.internal:8080\"\n\tcase \"us-east4-eqdc4a\":\n\t\ttargetUrl = \"http://openstatus-checker-us-east.railway.internal:8080\"\n\tcase \"us-west2\":\n\t\ttargetUrl = \"http://openstatus-checker-us-west.railway.internal:8080\"\n\tcase \"asia-southeast1-eqsg3a\":\n\t\ttargetUrl = \"http://checker-southeast-asia.railway.internal:8080\"\n\tdefault:\n\t\tfmt.Println(\"No region\")\n\t}\n\tremote, err := url.Parse(targetUrl)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tproxy := httputil.NewSingleHostReverseProxy(remote)\n\n\tc.Request.Host = remote.Host\n\n\tproxy.ServeHTTP(c.Writer, c.Request)\n}\n\nfunc main() {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tdone := make(chan os.Signal, 1)\n\tsignal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)\n\n\tgo func() {\n\t\t<-done\n\t\tcancel()\n\t}()\n\n\tcloudProvider := env(\"CLOUD_PROVIDER\", \"railway\")\n\tregion := env(\"RAILWAY_REPLICA_REGION\", env(\"REGION\", \"local\"))\n\trouter := gin.New()\n\n\t//Create a catchall route\n\trouter.NoRoute(proxy)\n\n\trouter.GET(\"/healthz\", func(c *gin.Context) {\n\t\tc.JSON(http.StatusOK, gin.H{\"message\": \"pong\", \"region\": region, \"provider\": cloudProvider})\n\t})\n\n\thttpServer := &http.Server{\n\t\tAddr:    fmt.Sprintf(\"0.0.0.0:%s\", env(\"PORT\", \"8080\")),\n\t\tHandler: router,\n\t}\n\n\tgo func() {\n\t\tif err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to start http server\")\n\t\t\tcancel()\n\t\t}\n\t}()\n\n\t<-ctx.Done()\n\tif err := httpServer.Shutdown(ctx); err != nil {\n\t\tlog.Ctx(ctx).Error().Err(err).Msg(\"failed to shutdown http server\")\n\n\t\treturn\n\t}\n\n}\n\nfunc env(key, fallback string) string {\n\tif value, ok := os.LookupEnv(key); ok {\n\t\treturn value\n\t}\n\n\treturn fallback\n}\n"
  },
  {
    "path": "apps/screenshot-service/.dockerignore",
    "content": "# flyctl launch added from .gitignore\n**/node_modules\n**/dist\n**/.wrangler\n**/.dev.vars\n\n# Change them to your taste:\n**/package-lock.json\n**/yarn.lock\n**/pnpm-lock.yaml\n**/bun.lockb\nfly.toml\n\nnode_modules\n"
  },
  {
    "path": "apps/screenshot-service/.gitignore",
    "content": "node_modules\ndist\n.wrangler\n.dev.vars\n\n# Change them to your taste:\npackage-lock.json\nyarn.lock\npnpm-lock.yaml\nbun.lockb"
  },
  {
    "path": "apps/screenshot-service/Dockerfile",
    "content": "FROM node:22-bookworm as dep\n\nRUN npx -y playwright@1.46.0 install --with-deps\n\nRUN npm install -g bun\n\nRUN npm install -g pnpm\n\nWORKDIR /app\n\nCOPY . .\n\n# To keep the image small ;)\nRUN rm -rf /app/apps/docs &&\\\n  rm -rf /app/apps/web &&\\\n  rm -rf /app/apps/server &&\\\n  rm -rf /app/packages/api &&\\\n  rm -rf /app/packages/integrations/vercel\n\nRUN pnpm install\nEXPOSE 3000\n\nWORKDIR /app/apps/screenshot-service\nCMD [\"bun\", \"start\"]\n"
  },
  {
    "path": "apps/screenshot-service/README.md",
    "content": "# Screenshot Worker\n\nThis is not used anymore.\n\nWill be used for browser check\n"
  },
  {
    "path": "apps/screenshot-service/fly.toml",
    "content": "# fly.toml app configuration file generated for openstatus-screenshot on 2024-04-06T11:12:20+02:00\n#\n# See https://fly.io/docs/reference/configuration/ for information about how to use this file.\n#\n\napp = 'openstatus-screenshot'\nprimary_region = 'ams'\n\n[build]\n  dockerfile = './Dockerfile'\n\n[deploy]\n  strategy = 'canary'\n\n[http_service]\n  internal_port = 3000\n  force_https = true\n  auto_stop_machines = \"suspend\"\n  auto_start_machines = true\n  min_machines_running = 0\n  processes = ['app']\n\n  [http_service.concurrency]\n    type = 'requests'\n    hard_limit = 4\n    soft_limit = 2\n\n[[http_service.checks]]\n    interval = '15s'\n    timeout = '5s'\n    grace_period = '10s'\n    method = 'GET'\n    path = '/ping'\n\n[[vm]]\n  cpu_kind = 'shared'\n  cpus = 2\n  memory_mb = 2048\n"
  },
  {
    "path": "apps/screenshot-service/package.json",
    "content": "{\n  \"name\": \"@openstatus/screenshot-service\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"bun run --hot src/index.ts\",\n    \"start\": \"NODE_ENV=production bun run src/index.ts\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-s3\": \"3.550.0\",\n    \"@hono/zod-validator\": \"0.2.2\",\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:^\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"@upstash/qstash\": \"2.6.2\",\n    \"hono\": \"4.5.3\",\n    \"playwright\": \"1.46.0\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "apps/screenshot-service/src/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    R2_TOKEN: z.string().min(1),\n    R2_URL: z.string().min(1),\n    R2_ACCESS_KEY: z.string().min(1),\n    R2_SECRET_KEY: z.string().min(1),\n    HEADER_TOKEN: z.string().min(1),\n    QSTASH_SIGNING_SECRET: z.string().min(1),\n    QSTASH_NEXT_SIGNING_SECRET: z.string().min(1),\n  },\n\n  /**\n   * What object holds the environment variables at runtime. This is usually\n   * `process.env` or `import.meta.env`.\n   */\n  runtimeEnv: process.env,\n\n  /**\n   * By default, this library will feed the environment variables directly to\n   * the Zod validator.\n   *\n   * This means that if you have an empty string for a value that is supposed\n   * to be a number (e.g. `PORT=` in a \".env\" file), Zod will incorrectly flag\n   * it as a type mismatch violation. Additionally, if you have an empty string\n   * for a value that is supposed to be a string with a default value (e.g.\n   * `DOMAIN=` in an \".env\" file), the default value will never be applied.\n   *\n   * In order to solve these issues, we recommend that all new projects\n   * explicitly specify this option as true.\n   */\n  skipValidation: true,\n});\n"
  },
  {
    "path": "apps/screenshot-service/src/index.ts",
    "content": "import { PutObjectCommand, S3Client } from \"@aws-sdk/client-s3\";\nimport { zValidator } from \"@hono/zod-validator\";\nimport { Hono } from \"hono\";\nimport playwright from \"playwright\";\nimport { z } from \"zod\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { incidentTable } from \"@openstatus/db/src/schema/incidents/incident\";\nimport { Receiver } from \"@upstash/qstash\";\n\nimport { env } from \"./env\";\n\nconst S3 = new S3Client({\n  region: \"auto\",\n  endpoint: env.R2_URL,\n  credentials: {\n    accessKeyId: env.R2_ACCESS_KEY,\n    secretAccessKey: env.R2_SECRET_KEY,\n  },\n});\n\nconst receiver = new Receiver({\n  currentSigningKey: env.QSTASH_SIGNING_SECRET,\n  nextSigningKey: env.QSTASH_NEXT_SIGNING_SECRET,\n});\n\nconst app = new Hono();\n\napp.get(\"/ping\", (c) =>\n  c.json({ ping: \"pong\", region: process.env.FLY_REGION }, 200),\n);\n\napp.post(\n  \"/\",\n  zValidator(\n    \"json\",\n    z.object({\n      url: z.url(),\n      incidentId: z.number(),\n      kind: z.enum([\"incident\", \"recovery\"]),\n    }),\n  ),\n  async (c) => {\n    const signature = c.req.header(\"Upstash-Signature\");\n    // if (auth !== `Basic ${env.HEADER_TOKEN}`) {\n    //   console.error(\"Unauthorized\");\n    //   return c.text(\"Unauthorized\", 401);\n    // }\n\n    const data = c.req.valid(\"json\");\n    const isValid = receiver.verify({\n      signature: signature || \"\",\n      body: JSON.stringify(data),\n    });\n    if (!isValid) {\n      console.error(\"Unauthorized\");\n      return c.text(\"Unauthorized\", 401);\n    }\n\n    const browser = await playwright.chromium.launch({\n      headless: true, // set this to true\n    });\n\n    try {\n      const page = await browser.newPage();\n      await page.goto(data.url, { waitUntil: \"load\" });\n      const img = await page.screenshot({ fullPage: true });\n      const id = `${data.incidentId}-${Date.now()}.png`;\n      const url = `https://screenshot.openstat.us/${id}`;\n\n      await S3.send(\n        new PutObjectCommand({\n          Body: img,\n          Bucket: \"incident-screenshot\",\n          Key: id,\n          ContentType: \"image/png\",\n        }),\n      );\n\n      if (data.kind === \"incident\") {\n        await db\n          .update(incidentTable)\n          .set({ incidentScreenshotUrl: url })\n          .where(eq(incidentTable.id, data.incidentId))\n          .run();\n      }\n      if (data.kind === \"recovery\") {\n        await db\n          .update(incidentTable)\n          .set({ recoveryScreenshotUrl: url })\n          .where(eq(incidentTable.id, data.incidentId))\n          .run();\n      }\n    } catch (e) {\n      console.log(\"could not take screenshot timeout\");\n      if (data.kind === \"incident\") {\n        await db\n          .update(incidentTable)\n          .set({\n            incidentScreenshotUrl:\n              \"https://screenshot.openstat.us/err-connection-timed-out.jpg\",\n          })\n          .where(eq(incidentTable.id, data.incidentId))\n          .run();\n      }\n      //\n      console.log(e);\n    }\n\n    return c.text(\"Screenshot saved\");\n  },\n);\n\nexport default app;\n"
  },
  {
    "path": "apps/screenshot-service/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\", \"**/*.ts\"],\n  \"compilerOptions\": {}\n}\n"
  },
  {
    "path": "apps/server/.dockerignore",
    "content": "# This file is generated by Dofigen v2.7.0\n# See https://github.com/lenra-io/dofigen\n\nnode_modules\n/apps/docs\n/apps/screenshot-service\n/apps/web\n/apps/dashboard\n/apps/status-page\n/apps/workflows\n/packages/api\n/packages/integrations/vercel\n"
  },
  {
    "path": "apps/server/.gitignore",
    "content": "node_modules\n"
  },
  {
    "path": "apps/server/CONNECTRPC_SPEC.md",
    "content": "# ConnectRPC API Specification\n\n## Overview\n\nThis document specifies the implementation of a ConnectRPC API for OpenStatus server. ConnectRPC will be used for **new features only** while the existing REST API remains for current functionality.\n\n## Architecture Decisions\n\n### Transport & Protocol\n- **Protocol**: Connect protocol only (HTTP/1.1 compatible)\n- **Streaming**: Unary calls only (request-response, no streaming)\n- **Mounting**: Same port as REST, mounted at `/rpc/*` path prefix on the existing Hono app\n\n### Schema Management\n- **Approach**: Schema-first with `.proto` files\n- **Tooling**: Buf (buf.yaml, buf.gen.yaml)\n- **Location**: `packages/proto` (shared package for monorepo consumption)\n- **Package naming**: `openstatus.<domain>.v1` (e.g., `openstatus.monitor.v1`)\n\n### Code Generation Targets\n- TypeScript (`@bufbuild/protobuf` + `@connectrpc/connect`)\n- Go (for potential backend service consumers)\n\n---\n\n## Authentication & Authorization\n\n### Supported Methods\nBoth authentication methods resolve to the same workspace context:\n\n1. **API Key** (existing system)\n   - Header: `x-openstatus-key`\n   - Formats: `os_[32-char-hex]` (custom) or Unkey keys\n   - Super admin: `sa_` prefix\n\n\n### Workspace Context\n- Workspace ID inferred from authenticated credentials\n- **Super-admin override**: Tokens with `sa_` prefix can specify target workspace via `x-workspace-id` metadata header\n\n---\n\n## Error Handling\n\n### Error Model\nUse ConnectRPC error codes with Google ErrorInfo for structured details:\n\n```protobuf\n// Error codes used: NOT_FOUND, INVALID_ARGUMENT, PERMISSION_DENIED,\n// UNAUTHENTICATED, RESOURCE_EXHAUSTED, INTERNAL, UNAVAILABLE\n\n// Include ErrorInfo details:\n// - domain: \"openstatus.com\"\n// - reason: Machine-readable error reason (e.g., \"MONITOR_NOT_FOUND\")\n// - metadata: Additional context (requestId, resourceId, etc.)\n```\n\n### Mapping to Existing Errors\nReuse `OpenStatusApiError` codes, map to ConnectRPC equivalents in interceptor.\n\n---\n\n## First Service: Monitor Management\n\n### Service Definition\n\n```protobuf\nsyntax = \"proto3\";\n\npackage openstatus.monitor.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\n// MonitorService provides CRUD and operational commands for monitors.\nservice MonitorService {\n  // CreateMonitor creates a new monitor in the workspace.\n  rpc CreateMonitor(CreateMonitorRequest) returns (CreateMonitorResponse);\n\n  // GetMonitor retrieves a single monitor by ID.\n  rpc GetMonitor(GetMonitorRequest) returns (GetMonitorResponse);\n\n  // ListMonitors returns a paginated list of monitors.\n  rpc ListMonitors(ListMonitorsRequest) returns (ListMonitorsResponse);\n\n  // DeleteMonitor removes a monitor.\n  rpc DeleteMonitor(DeleteMonitorRequest) returns (DeleteMonitorResponse);\n\n  // TriggerMonitor initiates an immediate check.\n  rpc TriggerMonitor(TriggerMonitorRequest) returns (TriggerMonitorResponse);\n\n}\n```\n\n### Monitor Type Modeling\n\nSeparate message types for each monitor kind:\n\n```protobuf\n// HttpMonitor configuration for HTTP/HTTPS endpoint monitoring.\nmessage HttpMonitor {\n  // The URL to monitor (required).\n  string url = 1 [(buf.validate.field).string.uri = true];\n\n  // HTTP method to use.\n  HttpMethod method = 2;\n\n  // Request headers to include.\n  map<string, string> headers = 3;\n\n  // Request body for POST/PUT/PATCH.\n  optional string body = 4;\n\n  // Timeout in milliseconds (default: 30000).\n  int32 timeout_ms = 5 [(buf.validate.field).int32 = {gte: 1000, lte: 60000}];\n\n  // Assertions to validate the response.\n  repeated HttpAssertion assertions = 6;\n}\n\n// TcpMonitor configuration for TCP connection monitoring.\nmessage TcpMonitor {\n  // Host to connect to (required).\n  string host = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Port number (required).\n  int32 port = 2 [(buf.validate.field).int32 = {gte: 1, lte: 65535}];\n\n  // Timeout in milliseconds.\n  int32 timeout_ms = 3;\n}\n\n// DnsMonitor configuration for DNS record monitoring.\nmessage DnsMonitor {\n  // Domain name to query (required).\n  string domain = 1 [(buf.validate.field).string.hostname = true];\n\n  // DNS record type to check.\n  DnsRecordType record_type = 2;\n\n  // Expected values for the record.\n  repeated string expected_values = 3;\n}\n```\n\n### Pagination\n\nOffset-based pagination for list operations (page_token is the numeric offset):\n\n```protobuf\nmessage ListMonitorsRequest {\n  // Maximum number of monitors to return (default: 50, max: 100).\n  int32 page_size = 1 [(buf.validate.field).int32 = {gte: 1, lte: 100}];\n\n  // Token from previous response for pagination.\n  optional string page_token = 2;\n\n  // Filter by monitor status.\n  optional MonitorStatus status_filter = 3;\n\n  // Filter by monitor type.\n  optional MonitorType type_filter = 4;\n}\n\nmessage ListMonitorsResponse {\n  // The monitors in this page.\n  repeated Monitor monitors = 1;\n\n  // Token for retrieving the next page, empty if no more results.\n  string next_page_token = 2;\n\n  // Total count of monitors matching the filter.\n  int32 total_count = 3;\n}\n```\n\n---\n\n## Validation\n\n### Approach\nUse **protovalidate** (Buf ecosystem) for request validation:\n\n- Validation rules defined in proto annotations\n- Runs before handler via interceptor\n- Returns `INVALID_ARGUMENT` with field-level details on failure\n\n### Example Annotations\n```protobuf\nmessage CreateMonitorRequest {\n  string name = 1 [(buf.validate.field).string = {min_len: 1, max_len: 256}];\n  string description = 2 [(buf.validate.field).string.max_len = 1024];\n  int32 periodicity = 3 [(buf.validate.field).int32 = {in: [60, 300, 600, 1800, 3600]}];\n  repeated string regions = 4 [(buf.validate.field).repeated = {min_items: 1, max_items: 35}];\n}\n```\n\n---\n\n## Code Organization\n\n### Shared Service Layer\n\nBoth REST and RPC handlers call the same business logic:\n\n```\npackages/proto/                    # Shared proto definitions\n├── buf.yaml                       # Buf configuration\n├── buf.gen.yaml                   # Code generation config\n├── openstatus/\n│   └── monitor/\n│       └── v1/\n│           ├── monitor.proto      # Message definitions\n│           └── service.proto      # Service definition\n└── gen/                           # Generated code\n    ├── ts/                        # TypeScript output\n    └── go/                        # Go output\n\napps/server/src/\n├── services/                      # Shared business logic (NEW)\n│   └── monitor/\n│       ├── create.ts\n│       ├── get.ts\n│       ├── list.ts\n│       ├── update.ts\n│       ├── delete.ts\n│       └── operations.ts          # trigger, pause, resume\n├── routes/\n│   └── v1/                        # REST handlers (existing)\n│       └── monitors/\n└── rpc/                           # ConnectRPC handlers (NEW)\n    ├── index.ts                   # Mount point\n    ├── interceptors/\n    │   ├── auth.ts                # Auth interceptor\n    │   ├── logging.ts             # Request logging\n    │   └── error.ts               # Error mapping\n    └── handlers/\n        └── monitor.ts             # MonitorService implementation\n```\n\n### Handler Pattern\n\n```typescript\n// apps/server/src/rpc/handlers/monitor.ts\nimport type { ConnectRouter } from \"@connectrpc/connect\";\nimport { MonitorService } from \"@openstatus/proto/gen/ts/openstatus/monitor/v1/service_connect\";\nimport * as monitorService from \"../../services/monitor\";\n\nexport default (router: ConnectRouter) =>\n  router.service(MonitorService, {\n    async createMonitor(req, ctx) {\n      const workspace = ctx.values.get(workspaceKey);\n      return await monitorService.create(workspace, req);\n    },\n    // ... other methods\n  });\n```\n\n---\n\n## Interceptors\n\n### Authentication Interceptor\n```typescript\n// Extracts and validates auth from headers\n// Sets workspace context for downstream handlers\n// Supports both API key and Bearer token\n```\n\n### Logging Interceptor\n```typescript\n// Integrates with existing LogTape setup\n// Logs: method, duration, status, workspace, requestId\n```\n\n### Error Interceptor\n```typescript\n// Maps internal errors to ConnectRPC codes\n// Attaches ErrorInfo details\n// Reports to Sentry (filtered for client errors)\n```\n\n---\n\n## Observability\n\n### Logging\n- Integrate with existing LogTape JSON logging\n- Log fields: `rpc.method`, `rpc.status_code`, `duration_ms`, `workspace_id`, `request_id`\n\n### Error Tracking\n- Sentry integration via interceptor\n- Filter client errors (INVALID_ARGUMENT, NOT_FOUND, etc.)\n- Include request context in error reports\n\n---\n\n## Rate Limiting\n\nUse existing infrastructure:\n- Hono middleware / upstream proxy handles rate limiting\n- No RPC-specific rate limiting interceptors needed\n\n---\n\n## Testing Strategy\n\n### Unit Tests\n- Test handlers directly with mocked service layer\n- Test interceptors in isolation\n- Test proto validation rules\n\n### Integration Tests\n- Spin up real server instance\n- Use generated TypeScript client to make RPC calls\n- Test full request lifecycle including auth\n\n### Test File Structure\n```\napps/server/src/rpc/\n├── __tests__/\n│   ├── handlers/\n│   │   └── monitor.test.ts        # Handler unit tests\n│   ├── interceptors/\n│   │   └── auth.test.ts           # Interceptor tests\n│   └── integration/\n│       └── monitor.integration.ts # Full flow tests\n```\n\n---\n\n## Additional Considerations\n\n### Health Check Endpoint\nAdd a simple `Health` service for load balancer probes at `/rpc`:\n\n```protobuf\nservice HealthService {\n  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage HealthCheckRequest {}\n\nmessage HealthCheckResponse {\n  enum ServingStatus {\n    UNKNOWN = 0;\n    SERVING = 1;\n    NOT_SERVING = 2;\n  }\n  ServingStatus status = 1;\n}\n```\n\n### Request ID Propagation\n- Generate `x-request-id` in logging interceptor if not present in request headers\n- Propagate request ID to all downstream services and log entries\n- Include request ID in error responses for debugging\n\n### Go Code Generation\n- Defer Go codegen until there are concrete Go service consumers\n- Reduces maintenance burden and build complexity initially\n- Can be enabled later by adding Go target to `buf.gen.yaml`\n\n### Proto Dependency Pinning\n- Use `buf.lock` to pin versions of:\n  - `buf.build/bufbuild/protovalidate`\n  - `buf.build/googleapis/googleapis` (if using google.protobuf types)\n- Run `buf mod update` to generate/update lock file\n\n---\n\n## Configuration Details\n\n### CORS Handling\n- `/rpc` endpoint should inherit existing CORS configuration from Hono app\n- If different CORS rules needed, configure via Hono middleware before mounting RPC routes\n- Connect protocol uses standard HTTP methods (POST), no special CORS requirements\n\n### Content-Type Support\nEnable both JSON and binary formats for flexibility:\n- `application/json` - Human-readable, easier debugging, slightly larger payloads\n- `application/proto` - Binary format, smaller payloads, better performance\n- Connect clients auto-negotiate based on `Content-Type` header\n\n### Deadline/Timeout Propagation\n- Client-specified timeouts via `connect-timeout-ms` header\n- Server interceptor should:\n  - Read timeout from request metadata\n  - Create context with deadline\n  - Cancel operations if deadline exceeded\n  - Return `DEADLINE_EXCEEDED` error code on timeout\n\n---\n\n## Dependencies\n\n### New Packages (packages/proto)\n```json\n{\n  \"devDependencies\": {\n    \"@bufbuild/buf\": \"latest\",\n    \"@bufbuild/protoc-gen-es\": \"latest\",\n    \"@connectrpc/protoc-gen-connect-es\": \"latest\"\n  },\n  \"dependencies\": {\n    \"@bufbuild/protobuf\": \"^2.0.0\",\n    \"@bufbuild/protobuf-conformance\": \"^2.0.0\"\n  }\n}\n```\n\n### Server App Additions\n```json\n{\n  \"dependencies\": {\n    \"@connectrpc/connect\": \"^2.0.0\",\n    \"@connectrpc/connect-node\": \"^2.0.0\",\n    \"@bufbuild/protovalidate\": \"^0.3.0\",\n    \"@openstatus/proto\": \"workspace:*\"\n  }\n}\n```\n\n---\n\n## Migration & Rollout\n\n### Phase 1: Foundation\n1. Create `packages/proto` with Buf setup\n2. Define monitor service proto\n3. Generate TypeScript and Go clients\n4. Add protovalidate annotations\n\n### Phase 2: Server Integration\n1. Add ConnectRPC dependencies to server\n2. Implement interceptors (auth, logging, error)\n3. Mount RPC routes at `/rpc` on Hono app\n4. Extract shared service layer from REST handlers\n\n### Phase 3: Handler Implementation\n1. Implement MonitorService handlers\n2. Write unit tests\n3. Write integration tests\n4. Internal testing\n\n### Phase 4: Release\n1. Documentation\n2. Client SDK examples\n3. Gradual rollout via feature flag (optional)\n\n---\n\n## Open Questions (Resolved)\n\n| Question | Decision |\n|----------|----------|\n| REST replacement or parallel? | New features only |\n| Transport protocol | Connect protocol only |\n| Streaming | Unary only |\n| Schema approach | Schema-first (.proto) |\n| Auth mechanism | Both API key + JWT |\n| Proto location | Shared package |\n| Tooling | Buf |\n| Error details | With ErrorInfo |\n| Code sharing | Shared service layer |\n| Client targets | TypeScript + Go |\n| Validation | protovalidate |\n| Type modeling | Separate messages |\n| Port strategy | Same port, /rpc prefix |\n| Pagination | Offset-based |\n| Rate limiting | Existing infrastructure |\n| Operations style | Separate methods |\n| Observability | Sentry + LogTape |\n| Testing | Unit + Integration |\n| Health check | Yes, HealthService |\n| Request ID | Generated + propagated |\n| Go codegen | Deferred |\n| CORS | Inherit from Hono |\n| Content-Type | JSON + Binary |\n| Timeouts | connect-timeout-ms header |\n\n---\n\n## References\n\n- [ConnectRPC Documentation](https://connectrpc.com/docs)\n- [Buf Documentation](https://buf.build/docs)\n- [protovalidate](https://github.com/bufbuild/protovalidate)\n- [Google Error Model](https://cloud.google.com/apis/design/errors)\n\n## Future work\n\n\n- Implement additional services and procedure: \n\n  // PauseMonitor suspends monitoring.\n  rpc PauseMonitor(PauseMonitorRequest) returns (PauseMonitorResponse);\n\n  // ResumeMonitor resumes a paused monitor.\n  rpc ResumeMonitor(ResumeMonitorRequest) returns (ResumeMonitorResponse);\n\n\n  // UpdateMonitor modifies an existing monitor.\n  rpc UpdateMonitor(UpdateMonitorRequest) returns (UpdateMonitorResponse);\n"
  },
  {
    "path": "apps/server/Dockerfile",
    "content": "# syntax=docker/dockerfile:1.19.0\n# This file is generated by Dofigen v2.7.0\n# See https://github.com/lenra-io/dofigen\n\n# install\nFROM oven/bun@sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a AS install\nLABEL \\\n    org.opencontainers.image.base.digest=\"sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a\" \\\n    org.opencontainers.image.base.name=\"docker.io/oven/bun:1.3.6\" \\\n    org.opencontainers.image.stage=\"install\"\nWORKDIR /app/\nRUN \\\n    --mount=type=bind,target=bunfig.toml,source=bunfig.toml \\\n    --mount=type=bind,target=package.json,source=package.json \\\n    --mount=type=bind,target=apps/server/package.json,source=apps/server/package.json \\\n    --mount=type=bind,target=packages/analytics/package.json,source=packages/analytics/package.json \\\n    --mount=type=bind,target=packages/db/package.json,source=packages/db/package.json \\\n    --mount=type=bind,target=packages/proto/package.json,source=packages/proto/package.json \\\n    --mount=type=bind,target=packages/emails/package.json,source=packages/emails/package.json \\\n    --mount=type=bind,target=packages/notifications/base/package.json,source=packages/notifications/base/package.json \\\n    --mount=type=bind,target=packages/notifications/discord/package.json,source=packages/notifications/discord/package.json \\\n    --mount=type=bind,target=packages/notifications/email/package.json,source=packages/notifications/email/package.json \\\n    --mount=type=bind,target=packages/notifications/grafana-oncall/package.json,source=packages/notifications/grafana-oncall/package.json \\\n    --mount=type=bind,target=packages/notifications/google-chat/package.json,source=packages/notifications/google-chat/package.json \\\n    --mount=type=bind,target=packages/notifications/ntfy/package.json,source=packages/notifications/ntfy/package.json \\\n    --mount=type=bind,target=packages/notifications/opsgenie/package.json,source=packages/notifications/opsgenie/package.json \\\n    --mount=type=bind,target=packages/notifications/pagerduty/package.json,source=packages/notifications/pagerduty/package.json \\\n    --mount=type=bind,target=packages/notifications/slack/package.json,source=packages/notifications/slack/package.json \\\n    --mount=type=bind,target=packages/notifications/telegram/package.json,source=packages/notifications/telegram/package.json \\\n    --mount=type=bind,target=packages/notifications/twillio-whatsapp/package.json,source=packages/notifications/twillio-whatsapp/package.json \\\n    --mount=type=bind,target=packages/notifications/twillio-sms/package.json,source=packages/notifications/twillio-sms/package.json \\\n    --mount=type=bind,target=packages/notifications/webhook/package.json,source=packages/notifications/webhook/package.json \\\n    --mount=type=bind,target=packages/error/package.json,source=packages/error/package.json \\\n    --mount=type=bind,target=packages/regions/package.json,source=packages/regions/package.json \\\n    --mount=type=bind,target=packages/tinybird/package.json,source=packages/tinybird/package.json \\\n    --mount=type=bind,target=packages/tracker/package.json,source=packages/tracker/package.json \\\n    --mount=type=bind,target=packages/upstash/package.json,source=packages/upstash/package.json \\\n    --mount=type=bind,target=packages/utils/package.json,source=packages/utils/package.json \\\n    --mount=type=bind,target=packages/tsconfig/package.json,source=packages/tsconfig/package.json \\\n    --mount=type=bind,target=packages/subscriptions/package.json,source=packages/subscriptions/package.json \\\n    --mount=type=bind,target=packages/assertions/package.json,source=packages/assertions/package.json \\\n    --mount=type=bind,target=packages/theme-store/package.json,source=packages/theme-store/package.json \\\n    --mount=type=cache,target=/root/.bun/install/cache,sharing=locked \\\n    bun install --production --frozen-lockfile --verbose\n\n# build\nFROM oven/bun@sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a AS build\nLABEL \\\n    org.opencontainers.image.base.digest=\"sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a\" \\\n    org.opencontainers.image.base.name=\"docker.io/oven/bun:1.3.6\" \\\n    org.opencontainers.image.stage=\"build\"\nENV NODE_ENV=\"production\"\nWORKDIR /app/apps/server\nCOPY \\\n    --link \\\n    \".\" \"/app/\"\nCOPY \\\n    --from=install \\\n    --link \\\n    \"/app/node_modules\" \"/app/node_modules\"\nRUN bun build --compile --sourcemap src/index.ts --outfile=app\n\n# runtime\nFROM debian@sha256:4333240150a6924f878e05ec2c998aec95238010e0e4d2fec6161c90128c4652 AS runtime\nLABEL \\\n    io.dofigen.version=\"2.7.0\" \\\n    org.opencontainers.image.authors=\"OpenStatus Team\" \\\n    org.opencontainers.image.base.digest=\"sha256:4333240150a6924f878e05ec2c998aec95238010e0e4d2fec6161c90128c4652\" \\\n    org.opencontainers.image.base.name=\"docker.io/debian:bullseye-slim\" \\\n    org.opencontainers.image.description=\"REST API server with Hono framework for OpenStatus\" \\\n    org.opencontainers.image.source=\"https://github.com/openstatusHQ/openstatus\" \\\n    org.opencontainers.image.title=\"OpenStatus Server\" \\\n    org.opencontainers.image.vendor=\"OpenStatus\"\nCOPY \\\n    --from=build \\\n    --chown=1000:1000 \\\n    --chmod=555 \\\n    --link \\\n    \"/app/apps/server/app\" \"/bin/\"\nUSER 0:0\nRUN <<EOF\napt-get update\napt-get install -y --no-install-recommends curl\nrm -rf /var/lib/apt/lists/*\nEOF\nUSER 1000:1000\nEXPOSE 3000\nHEALTHCHECK \\\n    --interval=30s \\\n    --timeout=10s \\\n    --start-period=30s \\\n    --retries=3 \\\n    CMD curl -f http://localhost:3000/ping || exit 1\nENTRYPOINT [\"/bin/app\"]\n"
  },
  {
    "path": "apps/server/README.md",
    "content": "# OpenStatus Server\n\n## Tech\n\n- Bun\n- HonoJS\n\n## Deploy\n\nFrom root\n\n```bash\nflyctl deploy --config apps/server/fly.toml --dockerfile  apps/server/Dockerfile\n```\n\n## Docker\n\nThe Dockerfile is generated thanks to [Dofigen](https://github.com/lenra-io/dofigen). To generate the Dockerfile, run the following command from the `apps/server` directory:\n\n```bash\n# Update the dependent image versions\ndofigen update\n# Generate the Dockerfile\ndofigen gen\n```\n\nBuild the docker image locally\n\n```bash\ndocker build . -t registry.fly.io/openstatus-docker:openstatus-docker-v0  --file  ./apps/server/Dockerfile --platform linux/amd64\n```\n\nif you want to run the docker image locally\n\n```bash\ndocker run -p 3000:3000  registry.fly.io/openstatus-docker:openstatus-docker-v0\n```\n\nPush to Fly Registry\n\n```bash\ndocker push registry.fly.io/openstatus-docker:openstatus-docker-v0\n\n```\n\nDeploy to Fly\n\n```bash\nflyctl deploy --app openstatus-docker \\\n  --image registry.fly.io/openstatus-docker:openstatus-docker-v0\n```\n"
  },
  {
    "path": "apps/server/bunfig.toml",
    "content": "[test]\npreload = [\"./src/libs/test/preload.ts\"]\n"
  },
  {
    "path": "apps/server/docker-compose.yaml",
    "content": "name: server\nservices:\n    server:\n        build:\n         context: ../..\n         dockerfile: apps/server/Dockerfile\n        ports:\n            - 3000:3000\n        image: server\n        command: .\n"
  },
  {
    "path": "apps/server/dofigen.yml",
    "content": "# Files to exclude from Docker context\nignore:\n  - node_modules\n  - /apps/docs\n  - /apps/screenshot-service\n  - /apps/web\n  - /apps/dashboard\n  - /apps/status-page\n  - /apps/workflows\n  - /packages/api\n  - /packages/integrations/vercel\n\nbuilders:\n  # Stage 1: Install production dependencies\n  install:\n    fromImage: oven/bun:1.3.6\n    workdir: /app/\n    labels:\n      org.opencontainers.image.stage: install\n    bind:\n      - bunfig.toml\n      - package.json\n      - apps/server/package.json\n      - packages/analytics/package.json\n      - packages/db/package.json\n      - packages/proto/package.json\n      - packages/emails/package.json\n      - packages/notifications/base/package.json\n      - packages/notifications/discord/package.json\n      - packages/notifications/email/package.json\n      - packages/notifications/grafana-oncall/package.json\n      - packages/notifications/google-chat/package.json\n      - packages/notifications/ntfy/package.json\n      - packages/notifications/opsgenie/package.json\n      - packages/notifications/pagerduty/package.json\n      - packages/notifications/slack/package.json\n      - packages/notifications/telegram/package.json\n      - packages/notifications/twillio-whatsapp/package.json\n      - packages/notifications/twillio-sms/package.json\n      - packages/notifications/webhook/package.json\n      - packages/error/package.json\n      - packages/regions/package.json\n      - packages/tinybird/package.json\n      - packages/tracker/package.json\n      - packages/upstash/package.json\n      - packages/utils/package.json\n      - packages/tsconfig/package.json\n      - packages/subscriptions/package.json\n      - packages/assertions/package.json\n      - packages/theme-store/package.json\n    run: bun install --production --frozen-lockfile --verbose\n    cache:\n      - /root/.bun/install/cache\n\n  # Stage 2: Build application (compile to binary)\n  build:\n    fromImage: oven/bun:1.3.6\n    workdir: /app/apps/server\n    labels:\n      org.opencontainers.image.stage: build\n    env:\n      NODE_ENV: production\n    copy:\n      - . /app/\n      - fromBuilder: install\n        source: /app/node_modules\n        target: /app/node_modules\n    run: bun build --compile --sourcemap src/index.ts --outfile=app\n\n# Runtime stage\nfromImage: debian:bullseye-slim\n\n# Metadata labels\nlabels:\n  org.opencontainers.image.title: OpenStatus Server\n  org.opencontainers.image.description: REST API server with Hono framework for OpenStatus\n  org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus\n  org.opencontainers.image.vendor: OpenStatus\n  org.opencontainers.image.authors: OpenStatus Team\n\n# Copy compiled binary\ncopy:\n  - fromBuilder: build\n    source: /app/apps/server/app\n    target: /bin/\n    chmod: \"555\"\n\n# Install curl for health checks\nroot:\n  run:\n    - apt-get update\n    - apt-get install -y --no-install-recommends curl\n    - rm -rf /var/lib/apt/lists/*\n\n# Security: run as non-root user\nuser: \"1000:1000\"\n\n# Expose port\nexpose: \"3000\"\n\n# Health check\nhealthcheck:\n  interval: 30s\n  timeout: 10s\n  start: 30s\n  retries: 3\n  cmd: curl -f http://localhost:3000/ping || exit 1\n\n# Start application\nentrypoint: /bin/app\n"
  },
  {
    "path": "apps/server/env.ts",
    "content": "const file = Bun.file(\"./.env.example\");\nawait Bun.write(\"./.env\", file);\n"
  },
  {
    "path": "apps/server/fly.sh",
    "content": "#!/bin/bash\n\n\nfly machines list  --json | jq .[].id | sed 's/\"//g' | while read machine; do\n  fly machines update $machine --restart always --yes\ndone"
  },
  {
    "path": "apps/server/fly.toml",
    "content": "# fly.toml app configuration file generated for openstatus-api on 2023-09-13T17:29:05+02:00\n#\n# See https://fly.io/docs/reference/configuration/ for information about how to use this file.\n#\n\napp = \"openstatus-api\"\nprimary_region = \"ams\"\n\n\n[build]\n  dockerfile = \"./Dockerfile\"\n\n[[vm]]\n  cpu_kind = \"shared\"\n  cpus = 1\n  memory_mb = 512\n\n[http_service]\n  internal_port = 3000\n  force_https = true\n  auto_stop_machines = \"suspend\"\n  auto_start_machines = true\n  min_machines_running = 1\n  processes = [\"app\"]\n\n[http_service.concurrency]\n    type = \"requests\"\n    hard_limit = 1000\n    soft_limit = 500\n\n[deploy]\n  strategy = \"bluegreen\"\n\n[[http_service.checks]]\n  grace_period = \"10s\"\n  interval = \"15s\"\n  method = \"GET\"\n  timeout = \"5s\"\n  path = \"/ping\"\n\n[env]\n  NODE_ENV = \"production\"\n  PORT = \"3000\"\n\n\n# [checks]\n#   [checks.name_of_your_http_check]\n#     port = 3000\n#     type = \"http\"\n#     interval = \"15s\"\n#     timeout = \"10s\"\n#     grace_period = \"30s\"\n#     method = \"get\"\n#     path = \"/ping\"\n\n# [[services.http_checks]]\n#   interval = 10000\n#   grace_period = \"5s\"\n#   method = \"get\"\n#   path = \"/ping\"\n#   protocol = \"https\"\n#   timeout = 2000\n#   tls_skip_verify = false\n"
  },
  {
    "path": "apps/server/log/fly.toml",
    "content": "# fly.toml app configuration file generated for openstatus-log on 2023-10-19T00:44:54+02:00\n#\n# See https://fly.io/docs/reference/configuration/ for information about how to use this file.\n#\n\napp = \"openstatus-log\"\nprimary_region = \"ams\"\n\n[build]\n  image = \"ghcr.io/superfly/fly-log-shipper:latest\"\n\n[http_service]\n  internal_port = 8686\n  force_https = true\n  auto_stop_machines = \"suspend\"\n  auto_start_machines = true\n  min_machines_running = 1\n  processes = [\"app\"]\n"
  },
  {
    "path": "apps/server/package.json",
    "content": "{\n  \"name\": \"@openstatus/server\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"type\": \"module\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"env\": \"bun env.ts\",\n    \"dev\": \"bun run --hot src/index.ts\",\n    \"start\": \"NODE_ENV=production bun run src/index.ts\",\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@bufbuild/protobuf\": \"2.10.2\",\n    \"@bufbuild/protovalidate\": \"^1.1.1\",\n    \"@connectrpc/connect\": \"2.1.1\",\n    \"@connectrpc/connect-node\": \"2.1.1\",\n    \"@connectrpc/validate\": \"^0.2.0\",\n    \"@hono/sentry\": \"1.2.2\",\n    \"@hono/zod-openapi\": \"1.1.5\",\n    \"@hono/zod-validator\": \"0.7.6\",\n    \"@logtape/logtape\": \"2.0.1\",\n    \"@logtape/otel\": \"2.0.1\",\n    \"@logtape/sentry\": \"2.0.1\",\n    \"@openstatus/analytics\": \"workspace:*\",\n    \"@openstatus/subscriptions\": \"workspace:*\",\n    \"@openstatus/notification-discord\": \"workspace:*\",\n    \"@openstatus/notification-google-chat\": \"workspace:*\",\n    \"@openstatus/notification-grafana-oncall\": \"workspace:*\",\n    \"@openstatus/notification-ntfy\": \"workspace:*\",\n    \"@openstatus/notification-opsgenie\": \"workspace:*\",\n    \"@openstatus/notification-pagerduty\": \"workspace:*\",\n    \"@openstatus/notification-slack\": \"workspace:*\",\n    \"@openstatus/notification-telegram\": \"workspace:*\",\n    \"@openstatus/notification-twillio-whatsapp\": \"workspace:*\",\n    \"@openstatus/notification-webhook\": \"workspace:*\",\n    \"@openstatus/assertions\": \"workspace:*\",\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/emails\": \"workspace:*\",\n    \"@openstatus/error\": \"workspace:*\",\n    \"@openstatus/proto\": \"workspace:*\",\n    \"@openstatus/regions\": \"workspace:*\",\n    \"@openstatus/tinybird\": \"workspace:*\",\n    \"@openstatus/tracker\": \"workspace:*\",\n    \"@openstatus/upstash\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:*\",\n    \"@opentelemetry/resources\": \"2.2.0\",\n    \"@opentelemetry/semantic-conventions\": \"1.38.0\",\n    \"@scalar/hono-api-reference\": \"0.8.5\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"@unkey/api\": \"2.2.0\",\n    \"@upstash/qstash\": \"2.6.2\",\n    \"hono\": \"4.11.3\",\n    \"nanoid\": \"5.0.7\",\n    \"percentile\": \"1.6.0\",\n    \"validator\": \"13.12.0\",\n    \"@slack/web-api\": \"7.15.0\",\n    \"@ai-sdk/anthropic\": \"1.2.0\",\n    \"ai\": \"6.0.94\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/validator\": \"13.12.0\",\n    \"bun-types\": \"1.3.1\",\n    \"dotenv\": \"16.3.1\"\n  }\n}\n"
  },
  {
    "path": "apps/server/src/env.ts",
    "content": "import { monitorRegions } from \"@openstatus/db/src/schema/constants\";\nimport { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    UNKEY_API_ID: z.string().min(1),\n    UNKEY_TOKEN: z.string().min(1),\n    TINY_BIRD_API_KEY: z.string().min(1),\n    UPSTASH_REDIS_REST_URL: z.string().min(1),\n    UPSTASH_REDIS_REST_TOKEN: z.string().min(1),\n    FLY_REGION: z.enum(monitorRegions),\n    CRON_SECRET: z.string(),\n    SCREENSHOT_SERVICE_URL: z.string(),\n    QSTASH_TOKEN: z.string(),\n    NODE_ENV: z.string().prefault(\"development\"),\n    SUPER_ADMIN_TOKEN: z.string(),\n    RESEND_API_KEY: z.string(),\n    AXIOM_TOKEN: z.string(),\n    AXIOM_DATASET: z.string(),\n    SLACK_SIGNING_SECRET: z.string().optional(),\n    SLACK_CLIENT_ID: z.string().optional(),\n    SLACK_CLIENT_SECRET: z.string().optional(),\n    SLACK_REDIRECT_URI: z.string().optional(),\n    AI_GATEWAY_API_KEY: z.string().optional(),\n  },\n\n  /**\n   * What object holds the environment variables at runtime. This is usually\n   * `process.env` or `import.meta.env`.\n   */\n  runtimeEnv: process.env,\n\n  /**\n   * By default, this library will feed the environment variables directly to\n   * the Zod validator.\n   *\n   * This means that if you have an empty string for a value that is supposed\n   * to be a number (e.g. `PORT=` in a \".env\" file), Zod will incorrectly flag\n   * it as a type mismatch violation. Additionally, if you have an empty string\n   * for a value that is supposed to be a string with a default value (e.g.\n   * `DOMAIN=` in an \".env\" file), the default value will never be applied.\n   *\n   * In order to solve these issues, we recommend that all new projects\n   * explicitly specify this option as true.\n   */\n  skipValidation: true,\n});\n"
  },
  {
    "path": "apps/server/src/index.ts",
    "content": "import { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport { sentry } from \"@hono/sentry\";\nimport {\n  configure,\n  // configureSync,\n  getConsoleSink,\n  getLogger,\n  jsonLinesFormatter,\n  withContext,\n} from \"@logtape/logtape\";\nimport { getOpenTelemetrySink } from \"@logtape/otel\";\nimport { Hono } from \"hono\";\nimport { showRoutes } from \"hono/dev\";\n\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { ATTR_DEPLOYMENT_ENVIRONMENT_NAME } from \"@opentelemetry/semantic-conventions/incubating\";\nimport { Scalar } from \"@scalar/hono-api-reference\";\nimport { prettyJSON } from \"hono/pretty-json\";\nimport { requestId } from \"hono/request-id\";\nimport openapiV1Json from \"../static/openapi-v1.json\" with { type: \"json\" };\nimport openapiYaml from \"../static/openapi.yaml\" with { type: \"text\" };\nimport { env } from \"./env\";\nimport { handleError } from \"./libs/errors\";\nimport { publicRoute } from \"./routes/public\";\nimport { mountRpcRoutes } from \"./routes/rpc\";\nimport { slackRoute } from \"./routes/slack\";\nimport { api } from \"./routes/v1\";\n\ntype Env = {\n  Variables: {\n    event: Record<string, unknown>;\n  };\n};\n\n// Export app before any top-level await to avoid \"Cannot access before initialization\" errors in tests\nexport const app = new Hono<Env>({\n  strict: false,\n});\n\nconst logger = getLogger(\"api-server\");\nconst otelLogger = getLogger(\"api-server-otel\");\n\n/**\n * Configure logging asynchronously without blocking module initialization.\n * This allows tests to import `app` immediately.\n */\n\nconst defaultLogger = getOpenTelemetrySink({\n  serviceName: \"openstatus-server\",\n  otlpExporterConfig: {\n    url: \"https://eu-central-1.aws.edge.axiom.co/v1/logs\",\n    headers: {\n      Authorization: `Bearer ${env.AXIOM_TOKEN}`,\n      \"X-Axiom-Dataset\": env.AXIOM_DATASET,\n    },\n  },\n  additionalResource: resourceFromAttributes({\n    [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: env.NODE_ENV,\n  }),\n});\n\nawait configure({\n  sinks: {\n    console: getConsoleSink({ formatter: jsonLinesFormatter }),\n    otel: defaultLogger,\n  },\n  loggers: [\n    {\n      category: \"api-server\",\n      lowestLevel: \"info\",\n      sinks: [\"console\"],\n    },\n    {\n      category: \"api-server-otel\",\n      lowestLevel: \"info\",\n      sinks: [\"otel\"],\n    },\n    {\n      category: [\"logtape\", \"meta\"],\n      lowestLevel: \"warning\",\n      sinks: [\"console\"],\n    },\n  ],\n  contextLocalStorage: new AsyncLocalStorage(),\n});\n\n/* biome-ignore lint/suspicious/noExplicitAny: <explanation> */\nfunction shouldSample(event: Record<string, any>): boolean {\n  // Always keep errors\n  if (event.status_code >= 500) return true;\n  if (event.error) return true;\n\n  // Always keep slow requests (above p99)\n  if (event.duration_ms > 2000) return true;\n\n  // Random sample the rest at 20%\n  return Math.random() < 0.2;\n}\n\n/**\n * Middleware\n */\napp.use(\"*\", sentry({ dsn: process.env.SENTRY_DSN }));\napp.use(\"*\", requestId());\napp.use(\"*\", prettyJSON());\n\napp.use(\"*\", async (c, next) => {\n  const reqId = c.get(\"requestId\");\n  const startTime = Date.now();\n\n  await withContext(\n    {\n      request_id: reqId,\n      method: c.req.method,\n      url: c.req.url,\n      user_agent: c.req.header(\"User-Agent\"),\n    },\n    async () => {\n      // Initialize wide event - one canonical log line per request\n      const event: Record<string, unknown> = {\n        timestamp: new Date().toISOString(),\n        request_id: reqId,\n        // Request context\n        method: c.req.method,\n        path: c.req.path,\n        url: c.req.url,\n        // Client context\n        user_agent: c.req.header(\"User-Agent\"),\n        // Request metadata\n        content_type: c.req.header(\"Content-Type\"),\n        // Environment characteristics\n        service: \"api-server\",\n        environment: env.NODE_ENV,\n        region: env.FLY_REGION,\n      };\n      c.set(\"event\", event);\n\n      await next();\n\n      // Performance\n      const duration = Date.now() - startTime;\n      event.duration_ms = duration;\n\n      // Response context\n      event.status_code = c.res.status;\n\n      // Outcome\n      if (c.error) {\n        event.outcome = \"error\";\n        event.error = {\n          type: c.error.name,\n          message: c.error.message,\n          stack: c.error.stack,\n        };\n      } else {\n        event.outcome = c.res.status < 400 ? \"success\" : \"failure\";\n      }\n\n      // Emit single canonical log line (sampled for otel, always for console in dev)\n      if (shouldSample(event)) {\n        otelLogger.info(\"request\", { ...event });\n      }\n\n      // Console logging only for errors in production\n      if (env.NODE_ENV !== \"production\" || c.res.status >= 500) {\n        logger.info(\"request\", {\n          request_id: requestId,\n          method: c.req.method,\n          path: c.req.path,\n          status_code: c.res.status,\n          duration_ms: duration,\n          outcome: event.outcome,\n        });\n      }\n    },\n  );\n});\n\napp.onError(handleError);\n\n/**\n * ConnectRPC Routes API v2 ftw\n */\n\nmountRpcRoutes(app);\n\n/**\n * Public Routes\n */\napp.route(\"/public\", publicRoute);\n\n/**\n * Ping Pong\n */\napp.get(\"/ping\", (c) => {\n  return c.json(\n    { ping: \"pong\", region: env.FLY_REGION, requestId: c.get(\"requestId\") },\n    200,\n  );\n});\n\napp.get(\"/openapi.yaml\", (c) => {\n  return c.text(openapiYaml, 200, { \"Content-Type\": \"application/yaml\" });\n});\napp.get(\"/openapi-v1.json\", (c) => {\n  return c.text(JSON.stringify(openapiV1Json), 200, {\n    \"Content-Type\": \"application/json\",\n  });\n});\n\napp.get(\n  \"/openapi\",\n  Scalar({\n    url: \"/openapi.yaml\",\n    servers: [\n      {\n        url: \"https://api.openstatus.dev/\",\n        description: \"Production server\",\n      },\n      {\n        url: \"http://localhost:3000/\",\n        description: \"Dev server\",\n      },\n    ],\n    metaData: {\n      title: \"OpenStatus API\",\n      description: \"Start building with OpenStatus API\",\n      ogDescription: \"API Reference\",\n      ogTitle: \"OpenStatus API\",\n      ogImage:\n        \"https://openstatus.dev/api/og?title=OpenStatus%20API&description=API%20Reference\",\n      twitterCard: \"summary_large_image\",\n    },\n  }),\n);\n\n/**\n * API Routes v1\n */\napp.route(\"/v1\", api);\n\n/**\n * Slack Agent Routes\n */\napp.route(\"/slack\", slackRoute);\n\n/**\n * TODO: move to `workflows` app\n * This route is used by our checker to update the status of the monitors,\n * create incidents, and send notifications.\n */\n\nconst isDev = process.env.NODE_ENV === \"development\";\nconst port = 3000;\n\nif (isDev) showRoutes(app, { verbose: true, colorize: true });\n\nlogger.info(\"Starting server\", { port, environment: env.NODE_ENV });\n\nconst server = { port, fetch: app.fetch };\n\nexport default server;\n"
  },
  {
    "path": "apps/server/src/libs/checker/index.ts",
    "content": "export * from \"./utils\";\n"
  },
  {
    "path": "apps/server/src/libs/checker/utils.ts",
    "content": "import { OpenStatusApiError } from \"@/libs/errors\";\nimport type { z } from \"@hono/zod-openapi\";\nimport type { selectMonitorSchema } from \"@openstatus/db/src/schema\";\nimport {\n  type httpPayloadSchema,\n  type tpcPayloadSchema,\n  transformHeaders,\n} from \"@openstatus/utils\";\n\nexport function getCheckerPayload(\n  monitor: z.infer<typeof selectMonitorSchema>,\n  status: z.infer<typeof selectMonitorSchema>[\"status\"],\n): z.infer<typeof httpPayloadSchema> | z.infer<typeof tpcPayloadSchema> {\n  const timestamp = new Date().getTime();\n  switch (monitor.jobType) {\n    case \"http\":\n      return {\n        workspaceId: String(monitor.workspaceId),\n        monitorId: String(monitor.id),\n        url: monitor.url,\n        method: monitor.method || \"GET\",\n        cronTimestamp: timestamp,\n        body: monitor.body,\n        headers: monitor.headers,\n        status: status,\n        assertions: monitor.assertions ? JSON.parse(monitor.assertions) : null,\n        degradedAfter: monitor.degradedAfter,\n        timeout: monitor.timeout,\n        trigger: \"api\",\n        otelConfig: monitor.otelEndpoint\n          ? {\n              endpoint: monitor.otelEndpoint,\n              headers: transformHeaders(monitor.otelHeaders),\n            }\n          : undefined,\n        retry: monitor.retry ?? 0,\n        followRedirects: monitor.followRedirects ?? false,\n      };\n    case \"tcp\":\n      return {\n        workspaceId: String(monitor.workspaceId),\n        monitorId: String(monitor.id),\n        uri: monitor.url,\n        status: status,\n        assertions: monitor.assertions ? JSON.parse(monitor.assertions) : null,\n        cronTimestamp: timestamp,\n        degradedAfter: monitor.degradedAfter,\n        timeout: monitor.timeout,\n        trigger: \"api\",\n        otelConfig: monitor.otelEndpoint\n          ? {\n              endpoint: monitor.otelEndpoint,\n              headers: transformHeaders(monitor.otelHeaders),\n            }\n          : undefined,\n        retry: monitor.retry ?? 0,\n        followRedirects: monitor.followRedirects ?? false,\n      };\n    default:\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message:\n          \"Invalid jobType, currently only 'http' and 'tcp' are supported\",\n      });\n  }\n}\n\nexport function getCheckerUrl(\n  monitor: z.infer<typeof selectMonitorSchema>,\n  opts: { trigger?: \"api\" | \"cron\"; data?: boolean } = {\n    trigger: \"api\",\n    data: false,\n  },\n): string {\n  switch (monitor.jobType) {\n    case \"http\":\n      return `https://openstatus-checker.fly.dev/checker/http?monitor_id=${monitor.id}&trigger=${opts.trigger}&data=${opts.data}`;\n    case \"tcp\":\n      return `https://openstatus-checker.fly.dev/checker/tcp?monitor_id=${monitor.id}&trigger=${opts.trigger}&data=${opts.data}`;\n    default:\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message:\n          \"Invalid jobType, currently only 'http' and 'tcp' are supported\",\n      });\n  }\n}\n"
  },
  {
    "path": "apps/server/src/libs/clients.ts",
    "content": "import { env } from \"@/env\";\nimport { OSTinybird } from \"@openstatus/tinybird\";\nimport { Redis } from \"@openstatus/upstash\";\n\n/**\n * Shared singleton instances for external services.\n * Using singletons prevents memory leaks from creating multiple instances\n * and ensures proper connection pooling.\n */\n\n// Tinybird client singleton\nexport const tb = new OSTinybird(env.TINY_BIRD_API_KEY);\n\n// Redis client singleton\nexport const redis = Redis.fromEnv();\n"
  },
  {
    "path": "apps/server/src/libs/errors/index.ts",
    "content": "export * from \"./utils\";\nexport * from \"./openapi-error-responses\";\n"
  },
  {
    "path": "apps/server/src/libs/errors/openapi-error-responses.ts",
    "content": "import type { RouteConfig } from \"@hono/zod-openapi\";\nimport { createErrorSchema } from \"./utils\";\n\nexport const openApiErrorResponses = {\n  400: {\n    description:\n      \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n    content: {\n      \"application/json\": {\n        schema: createErrorSchema(\"BAD_REQUEST\").openapi(\"ErrBadRequest\"),\n      },\n    },\n  },\n  401: {\n    description:\n      \"The client must authenticate itself to get the requested response.\",\n    content: {\n      \"application/json\": {\n        schema: createErrorSchema(\"UNAUTHORIZED\").openapi(\"ErrUnauthorized\"),\n      },\n    },\n  },\n  402: {\n    description: \"A higher pricing plan is required to access the resource.\",\n    content: {\n      \"application/json\": {\n        schema:\n          createErrorSchema(\"PAYMENT_REQUIRED\").openapi(\"ErrPaymentRequired\"),\n      },\n    },\n  },\n  403: {\n    description:\n      \"The client does not have the necessary permissions to access the resource.\",\n    content: {\n      \"application/json\": {\n        schema: createErrorSchema(\"FORBIDDEN\").openapi(\"ErrForbidden\"),\n      },\n    },\n  },\n  404: {\n    description: \"The server can't find the requested resource.\",\n    content: {\n      \"application/json\": {\n        schema: createErrorSchema(\"NOT_FOUND\").openapi(\"ErrNotFound\"),\n      },\n    },\n  },\n  409: {\n    description:\n      \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n    content: {\n      \"application/json\": {\n        schema: createErrorSchema(\"CONFLICT\").openapi(\"ErrConflict\"),\n      },\n    },\n  },\n  500: {\n    description:\n      \"The server has encountered a situation it doesn't know how to handle.\",\n    content: {\n      \"application/json\": {\n        schema: createErrorSchema(\"INTERNAL_SERVER_ERROR\").openapi(\n          \"ErrInternalServerError\",\n        ),\n      },\n    },\n  },\n} satisfies RouteConfig[\"responses\"];\n"
  },
  {
    "path": "apps/server/src/libs/errors/utils.ts",
    "content": "// Props to Unkey: https://github.com/unkeyed/unkey/blob/main/apps/api/src/pkg/errors/http.ts\nimport type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\n\nimport type { ErrorCode } from \"@openstatus/error\";\nimport {\n  ErrorCodes,\n  SchemaError,\n  codeToStatus,\n  statusToCode,\n} from \"@openstatus/error\";\n\nimport { z } from \"@hono/zod-openapi\";\nimport { getLogger } from \"@logtape/logtape\";\nimport { ZodError } from \"zod\";\n\nconst logger = getLogger(\"api-server\");\nexport class OpenStatusApiError extends HTTPException {\n  public readonly code: ErrorCode;\n\n  constructor({\n    code,\n    message,\n  }: {\n    code: ErrorCode;\n    message: HTTPException[\"message\"];\n  }) {\n    const status = codeToStatus(code);\n    super(status, { message });\n    this.code = code;\n  }\n}\n\nexport function handleError(err: Error, c: Context): Response {\n  if (err instanceof ZodError) {\n    const error = SchemaError.fromZod(err, c);\n\n    // If the error is a client error, we disable Sentry\n    c.get(\"sentry\").setEnabled(false);\n\n    return c.json<ErrorSchema>(\n      {\n        code: \"BAD_REQUEST\",\n        message: error.message,\n        docs: \"https://docs.openstatus.dev/api-references/errors/code/BAD_REQUEST\",\n        requestId: c.get(\"requestId\"),\n      },\n      { status: 400 },\n    );\n  }\n\n  /**\n   * This is a custom error that we throw in our code so we can handle it\n   */\n  if (err instanceof OpenStatusApiError) {\n    const code = statusToCode(err.status);\n\n    // If the error is a client error, we disable Sentry\n    if (err.status < 499) {\n      c.get(\"sentry\").setEnabled(false);\n    }\n\n    return c.json<ErrorSchema>(\n      {\n        code: code,\n        message: err.message,\n        docs: `https://docs.openstatus.dev/api-references/errors/code/${code}`,\n        requestId: c.get(\"requestId\"),\n      },\n      { status: err.status },\n    );\n  }\n\n  if (err instanceof HTTPException) {\n    const code = statusToCode(err.status);\n    return c.json<ErrorSchema>(\n      {\n        code: code,\n        message: err.message,\n        docs: `https://docs.openstatus.dev/api-references/errors/code/${code}`,\n        requestId: c.get(\"requestId\"),\n      },\n      { status: err.status },\n    );\n  }\n\n  logger.error(\"Request error\", {\n    error: {\n      name: err.name,\n      message: err.message,\n      stack: err.stack,\n    },\n    method: c.req.method,\n    url: c.req.url,\n  });\n  c.get(\"sentry\").captureException(err);\n\n  return c.json<ErrorSchema>(\n    {\n      code: \"INTERNAL_SERVER_ERROR\",\n      message: err.message ?? \"Something went wrong\",\n      docs: \"https://docs.openstatus.dev/api-references/errors/code/INTERNAL_SERVER_ERROR\",\n      requestId: c.get(\"requestId\"),\n    },\n\n    { status: 500 },\n  );\n}\n\nexport function handleZodError(\n  result:\n    | {\n        success: true;\n        data: unknown;\n      }\n    | {\n        success: false;\n        error: ZodError;\n      },\n  c: Context,\n) {\n  if (!result.success) {\n    const error = SchemaError.fromZod(result.error, c);\n    return c.json<z.infer<ReturnType<typeof createErrorSchema>>>(\n      {\n        code: \"BAD_REQUEST\",\n        docs: \"https://docs.openstatus.dev/api-references/errors/code/BAD_REQUEST\",\n        message: error.message,\n        requestId: c.get(\"requestId\"),\n      },\n      { status: 400 },\n    );\n  }\n}\n\nexport function createErrorSchema(code: ErrorCode) {\n  return z.object({\n    code: z.enum(ErrorCodes).openapi({\n      example: code,\n      description: \"The error code related to the status code.\",\n    }),\n    message: z.string().openapi({\n      description: \"A human readable message describing the issue.\",\n      example: \"<string>\",\n    }),\n    docs: z.string().openapi({\n      description: \"A link to the documentation for the error.\",\n      example: `https://docs.openstatus.dev/api-references/errors/code/${code}`,\n    }),\n    requestId: z.string().openapi({\n      description:\n        \"The request id to be used for debugging and error reporting.\",\n      example: \"<uuid>\",\n    }),\n  });\n}\n\nexport type ErrorSchema = z.infer<ReturnType<typeof createErrorSchema>>;\n"
  },
  {
    "path": "apps/server/src/libs/middlewares/auth.ts",
    "content": "import { UnkeyCore } from \"@unkey/api/core\";\nimport { keysVerifyKey } from \"@unkey/api/funcs/keysVerifyKey\";\nimport type { Context, Next } from \"hono\";\n\nimport { env } from \"@/env\";\nimport { OpenStatusApiError } from \"@/libs/errors\";\nimport type { Variables } from \"@/types\";\nimport { getLogger } from \"@logtape/logtape\";\nimport { db, eq } from \"@openstatus/db\";\nimport { selectWorkspaceSchema, workspace } from \"@openstatus/db/src/schema\";\nimport { apiKey } from \"@openstatus/db/src/schema/api-keys\";\nimport {\n  shouldUpdateLastUsed,\n  verifyApiKeyHash,\n} from \"@openstatus/db/src/utils/api-key\";\n\nconst logger = getLogger(\"api-server\");\n\n/**\n * Looks up a workspace by ID and validates the data.\n * Throws OpenStatusApiError if workspace is not found or invalid.\n */\nexport async function lookupWorkspace(workspaceId: number) {\n  const _workspace = await db\n    .select()\n    .from(workspace)\n    .where(eq(workspace.id, workspaceId))\n    .get();\n\n  if (!_workspace) {\n    throw new OpenStatusApiError({\n      code: \"NOT_FOUND\",\n      message: \"Workspace not found, please contact support\",\n    });\n  }\n\n  const validation = selectWorkspaceSchema.safeParse(_workspace);\n\n  if (!validation.success) {\n    throw new OpenStatusApiError({\n      code: \"BAD_REQUEST\",\n      message: \"Workspace data is invalid\",\n    });\n  }\n\n  return validation.data;\n}\n\nexport async function authMiddleware(\n  c: Context<{ Variables: Variables }, \"/*\">,\n  next: Next,\n) {\n  const key = c.req.header(\"x-openstatus-key\");\n  if (!key)\n    throw new OpenStatusApiError({\n      code: \"UNAUTHORIZED\",\n      message: \"Missing 'x-openstatus-key' header\",\n    });\n\n  const { error, result } = await validateKey(key);\n\n  if (error) {\n    throw new OpenStatusApiError({\n      code: \"UNAUTHORIZED\",\n      message: error.message,\n    });\n  }\n\n  if (!result.valid || !result.ownerId) {\n    throw new OpenStatusApiError({\n      code: \"UNAUTHORIZED\",\n      message: \"Invalid API Key\",\n    });\n  }\n\n  const ownerId = Number.parseInt(result.ownerId);\n\n  if (Number.isNaN(ownerId)) {\n    throw new OpenStatusApiError({\n      code: \"UNAUTHORIZED\",\n      message: \"API Key is Not a Number\",\n    });\n  }\n\n  const workspaceData = await lookupWorkspace(ownerId);\n\n  const event = c.get(\"event\");\n  event.workspace = {\n    id: workspaceData.id,\n    name: workspaceData.name,\n    plan: workspaceData.plan,\n    stripe_id: workspaceData.stripeId,\n  };\n  event.auth_method = result.authMethod;\n  c.set(\"workspace\", workspaceData);\n\n  await next();\n}\n\nexport async function validateKey(key: string): Promise<{\n  result: { valid: boolean; ownerId?: string; authMethod?: string };\n  error?: { message: string };\n}> {\n  if (env.NODE_ENV === \"production\") {\n    /**\n     * Both custom and Unkey API keys use the `os_` prefix for seamless transition.\n     * Custom keys are checked first in the database, then falls back to Unkey.\n     */\n    if (key.startsWith(\"os_\")) {\n      // 1. Try custom DB first\n      const prefix = key.slice(0, 11); // \"os_\" (3 chars) + 8 hex chars = 11 total\n      const customKey = await db\n        .select()\n        .from(apiKey)\n        .where(eq(apiKey.prefix, prefix))\n        .get();\n\n      if (customKey) {\n        // Verify hash using bcrypt-compatible verification\n        if (!(await verifyApiKeyHash(key, customKey.hashedToken))) {\n          return {\n            result: { valid: false },\n            error: { message: \"Invalid API Key\" },\n          };\n        }\n        // Check expiration\n        if (customKey.expiresAt && customKey.expiresAt < new Date()) {\n          return {\n            result: { valid: false },\n            error: { message: \"API Key expired\" },\n          };\n        }\n\n        // Update lastUsedAt (debounced)\n        if (shouldUpdateLastUsed(customKey.lastUsedAt)) {\n          await db\n            .update(apiKey)\n            .set({ lastUsedAt: new Date() })\n            .where(eq(apiKey.id, customKey.id));\n        }\n        return {\n          result: {\n            valid: true,\n            ownerId: String(customKey.workspaceId),\n            authMethod: \"custom_key\",\n          },\n        };\n      }\n\n      // 2. Fall back to Unkey (transition period)\n      const unkey = new UnkeyCore({ rootKey: env.UNKEY_TOKEN });\n      const res = await keysVerifyKey(unkey, { key });\n      if (!res.ok) {\n        logger.error(\"Unkey Error {*}\", { ...res.error });\n        return {\n          result: { valid: false, ownerId: undefined },\n          error: { message: \"Invalid API verification\" },\n        };\n      }\n      return {\n        result: {\n          valid: res.value.data.valid,\n          ownerId: res.value.data.identity?.externalId,\n          authMethod: \"unkey\",\n        },\n        error: undefined,\n      };\n    }\n    // Special bypass for our workspace\n    if (key.startsWith(\"sa_\") && key === env.SUPER_ADMIN_TOKEN) {\n      return {\n        result: { valid: true, ownerId: \"1\", authMethod: \"super_admin\" },\n      };\n    }\n    // In production, we only accept Unkey keys\n    throw new OpenStatusApiError({\n      code: \"UNAUTHORIZED\",\n      message: \"Invalid API Key\",\n    });\n  }\n\n  // In dev / test mode we can use the key as the ownerId\n  return { result: { valid: true, ownerId: key, authMethod: \"dev\" } };\n}\n"
  },
  {
    "path": "apps/server/src/libs/middlewares/index.ts",
    "content": "export * from \"./auth\";\nexport * from \"./track\";\nexport * from \"./plan\";\n"
  },
  {
    "path": "apps/server/src/libs/middlewares/plan.ts",
    "content": "import type { Variables } from \"@/types\";\nimport {\n  type Workspace,\n  workspacePlanHierarchy,\n} from \"@openstatus/db/src/schema\";\nimport type { Context, Next } from \"hono\";\nimport { OpenStatusApiError } from \"../errors\";\n\n/**\n * Checks if the workspace has a minimum required plan to access the endpoint\n */\nexport function minPlanMiddleware({ plan }: { plan: Workspace[\"plan\"] }) {\n  return async (c: Context<{ Variables: Variables }, \"/*\">, next: Next) => {\n    const workspace = c.get(\"workspace\");\n\n    if (workspacePlanHierarchy[workspace.plan] < workspacePlanHierarchy[plan]) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"You need to upgrade your plan to access this feature\",\n      });\n    }\n\n    await next();\n  };\n}\n"
  },
  {
    "path": "apps/server/src/libs/middlewares/track.ts",
    "content": "import type { Variables } from \"@/types\";\nimport { getLogger } from \"@logtape/logtape\";\nimport {\n  type EventProps,\n  parseInputToProps,\n  setupAnalytics,\n} from \"@openstatus/analytics\";\nimport type { Context, Next } from \"hono\";\n\nconst logger = getLogger(\"api-server\");\n\nexport function trackMiddleware(event: EventProps, eventProps?: string[]) {\n  return async (c: Context<{ Variables: Variables }, \"/*\">, next: Next) => {\n    await next();\n\n    // REMINDER: only track the event if the request was successful\n    const isValid = c.res.status.toString().startsWith(\"2\") && !c.error;\n\n    if (isValid) {\n      // We have checked the request to be valid already\n      let json: unknown;\n      if (c.req.raw.bodyUsed) {\n        try {\n          json = await c.req.json();\n        } catch {\n          json = {};\n        }\n      }\n      const additionalProps = parseInputToProps(json, eventProps);\n      const workspace = c.get(\"workspace\");\n\n      setupAnalytics({\n        userId: `api_${workspace.id}`,\n        workspaceId: `${workspace.id}`,\n        plan: workspace.plan,\n        location: c.req.raw.headers.get(\"x-forwarded-for\") ?? undefined,\n        userAgent: c.req.raw.headers.get(\"user-agent\") ?? undefined,\n      })\n        .then((analytics) => analytics.track({ ...event, additionalProps }))\n        .catch(() => {\n          logger.warn(\n            \"Failed to send analytics event {event} for workspace {workspaceId}\",\n            { event: event.name, workspaceId: workspace.id },\n          );\n        });\n    }\n  };\n}\n"
  },
  {
    "path": "apps/server/src/libs/test/preload.ts",
    "content": "import { mock } from \"bun:test\";\n\n// Subscription dispatch spies — accessible in tests via globalThis.__subscriptionSpies\nconst dispatchStatusReportUpdateSpy = mock((_id: number) => Promise.resolve());\nconst dispatchMaintenanceUpdateSpy = mock((_id: number) => Promise.resolve());\n\n(globalThis as Record<string, unknown>).__subscriptionSpies = {\n  dispatchStatusReportUpdate: dispatchStatusReportUpdateSpy,\n  dispatchMaintenanceUpdate: dispatchMaintenanceUpdateSpy,\n};\n\nmock.module(\"@openstatus/subscriptions\", () => ({\n  dispatchStatusReportUpdate: dispatchStatusReportUpdateSpy,\n  dispatchMaintenanceUpdate: dispatchMaintenanceUpdateSpy,\n}));\n\nconst testRedisStore = new Map<string, string>();\n(globalThis as Record<string, unknown>).__testRedisStore = testRedisStore;\n\nmock.module(\"@openstatus/upstash\", () => ({\n  Redis: {\n    fromEnv() {\n      return {\n        get: (key: string) => Promise.resolve(testRedisStore.get(key) ?? null),\n        set: (key: string, value: string) => {\n          testRedisStore.set(key, value);\n          return Promise.resolve(\"OK\");\n        },\n        del: (key: string) => {\n          const existed = testRedisStore.has(key) ? 1 : 0;\n          testRedisStore.delete(key);\n          return Promise.resolve(existed);\n        },\n        getdel: (key: string) => {\n          const value = testRedisStore.get(key) ?? null;\n          testRedisStore.delete(key);\n          return Promise.resolve(value);\n        },\n        expire: (_key: string, _seconds: number) => {\n          return Promise.resolve(1);\n        },\n      };\n    },\n  },\n}));\n\nmock.module(\"@openstatus/tinybird\", () => ({\n  OSTinybird: class {\n    get legacy_httpStatus45d() {\n      return () => Promise.resolve({ data: [] });\n    }\n    get legacy_tcpStatus45d() {\n      return () => Promise.resolve({ data: [] });\n    }\n    // HTTP metrics for GetMonitorSummary\n    get httpMetricsDaily() {\n      return () => Promise.resolve({ data: [] });\n    }\n    get httpMetricsWeekly() {\n      return () => Promise.resolve({ data: [] });\n    }\n    get httpMetricsBiweekly() {\n      return () => Promise.resolve({ data: [] });\n    }\n    // TCP metrics for GetMonitorSummary\n    get tcpMetricsDaily() {\n      return () => Promise.resolve({ data: [] });\n    }\n    get tcpMetricsWeekly() {\n      return () => Promise.resolve({ data: [] });\n    }\n    get tcpMetricsBiweekly() {\n      return () => Promise.resolve({ data: [] });\n    }\n    // DNS metrics for GetMonitorSummary\n    get dnsMetricsDaily() {\n      return () => Promise.resolve({ data: [] });\n    }\n    get dnsMetricsWeekly() {\n      return () => Promise.resolve({ data: [] });\n    }\n    get dnsMetricsBiweekly() {\n      return () => Promise.resolve({ data: [] });\n    }\n  },\n}));\n"
  },
  {
    "path": "apps/server/src/routes/public/index.ts",
    "content": "import { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport { timing } from \"hono/timing\";\n\nimport { status } from \"./status\";\nimport { unsubscribe } from \"./unsubscribe\";\n\nexport const publicRoute = new Hono();\npublicRoute.use(\"*\", cors());\npublicRoute.use(\"*\", timing());\n\npublicRoute.route(\"/status\", status);\npublicRoute.route(\"/unsubscribe\", unsubscribe);\n"
  },
  {
    "path": "apps/server/src/routes/public/status.test.ts",
    "content": "import {\n  afterAll,\n  beforeAll,\n  beforeEach,\n  describe,\n  expect,\n  test,\n} from \"bun:test\";\nimport { app } from \"@/index\";\nimport { db, eq } from \"@openstatus/db\";\n\nconst testRedisStore = (globalThis as Record<string, unknown>)\n  .__testRedisStore as Map<string, string> | undefined;\nimport {\n  incidentTable,\n  maintenance,\n  monitor,\n  page,\n  pageComponent,\n  statusReport,\n} from \"@openstatus/db/src/schema\";\n\n/**\n * Status Route Tests: Verify the status route uses pageComponents with single DB call\n *\n * These tests verify that the /public/status/:slug endpoint:\n * - Uses pageComponents instead of monitorsToPages\n * - Makes a single database query\n * - Correctly filters for active monitors only\n * - Correctly identifies ongoing incidents\n * - Correctly identifies unresolved status reports\n * - Correctly identifies ongoing maintenances\n * - Returns correct status based on the Tracker logic\n */\n\nconst TEST_PREFIX = \"status-test\";\nlet testPageId: number;\nlet testMonitorId: number;\nlet testMonitor2Id: number;\nlet testIncidentId: number;\nlet testStatusReportId: number;\nlet testMaintenanceId: number;\n\nbeforeAll(async () => {\n  // Clean up any existing test data by slug/name before creating new ones\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-page`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-private-page`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-cache-test`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor-1`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor-2`));\n\n  // Create test page\n  const testPage = await db\n    .insert(page)\n    .values({\n      workspaceId: 1,\n      title: \"Status Test Page\",\n      description: \"A test page for status route tests\",\n      slug: `${TEST_PREFIX}-page`,\n      customDomain: \"\",\n      accessType: \"public\",\n    })\n    .returning()\n    .get();\n  testPageId = testPage.id;\n\n  // Create first test monitor (active)\n  const testMonitor = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-monitor-1`,\n      url: \"https://status-test-1.example.com\",\n      periodicity: \"1m\",\n      active: true,\n      regions: \"ams\",\n      jobType: \"http\",\n    })\n    .returning()\n    .get();\n  testMonitorId = testMonitor.id;\n\n  // Create second test monitor (inactive)\n  const testMonitor2 = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-monitor-2`,\n      url: \"https://status-test-2.example.com\",\n      periodicity: \"1m\",\n      active: false, // Inactive monitor\n      regions: \"ams\",\n      jobType: \"http\",\n    })\n    .returning()\n    .get();\n  testMonitor2Id = testMonitor2.id;\n\n  // Create page components for both monitors\n  await db.insert(pageComponent).values({\n    workspaceId: 1,\n    pageId: testPageId,\n    monitorId: testMonitorId,\n    type: \"monitor\",\n    name: `${TEST_PREFIX}-monitor-1`,\n    order: 1,\n  });\n\n  await db.insert(pageComponent).values({\n    workspaceId: 1,\n    pageId: testPageId,\n    monitorId: testMonitor2Id,\n    type: \"monitor\",\n    name: `${TEST_PREFIX}-monitor-2`,\n    order: 2,\n  });\n});\n\nbeforeEach(() => {\n  testRedisStore?.clear();\n});\n\nafterAll(async () => {\n  // Clean up test data\n  if (testIncidentId) {\n    await db\n      .delete(incidentTable)\n      .where(eq(incidentTable.id, testIncidentId))\n      .catch(() => {});\n  }\n  if (testStatusReportId) {\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, testStatusReportId))\n      .catch(() => {});\n  }\n  if (testMaintenanceId) {\n    await db\n      .delete(maintenance)\n      .where(eq(maintenance.id, testMaintenanceId))\n      .catch(() => {});\n  }\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.pageId, testPageId))\n    .catch(() => {});\n  await db\n    .delete(page)\n    .where(eq(page.id, testPageId))\n    .catch(() => {});\n  await db\n    .delete(monitor)\n    .where(eq(monitor.id, testMonitorId))\n    .catch(() => {});\n  await db\n    .delete(monitor)\n    .where(eq(monitor.id, testMonitor2Id))\n    .catch(() => {});\n});\n\ndescribe(\"Status Route: Basic functionality\", () => {\n  test(\"returns operational status for page with no incidents\", async () => {\n    const res = await app.request(`/public/status/${TEST_PREFIX}-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.status).toBe(\"operational\");\n  });\n\n  test(\"returns unknown status for non-existent page\", async () => {\n    const res = await app.request(\"/public/status/non-existent-page\");\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.status).toBe(\"unknown\");\n  });\n\n  test(\"returns unknown status for non-public page\", async () => {\n    // Create a private page\n    const privatePage = await db\n      .insert(page)\n      .values({\n        workspaceId: 1,\n        title: \"Private Test Page\",\n        description: \"A private test page\",\n        slug: `${TEST_PREFIX}-private-page`,\n        customDomain: \"\",\n        accessType: \"password\",\n        password: \"secret\",\n      })\n      .returning()\n      .get();\n\n    const res = await app.request(`/public/status/${TEST_PREFIX}-private-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.status).toBe(\"unknown\");\n\n    // Clean up\n    await db.delete(page).where(eq(page.id, privatePage.id));\n  });\n});\n\ndescribe(\"Status Route: Active monitor filtering\", () => {\n  test(\"only considers active monitors for status calculation\", async () => {\n    // Create an incident for the inactive monitor\n    const inactiveIncident = await db\n      .insert(incidentTable)\n      .values({\n        monitorId: testMonitor2Id,\n        title: \"Inactive Monitor Incident\",\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n\n    const res = await app.request(`/public/status/${TEST_PREFIX}-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Status should still be operational because the monitor is inactive\n    expect(data.status).toBe(\"operational\");\n\n    // Clean up\n    await db\n      .delete(incidentTable)\n      .where(eq(incidentTable.id, inactiveIncident.id));\n  });\n});\n\ndescribe(\"Status Route: Incident detection\", () => {\n  test(\"returns incident status with ongoing incident\", async () => {\n    // Create an ongoing incident for the active monitor\n    const incident = await db\n      .insert(incidentTable)\n      .values({\n        monitorId: testMonitorId,\n        title: \"Test Incident\",\n        status: \"investigating\",\n        // resolvedAt is null, meaning it's ongoing\n      })\n      .returning()\n      .get();\n    testIncidentId = incident.id;\n\n    const res = await app.request(`/public/status/${TEST_PREFIX}-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.status).toBe(\"incident\");\n\n    // Clean up\n    await db.delete(incidentTable).where(eq(incidentTable.id, testIncidentId));\n    testIncidentId = 0;\n  });\n\n  test(\"ignores resolved incidents\", async () => {\n    // First clean up the ongoing incident from previous test if it still exists\n    if (testIncidentId) {\n      await db\n        .delete(incidentTable)\n        .where(eq(incidentTable.id, testIncidentId))\n        .catch(() => {});\n      testIncidentId = 0;\n    }\n\n    // Create a resolved incident\n    const resolvedIncident = await db\n      .insert(incidentTable)\n      .values({\n        monitorId: testMonitorId,\n        title: \"Resolved Incident\",\n        status: \"resolved\",\n        resolvedAt: new Date(),\n      })\n      .returning()\n      .get();\n\n    const res = await app.request(`/public/status/${TEST_PREFIX}-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Status should be operational because the incident is resolved\n    expect(data.status).toBe(\"operational\");\n\n    // Clean up\n    await db\n      .delete(incidentTable)\n      .where(eq(incidentTable.id, resolvedIncident.id));\n  });\n});\n\ndescribe(\"Status Route: Status report detection\", () => {\n  test(\"returns degraded_performance status with unresolved status report\", async () => {\n    // Create an unresolved status report\n    const report = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 1,\n        pageId: testPageId,\n        title: \"Test Status Report\",\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n    testStatusReportId = report.id;\n\n    const res = await app.request(`/public/status/${TEST_PREFIX}-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.status).toBe(\"degraded_performance\");\n\n    // Clean up\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, testStatusReportId));\n    testStatusReportId = 0;\n  });\n\n  test(\"ignores resolved status reports\", async () => {\n    // First clean up the ongoing status report from previous test if it still exists\n    if (testStatusReportId) {\n      await db\n        .delete(statusReport)\n        .where(eq(statusReport.id, testStatusReportId))\n        .catch(() => {});\n      testStatusReportId = 0;\n    }\n\n    // Create a resolved status report\n    const resolvedReport = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 1,\n        pageId: testPageId,\n        title: \"Resolved Status Report\",\n        status: \"resolved\",\n      })\n      .returning()\n      .get();\n\n    const res = await app.request(`/public/status/${TEST_PREFIX}-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Status should be operational because the report is resolved\n    expect(data.status).toBe(\"operational\");\n\n    // Clean up\n    await db.delete(statusReport).where(eq(statusReport.id, resolvedReport.id));\n  });\n});\n\ndescribe(\"Status Route: Maintenance detection\", () => {\n  test(\"returns under_maintenance status with ongoing maintenance\", async () => {\n    // Create an ongoing maintenance\n    const now = new Date();\n    const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);\n    const oneHourFromNow = new Date(now.getTime() + 60 * 60 * 1000);\n\n    const maint = await db\n      .insert(maintenance)\n      .values({\n        workspaceId: 1,\n        pageId: testPageId,\n        title: \"Test Maintenance\",\n        message: \"Ongoing maintenance\",\n        from: oneHourAgo,\n        to: oneHourFromNow,\n      })\n      .returning()\n      .get();\n    testMaintenanceId = maint.id;\n\n    const res = await app.request(`/public/status/${TEST_PREFIX}-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.status).toBe(\"under_maintenance\");\n\n    // Clean up\n    await db.delete(maintenance).where(eq(maintenance.id, testMaintenanceId));\n    testMaintenanceId = 0;\n  });\n\n  test(\"ignores past maintenances\", async () => {\n    // First clean up the ongoing maintenance from previous test if it still exists\n    if (testMaintenanceId) {\n      await db\n        .delete(maintenance)\n        .where(eq(maintenance.id, testMaintenanceId))\n        .catch(() => {});\n      testMaintenanceId = 0;\n    }\n\n    // Create a past maintenance\n    const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);\n    const oneDayAgo = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000);\n\n    const pastMaint = await db\n      .insert(maintenance)\n      .values({\n        workspaceId: 1,\n        pageId: testPageId,\n        title: \"Past Maintenance\",\n        message: \"Past maintenance\",\n        from: twoDaysAgo,\n        to: oneDayAgo,\n      })\n      .returning()\n      .get();\n\n    const res = await app.request(`/public/status/${TEST_PREFIX}-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Status should be operational because the maintenance is in the past\n    expect(data.status).toBe(\"operational\");\n\n    // Clean up\n    await db.delete(maintenance).where(eq(maintenance.id, pastMaint.id));\n  });\n\n  test(\"ignores future maintenances\", async () => {\n    // First clean up any ongoing maintenance from previous test if it still exists\n    if (testMaintenanceId) {\n      await db\n        .delete(maintenance)\n        .where(eq(maintenance.id, testMaintenanceId))\n        .catch(() => {});\n      testMaintenanceId = 0;\n    }\n\n    // Create a future maintenance\n    const oneDayFromNow = new Date(Date.now() + 1 * 24 * 60 * 60 * 1000);\n    const twoDaysFromNow = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000);\n\n    const futureMaint = await db\n      .insert(maintenance)\n      .values({\n        workspaceId: 1,\n        pageId: testPageId,\n        title: \"Future Maintenance\",\n        message: \"Future maintenance\",\n        from: oneDayFromNow,\n        to: twoDaysFromNow,\n      })\n      .returning()\n      .get();\n\n    const res = await app.request(`/public/status/${TEST_PREFIX}-page`);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Status should be operational because the maintenance is in the future\n    expect(data.status).toBe(\"operational\");\n\n    // Clean up\n    await db.delete(maintenance).where(eq(maintenance.id, futureMaint.id));\n  });\n});\n\ndescribe(\"Status Route: Cache functionality\", () => {\n  test(\"returns cached status on second request if cache is available\", async () => {\n    const slug = `${TEST_PREFIX}-cache-test`;\n\n    // Create a test page for cache testing\n    const cachePage = await db\n      .insert(page)\n      .values({\n        workspaceId: 1,\n        title: \"Cache Test Page\",\n        description: \"A test page for cache testing\",\n        slug,\n        customDomain: \"\",\n        accessType: \"public\",\n      })\n      .returning()\n      .get();\n\n    // First request should hit the database\n    const res1 = await app.request(`/public/status/${slug}`);\n    expect(res1.status).toBe(200);\n    const data1 = await res1.json();\n    expect(data1.status).toBe(\"operational\");\n    expect(res1.headers.get(\"OpenStatus-Cache\")).toBeNull();\n\n    // Second request may hit the cache if Redis is configured\n    const res2 = await app.request(`/public/status/${slug}`);\n    expect(res2.status).toBe(200);\n    const data2 = await res2.json();\n    expect(data2.status).toBe(\"operational\");\n    // Cache header may be \"HIT\" if Redis is available, or null if not\n    const cacheHeader = res2.headers.get(\"OpenStatus-Cache\");\n    if (cacheHeader !== null) {\n      expect(cacheHeader).toBe(\"HIT\");\n    }\n\n    // Clean up\n    await db.delete(page).where(eq(page.id, cachePage.id));\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/public/status.ts",
    "content": "import { getLogger } from \"@logtape/logtape\";\nimport { Hono } from \"hono\";\nimport { endTime, setMetric, startTime } from \"hono/timing\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { page } from \"@openstatus/db/src/schema\";\n\nconst logger = getLogger(\"api-server\");\nimport { Status, Tracker } from \"@openstatus/tracker\";\n\nimport { redis } from \"@/libs/clients\";\n\n// TODO: include ratelimiting\n\nexport const status = new Hono();\n\nstatus.get(\"/:slug\", async (c) => {\n  try {\n    const { slug } = c.req.param();\n\n    const cache = await redis.get(slug);\n\n    if (cache) {\n      setMetric(c, \"OpenStatus-Cache\", \"HIT\");\n      return c.json({ status: cache });\n    }\n\n    startTime(c, \"database\");\n\n    // Single query with all relations\n    const currentPage = await db.query.page.findFirst({\n      where: eq(page.slug, slug),\n      with: {\n        pageComponents: {\n          with: {\n            monitor: {\n              with: {\n                incidents: true,\n              },\n            },\n          },\n        },\n        statusReports: true,\n        maintenances: true,\n      },\n    });\n\n    endTime(c, \"database\");\n\n    if (!currentPage) {\n      return c.json({ status: Status.Unknown });\n    }\n\n    if (currentPage.accessType !== \"public\") {\n      return c.json({ status: Status.Unknown });\n    }\n\n    // Extract active monitor components\n    const monitorComponents = currentPage.pageComponents.filter(\n      (c) =>\n        c.type === \"monitor\" &&\n        c.monitor &&\n        c.monitor.active &&\n        !c.monitor.deletedAt,\n    );\n\n    // Extract all ongoing incidents from active monitors\n    const ongoingIncidents = monitorComponents.flatMap(\n      (c) => c.monitor?.incidents?.filter((inc) => !inc.resolvedAt) ?? [],\n    );\n\n    // Filter for unresolved status reports\n    const unresolvedStatusReports = currentPage.statusReports.filter(\n      (report) => report.status !== \"resolved\",\n    );\n\n    // Filter for ongoing maintenances\n    const now = new Date();\n    const ongoingMaintenances = currentPage.maintenances.filter(\n      (m) => m.from <= now && m.to >= now,\n    );\n\n    // Use the tracker to determine status\n    const tracker = new Tracker({\n      incidents: ongoingIncidents,\n      statusReports: unresolvedStatusReports,\n      maintenances: ongoingMaintenances,\n    });\n\n    const status = tracker.currentStatus;\n    await redis.set(slug, status, { ex: 60 }); // 1m cache\n\n    return c.json({ status });\n  } catch (e) {\n    logger.error(\"Error in public status page\", {\n      error: e instanceof Error ? e.message : String(e),\n      stack: e instanceof Error ? e.stack : undefined,\n    });\n    return c.json({ status: Status.Unknown });\n  }\n});\n"
  },
  {
    "path": "apps/server/src/routes/public/unsubscribe.ts",
    "content": "import { getLogger } from \"@logtape/logtape\";\nimport { Hono } from \"hono\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { pageSubscriber } from \"@openstatus/db/src/schema\";\n\nconst logger = getLogger(\"api-server\");\n\n/**\n * RFC 8058 One-Click Unsubscribe Endpoint\n *\n * This endpoint handles POST requests from email clients that support one-click unsubscribe.\n * Email clients send a POST request with form-urlencoded body containing \"List-Unsubscribe=One-Click\".\n *\n * @see https://datatracker.ietf.org/doc/html/rfc8058\n */\nexport const unsubscribe = new Hono();\n\nunsubscribe.post(\"/:token\", async (c) => {\n  try {\n    const { token } = c.req.param();\n\n    // Validate token is a valid UUID format\n    const uuidRegex =\n      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n    if (!uuidRegex.test(token)) {\n      return c.json({ error: \"Invalid token format\" }, 400);\n    }\n\n    // Find the subscriber by token\n    const subscriber = await db\n      .select()\n      .from(pageSubscriber)\n      .where(eq(pageSubscriber.token, token))\n      .get();\n\n    // Return 404 if subscriber not found\n    if (!subscriber) {\n      return c.json({ error: \"Subscriber not found\" }, 404);\n    }\n\n    // Check if subscriber has verified their subscription\n    if (!subscriber.acceptedAt) {\n      return c.json({ error: \"Subscription not verified\" }, 400);\n    }\n\n    // Check if already unsubscribed\n    if (subscriber.unsubscribedAt) {\n      // Return 200 OK even if already unsubscribed (idempotent)\n      return c.json({ message: \"Already unsubscribed\" }, 200);\n    }\n\n    // Set unsubscribedAt timestamp\n    await db\n      .update(pageSubscriber)\n      .set({ unsubscribedAt: new Date() })\n      .where(eq(pageSubscriber.id, subscriber.id));\n\n    // Return 200 OK on success\n    return c.json({ message: \"Successfully unsubscribed\" }, 200);\n  } catch (e) {\n    logger.error(\"Error in one-click unsubscribe\", {\n      error: e instanceof Error ? e.message : String(e),\n      stack: e instanceof Error ? e.stack : undefined,\n    });\n    return c.json({ error: \"Internal server error\" }, 500);\n  }\n});\n"
  },
  {
    "path": "apps/server/src/routes/rpc/index.ts",
    "content": "import {\n  universalServerRequestFromFetch,\n  universalServerResponseToFetch,\n} from \"@connectrpc/connect/protocol\";\nimport type { Hono } from \"hono\";\n\nimport { routes } from \"./router\";\n\n// Re-export for external use\nexport { routes } from \"./router\";\nexport { getRpcContext } from \"./interceptors\";\nexport type { RpcContext } from \"./interceptors\";\n\n/**\n * Mount ConnectRPC routes on a Hono app at /rpc prefix.\n *\n * @param app - The Hono app instance\n */\nexport function mountRpcRoutes(\n  app: Hono<{\n    Variables: {\n      event: Record<string, unknown>;\n    };\n  }>,\n) {\n  // Handle all RPC routes at /rpc/* prefix\n  app.all(\"/rpc/*\", async (c) => {\n    const url = new URL(c.req.url);\n    // Remove the /rpc prefix from the path for matching\n    const pathWithoutPrefix = url.pathname.replace(/^\\/rpc/, \"\");\n\n    // Find the handler that matches this request\n    const handler = routes.handlers.find(\n      (h) => h.requestPath === pathWithoutPrefix,\n    );\n\n    if (!handler) {\n      return c.json({ error: \"Not found\" }, 404);\n    }\n\n    // Check if the HTTP method is allowed\n    if (!handler.allowedMethods.includes(c.req.method)) {\n      return c.json({ error: \"Method not allowed\" }, 405);\n    }\n\n    // Convert fetch Request to universal request\n    const universalRequest = universalServerRequestFromFetch(c.req.raw, {});\n\n    // Call the handler\n    const universalResponse = await handler(universalRequest);\n\n    // Convert universal response back to fetch Response\n    return universalServerResponseToFetch(universalResponse);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/interceptors/auth.ts",
    "content": "import {\n  Code,\n  ConnectError,\n  type Interceptor,\n  createContextKey,\n} from \"@connectrpc/connect\";\nimport type { Workspace } from \"@openstatus/db/src/schema\";\nimport { nanoid } from \"nanoid\";\n\nimport { lookupWorkspace, validateKey } from \"@/libs/middlewares/auth\";\n\n/**\n * RPC context containing workspace and request information.\n * This is set by the auth interceptor and available to all handlers.\n */\nexport interface RpcContext {\n  workspace: Workspace;\n  requestId: string;\n}\n\n/**\n * Context key for storing RPC context in request context values.\n */\nexport const RPC_CONTEXT_KEY = createContextKey<RpcContext | undefined>(\n  undefined,\n);\n\n/**\n * Authentication interceptor for ConnectRPC.\n * Validates the x-openstatus-key header and sets workspace context.\n * Skips authentication for HealthService endpoints.\n */\nexport function authInterceptor(): Interceptor {\n  return (next) => async (req) => {\n    // Skip auth for HealthService\n    if (req.service.typeName === \"openstatus.health.v1.HealthService\") {\n      return next(req);\n    }\n\n    const apiKey = req.header.get(\"x-openstatus-key\");\n\n    if (!apiKey) {\n      throw new ConnectError(\n        \"Missing 'x-openstatus-key' header\",\n        Code.Unauthenticated,\n      );\n    }\n\n    const { error, result } = await validateKey(apiKey);\n\n    if (error) {\n      throw new ConnectError(error.message, Code.Unauthenticated);\n    }\n\n    if (!result.valid || !result.ownerId) {\n      throw new ConnectError(\"Invalid API Key\", Code.Unauthenticated);\n    }\n\n    const ownerId = Number.parseInt(result.ownerId);\n\n    if (Number.isNaN(ownerId)) {\n      throw new ConnectError(\"Invalid API Key format\", Code.Unauthenticated);\n    }\n\n    // lookupWorkspace throws OpenStatusApiError if not found\n    // The error interceptor will convert it to ConnectError\n    const workspace = await lookupWorkspace(ownerId);\n\n    // Generate request ID if not provided\n    const requestId = req.header.get(\"x-request-id\") ?? nanoid();\n\n    // Store context for handlers to access\n    const rpcContext: RpcContext = {\n      workspace,\n      requestId,\n    };\n\n    // Set context using ConnectRPC's context values\n    req.contextValues.set(RPC_CONTEXT_KEY, rpcContext);\n\n    return next(req);\n  };\n}\n\n/**\n * Helper to get RPC context from handler context.\n */\nexport function getRpcContext(ctx: {\n  values: { get: <T>(key: { id: symbol; defaultValue: T }) => T };\n}): RpcContext {\n  const rpcCtx = ctx.values.get(RPC_CONTEXT_KEY);\n  if (!rpcCtx) {\n    throw new ConnectError(\n      \"RPC context not found - auth interceptor may not have run\",\n      Code.Internal,\n    );\n  }\n  return rpcCtx;\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/interceptors/error.ts",
    "content": "import { Code, ConnectError, type Interceptor } from \"@connectrpc/connect\";\nimport { getLogger } from \"@logtape/logtape\";\nimport type { ErrorCode } from \"@openstatus/error\";\n\nimport { OpenStatusApiError } from \"@/libs/errors\";\nimport { RPC_CONTEXT_KEY } from \"./auth\";\n\nconst logger = getLogger(\"api-server\");\n\n/**\n * Mapping from OpenStatus error codes to ConnectRPC codes.\n */\nconst ERROR_CODE_MAP: Record<ErrorCode, Code> = {\n  BAD_REQUEST: Code.InvalidArgument,\n  UNAUTHORIZED: Code.Unauthenticated,\n  PAYMENT_REQUIRED: Code.ResourceExhausted,\n  FORBIDDEN: Code.PermissionDenied,\n  NOT_FOUND: Code.NotFound,\n  METHOD_NOT_ALLOWED: Code.Unimplemented,\n  CONFLICT: Code.AlreadyExists,\n  UNPROCESSABLE_ENTITY: Code.InvalidArgument,\n  INTERNAL_SERVER_ERROR: Code.Internal,\n};\n\n/**\n * Error mapping interceptor for ConnectRPC.\n * Converts OpenStatusApiError to ConnectError with appropriate codes.\n * Logs server errors and passes through client errors.\n */\nexport function errorInterceptor(): Interceptor {\n  return (next) => async (req) => {\n    try {\n      return await next(req);\n    } catch (error) {\n      const rpcCtx = req.contextValues.get(RPC_CONTEXT_KEY);\n\n      // Already a ConnectError, pass through\n      if (error instanceof ConnectError) {\n        throw error;\n      }\n\n      // Map OpenStatusApiError to ConnectError\n      if (error instanceof OpenStatusApiError) {\n        const code = ERROR_CODE_MAP[error.code] ?? Code.Internal;\n\n        // Log server errors (5xx equivalent)\n        if (error.status >= 500) {\n          logger.error(\"RPC server error\", {\n            error: {\n              code: error.code,\n              message: error.message,\n            },\n            requestId: rpcCtx?.requestId,\n          });\n        }\n\n        throw new ConnectError(error.message, code);\n      }\n\n      // Unknown error - log and wrap as Internal\n      logger.error(\"RPC unexpected error\", {\n        error: {\n          name: error instanceof Error ? error.name : \"Unknown\",\n          message: error instanceof Error ? error.message : String(error),\n          stack: error instanceof Error ? error.stack : undefined,\n        },\n        requestId: rpcCtx?.requestId,\n      });\n\n      throw new ConnectError(\n        error instanceof Error ? error.message : \"Internal server error\",\n        Code.Internal,\n      );\n    }\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/interceptors/index.ts",
    "content": "export { authInterceptor, getRpcContext, RPC_CONTEXT_KEY } from \"./auth\";\nexport type { RpcContext } from \"./auth\";\nexport { loggingInterceptor } from \"./logging\";\nexport { errorInterceptor } from \"./error\";\nexport { validationInterceptor } from \"./validation\";\n"
  },
  {
    "path": "apps/server/src/routes/rpc/interceptors/logging.ts",
    "content": "import type { Interceptor } from \"@connectrpc/connect\";\nimport { getLogger, withContext } from \"@logtape/logtape\";\n\nimport { env } from \"@/env\";\nimport { RPC_CONTEXT_KEY } from \"./auth\";\n\nconst logger = getLogger(\"api-server-otel\");\n\n/**\n * Logging interceptor for ConnectRPC.\n * Implements wide events pattern - emits ONE canonical log line per RPC request.\n * All context is collected during execution and emitted at completion.\n */\nexport function loggingInterceptor(): Interceptor {\n  return (next) => async (req) => {\n    const rpcCtx = req.contextValues.get(RPC_CONTEXT_KEY);\n\n    const serviceName = req.service.typeName;\n    const methodName = req.method.name;\n    const startTime = Date.now();\n\n    // Initialize wide event - will be emitted once at completion\n    const event: Record<string, unknown> = {\n      timestamp: new Date().toISOString(),\n      request_id: rpcCtx?.requestId ?? \"unknown\",\n      protocol: \"connectrpc\",\n      service: serviceName,\n      method: methodName,\n      // Business context\n      // Environment characteristics\n      environment: env.NODE_ENV,\n      region: env.FLY_REGION,\n    };\n\n    // Wrap in LogTape context for correlation\n    return withContext(\n      {\n        requestId: rpcCtx?.requestId ?? \"unknown\",\n        service: serviceName,\n        method: methodName,\n        workspaceId: rpcCtx?.workspace.id,\n        protocol: \"connectrpc\",\n      },\n      async () => {\n        try {\n          const response = await next(req);\n\n          event.duration_ms = Date.now() - startTime;\n          event.outcome = \"success\";\n          event.workspace_id = rpcCtx?.workspace.id;\n          event.workspace_plan = rpcCtx?.workspace.plan;\n\n          return response;\n        } catch (error) {\n          event.duration_ms = Date.now() - startTime;\n          event.outcome = \"error\";\n          event.error = {\n            type: error instanceof Error ? error.name : \"UnknownError\",\n            message: error instanceof Error ? error.message : String(error),\n          };\n\n          throw error;\n        } finally {\n          // Emit single canonical log line\n          logger.info(\"rpc_request\", { ...event });\n        }\n      },\n    );\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/interceptors/validation.ts",
    "content": "import type { Interceptor } from \"@connectrpc/connect\";\nimport { createValidateInterceptor } from \"@connectrpc/validate\";\n\n// Methods that skip standard protovalidate (they do manual validation in handlers)\n// These methods use partial updates where nested message fields are optional\nconst SKIP_VALIDATION_METHODS = new Set([\n  \"UpdateHTTPMonitor\",\n  \"UpdateTCPMonitor\",\n  \"UpdateDNSMonitor\",\n]);\n\n/**\n * Validation interceptor for ConnectRPC using protovalidate.\n * Validates incoming request messages against their proto constraints.\n *\n * Uses @connectrpc/validate which provides a proper interceptor that:\n * - Validates request messages using protovalidate rules\n * - Returns InvalidArgument error for validation failures\n * - Works with all message types defined with buf.validate constraints\n *\n * Note: Update methods skip validation because they support partial updates\n * where nested message fields are optional. Validation for these methods\n * is done in the service handlers.\n */\nexport function validationInterceptor(): Interceptor {\n  const baseInterceptor = createValidateInterceptor();\n  return (next) => async (req) => {\n    // Skip validation for update methods that support partial updates\n    if (SKIP_VALIDATION_METHODS.has(req.method.name)) {\n      return next(req);\n    }\n    return baseInterceptor(next)(req);\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/router.ts",
    "content": "import { createConnectRouter } from \"@connectrpc/connect\";\nimport { HealthService } from \"@openstatus/proto/health/v1\";\nimport { MaintenanceService } from \"@openstatus/proto/maintenance/v1\";\nimport { MonitorService } from \"@openstatus/proto/monitor/v1\";\nimport { NotificationService } from \"@openstatus/proto/notification/v1\";\nimport { StatusPageService } from \"@openstatus/proto/status_page/v1\";\nimport { StatusReportService } from \"@openstatus/proto/status_report/v1\";\n\nimport {\n  authInterceptor,\n  errorInterceptor,\n  loggingInterceptor,\n  validationInterceptor,\n} from \"./interceptors\";\nimport { healthServiceImpl } from \"./services/health\";\nimport { maintenanceServiceImpl } from \"./services/maintenance\";\nimport { monitorServiceImpl } from \"./services/monitor\";\nimport { notificationServiceImpl } from \"./services/notification\";\nimport { statusPageServiceImpl } from \"./services/status-page\";\nimport { statusReportServiceImpl } from \"./services/status-report\";\n\n/**\n * Create ConnectRPC router with services.\n * Interceptors are applied in order (outermost to innermost):\n * 1. errorInterceptor - Catches all errors and maps to ConnectError\n * 2. loggingInterceptor - Logs requests/responses with duration\n * 3. authInterceptor - Validates API key and sets workspace context\n * 4. validationInterceptor - Validates request messages using protovalidate\n */\nexport const routes = createConnectRouter({\n  interceptors: [\n    errorInterceptor(),\n    loggingInterceptor(),\n    authInterceptor(),\n    validationInterceptor(),\n  ],\n})\n  .service(MonitorService, monitorServiceImpl)\n  .service(HealthService, healthServiceImpl)\n  .service(StatusReportService, statusReportServiceImpl)\n  .service(StatusPageService, statusPageServiceImpl)\n  .service(MaintenanceService, maintenanceServiceImpl)\n  .service(NotificationService, notificationServiceImpl);\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/health/index.ts",
    "content": "import type { ServiceImpl } from \"@connectrpc/connect\";\nimport {\n  CheckResponse_ServingStatus,\n  type HealthService,\n} from \"@openstatus/proto/health/v1\";\n\n/**\n * Health service implementation.\n * Provides a simple health check endpoint for load balancer probes.\n */\nexport const healthServiceImpl: ServiceImpl<typeof HealthService> = {\n  async check(_req) {\n    return {\n      status: CheckResponse_ServingStatus.SERVING,\n    };\n  },\n};\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/maintenance/__tests__/maintenance.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  maintenance,\n  maintenancesToPageComponents,\n  page,\n  pageComponent,\n  pageSubscriber,\n} from \"@openstatus/db/src/schema\";\n\nimport { app } from \"@/index\";\n\nconst subscriptionSpies = (globalThis as Record<string, unknown>)\n  .__subscriptionSpies as {\n  dispatchStatusReportUpdate: ReturnType<typeof import(\"bun:test\").mock>;\n  dispatchMaintenanceUpdate: ReturnType<typeof import(\"bun:test\").mock>;\n};\n\n/**\n * Helper to make ConnectRPC requests using the Connect protocol (JSON).\n * Connect uses POST with JSON body at /rpc/<service>/<method>\n */\nasync function connectRequest(\n  method: string,\n  body: Record<string, unknown> = {},\n  headers: Record<string, string> = {},\n) {\n  return app.request(\n    `/rpc/openstatus.maintenance.v1.MaintenanceService/${method}`,\n    {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        ...headers,\n      },\n      body: JSON.stringify(body),\n    },\n  );\n}\n\nconst TEST_PREFIX = \"rpc-maintenance-test\";\nlet testPageComponentId: number;\nlet testMaintenanceId: number;\nlet testMaintenanceToDeleteId: number;\nlet testMaintenanceToUpdateId: number;\nlet testMaintenanceForNotifyId: number;\nlet testSubscriberId: number;\n// For mixed-page validation tests\nlet testPage2Id: number;\nlet testPage2ComponentId: number;\n\nbeforeAll(async () => {\n  // Clean up any existing test data\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.title, `${TEST_PREFIX}-main`));\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.title, `${TEST_PREFIX}-to-delete`));\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.title, `${TEST_PREFIX}-to-update`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n\n  // Create a test page component (using existing page 1 from seed)\n  const component = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      type: \"static\",\n      name: `${TEST_PREFIX}-component`,\n      description: \"Test component for maintenance tests\",\n      order: 100,\n    })\n    .returning()\n    .get();\n  testPageComponentId = component.id;\n\n  // Create a second page and component for mixed-page validation tests\n  const page2 = await db\n    .insert(page)\n    .values({\n      workspaceId: 1,\n      title: `${TEST_PREFIX}-page-2`,\n      slug: `${TEST_PREFIX}-page-2-slug`,\n      description: \"Second test page for mixed-page tests\",\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n  testPage2Id = page2.id;\n\n  const component2 = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 1,\n      pageId: testPage2Id,\n      type: \"static\",\n      name: `${TEST_PREFIX}-component-2`,\n      description: \"Test component on page 2\",\n      order: 100,\n    })\n    .returning()\n    .get();\n  testPage2ComponentId = component2.id;\n\n  // Create test maintenance\n  const maintenanceRecord = await db\n    .insert(maintenance)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-main`,\n      message: \"Test maintenance message\",\n      from: new Date(Date.now() + 24 * 60 * 60 * 1000), // 1 day from now\n      to: new Date(Date.now() + 25 * 60 * 60 * 1000), // 25 hours from now\n    })\n    .returning()\n    .get();\n  testMaintenanceId = maintenanceRecord.id;\n\n  // Create page component association\n  await db.insert(maintenancesToPageComponents).values({\n    maintenanceId: maintenanceRecord.id,\n    pageComponentId: testPageComponentId,\n  });\n\n  // Create maintenance to delete\n  const deleteRecord = await db\n    .insert(maintenance)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-to-delete`,\n      message: \"Maintenance to delete\",\n      from: new Date(Date.now() + 48 * 60 * 60 * 1000),\n      to: new Date(Date.now() + 49 * 60 * 60 * 1000),\n    })\n    .returning()\n    .get();\n  testMaintenanceToDeleteId = deleteRecord.id;\n\n  // Create maintenance to update\n  const updateRecord = await db\n    .insert(maintenance)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-to-update`,\n      message: \"Maintenance to update\",\n      from: new Date(Date.now() + 72 * 60 * 60 * 1000),\n      to: new Date(Date.now() + 73 * 60 * 60 * 1000),\n    })\n    .returning()\n    .get();\n  testMaintenanceToUpdateId = updateRecord.id;\n\n  await db.insert(maintenancesToPageComponents).values({\n    maintenanceId: updateRecord.id,\n    pageComponentId: testPageComponentId,\n  });\n\n  // Create maintenance for notify tests\n  const notifyRecord = await db\n    .insert(maintenance)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-for-notify`,\n      message: \"Maintenance for notify tests\",\n      from: new Date(Date.now() + 96 * 60 * 60 * 1000),\n      to: new Date(Date.now() + 97 * 60 * 60 * 1000),\n    })\n    .returning()\n    .get();\n  testMaintenanceForNotifyId = notifyRecord.id;\n\n  await db.insert(maintenancesToPageComponents).values({\n    maintenanceId: notifyRecord.id,\n    pageComponentId: testPageComponentId,\n  });\n\n  // Create a verified subscriber for notification tests\n  const subscriber = await db\n    .insert(pageSubscriber)\n    .values({\n      pageId: 1,\n      email: `${TEST_PREFIX}@example.com`,\n      token: `${TEST_PREFIX}-token`,\n      acceptedAt: new Date(),\n    })\n    .returning()\n    .get();\n  testSubscriberId = subscriber.id;\n});\n\nafterAll(async () => {\n  // Clean up subscriber first (only if it was created)\n  if (testSubscriberId) {\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.id, testSubscriberId));\n  }\n\n  // Clean up associations\n  await db\n    .delete(maintenancesToPageComponents)\n    .where(eq(maintenancesToPageComponents.maintenanceId, testMaintenanceId));\n  await db\n    .delete(maintenancesToPageComponents)\n    .where(\n      eq(maintenancesToPageComponents.maintenanceId, testMaintenanceToUpdateId),\n    );\n  await db\n    .delete(maintenancesToPageComponents)\n    .where(\n      eq(\n        maintenancesToPageComponents.maintenanceId,\n        testMaintenanceForNotifyId,\n      ),\n    );\n\n  // Clean up maintenances\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.title, `${TEST_PREFIX}-main`));\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.title, `${TEST_PREFIX}-to-delete`));\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.title, `${TEST_PREFIX}-to-update`));\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.title, `${TEST_PREFIX}-for-notify`));\n\n  // Clean up page component\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n\n  // Clean up second page component and page (for mixed-page tests)\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component-2`));\n  await db.delete(page).where(eq(page.title, `${TEST_PREFIX}-page-2`));\n});\n\ndescribe(\"MaintenanceService.CreateMaintenance\", () => {\n  test(\"creates a new maintenance\", async () => {\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-created`,\n        message: \"Scheduled maintenance for system upgrade.\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [String(testPageComponentId)],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"maintenance\");\n    expect(data.maintenance.title).toBe(`${TEST_PREFIX}-created`);\n    expect(data.maintenance.message).toBe(\n      \"Scheduled maintenance for system upgrade.\",\n    );\n    expect(data.maintenance.pageComponentIds).toContain(\n      String(testPageComponentId),\n    );\n\n    // Clean up\n    await db\n      .delete(maintenancesToPageComponents)\n      .where(\n        eq(\n          maintenancesToPageComponents.maintenanceId,\n          Number(data.maintenance.id),\n        ),\n      );\n    await db\n      .delete(maintenance)\n      .where(eq(maintenance.id, Number(data.maintenance.id)));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\"CreateMaintenance\", {\n      title: \"Unauthorized test\",\n      message: \"Test message\",\n      from: fromDate.toISOString(),\n      to: toDate.toISOString(),\n      pageId: \"1\",\n      pageComponentIds: [\"1\"],\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns error for invalid page component ID\", async () => {\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: \"Invalid component test\",\n        message: \"Test message\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [\"99999\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when page components are from different pages\", async () => {\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-mixed-pages`,\n        message: \"Test message\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [\n          String(testPageComponentId),\n          String(testPage2ComponentId),\n        ],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\n      \"All page components must belong to the same page\",\n    );\n  });\n\n  test(\"returns error when pageId does not match components page\", async () => {\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-pageid-mismatch`,\n        message: \"Test pageId mismatch with components.\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: \"1\", // This doesn't match testPage2ComponentId's page\n        pageComponentIds: [String(testPage2ComponentId)],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"does not match the page ID\");\n  });\n\n  test(\"creates maintenance when pageId matches component page\", async () => {\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-matching-pageid`,\n        message: \"Test with matching pageId and components.\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: String(testPage2Id), // Matching the component's page\n        pageComponentIds: [String(testPage2ComponentId)],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"maintenance\");\n    expect(data.maintenance.pageComponentIds).toContain(\n      String(testPage2ComponentId),\n    );\n\n    // Verify the pageId was set correctly\n    const createdRecord = await db\n      .select()\n      .from(maintenance)\n      .where(eq(maintenance.id, Number(data.maintenance.id)))\n      .get();\n    expect(createdRecord?.pageId).toBe(testPage2Id);\n\n    // Clean up\n    await db\n      .delete(maintenancesToPageComponents)\n      .where(\n        eq(\n          maintenancesToPageComponents.maintenanceId,\n          Number(data.maintenance.id),\n        ),\n      );\n    await db\n      .delete(maintenance)\n      .where(eq(maintenance.id, Number(data.maintenance.id)));\n  });\n\n  test(\"creates maintenance with empty pageComponentIds\", async () => {\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-no-components`,\n        message: \"Maintenance without components.\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"maintenance\");\n    expect(data.maintenance.title).toBe(`${TEST_PREFIX}-no-components`);\n    const pageComponentIds = data.maintenance.pageComponentIds ?? [];\n    expect(pageComponentIds).toHaveLength(0);\n\n    // Clean up\n    await db\n      .delete(maintenance)\n      .where(eq(maintenance.id, Number(data.maintenance.id)));\n  });\n\n  test(\"creates maintenance with notify=true\", async () => {\n    subscriptionSpies.dispatchMaintenanceUpdate.mockClear();\n\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-with-notify`,\n        message: \"Notifying subscribers about this maintenance.\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [String(testPageComponentId)],\n        notify: true,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"maintenance\");\n    expect(data.maintenance.title).toBe(`${TEST_PREFIX}-with-notify`);\n\n    // Verify dispatcher was called (dispatchers are mocked in preload.ts)\n    expect(subscriptionSpies.dispatchMaintenanceUpdate).toHaveBeenCalledTimes(\n      1,\n    );\n\n    // Clean up\n    await db\n      .delete(maintenancesToPageComponents)\n      .where(\n        eq(\n          maintenancesToPageComponents.maintenanceId,\n          Number(data.maintenance.id),\n        ),\n      );\n    await db\n      .delete(maintenance)\n      .where(eq(maintenance.id, Number(data.maintenance.id)));\n  });\n\n  test(\"creates maintenance with notify=false (default)\", async () => {\n    subscriptionSpies.dispatchMaintenanceUpdate.mockClear();\n\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-no-notify`,\n        message: \"No notification for this one.\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [],\n        notify: false,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"maintenance\");\n    expect(data.maintenance.title).toBe(`${TEST_PREFIX}-no-notify`);\n\n    // Verify dispatcher was NOT called\n    expect(subscriptionSpies.dispatchMaintenanceUpdate).not.toHaveBeenCalled();\n\n    // Clean up\n    await db\n      .delete(maintenance)\n      .where(eq(maintenance.id, Number(data.maintenance.id)));\n  });\n\n  test(\"returns error for invalid date format\", async () => {\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-invalid-date`,\n        message: \"Test with invalid date.\",\n        from: \"not-a-valid-date\",\n        to: new Date(\n          Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000,\n        ).toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"from: value does not match regex pattern\");\n  });\n\n  test(\"returns error when from date is after to date\", async () => {\n    const fromDate = new Date(Date.now() + 8 * 24 * 60 * 60 * 1000); // 8 days from now\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days from now (before from)\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-invalid-range`,\n        message: \"Test with invalid date range.\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"Start time (from) must be before end time\");\n  });\n\n  test(\"returns error for non-existent page\", async () => {\n    const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n    const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000);\n\n    const res = await connectRequest(\n      \"CreateMaintenance\",\n      {\n        title: `${TEST_PREFIX}-invalid-page`,\n        message: \"Test with non-existent page.\",\n        from: fromDate.toISOString(),\n        to: toDate.toISOString(),\n        pageId: \"99999\",\n        pageComponentIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n    const data = await res.json();\n    expect(data.message).toContain(\"Page not found\");\n  });\n});\n\ndescribe(\"MaintenanceService.GetMaintenance\", () => {\n  test(\"returns maintenance with page components\", async () => {\n    const res = await connectRequest(\n      \"GetMaintenance\",\n      { id: String(testMaintenanceId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"maintenance\");\n    expect(data.maintenance.id).toBe(String(testMaintenanceId));\n    expect(data.maintenance.title).toBe(`${TEST_PREFIX}-main`);\n    expect(data.maintenance.pageComponentIds).toContain(\n      String(testPageComponentId),\n    );\n    expect(data.maintenance).toHaveProperty(\"createdAt\");\n    expect(data.maintenance).toHaveProperty(\"updatedAt\");\n    expect(data.maintenance).toHaveProperty(\"from\");\n    expect(data.maintenance).toHaveProperty(\"to\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"GetMaintenance\", {\n      id: String(testMaintenanceId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent maintenance\", async () => {\n    const res = await connectRequest(\n      \"GetMaintenance\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 404 for maintenance in different workspace\", async () => {\n    // Create maintenance in workspace 2\n    const otherRecord = await db\n      .insert(maintenance)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace`,\n        message: \"Other workspace maintenance\",\n        from: new Date(Date.now() + 24 * 60 * 60 * 1000),\n        to: new Date(Date.now() + 25 * 60 * 60 * 1000),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"GetMaintenance\",\n        { id: String(otherRecord.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(maintenance).where(eq(maintenance.id, otherRecord.id));\n    }\n  });\n\n  test(\"returns error when ID is empty string\", async () => {\n    const res = await connectRequest(\n      \"GetMaintenance\",\n      { id: \"\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns error when ID is whitespace only\", async () => {\n    const res = await connectRequest(\n      \"GetMaintenance\",\n      { id: \"   \" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n});\n\ndescribe(\"MaintenanceService.ListMaintenances\", () => {\n  test(\"returns maintenances for authenticated workspace\", async () => {\n    const res = await connectRequest(\n      \"ListMaintenances\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"maintenances\");\n    expect(Array.isArray(data.maintenances)).toBe(true);\n    expect(data).toHaveProperty(\"totalSize\");\n  });\n\n  test(\"returns maintenances with correct structure (summary)\", async () => {\n    const res = await connectRequest(\n      \"ListMaintenances\",\n      { limit: 100 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    const record = data.maintenances?.find(\n      (m: { id: string }) => m.id === String(testMaintenanceId),\n    );\n\n    expect(record).toBeDefined();\n    expect(record.title).toBe(`${TEST_PREFIX}-main`);\n    expect(record.pageComponentIds).toBeDefined();\n    expect(record.createdAt).toBeDefined();\n    expect(record.updatedAt).toBeDefined();\n    expect(record.from).toBeDefined();\n    expect(record.to).toBeDefined();\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"ListMaintenances\", {});\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"respects limit parameter\", async () => {\n    const res = await connectRequest(\n      \"ListMaintenances\",\n      { limit: 1 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.maintenances?.length || 0).toBeLessThanOrEqual(1);\n  });\n\n  test(\"respects offset parameter\", async () => {\n    // Get total count first\n    const res1 = await connectRequest(\n      \"ListMaintenances\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n    const data1 = await res1.json();\n    const totalSize = data1.totalSize;\n\n    if (totalSize > 1) {\n      // Get first page\n      const res2 = await connectRequest(\n        \"ListMaintenances\",\n        { limit: 1, offset: 0 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n      const data2 = await res2.json();\n\n      // Get second page\n      const res3 = await connectRequest(\n        \"ListMaintenances\",\n        { limit: 1, offset: 1 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n      const data3 = await res3.json();\n\n      // Should have different maintenances\n      if (data2.maintenances?.length > 0 && data3.maintenances?.length > 0) {\n        expect(data2.maintenances[0].id).not.toBe(data3.maintenances[0].id);\n      }\n    }\n  });\n\n  test(\"filters by page_id\", async () => {\n    const res = await connectRequest(\n      \"ListMaintenances\",\n      { pageId: \"1\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // All returned maintenances should have pageId 1\n    for (const record of data.maintenances || []) {\n      expect(record.pageId).toBe(\"1\");\n    }\n  });\n\n  test(\"only returns maintenances for authenticated workspace\", async () => {\n    // Create maintenance in workspace 2\n    const otherRecord = await db\n      .insert(maintenance)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace-list`,\n        message: \"Other workspace maintenance\",\n        from: new Date(Date.now() + 24 * 60 * 60 * 1000),\n        to: new Date(Date.now() + 25 * 60 * 60 * 1000),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"ListMaintenances\",\n        { limit: 100 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      const recordIds = (data.maintenances || []).map(\n        (m: { id: string }) => m.id,\n      );\n\n      expect(recordIds).not.toContain(String(otherRecord.id));\n    } finally {\n      await db.delete(maintenance).where(eq(maintenance.id, otherRecord.id));\n    }\n  });\n});\n\ndescribe(\"MaintenanceService.UpdateMaintenance\", () => {\n  test(\"updates maintenance title\", async () => {\n    const res = await connectRequest(\n      \"UpdateMaintenance\",\n      {\n        id: String(testMaintenanceToUpdateId),\n        title: `${TEST_PREFIX}-updated-title`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"maintenance\");\n    expect(data.maintenance.title).toBe(`${TEST_PREFIX}-updated-title`);\n\n    // Restore original title\n    await db\n      .update(maintenance)\n      .set({ title: `${TEST_PREFIX}-to-update` })\n      .where(eq(maintenance.id, testMaintenanceToUpdateId));\n  });\n\n  test(\"updates maintenance message\", async () => {\n    const res = await connectRequest(\n      \"UpdateMaintenance\",\n      {\n        id: String(testMaintenanceToUpdateId),\n        message: \"Updated maintenance message\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.maintenance.message).toBe(\"Updated maintenance message\");\n\n    // Restore original message\n    await db\n      .update(maintenance)\n      .set({ message: \"Maintenance to update\" })\n      .where(eq(maintenance.id, testMaintenanceToUpdateId));\n  });\n\n  test(\"updates page component associations\", async () => {\n    // Use existing seeded page component 1\n    const res = await connectRequest(\n      \"UpdateMaintenance\",\n      {\n        id: String(testMaintenanceToUpdateId),\n        pageComponentIds: [\"1\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.maintenance.pageComponentIds).toContain(\"1\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UpdateMaintenance\", {\n      id: String(testMaintenanceToUpdateId),\n      title: \"Unauthorized update\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent maintenance\", async () => {\n    const res = await connectRequest(\n      \"UpdateMaintenance\",\n      { id: \"99999\", title: \"Non-existent update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when ID is empty string\", async () => {\n    const res = await connectRequest(\n      \"UpdateMaintenance\",\n      { id: \"\", title: \"Empty ID update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns error when ID is whitespace only\", async () => {\n    const res = await connectRequest(\n      \"UpdateMaintenance\",\n      { id: \"   \", title: \"Whitespace ID update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 404 for maintenance in different workspace\", async () => {\n    // Create maintenance in workspace 2\n    const otherRecord = await db\n      .insert(maintenance)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace-update`,\n        message: \"Other workspace maintenance\",\n        from: new Date(Date.now() + 24 * 60 * 60 * 1000),\n        to: new Date(Date.now() + 25 * 60 * 60 * 1000),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"UpdateMaintenance\",\n        { id: String(otherRecord.id), title: \"Should not update\" },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(maintenance).where(eq(maintenance.id, otherRecord.id));\n    }\n  });\n\n  test(\"returns error for invalid page component ID on update\", async () => {\n    const res = await connectRequest(\n      \"UpdateMaintenance\",\n      {\n        id: String(testMaintenanceToUpdateId),\n        pageComponentIds: [\"99999\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when updating with components from different pages\", async () => {\n    const res = await connectRequest(\n      \"UpdateMaintenance\",\n      {\n        id: String(testMaintenanceToUpdateId),\n        pageComponentIds: [\n          String(testPageComponentId),\n          String(testPage2ComponentId),\n        ],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\n      \"All page components must belong to the same page\",\n    );\n  });\n\n  test(\"validates date range on update\", async () => {\n    // Try to update with from > to\n    const record = await db\n      .select()\n      .from(maintenance)\n      .where(eq(maintenance.id, testMaintenanceToUpdateId))\n      .get();\n\n    const newFrom = new Date(record?.to?.getTime() ?? Date.now() + 86400000);\n\n    const res = await connectRequest(\n      \"UpdateMaintenance\",\n      {\n        id: String(testMaintenanceToUpdateId),\n        from: newFrom.toISOString(),\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"Start time (from) must be before end time\");\n  });\n});\n\ndescribe(\"MaintenanceService.DeleteMaintenance\", () => {\n  test(\"successfully deletes existing maintenance\", async () => {\n    const res = await connectRequest(\n      \"DeleteMaintenance\",\n      { id: String(testMaintenanceToDeleteId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.success).toBe(true);\n\n    // Verify it's deleted\n    const deleted = await db\n      .select()\n      .from(maintenance)\n      .where(eq(maintenance.id, testMaintenanceToDeleteId))\n      .get();\n    expect(deleted).toBeUndefined();\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"DeleteMaintenance\", { id: \"1\" });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent maintenance\", async () => {\n    const res = await connectRequest(\n      \"DeleteMaintenance\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when ID is empty string\", async () => {\n    const res = await connectRequest(\n      \"DeleteMaintenance\",\n      { id: \"\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns error when ID is whitespace only\", async () => {\n    const res = await connectRequest(\n      \"DeleteMaintenance\",\n      { id: \"   \" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 404 for maintenance in different workspace\", async () => {\n    // Create maintenance in workspace 2\n    const otherRecord = await db\n      .insert(maintenance)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace-delete`,\n        message: \"Other workspace maintenance\",\n        from: new Date(Date.now() + 24 * 60 * 60 * 1000),\n        to: new Date(Date.now() + 25 * 60 * 60 * 1000),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"DeleteMaintenance\",\n        { id: String(otherRecord.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n\n      // Verify it wasn't deleted\n      const stillExists = await db\n        .select()\n        .from(maintenance)\n        .where(eq(maintenance.id, otherRecord.id))\n        .get();\n      expect(stillExists).toBeDefined();\n    } finally {\n      await db.delete(maintenance).where(eq(maintenance.id, otherRecord.id));\n    }\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/maintenance/converters.ts",
    "content": "import type {\n  Maintenance,\n  MaintenanceSummary,\n} from \"@openstatus/proto/maintenance/v1\";\n\ntype DBMaintenance = {\n  id: number;\n  title: string;\n  message: string;\n  from: Date;\n  to: Date;\n  workspaceId: number | null;\n  pageId: number | null;\n  createdAt: Date | null;\n  updatedAt: Date | null;\n};\n\n/**\n * Convert a DB maintenance to proto summary format.\n */\nexport function dbMaintenanceToProtoSummary(\n  maintenance: DBMaintenance,\n  pageComponentIds: string[],\n): MaintenanceSummary {\n  return {\n    $typeName: \"openstatus.maintenance.v1.MaintenanceSummary\" as const,\n    id: String(maintenance.id),\n    title: maintenance.title,\n    message: maintenance.message,\n    from: maintenance.from.toISOString(),\n    to: maintenance.to.toISOString(),\n    pageId: maintenance.pageId ? String(maintenance.pageId) : \"\",\n    pageComponentIds,\n    createdAt: maintenance.createdAt?.toISOString() ?? \"\",\n    updatedAt: maintenance.updatedAt?.toISOString() ?? \"\",\n  };\n}\n\n/**\n * Convert a DB maintenance to full proto format.\n */\nexport function dbMaintenanceToProto(\n  maintenance: DBMaintenance,\n  pageComponentIds: string[],\n): Maintenance {\n  return {\n    $typeName: \"openstatus.maintenance.v1.Maintenance\" as const,\n    id: String(maintenance.id),\n    title: maintenance.title,\n    message: maintenance.message,\n    from: maintenance.from.toISOString(),\n    to: maintenance.to.toISOString(),\n    pageId: maintenance.pageId ? String(maintenance.pageId) : \"\",\n    pageComponentIds,\n    createdAt: maintenance.createdAt?.toISOString() ?? \"\",\n    updatedAt: maintenance.updatedAt?.toISOString() ?? \"\",\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/maintenance/errors.ts",
    "content": "import { Code, ConnectError } from \"@connectrpc/connect\";\n\n/**\n * Error reasons for structured error handling.\n */\nexport const ErrorReason = {\n  MAINTENANCE_NOT_FOUND: \"MAINTENANCE_NOT_FOUND\",\n  MAINTENANCE_ID_REQUIRED: \"MAINTENANCE_ID_REQUIRED\",\n  MAINTENANCE_CREATE_FAILED: \"MAINTENANCE_CREATE_FAILED\",\n  MAINTENANCE_UPDATE_FAILED: \"MAINTENANCE_UPDATE_FAILED\",\n  PAGE_COMPONENT_NOT_FOUND: \"PAGE_COMPONENT_NOT_FOUND\",\n  PAGE_COMPONENTS_MIXED_PAGES: \"PAGE_COMPONENTS_MIXED_PAGES\",\n  PAGE_ID_COMPONENT_MISMATCH: \"PAGE_ID_COMPONENT_MISMATCH\",\n  PAGE_NOT_FOUND: \"PAGE_NOT_FOUND\",\n  INVALID_DATE_FORMAT: \"INVALID_DATE_FORMAT\",\n  INVALID_DATE_RANGE: \"INVALID_DATE_RANGE\",\n} as const;\n\nexport type ErrorReason = (typeof ErrorReason)[keyof typeof ErrorReason];\n\nconst DOMAIN = \"openstatus.dev\";\n\n/**\n * Creates a ConnectError with structured metadata.\n */\nfunction createError(\n  message: string,\n  code: Code,\n  reason: ErrorReason,\n  metadata?: Record<string, string>,\n): ConnectError {\n  const headers = new Headers({\n    \"error-domain\": DOMAIN,\n    \"error-reason\": reason,\n  });\n\n  if (metadata) {\n    for (const [key, value] of Object.entries(metadata)) {\n      headers.set(`error-${key}`, value);\n    }\n  }\n\n  return new ConnectError(message, code, headers);\n}\n\n/**\n * Creates a \"maintenance not found\" error.\n */\nexport function maintenanceNotFoundError(maintenanceId: string): ConnectError {\n  return createError(\n    \"Maintenance not found\",\n    Code.NotFound,\n    ErrorReason.MAINTENANCE_NOT_FOUND,\n    { \"maintenance-id\": maintenanceId },\n  );\n}\n\n/**\n * Creates a \"maintenance ID required\" error.\n */\nexport function maintenanceIdRequiredError(): ConnectError {\n  return createError(\n    \"Maintenance ID is required\",\n    Code.InvalidArgument,\n    ErrorReason.MAINTENANCE_ID_REQUIRED,\n  );\n}\n\n/**\n * Creates a \"failed to create maintenance\" error.\n */\nexport function maintenanceCreateFailedError(): ConnectError {\n  return createError(\n    \"Failed to create maintenance\",\n    Code.Internal,\n    ErrorReason.MAINTENANCE_CREATE_FAILED,\n  );\n}\n\n/**\n * Creates a \"failed to update maintenance\" error.\n */\nexport function maintenanceUpdateFailedError(\n  maintenanceId: string,\n): ConnectError {\n  return createError(\n    \"Failed to update maintenance\",\n    Code.Internal,\n    ErrorReason.MAINTENANCE_UPDATE_FAILED,\n    { \"maintenance-id\": maintenanceId },\n  );\n}\n\n/**\n * Creates a \"page component not found\" error.\n */\nexport function pageComponentNotFoundError(\n  pageComponentId: string,\n): ConnectError {\n  return createError(\n    \"Page component not found\",\n    Code.NotFound,\n    ErrorReason.PAGE_COMPONENT_NOT_FOUND,\n    { \"page-component-id\": pageComponentId },\n  );\n}\n\n/**\n * Creates a \"page components from mixed pages\" error.\n */\nexport function pageComponentsMixedPagesError(): ConnectError {\n  return createError(\n    \"All page components must belong to the same page\",\n    Code.InvalidArgument,\n    ErrorReason.PAGE_COMPONENTS_MIXED_PAGES,\n  );\n}\n\n/**\n * Creates a \"page not found\" error.\n */\nexport function pageNotFoundError(pageId: string): ConnectError {\n  return createError(\n    \"Page not found\",\n    Code.NotFound,\n    ErrorReason.PAGE_NOT_FOUND,\n    {\n      \"page-id\": pageId,\n    },\n  );\n}\n\n/**\n * Creates an \"invalid date format\" error.\n */\nexport function invalidDateFormatError(dateValue: string): ConnectError {\n  return createError(\n    \"Invalid date format. Expected RFC 3339 format (e.g., 2024-01-15T10:30:00Z)\",\n    Code.InvalidArgument,\n    ErrorReason.INVALID_DATE_FORMAT,\n    { \"date-value\": dateValue },\n  );\n}\n\n/**\n * Creates an \"invalid date range\" error (from must be before to).\n */\nexport function invalidDateRangeError(from: string, to: string): ConnectError {\n  return createError(\n    \"Invalid date range. Start time (from) must be before end time (to)\",\n    Code.InvalidArgument,\n    ErrorReason.INVALID_DATE_RANGE,\n    { from, to },\n  );\n}\n\n/**\n * Creates a \"page ID and component page mismatch\" error.\n */\nexport function pageIdComponentMismatchError(\n  providedPageId: string,\n  componentPageId: string,\n): ConnectError {\n  return createError(\n    `Page ID ${providedPageId} does not match the page ID ${componentPageId} of the provided components`,\n    Code.InvalidArgument,\n    ErrorReason.PAGE_ID_COMPONENT_MISMATCH,\n    {\n      \"provided-page-id\": providedPageId,\n      \"component-page-id\": componentPageId,\n    },\n  );\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/maintenance/index.ts",
    "content": "import type { ServiceImpl } from \"@connectrpc/connect\";\nimport { and, db, desc, eq, inArray, sql } from \"@openstatus/db\";\nimport {\n  maintenance,\n  maintenancesToPageComponents,\n  page,\n  pageComponent,\n} from \"@openstatus/db/src/schema\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport type { MaintenanceService } from \"@openstatus/proto/maintenance/v1\";\n\nimport { dispatchMaintenanceUpdate } from \"@openstatus/subscriptions\";\n\nimport { getRpcContext } from \"../../interceptors\";\nimport {\n  dbMaintenanceToProto,\n  dbMaintenanceToProtoSummary,\n} from \"./converters\";\nimport {\n  invalidDateFormatError,\n  invalidDateRangeError,\n  maintenanceCreateFailedError,\n  maintenanceIdRequiredError,\n  maintenanceNotFoundError,\n  maintenanceUpdateFailedError,\n  pageComponentNotFoundError,\n  pageComponentsMixedPagesError,\n  pageIdComponentMismatchError,\n  pageNotFoundError,\n} from \"./errors\";\n\n// Type that works with both db instance and transaction\ntype DB = typeof db;\ntype Transaction = Parameters<Parameters<DB[\"transaction\"]>[0]>[0];\n\n/**\n * Helper to get a maintenance by ID with workspace scope.\n */\nasync function getMaintenanceById(id: number, workspaceId: number) {\n  return db\n    .select()\n    .from(maintenance)\n    .where(\n      and(eq(maintenance.id, id), eq(maintenance.workspaceId, workspaceId)),\n    )\n    .get();\n}\n\n/**\n * Helper to get page component IDs for a maintenance.\n */\nasync function getPageComponentIdsForMaintenance(maintenanceId: number) {\n  const components = await db\n    .select({ pageComponentId: maintenancesToPageComponents.pageComponentId })\n    .from(maintenancesToPageComponents)\n    .where(eq(maintenancesToPageComponents.maintenanceId, maintenanceId))\n    .all();\n\n  return components.map((c) => String(c.pageComponentId));\n}\n\n/**\n * Result of validating page component IDs.\n */\ninterface ValidatedPageComponents {\n  componentIds: number[];\n  pageId: number | null;\n}\n\n/**\n * Helper to validate page component IDs belong to the workspace and same page.\n * Accepts an optional transaction to ensure atomicity with subsequent operations.\n */\nexport async function validatePageComponentIds(\n  pageComponentIds: string[],\n  workspaceId: number,\n  tx: DB | Transaction = db,\n): Promise<ValidatedPageComponents> {\n  if (pageComponentIds.length === 0) {\n    return { componentIds: [], pageId: null };\n  }\n\n  const numericIds = pageComponentIds.map((id) => Number(id));\n\n  const validComponents = await tx\n    .select({ id: pageComponent.id, pageId: pageComponent.pageId })\n    .from(pageComponent)\n    .where(\n      and(\n        inArray(pageComponent.id, numericIds),\n        eq(pageComponent.workspaceId, workspaceId),\n      ),\n    )\n    .all();\n\n  const validComponentsMap = new Map(\n    validComponents.map((c) => [c.id, c.pageId]),\n  );\n\n  // Check all requested IDs exist\n  for (const id of numericIds) {\n    if (!validComponentsMap.has(id)) {\n      throw pageComponentNotFoundError(String(id));\n    }\n  }\n\n  // Validate all components belong to the same page\n  const pageIds = new Set(validComponents.map((c) => c.pageId));\n  if (pageIds.size > 1) {\n    throw pageComponentsMixedPagesError();\n  }\n\n  const pageId = validComponents[0]?.pageId ?? null;\n\n  return { componentIds: numericIds, pageId };\n}\n\n/**\n * Helper to update page component associations for a maintenance.\n * Accepts an optional transaction to ensure atomicity.\n */\nexport async function updatePageComponentAssociations(\n  maintenanceId: number,\n  pageComponentIds: number[],\n  tx: DB | Transaction = db,\n) {\n  // Delete existing associations\n  await tx\n    .delete(maintenancesToPageComponents)\n    .where(eq(maintenancesToPageComponents.maintenanceId, maintenanceId));\n\n  // Insert new associations\n  if (pageComponentIds.length > 0) {\n    await tx.insert(maintenancesToPageComponents).values(\n      pageComponentIds.map((pageComponentId) => ({\n        maintenanceId,\n        pageComponentId,\n      })),\n    );\n  }\n}\n\n/**\n * Parses and validates a date string.\n * Throws invalidDateFormatError if the date is invalid.\n */\nfunction parseDate(dateString: string): Date {\n  const date = new Date(dateString);\n  if (Number.isNaN(date.getTime())) {\n    throw invalidDateFormatError(dateString);\n  }\n  return date;\n}\n\n/**\n * Validates that from date is before to date.\n */\nfunction validateDateRange(\n  from: Date,\n  to: Date,\n  fromStr: string,\n  toStr: string,\n): void {\n  if (from >= to) {\n    throw invalidDateRangeError(fromStr, toStr);\n  }\n}\n\n/**\n * Helper to validate page exists in workspace.\n */\nexport async function validatePageExists(\n  pageId: number,\n  workspaceId: number,\n  tx: DB | Transaction = db,\n): Promise<void> {\n  const pageRecord = await tx\n    .select({ id: page.id })\n    .from(page)\n    .where(and(eq(page.id, pageId), eq(page.workspaceId, workspaceId)))\n    .get();\n\n  if (!pageRecord) {\n    throw pageNotFoundError(String(pageId));\n  }\n}\n\n/**\n * Helper to send maintenance notifications to page subscribers.\n * Uses the subscription dispatcher for component-aware filtering.\n */\nexport async function sendMaintenanceNotification(params: {\n  maintenanceId: number;\n  limits: Limits;\n}) {\n  const { maintenanceId, limits } = params;\n\n  if (!limits[\"status-subscribers\"]) {\n    return;\n  }\n\n  await dispatchMaintenanceUpdate(maintenanceId);\n}\n\n/**\n * Maintenance service implementation for ConnectRPC.\n */\nexport const maintenanceServiceImpl: ServiceImpl<typeof MaintenanceService> = {\n  async createMaintenance(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    // Parse and validate dates\n    const fromDate = parseDate(req.from);\n    const toDate = parseDate(req.to);\n    validateDateRange(fromDate, toDate, req.from, req.to);\n\n    const providedPageId = Number(req.pageId);\n\n    // Create maintenance and associations in a transaction\n    const newMaintenance = await db.transaction(async (tx) => {\n      // Validate page exists in workspace\n      await validatePageExists(providedPageId, workspaceId, tx);\n\n      // Validate page component IDs\n      const validatedComponents = await validatePageComponentIds(\n        req.pageComponentIds,\n        workspaceId,\n        tx,\n      );\n\n      // Validate that components belong to the same page as provided pageId\n      if (\n        validatedComponents.pageId !== null &&\n        validatedComponents.pageId !== providedPageId\n      ) {\n        throw pageIdComponentMismatchError(\n          req.pageId,\n          String(validatedComponents.pageId),\n        );\n      }\n\n      // Create the maintenance\n      const record = await tx\n        .insert(maintenance)\n        .values({\n          workspaceId,\n          pageId: providedPageId,\n          title: req.title,\n          message: req.message,\n          from: fromDate,\n          to: toDate,\n        })\n        .returning()\n        .get();\n\n      if (!record) {\n        throw maintenanceCreateFailedError();\n      }\n\n      // Create page component associations\n      await updatePageComponentAssociations(\n        record.id,\n        validatedComponents.componentIds,\n        tx,\n      );\n\n      return record;\n    });\n\n    // Send notifications if requested (outside transaction)\n    if (req.notify) {\n      await sendMaintenanceNotification({\n        maintenanceId: newMaintenance.id,\n        limits: rpcCtx.workspace.limits,\n      });\n    }\n\n    return {\n      maintenance: dbMaintenanceToProto(newMaintenance, req.pageComponentIds),\n    };\n  },\n\n  async getMaintenance(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    if (!req.id || req.id.trim() === \"\") {\n      throw maintenanceIdRequiredError();\n    }\n\n    const record = await getMaintenanceById(Number(req.id), workspaceId);\n    if (!record) {\n      throw maintenanceNotFoundError(req.id);\n    }\n\n    const pageComponentIds = await getPageComponentIdsForMaintenance(record.id);\n\n    return {\n      maintenance: dbMaintenanceToProto(record, pageComponentIds),\n    };\n  },\n\n  async listMaintenances(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const limit = Math.min(Math.max(req.limit ?? 50, 1), 100);\n    const offset = req.offset ?? 0;\n\n    // Build conditions\n    const conditions = [eq(maintenance.workspaceId, workspaceId)];\n\n    // Add page_id filter if provided\n    if (req.pageId && req.pageId.trim() !== \"\") {\n      conditions.push(eq(maintenance.pageId, Number(req.pageId)));\n    }\n\n    // Get total count\n    const countResult = await db\n      .select({ count: sql<number>`count(*)` })\n      .from(maintenance)\n      .where(and(...conditions))\n      .get();\n\n    const totalCount = countResult?.count ?? 0;\n\n    // Get maintenances\n    const records = await db\n      .select()\n      .from(maintenance)\n      .where(and(...conditions))\n      .orderBy(desc(maintenance.from))\n      .limit(limit)\n      .offset(offset)\n      .all();\n\n    // Get page component IDs for each maintenance\n    const maintenances = await Promise.all(\n      records.map(async (record) => {\n        const pageComponentIds = await getPageComponentIdsForMaintenance(\n          record.id,\n        );\n        return dbMaintenanceToProtoSummary(record, pageComponentIds);\n      }),\n    );\n\n    return {\n      maintenances,\n      totalSize: totalCount,\n    };\n  },\n\n  async updateMaintenance(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    if (!req.id || req.id.trim() === \"\") {\n      throw maintenanceIdRequiredError();\n    }\n\n    const record = await getMaintenanceById(Number(req.id), workspaceId);\n    if (!record) {\n      throw maintenanceNotFoundError(req.id);\n    }\n\n    // Parse dates if provided\n    const fromDate = req.from ? parseDate(req.from) : null;\n    const toDate = req.to ? parseDate(req.to) : null;\n\n    // Validate date range with updated values\n    const effectiveFrom = fromDate ?? record.from;\n    const effectiveTo = toDate ?? record.to;\n    const fromStr = fromDate && req.from ? req.from : record.from.toISOString();\n    const toStr = toDate && req.to ? req.to : record.to.toISOString();\n    validateDateRange(effectiveFrom, effectiveTo, fromStr, toStr);\n\n    // Update maintenance and associations in a transaction\n    const updatedMaintenance = await db.transaction(async (tx) => {\n      // Validate page component IDs\n      const validatedComponents = await validatePageComponentIds(\n        req.pageComponentIds,\n        workspaceId,\n        tx,\n      );\n\n      // Determine effective pageId\n      let effectivePageId = record.pageId;\n      if (req.pageId && req.pageId.trim() !== \"\") {\n        effectivePageId = Number(req.pageId);\n        await validatePageExists(effectivePageId, workspaceId, tx);\n      }\n\n      // Validate that components belong to the same page\n      if (\n        validatedComponents.pageId !== null &&\n        effectivePageId !== null &&\n        validatedComponents.pageId !== effectivePageId\n      ) {\n        throw pageIdComponentMismatchError(\n          String(effectivePageId),\n          String(validatedComponents.pageId),\n        );\n      }\n\n      // Build update values\n      const updateValues: Record<string, unknown> = {\n        updatedAt: new Date(),\n      };\n\n      if (req.title !== undefined && req.title !== \"\") {\n        updateValues.title = req.title;\n      }\n\n      if (req.message !== undefined && req.message !== \"\") {\n        updateValues.message = req.message;\n      }\n\n      if (fromDate) {\n        updateValues.from = fromDate;\n      }\n\n      if (toDate) {\n        updateValues.to = toDate;\n      }\n\n      if (req.pageId && req.pageId.trim() !== \"\") {\n        updateValues.pageId = effectivePageId;\n      }\n\n      // Update page component associations\n      await updatePageComponentAssociations(\n        record.id,\n        validatedComponents.componentIds,\n        tx,\n      );\n\n      // Update the maintenance\n      const updated = await tx\n        .update(maintenance)\n        .set(updateValues)\n        .where(eq(maintenance.id, record.id))\n        .returning()\n        .get();\n\n      if (!updated) {\n        throw maintenanceUpdateFailedError(req.id);\n      }\n\n      return updated;\n    });\n\n    // Fetch updated page component IDs\n    const pageComponentIds = await getPageComponentIdsForMaintenance(\n      updatedMaintenance.id,\n    );\n\n    return {\n      maintenance: dbMaintenanceToProto(updatedMaintenance, pageComponentIds),\n    };\n  },\n\n  async deleteMaintenance(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    if (!req.id || req.id.trim() === \"\") {\n      throw maintenanceIdRequiredError();\n    }\n\n    const record = await getMaintenanceById(Number(req.id), workspaceId);\n    if (!record) {\n      throw maintenanceNotFoundError(req.id);\n    }\n\n    // Delete the maintenance (cascade will delete associations)\n    await db.delete(maintenance).where(eq(maintenance.id, record.id));\n\n    return { success: true };\n  },\n};\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/__tests__/monitor.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport { monitor, pageComponent } from \"@openstatus/db/src/schema\";\nimport { monitorStatusTable } from \"@openstatus/db/src/schema/monitor_status/monitor_status\";\n\nimport { app } from \"@/index\";\n\n/**\n * Helper to make ConnectRPC requests using the Connect protocol (JSON).\n * Connect uses POST with JSON body at /rpc/<service>/<method>\n */\nasync function connectRequest(\n  method: string,\n  body: Record<string, unknown> = {},\n  headers: Record<string, string> = {},\n) {\n  return app.request(`/rpc/openstatus.monitor.v1.MonitorService/${method}`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      ...headers,\n    },\n    body: JSON.stringify(body),\n  });\n}\n\nconst TEST_PREFIX = \"rpc-monitor-test\";\nlet testHttpMonitorId: number;\nlet testTcpMonitorId: number;\nlet testDnsMonitorId: number;\nlet testMonitorToDeleteId: number;\nlet testMonitorWithStatusId: number;\n\nbeforeAll(async () => {\n  // Clean up any existing test data\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-http`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-tcp`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-dns`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-to-delete`));\n  await db\n    .delete(monitor)\n    .where(eq(monitor.name, `${TEST_PREFIX}-with-status`));\n\n  // Create test HTTP monitor\n  const httpMon = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-http`,\n      url: \"https://example.com\",\n      periodicity: \"1m\",\n      active: true,\n      regions: \"ams\",\n      jobType: \"http\",\n      method: \"GET\",\n      timeout: 30000,\n      headers: JSON.stringify([{ key: \"X-Test\", value: \"test-value\" }]),\n      assertions: JSON.stringify([\n        { type: \"status\", compare: \"eq\", target: 200 },\n        { type: \"textBody\", compare: \"contains\", target: \"success\" },\n        {\n          type: \"header\",\n          compare: \"eq\",\n          target: \"application/json\",\n          key: \"content-type\",\n        },\n      ]),\n    })\n    .returning()\n    .get();\n  testHttpMonitorId = httpMon.id;\n\n  // Create test TCP monitor\n  const tcpMon = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-tcp`,\n      url: \"tcp://example.com:443\",\n      periodicity: \"5m\",\n      active: true,\n      regions: \"ams\",\n      jobType: \"tcp\",\n      timeout: 10000,\n    })\n    .returning()\n    .get();\n  testTcpMonitorId = tcpMon.id;\n\n  // Create test DNS monitor\n  const dnsMon = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-dns`,\n      url: \"example.com\",\n      periodicity: \"10m\",\n      active: true,\n      regions: \"ams\",\n      jobType: \"dns\",\n      timeout: 5000,\n      assertions: JSON.stringify([\n        { type: \"dnsRecord\", compare: \"eq\", target: \"93.184.216.34\", key: \"A\" },\n      ]),\n    })\n    .returning()\n    .get();\n  testDnsMonitorId = dnsMon.id;\n\n  // Create monitor to be deleted\n  const deleteMon = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-to-delete`,\n      url: \"https://to-delete.example.com\",\n      periodicity: \"1m\",\n      active: true,\n      regions: \"ams\",\n      jobType: \"http\",\n    })\n    .returning()\n    .get();\n  testMonitorToDeleteId = deleteMon.id;\n\n  // Create monitor with status entries for GetMonitorStatus tests\n  const statusMon = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-with-status`,\n      url: \"https://with-status.example.com\",\n      periodicity: \"1m\",\n      active: true,\n      regions: \"ams,iad,fra\",\n      jobType: \"http\",\n    })\n    .returning()\n    .get();\n  testMonitorWithStatusId = statusMon.id;\n\n  // Create status entries for the monitor\n  await db.insert(monitorStatusTable).values([\n    { monitorId: statusMon.id, region: \"ams\", status: \"active\" },\n    { monitorId: statusMon.id, region: \"iad\", status: \"error\" },\n    { monitorId: statusMon.id, region: \"fra\", status: \"degraded\" },\n    // Add a stale region entry that is not in the monitor's configured regions\n    { monitorId: statusMon.id, region: \"lhr\", status: \"active\" },\n  ]);\n});\n\nafterAll(async () => {\n  // Clean up monitor status entries first (due to foreign key)\n  await db\n    .delete(monitorStatusTable)\n    .where(eq(monitorStatusTable.monitorId, testMonitorWithStatusId));\n  // Clean up test data\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-http`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-tcp`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-dns`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-to-delete`));\n  await db\n    .delete(monitor)\n    .where(eq(monitor.name, `${TEST_PREFIX}-with-status`));\n});\n\ndescribe(\"MonitorService.ListMonitors\", () => {\n  test(\"returns monitors for authenticated workspace\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      {},\n      {\n        \"x-openstatus-key\": \"1\",\n      },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"httpMonitors\");\n    expect(data).toHaveProperty(\"tcpMonitors\");\n    expect(data).toHaveProperty(\"dnsMonitors\");\n    expect(Array.isArray(data.httpMonitors)).toBe(true);\n    expect(Array.isArray(data.tcpMonitors)).toBe(true);\n    expect(Array.isArray(data.dnsMonitors)).toBe(true);\n  });\n\n  test(\"returns HTTP monitors with correct structure\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      { limit: 100 }, // Request more to ensure we get our test monitors\n      {\n        \"x-openstatus-key\": \"1\",\n      },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Proto3 may omit empty arrays\n    const httpMonitors = data.httpMonitors || [];\n    const httpMon = httpMonitors.find(\n      (m: { id: string }) => m.id === String(testHttpMonitorId),\n    );\n\n    expect(httpMon).toBeDefined();\n    expect(httpMon.url).toBe(\"https://example.com\");\n    expect(httpMon.periodicity).toBe(\"PERIODICITY_1M\");\n    expect(httpMon.method).toBe(\"HTTP_METHOD_GET\");\n    expect(httpMon.headers).toBeDefined();\n    expect(httpMon.statusCodeAssertions).toBeDefined();\n    expect(httpMon.bodyAssertions).toBeDefined();\n    expect(httpMon.headerAssertions).toBeDefined();\n  });\n\n  test(\"returns TCP monitors with correct structure\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      { limit: 100 }, // Request more to ensure we get our test monitors\n      {\n        \"x-openstatus-key\": \"1\",\n      },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Proto3 may omit empty arrays\n    const tcpMonitors = data.tcpMonitors || [];\n    const tcpMon = tcpMonitors.find(\n      (m: { id: string }) => m.id === String(testTcpMonitorId),\n    );\n\n    expect(tcpMon).toBeDefined();\n    expect(tcpMon.uri).toBe(\"tcp://example.com:443\");\n    expect(tcpMon.periodicity).toBe(\"PERIODICITY_5M\");\n  });\n\n  test(\"returns DNS monitors with correct structure\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      { limit: 100 }, // Request more to ensure we get our test monitors\n      {\n        \"x-openstatus-key\": \"1\",\n      },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Proto3 may omit empty arrays\n    const dnsMonitors = data.dnsMonitors || [];\n    const dnsMon = dnsMonitors.find(\n      (m: { id: string }) => m.id === String(testDnsMonitorId),\n    );\n\n    expect(dnsMon).toBeDefined();\n    expect(dnsMon.uri).toBe(\"example.com\");\n    expect(dnsMon.periodicity).toBe(\"PERIODICITY_10M\");\n    expect(dnsMon.recordAssertions).toBeDefined();\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"ListMonitors\", {});\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"respects limit parameter\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      { limit: 2 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Proto3 may omit empty repeated fields from JSON output\n    const totalMonitors =\n      (data.httpMonitors?.length || 0) +\n      (data.tcpMonitors?.length || 0) +\n      (data.dnsMonitors?.length || 0);\n\n    // Should return at most 2 monitors total\n    expect(totalMonitors).toBeLessThanOrEqual(2);\n  });\n\n  test(\"returns totalSize for pagination\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      { limit: 1 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Should have totalSize indicating total number of monitors\n    expect(data).toHaveProperty(\"totalSize\");\n    expect(typeof data.totalSize).toBe(\"number\");\n  });\n\n  test(\"uses offset for pagination\", async () => {\n    // First page\n    const res1 = await connectRequest(\n      \"ListMonitors\",\n      { limit: 1 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res1.status).toBe(200);\n    const data1 = await res1.json();\n\n    if (data1.totalSize > 1) {\n      // Second page using offset\n      const res2 = await connectRequest(\n        \"ListMonitors\",\n        { limit: 1, offset: 1 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res2.status).toBe(200);\n      const data2 = await res2.json();\n\n      // Proto3 may omit empty repeated fields from JSON output\n      // The monitors from second page should be different from first page\n      const firstPageIds = [\n        ...(data1.httpMonitors || []).map((m: { id: string }) => m.id),\n        ...(data1.tcpMonitors || []).map((m: { id: string }) => m.id),\n        ...(data1.dnsMonitors || []).map((m: { id: string }) => m.id),\n      ];\n      const secondPageIds = [\n        ...(data2.httpMonitors || []).map((m: { id: string }) => m.id),\n        ...(data2.tcpMonitors || []).map((m: { id: string }) => m.id),\n        ...(data2.dnsMonitors || []).map((m: { id: string }) => m.id),\n      ];\n\n      // Should have no overlap\n      const overlap = firstPageIds.filter((id: string) =>\n        secondPageIds.includes(id),\n      );\n      expect(overlap.length).toBe(0);\n    }\n  });\n\n  test(\"only returns monitors for the authenticated workspace\", async () => {\n    // Create a monitor for workspace 2\n    const otherWorkspaceMon = await db\n      .insert(monitor)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-other-workspace`,\n        url: \"https://other-workspace.example.com\",\n        periodicity: \"1m\",\n        active: true,\n        regions: \"ams\",\n        jobType: \"http\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"ListMonitors\",\n        { limit: 100 },\n        {\n          \"x-openstatus-key\": \"1\",\n        },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      // Proto3 may omit empty arrays\n      const allMonitorIds = [\n        ...(data.httpMonitors || []).map((m: { id: string }) => m.id),\n        ...(data.tcpMonitors || []).map((m: { id: string }) => m.id),\n        ...(data.dnsMonitors || []).map((m: { id: string }) => m.id),\n      ];\n\n      // Should not contain the other workspace's monitor\n      expect(allMonitorIds).not.toContain(String(otherWorkspaceMon.id));\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, otherWorkspaceMon.id));\n    }\n  });\n});\n\ndescribe(\"MonitorService.DeleteMonitor\", () => {\n  test(\"successfully deletes existing monitor\", async () => {\n    const res = await connectRequest(\n      \"DeleteMonitor\",\n      { id: String(testMonitorToDeleteId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.success).toBe(true);\n\n    // Verify the monitor was soft-deleted\n    const deletedMon = await db\n      .select()\n      .from(monitor)\n      .where(eq(monitor.id, testMonitorToDeleteId))\n      .get();\n\n    expect(deletedMon).toBeDefined();\n    expect(deletedMon?.deletedAt).not.toBeNull();\n    expect(deletedMon?.active).toBe(false);\n  });\n\n  test(\"returns 404 for non-existent monitor\", async () => {\n    const res = await connectRequest(\n      \"DeleteMonitor\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"DeleteMonitor\", { id: \"1\" });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"cannot delete monitor from another workspace\", async () => {\n    // Create a monitor for workspace 2\n    const otherWorkspaceMon = await db\n      .insert(monitor)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-delete-other-ws`,\n        url: \"https://other-ws-delete.example.com\",\n        periodicity: \"1m\",\n        active: true,\n        regions: \"ams\",\n        jobType: \"http\",\n      })\n      .returning()\n      .get();\n\n    try {\n      // Try to delete with workspace 1's key\n      const res = await connectRequest(\n        \"DeleteMonitor\",\n        { id: String(otherWorkspaceMon.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      // Should return 404 (not found in this workspace)\n      expect(res.status).toBe(404);\n\n      // Verify the monitor still exists\n      const stillExists = await db\n        .select()\n        .from(monitor)\n        .where(eq(monitor.id, otherWorkspaceMon.id))\n        .get();\n\n      expect(stillExists).toBeDefined();\n      expect(stillExists?.deletedAt).toBeNull();\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, otherWorkspaceMon.id));\n    }\n  });\n\n  test(\"deleting a monitor removes its page components\", async () => {\n    // Create a monitor\n    const mon = await db\n      .insert(monitor)\n      .values({\n        workspaceId: 1,\n        name: `${TEST_PREFIX}-delete-with-component`,\n        url: \"https://delete-component.example.com\",\n        periodicity: \"1m\",\n        active: true,\n        regions: \"ams\",\n        jobType: \"http\",\n      })\n      .returning()\n      .get();\n\n    // Create a pageComponent referencing that monitor\n    const component = await db\n      .insert(pageComponent)\n      .values({\n        workspaceId: 1,\n        pageId: 1,\n        type: \"monitor\",\n        monitorId: mon.id,\n        name: `${TEST_PREFIX}-component`,\n        order: 0,\n      })\n      .returning()\n      .get();\n\n    // Verify the component exists\n    const beforeDelete = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.id, component.id))\n      .get();\n    expect(beforeDelete).toBeDefined();\n\n    // Delete the monitor\n    const res = await connectRequest(\n      \"DeleteMonitor\",\n      { id: String(mon.id) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n    expect(res.status).toBe(200);\n\n    // Verify the page component was removed\n    const afterDelete = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.id, component.id))\n      .get();\n    expect(afterDelete).toBeUndefined();\n  });\n});\n\ndescribe(\"MonitorService.CreateHTTPMonitor\", () => {\n  test(\"successfully creates HTTP monitor\", async () => {\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-create-http\",\n          url: \"https://create-test.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n          method: \"HTTP_METHOD_POST\",\n          timeout: \"30000\",\n          followRedirects: true,\n          headers: [{ key: \"X-Custom\", value: \"test\" }],\n          statusCodeAssertions: [{ target: \"200\", comparator: 1 }],\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.url).toBe(\"https://create-test.example.com\");\n    expect(data.monitor.periodicity).toBe(\"PERIODICITY_5M\");\n    expect(data.monitor.method).toBe(\"HTTP_METHOD_POST\");\n\n    // Clean up\n    if (data.monitor.id) {\n      await db.delete(monitor).where(eq(monitor.id, Number(data.monitor.id)));\n    }\n  });\n\n  test(\"returns error when monitor is missing\", async () => {\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"CreateHTTPMonitor\", {\n      monitor: { url: \"https://test.example.com\" },\n    });\n\n    expect(res.status).toBe(401);\n  });\n});\n\ndescribe(\"MonitorService.CreateTCPMonitor\", () => {\n  test(\"successfully creates TCP monitor\", async () => {\n    const res = await connectRequest(\n      \"CreateTCPMonitor\",\n      {\n        monitor: {\n          name: \"test-create-tcp\",\n          uri: \"tcp://create-tcp-test.example.com:8080\",\n          periodicity: \"PERIODICITY_10M\",\n          timeout: \"15000\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.uri).toBe(\"tcp://create-tcp-test.example.com:8080\");\n    expect(data.monitor.periodicity).toBe(\"PERIODICITY_10M\");\n\n    // Clean up\n    if (data.monitor.id) {\n      await db.delete(monitor).where(eq(monitor.id, Number(data.monitor.id)));\n    }\n  });\n\n  test(\"returns error when monitor is missing\", async () => {\n    const res = await connectRequest(\n      \"CreateTCPMonitor\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n});\n\ndescribe(\"MonitorService.CreateDNSMonitor\", () => {\n  test(\"successfully creates DNS monitor\", async () => {\n    const res = await connectRequest(\n      \"CreateDNSMonitor\",\n      {\n        monitor: {\n          name: \"test-create-dns\",\n          uri: \"create-dns-test.example.com\",\n          periodicity: \"PERIODICITY_30M\",\n          timeout: \"5000\",\n          recordAssertions: [{ record: \"A\", target: \"1.2.3.4\", comparator: 1 }],\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.uri).toBe(\"create-dns-test.example.com\");\n    expect(data.monitor.periodicity).toBe(\"PERIODICITY_30M\");\n\n    // Clean up\n    if (data.monitor.id) {\n      await db.delete(monitor).where(eq(monitor.id, Number(data.monitor.id)));\n    }\n  });\n\n  test(\"returns error when monitor is missing\", async () => {\n    const res = await connectRequest(\n      \"CreateDNSMonitor\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n});\n\ndescribe(\"MonitorService.UpdateHTTPMonitor\", () => {\n  test(\"successfully updates HTTP monitor with partial data\", async () => {\n    const res = await connectRequest(\n      \"UpdateHTTPMonitor\",\n      {\n        id: String(testHttpMonitorId),\n        monitor: {\n          name: \"updated-http-name\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.name).toBe(\"updated-http-name\");\n    // Original URL should be preserved\n    expect(data.monitor.url).toBe(\"https://example.com\");\n\n    // Restore original name\n    await connectRequest(\n      \"UpdateHTTPMonitor\",\n      {\n        id: String(testHttpMonitorId),\n        monitor: {\n          name: `${TEST_PREFIX}-http`,\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n  });\n\n  test(\"successfully updates HTTP monitor URL\", async () => {\n    const res = await connectRequest(\n      \"UpdateHTTPMonitor\",\n      {\n        id: String(testHttpMonitorId),\n        monitor: {\n          url: \"https://updated-example.com\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor.url).toBe(\"https://updated-example.com\");\n\n    // Restore original URL\n    await connectRequest(\n      \"UpdateHTTPMonitor\",\n      {\n        id: String(testHttpMonitorId),\n        monitor: {\n          url: \"https://example.com\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n  });\n\n  test(\"successfully updates HTTP method\", async () => {\n    const res = await connectRequest(\n      \"UpdateHTTPMonitor\",\n      {\n        id: String(testHttpMonitorId),\n        monitor: {\n          method: \"HTTP_METHOD_POST\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor.method).toBe(\"HTTP_METHOD_POST\");\n\n    // Restore original method\n    await connectRequest(\n      \"UpdateHTTPMonitor\",\n      {\n        id: String(testHttpMonitorId),\n        monitor: {\n          method: \"HTTP_METHOD_GET\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n  });\n\n  test(\"returns current monitor when no monitor data provided\", async () => {\n    const res = await connectRequest(\n      \"UpdateHTTPMonitor\",\n      {\n        id: String(testHttpMonitorId),\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.id).toBe(String(testHttpMonitorId));\n  });\n\n  test(\"returns 404 for non-existent monitor\", async () => {\n    const res = await connectRequest(\n      \"UpdateHTTPMonitor\",\n      {\n        id: \"99999\",\n        monitor: { name: \"test\" },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when trying to update TCP monitor as HTTP\", async () => {\n    const res = await connectRequest(\n      \"UpdateHTTPMonitor\",\n      {\n        id: String(testTcpMonitorId),\n        monitor: { name: \"test\" },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"type mismatch\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UpdateHTTPMonitor\", {\n      id: String(testHttpMonitorId),\n      monitor: { name: \"test\" },\n    });\n\n    expect(res.status).toBe(401);\n  });\n});\n\ndescribe(\"MonitorService.UpdateTCPMonitor\", () => {\n  test(\"successfully updates TCP monitor with partial data\", async () => {\n    const res = await connectRequest(\n      \"UpdateTCPMonitor\",\n      {\n        id: String(testTcpMonitorId),\n        monitor: {\n          name: \"updated-tcp-name\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.name).toBe(\"updated-tcp-name\");\n    // Original URI should be preserved\n    expect(data.monitor.uri).toBe(\"tcp://example.com:443\");\n\n    // Restore original name\n    await connectRequest(\n      \"UpdateTCPMonitor\",\n      {\n        id: String(testTcpMonitorId),\n        monitor: {\n          name: `${TEST_PREFIX}-tcp`,\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n  });\n\n  test(\"successfully updates TCP monitor URI\", async () => {\n    const res = await connectRequest(\n      \"UpdateTCPMonitor\",\n      {\n        id: String(testTcpMonitorId),\n        monitor: {\n          uri: \"tcp://updated-example.com:8080\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor.uri).toBe(\"tcp://updated-example.com:8080\");\n\n    // Restore original URI\n    await connectRequest(\n      \"UpdateTCPMonitor\",\n      {\n        id: String(testTcpMonitorId),\n        monitor: {\n          uri: \"tcp://example.com:443\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n  });\n\n  test(\"returns current monitor when no monitor data provided\", async () => {\n    const res = await connectRequest(\n      \"UpdateTCPMonitor\",\n      {\n        id: String(testTcpMonitorId),\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.id).toBe(String(testTcpMonitorId));\n  });\n\n  test(\"returns 404 for non-existent monitor\", async () => {\n    const res = await connectRequest(\n      \"UpdateTCPMonitor\",\n      {\n        id: \"99999\",\n        monitor: { name: \"test\" },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when trying to update HTTP monitor as TCP\", async () => {\n    const res = await connectRequest(\n      \"UpdateTCPMonitor\",\n      {\n        id: String(testHttpMonitorId),\n        monitor: { name: \"test\" },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"type mismatch\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UpdateTCPMonitor\", {\n      id: String(testTcpMonitorId),\n      monitor: { name: \"test\" },\n    });\n\n    expect(res.status).toBe(401);\n  });\n});\n\ndescribe(\"MonitorService.UpdateDNSMonitor\", () => {\n  test(\"successfully updates DNS monitor with partial data\", async () => {\n    const res = await connectRequest(\n      \"UpdateDNSMonitor\",\n      {\n        id: String(testDnsMonitorId),\n        monitor: {\n          name: \"updated-dns-name\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.name).toBe(\"updated-dns-name\");\n    // Original URI should be preserved\n    expect(data.monitor.uri).toBe(\"example.com\");\n\n    // Restore original name\n    await connectRequest(\n      \"UpdateDNSMonitor\",\n      {\n        id: String(testDnsMonitorId),\n        monitor: {\n          name: `${TEST_PREFIX}-dns`,\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n  });\n\n  test(\"successfully updates DNS monitor URI\", async () => {\n    const res = await connectRequest(\n      \"UpdateDNSMonitor\",\n      {\n        id: String(testDnsMonitorId),\n        monitor: {\n          uri: \"updated-example.com\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor.uri).toBe(\"updated-example.com\");\n\n    // Restore original URI\n    await connectRequest(\n      \"UpdateDNSMonitor\",\n      {\n        id: String(testDnsMonitorId),\n        monitor: {\n          uri: \"example.com\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n  });\n\n  test(\"successfully updates DNS record assertions\", async () => {\n    const res = await connectRequest(\n      \"UpdateDNSMonitor\",\n      {\n        id: String(testDnsMonitorId),\n        monitor: {\n          recordAssertions: [\n            { record: \"A\", target: \"1.2.3.4\", comparator: 1 },\n            { record: \"AAAA\", target: \"2001:db8::1\", comparator: 1 },\n          ],\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor.recordAssertions).toHaveLength(2);\n\n    // Restore original assertions\n    await connectRequest(\n      \"UpdateDNSMonitor\",\n      {\n        id: String(testDnsMonitorId),\n        monitor: {\n          recordAssertions: [\n            { record: \"A\", target: \"93.184.216.34\", comparator: 1 },\n          ],\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n  });\n\n  test(\"returns current monitor when no monitor data provided\", async () => {\n    const res = await connectRequest(\n      \"UpdateDNSMonitor\",\n      {\n        id: String(testDnsMonitorId),\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.id).toBe(String(testDnsMonitorId));\n  });\n\n  test(\"returns 404 for non-existent monitor\", async () => {\n    const res = await connectRequest(\n      \"UpdateDNSMonitor\",\n      {\n        id: \"99999\",\n        monitor: { name: \"test\" },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when trying to update HTTP monitor as DNS\", async () => {\n    const res = await connectRequest(\n      \"UpdateDNSMonitor\",\n      {\n        id: String(testHttpMonitorId),\n        monitor: { name: \"test\" },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"type mismatch\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UpdateDNSMonitor\", {\n      id: String(testDnsMonitorId),\n      monitor: { name: \"test\" },\n    });\n\n    expect(res.status).toBe(401);\n  });\n});\n\ndescribe(\"MonitorService.TriggerMonitor\", () => {\n  test(\"returns 404 for non-existent monitor\", async () => {\n    const res = await connectRequest(\n      \"TriggerMonitor\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"TriggerMonitor\", { id: \"1\" });\n\n    expect(res.status).toBe(401);\n  });\n});\n\ndescribe(\"MonitorService - Authentication\", () => {\n  test(\"invalid API key returns 401\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      {},\n      {\n        \"x-openstatus-key\": \"invalid-key\",\n      },\n    );\n\n    expect(res.status).toBe(401);\n  });\n});\n\ndescribe(\"MonitorService - Validation\", () => {\n  test(\"returns error when name is missing for HTTP monitor\", async () => {\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          url: \"https://test.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"name\");\n  });\n\n  test(\"returns error when name is empty for HTTP monitor\", async () => {\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"\",\n          url: \"https://test.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"name\");\n  });\n\n  test(\"returns error when URL is missing for HTTP monitor\", async () => {\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-monitor\",\n          periodicity: \"PERIODICITY_5M\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    // protovalidate uses lowercase \"url\" in the error message\n    expect(data.message.toLowerCase()).toContain(\"url\");\n  });\n\n  test(\"returns error when URI is missing for TCP monitor\", async () => {\n    const res = await connectRequest(\n      \"CreateTCPMonitor\",\n      {\n        monitor: {\n          name: \"test-tcp-monitor\",\n          periodicity: \"PERIODICITY_5M\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    // protovalidate uses lowercase \"uri\" in the error message\n    expect(data.message.toLowerCase()).toContain(\"uri\");\n  });\n\n  test(\"returns error when URI is missing for DNS monitor\", async () => {\n    const res = await connectRequest(\n      \"CreateDNSMonitor\",\n      {\n        monitor: {\n          name: \"test-dns-monitor\",\n          periodicity: \"PERIODICITY_5M\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    // protovalidate uses lowercase \"uri\" in the error message\n    expect(data.message.toLowerCase()).toContain(\"uri\");\n  });\n\n  test(\"invalid region strings are filtered out\", async () => {\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-invalid-regions\",\n          url: \"https://test.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n          method: \"HTTP_METHOD_GET\",\n          regions: [\"invalid-region\", \"another-invalid\"],\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    // Invalid region strings are parsed as UNSPECIFIED and filtered out\n    // Monitor is created with empty regions\n    expect(res.status).toBe(200);\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    // Proto3 may omit empty arrays\n    expect(data.monitor.regions || []).toEqual([]);\n\n    // Clean up\n    if (data.monitor.id) {\n      await db.delete(monitor).where(eq(monitor.id, Number(data.monitor.id)));\n    }\n  });\n\n  test(\"accepts valid regions\", async () => {\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-valid-regions\",\n          url: \"https://test-valid-regions.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n          method: \"HTTP_METHOD_GET\",\n          regions: [\"REGION_FLY_AMS\", \"REGION_FLY_IAD\", \"REGION_FLY_SIN\"],\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.regions).toEqual([\n      \"REGION_FLY_AMS\",\n      \"REGION_FLY_IAD\",\n      \"REGION_FLY_SIN\",\n    ]);\n\n    // Clean up\n    if (data.monitor.id) {\n      await db.delete(monitor).where(eq(monitor.id, Number(data.monitor.id)));\n    }\n  });\n});\n\ndescribe(\"MonitorService - Assertions Round-trip\", () => {\n  test(\"HTTP assertions are correctly stored and retrieved\", async () => {\n    const createRes = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-assertions-roundtrip\",\n          url: \"https://test-assertions.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n          method: \"HTTP_METHOD_GET\",\n          statusCodeAssertions: [\n            { target: \"200\", comparator: 1 },\n            { target: \"201\", comparator: 1 },\n          ],\n          bodyAssertions: [{ target: \"success\", comparator: 3 }],\n          headerAssertions: [\n            { key: \"content-type\", target: \"application/json\", comparator: 1 },\n          ],\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(createRes.status).toBe(200);\n    const createData = await createRes.json();\n    const monitorId = createData.monitor.id;\n\n    try {\n      // List monitors to verify assertions are retrieved correctly\n      const listRes = await connectRequest(\n        \"ListMonitors\",\n        { limit: 100 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(listRes.status).toBe(200);\n      const listData = await listRes.json();\n\n      // Proto3 may omit empty arrays\n      const httpMonitors = listData.httpMonitors || [];\n      const foundMonitor = httpMonitors.find(\n        (m: { id: string }) => m.id === monitorId,\n      );\n\n      expect(foundMonitor).toBeDefined();\n      expect(foundMonitor.statusCodeAssertions).toHaveLength(2);\n      expect(foundMonitor.bodyAssertions).toHaveLength(1);\n      expect(foundMonitor.headerAssertions).toHaveLength(1);\n      expect(foundMonitor.headerAssertions[0].key).toBe(\"content-type\");\n    } finally {\n      // Clean up\n      await db.delete(monitor).where(eq(monitor.id, Number(monitorId)));\n    }\n  });\n\n  test(\"DNS record assertions are correctly stored and retrieved\", async () => {\n    const createRes = await connectRequest(\n      \"CreateDNSMonitor\",\n      {\n        monitor: {\n          name: \"test-dns-assertions\",\n          uri: \"test-dns-assertions.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n          recordAssertions: [\n            { record: \"A\", target: \"93.184.216.34\", comparator: 1 },\n            { record: \"AAAA\", target: \"2606:2800:220:1::\", comparator: 1 },\n          ],\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(createRes.status).toBe(200);\n    const createData = await createRes.json();\n    const monitorId = createData.monitor.id;\n\n    try {\n      const listRes = await connectRequest(\n        \"ListMonitors\",\n        { limit: 100 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(listRes.status).toBe(200);\n      const listData = await listRes.json();\n\n      // Proto3 may omit empty arrays\n      const dnsMonitors = listData.dnsMonitors || [];\n      const foundMonitor = dnsMonitors.find(\n        (m: { id: string }) => m.id === monitorId,\n      );\n\n      expect(foundMonitor).toBeDefined();\n      expect(foundMonitor.recordAssertions).toHaveLength(2);\n      expect(foundMonitor.recordAssertions[0].record).toBe(\"A\");\n      expect(foundMonitor.recordAssertions[1].record).toBe(\"AAAA\");\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, Number(monitorId)));\n    }\n  });\n});\n\ndescribe(\"MonitorService - OpenTelemetry Configuration\", () => {\n  test(\"OpenTelemetry config is correctly stored and retrieved\", async () => {\n    const createRes = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-otel-config\",\n          url: \"https://test-otel.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n          method: \"HTTP_METHOD_GET\",\n          openTelemetry: {\n            endpoint: \"https://otel-collector.example.com/v1/traces\",\n            headers: [\n              { key: \"Authorization\", value: \"Bearer test-token\" },\n              { key: \"X-Custom-Header\", value: \"custom-value\" },\n            ],\n          },\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(createRes.status).toBe(200);\n    const createData = await createRes.json();\n    const monitorId = createData.monitor.id;\n\n    try {\n      const listRes = await connectRequest(\n        \"ListMonitors\",\n        { limit: 100 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(listRes.status).toBe(200);\n      const listData = await listRes.json();\n\n      // Proto3 may omit empty arrays\n      const httpMonitors = listData.httpMonitors || [];\n      const foundMonitor = httpMonitors.find(\n        (m: { id: string }) => m.id === monitorId,\n      );\n\n      expect(foundMonitor).toBeDefined();\n      expect(foundMonitor.openTelemetry).toBeDefined();\n      expect(foundMonitor.openTelemetry.endpoint).toBe(\n        \"https://otel-collector.example.com/v1/traces\",\n      );\n      expect(foundMonitor.openTelemetry.headers).toHaveLength(2);\n      expect(foundMonitor.openTelemetry.headers[0].key).toBe(\"Authorization\");\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, Number(monitorId)));\n    }\n  });\n\n  test(\"Monitor without OpenTelemetry config works correctly\", async () => {\n    const createRes = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-no-otel\",\n          url: \"https://test-no-otel.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n          method: \"HTTP_METHOD_GET\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(createRes.status).toBe(200);\n    const createData = await createRes.json();\n    const monitorId = createData.monitor.id;\n\n    try {\n      const listRes = await connectRequest(\n        \"ListMonitors\",\n        { limit: 100 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      const listData = await listRes.json();\n      // Proto3 may omit empty arrays\n      const httpMonitors = listData.httpMonitors || [];\n      const foundMonitor = httpMonitors.find(\n        (m: { id: string }) => m.id === monitorId,\n      );\n\n      expect(foundMonitor).toBeDefined();\n      // OpenTelemetry should be undefined when not configured\n      expect(foundMonitor.openTelemetry).toBeUndefined();\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, Number(monitorId)));\n    }\n  });\n});\n\ndescribe(\"MonitorService - Default Values\", () => {\n  test(\"HTTP monitor uses default values when not specified\", async () => {\n    const createRes = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-defaults\",\n          url: \"https://test-defaults.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n          method: \"HTTP_METHOD_GET\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(createRes.status).toBe(200);\n    const createData = await createRes.json();\n    const monitorId = createData.monitor.id;\n\n    try {\n      expect(createData.monitor.timeout).toBe(\"45000\");\n      expect(createData.monitor.retry).toBe(\"3\");\n      // Server applies business defaults when proto fields are omitted\n      // followRedirects defaults to true (as documented in the proto)\n      expect(createData.monitor.followRedirects).toBe(true);\n      expect(createData.monitor.active ?? false).toBe(false);\n      expect(createData.monitor.public ?? false).toBe(false);\n      expect(createData.monitor.method).toBe(\"HTTP_METHOD_GET\");\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, Number(monitorId)));\n    }\n  });\n});\n\ndescribe(\"MonitorService - Limits\", () => {\n  // Workspace 2 has free plan with limited periodicity (10m, 30m, 1h) and max 6 regions\n  const FREE_PLAN_KEY = \"2\";\n\n  test(\"returns error when periodicity is not allowed by plan\", async () => {\n    // Free plan only allows 10m, 30m, 1h - PERIODICITY_30S is not allowed\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-periodicity-limit\",\n          url: \"https://test-periodicity.example.com\",\n          periodicity: \"PERIODICITY_30S\",\n          method: \"HTTP_METHOD_GET\",\n        },\n      },\n      { \"x-openstatus-key\": FREE_PLAN_KEY },\n    );\n\n    // Should return 403 (PermissionDenied maps to 403 in Connect)\n    expect(res.status).toBe(403);\n    const data = await res.json();\n    expect(data.message).toContain(\"periodicity\");\n  });\n\n  test(\"returns error when too many regions specified\", async () => {\n    // Free plan has max-regions: 6, try to use 8 regions\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-region-limit\",\n          url: \"https://test-region-limit.example.com\",\n          periodicity: \"PERIODICITY_10M\",\n          method: \"HTTP_METHOD_GET\",\n          regions: [\n            \"REGION_FLY_AMS\",\n            \"REGION_FLY_IAD\",\n            \"REGION_FLY_SIN\",\n            \"REGION_FLY_LHR\",\n            \"REGION_FLY_SYD\",\n            \"REGION_FLY_NRT\",\n            \"REGION_FLY_FRA\",\n            \"REGION_FLY_GRU\",\n          ],\n        },\n      },\n      { \"x-openstatus-key\": FREE_PLAN_KEY },\n    );\n\n    // Should return 403 (PermissionDenied maps to 403 in Connect)\n    expect(res.status).toBe(403);\n    const data = await res.json();\n    expect(data.message).toContain(\"region\");\n  });\n\n  test(\"allows valid periodicity for plan\", async () => {\n    // PERIODICITY_10M is allowed on free plan\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-valid-periodicity\",\n          url: \"https://test-valid-periodicity.example.com\",\n          periodicity: \"PERIODICITY_10M\",\n          method: \"HTTP_METHOD_GET\",\n        },\n      },\n      { \"x-openstatus-key\": FREE_PLAN_KEY },\n    );\n\n    expect(res.status).toBe(200);\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n\n    // Clean up\n    if (data.monitor.id) {\n      await db.delete(monitor).where(eq(monitor.id, Number(data.monitor.id)));\n    }\n  });\n\n  test(\"TCP monitor respects periodicity limits\", async () => {\n    const res = await connectRequest(\n      \"CreateTCPMonitor\",\n      {\n        monitor: {\n          name: \"test-tcp-periodicity-limit\",\n          uri: \"tcp://test-periodicity.example.com:443\",\n          periodicity: \"PERIODICITY_30S\",\n        },\n      },\n      { \"x-openstatus-key\": FREE_PLAN_KEY },\n    );\n\n    expect(res.status).toBe(403);\n    const data = await res.json();\n    expect(data.message).toContain(\"periodicity\");\n  });\n\n  test(\"DNS monitor respects periodicity limits\", async () => {\n    const res = await connectRequest(\n      \"CreateDNSMonitor\",\n      {\n        monitor: {\n          name: \"test-dns-periodicity-limit\",\n          uri: \"test-periodicity.example.com\",\n          periodicity: \"PERIODICITY_30S\",\n        },\n      },\n      { \"x-openstatus-key\": FREE_PLAN_KEY },\n    );\n\n    expect(res.status).toBe(403);\n    const data = await res.json();\n    expect(data.message).toContain(\"periodicity\");\n  });\n});\n\ndescribe(\"MonitorService - Status Field\", () => {\n  test(\"HTTP monitor includes status field in response\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      { limit: 100 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    const httpMonitors = data.httpMonitors || [];\n    const httpMon = httpMonitors.find(\n      (m: { id: string }) => m.id === String(testHttpMonitorId),\n    );\n\n    expect(httpMon).toBeDefined();\n    // Status should be present and be a valid MonitorStatus enum value\n    expect(httpMon.status).toBeDefined();\n    expect([\n      \"MONITOR_STATUS_ACTIVE\",\n      \"MONITOR_STATUS_DEGRADED\",\n      \"MONITOR_STATUS_ERROR\",\n      \"MONITOR_STATUS_UNSPECIFIED\",\n    ]).toContain(httpMon.status);\n  });\n\n  test(\"TCP monitor includes status field in response\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      { limit: 100 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    const tcpMonitors = data.tcpMonitors || [];\n    const tcpMon = tcpMonitors.find(\n      (m: { id: string }) => m.id === String(testTcpMonitorId),\n    );\n\n    expect(tcpMon).toBeDefined();\n    // Status should be present and be a valid MonitorStatus enum value\n    expect(tcpMon.status).toBeDefined();\n    expect([\n      \"MONITOR_STATUS_ACTIVE\",\n      \"MONITOR_STATUS_DEGRADED\",\n      \"MONITOR_STATUS_ERROR\",\n      \"MONITOR_STATUS_UNSPECIFIED\",\n    ]).toContain(tcpMon.status);\n  });\n\n  test(\"DNS monitor includes status field in response\", async () => {\n    const res = await connectRequest(\n      \"ListMonitors\",\n      { limit: 100 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    const dnsMonitors = data.dnsMonitors || [];\n    const dnsMon = dnsMonitors.find(\n      (m: { id: string }) => m.id === String(testDnsMonitorId),\n    );\n\n    expect(dnsMon).toBeDefined();\n    // Status should be present and be a valid MonitorStatus enum value\n    expect(dnsMon.status).toBeDefined();\n    expect([\n      \"MONITOR_STATUS_ACTIVE\",\n      \"MONITOR_STATUS_DEGRADED\",\n      \"MONITOR_STATUS_ERROR\",\n      \"MONITOR_STATUS_UNSPECIFIED\",\n    ]).toContain(dnsMon.status);\n  });\n\n  test(\"newly created HTTP monitor has active status by default\", async () => {\n    const res = await connectRequest(\n      \"CreateHTTPMonitor\",\n      {\n        monitor: {\n          name: \"test-status-default\",\n          url: \"https://test-status.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n          method: \"HTTP_METHOD_GET\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n    const data = await res.json();\n\n    expect(data.monitor).toBeDefined();\n    // New monitors default to active status\n    expect(data.monitor.status).toBe(\"MONITOR_STATUS_ACTIVE\");\n\n    // Clean up\n    if (data.monitor.id) {\n      await db.delete(monitor).where(eq(monitor.id, Number(data.monitor.id)));\n    }\n  });\n\n  test(\"newly created TCP monitor has active status by default\", async () => {\n    const res = await connectRequest(\n      \"CreateTCPMonitor\",\n      {\n        monitor: {\n          name: \"test-tcp-status-default\",\n          uri: \"tcp://test-status.example.com:443\",\n          periodicity: \"PERIODICITY_5M\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n    const data = await res.json();\n\n    expect(data.monitor).toBeDefined();\n    // New monitors default to active status\n    expect(data.monitor.status).toBe(\"MONITOR_STATUS_ACTIVE\");\n\n    // Clean up\n    if (data.monitor.id) {\n      await db.delete(monitor).where(eq(monitor.id, Number(data.monitor.id)));\n    }\n  });\n\n  test(\"newly created DNS monitor has active status by default\", async () => {\n    const res = await connectRequest(\n      \"CreateDNSMonitor\",\n      {\n        monitor: {\n          name: \"test-dns-status-default\",\n          uri: \"test-status.example.com\",\n          periodicity: \"PERIODICITY_5M\",\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n    const data = await res.json();\n\n    expect(data.monitor).toBeDefined();\n    // New monitors default to active status\n    expect(data.monitor.status).toBe(\"MONITOR_STATUS_ACTIVE\");\n\n    // Clean up\n    if (data.monitor.id) {\n      await db.delete(monitor).where(eq(monitor.id, Number(data.monitor.id)));\n    }\n  });\n});\n\ndescribe(\"MonitorService.GetMonitorStatus\", () => {\n  test(\"returns status for all configured regions\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorStatus\",\n      { id: String(testMonitorWithStatusId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.id).toBe(String(testMonitorWithStatusId));\n    expect(data.regions).toBeDefined();\n    expect(Array.isArray(data.regions)).toBe(true);\n    // Should have 3 regions (ams, iad, fra) - not lhr which is stale\n    expect(data.regions).toHaveLength(3);\n  });\n\n  test(\"returns correct status values for each region\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorStatus\",\n      { id: String(testMonitorWithStatusId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    const regions = data.regions || [];\n\n    // Find each region and verify status\n    const amsRegion = regions.find(\n      (r: { region: string }) => r.region === \"REGION_FLY_AMS\",\n    );\n    const iadRegion = regions.find(\n      (r: { region: string }) => r.region === \"REGION_FLY_IAD\",\n    );\n    const fraRegion = regions.find(\n      (r: { region: string }) => r.region === \"REGION_FLY_FRA\",\n    );\n\n    expect(amsRegion).toBeDefined();\n    expect(amsRegion.status).toBe(\"MONITOR_STATUS_ACTIVE\");\n\n    expect(iadRegion).toBeDefined();\n    expect(iadRegion.status).toBe(\"MONITOR_STATUS_ERROR\");\n\n    expect(fraRegion).toBeDefined();\n    expect(fraRegion.status).toBe(\"MONITOR_STATUS_DEGRADED\");\n  });\n\n  test(\"does not return stale regions not in monitor configuration\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorStatus\",\n      { id: String(testMonitorWithStatusId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    const regions = data.regions || [];\n\n    // lhr has a status entry but is not in the monitor's configured regions\n    const lhrRegion = regions.find(\n      (r: { region: string }) => r.region === \"REGION_FLY_LHR\",\n    );\n    expect(lhrRegion).toBeUndefined();\n  });\n\n  test(\"returns 404 for non-existent monitor\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorStatus\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"GetMonitorStatus\", {\n      id: String(testMonitorWithStatusId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"cannot get status from another workspace\", async () => {\n    // Create a monitor for workspace 2\n    const otherWorkspaceMon = await db\n      .insert(monitor)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-status-other-ws`,\n        url: \"https://other-ws-status.example.com\",\n        periodicity: \"1m\",\n        active: true,\n        regions: \"ams\",\n        jobType: \"http\",\n      })\n      .returning()\n      .get();\n\n    try {\n      // Try to get status with workspace 1's key\n      const res = await connectRequest(\n        \"GetMonitorStatus\",\n        { id: String(otherWorkspaceMon.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      // Should return 404 (not found in this workspace)\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, otherWorkspaceMon.id));\n    }\n  });\n\n  test(\"returns empty regions array when monitor has no status entries\", async () => {\n    // Create a monitor without status entries\n    const noStatusMon = await db\n      .insert(monitor)\n      .values({\n        workspaceId: 1,\n        name: `${TEST_PREFIX}-no-status`,\n        url: \"https://no-status.example.com\",\n        periodicity: \"1m\",\n        active: true,\n        regions: \"ams,iad\",\n        jobType: \"http\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"GetMonitorStatus\",\n        { id: String(noStatusMon.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      expect(data.id).toBe(String(noStatusMon.id));\n      // Proto3 may omit empty arrays\n      expect(data.regions || []).toEqual([]);\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, noStatusMon.id));\n    }\n  });\n});\n\ndescribe(\"MonitorService.GetMonitor\", () => {\n  test(\"returns HTTP monitor with correct structure\", async () => {\n    const res = await connectRequest(\n      \"GetMonitor\",\n      { id: String(testHttpMonitorId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.http).toBeDefined();\n    expect(data.monitor.http.id).toBe(String(testHttpMonitorId));\n    expect(data.monitor.http.url).toBe(\"https://example.com\");\n    expect(data.monitor.http.method).toBe(\"HTTP_METHOD_GET\");\n    expect(data.monitor.http.periodicity).toBe(\"PERIODICITY_1M\");\n    // Should not have tcp or dns\n    expect(data.monitor.tcp).toBeUndefined();\n    expect(data.monitor.dns).toBeUndefined();\n  });\n\n  test(\"returns TCP monitor with correct structure\", async () => {\n    const res = await connectRequest(\n      \"GetMonitor\",\n      { id: String(testTcpMonitorId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.tcp).toBeDefined();\n    expect(data.monitor.tcp.id).toBe(String(testTcpMonitorId));\n    expect(data.monitor.tcp.uri).toBe(\"tcp://example.com:443\");\n    expect(data.monitor.tcp.periodicity).toBe(\"PERIODICITY_5M\");\n    // Should not have http or dns\n    expect(data.monitor.http).toBeUndefined();\n    expect(data.monitor.dns).toBeUndefined();\n  });\n\n  test(\"returns DNS monitor with correct structure\", async () => {\n    const res = await connectRequest(\n      \"GetMonitor\",\n      { id: String(testDnsMonitorId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.monitor).toBeDefined();\n    expect(data.monitor.dns).toBeDefined();\n    expect(data.monitor.dns.id).toBe(String(testDnsMonitorId));\n    expect(data.monitor.dns.uri).toBe(\"example.com\");\n    expect(data.monitor.dns.periodicity).toBe(\"PERIODICITY_10M\");\n    // Should not have http or tcp\n    expect(data.monitor.http).toBeUndefined();\n    expect(data.monitor.tcp).toBeUndefined();\n  });\n\n  test(\"returns 404 for non-existent monitor\", async () => {\n    const res = await connectRequest(\n      \"GetMonitor\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"GetMonitor\", {\n      id: String(testHttpMonitorId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"cannot get monitor from another workspace\", async () => {\n    // Create a monitor for workspace 2\n    const otherWorkspaceMon = await db\n      .insert(monitor)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-get-other-ws`,\n        url: \"https://other-ws-get.example.com\",\n        periodicity: \"1m\",\n        active: true,\n        regions: \"ams\",\n        jobType: \"http\",\n      })\n      .returning()\n      .get();\n\n    try {\n      // Try to get with workspace 1's key\n      const res = await connectRequest(\n        \"GetMonitor\",\n        { id: String(otherWorkspaceMon.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      // Should return 404 (not found in this workspace)\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, otherWorkspaceMon.id));\n    }\n  });\n\n  test(\"invalid API key returns 401\", async () => {\n    const res = await connectRequest(\n      \"GetMonitor\",\n      { id: String(testHttpMonitorId) },\n      { \"x-openstatus-key\": \"invalid-key\" },\n    );\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 500 when monitor data fails schema parsing\", async () => {\n    // Insert a monitor with invalid data that will fail selectMonitorSchema parsing\n    const corruptedMon = await db\n      .insert(monitor)\n      // @ts-expect-error - intentionally invalid periodicity to test parse failure\n      .values({\n        workspaceId: 1,\n        name: `${TEST_PREFIX}-corrupted-data`,\n        url: \"https://corrupted.example.com\",\n        periodicity: \"invalid-periodicity\",\n        active: true,\n        regions: \"ams\",\n        jobType: \"http\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"GetMonitor\",\n        { id: String(corruptedMon.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(500);\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, corruptedMon.id));\n    }\n  });\n});\n\ndescribe(\"MonitorService.GetMonitorSummary\", () => {\n  test(\"returns summary for HTTP monitor with correct structure\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: String(testHttpMonitorId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.id).toBe(String(testHttpMonitorId));\n    // Proto3 JSON omits default values, so we check with defaults\n    // lastPingAt is empty string when no data, may be omitted\n    expect(data.lastPingAt ?? \"\").toBe(\"\");\n    // Numeric values default to \"0\" but may be omitted in proto3 JSON\n    expect(data.totalSuccessful ?? \"0\").toBe(\"0\");\n    expect(data.totalDegraded ?? \"0\").toBe(\"0\");\n    expect(data.totalFailed ?? \"0\").toBe(\"0\");\n    expect(data.p50 ?? \"0\").toBe(\"0\");\n    expect(data.p75 ?? \"0\").toBe(\"0\");\n    expect(data.p90 ?? \"0\").toBe(\"0\");\n    expect(data.p95 ?? \"0\").toBe(\"0\");\n    expect(data.p99 ?? \"0\").toBe(\"0\");\n    expect(data.timeRange).toBe(\"TIME_RANGE_1D\");\n    // regions array - check it exists (may be empty or omitted)\n    expect(Array.isArray(data.regions ?? [])).toBe(true);\n  });\n\n  test(\"returns summary for TCP monitor\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: String(testTcpMonitorId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.id).toBe(String(testTcpMonitorId));\n    expect(data).toHaveProperty(\"timeRange\");\n  });\n\n  test(\"returns summary for DNS monitor\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: String(testDnsMonitorId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.id).toBe(String(testDnsMonitorId));\n    expect(data).toHaveProperty(\"timeRange\");\n  });\n\n  test(\"defaults to TIME_RANGE_1D when time range not specified\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: String(testHttpMonitorId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.timeRange).toBe(\"TIME_RANGE_1D\");\n  });\n\n  test(\"accepts TIME_RANGE_7D parameter\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: String(testHttpMonitorId), timeRange: \"TIME_RANGE_7D\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.timeRange).toBe(\"TIME_RANGE_7D\");\n  });\n\n  test(\"accepts TIME_RANGE_14D parameter\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: String(testHttpMonitorId), timeRange: \"TIME_RANGE_14D\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.timeRange).toBe(\"TIME_RANGE_14D\");\n  });\n\n  test(\"accepts regions filter parameter\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      {\n        id: String(testMonitorWithStatusId),\n        regions: [\"REGION_FLY_AMS\", \"REGION_FLY_IAD\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.regions).toBeDefined();\n    expect(Array.isArray(data.regions)).toBe(true);\n    // Should return the requested regions\n    expect(data.regions).toContain(\"REGION_FLY_AMS\");\n    expect(data.regions).toContain(\"REGION_FLY_IAD\");\n    expect(data.regions).toHaveLength(2);\n  });\n\n  test(\"uses monitor configured regions when no regions filter provided\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: String(testMonitorWithStatusId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.regions).toBeDefined();\n    // Monitor has regions: \"ams,iad,fra\"\n    expect(data.regions).toContain(\"REGION_FLY_AMS\");\n    expect(data.regions).toContain(\"REGION_FLY_IAD\");\n    expect(data.regions).toContain(\"REGION_FLY_FRA\");\n  });\n\n  test(\"returns 404 for non-existent monitor\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"GetMonitorSummary\", {\n      id: String(testHttpMonitorId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"cannot get summary from another workspace\", async () => {\n    // Create a monitor for workspace 2\n    const otherWorkspaceMon = await db\n      .insert(monitor)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-summary-other-ws`,\n        url: \"https://other-ws-summary.example.com\",\n        periodicity: \"1m\",\n        active: true,\n        regions: \"ams\",\n        jobType: \"http\",\n      })\n      .returning()\n      .get();\n\n    try {\n      // Try to get summary with workspace 1's key\n      const res = await connectRequest(\n        \"GetMonitorSummary\",\n        { id: String(otherWorkspaceMon.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      // Should return 404 (not found in this workspace)\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(monitor).where(eq(monitor.id, otherWorkspaceMon.id));\n    }\n  });\n\n  test(\"returns zero values when no metrics data available\", async () => {\n    // In test environment, Tinybird returns empty data (NoopTinybird)\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: String(testHttpMonitorId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Proto3 JSON omits default values, so we check with defaults\n    expect(data.totalSuccessful ?? \"0\").toBe(\"0\");\n    expect(data.totalDegraded ?? \"0\").toBe(\"0\");\n    expect(data.totalFailed ?? \"0\").toBe(\"0\");\n    expect(data.p50 ?? \"0\").toBe(\"0\");\n    expect(data.p75 ?? \"0\").toBe(\"0\");\n    expect(data.p90 ?? \"0\").toBe(\"0\");\n    expect(data.p95 ?? \"0\").toBe(\"0\");\n    expect(data.p99 ?? \"0\").toBe(\"0\");\n    expect(data.lastPingAt ?? \"\").toBe(\"\");\n  });\n\n  test(\"invalid API key returns 401\", async () => {\n    const res = await connectRequest(\n      \"GetMonitorSummary\",\n      { id: String(testHttpMonitorId) },\n      { \"x-openstatus-key\": \"invalid-key\" },\n    );\n\n    expect(res.status).toBe(401);\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/converters/assertions.ts",
    "content": "import { getLogger } from \"@logtape/logtape\";\nimport {\n  type Assertion,\n  deserialize,\n  headerAssertion,\n  recordAssertion,\n  statusAssertion,\n  textBodyAssertion,\n} from \"@openstatus/assertions\";\nimport type {\n  BodyAssertion,\n  HeaderAssertion,\n  RecordAssertion,\n  StatusCodeAssertion,\n} from \"@openstatus/proto/monitor/v1\";\n\nimport {\n  compareToNumberComparator,\n  compareToRecordComparator,\n  compareToStringComparator,\n  numberComparatorToString,\n  recordComparatorToString,\n  stringComparatorToString,\n} from \"./comparators\";\n\nconst logger = getLogger(\"api-server\");\n\n// ============================================================\n// DB to Proto (for reads)\n// ============================================================\n\nexport interface HttpAssertions {\n  statusCodeAssertions: StatusCodeAssertion[];\n  bodyAssertions: BodyAssertion[];\n  headerAssertions: HeaderAssertion[];\n}\n\n/**\n * Parse database assertions JSON for HTTP monitors using @openstatus/assertions package.\n */\nexport function parseHttpAssertions(\n  assertionsJson: string | null,\n): HttpAssertions {\n  const result: HttpAssertions = {\n    statusCodeAssertions: [],\n    bodyAssertions: [],\n    headerAssertions: [],\n  };\n\n  if (!assertionsJson) {\n    return result;\n  }\n\n  try {\n    const assertions = deserialize(assertionsJson);\n\n    for (const a of assertions) {\n      const schema = a.schema;\n\n      switch (schema.type) {\n        case \"status\": {\n          const parsed = statusAssertion.parse(schema);\n          result.statusCodeAssertions.push({\n            $typeName: \"openstatus.monitor.v1.StatusCodeAssertion\",\n            target: BigInt(parsed.target),\n            comparator: compareToNumberComparator(parsed.compare),\n          });\n          break;\n        }\n        case \"textBody\":\n        case \"jsonBody\": {\n          const parsed = textBodyAssertion.parse(schema);\n          result.bodyAssertions.push({\n            $typeName: \"openstatus.monitor.v1.BodyAssertion\",\n            target: parsed.target,\n            comparator: compareToStringComparator(parsed.compare),\n          });\n          break;\n        }\n        case \"header\": {\n          const parsed = headerAssertion.parse(schema);\n          result.headerAssertions.push({\n            $typeName: \"openstatus.monitor.v1.HeaderAssertion\",\n            target: parsed.target,\n            comparator: compareToStringComparator(parsed.compare),\n            key: parsed.key,\n          });\n          break;\n        }\n      }\n    }\n  } catch (error) {\n    logger.error(\"Failed to parse HTTP assertions JSON\", {\n      error: error instanceof Error ? error.message : String(error),\n      assertions_json: assertionsJson,\n    });\n  }\n\n  return result;\n}\n\n/**\n * Parse database assertions JSON for DNS monitors using @openstatus/assertions package.\n */\nexport function parseDnsAssertions(\n  assertionsJson: string | null,\n): RecordAssertion[] {\n  if (!assertionsJson) {\n    return [];\n  }\n\n  try {\n    // Normalize legacy DNS format before deserializing\n    const assertions = deserialize(assertionsJson);\n\n    return assertions\n      .filter(\n        (a): a is Assertion & { schema: { type: \"dnsRecord\" } } =>\n          a.schema.type === \"dnsRecord\",\n      )\n      .map((a) => {\n        const parsed = recordAssertion.parse(a.schema);\n        return {\n          $typeName: \"openstatus.monitor.v1.RecordAssertion\" as const,\n          record: parsed.key,\n          target: parsed.target,\n          comparator: compareToRecordComparator(parsed.compare),\n        };\n      });\n  } catch (error) {\n    logger.error(\"Failed to parse DNS assertions JSON\", {\n      error: error instanceof Error ? error.message : String(error),\n      assertions_json: assertionsJson,\n    });\n    return [];\n  }\n}\n\n// ============================================================\n// Proto to DB (for writes)\n// ============================================================\n\n/**\n * Convert HTTP monitor proto assertions to database JSON string.\n * Uses @openstatus/assertions package format.\n */\nexport function httpAssertionsToDbJson(\n  statusCodeAssertions: StatusCodeAssertion[],\n  bodyAssertions: BodyAssertion[],\n  headerAssertions: HeaderAssertion[],\n): string | undefined {\n  const schemas: Array<Record<string, unknown>> = [];\n\n  for (const s of statusCodeAssertions) {\n    schemas.push({\n      version: \"v1\",\n      type: \"status\",\n      compare: numberComparatorToString(s.comparator),\n      target: Number(s.target),\n    });\n  }\n\n  for (const b of bodyAssertions) {\n    schemas.push({\n      version: \"v1\",\n      type: \"textBody\",\n      compare: stringComparatorToString(b.comparator),\n      target: b.target,\n    });\n  }\n\n  for (const h of headerAssertions) {\n    schemas.push({\n      version: \"v1\",\n      type: \"header\",\n      compare: stringComparatorToString(h.comparator),\n      target: h.target,\n      key: h.key,\n    });\n  }\n\n  return schemas.length > 0 ? JSON.stringify(schemas) : undefined;\n}\n\n/**\n * Convert DNS monitor proto assertions to database JSON string.\n * Uses @openstatus/assertions package format with dnsRecord type.\n */\nexport function dnsAssertionsToDbJson(\n  recordAssertions: RecordAssertion[],\n): string | undefined {\n  if (recordAssertions.length === 0) {\n    return undefined;\n  }\n\n  const schemas = recordAssertions.map((a) => ({\n    version: \"v1\",\n    type: \"dnsRecord\",\n    compare: recordComparatorToString(a.comparator),\n    target: a.target,\n    key: a.record,\n  }));\n\n  return JSON.stringify(schemas);\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/converters/comparators.ts",
    "content": "import {\n  NumberComparator,\n  RecordComparator,\n  StringComparator,\n} from \"@openstatus/proto/monitor/v1\";\n\n// ============================================================\n// DB to Proto (for reads)\n// ============================================================\n\nconst DB_TO_NUMBER_COMPARATOR: Record<string, NumberComparator> = {\n  eq: NumberComparator.EQUAL,\n  not_eq: NumberComparator.NOT_EQUAL,\n  gt: NumberComparator.GREATER_THAN,\n  gte: NumberComparator.GREATER_THAN_OR_EQUAL,\n  lt: NumberComparator.LESS_THAN,\n  lte: NumberComparator.LESS_THAN_OR_EQUAL,\n};\n\nconst DB_TO_STRING_COMPARATOR: Record<string, StringComparator> = {\n  eq: StringComparator.EQUAL,\n  not_eq: StringComparator.NOT_EQUAL,\n  contains: StringComparator.CONTAINS,\n  not_contains: StringComparator.NOT_CONTAINS,\n  empty: StringComparator.EMPTY,\n  not_empty: StringComparator.NOT_EMPTY,\n  gt: StringComparator.GREATER_THAN,\n  gte: StringComparator.GREATER_THAN_OR_EQUAL,\n  lt: StringComparator.LESS_THAN,\n  lte: StringComparator.LESS_THAN_OR_EQUAL,\n};\n\nconst DB_TO_RECORD_COMPARATOR: Record<string, RecordComparator> = {\n  eq: RecordComparator.EQUAL,\n  not_eq: RecordComparator.NOT_EQUAL,\n  contains: RecordComparator.CONTAINS,\n  not_contains: RecordComparator.NOT_CONTAINS,\n};\n\nexport function compareToNumberComparator(compare: string): NumberComparator {\n  return DB_TO_NUMBER_COMPARATOR[compare] ?? NumberComparator.UNSPECIFIED;\n}\n\nexport function compareToStringComparator(compare: string): StringComparator {\n  return DB_TO_STRING_COMPARATOR[compare] ?? StringComparator.UNSPECIFIED;\n}\n\nexport function compareToRecordComparator(compare: string): RecordComparator {\n  return DB_TO_RECORD_COMPARATOR[compare] ?? RecordComparator.UNSPECIFIED;\n}\n\n// ============================================================\n// Proto to DB (for writes)\n// ============================================================\n\nconst NUMBER_COMPARATOR_TO_DB: Record<NumberComparator, string> = {\n  [NumberComparator.EQUAL]: \"eq\",\n  [NumberComparator.NOT_EQUAL]: \"not_eq\",\n  [NumberComparator.GREATER_THAN]: \"gt\",\n  [NumberComparator.GREATER_THAN_OR_EQUAL]: \"gte\",\n  [NumberComparator.LESS_THAN]: \"lt\",\n  [NumberComparator.LESS_THAN_OR_EQUAL]: \"lte\",\n  [NumberComparator.UNSPECIFIED]: \"eq\",\n};\n\nconst STRING_COMPARATOR_TO_DB: Record<StringComparator, string> = {\n  [StringComparator.EQUAL]: \"eq\",\n  [StringComparator.NOT_EQUAL]: \"not_eq\",\n  [StringComparator.CONTAINS]: \"contains\",\n  [StringComparator.NOT_CONTAINS]: \"not_contains\",\n  [StringComparator.EMPTY]: \"empty\",\n  [StringComparator.NOT_EMPTY]: \"not_empty\",\n  [StringComparator.GREATER_THAN]: \"gt\",\n  [StringComparator.GREATER_THAN_OR_EQUAL]: \"gte\",\n  [StringComparator.LESS_THAN]: \"lt\",\n  [StringComparator.LESS_THAN_OR_EQUAL]: \"lte\",\n  [StringComparator.UNSPECIFIED]: \"eq\",\n};\n\nconst RECORD_COMPARATOR_TO_DB: Record<RecordComparator, string> = {\n  [RecordComparator.EQUAL]: \"eq\",\n  [RecordComparator.NOT_EQUAL]: \"not_eq\",\n  [RecordComparator.CONTAINS]: \"contains\",\n  [RecordComparator.NOT_CONTAINS]: \"not_contains\",\n  [RecordComparator.UNSPECIFIED]: \"eq\",\n};\n\nexport function numberComparatorToString(comp: NumberComparator): string {\n  return NUMBER_COMPARATOR_TO_DB[comp] ?? \"eq\";\n}\n\nexport function stringComparatorToString(comp: StringComparator): string {\n  return STRING_COMPARATOR_TO_DB[comp] ?? \"eq\";\n}\n\nexport function recordComparatorToString(comp: RecordComparator): string {\n  return RECORD_COMPARATOR_TO_DB[comp] ?? \"eq\";\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/converters/defaults.ts",
    "content": "/**\n * Default values for monitor fields.\n */\nexport const MONITOR_DEFAULTS = {\n  timeout: 45000,\n  retry: 3,\n  followRedirects: true,\n  active: false,\n  public: false,\n  description: \"\",\n} as const;\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/converters/enums.ts",
    "content": "import {\n  HTTPMethod,\n  MonitorStatus,\n  Periodicity,\n  TimeRange,\n} from \"@openstatus/proto/monitor/v1\";\n\nimport type { monitorPeriodicitySchema } from \"@openstatus/db/src/schema/constants\";\nimport type { z } from \"zod\";\n\n// ============================================================\n// Periodicity Conversions\n// ============================================================\n\nconst DB_TO_PERIODICITY: Record<string, Periodicity> = {\n  \"30s\": Periodicity.PERIODICITY_30S,\n  \"1m\": Periodicity.PERIODICITY_1M,\n  \"5m\": Periodicity.PERIODICITY_5M,\n  \"10m\": Periodicity.PERIODICITY_10M,\n  \"30m\": Periodicity.PERIODICITY_30M,\n  \"1h\": Periodicity.PERIODICITY_1H,\n};\n\nconst PERIODICITY_TO_DB: Record<\n  Periodicity,\n  z.infer<typeof monitorPeriodicitySchema>\n> = {\n  [Periodicity.PERIODICITY_30S]: \"30s\",\n  [Periodicity.PERIODICITY_1M]: \"1m\",\n  [Periodicity.PERIODICITY_5M]: \"5m\",\n  [Periodicity.PERIODICITY_10M]: \"10m\",\n  [Periodicity.PERIODICITY_30M]: \"30m\",\n  [Periodicity.PERIODICITY_1H]: \"1h\",\n  [Periodicity.PERIODICITY_UNSPECIFIED]: \"1m\",\n};\n\nexport function stringToPeriodicity(value: string): Periodicity {\n  return DB_TO_PERIODICITY[value] ?? Periodicity.PERIODICITY_UNSPECIFIED;\n}\n\nexport function periodicityToString(value: Periodicity) {\n  return PERIODICITY_TO_DB[value] ?? \"1m\";\n}\n\n// ============================================================\n// HTTP Method Conversions\n// ============================================================\n\nconst DB_TO_HTTP_METHOD: Record<string, HTTPMethod> = {\n  GET: HTTPMethod.HTTP_METHOD_GET,\n  POST: HTTPMethod.HTTP_METHOD_POST,\n  HEAD: HTTPMethod.HTTP_METHOD_HEAD,\n  PUT: HTTPMethod.HTTP_METHOD_PUT,\n  PATCH: HTTPMethod.HTTP_METHOD_PATCH,\n  DELETE: HTTPMethod.HTTP_METHOD_DELETE,\n  TRACE: HTTPMethod.HTTP_METHOD_TRACE,\n  CONNECT: HTTPMethod.HTTP_METHOD_CONNECT,\n  OPTIONS: HTTPMethod.HTTP_METHOD_OPTIONS,\n};\n\nconst HTTP_METHOD_TO_DB: Record<HTTPMethod, string> = {\n  [HTTPMethod.HTTP_METHOD_GET]: \"GET\",\n  [HTTPMethod.HTTP_METHOD_POST]: \"POST\",\n  [HTTPMethod.HTTP_METHOD_HEAD]: \"HEAD\",\n  [HTTPMethod.HTTP_METHOD_PUT]: \"PUT\",\n  [HTTPMethod.HTTP_METHOD_PATCH]: \"PATCH\",\n  [HTTPMethod.HTTP_METHOD_DELETE]: \"DELETE\",\n  [HTTPMethod.HTTP_METHOD_TRACE]: \"TRACE\",\n  [HTTPMethod.HTTP_METHOD_CONNECT]: \"CONNECT\",\n  [HTTPMethod.HTTP_METHOD_OPTIONS]: \"OPTIONS\",\n  [HTTPMethod.HTTP_METHOD_UNSPECIFIED]: \"GET\",\n};\n\nexport function stringToHttpMethod(value: string | undefined): HTTPMethod {\n  return (\n    DB_TO_HTTP_METHOD[value?.toUpperCase() ?? \"\"] ??\n    HTTPMethod.HTTP_METHOD_UNSPECIFIED\n  );\n}\n\nexport function httpMethodToString(value: HTTPMethod): string {\n  return HTTP_METHOD_TO_DB[value] ?? \"GET\";\n}\n\n// ============================================================\n// Monitor Status Conversions\n// ============================================================\n\nconst DB_TO_MONITOR_STATUS: Record<string, MonitorStatus> = {\n  active: MonitorStatus.ACTIVE,\n  degraded: MonitorStatus.DEGRADED,\n  error: MonitorStatus.ERROR,\n};\n\nexport function stringToMonitorStatus(value: string): MonitorStatus {\n  return DB_TO_MONITOR_STATUS[value] ?? MonitorStatus.UNSPECIFIED;\n}\n\n// ============================================================\n// Time Range Conversions\n// ============================================================\n\nexport type TimeRangeKey = \"1d\" | \"7d\" | \"14d\";\n\nconst TIME_RANGE_TO_KEY: Record<TimeRange, TimeRangeKey> = {\n  [TimeRange.TIME_RANGE_1D]: \"1d\",\n  [TimeRange.TIME_RANGE_7D]: \"7d\",\n  [TimeRange.TIME_RANGE_14D]: \"14d\",\n  [TimeRange.TIME_RANGE_UNSPECIFIED]: \"1d\",\n};\n\nexport function timeRangeToKey(value: TimeRange): TimeRangeKey {\n  return TIME_RANGE_TO_KEY[value] ?? \"1d\";\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/converters/headers.ts",
    "content": "import type {\n  Headers,\n  OpenTelemetryConfig,\n} from \"@openstatus/proto/monitor/v1\";\n\n// ============================================================\n// DB to Proto (for reads)\n// ============================================================\n\n/**\n * Convert headers array to proto Headers array.\n */\nexport function toProtoHeaders(\n  headers: Array<{ key: string; value: string }> | null | undefined,\n): Headers[] {\n  if (!headers || headers.length === 0) {\n    return [];\n  }\n\n  return headers.map((h) => ({\n    $typeName: \"openstatus.monitor.v1.Headers\" as const,\n    key: h.key,\n    value: h.value,\n  }));\n}\n\n/**\n * Parse OpenTelemetry configuration from database fields.\n */\nexport function parseOpenTelemetry(\n  endpoint: string | null,\n  headers: Array<{ key: string; value: string }> | null | undefined,\n): OpenTelemetryConfig | undefined {\n  if (!endpoint) {\n    return undefined;\n  }\n\n  return {\n    $typeName: \"openstatus.monitor.v1.OpenTelemetryConfig\",\n    endpoint,\n    headers: toProtoHeaders(headers),\n  };\n}\n\n// ============================================================\n// Proto to DB (for writes)\n// ============================================================\n\n/**\n * Convert proto Headers array to database JSON string.\n */\nexport function headersToDbJson(headers: Headers[]): string | undefined {\n  if (headers.length === 0) {\n    return undefined;\n  }\n\n  return JSON.stringify(headers.map((h) => ({ key: h.key, value: h.value })));\n}\n\n/**\n * Convert OpenTelemetry config to database fields.\n */\nexport function openTelemetryToDb(config: OpenTelemetryConfig | undefined): {\n  otelEndpoint: string | undefined;\n  otelHeaders: string | undefined;\n} {\n  if (!config || !config.endpoint) {\n    return {\n      otelEndpoint: undefined,\n      otelHeaders: undefined,\n    };\n  }\n\n  return {\n    otelEndpoint: config.endpoint,\n    otelHeaders: headersToDbJson(config.headers),\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/converters/index.ts",
    "content": "// Barrel file for converter modules\n\n// Assertions\nexport {\n  parseHttpAssertions,\n  parseDnsAssertions,\n  httpAssertionsToDbJson,\n  dnsAssertionsToDbJson,\n  type HttpAssertions,\n} from \"./assertions\";\n\n// Comparators\nexport {\n  compareToNumberComparator,\n  compareToStringComparator,\n  compareToRecordComparator,\n  numberComparatorToString,\n  stringComparatorToString,\n  recordComparatorToString,\n} from \"./comparators\";\n\n// Defaults\nexport { MONITOR_DEFAULTS } from \"./defaults\";\n\n// Enums\nexport {\n  stringToPeriodicity,\n  periodicityToString,\n  stringToHttpMethod,\n  httpMethodToString,\n  stringToMonitorStatus,\n  timeRangeToKey,\n  type TimeRangeKey,\n} from \"./enums\";\n\n// Headers\nexport {\n  toProtoHeaders,\n  parseOpenTelemetry,\n  headersToDbJson,\n  openTelemetryToDb,\n} from \"./headers\";\n\n// Monitors\nexport {\n  dbMonitorToHttpProto,\n  dbMonitorToTcpProto,\n  dbMonitorToDnsProto,\n} from \"./monitors\";\n\n// Regions\nexport {\n  stringToRegion,\n  regionToString,\n  stringsToRegions,\n  regionsToStrings,\n  regionsToDbString,\n  validateRegions,\n} from \"./regions\";\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/converters/monitors.ts",
    "content": "import type { Monitor } from \"@openstatus/db/src/schema/monitors/validation\";\nimport type {\n  DNSMonitor,\n  HTTPMonitor,\n  TCPMonitor,\n} from \"@openstatus/proto/monitor/v1\";\n\nimport { parseDnsAssertions, parseHttpAssertions } from \"./assertions\";\nimport { MONITOR_DEFAULTS } from \"./defaults\";\nimport {\n  stringToHttpMethod,\n  stringToMonitorStatus,\n  stringToPeriodicity,\n} from \"./enums\";\nimport { parseOpenTelemetry, toProtoHeaders } from \"./headers\";\nimport { stringsToRegions } from \"./regions\";\n\n/**\n * Transform database HTTP monitor to proto HTTPMonitor.\n */\nexport function dbMonitorToHttpProto(dbMon: Monitor): HTTPMonitor {\n  const assertions = parseHttpAssertions(dbMon.assertions);\n\n  return {\n    $typeName: \"openstatus.monitor.v1.HTTPMonitor\",\n    id: String(dbMon.id),\n    name: dbMon.name,\n    url: dbMon.url,\n    periodicity: stringToPeriodicity(dbMon.periodicity),\n    method: stringToHttpMethod(dbMon.method),\n    body: dbMon.body ?? \"\",\n    timeout: BigInt(dbMon.timeout),\n    degradedAt: dbMon.degradedAfter ? BigInt(dbMon.degradedAfter) : undefined,\n    retry: BigInt(dbMon.retry ?? MONITOR_DEFAULTS.retry),\n    followRedirects: dbMon.followRedirects ?? MONITOR_DEFAULTS.followRedirects,\n    headers: toProtoHeaders(dbMon.headers),\n    statusCodeAssertions: assertions.statusCodeAssertions,\n    bodyAssertions: assertions.bodyAssertions,\n    headerAssertions: assertions.headerAssertions,\n    description: dbMon.description,\n    active: dbMon.active ?? MONITOR_DEFAULTS.active,\n    public: dbMon.public ?? MONITOR_DEFAULTS.public,\n    regions: stringsToRegions(dbMon.regions),\n    openTelemetry: parseOpenTelemetry(dbMon.otelEndpoint, dbMon.otelHeaders),\n    status: stringToMonitorStatus(dbMon.status),\n  };\n}\n\n/**\n * Transform database TCP monitor to proto TCPMonitor.\n */\nexport function dbMonitorToTcpProto(dbMon: Monitor): TCPMonitor {\n  return {\n    $typeName: \"openstatus.monitor.v1.TCPMonitor\",\n    id: String(dbMon.id),\n    name: dbMon.name,\n    uri: dbMon.url,\n    periodicity: stringToPeriodicity(dbMon.periodicity),\n    timeout: BigInt(dbMon.timeout),\n    degradedAt: dbMon.degradedAfter ? BigInt(dbMon.degradedAfter) : undefined,\n    retry: BigInt(dbMon.retry ?? MONITOR_DEFAULTS.retry),\n    description: dbMon.description,\n    active: dbMon.active ?? MONITOR_DEFAULTS.active,\n    public: dbMon.public ?? MONITOR_DEFAULTS.public,\n    regions: stringsToRegions(dbMon.regions),\n    openTelemetry: parseOpenTelemetry(dbMon.otelEndpoint, dbMon.otelHeaders),\n    status: stringToMonitorStatus(dbMon.status),\n  };\n}\n\n/**\n * Transform database DNS monitor to proto DNSMonitor.\n */\nexport function dbMonitorToDnsProto(dbMon: Monitor): DNSMonitor {\n  return {\n    $typeName: \"openstatus.monitor.v1.DNSMonitor\",\n    id: String(dbMon.id),\n    name: dbMon.name,\n    uri: dbMon.url,\n    periodicity: stringToPeriodicity(dbMon.periodicity),\n    timeout: BigInt(dbMon.timeout),\n    degradedAt: dbMon.degradedAfter ? BigInt(dbMon.degradedAfter) : undefined,\n    retry: BigInt(dbMon.retry ?? MONITOR_DEFAULTS.retry),\n    recordAssertions: parseDnsAssertions(dbMon.assertions),\n    description: dbMon.description,\n    active: dbMon.active ?? MONITOR_DEFAULTS.active,\n    public: dbMon.public ?? MONITOR_DEFAULTS.public,\n    regions: stringsToRegions(dbMon.regions),\n    openTelemetry: parseOpenTelemetry(dbMon.otelEndpoint, dbMon.otelHeaders),\n    status: stringToMonitorStatus(dbMon.status),\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/converters/regions.ts",
    "content": "import { Region } from \"@openstatus/proto/monitor/v1\";\nimport { AVAILABLE_REGIONS } from \"@openstatus/regions\";\n\n/**\n * Mapping from database region strings to proto Region enum.\n */\nconst DB_TO_REGION: Record<string, Region> = {\n  // Fly.io regions\n  ams: Region.FLY_AMS,\n  arn: Region.FLY_ARN,\n  bom: Region.FLY_BOM,\n  cdg: Region.FLY_CDG,\n  dfw: Region.FLY_DFW,\n  ewr: Region.FLY_EWR,\n  fra: Region.FLY_FRA,\n  gru: Region.FLY_GRU,\n  iad: Region.FLY_IAD,\n  jnb: Region.FLY_JNB,\n  lax: Region.FLY_LAX,\n  lhr: Region.FLY_LHR,\n  nrt: Region.FLY_NRT,\n  ord: Region.FLY_ORD,\n  sjc: Region.FLY_SJC,\n  sin: Region.FLY_SIN,\n  syd: Region.FLY_SYD,\n  yyz: Region.FLY_YYZ,\n  // Koyeb regions\n  koyeb_fra: Region.KOYEB_FRA,\n  koyeb_par: Region.KOYEB_PAR,\n  koyeb_sfo: Region.KOYEB_SFO,\n  koyeb_sin: Region.KOYEB_SIN,\n  koyeb_tyo: Region.KOYEB_TYO,\n  koyeb_was: Region.KOYEB_WAS,\n  // Railway regions\n  \"railway_us-west2\": Region.RAILWAY_US_WEST2,\n  \"railway_us-east4\": Region.RAILWAY_US_EAST4,\n  \"railway_europe-west4\": Region.RAILWAY_EUROPE_WEST4,\n  \"railway_asia-southeast1\": Region.RAILWAY_ASIA_SOUTHEAST1,\n};\n\n/**\n * Mapping from proto Region enum to database strings.\n */\nconst REGION_TO_DB: Record<Region, string> = {\n  // Fly.io regions\n  [Region.FLY_AMS]: \"ams\",\n  [Region.FLY_ARN]: \"arn\",\n  [Region.FLY_BOM]: \"bom\",\n  [Region.FLY_CDG]: \"cdg\",\n  [Region.FLY_DFW]: \"dfw\",\n  [Region.FLY_EWR]: \"ewr\",\n  [Region.FLY_FRA]: \"fra\",\n  [Region.FLY_GRU]: \"gru\",\n  [Region.FLY_IAD]: \"iad\",\n  [Region.FLY_JNB]: \"jnb\",\n  [Region.FLY_LAX]: \"lax\",\n  [Region.FLY_LHR]: \"lhr\",\n  [Region.FLY_NRT]: \"nrt\",\n  [Region.FLY_ORD]: \"ord\",\n  [Region.FLY_SJC]: \"sjc\",\n  [Region.FLY_SIN]: \"sin\",\n  [Region.FLY_SYD]: \"syd\",\n  [Region.FLY_YYZ]: \"yyz\",\n  // Koyeb regions\n  [Region.KOYEB_FRA]: \"koyeb_fra\",\n  [Region.KOYEB_PAR]: \"koyeb_par\",\n  [Region.KOYEB_SFO]: \"koyeb_sfo\",\n  [Region.KOYEB_SIN]: \"koyeb_sin\",\n  [Region.KOYEB_TYO]: \"koyeb_tyo\",\n  [Region.KOYEB_WAS]: \"koyeb_was\",\n  // Railway regions\n  [Region.RAILWAY_US_WEST2]: \"railway_us-west2\",\n  [Region.RAILWAY_US_EAST4]: \"railway_us-east4\",\n  [Region.RAILWAY_EUROPE_WEST4]: \"railway_europe-west4\",\n  [Region.RAILWAY_ASIA_SOUTHEAST1]: \"railway_asia-southeast1\",\n  // Unspecified\n  [Region.UNSPECIFIED]: \"\",\n};\n\n/**\n * Convert database region string to proto Region enum.\n */\nexport function stringToRegion(value: string): Region {\n  return DB_TO_REGION[value.toLowerCase()] ?? Region.UNSPECIFIED;\n}\n\n/**\n * Convert proto Region enum to database string.\n */\nexport function regionToString(value: Region): string {\n  return REGION_TO_DB[value] ?? \"\";\n}\n\n/**\n * Convert database regions array to proto Region enum array.\n */\nexport function stringsToRegions(values: string[]): Region[] {\n  return values.map(stringToRegion).filter((r) => r !== Region.UNSPECIFIED);\n}\n\n/**\n * Convert proto Region enum array to database strings.\n */\nexport function regionsToStrings(values: Region[]): string[] {\n  return values.map(regionToString).filter((r) => r !== \"\");\n}\n\n/**\n * Convert regions array to database string format (comma-separated).\n */\nexport function regionsToDbString(regions: string[]): string {\n  return regions.join(\",\");\n}\n\n/**\n * Validate that all regions are valid available regions.\n * Returns an array of invalid region codes, or empty array if all valid.\n */\nexport function validateRegions(regions: string[]): string[] {\n  const availableSet = new Set(AVAILABLE_REGIONS);\n  return regions.filter(\n    (r) => !availableSet.has(r as (typeof AVAILABLE_REGIONS)[number]),\n  );\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/errors.ts",
    "content": "import { Code, ConnectError } from \"@connectrpc/connect\";\n\n/**\n * Error reasons for structured error handling.\n */\nexport const ErrorReason = {\n  MONITOR_NOT_FOUND: \"MONITOR_NOT_FOUND\",\n  MONITOR_REQUIRED: \"MONITOR_REQUIRED\",\n  MONITOR_ID_REQUIRED: \"MONITOR_ID_REQUIRED\",\n  MONITOR_CREATE_FAILED: \"MONITOR_CREATE_FAILED\",\n  MONITOR_UPDATE_FAILED: \"MONITOR_UPDATE_FAILED\",\n  MONITOR_PARSE_FAILED: \"MONITOR_PARSE_FAILED\",\n  MONITOR_RUN_CREATE_FAILED: \"MONITOR_RUN_CREATE_FAILED\",\n  MONITOR_INVALID_DATA: \"MONITOR_INVALID_DATA\",\n  MONITOR_TYPE_MISMATCH: \"MONITOR_TYPE_MISMATCH\",\n  RATE_LIMIT_EXCEEDED: \"RATE_LIMIT_EXCEEDED\",\n} as const;\n\nexport type ErrorReason = (typeof ErrorReason)[keyof typeof ErrorReason];\n\nconst DOMAIN = \"openstatus.dev\";\n\n/**\n * Creates a ConnectError with structured metadata.\n *\n * This provides machine-parseable error information via metadata headers\n * while maintaining human-readable error messages.\n */\nfunction createError(\n  message: string,\n  code: Code,\n  reason: ErrorReason,\n  metadata?: Record<string, string>,\n): ConnectError {\n  const headers = new Headers({\n    \"error-domain\": DOMAIN,\n    \"error-reason\": reason,\n  });\n\n  if (metadata) {\n    for (const [key, value] of Object.entries(metadata)) {\n      headers.set(`error-${key}`, value);\n    }\n  }\n\n  return new ConnectError(message, code, headers);\n}\n\n/**\n * Creates a \"monitor not found\" error with the monitor ID in metadata.\n */\nexport function monitorNotFoundError(monitorId: string): ConnectError {\n  return createError(\n    \"Monitor not found\",\n    Code.NotFound,\n    ErrorReason.MONITOR_NOT_FOUND,\n    { \"monitor-id\": monitorId },\n  );\n}\n\n/**\n * Creates a \"monitor required\" error.\n */\nexport function monitorRequiredError(): ConnectError {\n  return createError(\n    \"Monitor is required\",\n    Code.InvalidArgument,\n    ErrorReason.MONITOR_REQUIRED,\n  );\n}\n\n/**\n * Creates a \"monitor ID required\" error.\n */\nexport function monitorIdRequiredError(): ConnectError {\n  return createError(\n    \"Monitor ID is required\",\n    Code.InvalidArgument,\n    ErrorReason.MONITOR_ID_REQUIRED,\n  );\n}\n\n/**\n * Creates a \"failed to create monitor\" error.\n */\nexport function monitorCreateFailedError(): ConnectError {\n  return createError(\n    \"Failed to create monitor\",\n    Code.Internal,\n    ErrorReason.MONITOR_CREATE_FAILED,\n  );\n}\n\n/**\n * Creates a \"failed to update monitor\" error.\n */\nexport function monitorUpdateFailedError(monitorId: string): ConnectError {\n  return createError(\n    \"Failed to update monitor\",\n    Code.Internal,\n    ErrorReason.MONITOR_UPDATE_FAILED,\n    { \"monitor-id\": monitorId },\n  );\n}\n\n/**\n * Creates a \"monitor type mismatch\" error when trying to update with wrong type.\n */\nexport function monitorTypeMismatchError(\n  monitorId: string,\n  expectedType: string,\n  actualType: string,\n): ConnectError {\n  return createError(\n    `Monitor type mismatch: expected ${expectedType}, got ${actualType}`,\n    Code.InvalidArgument,\n    ErrorReason.MONITOR_TYPE_MISMATCH,\n    {\n      \"monitor-id\": monitorId,\n      \"expected-type\": expectedType,\n      \"actual-type\": actualType,\n    },\n  );\n}\n\n/**\n * Creates a \"failed to parse monitor data\" error.\n */\nexport function monitorParseFailedError(monitorId?: string): ConnectError {\n  return createError(\n    \"Failed to parse monitor data\",\n    Code.Internal,\n    ErrorReason.MONITOR_PARSE_FAILED,\n    monitorId ? { \"monitor-id\": monitorId } : undefined,\n  );\n}\n\n/**\n * Creates a \"failed to create monitor run\" error.\n */\nexport function monitorRunCreateFailedError(monitorId: string): ConnectError {\n  return createError(\n    \"Failed to create monitor run\",\n    Code.Internal,\n    ErrorReason.MONITOR_RUN_CREATE_FAILED,\n    { \"monitor-id\": monitorId },\n  );\n}\n\n/**\n * Creates an \"invalid monitor data\" error for corrupted data.\n */\nexport function monitorInvalidDataError(monitorId: string): ConnectError {\n  return createError(\n    \"Invalid monitor data, please contact support\",\n    Code.Internal,\n    ErrorReason.MONITOR_INVALID_DATA,\n    { \"monitor-id\": monitorId },\n  );\n}\n\n/**\n * Creates a rate limit exceeded error.\n */\nexport function rateLimitExceededError(\n  limit: number,\n  current: number,\n): ConnectError {\n  return createError(\n    \"Upgrade for more checks\",\n    Code.ResourceExhausted,\n    ErrorReason.RATE_LIMIT_EXCEEDED,\n    { limit: String(limit), current: String(current) },\n  );\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/index.ts",
    "content": "import { env } from \"@/env\";\nimport { getCheckerPayload, getCheckerUrl } from \"@/libs/checker\";\nimport { tb } from \"@/libs/clients\";\nimport type { ServiceImpl } from \"@connectrpc/connect\";\nimport { and, db, eq, gte, inArray, isNull, sql } from \"@openstatus/db\";\nimport {\n  monitor,\n  monitorRun,\n  monitorTagsToMonitors,\n  notificationsToMonitors,\n  pageComponent,\n} from \"@openstatus/db/src/schema\";\nimport { monitorStatusTable } from \"@openstatus/db/src/schema/monitor_status/monitor_status\";\nimport { selectMonitorSchema } from \"@openstatus/db/src/schema/monitors/validation\";\nimport type {\n  DNSMonitor,\n  GetMonitorResponse,\n  GetMonitorSummaryResponse,\n  HTTPMonitor,\n  MonitorConfig,\n  MonitorService,\n  RegionStatus,\n  TCPMonitor,\n} from \"@openstatus/proto/monitor/v1\";\nimport { TimeRange } from \"@openstatus/proto/monitor/v1\";\n\nimport { getRpcContext } from \"../../interceptors\";\nimport {\n  MONITOR_DEFAULTS,\n  type TimeRangeKey,\n  dbMonitorToDnsProto,\n  dbMonitorToHttpProto,\n  dbMonitorToTcpProto,\n  dnsAssertionsToDbJson,\n  headersToDbJson,\n  httpAssertionsToDbJson,\n  httpMethodToString,\n  regionsToStrings,\n  stringToMonitorStatus,\n  stringToRegion,\n  stringsToRegions,\n  timeRangeToKey,\n} from \"./converters\";\nimport {\n  monitorCreateFailedError,\n  monitorIdRequiredError,\n  monitorInvalidDataError,\n  monitorNotFoundError,\n  monitorParseFailedError,\n  monitorRequiredError,\n  monitorRunCreateFailedError,\n  monitorTypeMismatchError,\n  monitorUpdateFailedError,\n  rateLimitExceededError,\n} from \"./errors\";\nimport { checkMonitorLimits } from \"./limits\";\nimport {\n  getCommonDbValues,\n  getCommonDbValuesForUpdate,\n  toValidMethod,\n  validateCommonMonitorFields,\n} from \"./validators\";\n\n/**\n * Helper to get a monitor by ID with workspace scope.\n */\nasync function getMonitorById(id: number, workspaceId: number) {\n  return db\n    .select()\n    .from(monitor)\n    .where(\n      and(\n        eq(monitor.id, id),\n        eq(monitor.workspaceId, workspaceId),\n        isNull(monitor.deletedAt),\n      ),\n    )\n    .get();\n}\n\ntype DBMonitor = NonNullable<Awaited<ReturnType<typeof getMonitorById>>>;\n\n/**\n * Helper to validate and get a monitor for update operations.\n * Validates ID, fetches the monitor, and verifies the job type.\n */\nasync function validateAndGetMonitor(\n  id: string | undefined,\n  workspaceId: number,\n  expectedJobType: \"http\" | \"tcp\" | \"dns\",\n): Promise<DBMonitor> {\n  if (!id || id.trim() === \"\") {\n    throw monitorIdRequiredError();\n  }\n\n  const dbMon = await getMonitorById(Number(id), workspaceId);\n  if (!dbMon) {\n    throw monitorNotFoundError(id);\n  }\n\n  if (dbMon.jobType !== expectedJobType) {\n    throw monitorTypeMismatchError(id, expectedJobType, dbMon.jobType);\n  }\n\n  return dbMon;\n}\n\ntype ParsedMonitor = ReturnType<typeof selectMonitorSchema.parse>;\n\n/**\n * Helper to perform update and return the updated monitor.\n */\nasync function performUpdateAndReturn<T>(\n  monitorId: number,\n  requestId: string,\n  updateValues: Record<string, unknown>,\n  converter: (data: ParsedMonitor) => T,\n): Promise<{ monitor: T }> {\n  const updatedMonitor = await db\n    .update(monitor)\n    .set(updateValues)\n    .where(eq(monitor.id, monitorId))\n    .returning()\n    .get();\n\n  if (!updatedMonitor) {\n    throw monitorUpdateFailedError(requestId);\n  }\n\n  const parsed = selectMonitorSchema.safeParse(updatedMonitor);\n  if (!parsed.success) {\n    throw monitorParseFailedError(requestId);\n  }\n\n  return { monitor: converter(parsed.data) };\n}\n\n/**\n * Monitor service implementation for ConnectRPC.\n */\nexport const monitorServiceImpl: ServiceImpl<typeof MonitorService> = {\n  async createHTTPMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    if (!req.monitor) {\n      throw monitorRequiredError();\n    }\n\n    const mon = req.monitor;\n\n    // Validate required fields (proto validation handles name, url, periodicity)\n    validateCommonMonitorFields(mon);\n\n    // Check workspace limits\n    await checkMonitorLimits(workspaceId, limits, mon.periodicity, mon.regions);\n\n    // Get common DB values\n    const commonValues = getCommonDbValues(mon);\n\n    // Convert headers and assertions to DB format\n    const headers = headersToDbJson(mon.headers);\n    const assertions = httpAssertionsToDbJson(\n      mon.statusCodeAssertions,\n      mon.bodyAssertions,\n      mon.headerAssertions,\n    );\n\n    // Insert into database\n    const newMonitor = await db\n      .insert(monitor)\n      .values({\n        workspaceId,\n        jobType: \"http\",\n        url: mon.url,\n        method: toValidMethod(httpMethodToString(mon.method)),\n        body: mon.body || undefined,\n        headers,\n        assertions,\n        followRedirects:\n          mon.followRedirects ?? MONITOR_DEFAULTS.followRedirects,\n        ...commonValues,\n      })\n      .returning()\n      .get();\n\n    if (!newMonitor) {\n      throw monitorCreateFailedError();\n    }\n\n    // Parse through schema to transform fields\n    const parsed = selectMonitorSchema.safeParse(newMonitor);\n    if (!parsed.success) {\n      throw monitorParseFailedError();\n    }\n\n    return {\n      monitor: dbMonitorToHttpProto(parsed.data),\n    };\n  },\n\n  async createTCPMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    if (!req.monitor) {\n      throw monitorRequiredError();\n    }\n\n    const mon = req.monitor;\n\n    // Validate required fields (proto validation handles name, uri, periodicity)\n    validateCommonMonitorFields(mon);\n\n    // Check workspace limits\n    await checkMonitorLimits(workspaceId, limits, mon.periodicity, mon.regions);\n\n    // Get common DB values\n    const commonValues = getCommonDbValues(mon);\n\n    // Insert into database\n    const newMonitor = await db\n      .insert(monitor)\n      .values({\n        workspaceId,\n        jobType: \"tcp\",\n        url: mon.uri,\n        ...commonValues,\n      })\n      .returning()\n      .get();\n\n    if (!newMonitor) {\n      throw monitorCreateFailedError();\n    }\n\n    // Parse through schema to transform fields\n    const parsed = selectMonitorSchema.safeParse(newMonitor);\n    if (!parsed.success) {\n      throw monitorParseFailedError();\n    }\n\n    return {\n      monitor: dbMonitorToTcpProto(parsed.data),\n    };\n  },\n\n  async createDNSMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    if (!req.monitor) {\n      throw monitorRequiredError();\n    }\n\n    const mon = req.monitor;\n\n    // Validate required fields (proto validation handles name, uri, periodicity)\n    validateCommonMonitorFields(mon);\n\n    // Check workspace limits\n    await checkMonitorLimits(workspaceId, limits, mon.periodicity, mon.regions);\n\n    // Get common DB values\n    const commonValues = getCommonDbValues(mon);\n\n    // Convert assertions to DB format\n    const assertions = dnsAssertionsToDbJson(mon.recordAssertions);\n\n    // Insert into database\n    const newMonitor = await db\n      .insert(monitor)\n      .values({\n        workspaceId,\n        jobType: \"dns\",\n        url: mon.uri,\n        assertions,\n        ...commonValues,\n      })\n      .returning()\n      .get();\n\n    if (!newMonitor) {\n      throw monitorCreateFailedError();\n    }\n\n    // Parse through schema to transform fields\n    const parsed = selectMonitorSchema.safeParse(newMonitor);\n    if (!parsed.success) {\n      throw monitorParseFailedError();\n    }\n\n    return {\n      monitor: dbMonitorToDnsProto(parsed.data),\n    };\n  },\n\n  async updateHTTPMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    const dbMon = await validateAndGetMonitor(req.id, workspaceId, \"http\");\n\n    // If no monitor data provided, return current monitor\n    if (!req.monitor) {\n      const parsed = selectMonitorSchema.safeParse(dbMon);\n      if (!parsed.success) {\n        throw monitorParseFailedError(req.id);\n      }\n      return { monitor: dbMonitorToHttpProto(parsed.data) };\n    }\n\n    const mon = req.monitor;\n\n    // Validate regions if provided\n    validateCommonMonitorFields(mon);\n\n    // Check workspace limits if periodicity or regions are changing\n    if (mon.periodicity || (mon.regions && mon.regions.length > 0)) {\n      await checkMonitorLimits(\n        workspaceId,\n        limits,\n        mon.periodicity || undefined,\n        mon.regions && mon.regions.length > 0 ? mon.regions : undefined,\n      );\n    }\n\n    // Build update values - only include fields that are provided\n    const updateValues: Record<string, unknown> =\n      getCommonDbValuesForUpdate(mon);\n\n    // Handle HTTP-specific fields\n    if (mon.url !== undefined && mon.url !== \"\") {\n      updateValues.url = mon.url;\n    }\n\n    if (mon.method !== undefined && mon.method !== 0) {\n      updateValues.method = toValidMethod(httpMethodToString(mon.method));\n    }\n\n    if (mon.body !== undefined) {\n      updateValues.body = mon.body || undefined;\n    }\n\n    if (mon.followRedirects !== undefined) {\n      updateValues.followRedirects = mon.followRedirects;\n    }\n\n    if (mon.headers !== undefined) {\n      updateValues.headers = headersToDbJson(mon.headers);\n    }\n\n    // Handle assertions - update if any assertion type is provided\n    if (\n      mon.statusCodeAssertions !== undefined ||\n      mon.bodyAssertions !== undefined ||\n      mon.headerAssertions !== undefined\n    ) {\n      updateValues.assertions = httpAssertionsToDbJson(\n        mon.statusCodeAssertions ?? [],\n        mon.bodyAssertions ?? [],\n        mon.headerAssertions ?? [],\n      );\n    }\n\n    return performUpdateAndReturn(\n      dbMon.id,\n      req.id,\n      updateValues,\n      dbMonitorToHttpProto,\n    );\n  },\n\n  async updateTCPMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    const dbMon = await validateAndGetMonitor(req.id, workspaceId, \"tcp\");\n\n    // If no monitor data provided, return current monitor\n    if (!req.monitor) {\n      const parsed = selectMonitorSchema.safeParse(dbMon);\n      if (!parsed.success) {\n        throw monitorParseFailedError(req.id);\n      }\n      return { monitor: dbMonitorToTcpProto(parsed.data) };\n    }\n\n    const mon = req.monitor;\n\n    // Validate regions if provided\n    validateCommonMonitorFields(mon);\n\n    // Check workspace limits if periodicity or regions are changing\n    if (mon.periodicity || (mon.regions && mon.regions.length > 0)) {\n      await checkMonitorLimits(\n        workspaceId,\n        limits,\n        mon.periodicity || undefined,\n        mon.regions && mon.regions.length > 0 ? mon.regions : undefined,\n      );\n    }\n\n    // Build update values - only include fields that are provided\n    const updateValues: Record<string, unknown> =\n      getCommonDbValuesForUpdate(mon);\n\n    // Handle TCP-specific fields\n    if (mon.uri !== undefined && mon.uri !== \"\") {\n      updateValues.url = mon.uri;\n    }\n\n    return performUpdateAndReturn(\n      dbMon.id,\n      req.id,\n      updateValues,\n      dbMonitorToTcpProto,\n    );\n  },\n\n  async updateDNSMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    const dbMon = await validateAndGetMonitor(req.id, workspaceId, \"dns\");\n\n    // If no monitor data provided, return current monitor\n    if (!req.monitor) {\n      const parsed = selectMonitorSchema.safeParse(dbMon);\n      if (!parsed.success) {\n        throw monitorParseFailedError(req.id);\n      }\n      return { monitor: dbMonitorToDnsProto(parsed.data) };\n    }\n\n    const mon = req.monitor;\n\n    // Validate regions if provided\n    validateCommonMonitorFields(mon);\n\n    // Check workspace limits if periodicity or regions are changing\n    if (mon.periodicity || (mon.regions && mon.regions.length > 0)) {\n      await checkMonitorLimits(\n        workspaceId,\n        limits,\n        mon.periodicity || undefined,\n        mon.regions && mon.regions.length > 0 ? mon.regions : undefined,\n      );\n    }\n\n    // Build update values - only include fields that are provided\n    const updateValues: Record<string, unknown> =\n      getCommonDbValuesForUpdate(mon);\n\n    // Handle DNS-specific fields\n    if (mon.uri !== undefined && mon.uri !== \"\") {\n      updateValues.url = mon.uri;\n    }\n\n    // Handle DNS assertions\n    if (mon.recordAssertions !== undefined) {\n      updateValues.assertions = dnsAssertionsToDbJson(mon.recordAssertions);\n    }\n\n    return performUpdateAndReturn(\n      dbMon.id,\n      req.id,\n      updateValues,\n      dbMonitorToDnsProto,\n    );\n  },\n\n  async triggerMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    // Check rate limits\n    const lastMonth = new Date().setMonth(new Date().getMonth() - 1);\n    const countResult = await db\n      .select({ count: sql<number>`count(*)` })\n      .from(monitorRun)\n      .where(\n        and(\n          eq(monitorRun.workspaceId, workspaceId),\n          gte(monitorRun.createdAt, new Date(lastMonth)),\n        ),\n      )\n      .get();\n\n    const count = countResult?.count ?? 0;\n    if (count >= limits[\"synthetic-checks\"]) {\n      throw rateLimitExceededError(limits[\"synthetic-checks\"], count);\n    }\n\n    // Get the monitor\n    const dbMon = await getMonitorById(Number(req.id), workspaceId);\n    if (!dbMon) {\n      throw monitorNotFoundError(req.id);\n    }\n\n    // Validate monitor data\n    const validateMonitor = selectMonitorSchema.safeParse(dbMon);\n    if (!validateMonitor.success) {\n      throw monitorInvalidDataError(req.id);\n    }\n\n    const row = validateMonitor.data;\n\n    // Get monitor status for each region\n    const monitorStatuses = await db\n      .select()\n      .from(monitorStatusTable)\n      .where(eq(monitorStatusTable.monitorId, dbMon.id))\n      .all();\n\n    // Create a monitor run record\n    const timestamp = Date.now();\n    const newRun = await db\n      .insert(monitorRun)\n      .values({\n        monitorId: row.id,\n        workspaceId: row.workspaceId,\n        runnedAt: new Date(timestamp),\n      })\n      .returning()\n      .get();\n\n    if (!newRun) {\n      throw monitorRunCreateFailedError(req.id);\n    }\n\n    // Trigger checks for each region in parallel\n    await Promise.all(\n      validateMonitor.data.regions.map((region) => {\n        const statusEntry = monitorStatuses.find((m) => region === m.region);\n        const status = statusEntry?.status || \"active\";\n        const payload = getCheckerPayload(row, status);\n        const url = getCheckerUrl(row);\n\n        return fetch(url, {\n          headers: {\n            \"Content-Type\": \"application/json\",\n            \"fly-prefer-region\": region,\n            Authorization: `Basic ${env.CRON_SECRET}`,\n          },\n          method: \"POST\",\n          body: JSON.stringify(payload),\n        });\n      }),\n    );\n\n    return { success: true };\n  },\n\n  async deleteMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const dbMon = await getMonitorById(Number(req.id), workspaceId);\n\n    if (!dbMon) {\n      throw monitorNotFoundError(req.id);\n    }\n\n    // Soft delete and clean up related rows atomically\n    await db.transaction(async (tx) => {\n      await tx\n        .update(monitor)\n        .set({\n          active: false,\n          deletedAt: new Date(),\n        })\n        .where(eq(monitor.id, dbMon.id));\n      await tx\n        .delete(monitorTagsToMonitors)\n        .where(eq(monitorTagsToMonitors.monitorId, dbMon.id));\n      await tx\n        .delete(notificationsToMonitors)\n        .where(eq(notificationsToMonitors.monitorId, dbMon.id));\n      await tx\n        .delete(pageComponent)\n        .where(eq(pageComponent.monitorId, dbMon.id));\n    });\n\n    return { success: true };\n  },\n\n  async listMonitors(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const limit = Math.min(Math.max(req.limit ?? 50, 1), 100);\n    const offset = req.offset ?? 0;\n\n    // Build query conditions\n    const conditions = [\n      eq(monitor.workspaceId, workspaceId),\n      isNull(monitor.deletedAt),\n    ];\n\n    // Get total count\n    const countResult = await db\n      .select({ count: sql<number>`count(*)` })\n      .from(monitor)\n      .where(and(...conditions))\n      .get();\n\n    const totalCount = countResult?.count ?? 0;\n\n    // Get monitors\n    const monitors = await db\n      .select()\n      .from(monitor)\n      .where(and(...conditions))\n      .limit(limit)\n      .offset(offset)\n      .all();\n\n    // Group monitors by type\n    const httpMonitors: HTTPMonitor[] = [];\n    const tcpMonitors: TCPMonitor[] = [];\n    const dnsMonitors: DNSMonitor[] = [];\n\n    for (const m of monitors) {\n      // Parse through schema to transform fields\n      const parsed = selectMonitorSchema.safeParse(m);\n      if (!parsed.success) {\n        continue; // Skip invalid monitors\n      }\n\n      switch (parsed.data.jobType) {\n        case \"http\":\n          httpMonitors.push(dbMonitorToHttpProto(parsed.data));\n          break;\n        case \"tcp\":\n          tcpMonitors.push(dbMonitorToTcpProto(parsed.data));\n          break;\n        case \"dns\":\n          dnsMonitors.push(dbMonitorToDnsProto(parsed.data));\n          break;\n      }\n    }\n\n    return {\n      httpMonitors,\n      tcpMonitors,\n      dnsMonitors,\n      totalSize: totalCount,\n    };\n  },\n\n  async getMonitorStatus(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    // Get the monitor\n    const dbMon = await getMonitorById(Number(req.id), workspaceId);\n    if (!dbMon) {\n      throw monitorNotFoundError(req.id);\n    }\n\n    // Parse monitor to get configured regions\n    const parsed = selectMonitorSchema.safeParse(dbMon);\n    if (!parsed.success) {\n      throw monitorParseFailedError(req.id);\n    }\n\n    // Get monitor status only for configured regions\n    const monitorStatuses = await db\n      .select()\n      .from(monitorStatusTable)\n      .where(\n        and(\n          eq(monitorStatusTable.monitorId, dbMon.id),\n          inArray(monitorStatusTable.region, parsed.data.regions),\n        ),\n      )\n      .all();\n\n    // Map to proto format\n    const regions: RegionStatus[] = monitorStatuses.map((s) => ({\n      $typeName: \"openstatus.monitor.v1.RegionStatus\" as const,\n      region: stringToRegion(s.region),\n      status: stringToMonitorStatus(s.status),\n    }));\n\n    return {\n      id: String(dbMon.id),\n      regions,\n    };\n  },\n\n  async getMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    // Get the monitor\n    const dbMon = await getMonitorById(Number(req.id), workspaceId);\n    if (!dbMon) {\n      throw monitorNotFoundError(req.id);\n    }\n\n    // Parse monitor data\n    const parsed = selectMonitorSchema.safeParse(dbMon);\n    if (!parsed.success) {\n      throw monitorParseFailedError(req.id);\n    }\n\n    const monitorData = parsed.data;\n\n    // Convert to appropriate proto type based on jobType\n    let monitorConfig: MonitorConfig;\n\n    switch (monitorData.jobType) {\n      case \"http\":\n        monitorConfig = {\n          $typeName: \"openstatus.monitor.v1.MonitorConfig\",\n          config: { case: \"http\", value: dbMonitorToHttpProto(monitorData) },\n        };\n        break;\n      case \"tcp\":\n        monitorConfig = {\n          $typeName: \"openstatus.monitor.v1.MonitorConfig\",\n          config: { case: \"tcp\", value: dbMonitorToTcpProto(monitorData) },\n        };\n        break;\n      case \"dns\":\n        monitorConfig = {\n          $typeName: \"openstatus.monitor.v1.MonitorConfig\",\n          config: { case: \"dns\", value: dbMonitorToDnsProto(monitorData) },\n        };\n        break;\n      default: {\n        const _exhaustive: never = monitorData.jobType;\n        throw monitorTypeMismatchError(\n          req.id,\n          \"http, tcp, or dns\",\n          monitorData.jobType,\n        );\n      }\n    }\n\n    return {\n      $typeName: \"openstatus.monitor.v1.GetMonitorResponse\",\n      monitor: monitorConfig,\n    } satisfies GetMonitorResponse;\n  },\n\n  async getMonitorSummary(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    // Get the monitor\n    const dbMon = await getMonitorById(Number(req.id), workspaceId);\n    if (!dbMon) {\n      throw monitorNotFoundError(req.id);\n    }\n\n    // Parse monitor data\n    const parsed = selectMonitorSchema.safeParse(dbMon);\n    if (!parsed.success) {\n      throw monitorParseFailedError(req.id);\n    }\n\n    const monitorData = parsed.data;\n    const timeRangeKey = timeRangeToKey(req.timeRange);\n    const effectiveTimeRange =\n      req.timeRange === TimeRange.TIME_RANGE_UNSPECIFIED\n        ? TimeRange.TIME_RANGE_1D\n        : req.timeRange;\n\n    // Get regions to filter by (use request regions or monitor's configured regions)\n    const regionStrings =\n      req.regions.length > 0\n        ? regionsToStrings(req.regions)\n        : monitorData.regions;\n\n    // Build Tinybird query parameters\n    const queryParams = {\n      monitorId: req.id,\n      regions: regionStrings.length > 0 ? regionStrings : undefined,\n    };\n\n    // Call appropriate Tinybird method based on monitor type and time range\n    const metricsResult = await getMetricsByTypeAndRange(\n      monitorData.jobType,\n      timeRangeKey,\n      queryParams,\n    );\n\n    if (!metricsResult || metricsResult.data.length === 0) {\n      // Return empty response if no data\n      return {\n        $typeName: \"openstatus.monitor.v1.GetMonitorSummaryResponse\" as const,\n        id: req.id,\n        lastPingAt: \"\",\n        totalSuccessful: BigInt(0),\n        totalDegraded: BigInt(0),\n        totalFailed: BigInt(0),\n        p50: BigInt(0),\n        p75: BigInt(0),\n        p90: BigInt(0),\n        p95: BigInt(0),\n        p99: BigInt(0),\n        timeRange: effectiveTimeRange,\n        regions: stringsToRegions(regionStrings),\n      } satisfies GetMonitorSummaryResponse;\n    }\n\n    const metrics = metricsResult.data[0];\n\n    // Format last timestamp to RFC 3339\n    const lastPingAt = metrics.lastTimestamp\n      ? new Date(metrics.lastTimestamp).toISOString()\n      : \"\";\n\n    return {\n      $typeName: \"openstatus.monitor.v1.GetMonitorSummaryResponse\" as const,\n      id: req.id,\n      lastPingAt,\n      totalSuccessful: BigInt(metrics.success ?? 0),\n      totalDegraded: BigInt(metrics.degraded ?? 0),\n      totalFailed: BigInt(metrics.error ?? 0),\n      p50: BigInt(Math.round(metrics.p50Latency ?? 0)),\n      p75: BigInt(Math.round(metrics.p75Latency ?? 0)),\n      p90: BigInt(Math.round(metrics.p90Latency ?? 0)),\n      p95: BigInt(Math.round(metrics.p95Latency ?? 0)),\n      p99: BigInt(Math.round(metrics.p99Latency ?? 0)),\n      timeRange: effectiveTimeRange,\n      regions: stringsToRegions(regionStrings),\n    } satisfies GetMonitorSummaryResponse;\n  },\n};\n\n/**\n * Get metrics from Tinybird based on monitor type and time range.\n */\nasync function getMetricsByTypeAndRange(\n  jobType: string,\n  timeRange: TimeRangeKey,\n  params: { monitorId: string; regions?: string[] },\n) {\n  switch (jobType) {\n    case \"http\":\n      switch (timeRange) {\n        case \"1d\":\n          return tb.httpMetricsDaily(params);\n        case \"7d\":\n          return tb.httpMetricsWeekly(params);\n        case \"14d\":\n          return tb.httpMetricsBiweekly(params);\n      }\n      break;\n    case \"tcp\":\n      switch (timeRange) {\n        case \"1d\":\n          return tb.tcpMetricsDaily(params);\n        case \"7d\":\n          return tb.tcpMetricsWeekly(params);\n        case \"14d\":\n          return tb.tcpMetricsBiweekly(params);\n      }\n      break;\n    case \"dns\":\n      switch (timeRange) {\n        case \"1d\":\n          return tb.dnsMetricsDaily(params);\n        case \"7d\":\n          return tb.dnsMetricsWeekly(params);\n        case \"14d\":\n          return tb.dnsMetricsBiweekly(params);\n      }\n      break;\n  }\n  return null;\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/limits.ts",
    "content": "import { Code, ConnectError } from \"@connectrpc/connect\";\nimport { and, db, eq, isNull, sql } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\nimport { monitorRegionSchema } from \"@openstatus/db/src/schema/constants\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport type { Periodicity, Region } from \"@openstatus/proto/monitor/v1\";\nimport { z } from \"zod\";\n\nimport { periodicityToString, regionsToStrings } from \"./converters\";\n\n/**\n * Check workspace limits for creating a new monitor.\n * Throws ConnectError with PermissionDenied if any limit is exceeded.\n */\nexport async function checkMonitorLimits(\n  workspaceId: number,\n  limits: Limits,\n  periodicity: Periodicity | undefined,\n  regions: Region[] | undefined,\n): Promise<void> {\n  // Check monitor count limit\n  const countResult = await db\n    .select({ count: sql<number>`count(*)` })\n    .from(monitor)\n    .where(and(eq(monitor.workspaceId, workspaceId), isNull(monitor.deletedAt)))\n    .get();\n\n  const count = countResult?.count ?? 0;\n  if (count >= limits.monitors) {\n    throw new ConnectError(\"Upgrade for more monitors\", Code.PermissionDenied);\n  }\n\n  // Check periodicity limit\n  if (periodicity) {\n    const periodicityStr = periodicityToString(periodicity);\n    if (!limits.periodicity.includes(periodicityStr)) {\n      throw new ConnectError(\n        \"Upgrade for more periodicity options\",\n        Code.PermissionDenied,\n      );\n    }\n  }\n\n  // Check regions limits\n  if (regions && regions.length > 0) {\n    const regionStrings = z\n      .array(monitorRegionSchema)\n      .parse(regionsToStrings(regions));\n\n    // Check max regions limit\n    if (regionStrings.length > limits[\"max-regions\"]) {\n      throw new ConnectError(\"Upgrade for more regions\", Code.PermissionDenied);\n    }\n\n    // Check if each region is allowed\n    for (const region of regionStrings) {\n      if (!limits.regions.includes(region)) {\n        throw new ConnectError(\n          `Region '${region}' is not available on your plan. Upgrade for more regions`,\n          Code.PermissionDenied,\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/monitor/validators.ts",
    "content": "import { Code, ConnectError } from \"@connectrpc/connect\";\nimport { monitorPeriodicity } from \"@openstatus/db/src/schema/constants\";\nimport { monitorMethods } from \"@openstatus/db/src/schema/monitors/constants\";\nimport type { Periodicity, Region } from \"@openstatus/proto/monitor/v1\";\n\nimport {\n  MONITOR_DEFAULTS,\n  openTelemetryToDb,\n  periodicityToString,\n  regionsToDbString,\n  regionsToStrings,\n  validateRegions,\n} from \"./converters\";\n\ntype MonitorPeriodicity = (typeof monitorPeriodicity)[number];\ntype MonitorMethod = (typeof monitorMethods)[number];\n\n/**\n * Validate and convert periodicity string to enum type.\n */\nexport function toValidPeriodicity(\n  value: string | undefined,\n): MonitorPeriodicity {\n  const valid = monitorPeriodicity as readonly string[];\n  if (value && valid.includes(value)) {\n    return value as MonitorPeriodicity;\n  }\n  return \"1m\";\n}\n\n/**\n * Validate and convert method string to enum type.\n */\nexport function toValidMethod(value: string | undefined): MonitorMethod {\n  const upper = value?.toUpperCase();\n  const valid = monitorMethods as readonly string[];\n  if (upper && valid.includes(upper)) {\n    return upper as MonitorMethod;\n  }\n  return \"GET\";\n}\n\n/**\n * Validate required monitor fields common to all monitor types.\n * Note: name, url/uri, and periodicity are validated by protovalidate interceptor.\n * Throws ConnectError if validation fails.\n */\nexport function validateCommonMonitorFields(mon: {\n  regions?: Region[];\n}): void {\n  if (mon.regions && mon.regions.length > 0) {\n    const regionStrings = regionsToStrings(mon.regions);\n    const invalidRegions = validateRegions(regionStrings);\n    if (invalidRegions.length > 0) {\n      throw new ConnectError(\n        `Invalid regions: ${invalidRegions.join(\", \")}`,\n        Code.InvalidArgument,\n      );\n    }\n  }\n}\n\n/**\n * Extract common database values for all monitor types.\n */\nexport function getCommonDbValues(mon: {\n  name: string;\n  periodicity?: Periodicity;\n  timeout?: bigint;\n  degradedAt?: bigint;\n  active?: boolean;\n  description?: string;\n  public?: boolean;\n  regions?: Region[];\n  retry?: bigint;\n  openTelemetry?: Parameters<typeof openTelemetryToDb>[0];\n}) {\n  const otelConfig = openTelemetryToDb(mon.openTelemetry);\n\n  const periodicityStr = mon.periodicity\n    ? periodicityToString(mon.periodicity)\n    : undefined;\n  const regionStrings = mon.regions ? regionsToStrings(mon.regions) : [];\n\n  return {\n    name: mon.name,\n    periodicity: toValidPeriodicity(periodicityStr),\n    timeout: mon.timeout ? Number(mon.timeout) : MONITOR_DEFAULTS.timeout,\n    degradedAfter: mon.degradedAt ? Number(mon.degradedAt) : undefined,\n    active: mon.active ?? MONITOR_DEFAULTS.active,\n    description: mon.description || MONITOR_DEFAULTS.description,\n    public: mon.public ?? MONITOR_DEFAULTS.public,\n    regions: regionsToDbString(regionStrings),\n    retry: mon.retry ? Number(mon.retry) : MONITOR_DEFAULTS.retry,\n    otelEndpoint: otelConfig.otelEndpoint,\n    otelHeaders: otelConfig.otelHeaders,\n  };\n}\n\n/**\n * Extract common database values for update operations.\n * Only includes fields that are explicitly provided (not undefined).\n * This enables partial updates where only specified fields are changed.\n */\nexport function getCommonDbValuesForUpdate(mon: {\n  name?: string;\n  periodicity?: Periodicity;\n  timeout?: bigint;\n  degradedAt?: bigint;\n  active?: boolean;\n  description?: string;\n  public?: boolean;\n  regions?: Region[];\n  retry?: bigint;\n  openTelemetry?: Parameters<typeof openTelemetryToDb>[0];\n}) {\n  const result: Record<string, unknown> = {};\n\n  if (mon.name !== undefined && mon.name !== \"\") {\n    result.name = mon.name;\n  }\n\n  if (mon.periodicity !== undefined && mon.periodicity !== 0) {\n    const periodicityStr = periodicityToString(mon.periodicity);\n    result.periodicity = toValidPeriodicity(periodicityStr);\n  }\n\n  if (mon.timeout !== undefined && mon.timeout !== BigInt(0)) {\n    result.timeout = Number(mon.timeout);\n  }\n\n  if (mon.degradedAt !== undefined) {\n    result.degradedAfter = Number(mon.degradedAt);\n  }\n\n  if (mon.active !== undefined) {\n    result.active = mon.active;\n  }\n\n  if (mon.description !== undefined) {\n    result.description = mon.description;\n  }\n\n  if (mon.public !== undefined) {\n    result.public = mon.public;\n  }\n\n  if (mon.regions !== undefined && mon.regions.length > 0) {\n    const regionStrings = regionsToStrings(mon.regions);\n    result.regions = regionsToDbString(regionStrings);\n  }\n\n  if (mon.retry !== undefined && mon.retry !== BigInt(0)) {\n    result.retry = Number(mon.retry);\n  }\n\n  if (mon.openTelemetry !== undefined) {\n    const otelConfig = openTelemetryToDb(mon.openTelemetry);\n    result.otelEndpoint = otelConfig.otelEndpoint;\n    result.otelHeaders = otelConfig.otelHeaders;\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/notification/__tests__/notification.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  notification,\n  notificationsToMonitors,\n} from \"@openstatus/db/src/schema\";\n\nimport { app } from \"@/index\";\n\n/**\n * Helper to make ConnectRPC requests using the Connect protocol (JSON).\n * Connect uses POST with JSON body at /rpc/<service>/<method>\n */\nasync function connectRequest(\n  method: string,\n  body: Record<string, unknown> = {},\n  headers: Record<string, string> = {},\n) {\n  return app.request(\n    `/rpc/openstatus.notification.v1.NotificationService/${method}`,\n    {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        ...headers,\n      },\n      body: JSON.stringify(body),\n    },\n  );\n}\n\nconst TEST_PREFIX = \"rpc-notification-test\";\nlet testNotificationId: number;\nlet testNotificationToDeleteId: number;\nlet testNotificationToUpdateId: number;\n\nbeforeAll(async () => {\n  // Clean up any existing test data\n  await db\n    .delete(notification)\n    .where(eq(notification.name, `${TEST_PREFIX}-main`));\n  await db\n    .delete(notification)\n    .where(eq(notification.name, `${TEST_PREFIX}-to-delete`));\n  await db\n    .delete(notification)\n    .where(eq(notification.name, `${TEST_PREFIX}-to-update`));\n\n  // Create test notification (email type)\n  const notificationRecord = await db\n    .insert(notification)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-main`,\n      provider: \"email\",\n      data: JSON.stringify({ email: \"test@example.com\" }),\n    })\n    .returning()\n    .get();\n  testNotificationId = notificationRecord.id;\n\n  // Create monitor association\n  await db.insert(notificationsToMonitors).values({\n    notificationId: notificationRecord.id,\n    monitorId: 1, // Use seeded monitor\n  });\n\n  // Create notification to delete\n  const deleteRecord = await db\n    .insert(notification)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-to-delete`,\n      provider: \"discord\",\n      data: JSON.stringify({\n        discord: \"https://discord.com/api/webhooks/test\",\n      }),\n    })\n    .returning()\n    .get();\n  testNotificationToDeleteId = deleteRecord.id;\n\n  // Create notification to update\n  const updateRecord = await db\n    .insert(notification)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-to-update`,\n      provider: \"slack\",\n      data: JSON.stringify({ slack: \"https://hooks.slack.com/services/test\" }),\n    })\n    .returning()\n    .get();\n  testNotificationToUpdateId = updateRecord.id;\n\n  await db.insert(notificationsToMonitors).values({\n    notificationId: updateRecord.id,\n    monitorId: 1,\n  });\n});\n\nafterAll(async () => {\n  // Clean up associations\n  await db\n    .delete(notificationsToMonitors)\n    .where(eq(notificationsToMonitors.notificationId, testNotificationId));\n  await db\n    .delete(notificationsToMonitors)\n    .where(\n      eq(notificationsToMonitors.notificationId, testNotificationToUpdateId),\n    );\n\n  // Clean up notifications\n  await db\n    .delete(notification)\n    .where(eq(notification.name, `${TEST_PREFIX}-main`));\n  await db\n    .delete(notification)\n    .where(eq(notification.name, `${TEST_PREFIX}-to-delete`));\n  await db\n    .delete(notification)\n    .where(eq(notification.name, `${TEST_PREFIX}-to-update`));\n});\n\ndescribe(\"NotificationService.CreateNotification\", () => {\n  test(\"creates a new email notification\", async () => {\n    const res = await connectRequest(\n      \"CreateNotification\",\n      {\n        name: `${TEST_PREFIX}-created-email`,\n        provider: \"NOTIFICATION_PROVIDER_EMAIL\",\n        data: {\n          email: {\n            email: \"created@example.com\",\n          },\n        },\n        monitorIds: [\"1\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"notification\");\n    expect(data.notification.name).toBe(`${TEST_PREFIX}-created-email`);\n    expect(data.notification.provider).toBe(\"NOTIFICATION_PROVIDER_EMAIL\");\n    expect(data.notification.monitorIds).toContain(\"1\");\n\n    // Clean up\n    await db\n      .delete(notificationsToMonitors)\n      .where(\n        eq(\n          notificationsToMonitors.notificationId,\n          Number(data.notification.id),\n        ),\n      );\n    await db\n      .delete(notification)\n      .where(eq(notification.id, Number(data.notification.id)));\n  });\n\n  test(\"creates a new discord notification\", async () => {\n    const res = await connectRequest(\n      \"CreateNotification\",\n      {\n        name: `${TEST_PREFIX}-created-discord`,\n        provider: \"NOTIFICATION_PROVIDER_DISCORD\",\n        data: {\n          discord: {\n            webhookUrl: \"https://discord.com/api/webhooks/123/abc\",\n          },\n        },\n        monitorIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"notification\");\n    expect(data.notification.name).toBe(`${TEST_PREFIX}-created-discord`);\n    expect(data.notification.provider).toBe(\"NOTIFICATION_PROVIDER_DISCORD\");\n\n    // Clean up\n    await db\n      .delete(notification)\n      .where(eq(notification.id, Number(data.notification.id)));\n  });\n\n  test(\"creates a new slack notification\", async () => {\n    const res = await connectRequest(\n      \"CreateNotification\",\n      {\n        name: `${TEST_PREFIX}-created-slack`,\n        provider: \"NOTIFICATION_PROVIDER_SLACK\",\n        data: {\n          slack: {\n            webhookUrl: \"https://hooks.slack.com/services/T00/B00/XXX\",\n          },\n        },\n        monitorIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"notification\");\n    expect(data.notification.provider).toBe(\"NOTIFICATION_PROVIDER_SLACK\");\n\n    // Clean up\n    await db\n      .delete(notification)\n      .where(eq(notification.id, Number(data.notification.id)));\n  });\n\n  test(\"creates notification with multiple monitors\", async () => {\n    const res = await connectRequest(\n      \"CreateNotification\",\n      {\n        name: `${TEST_PREFIX}-multi-monitor`,\n        provider: \"NOTIFICATION_PROVIDER_EMAIL\",\n        data: {\n          email: {\n            email: \"multi@example.com\",\n          },\n        },\n        monitorIds: [\"1\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.notification.monitorIds).toContain(\"1\");\n\n    // Clean up\n    await db\n      .delete(notificationsToMonitors)\n      .where(\n        eq(\n          notificationsToMonitors.notificationId,\n          Number(data.notification.id),\n        ),\n      );\n    await db\n      .delete(notification)\n      .where(eq(notification.id, Number(data.notification.id)));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"CreateNotification\", {\n      name: \"Unauthorized test\",\n      provider: \"NOTIFICATION_PROVIDER_EMAIL\",\n      data: {\n        email: {\n          email: \"test@example.com\",\n        },\n      },\n      monitorIds: [],\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns error for invalid monitor ID\", async () => {\n    const res = await connectRequest(\n      \"CreateNotification\",\n      {\n        name: `${TEST_PREFIX}-invalid-monitor`,\n        provider: \"NOTIFICATION_PROVIDER_EMAIL\",\n        data: {\n          email: {\n            email: \"test@example.com\",\n          },\n        },\n        monitorIds: [\"99999\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error for unspecified provider\", async () => {\n    const res = await connectRequest(\n      \"CreateNotification\",\n      {\n        name: `${TEST_PREFIX}-no-provider`,\n        provider: \"NOTIFICATION_PROVIDER_UNSPECIFIED\",\n        data: {},\n        monitorIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n});\n\ndescribe(\"NotificationService.GetNotification\", () => {\n  test(\"returns notification with monitor IDs\", async () => {\n    const res = await connectRequest(\n      \"GetNotification\",\n      { id: String(testNotificationId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"notification\");\n    expect(data.notification.id).toBe(String(testNotificationId));\n    expect(data.notification.name).toBe(`${TEST_PREFIX}-main`);\n    expect(data.notification.provider).toBe(\"NOTIFICATION_PROVIDER_EMAIL\");\n    expect(data.notification.monitorIds).toContain(\"1\");\n    expect(data.notification).toHaveProperty(\"createdAt\");\n    expect(data.notification).toHaveProperty(\"updatedAt\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"GetNotification\", {\n      id: String(testNotificationId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent notification\", async () => {\n    const res = await connectRequest(\n      \"GetNotification\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 404 for notification in different workspace\", async () => {\n    // Create notification in workspace 2\n    const otherRecord = await db\n      .insert(notification)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-other-workspace`,\n        provider: \"email\",\n        data: JSON.stringify({ email: \"other@example.com\" }),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"GetNotification\",\n        { id: String(otherRecord.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(notification).where(eq(notification.id, otherRecord.id));\n    }\n  });\n\n  test(\"returns error when ID is empty string\", async () => {\n    const res = await connectRequest(\n      \"GetNotification\",\n      { id: \"\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n});\n\ndescribe(\"NotificationService.ListNotifications\", () => {\n  test(\"returns notifications for authenticated workspace\", async () => {\n    const res = await connectRequest(\n      \"ListNotifications\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"notifications\");\n    expect(Array.isArray(data.notifications)).toBe(true);\n    expect(data).toHaveProperty(\"totalSize\");\n  });\n\n  test(\"returns notifications with correct summary structure\", async () => {\n    const res = await connectRequest(\n      \"ListNotifications\",\n      { limit: 100 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    const record = data.notifications?.find(\n      (n: { id: string }) => n.id === String(testNotificationId),\n    );\n\n    expect(record).toBeDefined();\n    expect(record.name).toBe(`${TEST_PREFIX}-main`);\n    expect(record.provider).toBe(\"NOTIFICATION_PROVIDER_EMAIL\");\n    expect(record).toHaveProperty(\"monitorCount\");\n    expect(record).toHaveProperty(\"createdAt\");\n    expect(record).toHaveProperty(\"updatedAt\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"ListNotifications\", {});\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"respects limit parameter\", async () => {\n    const res = await connectRequest(\n      \"ListNotifications\",\n      { limit: 1 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.notifications?.length || 0).toBeLessThanOrEqual(1);\n  });\n\n  test(\"respects offset parameter\", async () => {\n    // Get total count first\n    const res1 = await connectRequest(\n      \"ListNotifications\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n    const data1 = await res1.json();\n    const totalSize = data1.totalSize;\n\n    if (totalSize > 1) {\n      // Get first page\n      const res2 = await connectRequest(\n        \"ListNotifications\",\n        { limit: 1, offset: 0 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n      const data2 = await res2.json();\n\n      // Get second page\n      const res3 = await connectRequest(\n        \"ListNotifications\",\n        { limit: 1, offset: 1 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n      const data3 = await res3.json();\n\n      // Should have different notifications\n      if (data2.notifications?.length > 0 && data3.notifications?.length > 0) {\n        expect(data2.notifications[0].id).not.toBe(data3.notifications[0].id);\n      }\n    }\n  });\n\n  test(\"only returns notifications for authenticated workspace\", async () => {\n    // Create notification in workspace 2\n    const otherRecord = await db\n      .insert(notification)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-other-workspace-list`,\n        provider: \"email\",\n        data: JSON.stringify({ email: \"other@example.com\" }),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"ListNotifications\",\n        { limit: 100 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      const recordIds = (data.notifications || []).map(\n        (n: { id: string }) => n.id,\n      );\n\n      expect(recordIds).not.toContain(String(otherRecord.id));\n    } finally {\n      await db.delete(notification).where(eq(notification.id, otherRecord.id));\n    }\n  });\n});\n\ndescribe(\"NotificationService.UpdateNotification\", () => {\n  test(\"updates notification name\", async () => {\n    const res = await connectRequest(\n      \"UpdateNotification\",\n      {\n        id: String(testNotificationToUpdateId),\n        name: `${TEST_PREFIX}-updated-name`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"notification\");\n    expect(data.notification.name).toBe(`${TEST_PREFIX}-updated-name`);\n\n    // Restore original name\n    await db\n      .update(notification)\n      .set({ name: `${TEST_PREFIX}-to-update` })\n      .where(eq(notification.id, testNotificationToUpdateId));\n  });\n\n  test(\"updates monitor associations\", async () => {\n    const res = await connectRequest(\n      \"UpdateNotification\",\n      {\n        id: String(testNotificationToUpdateId),\n        monitorIds: [\"1\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.notification.monitorIds).toContain(\"1\");\n  });\n\n  test(\"clears monitor associations with empty array\", async () => {\n    const res = await connectRequest(\n      \"UpdateNotification\",\n      {\n        id: String(testNotificationToUpdateId),\n        monitorIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    const monitorIds = data.notification.monitorIds ?? [];\n    expect(monitorIds).toHaveLength(0);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UpdateNotification\", {\n      id: String(testNotificationToUpdateId),\n      name: \"Unauthorized update\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent notification\", async () => {\n    const res = await connectRequest(\n      \"UpdateNotification\",\n      { id: \"99999\", name: \"Non-existent update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when ID is empty string\", async () => {\n    const res = await connectRequest(\n      \"UpdateNotification\",\n      { id: \"\", name: \"Empty ID update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 404 for notification in different workspace\", async () => {\n    // Create notification in workspace 2\n    const otherRecord = await db\n      .insert(notification)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-other-workspace-update`,\n        provider: \"email\",\n        data: JSON.stringify({ email: \"other@example.com\" }),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"UpdateNotification\",\n        { id: String(otherRecord.id), name: \"Should not update\" },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(notification).where(eq(notification.id, otherRecord.id));\n    }\n  });\n\n  test(\"returns error for invalid monitor ID on update\", async () => {\n    const res = await connectRequest(\n      \"UpdateNotification\",\n      {\n        id: String(testNotificationToUpdateId),\n        monitorIds: [\"99999\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n});\n\ndescribe(\"NotificationService.DeleteNotification\", () => {\n  test(\"successfully deletes existing notification\", async () => {\n    const res = await connectRequest(\n      \"DeleteNotification\",\n      { id: String(testNotificationToDeleteId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.success).toBe(true);\n\n    // Verify it's deleted\n    const deleted = await db\n      .select()\n      .from(notification)\n      .where(eq(notification.id, testNotificationToDeleteId))\n      .get();\n    expect(deleted).toBeUndefined();\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"DeleteNotification\", { id: \"1\" });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent notification\", async () => {\n    const res = await connectRequest(\n      \"DeleteNotification\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when ID is empty string\", async () => {\n    const res = await connectRequest(\n      \"DeleteNotification\",\n      { id: \"\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 404 for notification in different workspace\", async () => {\n    // Create notification in workspace 2\n    const otherRecord = await db\n      .insert(notification)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-other-workspace-delete`,\n        provider: \"email\",\n        data: JSON.stringify({ email: \"other@example.com\" }),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"DeleteNotification\",\n        { id: String(otherRecord.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n\n      // Verify it wasn't deleted\n      const stillExists = await db\n        .select()\n        .from(notification)\n        .where(eq(notification.id, otherRecord.id))\n        .get();\n      expect(stillExists).toBeDefined();\n    } finally {\n      await db.delete(notification).where(eq(notification.id, otherRecord.id));\n    }\n  });\n});\n\ndescribe(\"NotificationService.CheckNotificationLimit\", () => {\n  test(\"returns limit information for authenticated workspace\", async () => {\n    const res = await connectRequest(\n      \"CheckNotificationLimit\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Proto3 JSON omits fields with default values (false, 0)\n    // So we check that the response is valid and contains expected types\n    // when the values are non-default\n    const limitReached = data.limitReached ?? false;\n    const currentCount = data.currentCount ?? 0;\n    const maxCount = data.maxCount ?? 0;\n\n    expect(typeof limitReached).toBe(\"boolean\");\n    expect(typeof currentCount).toBe(\"number\");\n    expect(typeof maxCount).toBe(\"number\");\n    expect(currentCount).toBeGreaterThanOrEqual(0);\n    expect(maxCount).toBeGreaterThanOrEqual(0);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"CheckNotificationLimit\", {});\n\n    expect(res.status).toBe(401);\n  });\n});\n\ndescribe(\"NotificationService.SendTestNotification\", () => {\n  // Note: These tests verify error handling since we can't actually send\n  // real notifications in tests without mocking external services\n\n  test(\"returns error for unsupported email provider\", async () => {\n    const res = await connectRequest(\n      \"SendTestNotification\",\n      {\n        provider: \"NOTIFICATION_PROVIDER_EMAIL\",\n        data: {\n          email: {\n            email: \"test@example.com\",\n          },\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    // Email doesn't support test notifications\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"not supported\");\n  });\n\n  test(\"returns error for unsupported SMS provider\", async () => {\n    const res = await connectRequest(\n      \"SendTestNotification\",\n      {\n        provider: \"NOTIFICATION_PROVIDER_SMS\",\n        data: {\n          sms: {\n            phoneNumber: \"+1234567890\",\n          },\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    // SMS doesn't support test notifications\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"not supported\");\n  });\n\n  test(\"returns error when no data provided\", async () => {\n    const res = await connectRequest(\n      \"SendTestNotification\",\n      {\n        provider: \"NOTIFICATION_PROVIDER_DISCORD\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns error when data doesn't match provider\", async () => {\n    const res = await connectRequest(\n      \"SendTestNotification\",\n      {\n        provider: \"NOTIFICATION_PROVIDER_DISCORD\",\n        data: {\n          email: {\n            email: \"test@example.com\",\n          },\n        },\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"Expected discord data\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"SendTestNotification\", {\n      provider: \"NOTIFICATION_PROVIDER_EMAIL\",\n      data: {\n        email: {\n          email: \"test@example.com\",\n        },\n      },\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns error for unspecified provider\", async () => {\n    const res = await connectRequest(\n      \"SendTestNotification\",\n      {\n        provider: \"NOTIFICATION_PROVIDER_UNSPECIFIED\",\n        data: {},\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/notification/converters.ts",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport type { NotificationProvider as DBNotificationProvider } from \"@openstatus/db/src/schema\";\nimport type {\n  Notification,\n  NotificationData,\n  NotificationSummary,\n} from \"@openstatus/proto/notification/v1\";\nimport {\n  NotificationDataSchema,\n  NotificationProvider,\n  NotificationSchema,\n  NotificationSummarySchema,\n  OpsgenieRegion,\n} from \"@openstatus/proto/notification/v1\";\n\ntype DBNotification = {\n  id: number;\n  name: string;\n  provider: DBNotificationProvider;\n  data: string | null;\n  workspaceId: number | null;\n  createdAt: Date | null;\n  updatedAt: Date | null;\n};\n\n/**\n * Maps DB provider string to proto NotificationProvider enum.\n */\nexport function dbProviderToProto(\n  provider: DBNotificationProvider,\n): NotificationProvider {\n  const mapping: Record<DBNotificationProvider, NotificationProvider> = {\n    discord: NotificationProvider.DISCORD,\n    email: NotificationProvider.EMAIL,\n    \"google-chat\": NotificationProvider.GOOGLE_CHAT,\n    \"grafana-oncall\": NotificationProvider.GRAFANA_ONCALL,\n    ntfy: NotificationProvider.NTFY,\n    pagerduty: NotificationProvider.PAGERDUTY,\n    opsgenie: NotificationProvider.OPSGENIE,\n    slack: NotificationProvider.SLACK,\n    sms: NotificationProvider.SMS,\n    telegram: NotificationProvider.TELEGRAM,\n    webhook: NotificationProvider.WEBHOOK,\n    whatsapp: NotificationProvider.WHATSAPP,\n  };\n  return mapping[provider] ?? NotificationProvider.UNSPECIFIED;\n}\n\n/**\n * Maps NotificationProvider to expected NotificationData case.\n */\nexport function getExpectedDataCase(\n  provider: NotificationProvider,\n): string | undefined {\n  const mapping: Record<number, string> = {\n    [NotificationProvider.DISCORD]: \"discord\",\n    [NotificationProvider.EMAIL]: \"email\",\n    [NotificationProvider.GOOGLE_CHAT]: \"googleChat\",\n    [NotificationProvider.GRAFANA_ONCALL]: \"grafanaOncall\",\n    [NotificationProvider.NTFY]: \"ntfy\",\n    [NotificationProvider.PAGERDUTY]: \"pagerduty\",\n    [NotificationProvider.OPSGENIE]: \"opsgenie\",\n    [NotificationProvider.SLACK]: \"slack\",\n    [NotificationProvider.SMS]: \"sms\",\n    [NotificationProvider.TELEGRAM]: \"telegram\",\n    [NotificationProvider.WEBHOOK]: \"webhook\",\n    [NotificationProvider.WHATSAPP]: \"whatsapp\",\n  };\n  return mapping[provider];\n}\n\n/**\n * Validates that the notification data matches the provider.\n * Returns an error message if validation fails, undefined if valid.\n */\nexport function validateProviderDataConsistency(\n  provider: NotificationProvider,\n  data: NotificationData | undefined,\n): string | undefined {\n  if (provider === NotificationProvider.UNSPECIFIED) {\n    return \"Provider must be specified\";\n  }\n\n  const expectedCase = getExpectedDataCase(provider);\n  if (!expectedCase) {\n    return `Unknown provider: ${provider}`;\n  }\n\n  if (!data || data.data.case === undefined) {\n    return `Provider ${NotificationProvider[provider]} requires ${expectedCase} data, but no data was provided`;\n  }\n\n  const actualCase = data.data.case;\n  if (actualCase !== expectedCase) {\n    return `Provider ${NotificationProvider[provider]} requires ${expectedCase} data, got ${actualCase}`;\n  }\n\n  return undefined;\n}\n\n/**\n * Maps proto NotificationProvider enum to DB provider string.\n */\nexport function protoProviderToDb(\n  provider: NotificationProvider,\n): DBNotificationProvider {\n  const mapping: Record<number, DBNotificationProvider> = {\n    [NotificationProvider.DISCORD]: \"discord\",\n    [NotificationProvider.EMAIL]: \"email\",\n    [NotificationProvider.GOOGLE_CHAT]: \"google-chat\",\n    [NotificationProvider.GRAFANA_ONCALL]: \"grafana-oncall\",\n    [NotificationProvider.NTFY]: \"ntfy\",\n    [NotificationProvider.PAGERDUTY]: \"pagerduty\",\n    [NotificationProvider.OPSGENIE]: \"opsgenie\",\n    [NotificationProvider.SLACK]: \"slack\",\n    [NotificationProvider.SMS]: \"sms\",\n    [NotificationProvider.TELEGRAM]: \"telegram\",\n    [NotificationProvider.WEBHOOK]: \"webhook\",\n    [NotificationProvider.WHATSAPP]: \"whatsapp\",\n  };\n  return mapping[provider] ?? \"email\";\n}\n\n/**\n * Parses DB JSON data string to proto NotificationData.\n */\nexport function dbDataToProto(\n  provider: DBNotificationProvider,\n  dataStr: string | null,\n): NotificationData | undefined {\n  if (!dataStr) {\n    return undefined;\n  }\n\n  try {\n    const data = JSON.parse(dataStr);\n    const protoData = create(NotificationDataSchema);\n\n    switch (provider) {\n      case \"discord\":\n        if (data.discord) {\n          protoData.data = {\n            case: \"discord\",\n            value: {\n              $typeName: \"openstatus.notification.v1.DiscordData\",\n              webhookUrl: data.discord,\n            },\n          };\n        }\n        break;\n      case \"email\":\n        if (data.email) {\n          protoData.data = {\n            case: \"email\",\n            value: {\n              $typeName: \"openstatus.notification.v1.EmailData\",\n              email: data.email,\n            },\n          };\n        }\n        break;\n      case \"google-chat\":\n        if (data[\"google-chat\"]) {\n          protoData.data = {\n            case: \"googleChat\",\n            value: {\n              $typeName: \"openstatus.notification.v1.GoogleChatData\",\n              webhookUrl: data[\"google-chat\"],\n            },\n          };\n        }\n        break;\n      case \"grafana-oncall\":\n        if (data[\"grafana-oncall\"]) {\n          protoData.data = {\n            case: \"grafanaOncall\",\n            value: {\n              $typeName: \"openstatus.notification.v1.GrafanaOncallData\",\n              webhookUrl: data[\"grafana-oncall\"].webhookUrl,\n            },\n          };\n        }\n        break;\n      case \"ntfy\":\n        if (data.ntfy) {\n          protoData.data = {\n            case: \"ntfy\",\n            value: {\n              $typeName: \"openstatus.notification.v1.NtfyData\",\n              topic: data.ntfy.topic || \"\",\n              serverUrl: data.ntfy.serverUrl || \"https://ntfy.sh\",\n              token: data.ntfy.token,\n            },\n          };\n        }\n        break;\n      case \"pagerduty\":\n        if (data.pagerduty) {\n          protoData.data = {\n            case: \"pagerduty\",\n            value: {\n              $typeName: \"openstatus.notification.v1.PagerDutyData\",\n              integrationKey: data.pagerduty,\n            },\n          };\n        }\n        break;\n      case \"opsgenie\":\n        if (data.opsgenie) {\n          protoData.data = {\n            case: \"opsgenie\",\n            value: {\n              $typeName: \"openstatus.notification.v1.OpsgenieData\",\n              apiKey: data.opsgenie.apiKey,\n              region:\n                data.opsgenie.region === \"eu\"\n                  ? OpsgenieRegion.EU\n                  : OpsgenieRegion.US,\n            },\n          };\n        }\n        break;\n      case \"slack\":\n        if (data.slack) {\n          protoData.data = {\n            case: \"slack\",\n            value: {\n              $typeName: \"openstatus.notification.v1.SlackData\",\n              webhookUrl: data.slack,\n            },\n          };\n        }\n        break;\n      case \"sms\":\n        if (data.sms) {\n          protoData.data = {\n            case: \"sms\",\n            value: {\n              $typeName: \"openstatus.notification.v1.SmsData\",\n              phoneNumber: data.sms,\n            },\n          };\n        }\n        break;\n      case \"telegram\":\n        if (data.telegram) {\n          protoData.data = {\n            case: \"telegram\",\n            value: {\n              $typeName: \"openstatus.notification.v1.TelegramData\",\n              chatId: data.telegram.chatId,\n            },\n          };\n        }\n        break;\n      case \"webhook\":\n        if (data.webhook) {\n          protoData.data = {\n            case: \"webhook\",\n            value: {\n              $typeName: \"openstatus.notification.v1.WebhookData\",\n              endpoint: data.webhook.endpoint,\n              headers:\n                data.webhook.headers?.map(\n                  (h: { key: string; value: string }) => ({\n                    $typeName: \"openstatus.notification.v1.WebhookHeader\",\n                    key: h.key,\n                    value: h.value,\n                  }),\n                ) ?? [],\n            },\n          };\n        }\n        break;\n      case \"whatsapp\":\n        if (data.whatsapp) {\n          protoData.data = {\n            case: \"whatsapp\",\n            value: {\n              $typeName: \"openstatus.notification.v1.WhatsappData\",\n              phoneNumber: data.whatsapp,\n            },\n          };\n        }\n        break;\n    }\n\n    return protoData;\n  } catch (error) {\n    console.error(\"Failed to parse notification data:\", {\n      provider,\n      error: error instanceof Error ? error.message : \"Unknown error\",\n    });\n    return undefined;\n  }\n}\n\n/**\n * Converts proto NotificationData to DB JSON string format.\n */\nexport function protoDataToDb(\n  _provider: NotificationProvider,\n  data: NotificationData | undefined,\n): string {\n  if (!data || data.data.case === undefined) {\n    return \"{}\";\n  }\n\n  switch (data.data.case) {\n    case \"discord\":\n      return JSON.stringify({ discord: data.data.value.webhookUrl });\n    case \"email\":\n      return JSON.stringify({ email: data.data.value.email });\n    case \"googleChat\":\n      return JSON.stringify({ \"google-chat\": data.data.value.webhookUrl });\n    case \"grafanaOncall\":\n      return JSON.stringify({\n        \"grafana-oncall\": { webhookUrl: data.data.value.webhookUrl },\n      });\n    case \"ntfy\":\n      return JSON.stringify({\n        ntfy: {\n          topic: data.data.value.topic,\n          serverUrl: data.data.value.serverUrl,\n          token: data.data.value.token,\n        },\n      });\n    case \"pagerduty\":\n      return JSON.stringify({ pagerduty: data.data.value.integrationKey });\n    case \"opsgenie\":\n      return JSON.stringify({\n        opsgenie: {\n          apiKey: data.data.value.apiKey,\n          region: data.data.value.region === OpsgenieRegion.EU ? \"eu\" : \"us\",\n        },\n      });\n    case \"slack\":\n      return JSON.stringify({ slack: data.data.value.webhookUrl });\n    case \"sms\":\n      return JSON.stringify({ sms: data.data.value.phoneNumber });\n    case \"telegram\":\n      return JSON.stringify({\n        telegram: { chatId: data.data.value.chatId },\n      });\n    case \"webhook\":\n      return JSON.stringify({\n        webhook: {\n          endpoint: data.data.value.endpoint,\n          headers: data.data.value.headers?.map((h) => ({\n            key: h.key,\n            value: h.value,\n          })),\n        },\n      });\n    case \"whatsapp\":\n      return JSON.stringify({ whatsapp: data.data.value.phoneNumber });\n    default:\n      return \"{}\";\n  }\n}\n\n/**\n * Converts a DB notification to proto Notification format.\n */\nexport function dbNotificationToProto(\n  notification: DBNotification,\n  monitorIds: string[],\n): Notification {\n  return create(NotificationSchema, {\n    id: String(notification.id),\n    name: notification.name,\n    provider: dbProviderToProto(notification.provider),\n    data: dbDataToProto(notification.provider, notification.data),\n    monitorIds,\n    createdAt: notification.createdAt?.toISOString() ?? \"\",\n    updatedAt: notification.updatedAt?.toISOString() ?? \"\",\n  });\n}\n\n/**\n * Converts a DB notification to proto NotificationSummary format.\n */\nexport function dbNotificationToProtoSummary(\n  notification: DBNotification,\n  monitorCount: number,\n): NotificationSummary {\n  return create(NotificationSummarySchema, {\n    id: String(notification.id),\n    name: notification.name,\n    provider: dbProviderToProto(notification.provider),\n    monitorCount,\n    createdAt: notification.createdAt?.toISOString() ?? \"\",\n    updatedAt: notification.updatedAt?.toISOString() ?? \"\",\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/notification/errors.ts",
    "content": "import { Code, ConnectError } from \"@connectrpc/connect\";\n\n/**\n * Error reasons for structured error handling.\n */\nexport const ErrorReason = {\n  NOTIFICATION_NOT_FOUND: \"NOTIFICATION_NOT_FOUND\",\n  NOTIFICATION_ID_REQUIRED: \"NOTIFICATION_ID_REQUIRED\",\n  NOTIFICATION_CREATE_FAILED: \"NOTIFICATION_CREATE_FAILED\",\n  NOTIFICATION_UPDATE_FAILED: \"NOTIFICATION_UPDATE_FAILED\",\n  NOTIFICATION_LIMIT_REACHED: \"NOTIFICATION_LIMIT_REACHED\",\n  PROVIDER_NOT_ALLOWED: \"PROVIDER_NOT_ALLOWED\",\n  PROVIDER_NOT_SUPPORTED: \"PROVIDER_NOT_SUPPORTED\",\n  INVALID_NOTIFICATION_DATA: \"INVALID_NOTIFICATION_DATA\",\n  MONITOR_NOT_FOUND: \"MONITOR_NOT_FOUND\",\n  TEST_NOTIFICATION_FAILED: \"TEST_NOTIFICATION_FAILED\",\n} as const;\n\nexport type ErrorReason = (typeof ErrorReason)[keyof typeof ErrorReason];\n\nconst DOMAIN = \"openstatus.dev\";\n\n/**\n * Creates a ConnectError with structured metadata.\n */\nfunction createError(\n  message: string,\n  code: Code,\n  reason: ErrorReason,\n  metadata?: Record<string, string>,\n): ConnectError {\n  const headers = new Headers({\n    \"error-domain\": DOMAIN,\n    \"error-reason\": reason,\n  });\n\n  if (metadata) {\n    for (const [key, value] of Object.entries(metadata)) {\n      headers.set(`error-${key}`, value);\n    }\n  }\n\n  return new ConnectError(message, code, headers);\n}\n\n/**\n * Creates a \"notification not found\" error.\n */\nexport function notificationNotFoundError(\n  notificationId: string,\n): ConnectError {\n  return createError(\n    \"Notification not found\",\n    Code.NotFound,\n    ErrorReason.NOTIFICATION_NOT_FOUND,\n    { \"notification-id\": notificationId },\n  );\n}\n\n/**\n * Creates a \"notification ID required\" error.\n */\nexport function notificationIdRequiredError(): ConnectError {\n  return createError(\n    \"Notification ID is required\",\n    Code.InvalidArgument,\n    ErrorReason.NOTIFICATION_ID_REQUIRED,\n  );\n}\n\n/**\n * Creates a \"failed to create notification\" error.\n */\nexport function notificationCreateFailedError(): ConnectError {\n  return createError(\n    \"Failed to create notification\",\n    Code.Internal,\n    ErrorReason.NOTIFICATION_CREATE_FAILED,\n  );\n}\n\n/**\n * Creates a \"failed to update notification\" error.\n */\nexport function notificationUpdateFailedError(\n  notificationId: string,\n): ConnectError {\n  return createError(\n    \"Failed to update notification\",\n    Code.Internal,\n    ErrorReason.NOTIFICATION_UPDATE_FAILED,\n    { \"notification-id\": notificationId },\n  );\n}\n\n/**\n * Creates a \"notification limit reached\" error.\n */\nexport function notificationLimitReachedError(): ConnectError {\n  return createError(\n    \"You have reached your notification channel limit. Upgrade to add more.\",\n    Code.ResourceExhausted,\n    ErrorReason.NOTIFICATION_LIMIT_REACHED,\n  );\n}\n\n/**\n * Creates a \"provider not allowed\" error for limited providers.\n */\nexport function providerNotAllowedError(provider: string): ConnectError {\n  return createError(\n    `The ${provider} provider requires an upgraded plan.`,\n    Code.PermissionDenied,\n    ErrorReason.PROVIDER_NOT_ALLOWED,\n    { provider },\n  );\n}\n\n/**\n * Creates a \"provider not supported\" error.\n */\nexport function providerNotSupportedError(provider: string): ConnectError {\n  return createError(\n    `The provider ${provider} is not supported for test notifications.`,\n    Code.InvalidArgument,\n    ErrorReason.PROVIDER_NOT_SUPPORTED,\n    { provider },\n  );\n}\n\n/**\n * Creates an \"invalid notification data\" error.\n */\nexport function invalidNotificationDataError(details: string): ConnectError {\n  return createError(\n    `Invalid notification data: ${details}`,\n    Code.InvalidArgument,\n    ErrorReason.INVALID_NOTIFICATION_DATA,\n    { details },\n  );\n}\n\n/**\n * Creates a \"monitor not found\" error.\n */\nexport function monitorNotFoundError(monitorId: string): ConnectError {\n  return createError(\n    \"Monitor not found or not accessible\",\n    Code.NotFound,\n    ErrorReason.MONITOR_NOT_FOUND,\n    { \"monitor-id\": monitorId },\n  );\n}\n\n/**\n * Creates a \"test notification failed\" error.\n */\nexport function testNotificationFailedError(message: string): ConnectError {\n  return createError(\n    `Test notification failed: ${message}`,\n    Code.Internal,\n    ErrorReason.TEST_NOTIFICATION_FAILED,\n    { message },\n  );\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/notification/index.ts",
    "content": "import type { ServiceImpl } from \"@connectrpc/connect\";\nimport { and, count, db, desc, eq, inArray, sql } from \"@openstatus/db\";\nimport {\n  monitor,\n  notification,\n  notificationsToMonitors,\n} from \"@openstatus/db/src/schema\";\nimport {\n  NotificationProvider,\n  type NotificationService,\n} from \"@openstatus/proto/notification/v1\";\n\nimport { getRpcContext } from \"../../interceptors\";\nimport {\n  dbNotificationToProto,\n  dbNotificationToProtoSummary,\n  dbProviderToProto,\n  protoDataToDb,\n  protoProviderToDb,\n  validateProviderDataConsistency,\n} from \"./converters\";\nimport {\n  invalidNotificationDataError,\n  monitorNotFoundError,\n  notificationCreateFailedError,\n  notificationIdRequiredError,\n  notificationNotFoundError,\n  notificationUpdateFailedError,\n} from \"./errors\";\nimport {\n  checkNotificationLimit,\n  checkProviderAllowed,\n  getNotificationLimitInfo,\n} from \"./limits\";\nimport { sendTestNotification } from \"./test-providers\";\n\n// Type that works with both db instance and transaction\ntype DB = typeof db;\ntype Transaction = Parameters<Parameters<DB[\"transaction\"]>[0]>[0];\n\n/**\n * Helper to get a notification by ID with workspace scope.\n */\nasync function getNotificationById(id: number, workspaceId: number) {\n  return db\n    .select()\n    .from(notification)\n    .where(\n      and(eq(notification.id, id), eq(notification.workspaceId, workspaceId)),\n    )\n    .get();\n}\n\n/**\n * Helper to get monitor IDs for a notification.\n */\nasync function getMonitorIdsForNotification(\n  notificationId: number,\n): Promise<string[]> {\n  const monitors = await db\n    .select({ monitorId: notificationsToMonitors.monitorId })\n    .from(notificationsToMonitors)\n    .where(eq(notificationsToMonitors.notificationId, notificationId))\n    .all();\n\n  return monitors.map((m) => String(m.monitorId));\n}\n\n/**\n * Helper to get monitor count for a notification.\n */\nasync function getMonitorCountForNotification(\n  notificationId: number,\n): Promise<number> {\n  const result = await db\n    .select({ count: count() })\n    .from(notificationsToMonitors)\n    .where(eq(notificationsToMonitors.notificationId, notificationId))\n    .get();\n\n  return result?.count ?? 0;\n}\n\n/**\n * Validates that all monitor IDs belong to the workspace.\n * Throws monitorNotFoundError if any monitor is not found.\n */\nasync function validateMonitorIds(\n  monitorIds: string[],\n  workspaceId: number,\n  tx: DB | Transaction = db,\n): Promise<number[]> {\n  if (monitorIds.length === 0) {\n    return [];\n  }\n\n  const numericIds = monitorIds.map((id) => Number(id));\n\n  const validMonitors = await tx\n    .select({ id: monitor.id })\n    .from(monitor)\n    .where(\n      and(\n        inArray(monitor.id, numericIds),\n        eq(monitor.workspaceId, workspaceId),\n      ),\n    )\n    .all();\n\n  const validIds = new Set(validMonitors.map((m) => m.id));\n\n  for (const id of numericIds) {\n    if (!validIds.has(id)) {\n      throw monitorNotFoundError(String(id));\n    }\n  }\n\n  return numericIds;\n}\n\n/**\n * Helper to update monitor associations for a notification.\n */\nasync function updateMonitorAssociations(\n  notificationId: number,\n  monitorIds: number[],\n  tx: DB | Transaction = db,\n) {\n  // Delete existing associations\n  await tx\n    .delete(notificationsToMonitors)\n    .where(eq(notificationsToMonitors.notificationId, notificationId));\n\n  // Insert new associations\n  if (monitorIds.length > 0) {\n    await tx.insert(notificationsToMonitors).values(\n      monitorIds.map((monitorId) => ({\n        notificationId,\n        monitorId,\n      })),\n    );\n  }\n}\n\n/**\n * Notification service implementation for ConnectRPC.\n */\nexport const notificationServiceImpl: ServiceImpl<typeof NotificationService> =\n  {\n    async createNotification(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n      const limits = rpcCtx.workspace.limits;\n\n      // Check notification limit\n      await checkNotificationLimit(workspaceId, limits);\n\n      // Check if provider is allowed for this plan\n      checkProviderAllowed(req.provider, limits);\n\n      // Validate provider-data consistency\n      const validationError = validateProviderDataConsistency(\n        req.provider,\n        req.data,\n      );\n      if (validationError) {\n        throw invalidNotificationDataError(validationError);\n      }\n\n      // Create notification in a transaction\n      const newNotification = await db.transaction(async (tx) => {\n        // Validate monitor IDs\n        const validMonitorIds = await validateMonitorIds(\n          req.monitorIds,\n          workspaceId,\n          tx,\n        );\n\n        // Convert proto data to DB format\n        const dataStr = protoDataToDb(req.provider, req.data);\n\n        // Create the notification\n        const record = await tx\n          .insert(notification)\n          .values({\n            name: req.name,\n            provider: protoProviderToDb(req.provider),\n            data: dataStr,\n            workspaceId,\n          })\n          .returning()\n          .get();\n\n        if (!record) {\n          throw notificationCreateFailedError();\n        }\n\n        // Create monitor associations\n        await updateMonitorAssociations(record.id, validMonitorIds, tx);\n\n        return record;\n      });\n\n      return {\n        notification: dbNotificationToProto(newNotification, req.monitorIds),\n      };\n    },\n\n    async getNotification(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      if (!req.id || req.id.trim() === \"\") {\n        throw notificationIdRequiredError();\n      }\n\n      const record = await getNotificationById(Number(req.id), workspaceId);\n      if (!record) {\n        throw notificationNotFoundError(req.id);\n      }\n\n      const monitorIds = await getMonitorIdsForNotification(record.id);\n\n      return {\n        notification: dbNotificationToProto(record, monitorIds),\n      };\n    },\n\n    async listNotifications(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      const limit = Math.min(Math.max(req.limit ?? 50, 1), 100);\n      const offset = req.offset ?? 0;\n\n      // Get total count\n      const countResult = await db\n        .select({ count: sql<number>`count(*)` })\n        .from(notification)\n        .where(eq(notification.workspaceId, workspaceId))\n        .get();\n\n      const totalCount = countResult?.count ?? 0;\n\n      // Get notifications\n      const records = await db\n        .select()\n        .from(notification)\n        .where(eq(notification.workspaceId, workspaceId))\n        .orderBy(desc(notification.createdAt))\n        .limit(limit)\n        .offset(offset)\n        .all();\n\n      // Get monitor counts for each notification\n      const notifications = await Promise.all(\n        records.map(async (record) => {\n          const monitorCount = await getMonitorCountForNotification(record.id);\n          return dbNotificationToProtoSummary(record, monitorCount);\n        }),\n      );\n\n      return {\n        notifications,\n        totalSize: totalCount,\n      };\n    },\n\n    async updateNotification(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      if (!req.id || req.id.trim() === \"\") {\n        throw notificationIdRequiredError();\n      }\n\n      const record = await getNotificationById(Number(req.id), workspaceId);\n      if (!record) {\n        throw notificationNotFoundError(req.id);\n      }\n\n      // Validate provider-data consistency if data is being updated\n      if (req.data !== undefined) {\n        const existingProvider = dbProviderToProto(record.provider);\n        const validationError = validateProviderDataConsistency(\n          existingProvider,\n          req.data,\n        );\n        if (validationError) {\n          throw invalidNotificationDataError(validationError);\n        }\n      }\n\n      // Update notification in a transaction\n      const updatedNotification = await db.transaction(async (tx) => {\n        // Validate monitor IDs\n        const validMonitorIds = await validateMonitorIds(\n          req.monitorIds,\n          workspaceId,\n          tx,\n        );\n\n        // Build update values\n        const updateValues: Record<string, unknown> = {\n          updatedAt: new Date(),\n        };\n\n        if (req.name !== undefined && req.name !== \"\") {\n          updateValues.name = req.name;\n        }\n\n        if (req.data !== undefined) {\n          // Use the existing provider since we can't change provider on update\n          updateValues.data = protoDataToDb(\n            // Provider can't change, so we'll use the current data's case\n            req.data.data.case !== undefined\n              ? (() => {\n                  // Map case to NotificationProvider\n                  const caseToProvider: Record<string, number> = {\n                    discord: NotificationProvider.DISCORD,\n                    email: NotificationProvider.EMAIL,\n                    googleChat: NotificationProvider.GOOGLE_CHAT,\n                    grafanaOncall: NotificationProvider.GRAFANA_ONCALL,\n                    ntfy: NotificationProvider.NTFY,\n                    pagerduty: NotificationProvider.PAGERDUTY,\n                    opsgenie: NotificationProvider.OPSGENIE,\n                    slack: NotificationProvider.SLACK,\n                    sms: NotificationProvider.SMS,\n                    telegram: NotificationProvider.TELEGRAM,\n                    webhook: NotificationProvider.WEBHOOK,\n                    whatsapp: NotificationProvider.WHATSAPP,\n                  };\n                  return (\n                    caseToProvider[req.data.data.case] ??\n                    NotificationProvider.UNSPECIFIED\n                  );\n                })()\n              : 0,\n            req.data,\n          );\n        }\n\n        // Update monitor associations\n        await updateMonitorAssociations(record.id, validMonitorIds, tx);\n\n        // Update the notification\n        const updated = await tx\n          .update(notification)\n          .set(updateValues)\n          .where(eq(notification.id, record.id))\n          .returning()\n          .get();\n\n        if (!updated) {\n          throw notificationUpdateFailedError(req.id);\n        }\n\n        return updated;\n      });\n\n      // Fetch updated monitor IDs\n      const monitorIds = await getMonitorIdsForNotification(\n        updatedNotification.id,\n      );\n\n      return {\n        notification: dbNotificationToProto(updatedNotification, monitorIds),\n      };\n    },\n\n    async deleteNotification(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      if (!req.id || req.id.trim() === \"\") {\n        throw notificationIdRequiredError();\n      }\n\n      const record = await getNotificationById(Number(req.id), workspaceId);\n      if (!record) {\n        throw notificationNotFoundError(req.id);\n      }\n\n      // Delete the notification (cascade will delete associations)\n      await db.delete(notification).where(eq(notification.id, record.id));\n\n      return { success: true };\n    },\n\n    async sendTestNotification(req, _ctx) {\n      const result = await sendTestNotification(req.provider, req.data);\n      return result;\n    },\n\n    async checkNotificationLimit(_req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n      const limits = rpcCtx.workspace.limits;\n\n      const info = await getNotificationLimitInfo(workspaceId, limits);\n\n      return info;\n    },\n  };\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/notification/limits.ts",
    "content": "import { count, db, eq } from \"@openstatus/db\";\nimport { notification } from \"@openstatus/db/src/schema\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport { NotificationProvider } from \"@openstatus/proto/notification/v1\";\nimport {\n  notificationLimitReachedError,\n  providerNotAllowedError,\n} from \"./errors\";\n\n/**\n * Providers that require a paid plan.\n */\nconst LIMITED_PROVIDERS = new Set([\n  NotificationProvider.SMS,\n  NotificationProvider.PAGERDUTY,\n  NotificationProvider.OPSGENIE,\n  NotificationProvider.GRAFANA_ONCALL,\n  NotificationProvider.WHATSAPP,\n]);\n\n/**\n * Maps proto provider to the limit key in workspace limits.\n */\nfunction providerToLimitKey(\n  provider: NotificationProvider,\n): keyof Limits | null {\n  switch (provider) {\n    case NotificationProvider.SMS:\n      return \"sms\";\n    case NotificationProvider.PAGERDUTY:\n      return \"pagerduty\";\n    case NotificationProvider.OPSGENIE:\n      return \"opsgenie\";\n    case NotificationProvider.GRAFANA_ONCALL:\n      return \"grafana-oncall\";\n    case NotificationProvider.WHATSAPP:\n      return \"whatsapp\";\n    default:\n      return null;\n  }\n}\n\n/**\n * Maps proto provider to display name for error messages.\n */\nfunction providerToDisplayName(provider: NotificationProvider): string {\n  switch (provider) {\n    case NotificationProvider.DISCORD:\n      return \"Discord\";\n    case NotificationProvider.EMAIL:\n      return \"Email\";\n    case NotificationProvider.GOOGLE_CHAT:\n      return \"Google Chat\";\n    case NotificationProvider.GRAFANA_ONCALL:\n      return \"Grafana OnCall\";\n    case NotificationProvider.NTFY:\n      return \"Ntfy\";\n    case NotificationProvider.PAGERDUTY:\n      return \"PagerDuty\";\n    case NotificationProvider.OPSGENIE:\n      return \"Opsgenie\";\n    case NotificationProvider.SLACK:\n      return \"Slack\";\n    case NotificationProvider.SMS:\n      return \"SMS\";\n    case NotificationProvider.TELEGRAM:\n      return \"Telegram\";\n    case NotificationProvider.WEBHOOK:\n      return \"Webhook\";\n    case NotificationProvider.WHATSAPP:\n      return \"WhatsApp\";\n    default:\n      return \"Unknown\";\n  }\n}\n\n/**\n * Checks if the workspace has reached its notification limit.\n * Throws notificationLimitReachedError if limit is reached.\n */\nexport async function checkNotificationLimit(\n  workspaceId: number,\n  limits: Limits,\n): Promise<void> {\n  const maxCount = limits[\"notification-channels\"];\n\n  const result = await db\n    .select({ count: count() })\n    .from(notification)\n    .where(eq(notification.workspaceId, workspaceId))\n    .get();\n\n  const currentCount = result?.count ?? 0;\n\n  if (currentCount >= maxCount) {\n    throw notificationLimitReachedError();\n  }\n}\n\n/**\n * Checks if the provider is allowed for the workspace's plan.\n * Throws providerNotAllowedError if not allowed.\n */\nexport function checkProviderAllowed(\n  provider: NotificationProvider,\n  limits: Limits,\n): void {\n  if (!LIMITED_PROVIDERS.has(provider)) {\n    return; // Provider is free for all plans\n  }\n\n  const limitKey = providerToLimitKey(provider);\n  if (!limitKey) {\n    return; // No limit key means it's free\n  }\n\n  const isAllowed = limits[limitKey];\n  if (!isAllowed) {\n    throw providerNotAllowedError(providerToDisplayName(provider));\n  }\n}\n\n/**\n * Gets the current notification count and limit for a workspace.\n */\nexport async function getNotificationLimitInfo(\n  workspaceId: number,\n  limits: Limits,\n): Promise<{ currentCount: number; maxCount: number; limitReached: boolean }> {\n  const maxCount = limits[\"notification-channels\"];\n\n  const result = await db\n    .select({ count: count() })\n    .from(notification)\n    .where(eq(notification.workspaceId, workspaceId))\n    .get();\n\n  const currentCount = result?.count ?? 0;\n\n  return {\n    currentCount,\n    maxCount,\n    limitReached: currentCount >= maxCount,\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/notification/test-providers.ts",
    "content": "import { sendTestDiscordMessage } from \"@openstatus/notification-discord\";\nimport { sendTest as sendGoogleChatTest } from \"@openstatus/notification-google-chat\";\nimport { sendTest as sendGrafanaTest } from \"@openstatus/notification-grafana-oncall\";\nimport { sendTest as sendNtfyTest } from \"@openstatus/notification-ntfy\";\nimport { sendTest as sendOpsgenieTest } from \"@openstatus/notification-opsgenie\";\nimport { sendTest as sendPagerDutyTest } from \"@openstatus/notification-pagerduty\";\nimport { sendTestSlackMessage } from \"@openstatus/notification-slack\";\nimport { sendTest as sendTelegramTest } from \"@openstatus/notification-telegram\";\nimport { sendTest as sendWhatsAppTest } from \"@openstatus/notification-twillio-whatsapp\";\nimport { sendTest as sendWebhookTest } from \"@openstatus/notification-webhook\";\nimport {\n  type NotificationData,\n  NotificationProvider,\n  OpsgenieRegion,\n} from \"@openstatus/proto/notification/v1\";\nimport {\n  invalidNotificationDataError,\n  providerNotSupportedError,\n  testNotificationFailedError,\n} from \"./errors\";\n\n/**\n * Sends a test notification using the specified provider and data.\n * Throws an error if the provider is not supported or if sending fails.\n */\nexport async function sendTestNotification(\n  provider: NotificationProvider,\n  data: NotificationData | undefined,\n): Promise<{ success: boolean; errorMessage?: string }> {\n  if (!data || data.data.case === undefined) {\n    throw invalidNotificationDataError(\"No provider data specified\");\n  }\n\n  try {\n    switch (provider) {\n      case NotificationProvider.TELEGRAM: {\n        if (data.data.case !== \"telegram\") {\n          throw invalidNotificationDataError(\n            \"Expected telegram data for Telegram provider\",\n          );\n        }\n        await sendTelegramTest({ chatId: data.data.value.chatId });\n        return { success: true };\n      }\n\n      case NotificationProvider.WHATSAPP: {\n        if (data.data.case !== \"whatsapp\") {\n          throw invalidNotificationDataError(\n            \"Expected whatsapp data for WhatsApp provider\",\n          );\n        }\n        await sendWhatsAppTest({ phoneNumber: data.data.value.phoneNumber });\n        return { success: true };\n      }\n\n      case NotificationProvider.GOOGLE_CHAT: {\n        if (data.data.case !== \"googleChat\") {\n          throw invalidNotificationDataError(\n            \"Expected google_chat data for Google Chat provider\",\n          );\n        }\n        await sendGoogleChatTest(data.data.value.webhookUrl);\n        return { success: true };\n      }\n\n      case NotificationProvider.GRAFANA_ONCALL: {\n        if (data.data.case !== \"grafanaOncall\") {\n          throw invalidNotificationDataError(\n            \"Expected grafana_oncall data for Grafana OnCall provider\",\n          );\n        }\n        await sendGrafanaTest({ webhookUrl: data.data.value.webhookUrl });\n        return { success: true };\n      }\n\n      case NotificationProvider.DISCORD: {\n        if (data.data.case !== \"discord\") {\n          throw invalidNotificationDataError(\n            \"Expected discord data for Discord provider\",\n          );\n        }\n        await sendTestDiscordMessage(data.data.value.webhookUrl);\n        return { success: true };\n      }\n\n      case NotificationProvider.SLACK: {\n        if (data.data.case !== \"slack\") {\n          throw invalidNotificationDataError(\n            \"Expected slack data for Slack provider\",\n          );\n        }\n        await sendTestSlackMessage(data.data.value.webhookUrl);\n        return { success: true };\n      }\n\n      case NotificationProvider.NTFY: {\n        if (data.data.case !== \"ntfy\") {\n          throw invalidNotificationDataError(\n            \"Expected ntfy data for Ntfy provider\",\n          );\n        }\n        await sendNtfyTest({\n          topic: data.data.value.topic,\n          serverUrl: data.data.value.serverUrl || undefined,\n          token: data.data.value.token,\n        });\n        return { success: true };\n      }\n\n      case NotificationProvider.PAGERDUTY: {\n        if (data.data.case !== \"pagerduty\") {\n          throw invalidNotificationDataError(\n            \"Expected pagerduty data for PagerDuty provider\",\n          );\n        }\n        await sendPagerDutyTest({\n          integrationKey: data.data.value.integrationKey,\n        });\n        return { success: true };\n      }\n\n      case NotificationProvider.OPSGENIE: {\n        if (data.data.case !== \"opsgenie\") {\n          throw invalidNotificationDataError(\n            \"Expected opsgenie data for Opsgenie provider\",\n          );\n        }\n        await sendOpsgenieTest({\n          apiKey: data.data.value.apiKey,\n          region: data.data.value.region === OpsgenieRegion.EU ? \"eu\" : \"us\",\n        });\n        return { success: true };\n      }\n\n      case NotificationProvider.WEBHOOK: {\n        if (data.data.case !== \"webhook\") {\n          throw invalidNotificationDataError(\n            \"Expected webhook data for Webhook provider\",\n          );\n        }\n        await sendWebhookTest({\n          url: data.data.value.endpoint,\n          headers: data.data.value.headers?.map((h) => ({\n            key: h.key,\n            value: h.value,\n          })),\n        });\n        return { success: true };\n      }\n\n      // Providers that don't support test notifications yet\n      case NotificationProvider.EMAIL:\n      case NotificationProvider.SMS:\n        throw providerNotSupportedError(NotificationProvider[provider]);\n\n      default:\n        throw providerNotSupportedError(\"Unknown\");\n    }\n  } catch (error) {\n    if (error instanceof Error && \"code\" in error) {\n      // Re-throw ConnectErrors as-is\n      throw error;\n    }\n    // Wrap other errors\n    const message = error instanceof Error ? error.message : \"Unknown error\";\n    throw testNotificationFailedError(message);\n  }\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/status-page/__tests__/status-page.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  monitor,\n  page,\n  pageComponent,\n  pageComponentGroup,\n  pageSubscriber,\n  statusReport,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\n\nimport { app } from \"@/index\";\n\n/**\n * Helper to make ConnectRPC requests using the Connect protocol (JSON).\n * Connect uses POST with JSON body at /rpc/<service>/<method>\n */\nasync function connectRequest(\n  method: string,\n  body: Record<string, unknown> = {},\n  headers: Record<string, string> = {},\n) {\n  return app.request(\n    `/rpc/openstatus.status_page.v1.StatusPageService/${method}`,\n    {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        ...headers,\n      },\n      body: JSON.stringify(body),\n    },\n  );\n}\n\nconst TEST_PREFIX = \"rpc-status-page-test\";\nlet testPageId: number;\nlet testPageSlug: string;\nlet testPageToDeleteId: number;\nlet testPageToUpdateId: number;\nlet testComponentId: number;\nlet testComponentToDeleteId: number;\nlet testComponentToUpdateId: number;\nlet testGroupId: number;\nlet testGroupToDeleteId: number;\nlet testGroupToUpdateId: number;\nlet testMonitorId: number;\nlet testSubscriberId: number;\n\nbeforeAll(async () => {\n  // Clean up any existing test data\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, `${TEST_PREFIX}@example.com`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component-to-delete`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component-to-update`));\n  await db\n    .delete(pageComponentGroup)\n    .where(eq(pageComponentGroup.name, `${TEST_PREFIX}-group`));\n  await db\n    .delete(pageComponentGroup)\n    .where(eq(pageComponentGroup.name, `${TEST_PREFIX}-group-to-delete`));\n  await db\n    .delete(pageComponentGroup)\n    .where(eq(pageComponentGroup.name, `${TEST_PREFIX}-group-to-update`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug-to-delete`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug-to-update`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n\n  // Create a test monitor for component tests\n  const testMonitor = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-monitor`,\n      url: \"https://example.com\",\n      periodicity: \"1m\",\n      active: true,\n      jobType: \"http\",\n    })\n    .returning()\n    .get();\n  testMonitorId = testMonitor.id;\n\n  // Create a test page (published and public for testing public access)\n  const testPage = await db\n    .insert(page)\n    .values({\n      workspaceId: 1,\n      title: `${TEST_PREFIX}-page`,\n      slug: `${TEST_PREFIX}-slug`,\n      description: \"Test page for status page tests\",\n      customDomain: \"\",\n      published: true,\n      accessType: \"public\",\n    })\n    .returning()\n    .get();\n  testPageId = testPage.id;\n  testPageSlug = testPage.slug;\n\n  // Create page to delete\n  const pageToDelete = await db\n    .insert(page)\n    .values({\n      workspaceId: 1,\n      title: `${TEST_PREFIX}-page-to-delete`,\n      slug: `${TEST_PREFIX}-slug-to-delete`,\n      description: \"Test page to delete\",\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n  testPageToDeleteId = pageToDelete.id;\n\n  // Create page to update\n  const pageToUpdate = await db\n    .insert(page)\n    .values({\n      workspaceId: 1,\n      title: `${TEST_PREFIX}-page-to-update`,\n      slug: `${TEST_PREFIX}-slug-to-update`,\n      description: \"Test page to update\",\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n  testPageToUpdateId = pageToUpdate.id;\n\n  // Create a test component group\n  const testGroup = await db\n    .insert(pageComponentGroup)\n    .values({\n      workspaceId: 1,\n      pageId: testPageId,\n      name: `${TEST_PREFIX}-group`,\n    })\n    .returning()\n    .get();\n  testGroupId = testGroup.id;\n\n  // Create group to delete\n  const groupToDelete = await db\n    .insert(pageComponentGroup)\n    .values({\n      workspaceId: 1,\n      pageId: testPageId,\n      name: `${TEST_PREFIX}-group-to-delete`,\n    })\n    .returning()\n    .get();\n  testGroupToDeleteId = groupToDelete.id;\n\n  // Create group to update\n  const groupToUpdate = await db\n    .insert(pageComponentGroup)\n    .values({\n      workspaceId: 1,\n      pageId: testPageId,\n      name: `${TEST_PREFIX}-group-to-update`,\n    })\n    .returning()\n    .get();\n  testGroupToUpdateId = groupToUpdate.id;\n\n  // Create a test component\n  const testComponent = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 1,\n      pageId: testPageId,\n      type: \"static\",\n      name: `${TEST_PREFIX}-component`,\n      description: \"Test component\",\n      order: 100,\n    })\n    .returning()\n    .get();\n  testComponentId = testComponent.id;\n\n  // Create component to delete\n  const componentToDelete = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 1,\n      pageId: testPageId,\n      type: \"static\",\n      name: `${TEST_PREFIX}-component-to-delete`,\n      description: \"Test component to delete\",\n      order: 101,\n    })\n    .returning()\n    .get();\n  testComponentToDeleteId = componentToDelete.id;\n\n  // Create component to update\n  const componentToUpdate = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 1,\n      pageId: testPageId,\n      type: \"static\",\n      name: `${TEST_PREFIX}-component-to-update`,\n      description: \"Test component to update\",\n      order: 102,\n    })\n    .returning()\n    .get();\n  testComponentToUpdateId = componentToUpdate.id;\n\n  // Create a test subscriber\n  const testSubscriber = await db\n    .insert(pageSubscriber)\n    .values({\n      pageId: testPageId,\n      email: `${TEST_PREFIX}@example.com`,\n      token: `${TEST_PREFIX}-token`,\n      acceptedAt: new Date(),\n    })\n    .returning()\n    .get();\n  testSubscriberId = testSubscriber.id;\n});\n\nafterAll(async () => {\n  // Clean up test data in proper order\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, `${TEST_PREFIX}@example.com`));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, `${TEST_PREFIX}-subscribe@example.com`));\n\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component-to-delete`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component-to-update`));\n\n  await db\n    .delete(pageComponentGroup)\n    .where(eq(pageComponentGroup.name, `${TEST_PREFIX}-group`));\n  await db\n    .delete(pageComponentGroup)\n    .where(eq(pageComponentGroup.name, `${TEST_PREFIX}-group-to-delete`));\n  await db\n    .delete(pageComponentGroup)\n    .where(eq(pageComponentGroup.name, `${TEST_PREFIX}-group-to-update`));\n\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug-to-delete`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug-to-update`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-created-slug`));\n\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n});\n\n// ==========================================================================\n// Page CRUD\n// ==========================================================================\n\ndescribe(\"StatusPageService.CreateStatusPage\", () => {\n  test(\"creates a new status page\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusPage\",\n      {\n        title: `${TEST_PREFIX}-created`,\n        description: \"A new test page\",\n        slug: `${TEST_PREFIX}-created-slug`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusPage\");\n    expect(data.statusPage.title).toBe(`${TEST_PREFIX}-created`);\n    expect(data.statusPage.description).toBe(\"A new test page\");\n    expect(data.statusPage.slug).toBe(`${TEST_PREFIX}-created-slug`);\n\n    // Clean up\n    await db.delete(page).where(eq(page.id, Number(data.statusPage.id)));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"CreateStatusPage\", {\n      title: \"Unauthorized test\",\n      slug: \"unauthorized-slug\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns error when slug already exists\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusPage\",\n      {\n        title: \"Duplicate slug test\",\n        slug: testPageSlug, // Already exists\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(409); // AlreadyExists\n  });\n\n  test(\"returns 403 when status page limit is exceeded\", async () => {\n    // Workspace 2 is on free plan with status-pages limit of 1\n    // First, create a page for workspace 2 to hit the limit\n    const firstPage = await db\n      .insert(page)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-limit-test`,\n        slug: `${TEST_PREFIX}-limit-test-slug`,\n        description: \"First page for limit test\",\n        customDomain: \"\",\n      })\n      .returning()\n      .get();\n\n    try {\n      // Try to create a second page - should fail with PermissionDenied\n      const res = await connectRequest(\n        \"CreateStatusPage\",\n        {\n          title: `${TEST_PREFIX}-limit-exceeded`,\n          description: \"Should fail due to limit\",\n          slug: `${TEST_PREFIX}-limit-exceeded-slug`,\n        },\n        { \"x-openstatus-key\": \"2\" },\n      );\n\n      expect(res.status).toBe(403); // PermissionDenied\n\n      const data = await res.json();\n      expect(data.message).toContain(\"Upgrade for more status pages\");\n    } finally {\n      // Clean up\n      await db.delete(page).where(eq(page.id, firstPage.id));\n    }\n  });\n});\n\ndescribe(\"StatusPageService.GetStatusPage\", () => {\n  test(\"returns status page by ID\", async () => {\n    const res = await connectRequest(\n      \"GetStatusPage\",\n      { id: String(testPageId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusPage\");\n    expect(data.statusPage.id).toBe(String(testPageId));\n    expect(data.statusPage.slug).toBe(testPageSlug);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"GetStatusPage\", {\n      id: String(testPageId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent status page\", async () => {\n    const res = await connectRequest(\n      \"GetStatusPage\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when ID is empty\", async () => {\n    const res = await connectRequest(\n      \"GetStatusPage\",\n      { id: \"\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 404 for status page in different workspace\", async () => {\n    // Create page in workspace 2\n    const otherPage = await db\n      .insert(page)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace`,\n        slug: `${TEST_PREFIX}-other-workspace-slug`,\n        description: \"Other workspace page\",\n        customDomain: \"\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"GetStatusPage\",\n        { id: String(otherPage.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(page).where(eq(page.id, otherPage.id));\n    }\n  });\n});\n\ndescribe(\"StatusPageService.ListStatusPages\", () => {\n  test(\"returns status pages for authenticated workspace\", async () => {\n    const res = await connectRequest(\n      \"ListStatusPages\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusPages\");\n    expect(Array.isArray(data.statusPages)).toBe(true);\n    expect(data).toHaveProperty(\"totalSize\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"ListStatusPages\", {});\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"respects limit parameter\", async () => {\n    const res = await connectRequest(\n      \"ListStatusPages\",\n      { limit: 1 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.statusPages?.length || 0).toBeLessThanOrEqual(1);\n  });\n\n  test(\"respects offset parameter\", async () => {\n    // Get first page\n    const res1 = await connectRequest(\n      \"ListStatusPages\",\n      { limit: 1, offset: 0 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n    const data1 = await res1.json();\n\n    // Get second page\n    const res2 = await connectRequest(\n      \"ListStatusPages\",\n      { limit: 1, offset: 1 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n    const data2 = await res2.json();\n\n    // Should have different pages if multiple exist\n    if (data1.statusPages?.length > 0 && data2.statusPages?.length > 0) {\n      expect(data1.statusPages[0].id).not.toBe(data2.statusPages[0].id);\n    }\n  });\n});\n\ndescribe(\"StatusPageService.UpdateStatusPage\", () => {\n  test(\"updates status page title\", async () => {\n    const res = await connectRequest(\n      \"UpdateStatusPage\",\n      {\n        id: String(testPageToUpdateId),\n        title: `${TEST_PREFIX}-updated-title`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusPage\");\n    expect(data.statusPage.title).toBe(`${TEST_PREFIX}-updated-title`);\n\n    // Restore original title\n    await db\n      .update(page)\n      .set({ title: `${TEST_PREFIX}-page-to-update` })\n      .where(eq(page.id, testPageToUpdateId));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UpdateStatusPage\", {\n      id: String(testPageToUpdateId),\n      title: \"Unauthorized update\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent status page\", async () => {\n    const res = await connectRequest(\n      \"UpdateStatusPage\",\n      { id: \"99999\", title: \"Non-existent update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when slug conflicts with another page\", async () => {\n    const res = await connectRequest(\n      \"UpdateStatusPage\",\n      {\n        id: String(testPageToUpdateId),\n        slug: testPageSlug, // Already exists on another page\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(409);\n  });\n});\n\ndescribe(\"StatusPageService.DeleteStatusPage\", () => {\n  test(\"successfully deletes existing status page\", async () => {\n    const res = await connectRequest(\n      \"DeleteStatusPage\",\n      { id: String(testPageToDeleteId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.success).toBe(true);\n\n    // Verify it's deleted\n    const deleted = await db\n      .select()\n      .from(page)\n      .where(eq(page.id, testPageToDeleteId))\n      .get();\n    expect(deleted).toBeUndefined();\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"DeleteStatusPage\", { id: \"1\" });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent status page\", async () => {\n    const res = await connectRequest(\n      \"DeleteStatusPage\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n});\n\n// ==========================================================================\n// Component Management\n// ==========================================================================\n\ndescribe(\"StatusPageService.AddMonitorComponent\", () => {\n  test(\"adds monitor component to page\", async () => {\n    const res = await connectRequest(\n      \"AddMonitorComponent\",\n      {\n        pageId: String(testPageId),\n        monitorId: String(testMonitorId),\n        name: `${TEST_PREFIX}-monitor-component`,\n        order: 200,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"component\");\n    expect(data.component.name).toBe(`${TEST_PREFIX}-monitor-component`);\n    expect(data.component.type).toBe(\"PAGE_COMPONENT_TYPE_MONITOR\");\n    expect(data.component.monitorId).toBe(String(testMonitorId));\n\n    // Clean up\n    await db\n      .delete(pageComponent)\n      .where(eq(pageComponent.id, Number(data.component.id)));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"AddMonitorComponent\", {\n      pageId: String(testPageId),\n      monitorId: String(testMonitorId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent page\", async () => {\n    const res = await connectRequest(\n      \"AddMonitorComponent\",\n      {\n        pageId: \"99999\",\n        monitorId: String(testMonitorId),\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 404 for non-existent monitor\", async () => {\n    const res = await connectRequest(\n      \"AddMonitorComponent\",\n      {\n        pageId: String(testPageId),\n        monitorId: \"99999\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"adds component with group\", async () => {\n    const res = await connectRequest(\n      \"AddMonitorComponent\",\n      {\n        pageId: String(testPageId),\n        monitorId: String(testMonitorId),\n        name: `${TEST_PREFIX}-monitor-component-grouped`,\n        groupId: String(testGroupId),\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.component.groupId).toBe(String(testGroupId));\n\n    // Clean up\n    await db\n      .delete(pageComponent)\n      .where(eq(pageComponent.id, Number(data.component.id)));\n  });\n\n  test(\"returns 403 when page component limit is exceeded\", async () => {\n    // Workspace 2 is on free plan with page-components limit of 3\n    // Create a page for workspace 2\n    const limitTestPage = await db\n      .insert(page)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-component-limit-test`,\n        slug: `${TEST_PREFIX}-component-limit-test-slug`,\n        description: \"Page for component limit test\",\n        customDomain: \"\",\n      })\n      .returning()\n      .get();\n\n    // Create a monitor for workspace 2\n    const limitTestMonitor = await db\n      .insert(monitor)\n      .values({\n        workspaceId: 2,\n        name: `${TEST_PREFIX}-limit-monitor`,\n        url: \"https://example.com\",\n        periodicity: \"1m\",\n        active: true,\n        jobType: \"http\",\n      })\n      .returning()\n      .get();\n\n    // Create 3 components to hit the limit\n    const createdComponentIds: number[] = [];\n    for (let i = 0; i < 3; i++) {\n      const component = await db\n        .insert(pageComponent)\n        .values({\n          workspaceId: 2,\n          pageId: limitTestPage.id,\n          type: \"static\",\n          name: `${TEST_PREFIX}-limit-component-${i}`,\n          order: i,\n        })\n        .returning()\n        .get();\n      createdComponentIds.push(component.id);\n    }\n\n    try {\n      // Try to add a 4th component - should fail with PermissionDenied\n      const res = await connectRequest(\n        \"AddMonitorComponent\",\n        {\n          pageId: String(limitTestPage.id),\n          monitorId: String(limitTestMonitor.id),\n          name: `${TEST_PREFIX}-limit-exceeded-component`,\n        },\n        { \"x-openstatus-key\": \"2\" },\n      );\n\n      expect(res.status).toBe(403); // PermissionDenied\n\n      const data = await res.json();\n      expect(data.message).toContain(\"Upgrade for more page components\");\n    } finally {\n      // Clean up\n      for (const id of createdComponentIds) {\n        await db.delete(pageComponent).where(eq(pageComponent.id, id));\n      }\n      await db.delete(monitor).where(eq(monitor.id, limitTestMonitor.id));\n      await db.delete(page).where(eq(page.id, limitTestPage.id));\n    }\n  });\n});\n\ndescribe(\"StatusPageService.AddStaticComponent\", () => {\n  test(\"adds static component to page\", async () => {\n    const res = await connectRequest(\n      \"AddStaticComponent\",\n      {\n        pageId: String(testPageId),\n        name: `${TEST_PREFIX}-static-component`,\n        description: \"Static service\",\n        order: 300,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"component\");\n    expect(data.component.name).toBe(`${TEST_PREFIX}-static-component`);\n    expect(data.component.type).toBe(\"PAGE_COMPONENT_TYPE_STATIC\");\n    expect(data.component.description).toBe(\"Static service\");\n\n    // Clean up\n    await db\n      .delete(pageComponent)\n      .where(eq(pageComponent.id, Number(data.component.id)));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"AddStaticComponent\", {\n      pageId: String(testPageId),\n      name: \"Unauthorized component\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent page\", async () => {\n    const res = await connectRequest(\n      \"AddStaticComponent\",\n      {\n        pageId: \"99999\",\n        name: \"Component for non-existent page\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 403 when page component limit is exceeded\", async () => {\n    // Workspace 2 is on free plan with page-components limit of 3\n    // Create a page for workspace 2\n    const limitTestPage = await db\n      .insert(page)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-static-limit-test`,\n        slug: `${TEST_PREFIX}-static-limit-test-slug`,\n        description: \"Page for static component limit test\",\n        customDomain: \"\",\n      })\n      .returning()\n      .get();\n\n    // Create 3 components to hit the limit\n    const createdComponentIds: number[] = [];\n    for (let i = 0; i < 3; i++) {\n      const component = await db\n        .insert(pageComponent)\n        .values({\n          workspaceId: 2,\n          pageId: limitTestPage.id,\n          type: \"static\",\n          name: `${TEST_PREFIX}-static-limit-component-${i}`,\n          order: i,\n        })\n        .returning()\n        .get();\n      createdComponentIds.push(component.id);\n    }\n\n    try {\n      // Try to add a 4th component - should fail with PermissionDenied\n      const res = await connectRequest(\n        \"AddStaticComponent\",\n        {\n          pageId: String(limitTestPage.id),\n          name: `${TEST_PREFIX}-static-limit-exceeded`,\n          description: \"Should fail due to limit\",\n        },\n        { \"x-openstatus-key\": \"2\" },\n      );\n\n      expect(res.status).toBe(403); // PermissionDenied\n\n      const data = await res.json();\n      expect(data.message).toContain(\"Upgrade for more page components\");\n    } finally {\n      // Clean up\n      for (const id of createdComponentIds) {\n        await db.delete(pageComponent).where(eq(pageComponent.id, id));\n      }\n      await db.delete(page).where(eq(page.id, limitTestPage.id));\n    }\n  });\n});\n\ndescribe(\"StatusPageService.RemoveComponent\", () => {\n  test(\"successfully removes component\", async () => {\n    const res = await connectRequest(\n      \"RemoveComponent\",\n      { id: String(testComponentToDeleteId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.success).toBe(true);\n\n    // Verify it's deleted\n    const deleted = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.id, testComponentToDeleteId))\n      .get();\n    expect(deleted).toBeUndefined();\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"RemoveComponent\", { id: \"1\" });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent component\", async () => {\n    const res = await connectRequest(\n      \"RemoveComponent\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n});\n\ndescribe(\"StatusPageService.UpdateComponent\", () => {\n  test(\"updates component name\", async () => {\n    const res = await connectRequest(\n      \"UpdateComponent\",\n      {\n        id: String(testComponentToUpdateId),\n        name: `${TEST_PREFIX}-component-updated`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"component\");\n    expect(data.component.name).toBe(`${TEST_PREFIX}-component-updated`);\n\n    // Restore original name\n    await db\n      .update(pageComponent)\n      .set({ name: `${TEST_PREFIX}-component-to-update` })\n      .where(eq(pageComponent.id, testComponentToUpdateId));\n  });\n\n  test(\"updates component group\", async () => {\n    const res = await connectRequest(\n      \"UpdateComponent\",\n      {\n        id: String(testComponentToUpdateId),\n        groupId: String(testGroupId),\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.component.groupId).toBe(String(testGroupId));\n\n    // Remove from group\n    await db\n      .update(pageComponent)\n      .set({ groupId: null })\n      .where(eq(pageComponent.id, testComponentToUpdateId));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UpdateComponent\", {\n      id: String(testComponentToUpdateId),\n      name: \"Unauthorized update\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent component\", async () => {\n    const res = await connectRequest(\n      \"UpdateComponent\",\n      { id: \"99999\", name: \"Non-existent update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 404 for non-existent group\", async () => {\n    const res = await connectRequest(\n      \"UpdateComponent\",\n      {\n        id: String(testComponentToUpdateId),\n        groupId: \"99999\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n});\n\n// ==========================================================================\n// Component Groups\n// ==========================================================================\n\ndescribe(\"StatusPageService.CreateComponentGroup\", () => {\n  test(\"creates a new component group\", async () => {\n    const res = await connectRequest(\n      \"CreateComponentGroup\",\n      {\n        pageId: String(testPageId),\n        name: `${TEST_PREFIX}-new-group`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"group\");\n    expect(data.group.name).toBe(`${TEST_PREFIX}-new-group`);\n    expect(data.group.pageId).toBe(String(testPageId));\n\n    // Clean up\n    await db\n      .delete(pageComponentGroup)\n      .where(eq(pageComponentGroup.id, Number(data.group.id)));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"CreateComponentGroup\", {\n      pageId: String(testPageId),\n      name: \"Unauthorized group\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent page\", async () => {\n    const res = await connectRequest(\n      \"CreateComponentGroup\",\n      {\n        pageId: \"99999\",\n        name: \"Group for non-existent page\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n});\n\ndescribe(\"StatusPageService.DeleteComponentGroup\", () => {\n  test(\"successfully deletes component group\", async () => {\n    const res = await connectRequest(\n      \"DeleteComponentGroup\",\n      { id: String(testGroupToDeleteId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.success).toBe(true);\n\n    // Verify it's deleted\n    const deleted = await db\n      .select()\n      .from(pageComponentGroup)\n      .where(eq(pageComponentGroup.id, testGroupToDeleteId))\n      .get();\n    expect(deleted).toBeUndefined();\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"DeleteComponentGroup\", { id: \"1\" });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent group\", async () => {\n    const res = await connectRequest(\n      \"DeleteComponentGroup\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n});\n\ndescribe(\"StatusPageService.UpdateComponentGroup\", () => {\n  test(\"updates component group name\", async () => {\n    const res = await connectRequest(\n      \"UpdateComponentGroup\",\n      {\n        id: String(testGroupToUpdateId),\n        name: `${TEST_PREFIX}-group-updated`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"group\");\n    expect(data.group.name).toBe(`${TEST_PREFIX}-group-updated`);\n\n    // Restore original name\n    await db\n      .update(pageComponentGroup)\n      .set({ name: `${TEST_PREFIX}-group-to-update` })\n      .where(eq(pageComponentGroup.id, testGroupToUpdateId));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UpdateComponentGroup\", {\n      id: String(testGroupToUpdateId),\n      name: \"Unauthorized update\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent group\", async () => {\n    const res = await connectRequest(\n      \"UpdateComponentGroup\",\n      { id: \"99999\", name: \"Non-existent update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n});\n\n// ==========================================================================\n// Subscribers\n// ==========================================================================\n\ndescribe(\"StatusPageService.SubscribeToPage\", () => {\n  test(\"subscribes new user to page\", async () => {\n    const res = await connectRequest(\n      \"SubscribeToPage\",\n      {\n        pageId: String(testPageId),\n        email: `${TEST_PREFIX}-subscribe@example.com`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"subscriber\");\n    expect(data.subscriber.email).toBe(`${TEST_PREFIX}-subscribe@example.com`);\n    expect(data.subscriber.pageId).toBe(String(testPageId));\n\n    // Clean up\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.id, Number(data.subscriber.id)));\n  });\n\n  test(\"returns existing subscriber when already subscribed\", async () => {\n    const res = await connectRequest(\n      \"SubscribeToPage\",\n      {\n        pageId: String(testPageId),\n        email: `${TEST_PREFIX}@example.com`, // Already exists\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"subscriber\");\n    expect(data.subscriber.id).toBe(String(testSubscriberId));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"SubscribeToPage\", {\n      pageId: String(testPageId),\n      email: \"unauthorized@example.com\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent page\", async () => {\n    const res = await connectRequest(\n      \"SubscribeToPage\",\n      {\n        pageId: \"99999\",\n        email: \"test@example.com\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n});\n\ndescribe(\"StatusPageService.UnsubscribeFromPage\", () => {\n  test(\"unsubscribes by email\", async () => {\n    // First subscribe a new user\n    const subscribeRes = await connectRequest(\n      \"SubscribeToPage\",\n      {\n        pageId: String(testPageId),\n        email: `${TEST_PREFIX}-unsub-email@example.com`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n    const subscribeData = await subscribeRes.json();\n\n    // Then unsubscribe\n    const res = await connectRequest(\n      \"UnsubscribeFromPage\",\n      {\n        pageId: String(testPageId),\n        email: `${TEST_PREFIX}-unsub-email@example.com`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.success).toBe(true);\n\n    // Verify unsubscribedAt is set\n    const subscriber = await db\n      .select()\n      .from(pageSubscriber)\n      .where(eq(pageSubscriber.id, Number(subscribeData.subscriber.id)))\n      .get();\n    expect(subscriber?.unsubscribedAt).not.toBeNull();\n\n    // Clean up\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.id, Number(subscribeData.subscriber.id)));\n  });\n\n  test(\"unsubscribes by id\", async () => {\n    // First subscribe a new user\n    const subscribeRes = await connectRequest(\n      \"SubscribeToPage\",\n      {\n        pageId: String(testPageId),\n        email: `${TEST_PREFIX}-unsub-id@example.com`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n    const subscribeData = await subscribeRes.json();\n\n    // Then unsubscribe by id\n    const res = await connectRequest(\n      \"UnsubscribeFromPage\",\n      {\n        pageId: String(testPageId),\n        id: subscribeData.subscriber.id,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.success).toBe(true);\n\n    // Clean up\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.id, Number(subscribeData.subscriber.id)));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UnsubscribeFromPage\", {\n      pageId: String(testPageId),\n      email: \"test@example.com\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent subscriber\", async () => {\n    const res = await connectRequest(\n      \"UnsubscribeFromPage\",\n      {\n        pageId: String(testPageId),\n        email: \"nonexistent@example.com\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when no identifier provided\", async () => {\n    const res = await connectRequest(\n      \"UnsubscribeFromPage\",\n      {\n        pageId: String(testPageId),\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n});\n\ndescribe(\"StatusPageService.ListSubscribers\", () => {\n  test(\"returns subscribers for page\", async () => {\n    const res = await connectRequest(\n      \"ListSubscribers\",\n      { pageId: String(testPageId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"subscribers\");\n    expect(Array.isArray(data.subscribers)).toBe(true);\n    expect(data).toHaveProperty(\"totalSize\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"ListSubscribers\", {\n      pageId: String(testPageId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent page\", async () => {\n    const res = await connectRequest(\n      \"ListSubscribers\",\n      { pageId: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"filters out unsubscribed by default\", async () => {\n    // Create an unsubscribed subscriber\n    const unsubscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: `${TEST_PREFIX}-unsubbed@example.com`,\n        token: `${TEST_PREFIX}-unsubbed-token`,\n        unsubscribedAt: new Date(),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"ListSubscribers\",\n        { pageId: String(testPageId) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      const subscriberEmails = (data.subscribers || []).map(\n        (s: { email: string }) => s.email,\n      );\n      expect(subscriberEmails).not.toContain(\n        `${TEST_PREFIX}-unsubbed@example.com`,\n      );\n    } finally {\n      await db\n        .delete(pageSubscriber)\n        .where(eq(pageSubscriber.id, unsubscriber.id));\n    }\n  });\n\n  test(\"includes unsubscribed when flag is true\", async () => {\n    // Create an unsubscribed subscriber\n    const unsubscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: `${TEST_PREFIX}-unsubbed2@example.com`,\n        token: `${TEST_PREFIX}-unsubbed2-token`,\n        unsubscribedAt: new Date(),\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"ListSubscribers\",\n        { pageId: String(testPageId), includeUnsubscribed: true },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      const subscriberEmails = (data.subscribers || []).map(\n        (s: { email: string }) => s.email,\n      );\n      expect(subscriberEmails).toContain(\n        `${TEST_PREFIX}-unsubbed2@example.com`,\n      );\n    } finally {\n      await db\n        .delete(pageSubscriber)\n        .where(eq(pageSubscriber.id, unsubscriber.id));\n    }\n  });\n});\n\n// ==========================================================================\n// Full Content & Status\n// ==========================================================================\n\ndescribe(\"StatusPageService.GetStatusPageContent\", () => {\n  test(\"returns full content by ID\", async () => {\n    const res = await connectRequest(\n      \"GetStatusPageContent\",\n      { id: String(testPageId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusPage\");\n    expect(data).toHaveProperty(\"components\");\n    expect(data).toHaveProperty(\"groups\");\n    // statusReports may be undefined/empty if there are no active reports\n    expect(data.statusPage.id).toBe(String(testPageId));\n  });\n\n  test(\"returns full content by slug\", async () => {\n    const res = await connectRequest(\n      \"GetStatusPageContent\",\n      { slug: testPageSlug },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusPage\");\n    expect(data.statusPage.slug).toBe(testPageSlug);\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"GetStatusPageContent\", {\n      id: String(testPageId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent page\", async () => {\n    const res = await connectRequest(\n      \"GetStatusPageContent\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 404 for unpublished page accessed by slug\", async () => {\n    // Create an unpublished page\n    const unpublishedPage = await db\n      .insert(page)\n      .values({\n        workspaceId: 1,\n        title: `${TEST_PREFIX}-unpublished`,\n        slug: `${TEST_PREFIX}-unpublished-slug`,\n        description: \"Unpublished page\",\n        customDomain: \"\",\n        published: false,\n        accessType: \"public\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"GetStatusPageContent\",\n        { slug: unpublishedPage.slug },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(page).where(eq(page.id, unpublishedPage.id));\n    }\n  });\n\n  test(\"returns 403 for password-protected page accessed by slug\", async () => {\n    // Create a password-protected page\n    const protectedPage = await db\n      .insert(page)\n      .values({\n        workspaceId: 1,\n        title: `${TEST_PREFIX}-protected`,\n        slug: `${TEST_PREFIX}-protected-slug`,\n        description: \"Password protected page\",\n        customDomain: \"\",\n        published: true,\n        accessType: \"password\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"GetStatusPageContent\",\n        { slug: protectedPage.slug },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(403);\n    } finally {\n      await db.delete(page).where(eq(page.id, protectedPage.id));\n    }\n  });\n\n  test(\"allows workspace owner to access unpublished page by ID\", async () => {\n    // Create an unpublished page\n    const unpublishedPage = await db\n      .insert(page)\n      .values({\n        workspaceId: 1,\n        title: `${TEST_PREFIX}-unpublished-by-id`,\n        slug: `${TEST_PREFIX}-unpublished-by-id-slug`,\n        description: \"Unpublished page accessible by ID\",\n        customDomain: \"\",\n        published: false,\n        accessType: \"public\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"GetStatusPageContent\",\n        { id: String(unpublishedPage.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      // Workspace owner can access their own unpublished pages by ID\n      expect(res.status).toBe(200);\n    } finally {\n      await db.delete(page).where(eq(page.id, unpublishedPage.id));\n    }\n  });\n\n  test(\"includes active status reports\", async () => {\n    // Create an active status report for the test page\n    const report = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 1,\n        pageId: testPageId,\n        title: `${TEST_PREFIX}-active-report`,\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n\n    await db.insert(statusReportsToPageComponents).values({\n      statusReportId: report.id,\n      pageComponentId: testComponentId,\n    });\n\n    try {\n      const res = await connectRequest(\n        \"GetStatusPageContent\",\n        { id: String(testPageId) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      expect(data.statusReports.length).toBeGreaterThan(0);\n\n      const testReport = data.statusReports.find(\n        (r: { title: string }) => r.title === `${TEST_PREFIX}-active-report`,\n      );\n      expect(testReport).toBeDefined();\n    } finally {\n      await db\n        .delete(statusReportsToPageComponents)\n        .where(eq(statusReportsToPageComponents.statusReportId, report.id));\n      await db.delete(statusReport).where(eq(statusReport.id, report.id));\n    }\n  });\n});\n\ndescribe(\"StatusPageService.GetOverallStatus\", () => {\n  test(\"returns overall status by ID\", async () => {\n    const res = await connectRequest(\n      \"GetOverallStatus\",\n      { id: String(testPageId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"overallStatus\");\n    expect(data).toHaveProperty(\"componentStatuses\");\n  });\n\n  test(\"returns overall status by slug\", async () => {\n    const res = await connectRequest(\n      \"GetOverallStatus\",\n      { slug: testPageSlug },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"overallStatus\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"GetOverallStatus\", {\n      id: String(testPageId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent page\", async () => {\n    const res = await connectRequest(\n      \"GetOverallStatus\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns degraded status when there are active incidents\", async () => {\n    // Create an active status report for the test page\n    const report = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 1,\n        pageId: testPageId,\n        title: `${TEST_PREFIX}-incident-report`,\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n\n    await db.insert(statusReportsToPageComponents).values({\n      statusReportId: report.id,\n      pageComponentId: testComponentId,\n    });\n\n    try {\n      const res = await connectRequest(\n        \"GetOverallStatus\",\n        { id: String(testPageId) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      expect(data.overallStatus).toBe(\"OVERALL_STATUS_DEGRADED\");\n    } finally {\n      await db\n        .delete(statusReportsToPageComponents)\n        .where(eq(statusReportsToPageComponents.statusReportId, report.id));\n      await db.delete(statusReport).where(eq(statusReport.id, report.id));\n    }\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/status-page/converters.ts",
    "content": "import type {\n  PageComponent,\n  PageComponentGroup,\n  PageSubscriber,\n  StatusPage,\n  StatusPageSummary,\n} from \"@openstatus/proto/status_page/v1\";\nimport {\n  OverallStatus,\n  PageAccessType,\n  PageComponentType,\n  PageTheme,\n} from \"@openstatus/proto/status_page/v1\";\n\n/**\n * Database types\n */\ntype DBPage = {\n  id: number;\n  title: string;\n  description: string;\n  slug: string;\n  customDomain: string;\n  published: boolean | null;\n  forceTheme: \"system\" | \"light\" | \"dark\";\n  accessType: \"public\" | \"password\" | \"email-domain\" | null;\n  homepageUrl: string | null;\n  contactUrl: string | null;\n  icon: string | null;\n  createdAt: Date | null;\n  updatedAt: Date | null;\n};\n\ntype DBPageComponent = {\n  id: number;\n  pageId: number;\n  name: string;\n  description: string | null;\n  type: \"static\" | \"monitor\";\n  monitorId: number | null;\n  order: number | null;\n  groupId: number | null;\n  groupOrder: number | null;\n  createdAt: Date | null;\n  updatedAt: Date | null;\n};\n\ntype DBPageComponentGroup = {\n  id: number;\n  pageId: number;\n  name: string;\n  createdAt: Date | null;\n  updatedAt: Date | null;\n};\n\ntype DBPageSubscriber = {\n  id: number;\n  pageId: number;\n  email: string;\n  acceptedAt: Date | null;\n  unsubscribedAt: Date | null;\n  createdAt: Date | null;\n  updatedAt: Date | null;\n};\n\n/**\n * Convert DB access type string to proto enum.\n */\nexport function dbAccessTypeToProto(\n  accessType: \"public\" | \"password\" | \"email-domain\" | null,\n): PageAccessType {\n  switch (accessType) {\n    case \"public\":\n      return PageAccessType.PUBLIC;\n    case \"password\":\n      return PageAccessType.PASSWORD_PROTECTED;\n    case \"email-domain\":\n      return PageAccessType.AUTHENTICATED;\n    default:\n      return PageAccessType.PUBLIC;\n  }\n}\n\n/**\n * Convert proto access type enum to DB string.\n */\nexport function protoAccessTypeToDb(\n  accessType: PageAccessType,\n): \"public\" | \"password\" | \"email-domain\" {\n  switch (accessType) {\n    case PageAccessType.PUBLIC:\n      return \"public\";\n    case PageAccessType.PASSWORD_PROTECTED:\n      return \"password\";\n    case PageAccessType.AUTHENTICATED:\n      return \"email-domain\";\n    default:\n      return \"public\";\n  }\n}\n\n/**\n * Convert DB theme string to proto enum.\n */\nexport function dbThemeToProto(theme: \"system\" | \"light\" | \"dark\"): PageTheme {\n  switch (theme) {\n    case \"system\":\n      return PageTheme.SYSTEM;\n    case \"light\":\n      return PageTheme.LIGHT;\n    case \"dark\":\n      return PageTheme.DARK;\n    default:\n      return PageTheme.SYSTEM;\n  }\n}\n\n/**\n * Convert proto theme enum to DB string.\n */\nexport function protoThemeToDb(theme: PageTheme): \"system\" | \"light\" | \"dark\" {\n  switch (theme) {\n    case PageTheme.SYSTEM:\n      return \"system\";\n    case PageTheme.LIGHT:\n      return \"light\";\n    case PageTheme.DARK:\n      return \"dark\";\n    default:\n      return \"system\";\n  }\n}\n\n/**\n * Convert DB component type string to proto enum.\n */\nexport function dbComponentTypeToProto(\n  type: \"static\" | \"monitor\",\n): PageComponentType {\n  switch (type) {\n    case \"monitor\":\n      return PageComponentType.MONITOR;\n    case \"static\":\n      return PageComponentType.STATIC;\n    default:\n      return PageComponentType.UNSPECIFIED;\n  }\n}\n\n/**\n * Convert proto component type enum to DB string.\n */\nexport function protoComponentTypeToDb(\n  type: PageComponentType,\n): \"static\" | \"monitor\" {\n  switch (type) {\n    case PageComponentType.MONITOR:\n      return \"monitor\";\n    case PageComponentType.STATIC:\n      return \"static\";\n    default:\n      return \"static\";\n  }\n}\n\n/**\n * Convert a DB status page to full proto format.\n */\nexport function dbPageToProto(page: DBPage): StatusPage {\n  return {\n    $typeName: \"openstatus.status_page.v1.StatusPage\" as const,\n    id: String(page.id),\n    title: page.title,\n    description: page.description,\n    slug: page.slug,\n    customDomain: page.customDomain ?? \"\",\n    published: page.published ?? false,\n    accessType: dbAccessTypeToProto(page.accessType),\n    theme: dbThemeToProto(page.forceTheme),\n    homepageUrl: page.homepageUrl ?? \"\",\n    contactUrl: page.contactUrl ?? \"\",\n    icon: page.icon ?? \"\",\n    createdAt: page.createdAt?.toISOString() ?? \"\",\n    updatedAt: page.updatedAt?.toISOString() ?? \"\",\n  };\n}\n\n/**\n * Convert a DB status page to summary proto format.\n */\nexport function dbPageToProtoSummary(page: DBPage): StatusPageSummary {\n  return {\n    $typeName: \"openstatus.status_page.v1.StatusPageSummary\" as const,\n    id: String(page.id),\n    title: page.title,\n    slug: page.slug,\n    published: page.published ?? false,\n    createdAt: page.createdAt?.toISOString() ?? \"\",\n    updatedAt: page.updatedAt?.toISOString() ?? \"\",\n  };\n}\n\n/**\n * Convert a DB page component to proto format.\n */\nexport function dbComponentToProto(component: DBPageComponent): PageComponent {\n  return {\n    $typeName: \"openstatus.status_page.v1.PageComponent\" as const,\n    id: String(component.id),\n    pageId: String(component.pageId),\n    name: component.name,\n    description: component.description ?? \"\",\n    type: dbComponentTypeToProto(component.type),\n    monitorId: component.monitorId != null ? String(component.monitorId) : \"\",\n    order: component.order ?? 0,\n    groupId: component.groupId != null ? String(component.groupId) : \"\",\n    groupOrder: component.groupOrder ?? 0,\n    createdAt: component.createdAt?.toISOString() ?? \"\",\n    updatedAt: component.updatedAt?.toISOString() ?? \"\",\n  };\n}\n\n/**\n * Convert a DB component group to proto format.\n */\nexport function dbGroupToProto(\n  group: DBPageComponentGroup,\n): PageComponentGroup {\n  return {\n    $typeName: \"openstatus.status_page.v1.PageComponentGroup\" as const,\n    id: String(group.id),\n    pageId: String(group.pageId),\n    name: group.name,\n    createdAt: group.createdAt?.toISOString() ?? \"\",\n    updatedAt: group.updatedAt?.toISOString() ?? \"\",\n  };\n}\n\n/**\n * Convert a DB subscriber to proto format.\n */\nexport function dbSubscriberToProto(\n  subscriber: DBPageSubscriber,\n): PageSubscriber {\n  return {\n    $typeName: \"openstatus.status_page.v1.PageSubscriber\" as const,\n    id: String(subscriber.id),\n    pageId: String(subscriber.pageId),\n    email: subscriber.email,\n    acceptedAt: subscriber.acceptedAt?.toISOString() ?? \"\",\n    unsubscribedAt: subscriber.unsubscribedAt?.toISOString() ?? \"\",\n    createdAt: subscriber.createdAt?.toISOString() ?? \"\",\n    updatedAt: subscriber.updatedAt?.toISOString() ?? \"\",\n  };\n}\n\n/**\n * Get overall status based on component statuses.\n * This is a placeholder - the actual implementation would look at\n * monitor statuses, active incidents, and maintenance windows.\n */\nexport function getOverallStatusValue(): OverallStatus {\n  // Default to operational - in a real implementation this would\n  // aggregate status from monitors and incidents\n  return OverallStatus.OPERATIONAL;\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/status-page/errors.ts",
    "content": "import { Code, ConnectError } from \"@connectrpc/connect\";\n\n/**\n * Error reasons for structured error handling.\n */\nexport const ErrorReason = {\n  STATUS_PAGE_NOT_FOUND: \"STATUS_PAGE_NOT_FOUND\",\n  STATUS_PAGE_ID_REQUIRED: \"STATUS_PAGE_ID_REQUIRED\",\n  STATUS_PAGE_CREATE_FAILED: \"STATUS_PAGE_CREATE_FAILED\",\n  STATUS_PAGE_UPDATE_FAILED: \"STATUS_PAGE_UPDATE_FAILED\",\n  STATUS_PAGE_NOT_PUBLISHED: \"STATUS_PAGE_NOT_PUBLISHED\",\n  STATUS_PAGE_ACCESS_DENIED: \"STATUS_PAGE_ACCESS_DENIED\",\n  SLUG_ALREADY_EXISTS: \"SLUG_ALREADY_EXISTS\",\n  PAGE_COMPONENT_NOT_FOUND: \"PAGE_COMPONENT_NOT_FOUND\",\n  PAGE_COMPONENT_CREATE_FAILED: \"PAGE_COMPONENT_CREATE_FAILED\",\n  PAGE_COMPONENT_UPDATE_FAILED: \"PAGE_COMPONENT_UPDATE_FAILED\",\n  COMPONENT_GROUP_NOT_FOUND: \"COMPONENT_GROUP_NOT_FOUND\",\n  COMPONENT_GROUP_CREATE_FAILED: \"COMPONENT_GROUP_CREATE_FAILED\",\n  COMPONENT_GROUP_UPDATE_FAILED: \"COMPONENT_GROUP_UPDATE_FAILED\",\n  MONITOR_NOT_FOUND: \"MONITOR_NOT_FOUND\",\n  SUBSCRIBER_NOT_FOUND: \"SUBSCRIBER_NOT_FOUND\",\n  SUBSCRIBER_CREATE_FAILED: \"SUBSCRIBER_CREATE_FAILED\",\n  IDENTIFIER_REQUIRED: \"IDENTIFIER_REQUIRED\",\n} as const;\n\nexport type ErrorReason = (typeof ErrorReason)[keyof typeof ErrorReason];\n\nconst DOMAIN = \"openstatus.dev\";\n\n/**\n * Creates a ConnectError with structured metadata.\n */\nfunction createError(\n  message: string,\n  code: Code,\n  reason: ErrorReason,\n  metadata?: Record<string, string>,\n): ConnectError {\n  const headers = new Headers({\n    \"error-domain\": DOMAIN,\n    \"error-reason\": reason,\n  });\n\n  if (metadata) {\n    for (const [key, value] of Object.entries(metadata)) {\n      headers.set(`error-${key}`, value);\n    }\n  }\n\n  return new ConnectError(message, code, headers);\n}\n\n/**\n * Creates a \"status page not found\" error.\n */\nexport function statusPageNotFoundError(pageId: string): ConnectError {\n  return createError(\n    \"Status page not found\",\n    Code.NotFound,\n    ErrorReason.STATUS_PAGE_NOT_FOUND,\n    { \"page-id\": pageId },\n  );\n}\n\n/**\n * Creates a \"status page ID required\" error.\n */\nexport function statusPageIdRequiredError(): ConnectError {\n  return createError(\n    \"Status page ID is required\",\n    Code.InvalidArgument,\n    ErrorReason.STATUS_PAGE_ID_REQUIRED,\n  );\n}\n\n/**\n * Creates a \"failed to create status page\" error.\n */\nexport function statusPageCreateFailedError(): ConnectError {\n  return createError(\n    \"Failed to create status page\",\n    Code.Internal,\n    ErrorReason.STATUS_PAGE_CREATE_FAILED,\n  );\n}\n\n/**\n * Creates a \"failed to update status page\" error.\n */\nexport function statusPageUpdateFailedError(pageId: string): ConnectError {\n  return createError(\n    \"Failed to update status page\",\n    Code.Internal,\n    ErrorReason.STATUS_PAGE_UPDATE_FAILED,\n    { \"page-id\": pageId },\n  );\n}\n\n/**\n * Creates a \"slug already exists\" error.\n */\nexport function slugAlreadyExistsError(slug: string): ConnectError {\n  return createError(\n    \"A status page with this slug already exists\",\n    Code.AlreadyExists,\n    ErrorReason.SLUG_ALREADY_EXISTS,\n    { slug },\n  );\n}\n\n/**\n * Creates a \"status page not published\" error.\n * Used when trying to access an unpublished page via public slug.\n */\nexport function statusPageNotPublishedError(slug: string): ConnectError {\n  return createError(\n    \"Status page is not published\",\n    Code.NotFound,\n    ErrorReason.STATUS_PAGE_NOT_PUBLISHED,\n    { slug },\n  );\n}\n\n/**\n * Creates a \"status page access denied\" error.\n * Used when trying to access a protected page without proper authentication.\n */\nexport function statusPageAccessDeniedError(\n  slug: string,\n  accessType: string,\n): ConnectError {\n  return createError(\n    `Status page requires ${accessType} access`,\n    Code.PermissionDenied,\n    ErrorReason.STATUS_PAGE_ACCESS_DENIED,\n    { slug, \"access-type\": accessType },\n  );\n}\n\n/**\n * Creates a \"page component not found\" error.\n */\nexport function pageComponentNotFoundError(componentId: string): ConnectError {\n  return createError(\n    \"Page component not found\",\n    Code.NotFound,\n    ErrorReason.PAGE_COMPONENT_NOT_FOUND,\n    { \"component-id\": componentId },\n  );\n}\n\n/**\n * Creates a \"failed to create page component\" error.\n */\nexport function pageComponentCreateFailedError(): ConnectError {\n  return createError(\n    \"Failed to create page component\",\n    Code.Internal,\n    ErrorReason.PAGE_COMPONENT_CREATE_FAILED,\n  );\n}\n\n/**\n * Creates a \"failed to update page component\" error.\n */\nexport function pageComponentUpdateFailedError(\n  componentId: string,\n): ConnectError {\n  return createError(\n    \"Failed to update page component\",\n    Code.Internal,\n    ErrorReason.PAGE_COMPONENT_UPDATE_FAILED,\n    { \"component-id\": componentId },\n  );\n}\n\n/**\n * Creates a \"component group not found\" error.\n */\nexport function componentGroupNotFoundError(groupId: string): ConnectError {\n  return createError(\n    \"Component group not found\",\n    Code.NotFound,\n    ErrorReason.COMPONENT_GROUP_NOT_FOUND,\n    { \"group-id\": groupId },\n  );\n}\n\n/**\n * Creates a \"failed to create component group\" error.\n */\nexport function componentGroupCreateFailedError(): ConnectError {\n  return createError(\n    \"Failed to create component group\",\n    Code.Internal,\n    ErrorReason.COMPONENT_GROUP_CREATE_FAILED,\n  );\n}\n\n/**\n * Creates a \"failed to update component group\" error.\n */\nexport function componentGroupUpdateFailedError(groupId: string): ConnectError {\n  return createError(\n    \"Failed to update component group\",\n    Code.Internal,\n    ErrorReason.COMPONENT_GROUP_UPDATE_FAILED,\n    { \"group-id\": groupId },\n  );\n}\n\n/**\n * Creates a \"monitor not found\" error.\n */\nexport function monitorNotFoundError(monitorId: string): ConnectError {\n  return createError(\n    \"Monitor not found\",\n    Code.NotFound,\n    ErrorReason.MONITOR_NOT_FOUND,\n    { \"monitor-id\": monitorId },\n  );\n}\n\n/**\n * Creates a \"subscriber not found\" error.\n */\nexport function subscriberNotFoundError(identifier: string): ConnectError {\n  return createError(\n    \"Subscriber not found\",\n    Code.NotFound,\n    ErrorReason.SUBSCRIBER_NOT_FOUND,\n    { identifier },\n  );\n}\n\n/**\n * Creates a \"failed to create subscriber\" error.\n */\nexport function subscriberCreateFailedError(): ConnectError {\n  return createError(\n    \"Failed to create subscriber\",\n    Code.Internal,\n    ErrorReason.SUBSCRIBER_CREATE_FAILED,\n  );\n}\n\n/**\n * Creates an \"identifier required\" error.\n */\nexport function identifierRequiredError(): ConnectError {\n  return createError(\n    \"Either email or token is required to identify the subscriber\",\n    Code.InvalidArgument,\n    ErrorReason.IDENTIFIER_REQUIRED,\n  );\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/status-page/index.ts",
    "content": "import type { ServiceImpl } from \"@connectrpc/connect\";\nimport {\n  and,\n  count,\n  db,\n  desc,\n  eq,\n  gte,\n  inArray,\n  isNull,\n  lte,\n} from \"@openstatus/db\";\nimport {\n  maintenance,\n  maintenancesToPageComponents,\n  monitor,\n  page,\n  pageComponent,\n  pageComponentGroup,\n  pageSubscriber,\n  statusReport,\n  statusReportUpdate,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\nimport type { StatusPageService } from \"@openstatus/proto/status_page/v1\";\nimport { OverallStatus } from \"@openstatus/proto/status_page/v1\";\nimport { nanoid } from \"nanoid\";\n\nimport { getRpcContext } from \"../../interceptors\";\nimport {\n  dbComponentToProto,\n  dbGroupToProto,\n  dbPageToProto,\n  dbPageToProtoSummary,\n  dbSubscriberToProto,\n} from \"./converters\";\nimport {\n  componentGroupCreateFailedError,\n  componentGroupNotFoundError,\n  componentGroupUpdateFailedError,\n  identifierRequiredError,\n  monitorNotFoundError,\n  pageComponentCreateFailedError,\n  pageComponentNotFoundError,\n  pageComponentUpdateFailedError,\n  slugAlreadyExistsError,\n  statusPageAccessDeniedError,\n  statusPageCreateFailedError,\n  statusPageIdRequiredError,\n  statusPageNotFoundError,\n  statusPageNotPublishedError,\n  statusPageUpdateFailedError,\n  subscriberCreateFailedError,\n  subscriberNotFoundError,\n} from \"./errors\";\nimport { checkPageComponentLimits, checkStatusPageLimits } from \"./limits\";\n\n/**\n * Helper to get a status page by ID with workspace scope.\n */\nasync function getPageById(id: number, workspaceId: number) {\n  return db\n    .select()\n    .from(page)\n    .where(and(eq(page.id, id), eq(page.workspaceId, workspaceId)))\n    .get();\n}\n\n/**\n * Helper to get a status page by slug.\n * Normalizes the slug to lowercase before querying.\n */\nasync function getPageBySlug(slug: string) {\n  const normalizedSlug = slug.toLowerCase();\n  return db.select().from(page).where(eq(page.slug, normalizedSlug)).get();\n}\n\n/**\n * Validates public access to a status page.\n * Checks that the page is published and has public access type.\n * Throws appropriate errors if access is denied.\n */\nfunction validatePublicAccess(\n  pageData: { published: boolean | null; accessType: string | null },\n  slug: string,\n): void {\n  // Check if page is published\n  if (!pageData.published) {\n    throw statusPageNotPublishedError(slug);\n  }\n\n  // Check access type - only public pages are accessible without authentication\n  if (pageData.accessType && pageData.accessType !== \"public\") {\n    throw statusPageAccessDeniedError(slug, pageData.accessType);\n  }\n}\n\n/**\n * Helper to get a component by ID with workspace scope.\n */\nasync function getComponentById(id: number, workspaceId: number) {\n  return db\n    .select()\n    .from(pageComponent)\n    .where(\n      and(eq(pageComponent.id, id), eq(pageComponent.workspaceId, workspaceId)),\n    )\n    .get();\n}\n\n/**\n * Helper to get a component group by ID with workspace scope.\n */\nasync function getGroupById(id: number, workspaceId: number) {\n  return db\n    .select()\n    .from(pageComponentGroup)\n    .where(\n      and(\n        eq(pageComponentGroup.id, id),\n        eq(pageComponentGroup.workspaceId, workspaceId),\n      ),\n    )\n    .get();\n}\n\n/**\n * Helper to get a monitor by ID with workspace scope.\n */\nasync function getMonitorById(id: number, workspaceId: number) {\n  return db\n    .select()\n    .from(monitor)\n    .where(and(eq(monitor.id, id), eq(monitor.workspaceId, workspaceId)))\n    .get();\n}\n\n/**\n * Status page service implementation for ConnectRPC.\n */\nexport const statusPageServiceImpl: ServiceImpl<typeof StatusPageService> = {\n  // ==========================================================================\n  // Page CRUD\n  // ==========================================================================\n\n  async createStatusPage(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    // Check workspace limits for status pages\n    await checkStatusPageLimits(workspaceId, limits);\n\n    // Check if slug already exists\n    const existingPage = await getPageBySlug(req.slug);\n    if (existingPage) {\n      throw slugAlreadyExistsError(req.slug);\n    }\n\n    // Create the status page\n    const newPage = await db\n      .insert(page)\n      .values({\n        workspaceId,\n        title: req.title,\n        description: req.description ?? \"\",\n        slug: req.slug,\n        customDomain: \"\",\n        published: false,\n        homepageUrl: req.homepageUrl ?? null,\n        contactUrl: req.contactUrl ?? null,\n      })\n      .returning()\n      .get();\n\n    if (!newPage) {\n      throw statusPageCreateFailedError();\n    }\n\n    return {\n      statusPage: dbPageToProto(newPage),\n    };\n  },\n\n  async getStatusPage(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const id = req.id?.trim();\n    if (!id) {\n      throw statusPageIdRequiredError();\n    }\n\n    const pageData = await getPageById(Number(id), workspaceId);\n    if (!pageData) {\n      throw statusPageNotFoundError(id);\n    }\n\n    return {\n      statusPage: dbPageToProto(pageData),\n    };\n  },\n\n  async listStatusPages(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const limit = Math.min(Math.max(req.limit ?? 50, 1), 100);\n    const offset = req.offset ?? 0;\n\n    // Get total count\n    const countResult = await db\n      .select({ count: count() })\n      .from(page)\n      .where(eq(page.workspaceId, workspaceId))\n      .get();\n\n    const totalCount = countResult?.count ?? 0;\n\n    // Get pages\n    const pages = await db\n      .select()\n      .from(page)\n      .where(eq(page.workspaceId, workspaceId))\n      .orderBy(desc(page.createdAt))\n      .limit(limit)\n      .offset(offset)\n      .all();\n\n    return {\n      statusPages: pages.map(dbPageToProtoSummary),\n      totalSize: totalCount,\n    };\n  },\n\n  async updateStatusPage(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const id = req.id?.trim();\n    if (!id) {\n      throw statusPageIdRequiredError();\n    }\n\n    const pageData = await getPageById(Number(id), workspaceId);\n    if (!pageData) {\n      throw statusPageNotFoundError(id);\n    }\n\n    // Check if new slug conflicts with another page\n    if (req.slug && req.slug !== pageData.slug) {\n      const existingPage = await getPageBySlug(req.slug);\n      if (existingPage && existingPage.id !== pageData.id) {\n        throw slugAlreadyExistsError(req.slug);\n      }\n    }\n\n    // Build update values\n    const updateValues: Record<string, unknown> = {\n      updatedAt: new Date(),\n    };\n\n    if (req.title !== undefined && req.title !== \"\") {\n      updateValues.title = req.title;\n    }\n    if (req.description !== undefined) {\n      updateValues.description = req.description;\n    }\n    if (req.slug !== undefined && req.slug !== \"\") {\n      updateValues.slug = req.slug;\n    }\n    if (req.homepageUrl !== undefined) {\n      updateValues.homepageUrl = req.homepageUrl || null;\n    }\n    if (req.contactUrl !== undefined) {\n      updateValues.contactUrl = req.contactUrl || null;\n    }\n\n    const updatedPage = await db\n      .update(page)\n      .set(updateValues)\n      .where(eq(page.id, pageData.id))\n      .returning()\n      .get();\n\n    if (!updatedPage) {\n      throw statusPageUpdateFailedError(req.id);\n    }\n\n    return {\n      statusPage: dbPageToProto(updatedPage),\n    };\n  },\n\n  async deleteStatusPage(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const id = req.id?.trim();\n    if (!id) {\n      throw statusPageIdRequiredError();\n    }\n\n    const pageData = await getPageById(Number(id), workspaceId);\n    if (!pageData) {\n      throw statusPageNotFoundError(id);\n    }\n\n    // Delete the page (cascade will delete components, groups, subscribers)\n    await db.delete(page).where(eq(page.id, pageData.id));\n\n    return { success: true };\n  },\n\n  // ==========================================================================\n  // Component Management\n  // ==========================================================================\n\n  async addMonitorComponent(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    // Verify page exists and belongs to workspace\n    const pageData = await getPageById(Number(req.pageId), workspaceId);\n    if (!pageData) {\n      throw statusPageNotFoundError(req.pageId);\n    }\n\n    // Check workspace limits for page components\n    await checkPageComponentLimits(pageData.id, limits);\n\n    // Verify monitor exists and belongs to workspace\n    const monitorData = await getMonitorById(\n      Number(req.monitorId),\n      workspaceId,\n    );\n    if (!monitorData) {\n      throw monitorNotFoundError(req.monitorId);\n    }\n\n    // Validate group exists if provided\n    if (req.groupId) {\n      const group = await getGroupById(Number(req.groupId), workspaceId);\n      if (!group) {\n        throw componentGroupNotFoundError(req.groupId);\n      }\n    }\n\n    // Create the component\n    const newComponent = await db\n      .insert(pageComponent)\n      .values({\n        workspaceId,\n        pageId: pageData.id,\n        type: \"monitor\",\n        monitorId: monitorData.id,\n        name: req.name ?? monitorData.name,\n        description: req.description ?? null,\n        order: req.order ?? 0,\n        groupId: req.groupId ? Number(req.groupId) : null,\n      })\n      .returning()\n      .get();\n\n    if (!newComponent) {\n      throw pageComponentCreateFailedError();\n    }\n\n    return {\n      component: dbComponentToProto(newComponent),\n    };\n  },\n\n  async addStaticComponent(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    // Verify page exists and belongs to workspace\n    const pageData = await getPageById(Number(req.pageId), workspaceId);\n    if (!pageData) {\n      throw statusPageNotFoundError(req.pageId);\n    }\n\n    // Check workspace limits for page components\n    await checkPageComponentLimits(pageData.id, limits);\n\n    // Validate group exists if provided\n    if (req.groupId) {\n      const group = await getGroupById(Number(req.groupId), workspaceId);\n      if (!group) {\n        throw componentGroupNotFoundError(req.groupId);\n      }\n    }\n\n    // Create the component\n    const newComponent = await db\n      .insert(pageComponent)\n      .values({\n        workspaceId,\n        pageId: pageData.id,\n        type: \"static\",\n        monitorId: null,\n        name: req.name,\n        description: req.description ?? null,\n        order: req.order ?? 0,\n        groupId: req.groupId ? Number(req.groupId) : null,\n      })\n      .returning()\n      .get();\n\n    if (!newComponent) {\n      throw pageComponentCreateFailedError();\n    }\n\n    return {\n      component: dbComponentToProto(newComponent),\n    };\n  },\n\n  async removeComponent(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const id = req.id?.trim();\n    if (!id) {\n      throw pageComponentNotFoundError(req.id);\n    }\n\n    const component = await getComponentById(Number(id), workspaceId);\n    if (!component) {\n      throw pageComponentNotFoundError(id);\n    }\n\n    // Delete the component\n    await db.delete(pageComponent).where(eq(pageComponent.id, component.id));\n\n    return { success: true };\n  },\n\n  async updateComponent(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const id = req.id?.trim();\n    if (!id) {\n      throw pageComponentNotFoundError(req.id);\n    }\n\n    const component = await getComponentById(Number(id), workspaceId);\n    if (!component) {\n      throw pageComponentNotFoundError(id);\n    }\n\n    // Validate group exists if provided\n    if (req.groupId !== undefined && req.groupId !== \"\") {\n      const group = await getGroupById(Number(req.groupId), workspaceId);\n      if (!group) {\n        throw componentGroupNotFoundError(req.groupId);\n      }\n    }\n\n    // Build update values\n    const updateValues: Record<string, unknown> = {\n      updatedAt: new Date(),\n    };\n\n    if (req.name !== undefined && req.name !== \"\") {\n      updateValues.name = req.name;\n    }\n    if (req.description !== undefined) {\n      updateValues.description = req.description || null;\n    }\n    if (req.order !== undefined) {\n      updateValues.order = req.order;\n    }\n    if (req.groupId !== undefined) {\n      // Empty string means remove from group\n      updateValues.groupId = req.groupId === \"\" ? null : Number(req.groupId);\n    }\n    if (req.groupOrder !== undefined) {\n      updateValues.groupOrder = req.groupOrder;\n    }\n\n    const updatedComponent = await db\n      .update(pageComponent)\n      .set(updateValues)\n      .where(eq(pageComponent.id, component.id))\n      .returning()\n      .get();\n\n    if (!updatedComponent) {\n      throw pageComponentUpdateFailedError(req.id);\n    }\n\n    return {\n      component: dbComponentToProto(updatedComponent),\n    };\n  },\n\n  // ==========================================================================\n  // Component Groups\n  // ==========================================================================\n\n  async createComponentGroup(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    // Verify page exists and belongs to workspace\n    const pageData = await getPageById(Number(req.pageId), workspaceId);\n    if (!pageData) {\n      throw statusPageNotFoundError(req.pageId);\n    }\n\n    // Create the group\n    const newGroup = await db\n      .insert(pageComponentGroup)\n      .values({\n        workspaceId,\n        pageId: pageData.id,\n        name: req.name,\n      })\n      .returning()\n      .get();\n\n    if (!newGroup) {\n      throw componentGroupCreateFailedError();\n    }\n\n    return {\n      group: dbGroupToProto(newGroup),\n    };\n  },\n\n  async deleteComponentGroup(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const id = req.id?.trim();\n    if (!id) {\n      throw componentGroupNotFoundError(req.id);\n    }\n\n    const group = await getGroupById(Number(id), workspaceId);\n    if (!group) {\n      throw componentGroupNotFoundError(id);\n    }\n\n    // Delete the group (components will have groupId set to null due to FK constraint)\n    await db\n      .delete(pageComponentGroup)\n      .where(eq(pageComponentGroup.id, group.id));\n\n    return { success: true };\n  },\n\n  async updateComponentGroup(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const id = req.id?.trim();\n    if (!id) {\n      throw componentGroupNotFoundError(req.id);\n    }\n\n    const group = await getGroupById(Number(id), workspaceId);\n    if (!group) {\n      throw componentGroupNotFoundError(id);\n    }\n\n    // Build update values\n    const updateValues: Record<string, unknown> = {\n      updatedAt: new Date(),\n    };\n\n    if (req.name !== undefined && req.name !== \"\") {\n      updateValues.name = req.name;\n    }\n\n    const updatedGroup = await db\n      .update(pageComponentGroup)\n      .set(updateValues)\n      .where(eq(pageComponentGroup.id, group.id))\n      .returning()\n      .get();\n\n    if (!updatedGroup) {\n      throw componentGroupUpdateFailedError(req.id);\n    }\n\n    return {\n      group: dbGroupToProto(updatedGroup),\n    };\n  },\n\n  // ==========================================================================\n  // Subscribers\n  // ==========================================================================\n\n  async subscribeToPage(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    // Verify page exists and belongs to workspace\n    const pageData = await getPageById(Number(req.pageId), workspaceId);\n    if (!pageData) {\n      throw statusPageNotFoundError(req.pageId);\n    }\n\n    // Check if already subscribed\n    const existingSubscriber = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, pageData.id),\n          eq(pageSubscriber.email, req.email),\n        ),\n      )\n      .get();\n\n    if (existingSubscriber) {\n      // If unsubscribed, resubscribe within a transaction to ensure atomicity\n      if (existingSubscriber.unsubscribedAt) {\n        const updatedSubscriber = await db.transaction(async (tx) => {\n          const result = await tx\n            .update(pageSubscriber)\n            .set({\n              unsubscribedAt: null,\n              updatedAt: new Date(),\n              token: nanoid(),\n            })\n            .where(eq(pageSubscriber.id, existingSubscriber.id))\n            .returning()\n            .get();\n\n          if (!result) {\n            throw subscriberCreateFailedError();\n          }\n\n          return result;\n        });\n\n        return {\n          subscriber: dbSubscriberToProto(updatedSubscriber),\n        };\n      }\n\n      // Already subscribed, return existing\n      return {\n        subscriber: dbSubscriberToProto(existingSubscriber),\n      };\n    }\n\n    // Create new subscriber\n    const newSubscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: pageData.id,\n        email: req.email,\n        token: nanoid(),\n      })\n      .returning()\n      .get();\n\n    if (!newSubscriber) {\n      throw subscriberCreateFailedError();\n    }\n\n    return {\n      subscriber: dbSubscriberToProto(newSubscriber),\n    };\n  },\n\n  async unsubscribeFromPage(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    // Verify page exists and belongs to workspace\n    const pageData = await getPageById(Number(req.pageId), workspaceId);\n    if (!pageData) {\n      throw statusPageNotFoundError(req.pageId);\n    }\n\n    // Find subscriber based on identifier type\n    if (req.identifier.case === \"email\") {\n      const subscriber = await db\n        .select()\n        .from(pageSubscriber)\n        .where(\n          and(\n            eq(pageSubscriber.pageId, pageData.id),\n            eq(pageSubscriber.email, req.identifier.value),\n          ),\n        )\n        .get();\n\n      if (!subscriber) {\n        throw subscriberNotFoundError(req.identifier.value);\n      }\n\n      await db\n        .update(pageSubscriber)\n        .set({ unsubscribedAt: new Date(), updatedAt: new Date() })\n        .where(eq(pageSubscriber.id, subscriber.id));\n\n      return { success: true };\n    }\n\n    if (req.identifier.case === \"id\") {\n      const subscriber = await db\n        .select()\n        .from(pageSubscriber)\n        .where(\n          and(\n            eq(pageSubscriber.pageId, pageData.id),\n            eq(pageSubscriber.id, Number(req.identifier.value)),\n          ),\n        )\n        .get();\n\n      if (!subscriber) {\n        throw subscriberNotFoundError(req.identifier.value);\n      }\n\n      await db\n        .update(pageSubscriber)\n        .set({ unsubscribedAt: new Date(), updatedAt: new Date() })\n        .where(eq(pageSubscriber.id, subscriber.id));\n\n      return { success: true };\n    }\n\n    throw identifierRequiredError();\n  },\n\n  async listSubscribers(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    // Verify page exists and belongs to workspace\n    const pageData = await getPageById(Number(req.pageId), workspaceId);\n    if (!pageData) {\n      throw statusPageNotFoundError(req.pageId);\n    }\n\n    const limit = Math.min(Math.max(req.limit ?? 50, 1), 100);\n    const offset = req.offset ?? 0;\n\n    // Build conditions\n    const conditions = [eq(pageSubscriber.pageId, pageData.id)];\n    if (!req.includeUnsubscribed) {\n      conditions.push(isNull(pageSubscriber.unsubscribedAt));\n    }\n\n    // Get total count\n    const countResult = await db\n      .select({ count: count() })\n      .from(pageSubscriber)\n      .where(and(...conditions))\n      .get();\n\n    const totalCount = countResult?.count ?? 0;\n\n    // Get subscribers\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(and(...conditions))\n      .orderBy(desc(pageSubscriber.createdAt))\n      .limit(limit)\n      .offset(offset)\n      .all();\n\n    return {\n      subscribers: subscribers.map(dbSubscriberToProto),\n      totalSize: totalCount,\n    };\n  },\n\n  // ==========================================================================\n  // Full Content & Status\n  // ==========================================================================\n\n  async getStatusPageContent(req, ctx) {\n    // Note: This endpoint may be used publicly, so we need to handle\n    // the case where we look up by slug without workspace scope\n    type PageData = Awaited<ReturnType<typeof getPageById>>;\n    let pageData: PageData;\n    let identifierValue: string;\n    let isPublicAccess = false;\n\n    if (req.identifier.case === \"id\") {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n      identifierValue = req.identifier.value;\n      pageData = await getPageById(Number(identifierValue), workspaceId);\n    } else if (req.identifier.case === \"slug\") {\n      identifierValue = req.identifier.value;\n      pageData = await getPageBySlug(identifierValue);\n      isPublicAccess = true;\n    } else {\n      throw statusPageIdRequiredError();\n    }\n\n    if (!pageData) {\n      throw statusPageNotFoundError(identifierValue);\n    }\n\n    // Access control differs based on how the page is accessed:\n    // - By slug (public): Validates page is published and publicly accessible\n    // - By ID (workspace): Allows workspace members to preview unpublished pages\n    if (isPublicAccess) {\n      validatePublicAccess(pageData, identifierValue);\n    }\n\n    // Get components\n    const components = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.pageId, pageData.id))\n      .orderBy(pageComponent.order)\n      .all();\n\n    // Get groups\n    const groups = await db\n      .select()\n      .from(pageComponentGroup)\n      .where(eq(pageComponentGroup.pageId, pageData.id))\n      .all();\n\n    // Get active status reports (not resolved)\n    const activeReports = await db\n      .select()\n      .from(statusReport)\n      .where(\n        and(\n          eq(statusReport.pageId, pageData.id),\n          inArray(statusReport.status, [\n            \"investigating\",\n            \"identified\",\n            \"monitoring\",\n          ]),\n        ),\n      )\n      .orderBy(desc(statusReport.createdAt))\n      .all();\n\n    // Get status report updates for active reports\n    const reportIds = activeReports.map((r) => r.id);\n    const reportUpdates =\n      reportIds.length > 0\n        ? await db\n            .select()\n            .from(statusReportUpdate)\n            .where(inArray(statusReportUpdate.statusReportId, reportIds))\n            .orderBy(desc(statusReportUpdate.date))\n            .all()\n        : [];\n\n    // Get page component IDs for each report\n    const reportComponents =\n      reportIds.length > 0\n        ? await db\n            .select()\n            .from(statusReportsToPageComponents)\n            .where(\n              inArray(statusReportsToPageComponents.statusReportId, reportIds),\n            )\n            .all()\n        : [];\n\n    // Import the converter from status-report service\n    const { dbStatusToProto } = await import(\"../status-report/converters\");\n\n    // Convert reports to proto format\n    const statusReports = activeReports.map((report) => {\n      const updates = reportUpdates.filter(\n        (u) => u.statusReportId === report.id,\n      );\n      const componentIds = reportComponents\n        .filter((rc) => rc.statusReportId === report.id)\n        .map((rc) => String(rc.pageComponentId));\n\n      return {\n        $typeName: \"openstatus.status_report.v1.StatusReport\" as const,\n        id: String(report.id),\n        status: dbStatusToProto(report.status),\n        title: report.title,\n        pageComponentIds: componentIds,\n        updates: updates.map((u) => ({\n          $typeName: \"openstatus.status_report.v1.StatusReportUpdate\" as const,\n          id: String(u.id),\n          status: dbStatusToProto(u.status),\n          date: u.date.toISOString(),\n          message: u.message,\n          createdAt: u.createdAt?.toISOString() ?? \"\",\n        })),\n        createdAt: report.createdAt?.toISOString() ?? \"\",\n        updatedAt: report.updatedAt?.toISOString() ?? \"\",\n      };\n    });\n\n    // Get maintenances for the page (upcoming and recent)\n    const pageMaintenances = await db\n      .select()\n      .from(maintenance)\n      .where(eq(maintenance.pageId, pageData.id))\n      .orderBy(desc(maintenance.from))\n      .all();\n\n    // Get component associations for maintenances\n    const maintenanceIds = pageMaintenances.map((m) => m.id);\n    const maintenanceComponents =\n      maintenanceIds.length > 0\n        ? await db\n            .select()\n            .from(maintenancesToPageComponents)\n            .where(\n              inArray(\n                maintenancesToPageComponents.maintenanceId,\n                maintenanceIds,\n              ),\n            )\n            .all()\n        : [];\n\n    // Convert maintenances to proto format\n    const maintenancesProto = pageMaintenances.map((m) => {\n      const componentIds = maintenanceComponents\n        .filter((mc) => mc.maintenanceId === m.id)\n        .map((mc) => String(mc.pageComponentId));\n\n      return {\n        $typeName: \"openstatus.maintenance.v1.MaintenanceSummary\" as const,\n        id: String(m.id),\n        title: m.title,\n        message: m.message,\n        from: m.from.toISOString(),\n        to: m.to.toISOString(),\n        pageId: String(pageData.id),\n        pageComponentIds: componentIds,\n        createdAt: m.createdAt?.toISOString() ?? \"\",\n        updatedAt: m.updatedAt?.toISOString() ?? \"\",\n      };\n    });\n\n    return {\n      statusPage: dbPageToProto(pageData),\n      components: components.map(dbComponentToProto),\n      groups: groups.map(dbGroupToProto),\n      statusReports,\n      maintenances: maintenancesProto,\n    };\n  },\n\n  async getOverallStatus(req, ctx) {\n    type PageData = Awaited<ReturnType<typeof getPageById>>;\n    let pageData: PageData;\n    let identifierValue: string;\n    let isPublicAccess = false;\n\n    if (req.identifier.case === \"id\") {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n      identifierValue = req.identifier.value;\n      pageData = await getPageById(Number(identifierValue), workspaceId);\n    } else if (req.identifier.case === \"slug\") {\n      identifierValue = req.identifier.value;\n      pageData = await getPageBySlug(identifierValue);\n      isPublicAccess = true;\n    } else {\n      throw statusPageIdRequiredError();\n    }\n\n    if (!pageData) {\n      throw statusPageNotFoundError(identifierValue);\n    }\n\n    // Access control differs based on how the page is accessed:\n    // - By slug (public): Validates page is published and publicly accessible\n    // - By ID (workspace): Allows workspace members to preview unpublished pages\n    if (isPublicAccess) {\n      validatePublicAccess(pageData, identifierValue);\n    }\n\n    // Get components\n    const components = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.pageId, pageData.id))\n      .all();\n\n    const componentIds = components.map((c) => c.id);\n    const now = new Date();\n\n    // Check for active status reports (degraded state)\n    let hasActiveStatusReport = false;\n    const componentReportStatus = new Map<number, boolean>();\n\n    if (componentIds.length > 0) {\n      const activeReports = await db\n        .select({\n          componentId: statusReportsToPageComponents.pageComponentId,\n        })\n        .from(statusReportsToPageComponents)\n        .innerJoin(\n          statusReport,\n          eq(statusReportsToPageComponents.statusReportId, statusReport.id),\n        )\n        .where(\n          and(\n            inArray(\n              statusReportsToPageComponents.pageComponentId,\n              componentIds,\n            ),\n            inArray(statusReport.status, [\n              \"investigating\",\n              \"identified\",\n              \"monitoring\",\n            ]),\n          ),\n        )\n        .all();\n\n      hasActiveStatusReport = activeReports.length > 0;\n\n      // Track which components have active reports\n      for (const report of activeReports) {\n        componentReportStatus.set(report.componentId, true);\n      }\n    }\n\n    // Check for active maintenances (info state - current time between from and to)\n    let hasActiveMaintenance = false;\n    const componentMaintenanceStatus = new Map<number, boolean>();\n\n    const activeMaintenances = await db\n      .select()\n      .from(maintenance)\n      .where(\n        and(\n          eq(maintenance.pageId, pageData.id),\n          lte(maintenance.from, now),\n          gte(maintenance.to, now),\n        ),\n      )\n      .all();\n\n    hasActiveMaintenance = activeMaintenances.length > 0;\n\n    // Get component associations for active maintenances\n    if (activeMaintenances.length > 0) {\n      const maintenanceIds = activeMaintenances.map((m) => m.id);\n      const maintenanceComponentAssocs = await db\n        .select()\n        .from(maintenancesToPageComponents)\n        .where(\n          inArray(maintenancesToPageComponents.maintenanceId, maintenanceIds),\n        )\n        .all();\n\n      // Track which components are under maintenance\n      for (const assoc of maintenanceComponentAssocs) {\n        componentMaintenanceStatus.set(assoc.pageComponentId, true);\n      }\n    }\n\n    // Determine overall status based on priority: degraded > maintenance > operational\n    // Note: In the existing codebase, status reports indicate \"degraded\" state\n    // and maintenances indicate \"info/maintenance\" state\n    const overallStatus = hasActiveStatusReport\n      ? OverallStatus.DEGRADED\n      : hasActiveMaintenance\n        ? OverallStatus.MAINTENANCE\n        : OverallStatus.OPERATIONAL;\n\n    // Build component statuses based on their individual state\n    const componentStatuses = components.map((c) => {\n      const hasReport = componentReportStatus.get(c.id) ?? false;\n      const hasMaintenance = componentMaintenanceStatus.get(c.id) ?? false;\n\n      const status = hasReport\n        ? OverallStatus.DEGRADED\n        : hasMaintenance\n          ? OverallStatus.MAINTENANCE\n          : OverallStatus.OPERATIONAL;\n\n      return {\n        $typeName: \"openstatus.status_page.v1.ComponentStatus\" as const,\n        componentId: String(c.id),\n        status,\n      };\n    });\n\n    return {\n      overallStatus,\n      componentStatuses,\n    };\n  },\n};\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/status-page/limits.ts",
    "content": "import { Code, ConnectError } from \"@connectrpc/connect\";\nimport { count, db, eq } from \"@openstatus/db\";\nimport { page, pageComponent } from \"@openstatus/db/src/schema\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\n\n/**\n * Check workspace limits for creating a new status page.\n * Throws ConnectError with PermissionDenied if limit is exceeded.\n */\nexport async function checkStatusPageLimits(\n  workspaceId: number,\n  limits: Limits,\n): Promise<void> {\n  // Check status page count limit\n  const countResult = await db\n    .select({ count: count() })\n    .from(page)\n    .where(eq(page.workspaceId, workspaceId))\n    .get();\n\n  const currentCount = countResult?.count ?? 0;\n  if (currentCount >= limits[\"status-pages\"]) {\n    throw new ConnectError(\n      \"Upgrade for more status pages\",\n      Code.PermissionDenied,\n    );\n  }\n}\n\n/**\n * Check if custom domain feature is available on the workspace plan.\n * Throws ConnectError with PermissionDenied if not available.\n */\nexport function checkCustomDomainLimit(limits: Limits): void {\n  if (!limits[\"custom-domain\"]) {\n    throw new ConnectError(\"Upgrade for custom domains\", Code.PermissionDenied);\n  }\n}\n\n/**\n * Check if password protection feature is available on the workspace plan.\n * Throws ConnectError with PermissionDenied if not available.\n */\nexport function checkPasswordProtectionLimit(limits: Limits): void {\n  if (!limits[\"password-protection\"]) {\n    throw new ConnectError(\n      \"Upgrade for password protection\",\n      Code.PermissionDenied,\n    );\n  }\n}\n\n/**\n * Check if email domain protection feature is available on the workspace plan.\n * Throws ConnectError with PermissionDenied if not available.\n */\nexport function checkEmailDomainProtectionLimit(limits: Limits): void {\n  if (!limits[\"email-domain-protection\"]) {\n    throw new ConnectError(\n      \"Upgrade for email domain protection\",\n      Code.PermissionDenied,\n    );\n  }\n}\n\n/**\n * Check workspace limits for creating a new page component.\n * Throws ConnectError with PermissionDenied if limit is exceeded.\n */\nexport async function checkPageComponentLimits(\n  pageId: number,\n  limits: Limits,\n): Promise<void> {\n  const countResult = await db\n    .select({ count: count() })\n    .from(pageComponent)\n    .where(eq(pageComponent.pageId, pageId))\n    .get();\n\n  const currentCount = countResult?.count ?? 0;\n  if (currentCount >= limits[\"page-components\"]) {\n    throw new ConnectError(\n      \"Upgrade for more page components\",\n      Code.PermissionDenied,\n    );\n  }\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/status-report/__tests__/status-report.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  page,\n  pageComponent,\n  pageSubscriber,\n  statusReport,\n  statusReportUpdate,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\nimport { StatusReportStatus } from \"@openstatus/proto/status_report/v1\";\n\nimport { app } from \"@/index\";\nimport { protoStatusToDb } from \"../converters\";\n\nconst subscriptionSpies = (globalThis as Record<string, unknown>)\n  .__subscriptionSpies as {\n  dispatchStatusReportUpdate: ReturnType<typeof import(\"bun:test\").mock>;\n  dispatchMaintenanceUpdate: ReturnType<typeof import(\"bun:test\").mock>;\n};\n\n/**\n * Helper to make ConnectRPC requests using the Connect protocol (JSON).\n * Connect uses POST with JSON body at /rpc/<service>/<method>\n */\nasync function connectRequest(\n  method: string,\n  body: Record<string, unknown> = {},\n  headers: Record<string, string> = {},\n) {\n  return app.request(\n    `/rpc/openstatus.status_report.v1.StatusReportService/${method}`,\n    {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        ...headers,\n      },\n      body: JSON.stringify(body),\n    },\n  );\n}\n\nconst TEST_PREFIX = \"rpc-status-report-test\";\nlet testPageComponentId: number;\nlet testStatusReportId: number;\nlet testStatusReportToDeleteId: number;\nlet testStatusReportToUpdateId: number;\nlet testStatusReportForNotifyId: number;\nlet testSubscriberId: number;\n// For mixed-page validation tests\nlet testPage2Id: number;\nlet testPage2ComponentId: number;\n\nbeforeAll(async () => {\n  // Clean up any existing test data\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.title, `${TEST_PREFIX}-main`));\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.title, `${TEST_PREFIX}-to-delete`));\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.title, `${TEST_PREFIX}-to-update`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n\n  // Create a test page component (using existing page 1 from seed)\n  const component = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      type: \"static\",\n      name: `${TEST_PREFIX}-component`,\n      description: \"Test component for status report tests\",\n      order: 100,\n    })\n    .returning()\n    .get();\n  testPageComponentId = component.id;\n\n  // Create a second page and component for mixed-page validation tests\n  const page2 = await db\n    .insert(page)\n    .values({\n      workspaceId: 1,\n      title: `${TEST_PREFIX}-page-2`,\n      slug: `${TEST_PREFIX}-page-2-slug`,\n      description: \"Second test page for mixed-page tests\",\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n  testPage2Id = page2.id;\n\n  const component2 = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 1,\n      pageId: testPage2Id,\n      type: \"static\",\n      name: `${TEST_PREFIX}-component-2`,\n      description: \"Test component on page 2\",\n      order: 100,\n    })\n    .returning()\n    .get();\n  testPage2ComponentId = component2.id;\n\n  // Create test status report\n  const report = await db\n    .insert(statusReport)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-main`,\n      status: \"investigating\",\n    })\n    .returning()\n    .get();\n  testStatusReportId = report.id;\n\n  // Create page component association\n  await db.insert(statusReportsToPageComponents).values({\n    statusReportId: report.id,\n    pageComponentId: testPageComponentId,\n  });\n\n  // Create status report updates\n  await db.insert(statusReportUpdate).values([\n    {\n      statusReportId: report.id,\n      status: \"investigating\",\n      date: new Date(Date.now() - 60 * 60 * 1000), // 1 hour ago\n      message: \"We are investigating the issue.\",\n    },\n    {\n      statusReportId: report.id,\n      status: \"identified\",\n      date: new Date(Date.now() - 30 * 60 * 1000), // 30 min ago\n      message: \"We have identified the root cause.\",\n    },\n  ]);\n\n  // Create status report to delete\n  const deleteReport = await db\n    .insert(statusReport)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-to-delete`,\n      status: \"investigating\",\n    })\n    .returning()\n    .get();\n  testStatusReportToDeleteId = deleteReport.id;\n\n  // Create status report to update\n  const updateReport = await db\n    .insert(statusReport)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-to-update`,\n      status: \"investigating\",\n    })\n    .returning()\n    .get();\n  testStatusReportToUpdateId = updateReport.id;\n\n  await db.insert(statusReportsToPageComponents).values({\n    statusReportId: updateReport.id,\n    pageComponentId: testPageComponentId,\n  });\n\n  // Create status report for notify tests\n  const notifyReport = await db\n    .insert(statusReport)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-for-notify`,\n      status: \"investigating\",\n    })\n    .returning()\n    .get();\n  testStatusReportForNotifyId = notifyReport.id;\n\n  await db.insert(statusReportsToPageComponents).values({\n    statusReportId: notifyReport.id,\n    pageComponentId: testPageComponentId,\n  });\n\n  // Create a verified subscriber for notification tests\n  const subscriber = await db\n    .insert(pageSubscriber)\n    .values({\n      pageId: 1,\n      email: `${TEST_PREFIX}@example.com`,\n      token: `${TEST_PREFIX}-token`,\n      acceptedAt: new Date(),\n    })\n    .returning()\n    .get();\n  testSubscriberId = subscriber.id;\n});\n\nafterAll(async () => {\n  // Clean up subscriber first (only if it was created)\n  if (testSubscriberId) {\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.id, testSubscriberId));\n  }\n\n  // Clean up status report updates first (due to foreign key)\n  await db\n    .delete(statusReportUpdate)\n    .where(eq(statusReportUpdate.statusReportId, testStatusReportId));\n  await db\n    .delete(statusReportUpdate)\n    .where(eq(statusReportUpdate.statusReportId, testStatusReportToUpdateId));\n  await db\n    .delete(statusReportUpdate)\n    .where(eq(statusReportUpdate.statusReportId, testStatusReportForNotifyId));\n\n  // Clean up associations\n  await db\n    .delete(statusReportsToPageComponents)\n    .where(\n      eq(statusReportsToPageComponents.statusReportId, testStatusReportId),\n    );\n  await db\n    .delete(statusReportsToPageComponents)\n    .where(\n      eq(\n        statusReportsToPageComponents.statusReportId,\n        testStatusReportToUpdateId,\n      ),\n    );\n  await db\n    .delete(statusReportsToPageComponents)\n    .where(\n      eq(\n        statusReportsToPageComponents.statusReportId,\n        testStatusReportForNotifyId,\n      ),\n    );\n\n  // Clean up status reports\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.title, `${TEST_PREFIX}-main`));\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.title, `${TEST_PREFIX}-to-delete`));\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.title, `${TEST_PREFIX}-to-update`));\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.title, `${TEST_PREFIX}-for-notify`));\n\n  // Clean up page component\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n\n  // Clean up second page component and page (for mixed-page tests)\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component-2`));\n  await db.delete(page).where(eq(page.title, `${TEST_PREFIX}-page-2`));\n});\n\ndescribe(\"StatusReportService.CreateStatusReport\", () => {\n  test(\"creates a new status report with initial update\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: `${TEST_PREFIX}-created`,\n        status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        message: \"We are looking into this issue.\",\n        date: new Date().toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [String(testPageComponentId)],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.title).toBe(`${TEST_PREFIX}-created`);\n    expect(data.statusReport.status).toBe(\"STATUS_REPORT_STATUS_INVESTIGATING\");\n    expect(data.statusReport.pageComponentIds).toContain(\n      String(testPageComponentId),\n    );\n    expect(data.statusReport.updates).toHaveLength(1);\n    expect(data.statusReport.updates[0].message).toBe(\n      \"We are looking into this issue.\",\n    );\n\n    // Clean up\n    await db\n      .delete(statusReportUpdate)\n      .where(\n        eq(statusReportUpdate.statusReportId, Number(data.statusReport.id)),\n      );\n    await db\n      .delete(statusReportsToPageComponents)\n      .where(\n        eq(\n          statusReportsToPageComponents.statusReportId,\n          Number(data.statusReport.id),\n        ),\n      );\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, Number(data.statusReport.id)));\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"CreateStatusReport\", {\n      title: \"Unauthorized test\",\n      status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n      message: \"Test message\",\n      date: new Date().toISOString(),\n      pageId: \"1\",\n      pageComponentIds: [\"1\"],\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns error for invalid page component ID\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: \"Invalid component test\",\n        status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        message: \"Test message\",\n        date: new Date().toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [\"99999\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when page components are from different pages\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: `${TEST_PREFIX}-mixed-pages`,\n        status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        message: \"Test message\",\n        date: new Date().toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [\n          String(testPageComponentId),\n          String(testPage2ComponentId),\n        ],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\n      \"All page components must belong to the same page\",\n    );\n  });\n\n  test(\"returns error when pageId does not match components page\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: `${TEST_PREFIX}-pageid-mismatch`,\n        status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        message: \"Test pageId mismatch with components.\",\n        date: new Date().toISOString(),\n        pageId: \"1\", // This doesn't match testPage2ComponentId's page\n        pageComponentIds: [String(testPage2ComponentId)],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"does not match the page ID\");\n  });\n\n  test(\"creates status report when pageId matches component page\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: `${TEST_PREFIX}-matching-pageid`,\n        status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        message: \"Test with matching pageId and components.\",\n        date: new Date().toISOString(),\n        pageId: String(testPage2Id), // Matching the component's page\n        pageComponentIds: [String(testPage2ComponentId)],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.pageComponentIds).toContain(\n      String(testPage2ComponentId),\n    );\n\n    // Verify the pageId was set correctly\n    const createdReport = await db\n      .select()\n      .from(statusReport)\n      .where(eq(statusReport.id, Number(data.statusReport.id)))\n      .get();\n    expect(createdReport?.pageId).toBe(testPage2Id);\n\n    // Clean up\n    await db\n      .delete(statusReportUpdate)\n      .where(\n        eq(statusReportUpdate.statusReportId, Number(data.statusReport.id)),\n      );\n    await db\n      .delete(statusReportsToPageComponents)\n      .where(\n        eq(\n          statusReportsToPageComponents.statusReportId,\n          Number(data.statusReport.id),\n        ),\n      );\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, Number(data.statusReport.id)));\n  });\n\n  test(\"preserves pageId when no components provided\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: `${TEST_PREFIX}-null-pageid`,\n        status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        message: \"Test pageId preserved when no components.\",\n        date: new Date().toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n\n    // Verify the pageId is preserved from the request\n    const createdReport = await db\n      .select()\n      .from(statusReport)\n      .where(eq(statusReport.id, Number(data.statusReport.id)))\n      .get();\n    expect(createdReport?.pageId).toBe(1);\n\n    // Clean up\n    await db\n      .delete(statusReportUpdate)\n      .where(\n        eq(statusReportUpdate.statusReportId, Number(data.statusReport.id)),\n      );\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, Number(data.statusReport.id)));\n  });\n\n  test(\"creates status report with empty pageComponentIds\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: `${TEST_PREFIX}-no-components`,\n        status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        message: \"Report without components.\",\n        date: new Date().toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.title).toBe(`${TEST_PREFIX}-no-components`);\n    // Empty array may be serialized as undefined or empty array in proto\n    const pageComponentIds = data.statusReport.pageComponentIds ?? [];\n    expect(pageComponentIds).toHaveLength(0);\n\n    // Clean up\n    await db\n      .delete(statusReportUpdate)\n      .where(\n        eq(statusReportUpdate.statusReportId, Number(data.statusReport.id)),\n      );\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, Number(data.statusReport.id)));\n  });\n\n  test(\"creates status report with notify=true\", async () => {\n    subscriptionSpies.dispatchStatusReportUpdate.mockClear();\n\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: `${TEST_PREFIX}-with-notify`,\n        status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        message: \"Notifying subscribers about this issue.\",\n        date: new Date().toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [String(testPageComponentId)],\n        notify: true,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.title).toBe(`${TEST_PREFIX}-with-notify`);\n    expect(data.statusReport.updates).toHaveLength(1);\n\n    // Verify dispatcher was called (dispatchers are mocked in preload.ts)\n    expect(subscriptionSpies.dispatchStatusReportUpdate).toHaveBeenCalledTimes(\n      1,\n    );\n\n    // Clean up\n    await db\n      .delete(statusReportUpdate)\n      .where(\n        eq(statusReportUpdate.statusReportId, Number(data.statusReport.id)),\n      );\n    await db\n      .delete(statusReportsToPageComponents)\n      .where(\n        eq(\n          statusReportsToPageComponents.statusReportId,\n          Number(data.statusReport.id),\n        ),\n      );\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, Number(data.statusReport.id)));\n  });\n\n  test(\"creates status report with notify=false (default)\", async () => {\n    subscriptionSpies.dispatchStatusReportUpdate.mockClear();\n\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: `${TEST_PREFIX}-no-notify`,\n        status: \"STATUS_REPORT_STATUS_IDENTIFIED\",\n        message: \"No notification for this one.\",\n        date: new Date().toISOString(),\n        pageId: \"1\",\n        pageComponentIds: [],\n        notify: false,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.title).toBe(`${TEST_PREFIX}-no-notify`);\n\n    // Verify dispatcher was NOT called\n    expect(subscriptionSpies.dispatchStatusReportUpdate).not.toHaveBeenCalled();\n\n    // Clean up\n    await db\n      .delete(statusReportUpdate)\n      .where(\n        eq(statusReportUpdate.statusReportId, Number(data.statusReport.id)),\n      );\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, Number(data.statusReport.id)));\n  });\n\n  test(\"returns error for invalid date format\", async () => {\n    const res = await connectRequest(\n      \"CreateStatusReport\",\n      {\n        title: `${TEST_PREFIX}-invalid-date`,\n        status: \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        message: \"Test with invalid date.\",\n        date: \"not-a-valid-date\",\n        pageId: \"1\",\n        pageComponentIds: [],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"date: value does not match regex pattern\");\n  });\n});\n\ndescribe(\"StatusReportService.GetStatusReport\", () => {\n  test(\"returns status report with updates\", async () => {\n    const res = await connectRequest(\n      \"GetStatusReport\",\n      { id: String(testStatusReportId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.id).toBe(String(testStatusReportId));\n    expect(data.statusReport.title).toBe(`${TEST_PREFIX}-main`);\n    expect(data.statusReport.pageComponentIds).toContain(\n      String(testPageComponentId),\n    );\n    expect(data.statusReport.updates).toHaveLength(2);\n    expect(data.statusReport).toHaveProperty(\"createdAt\");\n    expect(data.statusReport).toHaveProperty(\"updatedAt\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"GetStatusReport\", {\n      id: String(testStatusReportId),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent status report\", async () => {\n    const res = await connectRequest(\n      \"GetStatusReport\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns 404 for status report in different workspace\", async () => {\n    // Create status report in workspace 2\n    const otherReport = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace`,\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"GetStatusReport\",\n        { id: String(otherReport.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(statusReport).where(eq(statusReport.id, otherReport.id));\n    }\n  });\n\n  test(\"returns error when ID is empty string\", async () => {\n    const res = await connectRequest(\n      \"GetStatusReport\",\n      { id: \"\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns error when ID is whitespace only\", async () => {\n    const res = await connectRequest(\n      \"GetStatusReport\",\n      { id: \"   \" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n});\n\ndescribe(\"StatusReportService.ListStatusReports\", () => {\n  test(\"returns status reports for authenticated workspace\", async () => {\n    const res = await connectRequest(\n      \"ListStatusReports\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReports\");\n    expect(Array.isArray(data.statusReports)).toBe(true);\n    expect(data).toHaveProperty(\"totalSize\");\n  });\n\n  test(\"returns status reports with correct structure (summary only)\", async () => {\n    const res = await connectRequest(\n      \"ListStatusReports\",\n      { limit: 100 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    const report = data.statusReports?.find(\n      (r: { id: string }) => r.id === String(testStatusReportId),\n    );\n\n    expect(report).toBeDefined();\n    expect(report.title).toBe(`${TEST_PREFIX}-main`);\n    expect(report.pageComponentIds).toBeDefined();\n    expect(report.createdAt).toBeDefined();\n    expect(report.updatedAt).toBeDefined();\n    // Summary should NOT include updates\n    expect(report.updates).toBeUndefined();\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"ListStatusReports\", {});\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"respects limit parameter\", async () => {\n    const res = await connectRequest(\n      \"ListStatusReports\",\n      { limit: 1 },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.statusReports?.length || 0).toBeLessThanOrEqual(1);\n  });\n\n  test(\"respects offset parameter\", async () => {\n    // Get total count first\n    const res1 = await connectRequest(\n      \"ListStatusReports\",\n      {},\n      { \"x-openstatus-key\": \"1\" },\n    );\n    const data1 = await res1.json();\n    const totalSize = data1.totalSize;\n\n    if (totalSize > 1) {\n      // Get first page\n      const res2 = await connectRequest(\n        \"ListStatusReports\",\n        { limit: 1, offset: 0 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n      const data2 = await res2.json();\n\n      // Get second page\n      const res3 = await connectRequest(\n        \"ListStatusReports\",\n        { limit: 1, offset: 1 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n      const data3 = await res3.json();\n\n      // Should have different reports\n      if (data2.statusReports?.length > 0 && data3.statusReports?.length > 0) {\n        expect(data2.statusReports[0].id).not.toBe(data3.statusReports[0].id);\n      }\n    }\n  });\n\n  test(\"filters by status\", async () => {\n    const res = await connectRequest(\n      \"ListStatusReports\",\n      { statuses: [\"STATUS_REPORT_STATUS_INVESTIGATING\"] },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // All returned reports should have investigating status\n    for (const report of data.statusReports || []) {\n      expect(report.status).toBe(\"STATUS_REPORT_STATUS_INVESTIGATING\");\n    }\n  });\n\n  test(\"only returns reports for authenticated workspace\", async () => {\n    // Create status report in workspace 2\n    const otherReport = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace-list`,\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"ListStatusReports\",\n        { limit: 100 },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      const reportIds = (data.statusReports || []).map(\n        (r: { id: string }) => r.id,\n      );\n\n      expect(reportIds).not.toContain(String(otherReport.id));\n    } finally {\n      await db.delete(statusReport).where(eq(statusReport.id, otherReport.id));\n    }\n  });\n\n  test(\"filters by multiple statuses\", async () => {\n    // Create reports with different statuses\n    const monitoringReport = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 1,\n        pageId: 1,\n        title: `${TEST_PREFIX}-monitoring-filter`,\n        status: \"monitoring\",\n      })\n      .returning()\n      .get();\n\n    const resolvedReport = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 1,\n        pageId: 1,\n        title: `${TEST_PREFIX}-resolved-filter`,\n        status: \"resolved\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"ListStatusReports\",\n        {\n          statuses: [\n            \"STATUS_REPORT_STATUS_MONITORING\",\n            \"STATUS_REPORT_STATUS_RESOLVED\",\n          ],\n        },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      const data = await res.json();\n      // All returned reports should have monitoring or resolved status\n      for (const report of data.statusReports || []) {\n        expect([\n          \"STATUS_REPORT_STATUS_MONITORING\",\n          \"STATUS_REPORT_STATUS_RESOLVED\",\n        ]).toContain(report.status);\n      }\n    } finally {\n      await db\n        .delete(statusReport)\n        .where(eq(statusReport.id, monitoringReport.id));\n      await db\n        .delete(statusReport)\n        .where(eq(statusReport.id, resolvedReport.id));\n    }\n  });\n\n  test(\"returns all statuses when statuses filter is empty\", async () => {\n    const res = await connectRequest(\n      \"ListStatusReports\",\n      { statuses: [] },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReports\");\n    expect(data).toHaveProperty(\"totalSize\");\n  });\n\n  test(\"ignores UNSPECIFIED status in filter\", async () => {\n    const res = await connectRequest(\n      \"ListStatusReports\",\n      {\n        statuses: [\n          \"STATUS_REPORT_STATUS_UNSPECIFIED\",\n          \"STATUS_REPORT_STATUS_INVESTIGATING\",\n        ],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    // Should only return investigating status (UNSPECIFIED is ignored)\n    for (const report of data.statusReports || []) {\n      expect(report.status).toBe(\"STATUS_REPORT_STATUS_INVESTIGATING\");\n    }\n  });\n});\n\ndescribe(\"StatusReportService.UpdateStatusReport\", () => {\n  test(\"updates status report title\", async () => {\n    const res = await connectRequest(\n      \"UpdateStatusReport\",\n      {\n        id: String(testStatusReportToUpdateId),\n        title: `${TEST_PREFIX}-updated-title`,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.title).toBe(`${TEST_PREFIX}-updated-title`);\n\n    // Restore original title\n    await db\n      .update(statusReport)\n      .set({ title: `${TEST_PREFIX}-to-update` })\n      .where(eq(statusReport.id, testStatusReportToUpdateId));\n  });\n\n  test(\"updates page component associations\", async () => {\n    // Use existing seeded page component 1\n    const res = await connectRequest(\n      \"UpdateStatusReport\",\n      {\n        id: String(testStatusReportToUpdateId),\n        pageComponentIds: [\"1\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.statusReport.pageComponentIds).toContain(\"1\");\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"UpdateStatusReport\", {\n      id: String(testStatusReportToUpdateId),\n      title: \"Unauthorized update\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent status report\", async () => {\n    const res = await connectRequest(\n      \"UpdateStatusReport\",\n      { id: \"99999\", title: \"Non-existent update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when ID is empty string\", async () => {\n    const res = await connectRequest(\n      \"UpdateStatusReport\",\n      { id: \"\", title: \"Empty ID update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns error when ID is whitespace only\", async () => {\n    const res = await connectRequest(\n      \"UpdateStatusReport\",\n      { id: \"   \", title: \"Whitespace ID update\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 404 for status report in different workspace\", async () => {\n    // Create status report in workspace 2\n    const otherReport = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace-update`,\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"UpdateStatusReport\",\n        { id: String(otherReport.id), title: \"Should not update\" },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(statusReport).where(eq(statusReport.id, otherReport.id));\n    }\n  });\n\n  test(\"returns error for invalid page component ID on update\", async () => {\n    const res = await connectRequest(\n      \"UpdateStatusReport\",\n      {\n        id: String(testStatusReportToUpdateId),\n        pageComponentIds: [\"99999\"],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when updating with components from different pages\", async () => {\n    const res = await connectRequest(\n      \"UpdateStatusReport\",\n      {\n        id: String(testStatusReportToUpdateId),\n        pageComponentIds: [\n          String(testPageComponentId),\n          String(testPage2ComponentId),\n        ],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\n      \"All page components must belong to the same page\",\n    );\n  });\n\n  test(\"updates pageId when changing components to different page\", async () => {\n    // First verify the current pageId\n    const beforeReport = await db\n      .select()\n      .from(statusReport)\n      .where(eq(statusReport.id, testStatusReportToUpdateId))\n      .get();\n    expect(beforeReport?.pageId).toBe(1);\n\n    // Update to use component from page 2\n    const res = await connectRequest(\n      \"UpdateStatusReport\",\n      {\n        id: String(testStatusReportToUpdateId),\n        pageComponentIds: [String(testPage2ComponentId)],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    // Verify the pageId was updated to page 2\n    const afterReport = await db\n      .select()\n      .from(statusReport)\n      .where(eq(statusReport.id, testStatusReportToUpdateId))\n      .get();\n    expect(afterReport?.pageId).toBe(testPage2Id);\n\n    // Restore original component association\n    await connectRequest(\n      \"UpdateStatusReport\",\n      {\n        id: String(testStatusReportToUpdateId),\n        pageComponentIds: [String(testPageComponentId)],\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n  });\n\n  test(\"clears pageId when removing all components\", async () => {\n    // Create a temporary status report for this test\n    const tempReport = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 1,\n        pageId: 1,\n        title: `${TEST_PREFIX}-clear-pageid`,\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n\n    await db.insert(statusReportsToPageComponents).values({\n      statusReportId: tempReport.id,\n      pageComponentId: testPageComponentId,\n    });\n\n    try {\n      // Clear all components\n      const res = await connectRequest(\n        \"UpdateStatusReport\",\n        {\n          id: String(tempReport.id),\n          pageComponentIds: [],\n        },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(200);\n\n      // Verify the pageId is now null\n      const afterReport = await db\n        .select()\n        .from(statusReport)\n        .where(eq(statusReport.id, tempReport.id))\n        .get();\n      expect(afterReport?.pageId).toBeNull();\n    } finally {\n      // Clean up\n      await db\n        .delete(statusReportsToPageComponents)\n        .where(eq(statusReportsToPageComponents.statusReportId, tempReport.id));\n      await db.delete(statusReport).where(eq(statusReport.id, tempReport.id));\n    }\n  });\n});\n\ndescribe(\"StatusReportService.DeleteStatusReport\", () => {\n  test(\"successfully deletes existing status report\", async () => {\n    const res = await connectRequest(\n      \"DeleteStatusReport\",\n      { id: String(testStatusReportToDeleteId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.success).toBe(true);\n\n    // Verify it's deleted\n    const deleted = await db\n      .select()\n      .from(statusReport)\n      .where(eq(statusReport.id, testStatusReportToDeleteId))\n      .get();\n    expect(deleted).toBeUndefined();\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"DeleteStatusReport\", { id: \"1\" });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent status report\", async () => {\n    const res = await connectRequest(\n      \"DeleteStatusReport\",\n      { id: \"99999\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when ID is empty string\", async () => {\n    const res = await connectRequest(\n      \"DeleteStatusReport\",\n      { id: \"\" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns error when ID is whitespace only\", async () => {\n    const res = await connectRequest(\n      \"DeleteStatusReport\",\n      { id: \"   \" },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 404 for status report in different workspace\", async () => {\n    // Create status report in workspace 2\n    const otherReport = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace-delete`,\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"DeleteStatusReport\",\n        { id: String(otherReport.id) },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n\n      // Verify it wasn't deleted\n      const stillExists = await db\n        .select()\n        .from(statusReport)\n        .where(eq(statusReport.id, otherReport.id))\n        .get();\n      expect(stillExists).toBeDefined();\n    } finally {\n      await db.delete(statusReport).where(eq(statusReport.id, otherReport.id));\n    }\n  });\n});\n\ndescribe(\"StatusReportService.AddStatusReportUpdate\", () => {\n  test(\"adds update to existing status report\", async () => {\n    const res = await connectRequest(\n      \"AddStatusReportUpdate\",\n      {\n        statusReportId: String(testStatusReportId),\n        status: \"STATUS_REPORT_STATUS_MONITORING\",\n        message: \"We are monitoring the fix.\",\n        date: new Date().toISOString(),\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.status).toBe(\"STATUS_REPORT_STATUS_MONITORING\");\n    // Should now have 3 updates (2 initial + 1 new)\n    expect(data.statusReport.updates.length).toBeGreaterThanOrEqual(3);\n\n    const newUpdate = data.statusReport.updates.find(\n      (u: { message: string }) => u.message === \"We are monitoring the fix.\",\n    );\n    expect(newUpdate).toBeDefined();\n    expect(newUpdate.status).toBe(\"STATUS_REPORT_STATUS_MONITORING\");\n  });\n\n  test(\"uses current time when date is not provided\", async () => {\n    // Allow 2 second tolerance for timing differences\n    const beforeTime = new Date(Date.now() - 2000);\n\n    const res = await connectRequest(\n      \"AddStatusReportUpdate\",\n      {\n        statusReportId: String(testStatusReportId),\n        status: \"STATUS_REPORT_STATUS_RESOLVED\",\n        message: \"Issue has been resolved.\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    const afterTime = new Date(Date.now() + 2000);\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.statusReport.status).toBe(\"STATUS_REPORT_STATUS_RESOLVED\");\n\n    const newUpdate = data.statusReport.updates.find(\n      (u: { message: string }) => u.message === \"Issue has been resolved.\",\n    );\n    expect(newUpdate).toBeDefined();\n\n    const updateDate = new Date(newUpdate.date);\n    expect(updateDate.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime());\n    expect(updateDate.getTime()).toBeLessThanOrEqual(afterTime.getTime());\n  });\n\n  test(\"returns 401 when no auth key provided\", async () => {\n    const res = await connectRequest(\"AddStatusReportUpdate\", {\n      statusReportId: String(testStatusReportId),\n      status: \"STATUS_REPORT_STATUS_RESOLVED\",\n      message: \"Unauthorized update\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"returns 404 for non-existent status report\", async () => {\n    const res = await connectRequest(\n      \"AddStatusReportUpdate\",\n      {\n        statusReportId: \"99999\",\n        status: \"STATUS_REPORT_STATUS_RESOLVED\",\n        message: \"Non-existent report\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(404);\n  });\n\n  test(\"returns error when statusReportId is empty string\", async () => {\n    const res = await connectRequest(\n      \"AddStatusReportUpdate\",\n      {\n        statusReportId: \"\",\n        status: \"STATUS_REPORT_STATUS_RESOLVED\",\n        message: \"Empty ID update\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns error when statusReportId is whitespace only\", async () => {\n    const res = await connectRequest(\n      \"AddStatusReportUpdate\",\n      {\n        statusReportId: \"   \",\n        status: \"STATUS_REPORT_STATUS_RESOLVED\",\n        message: \"Whitespace ID update\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 404 for status report in different workspace\", async () => {\n    // Create status report in workspace 2\n    const otherReport = await db\n      .insert(statusReport)\n      .values({\n        workspaceId: 2,\n        title: `${TEST_PREFIX}-other-workspace-add-update`,\n        status: \"investigating\",\n      })\n      .returning()\n      .get();\n\n    try {\n      const res = await connectRequest(\n        \"AddStatusReportUpdate\",\n        {\n          statusReportId: String(otherReport.id),\n          status: \"STATUS_REPORT_STATUS_RESOLVED\",\n          message: \"Should not add to other workspace\",\n        },\n        { \"x-openstatus-key\": \"1\" },\n      );\n\n      expect(res.status).toBe(404);\n    } finally {\n      await db.delete(statusReport).where(eq(statusReport.id, otherReport.id));\n    }\n  });\n\n  test(\"adds update with notify=true\", async () => {\n    subscriptionSpies.dispatchStatusReportUpdate.mockClear();\n\n    const res = await connectRequest(\n      \"AddStatusReportUpdate\",\n      {\n        statusReportId: String(testStatusReportForNotifyId),\n        status: \"STATUS_REPORT_STATUS_IDENTIFIED\",\n        message: \"We identified the issue and are notifying subscribers.\",\n        date: new Date().toISOString(),\n        notify: true,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.status).toBe(\"STATUS_REPORT_STATUS_IDENTIFIED\");\n\n    const newUpdate = data.statusReport.updates.find(\n      (u: { message: string }) =>\n        u.message === \"We identified the issue and are notifying subscribers.\",\n    );\n    expect(newUpdate).toBeDefined();\n\n    // Verify dispatcher was called (dispatchers are mocked in preload.ts)\n    expect(subscriptionSpies.dispatchStatusReportUpdate).toHaveBeenCalledTimes(\n      1,\n    );\n  });\n\n  test(\"adds update with notify=false\", async () => {\n    subscriptionSpies.dispatchStatusReportUpdate.mockClear();\n\n    const res = await connectRequest(\n      \"AddStatusReportUpdate\",\n      {\n        statusReportId: String(testStatusReportForNotifyId),\n        status: \"STATUS_REPORT_STATUS_MONITORING\",\n        message: \"Monitoring without notification.\",\n        date: new Date().toISOString(),\n        notify: false,\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data).toHaveProperty(\"statusReport\");\n    expect(data.statusReport.status).toBe(\"STATUS_REPORT_STATUS_MONITORING\");\n\n    // Verify dispatcher was NOT called\n    expect(subscriptionSpies.dispatchStatusReportUpdate).not.toHaveBeenCalled();\n  });\n\n  test(\"updates status report status when adding update\", async () => {\n    // First verify the current status\n    const getRes = await connectRequest(\n      \"GetStatusReport\",\n      { id: String(testStatusReportForNotifyId) },\n      { \"x-openstatus-key\": \"1\" },\n    );\n    const getData = await getRes.json();\n    const initialStatus = getData.statusReport.status;\n\n    // Add update with different status\n    const newStatus =\n      initialStatus === \"STATUS_REPORT_STATUS_RESOLVED\"\n        ? \"STATUS_REPORT_STATUS_INVESTIGATING\"\n        : \"STATUS_REPORT_STATUS_RESOLVED\";\n\n    const res = await connectRequest(\n      \"AddStatusReportUpdate\",\n      {\n        statusReportId: String(testStatusReportForNotifyId),\n        status: newStatus,\n        message: \"Status change test.\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(200);\n\n    const data = await res.json();\n    expect(data.statusReport.status).toBe(newStatus);\n  });\n\n  test(\"returns error for invalid date format\", async () => {\n    const res = await connectRequest(\n      \"AddStatusReportUpdate\",\n      {\n        statusReportId: String(testStatusReportId),\n        status: \"STATUS_REPORT_STATUS_MONITORING\",\n        message: \"Test with invalid date.\",\n        date: \"not-a-valid-date\",\n      },\n      { \"x-openstatus-key\": \"1\" },\n    );\n\n    expect(res.status).toBe(400);\n    const data = await res.json();\n    expect(data.message).toContain(\"date: value does not match regex pattern\");\n  });\n});\n\ndescribe(\"protoStatusToDb converter\", () => {\n  test(\"converts valid statuses correctly\", () => {\n    expect(protoStatusToDb(StatusReportStatus.INVESTIGATING)).toBe(\n      \"investigating\",\n    );\n    expect(protoStatusToDb(StatusReportStatus.IDENTIFIED)).toBe(\"identified\");\n    expect(protoStatusToDb(StatusReportStatus.MONITORING)).toBe(\"monitoring\");\n    expect(protoStatusToDb(StatusReportStatus.RESOLVED)).toBe(\"resolved\");\n  });\n\n  test(\"throws error for UNSPECIFIED status\", () => {\n    expect(() => protoStatusToDb(StatusReportStatus.UNSPECIFIED)).toThrow(\n      \"Invalid status value\",\n    );\n  });\n\n  test(\"throws error for unknown status values\", () => {\n    // Simulate a new status value being added to the proto\n    const unknownStatus = 999 as StatusReportStatus;\n    expect(() => protoStatusToDb(unknownStatus)).toThrow(\n      \"Invalid status value\",\n    );\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/status-report/converters.ts",
    "content": "import type {\n  StatusReport,\n  StatusReportSummary,\n  StatusReportUpdate,\n} from \"@openstatus/proto/status_report/v1\";\nimport { StatusReportStatus } from \"@openstatus/proto/status_report/v1\";\nimport { invalidStatusError } from \"./errors\";\n\ntype DBStatusReport = {\n  id: number;\n  status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\";\n  title: string;\n  workspaceId: number | null;\n  pageId: number | null;\n  createdAt: Date | null;\n  updatedAt: Date | null;\n};\n\ntype DBStatusReportUpdate = {\n  id: number;\n  status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\";\n  date: Date;\n  message: string;\n  statusReportId: number;\n  createdAt: Date | null;\n  updatedAt: Date | null;\n};\n\n/**\n * Convert DB status string to proto enum.\n */\nexport function dbStatusToProto(\n  status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\",\n): StatusReportStatus {\n  switch (status) {\n    case \"investigating\":\n      return StatusReportStatus.INVESTIGATING;\n    case \"identified\":\n      return StatusReportStatus.IDENTIFIED;\n    case \"monitoring\":\n      return StatusReportStatus.MONITORING;\n    case \"resolved\":\n      return StatusReportStatus.RESOLVED;\n    default:\n      return StatusReportStatus.UNSPECIFIED;\n  }\n}\n\n/**\n * Convert proto enum to DB status string.\n */\nexport function protoStatusToDb(\n  status: StatusReportStatus,\n): \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\" {\n  switch (status) {\n    case StatusReportStatus.INVESTIGATING:\n      return \"investigating\";\n    case StatusReportStatus.IDENTIFIED:\n      return \"identified\";\n    case StatusReportStatus.MONITORING:\n      return \"monitoring\";\n    case StatusReportStatus.RESOLVED:\n      return \"resolved\";\n    case StatusReportStatus.UNSPECIFIED:\n      throw invalidStatusError(status);\n    default:\n      throw invalidStatusError(status);\n  }\n}\n\n/**\n * Convert a DB status report update to proto format.\n */\nexport function dbUpdateToProto(\n  update: DBStatusReportUpdate,\n): StatusReportUpdate {\n  return {\n    $typeName: \"openstatus.status_report.v1.StatusReportUpdate\" as const,\n    id: String(update.id),\n    status: dbStatusToProto(update.status),\n    date: update.date.toISOString(),\n    message: update.message,\n    createdAt: update.createdAt?.toISOString() ?? \"\",\n  };\n}\n\n/**\n * Convert a DB status report to proto summary format (metadata only).\n */\nexport function dbReportToProtoSummary(\n  report: DBStatusReport,\n  pageComponentIds: string[],\n): StatusReportSummary {\n  return {\n    $typeName: \"openstatus.status_report.v1.StatusReportSummary\" as const,\n    id: String(report.id),\n    status: dbStatusToProto(report.status),\n    title: report.title,\n    pageComponentIds,\n    createdAt: report.createdAt?.toISOString() ?? \"\",\n    updatedAt: report.updatedAt?.toISOString() ?? \"\",\n  };\n}\n\n/**\n * Convert a DB status report to full proto format (with updates).\n */\nexport function dbReportToProto(\n  report: DBStatusReport,\n  pageComponentIds: string[],\n  updates: DBStatusReportUpdate[],\n): StatusReport {\n  return {\n    $typeName: \"openstatus.status_report.v1.StatusReport\" as const,\n    id: String(report.id),\n    status: dbStatusToProto(report.status),\n    title: report.title,\n    pageComponentIds,\n    updates: updates.map(dbUpdateToProto),\n    createdAt: report.createdAt?.toISOString() ?? \"\",\n    updatedAt: report.updatedAt?.toISOString() ?? \"\",\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/status-report/errors.ts",
    "content": "import { Code, ConnectError } from \"@connectrpc/connect\";\n\n/**\n * Error reasons for structured error handling.\n */\nexport const ErrorReason = {\n  STATUS_REPORT_NOT_FOUND: \"STATUS_REPORT_NOT_FOUND\",\n  STATUS_REPORT_ID_REQUIRED: \"STATUS_REPORT_ID_REQUIRED\",\n  STATUS_REPORT_CREATE_FAILED: \"STATUS_REPORT_CREATE_FAILED\",\n  STATUS_REPORT_UPDATE_FAILED: \"STATUS_REPORT_UPDATE_FAILED\",\n  PAGE_COMPONENT_NOT_FOUND: \"PAGE_COMPONENT_NOT_FOUND\",\n  PAGE_COMPONENTS_MIXED_PAGES: \"PAGE_COMPONENTS_MIXED_PAGES\",\n  PAGE_ID_COMPONENT_MISMATCH: \"PAGE_ID_COMPONENT_MISMATCH\",\n  INVALID_DATE_FORMAT: \"INVALID_DATE_FORMAT\",\n  INVALID_STATUS: \"INVALID_STATUS\",\n} as const;\n\nexport type ErrorReason = (typeof ErrorReason)[keyof typeof ErrorReason];\n\nconst DOMAIN = \"openstatus.dev\";\n\n/**\n * Creates a ConnectError with structured metadata.\n */\nfunction createError(\n  message: string,\n  code: Code,\n  reason: ErrorReason,\n  metadata?: Record<string, string>,\n): ConnectError {\n  const headers = new Headers({\n    \"error-domain\": DOMAIN,\n    \"error-reason\": reason,\n  });\n\n  if (metadata) {\n    for (const [key, value] of Object.entries(metadata)) {\n      headers.set(`error-${key}`, value);\n    }\n  }\n\n  return new ConnectError(message, code, headers);\n}\n\n/**\n * Creates a \"status report not found\" error.\n */\nexport function statusReportNotFoundError(\n  statusReportId: string,\n): ConnectError {\n  return createError(\n    \"Status report not found\",\n    Code.NotFound,\n    ErrorReason.STATUS_REPORT_NOT_FOUND,\n    { \"status-report-id\": statusReportId },\n  );\n}\n\n/**\n * Creates a \"status report ID required\" error.\n */\nexport function statusReportIdRequiredError(): ConnectError {\n  return createError(\n    \"Status report ID is required\",\n    Code.InvalidArgument,\n    ErrorReason.STATUS_REPORT_ID_REQUIRED,\n  );\n}\n\n/**\n * Creates a \"failed to create status report\" error.\n */\nexport function statusReportCreateFailedError(): ConnectError {\n  return createError(\n    \"Failed to create status report\",\n    Code.Internal,\n    ErrorReason.STATUS_REPORT_CREATE_FAILED,\n  );\n}\n\n/**\n * Creates a \"failed to update status report\" error.\n */\nexport function statusReportUpdateFailedError(\n  statusReportId: string,\n): ConnectError {\n  return createError(\n    \"Failed to update status report\",\n    Code.Internal,\n    ErrorReason.STATUS_REPORT_UPDATE_FAILED,\n    { \"status-report-id\": statusReportId },\n  );\n}\n\n/**\n * Creates a \"page component not found\" error.\n */\nexport function pageComponentNotFoundError(\n  pageComponentId: string,\n): ConnectError {\n  return createError(\n    \"Page component not found\",\n    Code.NotFound,\n    ErrorReason.PAGE_COMPONENT_NOT_FOUND,\n    { \"page-component-id\": pageComponentId },\n  );\n}\n\n/**\n * Creates a \"page components from mixed pages\" error.\n */\nexport function pageComponentsMixedPagesError(): ConnectError {\n  return createError(\n    \"All page components must belong to the same page\",\n    Code.InvalidArgument,\n    ErrorReason.PAGE_COMPONENTS_MIXED_PAGES,\n  );\n}\n\n/**\n * Creates an \"invalid date format\" error.\n */\nexport function invalidDateFormatError(dateValue: string): ConnectError {\n  return createError(\n    \"Invalid date format. Expected RFC 3339 format (e.g., 2024-01-15T10:30:00Z)\",\n    Code.InvalidArgument,\n    ErrorReason.INVALID_DATE_FORMAT,\n    { \"date-value\": dateValue },\n  );\n}\n\n/**\n * Creates an \"invalid status\" error.\n */\nexport function invalidStatusError(statusValue: number): ConnectError {\n  return createError(\n    `Invalid status value: ${statusValue}. Expected INVESTIGATING, IDENTIFIED, MONITORING, or RESOLVED`,\n    Code.InvalidArgument,\n    ErrorReason.INVALID_STATUS,\n    { \"status-value\": String(statusValue) },\n  );\n}\n\n/**\n * Creates a \"page ID and component page mismatch\" error.\n */\nexport function pageIdComponentMismatchError(\n  providedPageId: string,\n  componentPageId: string,\n): ConnectError {\n  return createError(\n    `Page ID ${providedPageId} does not match the page ID ${componentPageId} of the provided components`,\n    Code.InvalidArgument,\n    ErrorReason.PAGE_ID_COMPONENT_MISMATCH,\n    {\n      \"provided-page-id\": providedPageId,\n      \"component-page-id\": componentPageId,\n    },\n  );\n}\n"
  },
  {
    "path": "apps/server/src/routes/rpc/services/status-report/index.ts",
    "content": "import type { ServiceImpl } from \"@connectrpc/connect\";\nimport { and, db, desc, eq, inArray, sql } from \"@openstatus/db\";\n\n// Type that works with both db instance and transaction\ntype DB = typeof db;\ntype Transaction = Parameters<Parameters<DB[\"transaction\"]>[0]>[0];\nimport {\n  pageComponent,\n  statusReport,\n  statusReportUpdate,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport type { StatusReportService } from \"@openstatus/proto/status_report/v1\";\nimport { StatusReportStatus } from \"@openstatus/proto/status_report/v1\";\n\nimport { dispatchStatusReportUpdate } from \"@openstatus/subscriptions\";\n\nimport { getRpcContext } from \"../../interceptors\";\nimport {\n  dbReportToProto,\n  dbReportToProtoSummary,\n  protoStatusToDb,\n} from \"./converters\";\nimport {\n  invalidDateFormatError,\n  pageComponentNotFoundError,\n  pageComponentsMixedPagesError,\n  pageIdComponentMismatchError,\n  statusReportCreateFailedError,\n  statusReportIdRequiredError,\n  statusReportNotFoundError,\n  statusReportUpdateFailedError,\n} from \"./errors\";\n\n/**\n * Helper to send status report notifications to page subscribers.\n * Uses the subscription dispatcher for component-aware filtering.\n */\nexport async function sendStatusReportNotification(params: {\n  statusReportUpdateId: number;\n  limits: Limits;\n}) {\n  const { statusReportUpdateId, limits } = params;\n\n  if (!limits[\"status-subscribers\"]) {\n    return;\n  }\n\n  await dispatchStatusReportUpdate(statusReportUpdateId);\n}\n\n/**\n * Helper to get a status report by ID with workspace scope.\n */\nexport async function getStatusReportById(id: number, workspaceId: number) {\n  return db\n    .select()\n    .from(statusReport)\n    .where(\n      and(eq(statusReport.id, id), eq(statusReport.workspaceId, workspaceId)),\n    )\n    .get();\n}\n\n/**\n * Helper to get page component IDs for a status report.\n */\nexport async function getPageComponentIdsForReport(statusReportId: number) {\n  const components = await db\n    .select({ pageComponentId: statusReportsToPageComponents.pageComponentId })\n    .from(statusReportsToPageComponents)\n    .where(eq(statusReportsToPageComponents.statusReportId, statusReportId))\n    .all();\n\n  return components.map((c) => String(c.pageComponentId));\n}\n\n/**\n * Helper to get updates for a status report, ordered by date descending.\n */\nasync function getUpdatesForReport(statusReportId: number) {\n  return db\n    .select()\n    .from(statusReportUpdate)\n    .where(eq(statusReportUpdate.statusReportId, statusReportId))\n    .orderBy(desc(statusReportUpdate.date))\n    .all();\n}\n\n/**\n * Result of validating page component IDs.\n */\ninterface ValidatedPageComponents {\n  componentIds: number[];\n  pageId: number | null;\n}\n\n/**\n * Helper to validate page component IDs belong to the workspace and same page.\n * Accepts an optional transaction to ensure atomicity with subsequent operations.\n */\nexport async function validatePageComponentIds(\n  pageComponentIds: string[],\n  workspaceId: number,\n  tx: DB | Transaction = db,\n): Promise<ValidatedPageComponents> {\n  if (pageComponentIds.length === 0) {\n    return { componentIds: [], pageId: null };\n  }\n\n  const numericIds = pageComponentIds.map((id) => Number(id));\n\n  const validComponents = await tx\n    .select({ id: pageComponent.id, pageId: pageComponent.pageId })\n    .from(pageComponent)\n    .where(\n      and(\n        inArray(pageComponent.id, numericIds),\n        eq(pageComponent.workspaceId, workspaceId),\n      ),\n    )\n    .all();\n\n  const validComponentsMap = new Map(\n    validComponents.map((c) => [c.id, c.pageId]),\n  );\n\n  // Check all requested IDs exist\n  for (const id of numericIds) {\n    if (!validComponentsMap.has(id)) {\n      throw pageComponentNotFoundError(String(id));\n    }\n  }\n\n  // Validate all components belong to the same page\n  const pageIds = new Set(validComponents.map((c) => c.pageId));\n  if (pageIds.size > 1) {\n    throw pageComponentsMixedPagesError();\n  }\n\n  const pageId = validComponents[0]?.pageId ?? null;\n\n  return { componentIds: numericIds, pageId };\n}\n\n/**\n * Helper to update page component associations for a status report.\n * Accepts an optional transaction to ensure atomicity.\n */\nexport async function updatePageComponentAssociations(\n  statusReportId: number,\n  pageComponentIds: number[],\n  tx: DB | Transaction = db,\n) {\n  // Delete existing associations\n  await tx\n    .delete(statusReportsToPageComponents)\n    .where(eq(statusReportsToPageComponents.statusReportId, statusReportId));\n\n  // Insert new associations\n  if (pageComponentIds.length > 0) {\n    await tx.insert(statusReportsToPageComponents).values(\n      pageComponentIds.map((pageComponentId) => ({\n        statusReportId,\n        pageComponentId,\n      })),\n    );\n  }\n}\n\n/**\n * Parses and validates a date string.\n * Throws invalidDateFormatError if the date is invalid.\n */\nfunction parseDate(dateString: string): Date {\n  const date = new Date(dateString);\n  if (Number.isNaN(date.getTime())) {\n    throw invalidDateFormatError(dateString);\n  }\n  return date;\n}\n\n/**\n * Status report service implementation for ConnectRPC.\n */\nexport const statusReportServiceImpl: ServiceImpl<typeof StatusReportService> =\n  {\n    async createStatusReport(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      // Parse and validate the date before the transaction\n      const date = parseDate(req.date);\n\n      // Create status report, associations, and initial update in a transaction\n      const { report: newReport, newUpdate } = await db.transaction(\n        async (tx) => {\n          // Validate page component IDs inside transaction to prevent TOCTOU race condition\n          const validatedComponents = await validatePageComponentIds(\n            req.pageComponentIds,\n            workspaceId,\n            tx,\n          );\n\n          // Validate that provided pageId matches the components' page\n          const derivedPageId = validatedComponents.pageId;\n          const providedPageId = req.pageId?.trim();\n          if (\n            derivedPageId !== null &&\n            providedPageId &&\n            providedPageId !== \"\" &&\n            Number(providedPageId) !== derivedPageId\n          ) {\n            throw pageIdComponentMismatchError(\n              providedPageId,\n              String(derivedPageId),\n            );\n          }\n\n          // Use the derived pageId from components, or parse the provided one\n          const pageId =\n            derivedPageId ?? (providedPageId ? Number(providedPageId) : null);\n\n          // Create the status report\n          const report = await tx\n            .insert(statusReport)\n            .values({\n              workspaceId,\n              pageId,\n              title: req.title,\n              status: protoStatusToDb(req.status),\n            })\n            .returning()\n            .get();\n\n          if (!report) {\n            throw statusReportCreateFailedError();\n          }\n\n          // Create page component associations\n          await updatePageComponentAssociations(\n            report.id,\n            validatedComponents.componentIds,\n            tx,\n          );\n\n          // Create the initial update\n          const newUpdate = await tx\n            .insert(statusReportUpdate)\n            .values({\n              statusReportId: report.id,\n              status: protoStatusToDb(req.status),\n              date,\n              message: req.message,\n            })\n            .returning()\n            .get();\n\n          if (!newUpdate) {\n            throw statusReportCreateFailedError();\n          }\n\n          return { report, newUpdate, pageId };\n        },\n      );\n\n      // Send notifications if requested (outside transaction)\n      if (req.notify) {\n        await sendStatusReportNotification({\n          statusReportUpdateId: newUpdate.id,\n          limits: rpcCtx.workspace.limits,\n        });\n      }\n\n      // Fetch the updates for the response\n      const updates = await getUpdatesForReport(newReport.id);\n\n      return {\n        statusReport: dbReportToProto(newReport, req.pageComponentIds, updates),\n      };\n    },\n\n    async getStatusReport(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      if (!req.id || req.id.trim() === \"\") {\n        throw statusReportIdRequiredError();\n      }\n\n      const report = await getStatusReportById(Number(req.id), workspaceId);\n      if (!report) {\n        throw statusReportNotFoundError(req.id);\n      }\n\n      const pageComponentIds = await getPageComponentIdsForReport(report.id);\n      const updates = await getUpdatesForReport(report.id);\n\n      return {\n        statusReport: dbReportToProto(report, pageComponentIds, updates),\n      };\n    },\n\n    async listStatusReports(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      const limit = Math.min(Math.max(req.limit ?? 50, 1), 100);\n      const offset = req.offset ?? 0;\n\n      // Build conditions\n      const conditions = [eq(statusReport.workspaceId, workspaceId)];\n\n      // Add status filter if provided\n      if (req.statuses.length > 0) {\n        const dbStatuses = req.statuses\n          .filter((s) => s !== StatusReportStatus.UNSPECIFIED)\n          .map(protoStatusToDb);\n        if (dbStatuses.length > 0) {\n          conditions.push(inArray(statusReport.status, dbStatuses));\n        }\n      }\n\n      // Get total count\n      const countResult = await db\n        .select({ count: sql<number>`count(*)` })\n        .from(statusReport)\n        .where(and(...conditions))\n        .get();\n\n      const totalCount = countResult?.count ?? 0;\n\n      // Get status reports\n      const reports = await db\n        .select()\n        .from(statusReport)\n        .where(and(...conditions))\n        .orderBy(desc(statusReport.createdAt))\n        .limit(limit)\n        .offset(offset)\n        .all();\n\n      // Get page component IDs for each report\n      const statusReports = await Promise.all(\n        reports.map(async (report) => {\n          const pageComponentIds = await getPageComponentIdsForReport(\n            report.id,\n          );\n          return dbReportToProtoSummary(report, pageComponentIds);\n        }),\n      );\n\n      return {\n        statusReports,\n        totalSize: totalCount,\n      };\n    },\n\n    async updateStatusReport(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      if (!req.id || req.id.trim() === \"\") {\n        throw statusReportIdRequiredError();\n      }\n\n      const report = await getStatusReportById(Number(req.id), workspaceId);\n      if (!report) {\n        throw statusReportNotFoundError(req.id);\n      }\n\n      // Update report, associations in a transaction\n      const updatedReport = await db.transaction(async (tx) => {\n        // Validate page component IDs inside transaction to prevent TOCTOU race condition\n        // Allows empty array to clear associations; ensures all components belong to same page\n        const validatedComponents = await validatePageComponentIds(\n          req.pageComponentIds,\n          workspaceId,\n          tx,\n        );\n\n        // Build update values\n        const updateValues: Record<string, unknown> = {\n          updatedAt: new Date(),\n          // Set pageId from validated components (null if no components)\n          pageId: validatedComponents.pageId,\n        };\n\n        if (req.title !== undefined && req.title !== \"\") {\n          updateValues.title = req.title;\n        }\n\n        // Always update page component associations (empty array clears all)\n        await updatePageComponentAssociations(\n          report.id,\n          validatedComponents.componentIds,\n          tx,\n        );\n\n        // Update the report\n        const updated = await tx\n          .update(statusReport)\n          .set(updateValues)\n          .where(eq(statusReport.id, report.id))\n          .returning()\n          .get();\n\n        if (!updated) {\n          throw statusReportUpdateFailedError(req.id);\n        }\n\n        return updated;\n      });\n\n      // Fetch updated data\n      const pageComponentIds = await getPageComponentIdsForReport(\n        updatedReport.id,\n      );\n      const updates = await getUpdatesForReport(updatedReport.id);\n\n      return {\n        statusReport: dbReportToProto(updatedReport, pageComponentIds, updates),\n      };\n    },\n\n    async deleteStatusReport(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      if (!req.id || req.id.trim() === \"\") {\n        throw statusReportIdRequiredError();\n      }\n\n      const report = await getStatusReportById(Number(req.id), workspaceId);\n      if (!report) {\n        throw statusReportNotFoundError(req.id);\n      }\n\n      // Delete the status report (cascade will delete updates and associations)\n      await db.delete(statusReport).where(eq(statusReport.id, report.id));\n\n      return { success: true };\n    },\n\n    async addStatusReportUpdate(req, ctx) {\n      const rpcCtx = getRpcContext(ctx);\n      const workspaceId = rpcCtx.workspace.id;\n\n      if (!req.statusReportId || req.statusReportId.trim() === \"\") {\n        throw statusReportIdRequiredError();\n      }\n\n      const report = await getStatusReportById(\n        Number(req.statusReportId),\n        workspaceId,\n      );\n      if (!report) {\n        throw statusReportNotFoundError(req.statusReportId);\n      }\n\n      // Parse and validate the date or use current time\n      const date = req.date ? parseDate(req.date) : new Date();\n\n      // Create update and update status report in a transaction\n      const { updatedReport, newUpdate } = await db.transaction(async (tx) => {\n        // Create the update\n        const newUpdate = await tx\n          .insert(statusReportUpdate)\n          .values({\n            statusReportId: report.id,\n            status: protoStatusToDb(req.status),\n            date,\n            message: req.message,\n          })\n          .returning()\n          .get();\n\n        if (!newUpdate) {\n          throw statusReportUpdateFailedError(req.statusReportId);\n        }\n\n        // Update the status report's status and updatedAt\n        const updated = await tx\n          .update(statusReport)\n          .set({\n            status: protoStatusToDb(req.status),\n            updatedAt: new Date(),\n          })\n          .where(eq(statusReport.id, report.id))\n          .returning()\n          .get();\n\n        if (!updated) {\n          throw statusReportUpdateFailedError(req.statusReportId);\n        }\n\n        return { updatedReport: updated, newUpdate };\n      });\n\n      // Send notifications if requested (outside transaction)\n      if (req.notify && updatedReport.pageId) {\n        await sendStatusReportNotification({\n          statusReportUpdateId: newUpdate.id,\n          limits: rpcCtx.workspace.limits,\n        });\n      }\n\n      // Fetch all updates\n      const pageComponentIds = await getPageComponentIdsForReport(\n        updatedReport.id,\n      );\n      const updates = await getUpdatesForReport(updatedReport.id);\n\n      return {\n        statusReport: dbReportToProto(updatedReport, pageComponentIds, updates),\n      };\n    },\n  };\n"
  },
  {
    "path": "apps/server/src/routes/slack/agent.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\";\nimport { buildSystemPrompt } from \"./agent\";\n\ndescribe(\"buildSystemPrompt\", () => {\n  test(\"includes the current date and time in ISO 8601 format\", () => {\n    const before = new Date();\n    const prompt = buildSystemPrompt(\"My Workspace\");\n    const after = new Date();\n\n    const match = prompt.match(\n      /The current date and time is: (\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z) \\(UTC\\)/,\n    );\n    expect(match).not.toBeNull();\n\n    // biome-ignore lint/style/noNonNullAssertion: <explanation>\n    const promptDate = new Date(match![1]);\n    expect(promptDate.getTime()).toBeGreaterThanOrEqual(before.getTime());\n    expect(promptDate.getTime()).toBeLessThanOrEqual(after.getTime());\n  });\n\n  test(\"includes the workspace name\", () => {\n    const prompt = buildSystemPrompt(\"Acme Corp\");\n    expect(prompt).toContain('workspace \"Acme Corp\"');\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/agent.ts",
    "content": "import type { Workspace } from \"@openstatus/db/src/schema/workspaces/validation\";\nimport { generateText, stepCountIs } from \"ai\";\nimport type { ModelMessage } from \"ai\";\nimport { createTools } from \"./tools\";\n\ninterface SlackThreadMessage {\n  user?: string;\n  bot_id?: string;\n  text?: string;\n}\n\ninterface AgentResult {\n  text: string;\n  toolResults: Array<{ toolName: string; result: unknown }>;\n}\n\nexport function buildSystemPrompt(workspaceName: string): string {\n  const now = new Date().toISOString();\n  return `You are the OpenStatus assistant for workspace \"${workspaceName}\".\nThe current date and time is: ${now} (UTC).\nYou help teams create and manage status reports and maintenance windows through Slack.\n\nIMPORTANT: You have NO knowledge of this workspace's data. NEVER guess or make up IDs (page IDs, component IDs, report IDs). You MUST call the appropriate tool first to get real data.\n- Questions about pages or components -> call listStatusPages FIRST\n- Questions about reports -> call listStatusReports FIRST\n- Questions about maintenances -> call listMaintenances FIRST\n- Creating a report -> you MUST call listStatusPages first to get the real pageId, then call createStatusReport with that pageId\n- Scheduling maintenance -> you MUST call listStatusPages first to get the real pageId, then call createMaintenance with that pageId\n- NEVER pass a pageId you did not receive from listStatusPages. Guessing a pageId WILL cause an error.\n\nCapabilities:\n- Create status reports on status pages (createStatusReport)\n- Publish progress updates to existing reports (addStatusReportUpdate)\n- Edit report metadata like title or components (updateStatusReport)\n- List active status reports and status pages\n- Schedule maintenance windows (createMaintenance)\n- List upcoming maintenance windows (listMaintenances)\n\nLifecycle: createStatusReport once -> addStatusReportUpdate repeatedly -> resolved.\n- \"provide an update\", \"we found the cause\", \"resolve it\" -> addStatusReportUpdate\n- \"rename the report\", \"add a component\" -> updateStatusReport (metadata only)\n\nGuidelines:\n- If multiple status pages exist, ask which one to use. If only one, use it automatically.\n- Infer the status from conversation context:\n  \"we have an incident\" -> investigating\n  \"we found the root cause\" -> identified\n  \"we're watching it\" -> monitoring\n  \"it's fixed\" -> resolved\n- Draft professional status page updates. Don't repeat the user verbatim.\n- When tagged in a thread, synthesize the full thread into a status report draft.\n- Status progression: investigating -> identified -> monitoring -> resolved\n- Be concise. Use Slack mrkdwn formatting (*bold*, _italic_).\n- For any mutation, always call the tool so the user sees a confirmation.\n\nMaintenance scheduling:\n- Parse natural language dates into ISO 8601 format. Convert relative dates like \"next Friday from 2-3 PM\" into proper ISO 8601 timestamps.\n- If the user doesn't specify a timezone, default to UTC and mention that in your response.\n- The \"from\" time must be before the \"to\" time.\n- Write a professional maintenance message describing what will happen during the window.`;\n}\n\nfunction convertThreadToMessages(\n  thread: SlackThreadMessage[],\n  botUserId: string,\n): ModelMessage[] {\n  const messages: ModelMessage[] = [];\n  for (const msg of thread) {\n    if (!msg.text) continue;\n    if (msg.bot_id || msg.user === botUserId) {\n      messages.push({ role: \"assistant\", content: msg.text });\n    } else {\n      messages.push({ role: \"user\", content: msg.text });\n    }\n  }\n  // The API requires the first message to have role \"user\".\n  // Drop any leading assistant messages (e.g. bot confirmations from a prior turn).\n  while (messages.length > 0 && messages[0].role !== \"user\") {\n    messages.shift();\n  }\n  return messages;\n}\n\nexport async function runAgent(\n  workspace: Workspace,\n  thread: SlackThreadMessage[],\n  botUserId: string,\n  userText?: string,\n): Promise<AgentResult> {\n  const tools = createTools(workspace);\n  let messages = convertThreadToMessages(thread, botUserId);\n\n  if (messages.length === 0 && userText) {\n    messages = [{ role: \"user\" as const, content: userText }];\n  }\n\n  if (messages.length === 0) {\n    return {\n      text: \"I couldn't read your message. Please try again.\",\n      toolResults: [],\n    };\n  }\n\n  const result = await generateText({\n    model: \"anthropic/claude-sonnet-4.5\",\n    system: buildSystemPrompt(workspace.name ?? \"Unknown\"),\n    messages,\n    tools,\n    stopWhen: stepCountIs(5),\n  });\n\n  const toolResults: AgentResult[\"toolResults\"] = [];\n  for (const step of result.steps) {\n    for (const tc of step.toolResults) {\n      toolResults.push({ toolName: tc.toolName, result: tc.output });\n    }\n  }\n\n  return { text: result.text, toolResults };\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/blocks.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\";\nimport { buildConfirmationBlocks } from \"./blocks\";\nimport type { PendingAction } from \"./confirmation-store\";\n\ndescribe(\"buildConfirmationBlocks\", () => {\n  test(\"createStatusReport includes 3 buttons\", () => {\n    const action: PendingAction[\"action\"] = {\n      type: \"createStatusReport\",\n      params: {\n        title: \"API Outage\",\n        status: \"investigating\",\n        message: \"API is returning 500 errors\",\n        pageId: 1,\n      },\n    };\n\n    const blocks = buildConfirmationBlocks(\"abc123\", action);\n\n    const section = blocks.find((b) => b.type === \"section\");\n    expect(section).toBeDefined();\n    expect((section as { text: { text: string } }).text.text).toContain(\n      \"API Outage\",\n    );\n    expect((section as { text: { text: string } }).text.text).toContain(\n      \"Investigating\",\n    );\n\n    const actions = blocks.find((b) => b.type === \"actions\") as {\n      elements: { action_id: string }[];\n    };\n    expect(actions.elements).toHaveLength(3);\n    expect(actions.elements[0].action_id).toBe(\"approve_abc123\");\n    expect(actions.elements[1].action_id).toBe(\"approve_notify_abc123\");\n    expect(actions.elements[2].action_id).toBe(\"cancel_abc123\");\n  });\n\n  test(\"createStatusReport shows components when provided\", () => {\n    const action: PendingAction[\"action\"] = {\n      type: \"createStatusReport\",\n      params: {\n        title: \"Test\",\n        status: \"investigating\",\n        message: \"msg\",\n        pageId: 1,\n        pageComponentIds: [\"comp-1\", \"comp-2\"],\n      },\n    };\n\n    const blocks = buildConfirmationBlocks(\"id1\", action);\n    const section = blocks.find((b) => b.type === \"section\") as {\n      text: { text: string };\n    };\n    expect(section.text.text).toContain(\"comp-1, comp-2\");\n  });\n\n  test(\"addStatusReportUpdate includes 3 buttons\", () => {\n    const action: PendingAction[\"action\"] = {\n      type: \"addStatusReportUpdate\",\n      params: {\n        statusReportId: 42,\n        status: \"identified\",\n        message: \"Root cause found\",\n      },\n    };\n\n    const blocks = buildConfirmationBlocks(\"abc\", action);\n\n    const section = blocks.find((b) => b.type === \"section\") as {\n      text: { text: string };\n    };\n    expect(section.text.text).toContain(\"42\");\n    expect(section.text.text).toContain(\"Identified\");\n\n    const actions = blocks.find((b) => b.type === \"actions\") as {\n      elements: { action_id: string }[];\n    };\n    expect(actions.elements).toHaveLength(3);\n  });\n\n  test(\"updateStatusReport includes 2 buttons (no notify)\", () => {\n    const action: PendingAction[\"action\"] = {\n      type: \"updateStatusReport\",\n      params: {\n        statusReportId: 10,\n        title: \"Updated Title\",\n      },\n    };\n\n    const blocks = buildConfirmationBlocks(\"xyz\", action);\n\n    const section = blocks.find((b) => b.type === \"section\") as {\n      text: { text: string };\n    };\n    expect(section.text.text).toContain(\"Updated Title\");\n\n    const actions = blocks.find((b) => b.type === \"actions\") as {\n      elements: { action_id: string }[];\n    };\n    expect(actions.elements).toHaveLength(2);\n    expect(actions.elements[0].action_id).toBe(\"approve_xyz\");\n    expect(actions.elements[1].action_id).toBe(\"cancel_xyz\");\n  });\n\n  test(\"resolveStatusReport includes 3 buttons\", () => {\n    const action: PendingAction[\"action\"] = {\n      type: \"resolveStatusReport\",\n      params: {\n        statusReportId: 5,\n        message: \"Issue has been resolved\",\n      },\n    };\n\n    const blocks = buildConfirmationBlocks(\"res1\", action);\n\n    const section = blocks.find((b) => b.type === \"section\") as {\n      text: { text: string };\n    };\n    expect(section.text.text).toContain(\"5\");\n    expect(section.text.text).toContain(\"Issue has been resolved\");\n\n    const actions = blocks.find((b) => b.type === \"actions\") as {\n      elements: { action_id: string }[];\n    };\n    expect(actions.elements).toHaveLength(3);\n  });\n\n  test(\"all blocks include a divider\", () => {\n    const action: PendingAction[\"action\"] = {\n      type: \"createStatusReport\",\n      params: {\n        title: \"T\",\n        status: \"investigating\",\n        message: \"m\",\n        pageId: 1,\n      },\n    };\n\n    const blocks = buildConfirmationBlocks(\"d1\", action);\n    expect(blocks.some((b) => b.type === \"divider\")).toBe(true);\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/blocks.ts",
    "content": "import type { PendingAction } from \"./confirmation-store\";\n\ninterface TextObject {\n  type: \"plain_text\" | \"mrkdwn\";\n  text: string;\n  emoji?: boolean;\n}\n\ninterface SectionBlock {\n  type: \"section\";\n  text: TextObject;\n}\n\ninterface ActionsBlock {\n  type: \"actions\";\n  elements: ButtonElement[];\n}\n\ninterface DividerBlock {\n  type: \"divider\";\n}\n\ninterface ButtonElement {\n  type: \"button\";\n  text: TextObject;\n  action_id: string;\n  value?: string;\n  style?: \"primary\" | \"danger\";\n}\n\ntype Block = SectionBlock | ActionsBlock | DividerBlock;\n\nexport function buildConfirmationBlocks(\n  actionId: string,\n  action: PendingAction[\"action\"],\n): Block[] {\n  const blocks: Block[] = [];\n\n  switch (action.type) {\n    case \"createStatusReport\": {\n      const { title, status, message, pageId, pageComponentIds } =\n        action.params;\n      blocks.push({\n        type: \"section\",\n        text: {\n          type: \"mrkdwn\",\n          text: `*Create Status Report*\\n\\n*Title:* ${title}\\n*Status:* ${capitalize(status)}\\n*Page ID:* ${pageId}${\n            pageComponentIds?.length\n              ? `\\n*Components:* ${pageComponentIds.join(\", \")}`\n              : \"\"\n          }\\n*Message:* ${message}`,\n        },\n      });\n      blocks.push({ type: \"divider\" });\n      blocks.push({\n        type: \"actions\",\n        elements: [\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Approve\", emoji: true },\n            action_id: `approve_${actionId}`,\n            style: \"primary\",\n          },\n          {\n            type: \"button\",\n            text: {\n              type: \"plain_text\",\n              text: \"Approve & Notify\",\n              emoji: true,\n            },\n            action_id: `approve_notify_${actionId}`,\n            style: \"primary\",\n          },\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Cancel\", emoji: true },\n            action_id: `cancel_${actionId}`,\n            style: \"danger\",\n          },\n        ],\n      });\n      break;\n    }\n    case \"addStatusReportUpdate\": {\n      const { statusReportId, status, message } = action.params;\n      blocks.push({\n        type: \"section\",\n        text: {\n          type: \"mrkdwn\",\n          text: `*Add Status Report Update*\\n\\n*Report ID:* ${statusReportId}\\n*New Status:* ${capitalize(status)}\\n*Message:* ${message}`,\n        },\n      });\n      blocks.push({ type: \"divider\" });\n      blocks.push({\n        type: \"actions\",\n        elements: [\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Approve\", emoji: true },\n            action_id: `approve_${actionId}`,\n            style: \"primary\",\n          },\n          {\n            type: \"button\",\n            text: {\n              type: \"plain_text\",\n              text: \"Approve & Notify\",\n              emoji: true,\n            },\n            action_id: `approve_notify_${actionId}`,\n            style: \"primary\",\n          },\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Cancel\", emoji: true },\n            action_id: `cancel_${actionId}`,\n            style: \"danger\",\n          },\n        ],\n      });\n      break;\n    }\n    case \"updateStatusReport\": {\n      const { statusReportId, title, pageComponentIds } = action.params;\n      let text = `*Update Status Report*\\n\\n*Report ID:* ${statusReportId}`;\n      if (title) text += `\\n*New Title:* ${title}`;\n      if (pageComponentIds?.length)\n        text += `\\n*Components:* ${pageComponentIds.join(\", \")}`;\n      blocks.push({ type: \"section\", text: { type: \"mrkdwn\", text } });\n      blocks.push({ type: \"divider\" });\n      blocks.push({\n        type: \"actions\",\n        elements: [\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Approve\", emoji: true },\n            action_id: `approve_${actionId}`,\n            style: \"primary\",\n          },\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Cancel\", emoji: true },\n            action_id: `cancel_${actionId}`,\n            style: \"danger\",\n          },\n        ],\n      });\n      break;\n    }\n    case \"resolveStatusReport\": {\n      const { statusReportId, message } = action.params;\n      blocks.push({\n        type: \"section\",\n        text: {\n          type: \"mrkdwn\",\n          text: `*Resolve Status Report*\\n\\n*Report ID:* ${statusReportId}\\n*Message:* ${message}`,\n        },\n      });\n      blocks.push({ type: \"divider\" });\n      blocks.push({\n        type: \"actions\",\n        elements: [\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Approve\", emoji: true },\n            action_id: `approve_${actionId}`,\n            style: \"primary\",\n          },\n          {\n            type: \"button\",\n            text: {\n              type: \"plain_text\",\n              text: \"Approve & Notify\",\n              emoji: true,\n            },\n            action_id: `approve_notify_${actionId}`,\n            style: \"primary\",\n          },\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Cancel\", emoji: true },\n            action_id: `cancel_${actionId}`,\n            style: \"danger\",\n          },\n        ],\n      });\n      break;\n    }\n    case \"createMaintenance\": {\n      const { title, message, from, to, pageComponentIds } = action.params;\n      blocks.push({\n        type: \"section\",\n        text: {\n          type: \"mrkdwn\",\n          text: `*Schedule Maintenance*\\n\\n*Title:* ${title}\\n*From:* ${formatDate(from)}\\n*To:* ${formatDate(to)}${\n            pageComponentIds?.length\n              ? `\\n*Components:* ${pageComponentIds.join(\", \")}`\n              : \"\"\n          }\\n*Message:* ${message}`,\n        },\n      });\n      blocks.push({ type: \"divider\" });\n      blocks.push({\n        type: \"actions\",\n        elements: [\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Approve\", emoji: true },\n            action_id: `approve_${actionId}`,\n            style: \"primary\",\n          },\n          {\n            type: \"button\",\n            text: {\n              type: \"plain_text\",\n              text: \"Approve & Notify\",\n              emoji: true,\n            },\n            action_id: `approve_notify_${actionId}`,\n            style: \"primary\",\n          },\n          {\n            type: \"button\",\n            text: { type: \"plain_text\", text: \"Cancel\", emoji: true },\n            action_id: `cancel_${actionId}`,\n            style: \"danger\",\n          },\n        ],\n      });\n      break;\n    }\n  }\n\n  return blocks;\n}\n\nfunction formatDate(iso: string): string {\n  const d = new Date(iso);\n  if (Number.isNaN(d.getTime())) return iso;\n  return d.toLocaleString(\"en-US\", {\n    weekday: \"short\",\n    year: \"numeric\",\n    month: \"short\",\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"2-digit\",\n    timeZoneName: \"short\",\n  });\n}\n\nfunction capitalize(s: string): string {\n  return s.charAt(0).toUpperCase() + s.slice(1);\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/confirmation-store.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"bun:test\";\nimport {\n  consume,\n  findByThread,\n  get,\n  replace,\n  store,\n} from \"./confirmation-store\";\nimport type { PendingAction } from \"./confirmation-store\";\n\nconst redisStore = (globalThis as Record<string, unknown>)\n  .__testRedisStore as Map<string, string>;\n\nfunction makePendingInput(): Omit<PendingAction, \"id\" | \"createdAt\"> {\n  return {\n    workspaceId: 1,\n    limits: {},\n    botToken: \"xoxb-test-token\",\n    channelId: \"C123\",\n    threadTs: \"1234567890.123456\",\n    messageTs: \"1234567890.654321\",\n    userId: \"U123\",\n    action: {\n      type: \"createStatusReport\",\n      params: {\n        title: \"Test Incident\",\n        status: \"investigating\",\n        message: \"We are investigating\",\n        pageId: 1,\n      },\n    },\n  };\n}\n\ndescribe(\"confirmation-store\", () => {\n  beforeEach(() => {\n    redisStore.clear();\n  });\n\n  describe(\"store\", () => {\n    test(\"returns an action id\", async () => {\n      const id = await store(makePendingInput());\n      expect(typeof id).toBe(\"string\");\n      expect(id.length).toBeGreaterThan(0);\n    });\n\n    test(\"saves action and thread index to redis\", async () => {\n      const input = makePendingInput();\n      const id = await store(input);\n\n      const actionKey = `slack:action:${id}`;\n      const threadKey = `slack:thread:${input.threadTs}`;\n\n      expect(redisStore.has(actionKey)).toBe(true);\n      expect(redisStore.has(threadKey)).toBe(true);\n\n      const stored = JSON.parse(redisStore.get(actionKey) as string);\n      expect(stored.id).toBe(id);\n      expect(stored.workspaceId).toBe(1);\n      expect(stored.action.type).toBe(\"createStatusReport\");\n\n      expect(redisStore.get(threadKey)).toBe(id);\n    });\n  });\n\n  describe(\"get\", () => {\n    test(\"returns stored action without deleting it\", async () => {\n      const input = makePendingInput();\n      const id = await store(input);\n\n      const result = await get(id);\n      expect(result).toBeDefined();\n      expect(result?.id).toBe(id);\n      expect(result?.action.type).toBe(\"createStatusReport\");\n\n      // Keys should still exist\n      expect(redisStore.has(`slack:action:${id}`)).toBe(true);\n      expect(redisStore.has(`slack:thread:${input.threadTs}`)).toBe(true);\n    });\n\n    test(\"returns undefined for unknown id\", async () => {\n      const result = await get(\"nonexistent\");\n      expect(result).toBeUndefined();\n    });\n\n    test(\"returns undefined for invalid data in redis\", async () => {\n      redisStore.set(\"slack:action:bad\", JSON.stringify({ invalid: true }));\n\n      const result = await get(\"bad\");\n      expect(result).toBeUndefined();\n    });\n  });\n\n  describe(\"consume\", () => {\n    test(\"returns stored action and deletes it\", async () => {\n      const input = makePendingInput();\n      const id = await store(input);\n\n      const result = await consume(id);\n      expect(result).toBeDefined();\n      expect(result?.id).toBe(id);\n      expect(result?.action.type).toBe(\"createStatusReport\");\n\n      expect(redisStore.has(`slack:action:${id}`)).toBe(false);\n      expect(redisStore.has(`slack:thread:${input.threadTs}`)).toBe(false);\n    });\n\n    test(\"returns undefined for unknown id\", async () => {\n      const result = await consume(\"nonexistent\");\n      expect(result).toBeUndefined();\n    });\n\n    test(\"returns undefined for invalid data in redis\", async () => {\n      redisStore.set(\"slack:action:bad\", JSON.stringify({ invalid: true }));\n\n      const result = await consume(\"bad\");\n      expect(result).toBeUndefined();\n    });\n  });\n\n  describe(\"findByThread\", () => {\n    test(\"finds action by thread timestamp\", async () => {\n      const input = makePendingInput();\n      const id = await store(input);\n\n      const result = await findByThread(input.threadTs);\n      expect(result).toBeDefined();\n      expect(result?.id).toBe(id);\n    });\n\n    test(\"returns undefined for unknown thread\", async () => {\n      const result = await findByThread(\"unknown.thread\");\n      expect(result).toBeUndefined();\n    });\n\n    test(\"cleans up orphaned thread index\", async () => {\n      redisStore.set(\"slack:thread:orphan.ts\", \"missing-id\");\n\n      const result = await findByThread(\"orphan.ts\");\n      expect(result).toBeUndefined();\n      expect(redisStore.has(\"slack:thread:orphan.ts\")).toBe(false);\n    });\n  });\n\n  describe(\"replace\", () => {\n    test(\"replaces the action on an existing pending\", async () => {\n      const input = makePendingInput();\n      const id = await store(input);\n\n      const newAction: PendingAction[\"action\"] = {\n        type: \"addStatusReportUpdate\",\n        params: {\n          statusReportId: 42,\n          status: \"identified\",\n          message: \"Root cause found\",\n        },\n      };\n\n      await replace(id, newAction);\n\n      const result = await consume(id);\n      expect(result).toBeDefined();\n      expect(result?.action.type).toBe(\"addStatusReportUpdate\");\n      if (result?.action.type === \"addStatusReportUpdate\") {\n        expect(result?.action.params.statusReportId).toBe(42);\n      }\n    });\n\n    test(\"does nothing for unknown id\", async () => {\n      await replace(\"nonexistent\", {\n        type: \"resolveStatusReport\",\n        params: { statusReportId: 1, message: \"fixed\" },\n      });\n      expect(redisStore.size).toBe(0);\n    });\n  });\n\n  describe(\"zod validation\", () => {\n    test(\"validates all action types\", async () => {\n      const actions: PendingAction[\"action\"][] = [\n        {\n          type: \"createStatusReport\",\n          params: {\n            title: \"Test\",\n            status: \"investigating\",\n            message: \"msg\",\n            pageId: 1,\n          },\n        },\n        {\n          type: \"addStatusReportUpdate\",\n          params: {\n            statusReportId: 1,\n            status: \"identified\",\n            message: \"update\",\n          },\n        },\n        {\n          type: \"updateStatusReport\",\n          params: { statusReportId: 1, title: \"New Title\" },\n        },\n        {\n          type: \"resolveStatusReport\",\n          params: { statusReportId: 1, message: \"resolved\" },\n        },\n      ];\n\n      for (const action of actions) {\n        const input = { ...makePendingInput(), action };\n        const id = await store(input);\n        const result = await consume(id);\n        expect(result).toBeDefined();\n        expect(result?.action.type).toBe(action.type);\n      }\n    });\n\n    test(\"rejects invalid status values\", async () => {\n      const raw = JSON.stringify({\n        id: \"test\",\n        workspaceId: 1,\n        limits: makePendingInput().limits,\n        botToken: \"tok\",\n        channelId: \"C1\",\n        threadTs: \"1.1\",\n        messageTs: \"1.2\",\n        userId: \"U1\",\n        createdAt: Date.now(),\n        action: {\n          type: \"createStatusReport\",\n          params: {\n            title: \"T\",\n            status: \"invalid_status\",\n            message: \"m\",\n            pageId: 1,\n          },\n        },\n      });\n\n      redisStore.set(\"slack:action:bad-status\", raw);\n      const result = await consume(\"bad-status\");\n      expect(result).toBeUndefined();\n    });\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/confirmation-store.ts",
    "content": "import { redis } from \"@/libs/clients\";\nimport { limitsSchema } from \"@openstatus/db/src/schema/plan/schema\";\nimport { nanoid } from \"nanoid\";\nimport { z } from \"zod\";\n\nconst statusEnum = z.enum([\n  \"investigating\",\n  \"identified\",\n  \"monitoring\",\n  \"resolved\",\n]);\n\nconst createStatusReportActionSchema = z.object({\n  type: z.literal(\"createStatusReport\"),\n  params: z.object({\n    title: z.string(),\n    status: statusEnum,\n    message: z.string(),\n    pageId: z.number(),\n    pageComponentIds: z.array(z.string()).optional(),\n  }),\n});\n\nconst addStatusReportUpdateActionSchema = z.object({\n  type: z.literal(\"addStatusReportUpdate\"),\n  params: z.object({\n    statusReportId: z.number(),\n    status: statusEnum,\n    message: z.string(),\n  }),\n});\n\nconst updateStatusReportActionSchema = z.object({\n  type: z.literal(\"updateStatusReport\"),\n  params: z.object({\n    statusReportId: z.number(),\n    title: z.string().optional(),\n    pageComponentIds: z.array(z.string()).optional(),\n  }),\n});\n\nconst resolveStatusReportActionSchema = z.object({\n  type: z.literal(\"resolveStatusReport\"),\n  params: z.object({\n    statusReportId: z.number(),\n    message: z.string(),\n  }),\n});\n\nconst createMaintenanceActionSchema = z.object({\n  type: z.literal(\"createMaintenance\"),\n  params: z.object({\n    title: z.string(),\n    message: z.string(),\n    from: z.string(),\n    to: z.string(),\n    pageId: z.number(),\n    pageComponentIds: z.array(z.string()).optional(),\n  }),\n});\n\nconst actionSchema = z.discriminatedUnion(\"type\", [\n  createStatusReportActionSchema,\n  addStatusReportUpdateActionSchema,\n  updateStatusReportActionSchema,\n  resolveStatusReportActionSchema,\n  createMaintenanceActionSchema,\n]);\n\nconst pendingActionSchema = z.object({\n  id: z.string(),\n  workspaceId: z.number(),\n  limits: limitsSchema,\n  botToken: z.string(),\n  channelId: z.string(),\n  threadTs: z.string(),\n  messageTs: z.string(),\n  userId: z.string(),\n  createdAt: z.number(),\n  action: actionSchema,\n});\n\nexport type PendingAction = z.infer<typeof pendingActionSchema>;\n\nconst TTL_SECONDS = 5 * 60;\nconst ACTION_PREFIX = \"slack:action:\";\nconst THREAD_PREFIX = \"slack:thread:\";\n\nfunction parse(raw: unknown): PendingAction | undefined {\n  const data = typeof raw === \"string\" ? JSON.parse(raw) : raw;\n  const result = pendingActionSchema.safeParse(data);\n  if (!result.success) {\n    console.error(\"[slack confirmation-store] invalid data:\", result.error);\n    return undefined;\n  }\n  return result.data;\n}\n\nexport async function store(\n  action: Omit<PendingAction, \"id\" | \"createdAt\">,\n): Promise<string> {\n  const id = nanoid();\n  const pending: PendingAction = { ...action, id, createdAt: Date.now() };\n\n  await Promise.all([\n    redis.set(`${ACTION_PREFIX}${id}`, JSON.stringify(pending), {\n      ex: TTL_SECONDS,\n    }),\n    redis.set(`${THREAD_PREFIX}${action.threadTs}`, id, {\n      ex: TTL_SECONDS,\n    }),\n  ]);\n\n  return id;\n}\n\nexport async function get(\n  actionId: string,\n): Promise<PendingAction | undefined> {\n  const raw = await redis.get<string>(`${ACTION_PREFIX}${actionId}`);\n  if (!raw) return undefined;\n  return parse(raw);\n}\n\nexport async function consume(\n  actionId: string,\n): Promise<PendingAction | undefined> {\n  // Atomic read+delete to prevent double execution from concurrent requests\n  const raw = await redis.getdel<string>(`${ACTION_PREFIX}${actionId}`);\n  if (!raw) return undefined;\n\n  const action = parse(raw);\n  if (!action) return undefined;\n\n  // Clean up the thread mapping (best-effort, not critical for atomicity)\n  await redis.del(`${THREAD_PREFIX}${action.threadTs}`);\n\n  return action;\n}\n\nexport async function findByThread(\n  threadTs: string,\n): Promise<PendingAction | undefined> {\n  const actionId = await redis.get<string>(`${THREAD_PREFIX}${threadTs}`);\n  if (!actionId) return undefined;\n\n  const raw = await redis.get<string>(`${ACTION_PREFIX}${actionId}`);\n  if (!raw) {\n    await redis.del(`${THREAD_PREFIX}${threadTs}`);\n    return undefined;\n  }\n\n  return parse(raw);\n}\n\nexport async function replace(\n  actionId: string,\n  newAction: PendingAction[\"action\"],\n): Promise<void> {\n  const raw = await redis.get<string>(`${ACTION_PREFIX}${actionId}`);\n  if (!raw) return;\n\n  const existing = parse(raw);\n  if (!existing) return;\n\n  existing.action = newAction;\n  existing.createdAt = Date.now();\n\n  await Promise.all([\n    redis.set(`${ACTION_PREFIX}${actionId}`, JSON.stringify(existing), {\n      ex: TTL_SECONDS,\n    }),\n    redis.expire(`${THREAD_PREFIX}${existing.threadTs}`, TTL_SECONDS),\n  ]);\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/handler.test.ts",
    "content": "import { beforeEach, describe, expect, mock, test } from \"bun:test\";\nimport crypto from \"node:crypto\";\nimport { Hono } from \"hono\";\n\nconst SIGNING_SECRET = \"test-signing-secret\";\n\nprocess.env.SLACK_SIGNING_SECRET = SIGNING_SECRET;\nprocess.env.AI_GATEWAY_API_KEY = \"test-key\";\n\nmock.module(\"./workspace-resolver\", () => ({\n  resolveWorkspace: (teamId: string) => {\n    if (teamId === \"T_KNOWN\") {\n      return Promise.resolve({\n        workspace: {\n          id: 1,\n          name: \"Test Workspace\",\n          slug: \"test\",\n          plan: \"free\",\n          limits: {},\n        },\n        botToken: \"xoxb-test\",\n        botUserId: \"UBOT\",\n      });\n    }\n    return Promise.resolve(null);\n  },\n}));\n\nlet slackMessages: Array<{ method: string; args: Record<string, unknown> }> =\n  [];\nlet postMessageOverride:\n  | ((args: Record<string, unknown>) => Promise<{ ts: string }>)\n  | null = null;\nlet updateOverride:\n  | ((args: Record<string, unknown>) => Promise<{ ts: string }>)\n  | null = null;\n\nmock.module(\"@slack/web-api\", () => ({\n  WebClient: class {\n    chat = {\n      postMessage: (args: Record<string, unknown>) => {\n        if (postMessageOverride) return postMessageOverride(args);\n        slackMessages.push({ method: \"postMessage\", args });\n        return Promise.resolve({ ts: \"msg.ts\" });\n      },\n      update: (args: Record<string, unknown>) => {\n        if (updateOverride) return updateOverride(args);\n        slackMessages.push({ method: \"update\", args });\n        return Promise.resolve({ ts: \"msg.ts\" });\n      },\n    };\n    conversations = {\n      replies: () =>\n        Promise.resolve({\n          messages: [{ user: \"U1\", text: \"test message\", ts: \"1.1\" }],\n        }),\n    };\n  },\n}));\n\nlet runAgentOverride:\n  | (() => Promise<{ text: string; toolResults: never[] }>)\n  | null = null;\n\nmock.module(\"./agent\", () => ({\n  runAgent: () => {\n    if (runAgentOverride) return runAgentOverride();\n    return Promise.resolve({\n      text: \"Here is my response\",\n      toolResults: [],\n    });\n  },\n}));\n\nconst { handleSlackEvent } = await import(\"./handler\");\nconst { verifySlackSignature } = await import(\"./verify\");\n\nfunction createTestApp() {\n  const app = new Hono<{ Variables: { slackBody: unknown } }>();\n  app.post(\"/slack/events\", verifySlackSignature, handleSlackEvent);\n  return app;\n}\n\nfunction signAndPost(\n  app: ReturnType<typeof createTestApp>,\n  body: Record<string, unknown>,\n) {\n  const rawBody = JSON.stringify(body);\n  const timestamp = Math.floor(Date.now() / 1000);\n  const basestring = `v0:${timestamp}:${rawBody}`;\n  const sig = crypto\n    .createHmac(\"sha256\", SIGNING_SECRET)\n    .update(basestring)\n    .digest(\"hex\");\n\n  return app.request(\"/slack/events\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      \"x-slack-request-timestamp\": String(timestamp),\n      \"x-slack-signature\": `v0=${sig}`,\n    },\n    body: rawBody,\n  });\n}\n\ndescribe(\"handleSlackEvent\", () => {\n  const app = createTestApp();\n\n  beforeEach(() => {\n    slackMessages = [];\n    postMessageOverride = null;\n    updateOverride = null;\n    runAgentOverride = null;\n  });\n\n  test(\"responds to url_verification challenge\", async () => {\n    const res = await signAndPost(app, {\n      type: \"url_verification\",\n      challenge: \"test-challenge-123\",\n    });\n\n    expect(res.status).toBe(200);\n    const json = (await res.json()) as { challenge: string };\n    expect(json.challenge).toBe(\"test-challenge-123\");\n  });\n\n  test(\"returns ok for non-event_callback types\", async () => {\n    const res = await signAndPost(app, {\n      type: \"app_rate_limited\",\n    });\n\n    expect(res.status).toBe(200);\n    const json = (await res.json()) as { ok: boolean };\n    expect(json.ok).toBe(true);\n  });\n\n  test(\"returns ok for event_callback\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_${Date.now()}_1`,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> create an incident\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: \"100.1\",\n      },\n    });\n\n    expect(res.status).toBe(200);\n    const json = (await res.json()) as { ok: boolean };\n    expect(json.ok).toBe(true);\n  });\n\n  test(\"handles app_uninstalled event\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_uninstall_${Date.now()}`,\n      event: {\n        type: \"app_uninstalled\",\n      },\n    });\n\n    expect(res.status).toBe(200);\n  });\n\n  test(\"handles tokens_revoked event\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_revoked_${Date.now()}`,\n      event: {\n        type: \"tokens_revoked\",\n      },\n    });\n\n    expect(res.status).toBe(200);\n  });\n\n  test(\"deduplicates events with same event_id\", async () => {\n    const eventId = `evt_dedup_${Date.now()}`;\n    const body = {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: eventId,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> hello\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: `${Date.now()}.1`,\n      },\n    };\n\n    await signAndPost(app, body);\n    await new Promise((r) => setTimeout(r, 50));\n\n    slackMessages = [];\n    await signAndPost(app, body);\n    await new Promise((r) => setTimeout(r, 50));\n\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"ignores events from unknown teams\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_UNKNOWN\",\n      event_id: `evt_unknown_${Date.now()}`,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> hello\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: `${Date.now()}.2`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"ignores message events from bots\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_bot_${Date.now()}`,\n      event: {\n        type: \"message\",\n        text: \"bot message\",\n        bot_id: \"B123\",\n        channel: \"C1\",\n        ts: `${Date.now()}.3`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"ignores channel message without bot mention\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_nomention_${Date.now()}`,\n      event: {\n        type: \"message\",\n        text: \"just a regular message\",\n        user: \"U1\",\n        channel: \"C1\",\n        channel_type: \"channel\",\n        ts: `${Date.now()}.4`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"processes DM messages without bot mention\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_dm_${Date.now()}`,\n      event: {\n        type: \"message\",\n        text: \"hello in DM\",\n        user: \"U1\",\n        channel: \"D1\",\n        channel_type: \"im\",\n        ts: `${Date.now()}.5`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    // DM should trigger a response (postMessage for \"Thinking...\")\n    expect(slackMessages.length).toBeGreaterThan(0);\n  });\n\n  test(\"ignores events without channel\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_nochan_${Date.now()}`,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> hello\",\n        user: \"U1\",\n        ts: `${Date.now()}.6`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"ignores events without timestamp\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_nots_${Date.now()}`,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> hello\",\n        user: \"U1\",\n        channel: \"C1\",\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"ignores events without team_id\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      event_id: `evt_noteam_${Date.now()}`,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> hello\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: `${Date.now()}.7`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"ignores unsupported event types\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_unsupported_${Date.now()}`,\n      event: {\n        type: \"channel_created\",\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"ignores channel_join system messages\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_join_${Date.now()}`,\n      event: {\n        type: \"message\",\n        subtype: \"channel_join\",\n        text: \"<@U1> has joined the channel\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: `${Date.now()}.10`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"ignores channel_leave system messages\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_leave_${Date.now()}`,\n      event: {\n        type: \"message\",\n        subtype: \"channel_leave\",\n        text: \"<@U1> has left the channel\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: `${Date.now()}.11`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"ignores events with no event payload\", async () => {\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_noevent_${Date.now()}`,\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 50));\n    expect(slackMessages.length).toBe(0);\n  });\n\n  test(\"falls back to top-level message on cannot_reply_to_message\", async () => {\n    let callCount = 0;\n    postMessageOverride = (args: Record<string, unknown>) => {\n      callCount++;\n      if (callCount === 1) {\n        const err = new Error(\"An API error occurred: cannot_reply_to_message\");\n        Object.assign(err, {\n          code: \"slack_webapi_platform_error\",\n          data: { ok: false, error: \"cannot_reply_to_message\" },\n        });\n        return Promise.reject(err);\n      }\n      slackMessages.push({ method: \"postMessage\", args });\n      return Promise.resolve({ ts: \"fallback.ts\" });\n    };\n\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_cantreply_${Date.now()}`,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> hello\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: `${Date.now()}.20`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 100));\n\n    const fallbackPost = slackMessages.find(\n      (m) => m.method === \"postMessage\" && !m.args.thread_ts,\n    );\n    expect(fallbackPost).toBeDefined();\n  });\n\n  test(\"returns early on non-recoverable postMessage error\", async () => {\n    postMessageOverride = () => {\n      const err = new Error(\"An API error occurred: channel_not_found\");\n      Object.assign(err, {\n        code: \"slack_webapi_platform_error\",\n        data: { ok: false, error: \"channel_not_found\" },\n      });\n      return Promise.reject(err);\n    };\n\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_channotfound_${Date.now()}`,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> hello\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: `${Date.now()}.21`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 100));\n\n    const updateMessages = slackMessages.filter((m) => m.method === \"update\");\n    expect(updateMessages.length).toBe(0);\n  });\n\n  test(\"shows error message when runAgent throws\", async () => {\n    runAgentOverride = () => Promise.reject(new Error(\"agent exploded\"));\n\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_agenterr_${Date.now()}`,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> hello\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: `${Date.now()}.30`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 100));\n\n    const errorUpdate = slackMessages.find(\n      (m) =>\n        m.method === \"update\" &&\n        typeof m.args.text === \"string\" &&\n        m.args.text.includes(\"Something went wrong\"),\n    );\n    expect(errorUpdate).toBeDefined();\n  });\n\n  test(\"does not throw when both runAgent and error update fail\", async () => {\n    runAgentOverride = () => Promise.reject(new Error(\"agent exploded\"));\n    updateOverride = () => {\n      const err = new Error(\"An API error occurred: channel_not_found\");\n      Object.assign(err, {\n        code: \"slack_webapi_platform_error\",\n        data: { ok: false, error: \"channel_not_found\" },\n      });\n      return Promise.reject(err);\n    };\n\n    const res = await signAndPost(app, {\n      type: \"event_callback\",\n      team_id: \"T_KNOWN\",\n      event_id: `evt_doublefail_${Date.now()}`,\n      event: {\n        type: \"app_mention\",\n        text: \"<@UBOT> hello\",\n        user: \"U1\",\n        channel: \"C1\",\n        ts: `${Date.now()}.31`,\n      },\n    });\n\n    expect(res.status).toBe(200);\n    await new Promise((r) => setTimeout(r, 100));\n    // No unhandled rejection — the .catch() in the error handler swallows it\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/handler.ts",
    "content": "import { getLogger } from \"@logtape/logtape\";\nimport { and, db, eq } from \"@openstatus/db\";\nimport { integration } from \"@openstatus/db/src/schema\";\nimport { WebClient } from \"@slack/web-api\";\nimport type { Context } from \"hono\";\nimport { z } from \"zod\";\nimport { runAgent } from \"./agent\";\nimport { buildConfirmationBlocks } from \"./blocks\";\nimport { findByThread, replace, store } from \"./confirmation-store\";\nimport type { PendingAction } from \"./confirmation-store\";\nimport { resolveWorkspace } from \"./workspace-resolver\";\n\nconst logger = getLogger(\"api-server\");\n\nconst processedEvents = new Map<string, number>();\n\nfunction dedup(eventId: string): boolean {\n  const now = Date.now();\n  for (const [id, ts] of processedEvents) {\n    if (now - ts > 300_000) processedEvents.delete(id);\n  }\n  if (processedEvents.has(eventId)) return true;\n  processedEvents.set(eventId, now);\n  return false;\n}\n\nconst slackEventSchema = z.object({\n  type: z.string(),\n  event: z\n    .object({\n      type: z.string(),\n      subtype: z.string().optional(),\n      text: z.string().optional(),\n      user: z.string().optional(),\n      channel: z.string().optional(),\n      channel_type: z.string().optional(),\n      ts: z.string().optional(),\n      thread_ts: z.string().optional(),\n      bot_id: z.string().optional(),\n    })\n    .optional(),\n  event_id: z.string().optional(),\n  team_id: z.string().optional(),\n  challenge: z.string().optional(),\n});\n\ntype SlackEvent = z.infer<typeof slackEventSchema>;\n\nconst threadMessageSchema = z.object({\n  user: z.string().optional(),\n  bot_id: z.string().optional(),\n  text: z.string().optional(),\n  ts: z.string().optional(),\n});\n\ntype ThreadMessage = z.infer<typeof threadMessageSchema>;\n\nconst slackPlatformErrorSchema = z.object({\n  code: z.literal(\"slack_webapi_platform_error\"),\n  data: z.object({\n    error: z.string(),\n  }),\n});\n\nfunction isSlackPlatformError(err: unknown, errorCode: string): boolean {\n  const parsed = slackPlatformErrorSchema.safeParse(err);\n  return parsed.success && parsed.data.data.error === errorCode;\n}\n\nexport async function handleSlackEvent(c: Context) {\n  const body = c.get(\"slackBody\") as SlackEvent;\n\n  if (body.type === \"url_verification\") {\n    return c.json({ challenge: body.challenge });\n  }\n\n  if (body.type !== \"event_callback\") {\n    return c.json({ ok: true });\n  }\n\n  if (body.event_id && dedup(body.event_id)) {\n    return c.json({ ok: true });\n  }\n\n  const promise = processEvent(body);\n  promise.catch((err) =>\n    logger.error(\"slack event processing error\", {\n      error: err,\n      teamId: body.team_id,\n      eventId: body.event_id,\n    }),\n  );\n\n  return c.json({ ok: true });\n}\n\nasync function processEvent(body: SlackEvent) {\n  const event = body.event;\n  if (!event) return;\n\n  if (event.type === \"app_uninstalled\" || event.type === \"tokens_revoked\") {\n    const teamId = body.team_id;\n    if (teamId) {\n      await db\n        .delete(integration)\n        .where(\n          and(\n            eq(integration.name, \"slack-agent\"),\n            eq(integration.externalId, teamId),\n          ),\n        );\n      logger.info(\"slack integration cleaned up\", { teamId });\n    }\n    return;\n  }\n\n  if (event.type !== \"app_mention\" && event.type !== \"message\") return;\n  if (event.type === \"message\" && event.bot_id) return;\n\n  const ignoredSubtypes = [\n    \"channel_join\",\n    \"channel_leave\",\n    \"channel_topic\",\n    \"channel_purpose\",\n    \"channel_name\",\n  ];\n  if (event.subtype && ignoredSubtypes.includes(event.subtype)) return;\n\n  const teamId = body.team_id;\n  if (!teamId || !event.channel || !event.ts) return;\n\n  const resolved = await resolveWorkspace(teamId);\n  if (!resolved) {\n    logger.warn(\"slack integration not found\", { teamId });\n    return;\n  }\n\n  const slack = new WebClient(resolved.botToken);\n  const botUserId = resolved.botUserId;\n  const threadTs = event.thread_ts ?? event.ts;\n\n  if (event.type === \"message\" && event.channel_type !== \"im\") {\n    if (!event.text?.includes(`<@${botUserId}>`)) return;\n  }\n\n  logger.info(\"slack event received\", {\n    teamId,\n    channel: event.channel,\n    eventType: event.type,\n    threadTs,\n    user: event.user,\n  });\n\n  let thinkingTs: string | undefined;\n  try {\n    const thinkingMsg = await slack.chat.postMessage({\n      channel: event.channel,\n      thread_ts: threadTs,\n      text: \":hourglass_flowing_sand: Thinking...\",\n    });\n    thinkingTs = thinkingMsg.ts;\n  } catch (err) {\n    if (isSlackPlatformError(err, \"cannot_reply_to_message\")) {\n      logger.warn(\"slack cannot reply to message, falling back to top-level\", {\n        channel: event.channel,\n        teamId,\n        threadTs,\n      });\n      try {\n        const fallbackMsg = await slack.chat.postMessage({\n          channel: event.channel,\n          text: \":hourglass_flowing_sand: Thinking...\",\n        });\n        thinkingTs = fallbackMsg.ts;\n      } catch (fallbackErr) {\n        logger.error(\"slack failed to post fallback thinking message\", {\n          error: fallbackErr,\n          channel: event.channel,\n          teamId,\n        });\n        return;\n      }\n    } else {\n      logger.error(\"slack failed to post thinking message\", {\n        error: err,\n        channel: event.channel,\n        teamId,\n        threadTs,\n      });\n      return;\n    }\n  }\n\n  if (!thinkingTs) {\n    logger.error(\"slack thinking message returned no ts\", {\n      channel: event.channel,\n      teamId,\n    });\n    return;\n  }\n\n  try {\n    let thread: ThreadMessage[] = [];\n    if (event.thread_ts) {\n      const replies = await slack.conversations.replies({\n        channel: event.channel,\n        ts: event.thread_ts,\n        limit: 100,\n      });\n      thread = ((replies.messages ?? []) as ThreadMessage[]).filter(\n        (msg) => msg.ts !== thinkingTs,\n      );\n    } else {\n      thread = [{ user: event.user, text: event.text, ts: event.ts }];\n    }\n\n    logger.info(\"slack agent invoked\", {\n      teamId,\n      channel: event.channel,\n      threadTs,\n      messageCount: thread.length,\n    });\n\n    const result = await runAgent(\n      resolved.workspace,\n      thread,\n      botUserId,\n      event.text,\n    );\n\n    logger.info(\"slack agent completed\", {\n      teamId,\n      channel: event.channel,\n      threadTs,\n      toolCalls: result.toolResults.map((tr) => tr.toolName),\n    });\n\n    const confirmationResult = result.toolResults.find(\n      (tr) =>\n        tr.result &&\n        typeof tr.result === \"object\" &&\n        \"needsConfirmation\" in tr.result &&\n        (tr.result as { needsConfirmation: boolean }).needsConfirmation,\n    );\n\n    if (confirmationResult) {\n      logger.info(\"slack confirmation requested\", {\n        teamId,\n        channel: event.channel,\n        threadTs,\n        toolName: confirmationResult.toolName,\n      });\n      await handleConfirmation(\n        slack,\n        event.channel,\n        threadTs,\n        thinkingTs,\n        event.user ?? \"\",\n        resolved.workspace,\n        resolved.botToken,\n        confirmationResult,\n      );\n    } else {\n      await slack.chat.update({\n        channel: event.channel,\n        ts: thinkingTs,\n        text: result.text || \"Done!\",\n      });\n      logger.info(\"slack response sent\", {\n        teamId,\n        channel: event.channel,\n        threadTs,\n      });\n    }\n  } catch (err) {\n    logger.error(\"slack agent error\", {\n      error: err,\n      channel: event.channel,\n      teamId,\n      threadTs,\n    });\n    if (thinkingTs) {\n      await slack.chat\n        .update({\n          channel: event.channel,\n          ts: thinkingTs,\n          text: \":x: Something went wrong. Please try again.\",\n        })\n        .catch((updateErr: unknown) => {\n          logger.error(\"slack failed to update error message\", {\n            error: updateErr,\n            channel: event.channel,\n            thinkingTs,\n          });\n        });\n    }\n  }\n}\n\nasync function handleConfirmation(\n  slack: WebClient,\n  channel: string,\n  threadTs: string,\n  thinkingTs: string,\n  userId: string,\n  workspace: { id: number; limits: PendingAction[\"limits\"] },\n  botToken: string,\n  confirmationResult: { toolName: string; result: unknown },\n) {\n  const { params } = confirmationResult.result as {\n    needsConfirmation: boolean;\n    params: Record<string, unknown>;\n  };\n\n  const actionType =\n    confirmationResult.toolName as PendingAction[\"action\"][\"type\"];\n  const action = { type: actionType, params } as PendingAction[\"action\"];\n\n  const existing = await findByThread(threadTs);\n  if (existing) {\n    await replace(existing.id, action);\n\n    const blocks = buildConfirmationBlocks(existing.id, action);\n    await slack.chat.update({\n      channel,\n      ts: thinkingTs,\n      text: getConfirmationText(action),\n      blocks,\n    });\n    await slack.chat.update({\n      channel,\n      ts: existing.messageTs,\n      text: getConfirmationText(action),\n      blocks,\n    });\n  } else {\n    const actionId = await store({\n      workspaceId: workspace.id,\n      limits: workspace.limits,\n      botToken,\n      channelId: channel,\n      threadTs,\n      messageTs: thinkingTs,\n      userId,\n      action,\n    });\n\n    const blocks = buildConfirmationBlocks(actionId, action);\n    await slack.chat.update({\n      channel,\n      ts: thinkingTs,\n      text: getConfirmationText(action),\n      blocks,\n    });\n  }\n}\n\nfunction getConfirmationText(action: PendingAction[\"action\"]): string {\n  switch (action.type) {\n    case \"createStatusReport\":\n      return `Create Status Report: ${action.params.title}`;\n    case \"addStatusReportUpdate\":\n      return `Add Status Report Update (${action.params.status})`;\n    case \"updateStatusReport\":\n      return `Update Status Report${action.params.title ? `: ${action.params.title}` : \"\"}`;\n    case \"resolveStatusReport\":\n      return \"Resolve Status Report\";\n    case \"createMaintenance\":\n      return `Schedule Maintenance: ${action.params.title}`;\n  }\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/index.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"bun:test\";\nimport crypto from \"node:crypto\";\nimport { Hono } from \"hono\";\n\nconst SIGNING_SECRET = \"test-signing-secret\";\n\nfunction signRequest(body: string, timestamp: number): string {\n  const basestring = `v0:${timestamp}:${body}`;\n  const hmac = crypto\n    .createHmac(\"sha256\", SIGNING_SECRET)\n    .update(basestring)\n    .digest(\"hex\");\n  return `v0=${hmac}`;\n}\n\nfunction makeInstallToken(workspaceId: number): string {\n  const payload = JSON.stringify({ workspaceId, ts: Date.now() });\n  const sig = crypto\n    .createHmac(\"sha256\", SIGNING_SECRET)\n    .update(payload)\n    .digest(\"hex\");\n  return Buffer.from(`${payload}.${sig}`).toString(\"base64url\");\n}\n\ndescribe(\"slack route middleware\", () => {\n  beforeEach(() => {\n    process.env.SLACK_SIGNING_SECRET = SIGNING_SECRET;\n    process.env.AI_GATEWAY_API_KEY = \"test-key\";\n    process.env.SLACK_CLIENT_ID = \"test-client-id\";\n  });\n\n  test(\"returns 503 when SLACK_SIGNING_SECRET is missing\", async () => {\n    process.env.SLACK_SIGNING_SECRET = \"\";\n\n    const { slackRoute } = await import(\"./index\");\n    const app = new Hono();\n    app.route(\"/slack\", slackRoute);\n\n    const res = await app.request(\"/slack/install?token=invalid\");\n\n    expect(res.status).toBe(503);\n    const json = (await res.json()) as { error: string };\n    expect(json.error).toBe(\"Slack agent not configured\");\n  });\n\n  test(\"returns 503 when AI_GATEWAY_API_KEY is missing\", async () => {\n    process.env.AI_GATEWAY_API_KEY = \"\";\n\n    const { slackRoute } = await import(\"./index\");\n    const app = new Hono();\n    app.route(\"/slack\", slackRoute);\n\n    const res = await app.request(\"/slack/install?token=invalid\");\n\n    expect(res.status).toBe(503);\n  });\n\n  test(\"GET /install is accessible with valid token\", async () => {\n    const { slackRoute } = await import(\"./index\");\n    const app = new Hono();\n    app.route(\"/slack\", slackRoute);\n\n    const token = makeInstallToken(1);\n    const res = await app.request(`/slack/install?token=${token}`);\n    // Should redirect (302) to Slack OAuth, not 404\n    expect(res.status).toBe(302);\n  });\n\n  test(\"GET /install rejects invalid token\", async () => {\n    const { slackRoute } = await import(\"./index\");\n    const app = new Hono();\n    app.route(\"/slack\", slackRoute);\n\n    const res = await app.request(\"/slack/install?token=invalid\");\n    expect(res.status).toBe(403);\n  });\n\n  test(\"POST /events requires signature verification\", async () => {\n    const { slackRoute } = await import(\"./index\");\n    const app = new Hono();\n    app.route(\"/slack\", slackRoute);\n\n    // POST without signature headers should fail\n    const res = await app.request(\"/slack/events\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ type: \"url_verification\", challenge: \"test\" }),\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"POST /events accepts valid signed request\", async () => {\n    const { slackRoute } = await import(\"./index\");\n    const app = new Hono();\n    app.route(\"/slack\", slackRoute);\n\n    const body = JSON.stringify({\n      type: \"url_verification\",\n      challenge: \"test-challenge\",\n    });\n    const timestamp = Math.floor(Date.now() / 1000);\n    const signature = signRequest(body, timestamp);\n\n    const res = await app.request(\"/slack/events\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": signature,\n      },\n      body,\n    });\n\n    expect(res.status).toBe(200);\n    const json = (await res.json()) as { challenge: string };\n    expect(json.challenge).toBe(\"test-challenge\");\n  });\n\n  test(\"POST /interactions requires signature verification\", async () => {\n    const { slackRoute } = await import(\"./index\");\n    const app = new Hono();\n    app.route(\"/slack\", slackRoute);\n\n    const res = await app.request(\"/slack/interactions\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n      body: \"payload={}\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/index.ts",
    "content": "import { env } from \"@/env\";\nimport { Hono } from \"hono\";\nimport { handleSlackEvent } from \"./handler\";\nimport { handleSlackInteraction } from \"./interactions\";\nimport { handleSlackInstall, handleSlackOAuthCallback } from \"./oauth\";\nimport { verifySlackSignature } from \"./verify\";\n\ntype SlackEnv = {\n  Variables: {\n    slackBody: unknown;\n    event: Record<string, unknown>;\n  };\n};\n\nconst slack = new Hono<SlackEnv>();\n\nslack.use(\"*\", async (c, next) => {\n  if (!env.SLACK_SIGNING_SECRET || !env.AI_GATEWAY_API_KEY) {\n    return c.json({ error: \"Slack agent not configured\" }, 503);\n  }\n  await next();\n});\n\nslack.get(\"/install\", handleSlackInstall);\nslack.get(\"/oauth/callback\", handleSlackOAuthCallback);\n\nslack.post(\"/events\", verifySlackSignature, handleSlackEvent);\nslack.post(\"/interactions\", verifySlackSignature, handleSlackInteraction);\n\nexport { slack as slackRoute };\n"
  },
  {
    "path": "apps/server/src/routes/slack/interactions.test.ts",
    "content": "import { beforeEach, describe, expect, mock, test } from \"bun:test\";\nimport crypto from \"node:crypto\";\nimport { Hono } from \"hono\";\n\nconst SIGNING_SECRET = \"test-signing-secret\";\n\nprocess.env.SLACK_SIGNING_SECRET = SIGNING_SECRET;\nprocess.env.AI_GATEWAY_API_KEY = \"test-key\";\n\nconst redisStore = (globalThis as Record<string, unknown>)\n  .__testRedisStore as Map<string, string>;\n\nconst pendingData = {\n  id: \"pending-123\",\n  workspaceId: 1,\n  limits: {},\n  botToken: \"xoxb-test\",\n  channelId: \"C1\",\n  threadTs: \"1.1\",\n  messageTs: \"1.2\",\n  userId: \"U_OWNER\",\n  createdAt: Date.now(),\n  action: {\n    type: \"createStatusReport\" as const,\n    params: {\n      title: \"Test Incident\",\n      status: \"investigating\" as const,\n      message: \"Investigating the issue\",\n      pageId: 1,\n    },\n  },\n};\n\nmock.module(\"./workspace-resolver\", () => ({\n  resolveWorkspace: (teamId: string) => {\n    if (teamId === \"T_KNOWN\") {\n      return Promise.resolve({ botToken: \"xoxb-fallback\" });\n    }\n    return Promise.resolve(null);\n  },\n}));\n\nlet slackCalls: Array<{ method: string; args: Record<string, unknown> }> = [];\n\nmock.module(\"@slack/web-api\", () => ({\n  WebClient: class {\n    chat = {\n      update: (args: Record<string, unknown>) => {\n        slackCalls.push({ method: \"update\", args });\n        return Promise.resolve();\n      },\n      postEphemeral: (args: Record<string, unknown>) => {\n        slackCalls.push({ method: \"postEphemeral\", args });\n        return Promise.resolve();\n      },\n    };\n  },\n}));\n\nconst { handleSlackInteraction } = await import(\"./interactions\");\nconst { verifySlackSignature } = await import(\"./verify\");\n\nfunction createTestApp() {\n  const app = new Hono<{ Variables: { slackBody: unknown } }>();\n  app.post(\"/slack/interactions\", verifySlackSignature, handleSlackInteraction);\n  return app;\n}\n\nfunction seedPendingAction() {\n  redisStore.set(`slack:action:${pendingData.id}`, JSON.stringify(pendingData));\n  redisStore.set(`slack:thread:${pendingData.threadTs}`, pendingData.id);\n}\n\nfunction signAndPost(\n  app: ReturnType<typeof createTestApp>,\n  payload: Record<string, unknown>,\n) {\n  const payloadStr = JSON.stringify(payload);\n  const body = `payload=${encodeURIComponent(payloadStr)}`;\n  const timestamp = Math.floor(Date.now() / 1000);\n  const basestring = `v0:${timestamp}:${body}`;\n  const sig = crypto\n    .createHmac(\"sha256\", SIGNING_SECRET)\n    .update(basestring)\n    .digest(\"hex\");\n\n  return app.request(\"/slack/interactions\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/x-www-form-urlencoded\",\n      \"x-slack-request-timestamp\": String(timestamp),\n      \"x-slack-signature\": `v0=${sig}`,\n    },\n    body,\n  });\n}\n\ndescribe(\"handleSlackInteraction\", () => {\n  const app = createTestApp();\n\n  beforeEach(() => {\n    slackCalls = [];\n    redisStore.clear();\n  });\n\n  test(\"returns ok for non-block_actions\", async () => {\n    const res = await signAndPost(app, {\n      type: \"message_action\",\n      actions: [],\n    });\n\n    expect(res.status).toBe(200);\n    expect(slackCalls).toHaveLength(0);\n  });\n\n  test(\"returns ok for unknown action_id prefix\", async () => {\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U1\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.1\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"unknown_action\" }],\n    });\n\n    expect(res.status).toBe(200);\n    expect(slackCalls).toHaveLength(0);\n  });\n\n  test(\"cancel updates message to cancelled\", async () => {\n    seedPendingAction();\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"cancel_pending-123\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const cancelCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" && (c.args.text as string).includes(\"Cancelled\"),\n    );\n    expect(cancelCall).toBeDefined();\n  });\n\n  test(\"rejects action from wrong user\", async () => {\n    seedPendingAction();\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OTHER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_pending-123\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const ephemeral = slackCalls.find((c) => c.method === \"postEphemeral\");\n    expect(ephemeral).toBeDefined();\n    expect(ephemeral?.args.text as string).toContain(\"Only the person\");\n\n    // Pending action should NOT be consumed — still available for the real owner\n    expect(redisStore.has(`slack:action:${pendingData.id}`)).toBe(true);\n  });\n\n  test(\"shows expired message when pending action not found\", async () => {\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U1\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_unknown-id\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const expiredCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" && (c.args.text as string).includes(\"expired\"),\n    );\n    expect(expiredCall).toBeDefined();\n  });\n\n  test(\"returns ok when no bot token available\", async () => {\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U1\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.2\" },\n      team: { id: \"T_UNKNOWN\" },\n      actions: [{ action_id: \"approve_some-id\" }],\n    });\n\n    expect(res.status).toBe(200);\n    expect(slackCalls).toHaveLength(0);\n  });\n\n  test(\"falls back to workspace resolver when pending has no botToken\", async () => {\n    const noTokenPending = {\n      ...pendingData,\n      id: \"pending-notoken\",\n      botToken: \"\",\n      createdAt: Date.now(),\n    };\n    redisStore.set(\n      `slack:action:${noTokenPending.id}`,\n      JSON.stringify(noTokenPending),\n    );\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"cancel_pending-notoken\" }],\n    });\n\n    expect(res.status).toBe(200);\n    // Should still work via workspace resolver fallback\n    const cancelCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" && (c.args.text as string).includes(\"Cancelled\"),\n    );\n    expect(cancelCall).toBeDefined();\n  });\n\n  test(\"returns ok with empty actions array\", async () => {\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U1\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [],\n    });\n\n    expect(res.status).toBe(200);\n    expect(slackCalls).toHaveLength(0);\n  });\n\n  test(\"parses approve_notify prefix correctly\", async () => {\n    seedPendingAction();\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_notify_pending-123\" }],\n    });\n\n    // approve_notify should extract pending ID as \"pending-123\"\n    // Since the action exists and user matches, it tries to execute\n    expect(res.status).toBe(200);\n  });\n\n  test(\"returns ok when no team id and no pending\", async () => {\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U1\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.2\" },\n      actions: [{ action_id: \"approve_orphan-id\" }],\n    });\n\n    expect(res.status).toBe(200);\n    expect(slackCalls).toHaveLength(0);\n  });\n\n  test(\"cancel consumes pending action from redis\", async () => {\n    seedPendingAction();\n\n    await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"1.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"cancel_pending-123\" }],\n    });\n\n    // After cancel, the pending action should be consumed from redis\n    expect(redisStore.has(`slack:action:${pendingData.id}`)).toBe(false);\n  });\n});\n\ndescribe(\"createMaintenance execution\", () => {\n  const app = createTestApp();\n\n  beforeEach(() => {\n    slackCalls = [];\n    redisStore.clear();\n  });\n\n  function seedMaintenanceAction(overrides: Record<string, unknown> = {}) {\n    const data = {\n      id: \"maint-001\",\n      workspaceId: 1,\n      limits: {},\n      botToken: \"xoxb-test\",\n      channelId: \"C1\",\n      threadTs: \"2.1\",\n      messageTs: \"2.2\",\n      userId: \"U_OWNER\",\n      createdAt: Date.now(),\n      action: {\n        type: \"createMaintenance\" as const,\n        params: {\n          title: \"DB Maintenance\",\n          message: \"Scheduled database upgrade.\",\n          from: new Date(Date.now() + 86400000).toISOString(),\n          to: new Date(Date.now() + 86400000 + 3600000).toISOString(),\n          pageId: 1,\n          ...overrides,\n        },\n      },\n    };\n    redisStore.set(`slack:action:${data.id}`, JSON.stringify(data));\n    redisStore.set(`slack:thread:${data.threadTs}`, data.id);\n    return data;\n  }\n\n  test(\"approve creates maintenance and shows success\", async () => {\n    seedMaintenanceAction();\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"2.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_maint-001\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const successCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" &&\n        (c.args.text as string).includes(\n          \"Maintenance *DB Maintenance* scheduled\",\n        ),\n    );\n    expect(successCall).toBeDefined();\n    expect(successCall?.args.text as string).not.toContain(\n      \"subscribers notified\",\n    );\n  });\n\n  test(\"approve_notify creates maintenance and notifies\", async () => {\n    seedMaintenanceAction();\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"2.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_notify_maint-001\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const successCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" &&\n        (c.args.text as string).includes(\"subscribers notified\"),\n    );\n    expect(successCall).toBeDefined();\n  });\n\n  test(\"cancel does not create maintenance\", async () => {\n    seedMaintenanceAction();\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"2.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"cancel_maint-001\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const cancelCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" && (c.args.text as string).includes(\"Cancelled\"),\n    );\n    expect(cancelCall).toBeDefined();\n  });\n\n  test(\"shows error when AI hallucinates page id\", async () => {\n    seedMaintenanceAction({ pageId: 99999 });\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"2.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_maint-001\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const errorCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" &&\n        (c.args.text as string).includes(\"Something went wrong\"),\n    );\n    expect(errorCall).toBeDefined();\n  });\n\n  test(\"shows error when from is after to\", async () => {\n    const now = Date.now();\n    seedMaintenanceAction({\n      from: new Date(now + 7200000).toISOString(),\n      to: new Date(now + 3600000).toISOString(),\n    });\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"2.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_maint-001\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const errorCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" &&\n        (c.args.text as string).includes(\"Something went wrong\"),\n    );\n    expect(errorCall).toBeDefined();\n  });\n\n  test(\"creates maintenance with page components\", async () => {\n    seedMaintenanceAction({ pageComponentIds: [\"1\", \"2\"] });\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"2.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_maint-001\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const successCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" &&\n        (c.args.text as string).includes(\n          \"Maintenance *DB Maintenance* scheduled\",\n        ),\n    );\n    expect(successCall).toBeDefined();\n  });\n\n  test(\"shows error for invalid page component ids\", async () => {\n    seedMaintenanceAction({ pageComponentIds: [\"99999\"] });\n\n    const res = await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"2.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_maint-001\" }],\n    });\n\n    expect(res.status).toBe(200);\n    const errorCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" &&\n        (c.args.text as string).includes(\"Something went wrong\"),\n    );\n    expect(errorCall).toBeDefined();\n  });\n\n  test(\"does not leak internal error details to user\", async () => {\n    const data = {\n      id: \"maint-001\",\n      workspaceId: 2,\n      limits: {},\n      botToken: \"xoxb-test\",\n      channelId: \"C1\",\n      threadTs: \"2.1\",\n      messageTs: \"2.2\",\n      userId: \"U_OWNER\",\n      createdAt: Date.now(),\n      action: {\n        type: \"createMaintenance\" as const,\n        params: {\n          title: \"DB Maintenance\",\n          message: \"Upgrade.\",\n          from: new Date(Date.now() + 86400000).toISOString(),\n          to: new Date(Date.now() + 86400000 + 3600000).toISOString(),\n          pageId: 99999,\n        },\n      },\n    };\n    redisStore.set(`slack:action:${data.id}`, JSON.stringify(data));\n    redisStore.set(`slack:thread:${data.threadTs}`, data.id);\n\n    await signAndPost(app, {\n      type: \"block_actions\",\n      user: { id: \"U_OWNER\" },\n      channel: { id: \"C1\" },\n      message: { ts: \"2.2\" },\n      team: { id: \"T_KNOWN\" },\n      actions: [{ action_id: \"approve_maint-001\" }],\n    });\n\n    const errorCall = slackCalls.find(\n      (c) =>\n        c.method === \"update\" &&\n        (c.args.text as string).includes(\"Something went wrong\"),\n    );\n    expect(errorCall).toBeDefined();\n    const text = errorCall?.args.text as string;\n    expect(text).not.toContain(\"Page 99999\");\n    expect(text).not.toContain(\"ConnectError\");\n    expect(text).not.toContain(\"select\");\n    expect(text).not.toContain(\"query\");\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/interactions.ts",
    "content": "import { and, db, eq, inArray } from \"@openstatus/db\";\nimport {\n  maintenance,\n  maintenancesToPageComponents,\n  page,\n  pageComponent,\n  statusReport,\n  statusReportUpdate,\n} from \"@openstatus/db/src/schema\";\nimport { WebClient } from \"@slack/web-api\";\nimport type { Context } from \"hono\";\nimport { sendMaintenanceNotification } from \"../rpc/services/maintenance\";\nimport {\n  getStatusReportById,\n  sendStatusReportNotification,\n  updatePageComponentAssociations,\n  validatePageComponentIds,\n} from \"../rpc/services/status-report\";\nimport { consume, get } from \"./confirmation-store\";\nimport type { PendingAction } from \"./confirmation-store\";\nimport { resolveWorkspace } from \"./workspace-resolver\";\n\ninterface SlackInteractionPayload {\n  type: string;\n  user: { id: string };\n  channel: { id: string };\n  message: { ts: string };\n  team?: { id: string };\n  actions: Array<{ action_id: string; value?: string }>;\n}\n\nexport async function handleSlackInteraction(c: Context) {\n  const payload = c.get(\"slackBody\") as SlackInteractionPayload;\n\n  if (payload.type !== \"block_actions\" || !payload.actions?.length) {\n    return c.json({ ok: true });\n  }\n\n  const actionId = payload.actions[0].action_id;\n  const channelId = payload.channel.id;\n  const messageTs = payload.message.ts;\n  const userId = payload.user.id;\n  const teamId = payload.team?.id;\n\n  let type: \"approve\" | \"approve_notify\" | \"cancel\";\n  let pendingId: string;\n\n  if (actionId.startsWith(\"approve_notify_\")) {\n    type = \"approve_notify\";\n    pendingId = actionId.replace(\"approve_notify_\", \"\");\n  } else if (actionId.startsWith(\"approve_\")) {\n    type = \"approve\";\n    pendingId = actionId.replace(\"approve_\", \"\");\n  } else if (actionId.startsWith(\"cancel_\")) {\n    type = \"cancel\";\n    pendingId = actionId.replace(\"cancel_\", \"\");\n  } else {\n    return c.json({ ok: true });\n  }\n\n  // Non-atomic read for botToken resolution and authorization checks\n  const pending = await get(pendingId);\n\n  let botToken: string | undefined = pending?.botToken;\n  if (!botToken && teamId) {\n    const resolved = await resolveWorkspace(teamId);\n    botToken = resolved?.botToken;\n  }\n\n  if (!botToken) {\n    return c.json({ ok: true });\n  }\n\n  const slack = new WebClient(botToken);\n\n  if (!pending) {\n    await slack.chat.update({\n      channel: channelId,\n      ts: messageTs,\n      text: \":x: This action has expired. Please try again.\",\n      blocks: [],\n    });\n    return c.json({ ok: true });\n  }\n\n  if (pending.userId !== userId) {\n    await slack.chat.postEphemeral({\n      channel: channelId,\n      user: userId,\n      text: \"Only the person who initiated this action can approve or cancel it.\",\n    });\n    return c.json({ ok: true });\n  }\n\n  // Atomic consume — prevents double execution from concurrent requests (e.g. double-click).\n  // If another request already consumed this action, consume() returns undefined.\n  const consumed = await consume(pendingId);\n  if (!consumed) {\n    return c.json({ ok: true });\n  }\n\n  if (type === \"cancel\") {\n    await slack.chat.update({\n      channel: channelId,\n      ts: messageTs,\n      text: \":no_entry_sign: Cancelled.\",\n      blocks: [],\n    });\n    return c.json({ ok: true });\n  }\n\n  const notify = type === \"approve_notify\";\n\n  try {\n    await executeAction(consumed, notify, slack, channelId, messageTs);\n  } catch (err) {\n    console.error(\"[slack] action execution error:\", err);\n    await slack.chat.update({\n      channel: channelId,\n      ts: messageTs,\n      text: \":x: Something went wrong. Please try again.\",\n      blocks: [],\n    });\n  }\n\n  return c.json({ ok: true });\n}\n\nasync function getPageUrl(pageId: number): Promise<string | null> {\n  const statusPage = await db\n    .select({ slug: page.slug, customDomain: page.customDomain })\n    .from(page)\n    .where(eq(page.id, pageId))\n    .get();\n\n  if (!statusPage) return null;\n\n  return statusPage.customDomain\n    ? `https://${statusPage.customDomain}`\n    : `https://${statusPage.slug}.openstatus.dev`;\n}\n\nasync function getReportUrl(pageId: number, reportId: number): Promise<string> {\n  const statusPage = await db\n    .select({ slug: page.slug, customDomain: page.customDomain })\n    .from(page)\n    .where(eq(page.id, pageId))\n    .get();\n\n  const baseUrl = statusPage?.customDomain\n    ? `https://${statusPage.customDomain}`\n    : `https://${statusPage?.slug}.openstatus.dev`;\n  return `${baseUrl}/events/report/${reportId}`;\n}\n\nasync function executeAction(\n  pending: PendingAction,\n  notify: boolean,\n  slack: WebClient,\n  channelId: string,\n  messageTs: string,\n) {\n  const { action, workspaceId, limits } = pending;\n\n  switch (action.type) {\n    case \"createStatusReport\": {\n      const { title, status, message, pageId, pageComponentIds } =\n        action.params;\n\n      const result = await db.transaction(async (tx) => {\n        const validated = pageComponentIds?.length\n          ? await validatePageComponentIds(pageComponentIds, workspaceId, tx)\n          : { componentIds: [], pageId: null };\n\n        // Validate that provided pageId matches the components' page\n        if (\n          validated.pageId !== null &&\n          pageId != null &&\n          pageId !== validated.pageId\n        ) {\n          throw new Error(\n            `pageId ${pageId} does not match the page (${validated.pageId}) that the selected components belong to`,\n          );\n        }\n\n        // Prefer the validated pageId derived from components\n        const resolvedPageId = validated.pageId ?? pageId;\n\n        const report = await tx\n          .insert(statusReport)\n          .values({\n            workspaceId,\n            pageId: resolvedPageId,\n            title,\n            status,\n          })\n          .returning()\n          .get();\n\n        if (validated.componentIds.length > 0) {\n          await updatePageComponentAssociations(\n            report.id,\n            validated.componentIds,\n            tx,\n          );\n        }\n\n        const newUpdate = await tx\n          .insert(statusReportUpdate)\n          .values({\n            statusReportId: report.id,\n            status,\n            date: new Date(),\n            message,\n          })\n          .returning()\n          .get();\n\n        return { report, updateId: newUpdate.id };\n      });\n      if (!result || !result.report.pageId) {\n        throw new Error(\"Failed to create status report\");\n      }\n      if (notify) {\n        await sendStatusReportNotification({\n          statusReportUpdateId: result.updateId,\n          limits,\n        });\n      }\n\n      const reportUrl = await getReportUrl(\n        result.report.pageId,\n        result.report.id,\n      );\n\n      await slack.chat.update({\n        channel: channelId,\n        ts: messageTs,\n        text: `:white_check_mark: Status report *${title}* created${notify ? \" and subscribers notified\" : \"\"}.\\n<${reportUrl}|View on status page>`,\n        blocks: [],\n      });\n      break;\n    }\n\n    case \"addStatusReportUpdate\": {\n      const { statusReportId, status, message } = action.params;\n\n      const report = await getStatusReportById(statusReportId, workspaceId);\n      if (!report) {\n        throw new Error(\"Status report not found\");\n      }\n\n      const updateId = await db.transaction(async (tx) => {\n        const newUpdate = await tx\n          .insert(statusReportUpdate)\n          .values({\n            statusReportId: report.id,\n            status,\n            date: new Date(),\n            message,\n          })\n          .returning()\n          .get();\n\n        await tx\n          .update(statusReport)\n          .set({ status, updatedAt: new Date() })\n          .where(eq(statusReport.id, report.id));\n\n        return newUpdate.id;\n      });\n\n      if (notify && report.pageId) {\n        await sendStatusReportNotification({\n          statusReportUpdateId: updateId,\n          limits,\n        });\n      }\n\n      const updateReportUrl = report.pageId\n        ? await getReportUrl(report.pageId, report.id)\n        : null;\n\n      await slack.chat.update({\n        channel: channelId,\n        ts: messageTs,\n        text: `:white_check_mark: Update added to *${report.title}* (${status})${notify ? \" and subscribers notified\" : \"\"}.\\n>${message}${updateReportUrl ? `\\n<${updateReportUrl}|View on status page>` : \"\"}`,\n        blocks: [],\n      });\n      break;\n    }\n\n    case \"updateStatusReport\": {\n      const { statusReportId, title, pageComponentIds } = action.params;\n\n      const report = await getStatusReportById(statusReportId, workspaceId);\n      if (!report) {\n        throw new Error(\"Status report not found\");\n      }\n\n      await db.transaction(async (tx) => {\n        if (pageComponentIds) {\n          const validated = await validatePageComponentIds(\n            pageComponentIds,\n            workspaceId,\n            tx,\n          );\n          await updatePageComponentAssociations(\n            report.id,\n            validated.componentIds,\n            tx,\n          );\n        }\n\n        const updateValues: Record<string, unknown> = {\n          updatedAt: new Date(),\n        };\n        if (title) updateValues.title = title;\n\n        await tx\n          .update(statusReport)\n          .set(updateValues)\n          .where(eq(statusReport.id, report.id));\n      });\n\n      await slack.chat.update({\n        channel: channelId,\n        ts: messageTs,\n        text: `:white_check_mark: Status report *${title ?? report.title}* updated.`,\n        blocks: [],\n      });\n      break;\n    }\n\n    case \"resolveStatusReport\": {\n      const { statusReportId, message } = action.params;\n\n      const report = await getStatusReportById(statusReportId, workspaceId);\n      if (!report) {\n        throw new Error(\"Status report not found\");\n      }\n\n      const resolveUpdateId = await db.transaction(async (tx) => {\n        const newUpdate = await tx\n          .insert(statusReportUpdate)\n          .values({\n            statusReportId: report.id,\n            status: \"resolved\",\n            date: new Date(),\n            message,\n          })\n          .returning()\n          .get();\n\n        await tx\n          .update(statusReport)\n          .set({ status: \"resolved\", updatedAt: new Date() })\n          .where(eq(statusReport.id, report.id));\n\n        return newUpdate.id;\n      });\n\n      if (notify && report.pageId) {\n        await sendStatusReportNotification({\n          statusReportUpdateId: resolveUpdateId,\n          limits,\n        });\n      }\n\n      const resolveReportUrl = report.pageId\n        ? await getReportUrl(report.pageId, report.id)\n        : null;\n\n      await slack.chat.update({\n        channel: channelId,\n        ts: messageTs,\n        text: `:white_check_mark: *${report.title}* resolved${notify ? \" and subscribers notified\" : \"\"}.${message ? `\\n>${message}` : \"\"}${resolveReportUrl ? `\\n<${resolveReportUrl}|View on status page>` : \"\"}`,\n        blocks: [],\n      });\n      break;\n    }\n\n    case \"createMaintenance\": {\n      const {\n        title,\n        message,\n        from,\n        to,\n        pageId: maintenancePageId,\n        pageComponentIds: maintenanceComponentIds,\n      } = action.params;\n\n      const fromDate = new Date(from);\n      const toDate = new Date(to);\n      if (fromDate >= toDate) {\n        throw new Error(\"Start time must be before end time\");\n      }\n\n      const newMaintenance = await db.transaction(async (tx) => {\n        const pageRecord = await tx\n          .select({ id: page.id })\n          .from(page)\n          .where(\n            and(\n              eq(page.id, maintenancePageId),\n              eq(page.workspaceId, workspaceId),\n            ),\n          )\n          .get();\n\n        if (!pageRecord) {\n          throw new Error(\"Page not found in this workspace\");\n        }\n\n        const resolvedPageId = pageRecord.id;\n\n        let componentIds: number[] = [];\n        if (maintenanceComponentIds?.length) {\n          const numericIds = maintenanceComponentIds.map((id) => Number(id));\n          const validComponents = await tx\n            .select({ id: pageComponent.id, pageId: pageComponent.pageId })\n            .from(pageComponent)\n            .where(\n              and(\n                inArray(pageComponent.id, numericIds),\n                eq(pageComponent.workspaceId, workspaceId),\n              ),\n            )\n            .all();\n\n          if (validComponents.length !== numericIds.length) {\n            throw new Error(\"One or more page components not found\");\n          }\n\n          const componentPageIds = new Set(\n            validComponents.map((c) => c.pageId),\n          );\n          if (componentPageIds.size > 1) {\n            throw new Error(\"All components must belong to the same page\");\n          }\n\n          const componentPageId = validComponents[0]?.pageId;\n          if (componentPageId !== null && componentPageId !== resolvedPageId) {\n            throw new Error(\n              \"Selected components do not belong to the target status page\",\n            );\n          }\n\n          componentIds = numericIds;\n        }\n\n        const record = await tx\n          .insert(maintenance)\n          .values({\n            workspaceId,\n            pageId: resolvedPageId,\n            title,\n            message,\n            from: fromDate,\n            to: toDate,\n          })\n          .returning()\n          .get();\n\n        if (componentIds.length > 0) {\n          await tx.insert(maintenancesToPageComponents).values(\n            componentIds.map((pageComponentId) => ({\n              maintenanceId: record.id,\n              pageComponentId,\n            })),\n          );\n        }\n\n        return record;\n      });\n\n      if (notify) {\n        await sendMaintenanceNotification({\n          maintenanceId: newMaintenance.id,\n          limits,\n        });\n      }\n\n      const maintenancePageUrl = newMaintenance.pageId\n        ? await getPageUrl(newMaintenance.pageId)\n        : null;\n\n      await slack.chat.update({\n        channel: channelId,\n        ts: messageTs,\n        text: `:white_check_mark: Maintenance *${title}* scheduled${notify ? \" and subscribers notified\" : \"\"}.${maintenancePageUrl ? `\\n<${maintenancePageUrl}|View status page>` : \"\"}`,\n        blocks: [],\n      });\n      break;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/oauth.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\";\nimport crypto from \"node:crypto\";\nimport { Hono } from \"hono\";\nimport { handleSlackInstall, handleSlackOAuthCallback } from \"./oauth\";\n\nconst SIGNING_SECRET =\n  process.env.SLACK_SIGNING_SECRET ?? \"test-signing-secret\";\n\nprocess.env.SLACK_SIGNING_SECRET = SIGNING_SECRET;\nprocess.env.SLACK_CLIENT_ID = \"test-client-id\";\nprocess.env.SLACK_CLIENT_SECRET = \"test-client-secret\";\nprocess.env.NODE_ENV = \"development\";\n\nfunction createTestApp() {\n  const app = new Hono();\n  app.get(\"/slack/install\", handleSlackInstall);\n  app.get(\"/slack/oauth/callback\", handleSlackOAuthCallback);\n  return app;\n}\n\nfunction signToken(data: { workspaceId: number; ts: number }): string {\n  const payload = JSON.stringify(data);\n  const signature = crypto\n    .createHmac(\"sha256\", SIGNING_SECRET)\n    .update(payload)\n    .digest(\"hex\");\n  return Buffer.from(`${payload}.${signature}`).toString(\"base64url\");\n}\n\nfunction encodeState(state: { workspaceId: number; ts: number }): string {\n  return signToken(state);\n}\n\nfunction makeInstallToken(workspaceId: number): string {\n  return signToken({ workspaceId, ts: Date.now() });\n}\n\ndescribe(\"handleSlackInstall\", () => {\n  const app = createTestApp();\n\n  test(\"redirects to Slack OAuth URL with correct params\", async () => {\n    const token = makeInstallToken(1);\n    const res = await app.request(`/slack/install?token=${token}`);\n\n    expect(res.status).toBe(302);\n    const location = res.headers.get(\"location\");\n    expect(location).toBeDefined();\n    expect(location).toContain(\"https://slack.com/oauth/v2/authorize\");\n    expect(location).toContain(\"client_id=test-client-id\");\n    expect(location).toContain(\"scope=\");\n    expect(location).toContain(\"state=\");\n    expect(location).toContain(\"redirect_uri=\");\n  });\n\n  test(\"returns 400 when token is missing\", async () => {\n    const res = await app.request(\"/slack/install\");\n\n    expect(res.status).toBe(400);\n    const json = (await res.json()) as { error: string };\n    expect(json.error).toBe(\"token is required\");\n  });\n\n  test(\"returns 403 for invalid token\", async () => {\n    const res = await app.request(\"/slack/install?token=invalid-token\");\n\n    expect(res.status).toBe(403);\n    const json = (await res.json()) as { error: string };\n    expect(json.error).toBe(\"Invalid or expired token\");\n  });\n\n  test(\"returns 403 for expired token\", async () => {\n    const expired = signToken({\n      workspaceId: 1,\n      ts: Date.now() - 10 * 60 * 1000,\n    });\n    const res = await app.request(`/slack/install?token=${expired}`);\n\n    expect(res.status).toBe(403);\n    const json = (await res.json()) as { error: string };\n    expect(json.error).toBe(\"Invalid or expired token\");\n  });\n\n  test(\"includes all required bot scopes\", async () => {\n    const token = makeInstallToken(1);\n    const res = await app.request(`/slack/install?token=${token}`);\n    const location = res.headers.get(\"location\");\n    expect(location).toBeDefined();\n    const url = new URL(location as string);\n    const scope = url.searchParams.get(\"scope\");\n\n    const expectedScopes = [\n      \"app_mentions:read\",\n      \"channels:history\",\n      \"chat:write\",\n      \"groups:history\",\n      \"groups:read\",\n      \"groups:write\",\n      \"im:history\",\n      \"im:read\",\n      \"im:write\",\n      \"mpim:history\",\n    ];\n\n    for (const s of expectedScopes) {\n      expect(scope).toContain(s);\n    }\n  });\n\n  test(\"state contains signed workspaceId\", async () => {\n    const token = makeInstallToken(42);\n    const res = await app.request(`/slack/install?token=${token}`);\n    const location = res.headers.get(\"location\");\n    expect(location).toBeDefined();\n    const url = new URL(location as string);\n    const state = url.searchParams.get(\"state\");\n    expect(state).toBeDefined();\n\n    const decoded = Buffer.from(state as string, \"base64url\").toString();\n    const dotIdx = decoded.lastIndexOf(\".\");\n    const payload = JSON.parse(decoded.slice(0, dotIdx));\n\n    expect(payload.workspaceId).toBe(42);\n    expect(payload.ts).toBeGreaterThan(0);\n  });\n});\n\ndescribe(\"handleSlackOAuthCallback\", () => {\n  const app = createTestApp();\n\n  test(\"redirects to error page on Slack error\", async () => {\n    const res = await app.request(\"/slack/oauth/callback?error=access_denied\");\n\n    expect(res.status).toBe(302);\n    const location = res.headers.get(\"location\");\n    expect(location).toContain(\"slack=error\");\n  });\n\n  test(\"returns 400 when code is missing\", async () => {\n    const state = encodeState({ workspaceId: 1, ts: Date.now() });\n    const res = await app.request(`/slack/oauth/callback?state=${state}`);\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 400 when state is missing\", async () => {\n    const res = await app.request(\"/slack/oauth/callback?code=test-code\");\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 400 for expired state\", async () => {\n    const expiredState = encodeState({\n      workspaceId: 1,\n      ts: Date.now() - 15 * 60 * 1000,\n    });\n    const res = await app.request(\n      `/slack/oauth/callback?code=test-code&state=${expiredState}`,\n    );\n\n    expect(res.status).toBe(400);\n    const json = (await res.json()) as { error: string };\n    expect(json.error).toBe(\"Invalid or expired state\");\n  });\n\n  test(\"returns 400 for tampered state\", async () => {\n    const payload = JSON.stringify({ workspaceId: 1, ts: Date.now() });\n    const tamperedState = Buffer.from(`${payload}.invalidsignature`).toString(\n      \"base64url\",\n    );\n    const res = await app.request(\n      `/slack/oauth/callback?code=test-code&state=${tamperedState}`,\n    );\n\n    expect(res.status).toBe(400);\n    const json = (await res.json()) as { error: string };\n    expect(json.error).toBe(\"Invalid or expired state\");\n  });\n\n  test(\"returns 400 for invalid base64 state\", async () => {\n    const res = await app.request(\n      \"/slack/oauth/callback?code=test-code&state=not-valid-base64!!!\",\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 400 for state without dot separator\", async () => {\n    const noDotState = Buffer.from(\"nodothere\").toString(\"base64url\");\n    const res = await app.request(\n      `/slack/oauth/callback?code=test-code&state=${noDotState}`,\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 400 for state with valid signature but invalid JSON\", async () => {\n    const payload = \"not-json\";\n    const signature = crypto\n      .createHmac(\"sha256\", SIGNING_SECRET)\n      .update(payload)\n      .digest(\"hex\");\n    const state = Buffer.from(`${payload}.${signature}`).toString(\"base64url\");\n\n    const res = await app.request(\n      `/slack/oauth/callback?code=test-code&state=${state}`,\n    );\n\n    expect(res.status).toBe(400);\n  });\n\n  test(\"returns 400 when both code and state are missing\", async () => {\n    const res = await app.request(\"/slack/oauth/callback\");\n    expect(res.status).toBe(400);\n  });\n\n  test(\"accepts state within 10 minute window\", async () => {\n    const validState = encodeState({\n      workspaceId: 1,\n      ts: Date.now() - 9 * 60 * 1000,\n    });\n    // This will proceed to the token exchange which will fail (no mock for fetch)\n    // but it won't fail on state validation\n    const res = await app.request(\n      `/slack/oauth/callback?code=test-code&state=${validState}`,\n    );\n\n    // Will get a redirect to error page because token exchange fails,\n    // but NOT a 400 for invalid state\n    expect(res.status).not.toBe(400);\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/oauth.ts",
    "content": "import crypto from \"node:crypto\";\nimport { env } from \"@/env\";\nimport { and, db, eq } from \"@openstatus/db\";\nimport { integration } from \"@openstatus/db/src/schema\";\nimport type { Context } from \"hono\";\n\nconst SLACK_OAUTH_URL = \"https://slack.com/oauth/v2/authorize\";\nconst SLACK_TOKEN_URL = \"https://slack.com/api/oauth.v2.access\";\n\nconst BOT_SCOPES = [\n  \"app_mentions:read\",\n  \"channels:history\",\n  \"chat:write\",\n  \"groups:history\",\n  \"groups:read\",\n  \"groups:write\",\n  \"im:history\",\n  \"im:read\",\n  \"im:write\",\n  \"mpim:history\",\n].join(\",\");\n\ninterface OAuthState {\n  workspaceId: number;\n  ts: number;\n}\n\ninterface SlackOAuthResponse {\n  ok: boolean;\n  error?: string;\n  access_token: string;\n  token_type: string;\n  scope: string;\n  bot_user_id: string;\n  app_id: string;\n  team: { id: string; name: string };\n  authed_user: { id: string };\n  enterprise?: { id: string; name: string } | null;\n}\n\nexport async function handleSlackInstall(c: Context) {\n  const token = c.req.query(\"token\");\n  if (!token) {\n    return c.json({ error: \"token is required\" }, 400);\n  }\n\n  const installPayload = verifyInstallToken(token);\n  if (!installPayload) {\n    return c.json({ error: \"Invalid or expired token\" }, 403);\n  }\n\n  if (!env.SLACK_CLIENT_ID) {\n    return c.json({ error: \"Slack OAuth not configured\" }, 503);\n  }\n\n  const state = encodeState({\n    workspaceId: installPayload.workspaceId,\n    ts: Date.now(),\n  });\n\n  const params = new URLSearchParams({\n    client_id: env.SLACK_CLIENT_ID,\n    scope: BOT_SCOPES,\n    redirect_uri: getRedirectUri(c),\n    state,\n  });\n\n  return c.redirect(`${SLACK_OAUTH_URL}?${params.toString()}`);\n}\n\nexport async function handleSlackOAuthCallback(c: Context) {\n  const code = c.req.query(\"code\");\n  const stateParam = c.req.query(\"state\");\n  const error = c.req.query(\"error\");\n\n  if (error) {\n    return c.redirect(`${getDashboardUrl()}/settings/integrations?slack=error`);\n  }\n\n  if (!code || !stateParam) {\n    return c.json({ error: \"Missing code or state\" }, 400);\n  }\n\n  const state = decodeState(stateParam);\n  if (!state || Date.now() - state.ts > 10 * 60 * 1000) {\n    return c.json({ error: \"Invalid or expired state\" }, 400);\n  }\n\n  if (!env.SLACK_CLIENT_ID || !env.SLACK_CLIENT_SECRET) {\n    return c.json({ error: \"Slack OAuth not configured\" }, 503);\n  }\n\n  const tokenRes = await fetch(SLACK_TOKEN_URL, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n    body: new URLSearchParams({\n      client_id: env.SLACK_CLIENT_ID,\n      client_secret: env.SLACK_CLIENT_SECRET,\n      code,\n      redirect_uri: getRedirectUri(c),\n    }),\n  });\n\n  const tokenData = (await tokenRes.json()) as SlackOAuthResponse;\n  if (!tokenData.ok) {\n    console.error(\"[slack oauth] token exchange failed:\", tokenData.error);\n    return c.redirect(`${getDashboardUrl()}/settings/integrations?slack=error`);\n  }\n\n  const credential = {\n    botToken: tokenData.access_token,\n    botUserId: tokenData.bot_user_id,\n  };\n\n  const data = {\n    teamId: tokenData.team.id,\n    teamName: tokenData.team.name,\n    appId: tokenData.app_id,\n    scopes: tokenData.scope,\n    installedBy: tokenData.authed_user.id,\n  };\n\n  const existing = await db\n    .select()\n    .from(integration)\n    .where(\n      and(\n        eq(integration.name, \"slack-agent\"),\n        eq(integration.workspaceId, state.workspaceId),\n      ),\n    )\n    .get();\n\n  if (existing) {\n    await db\n      .update(integration)\n      .set({\n        externalId: tokenData.team.id,\n        credential,\n        data,\n        updatedAt: new Date(),\n      })\n      .where(eq(integration.id, existing.id));\n  } else {\n    await db.insert(integration).values({\n      name: \"slack-agent\",\n      workspaceId: state.workspaceId,\n      externalId: tokenData.team.id,\n      credential,\n      data,\n    });\n  }\n\n  return c.redirect(`${getDashboardUrl()}/settings/integrations?slack=success`);\n}\n\nfunction getRedirectUri(c: Context): string {\n  if (env.SLACK_REDIRECT_URI) return env.SLACK_REDIRECT_URI;\n  const url = new URL(c.req.url);\n  return `${url.origin}/slack/oauth/callback`;\n}\n\nfunction getDashboardUrl(): string {\n  return env.NODE_ENV === \"production\"\n    ? \"https://app.openstatus.dev\"\n    : \"http://localhost:3000\";\n}\n\nfunction encodeState(state: OAuthState): string {\n  const payload = JSON.stringify(state);\n  const signature = computeHmac(payload);\n  return Buffer.from(`${payload}.${signature}`).toString(\"base64url\");\n}\n\nfunction decodeState(encoded: string): OAuthState | null {\n  try {\n    const decoded = Buffer.from(encoded, \"base64url\").toString();\n    const dotIdx = decoded.lastIndexOf(\".\");\n    if (dotIdx === -1) return null;\n\n    const payload = decoded.slice(0, dotIdx);\n    const signature = decoded.slice(dotIdx + 1);\n\n    if (!verifyHmac(payload, signature)) return null;\n\n    return JSON.parse(payload) as OAuthState;\n  } catch {\n    return null;\n  }\n}\n\nfunction computeHmac(payload: string): string {\n  const secret = env.SLACK_SIGNING_SECRET;\n  if (!secret) throw new Error(\"Slack signing secret not configured\");\n  return crypto.createHmac(\"sha256\", secret).update(payload).digest(\"hex\");\n}\n\nfunction verifyHmac(payload: string, signature: string): boolean {\n  const expected = computeHmac(payload);\n  if (expected.length !== signature.length) return false;\n  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));\n}\n\nconst INSTALL_TOKEN_TTL_MS = 5 * 60 * 1000;\n\nfunction verifyInstallToken(token: string): { workspaceId: number } | null {\n  try {\n    const decoded = Buffer.from(token, \"base64url\").toString();\n    const dotIdx = decoded.lastIndexOf(\".\");\n    if (dotIdx === -1) return null;\n\n    const payload = decoded.slice(0, dotIdx);\n    const signature = decoded.slice(dotIdx + 1);\n\n    if (!verifyHmac(payload, signature)) return null;\n\n    const data = JSON.parse(payload) as { workspaceId: number; ts: number };\n    if (Date.now() - data.ts > INSTALL_TOKEN_TTL_MS) return null;\n\n    return { workspaceId: data.workspaceId };\n  } catch {\n    return null;\n  }\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/add-status-report-update.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport function createAddStatusReportUpdateTool() {\n  return tool({\n    description:\n      \"Add a progress update to an existing status report. This creates a new update entry and changes the report's status. Use for progress updates and resolving incidents.\",\n    inputSchema: z.object({\n      statusReportId: z.number().describe(\"ID of the status report to update\"),\n      status: z\n        .enum([\"investigating\", \"identified\", \"monitoring\", \"resolved\"])\n        .describe(\"New status for the report\"),\n      message: z\n        .string()\n        .describe(\"Professional update message for the public status page\"),\n    }),\n    execute: async (input) => {\n      return { needsConfirmation: true as const, params: input };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/create-maintenance.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport function createCreateMaintenanceTool() {\n  return tool({\n    description:\n      \"Schedule a maintenance window on a status page. IMPORTANT: You MUST call listStatusPages first to get the real pageId — never guess or make up a pageId. Parse natural language dates into ISO 8601 format (e.g. 'next Friday 2-3 PM' -> proper ISO strings). The maintenance will be shown to the user for confirmation before publishing.\",\n    inputSchema: z.object({\n      title: z.string().describe(\"Short title for the maintenance window\"),\n      message: z\n        .string()\n        .describe(\n          \"Professional maintenance message for the public status page\",\n        ),\n      from: z\n        .string()\n        .describe(\"Start time in ISO 8601 format (e.g. 2025-03-14T14:00:00Z)\"),\n      to: z\n        .string()\n        .describe(\"End time in ISO 8601 format (e.g. 2025-03-14T15:00:00Z)\"),\n      pageId: z\n        .number()\n        .describe(\n          \"ID of the status page — MUST come from listStatusPages, never guess this value\",\n        ),\n      pageComponentIds: z\n        .array(z.string())\n        .optional()\n        .describe(\"IDs of affected page components (optional)\"),\n    }),\n    execute: async (input) => {\n      return { needsConfirmation: true as const, params: input };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/create-status-report.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport function createCreateStatusReportTool() {\n  return tool({\n    description:\n      \"Create a new status report. IMPORTANT: You MUST call listStatusPages first to get the real pageId — never guess or make up a pageId. Draft the title and message based on the conversation. The report will be shown to the user for confirmation before publishing.\",\n    inputSchema: z.object({\n      title: z.string().describe(\"Short title for the status report\"),\n      status: z\n        .enum([\"investigating\", \"identified\", \"monitoring\", \"resolved\"])\n        .describe(\"Current status of the incident\"),\n      message: z\n        .string()\n        .describe(\n          \"Professional status update message for the public status page\",\n        ),\n      pageId: z\n        .number()\n        .describe(\n          \"ID of the status page — MUST come from listStatusPages, never guess this value\",\n        ),\n      pageComponentIds: z\n        .array(z.string())\n        .optional()\n        .describe(\"IDs of affected page components (optional)\"),\n    }),\n    execute: async (input) => {\n      return { needsConfirmation: true as const, params: input };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/index.ts",
    "content": "import type { Workspace } from \"@openstatus/db/src/schema/workspaces/validation\";\nimport { createAddStatusReportUpdateTool } from \"./add-status-report-update\";\nimport { createCreateMaintenanceTool } from \"./create-maintenance\";\nimport { createCreateStatusReportTool } from \"./create-status-report\";\nimport { createListMaintenancesTool } from \"./list-maintenances\";\nimport { createListStatusPagesTool } from \"./list-status-pages\";\nimport { createListStatusReportsTool } from \"./list-status-reports\";\nimport { createResolveStatusReportTool } from \"./resolve-status-report\";\nimport { createUpdateStatusReportTool } from \"./update-status-report\";\n\nexport function createTools(workspace: Workspace) {\n  return {\n    listStatusPages: createListStatusPagesTool(workspace.id),\n    listStatusReports: createListStatusReportsTool(workspace.id),\n    createStatusReport: createCreateStatusReportTool(),\n    addStatusReportUpdate: createAddStatusReportUpdateTool(),\n    updateStatusReport: createUpdateStatusReportTool(),\n    resolveStatusReport: createResolveStatusReportTool(),\n    listMaintenances: createListMaintenancesTool(workspace.id),\n    createMaintenance: createCreateMaintenanceTool(),\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/list-maintenances.ts",
    "content": "import { and, asc, db, desc, eq, gt } from \"@openstatus/db\";\nimport { maintenance } from \"@openstatus/db/src/schema\";\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport function createListMaintenancesTool(workspaceId: number) {\n  return tool({\n    description:\n      \"List maintenance windows for this workspace. By default returns only upcoming maintenances. Use this to check existing scheduled maintenance.\",\n    inputSchema: z.object({\n      filter: z\n        .enum([\"upcoming\", \"all\"])\n        .optional()\n        .describe(\n          \"Filter: 'upcoming' for future maintenances (default), 'all' for everything\",\n        ),\n    }),\n    execute: async ({ filter = \"upcoming\" }) => {\n      const conditions = [eq(maintenance.workspaceId, workspaceId)];\n      if (filter === \"upcoming\") {\n        conditions.push(gt(maintenance.from, new Date()));\n      }\n\n      const records = await db\n        .select({\n          id: maintenance.id,\n          title: maintenance.title,\n          message: maintenance.message,\n          from: maintenance.from,\n          to: maintenance.to,\n          pageId: maintenance.pageId,\n        })\n        .from(maintenance)\n        .where(and(...conditions))\n        .orderBy(\n          filter === \"upcoming\"\n            ? asc(maintenance.from)\n            : desc(maintenance.from),\n        )\n        .limit(20)\n        .all();\n\n      return {\n        maintenances: records.map((r) => ({\n          id: r.id,\n          title: r.title,\n          message: r.message,\n          from: r.from.toISOString(),\n          to: r.to.toISOString(),\n          pageId: r.pageId,\n        })),\n      };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/list-status-pages.ts",
    "content": "import { db, eq } from \"@openstatus/db\";\nimport { page, pageComponent } from \"@openstatus/db/src/schema\";\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport function createListStatusPagesTool(workspaceId: number) {\n  return tool({\n    description:\n      \"List all status pages for this workspace, including their components. Use this to find which page and components to use when creating a status report.\",\n    inputSchema: z.object({}),\n    execute: async () => {\n      const pages = await db\n        .select({\n          id: page.id,\n          title: page.title,\n          slug: page.slug,\n        })\n        .from(page)\n        .where(eq(page.workspaceId, workspaceId))\n        .all();\n\n      const result = await Promise.all(\n        pages.map(async (p) => {\n          const components = await db\n            .select({\n              id: pageComponent.id,\n              name: pageComponent.name,\n            })\n            .from(pageComponent)\n            .where(eq(pageComponent.pageId, p.id))\n            .all();\n\n          return {\n            id: p.id,\n            title: p.title,\n            slug: p.slug,\n            components: components.map((c) => ({\n              id: String(c.id),\n              name: c.name,\n            })),\n          };\n        }),\n      );\n\n      return { pages: result };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/list-status-reports.ts",
    "content": "import { and, db, desc, eq, ne } from \"@openstatus/db\";\nimport { statusReport, statusReportUpdate } from \"@openstatus/db/src/schema\";\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport function createListStatusReportsTool(workspaceId: number) {\n  return tool({\n    description:\n      \"List status reports for this workspace. By default returns only active (non-resolved) reports. Use this to find existing reports when adding updates or editing.\",\n    inputSchema: z.object({\n      filter: z\n        .enum([\"active\", \"all\"])\n        .optional()\n        .describe(\n          \"Filter: 'active' for non-resolved reports (default), 'all' for everything\",\n        ),\n    }),\n    execute: async ({ filter = \"active\" }) => {\n      const conditions = [eq(statusReport.workspaceId, workspaceId)];\n      if (filter === \"active\") {\n        conditions.push(ne(statusReport.status, \"resolved\"));\n      }\n\n      const reports = await db\n        .select()\n        .from(statusReport)\n        .where(and(...conditions))\n        .orderBy(desc(statusReport.updatedAt))\n        .limit(20)\n        .all();\n\n      const result = await Promise.all(\n        reports.map(async (r) => {\n          const latestUpdate = await db\n            .select({\n              message: statusReportUpdate.message,\n              status: statusReportUpdate.status,\n              date: statusReportUpdate.date,\n            })\n            .from(statusReportUpdate)\n            .where(eq(statusReportUpdate.statusReportId, r.id))\n            .orderBy(desc(statusReportUpdate.date))\n            .limit(1)\n            .get();\n\n          return {\n            id: r.id,\n            title: r.title,\n            status: r.status,\n            pageId: r.pageId,\n            latestUpdate: latestUpdate\n              ? {\n                  message: latestUpdate.message,\n                  status: latestUpdate.status,\n                  date: latestUpdate.date?.toISOString() ?? null,\n                }\n              : null,\n          };\n        }),\n      );\n\n      return { reports: result };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/resolve-status-report.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport function createResolveStatusReportTool() {\n  return tool({\n    description:\n      \"Resolve an active status report. This marks the incident as resolved and adds a final update message to the public status page.\",\n    inputSchema: z.object({\n      statusReportId: z.number().describe(\"ID of the status report to resolve\"),\n      message: z\n        .string()\n        .describe(\n          \"Resolution message explaining what was fixed, for the public status page\",\n        ),\n    }),\n    execute: async (input) => {\n      return { needsConfirmation: true as const, params: input };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/tools.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\";\nimport type { Workspace } from \"@openstatus/db/src/schema/workspaces/validation\";\nimport { createAddStatusReportUpdateTool } from \"./add-status-report-update\";\nimport { createCreateStatusReportTool } from \"./create-status-report\";\nimport { createTools } from \"./index\";\nimport { createUpdateStatusReportTool } from \"./update-status-report\";\n\nconst mockWorkspace = {\n  id: 1,\n  name: \"Test\",\n  slug: \"test\",\n  plan: \"free\",\n  limits: {},\n} as Workspace;\n\ndescribe(\"createTools\", () => {\n  test(\"returns all expected tool keys\", () => {\n    const tools = createTools(mockWorkspace);\n    expect(Object.keys(tools).sort()).toEqual([\n      \"addStatusReportUpdate\",\n      \"createMaintenance\",\n      \"createStatusReport\",\n      \"listMaintenances\",\n      \"listStatusPages\",\n      \"listStatusReports\",\n      \"resolveStatusReport\",\n      \"updateStatusReport\",\n    ]);\n  });\n});\n\ndescribe(\"createCreateStatusReportTool\", () => {\n  const tool = createCreateStatusReportTool();\n\n  test(\"returns needsConfirmation with params\", async () => {\n    const input = {\n      title: \"API Outage\",\n      status: \"investigating\" as const,\n      message: \"We are investigating the issue\",\n      pageId: 1,\n    };\n    const result = await tool.execute(input, {\n      toolCallId: \"test\",\n      messages: [],\n    });\n    expect(result).toEqual({ needsConfirmation: true, params: input });\n  });\n\n  test(\"includes optional pageComponentIds\", async () => {\n    const input = {\n      title: \"Outage\",\n      status: \"investigating\" as const,\n      message: \"msg\",\n      pageId: 1,\n      pageComponentIds: [\"comp-1\", \"comp-2\"],\n    };\n    const result = await tool.execute(input, {\n      toolCallId: \"test\",\n      messages: [],\n    });\n    expect(result.params.pageComponentIds).toEqual([\"comp-1\", \"comp-2\"]);\n  });\n});\n\ndescribe(\"createAddStatusReportUpdateTool\", () => {\n  const tool = createAddStatusReportUpdateTool();\n\n  test(\"returns needsConfirmation with params\", async () => {\n    const input = {\n      statusReportId: 42,\n      status: \"identified\" as const,\n      message: \"Root cause identified\",\n    };\n    const result = await tool.execute(input, {\n      toolCallId: \"test\",\n      messages: [],\n    });\n    expect(result).toEqual({ needsConfirmation: true, params: input });\n  });\n\n  test(\"works with resolved status\", async () => {\n    const input = {\n      statusReportId: 42,\n      status: \"resolved\" as const,\n      message: \"Issue has been fixed\",\n    };\n    const result = await tool.execute(input, {\n      toolCallId: \"test\",\n      messages: [],\n    });\n    expect(result.params.status).toBe(\"resolved\");\n  });\n});\n\ndescribe(\"createUpdateStatusReportTool\", () => {\n  const tool = createUpdateStatusReportTool();\n\n  test(\"returns needsConfirmation with title update\", async () => {\n    const input = {\n      statusReportId: 10,\n      title: \"Updated Title\",\n    };\n    const result = await tool.execute(input, {\n      toolCallId: \"test\",\n      messages: [],\n    });\n    expect(result).toEqual({ needsConfirmation: true, params: input });\n  });\n\n  test(\"works with only pageComponentIds\", async () => {\n    const input = {\n      statusReportId: 10,\n      pageComponentIds: [\"comp-1\"],\n    };\n    const result = await tool.execute(input, {\n      toolCallId: \"test\",\n      messages: [],\n    });\n    expect(result.params.pageComponentIds).toEqual([\"comp-1\"]);\n  });\n\n  test(\"works with both title and components\", async () => {\n    const input = {\n      statusReportId: 10,\n      title: \"New Title\",\n      pageComponentIds: [\"comp-1\", \"comp-2\"],\n    };\n    const result = await tool.execute(input, {\n      toolCallId: \"test\",\n      messages: [],\n    });\n    expect(result.params.title).toBe(\"New Title\");\n    expect(result.params.pageComponentIds).toEqual([\"comp-1\", \"comp-2\"]);\n  });\n\n  test(\"works with only statusReportId\", async () => {\n    const input = { statusReportId: 10 };\n    const result = await tool.execute(input, {\n      toolCallId: \"test\",\n      messages: [],\n    });\n    expect(result.params.statusReportId).toBe(10);\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/tools/update-status-report.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport function createUpdateStatusReportTool() {\n  return tool({\n    description:\n      \"Edit a status report's metadata (title, components). This does NOT add a new update message — use addStatusReportUpdate for that. No subscriber notifications are sent.\",\n    inputSchema: z.object({\n      statusReportId: z.number().describe(\"ID of the status report to edit\"),\n      title: z.string().optional().describe(\"New title for the report\"),\n      pageComponentIds: z\n        .array(z.string())\n        .optional()\n        .describe(\"Updated list of affected component IDs\"),\n    }),\n    execute: async (input) => {\n      return { needsConfirmation: true as const, params: input };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/slack/verify.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\";\nimport crypto from \"node:crypto\";\nimport { Hono } from \"hono\";\nimport { verifySlackSignature } from \"./verify\";\n\nconst SIGNING_SECRET = \"test-signing-secret\";\nprocess.env.SLACK_SIGNING_SECRET = SIGNING_SECRET;\n\nfunction signRequest(body: string, timestamp: number): string {\n  const basestring = `v0:${timestamp}:${body}`;\n  const hmac = crypto\n    .createHmac(\"sha256\", SIGNING_SECRET)\n    .update(basestring)\n    .digest(\"hex\");\n  return `v0=${hmac}`;\n}\n\nfunction createTestApp() {\n  const app = new Hono<{ Variables: { slackBody: unknown } }>();\n  app.post(\"/test\", verifySlackSignature, (c) => {\n    return c.json({ body: c.get(\"slackBody\") });\n  });\n  return app;\n}\n\ndescribe(\"verifySlackSignature\", () => {\n  const app = createTestApp();\n\n  test(\"accepts valid JSON signature\", async () => {\n    const body = JSON.stringify({ type: \"event_callback\", event: {} });\n    const timestamp = Math.floor(Date.now() / 1000);\n    const signature = signRequest(body, timestamp);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": signature,\n      },\n      body,\n    });\n\n    expect(res.status).toBe(200);\n    const json = (await res.json()) as { body: { type: string } };\n    expect(json.body.type).toBe(\"event_callback\");\n  });\n\n  test(\"accepts valid form-urlencoded payload\", async () => {\n    const payload = JSON.stringify({\n      type: \"block_actions\",\n      user: { id: \"U1\" },\n    });\n    const body = `payload=${encodeURIComponent(payload)}`;\n    const timestamp = Math.floor(Date.now() / 1000);\n    const signature = signRequest(body, timestamp);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": signature,\n      },\n      body,\n    });\n\n    expect(res.status).toBe(200);\n    const json = (await res.json()) as { body: { type: string } };\n    expect(json.body.type).toBe(\"block_actions\");\n  });\n\n  test(\"rejects missing headers\", async () => {\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: \"{}\",\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"rejects old timestamp\", async () => {\n    const body = \"{}\";\n    const timestamp = Math.floor(Date.now() / 1000) - 600;\n    const signature = signRequest(body, timestamp);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": signature,\n      },\n      body,\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"rejects invalid signature\", async () => {\n    const body = \"{}\";\n    const timestamp = Math.floor(Date.now() / 1000);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\":\n          \"v0=invalidsignature000000000000000000000000000000000000000000000000\",\n      },\n      body,\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"rejects signature with wrong length\", async () => {\n    const body = \"{}\";\n    const timestamp = Math.floor(Date.now() / 1000);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": \"v0=short\",\n      },\n      body,\n    });\n\n    expect(res.status).toBe(401);\n  });\n\n  test(\"rejects missing timestamp header only\", async () => {\n    const body = \"{}\";\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-signature\": \"v0=abc\",\n      },\n      body,\n    });\n    expect(res.status).toBe(401);\n  });\n\n  test(\"rejects missing signature header only\", async () => {\n    const body = \"{}\";\n    const timestamp = Math.floor(Date.now() / 1000);\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n      },\n      body,\n    });\n    expect(res.status).toBe(401);\n  });\n\n  test(\"rejects future timestamp beyond 5 minutes\", async () => {\n    const body = \"{}\";\n    const timestamp = Math.floor(Date.now() / 1000) + 600;\n    const signature = signRequest(body, timestamp);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": signature,\n      },\n      body,\n    });\n    expect(res.status).toBe(401);\n  });\n\n  test(\"accepts timestamp within 5 minute window\", async () => {\n    const body = JSON.stringify({ type: \"test\" });\n    const timestamp = Math.floor(Date.now() / 1000) - 200;\n    const signature = signRequest(body, timestamp);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": signature,\n      },\n      body,\n    });\n    expect(res.status).toBe(200);\n  });\n\n  test(\"rejects signature computed with wrong body\", async () => {\n    const body = JSON.stringify({ type: \"real_body\" });\n    const timestamp = Math.floor(Date.now() / 1000);\n    const wrongSignature = signRequest(\"different body\", timestamp);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": wrongSignature,\n      },\n      body,\n    });\n    expect(res.status).toBe(401);\n  });\n\n  test(\"handles form-urlencoded without payload param\", async () => {\n    const body = \"key=value\";\n    const timestamp = Math.floor(Date.now() / 1000);\n    const signature = signRequest(body, timestamp);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": signature,\n      },\n      body,\n    });\n    expect(res.status).toBe(200);\n  });\n\n  test(\"handles large JSON payload\", async () => {\n    const largePayload = { type: \"test\", data: \"x\".repeat(10000) };\n    const body = JSON.stringify(largePayload);\n    const timestamp = Math.floor(Date.now() / 1000);\n    const signature = signRequest(body, timestamp);\n\n    const res = await app.request(\"/test\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-slack-request-timestamp\": String(timestamp),\n        \"x-slack-signature\": signature,\n      },\n      body,\n    });\n    expect(res.status).toBe(200);\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/verify.ts",
    "content": "import { env } from \"@/env\";\nimport { createMiddleware } from \"hono/factory\";\n\nexport const verifySlackSignature = createMiddleware<{\n  Variables: { slackBody: unknown };\n}>(async (c, next) => {\n  const signingSecret = env.SLACK_SIGNING_SECRET;\n\n  if (!signingSecret) {\n    return c.json({ error: \"Slack not configured\" }, 503);\n  }\n\n  const timestamp = c.req.header(\"x-slack-request-timestamp\");\n  const signature = c.req.header(\"x-slack-signature\");\n\n  if (!timestamp || !signature) {\n    return c.json({ error: \"Missing Slack headers\" }, 401);\n  }\n\n  const now = Math.floor(Date.now() / 1000);\n  if (Math.abs(now - Number(timestamp)) > 300) {\n    return c.json({ error: \"Request too old\" }, 401);\n  }\n\n  const rawBody = await c.req.text();\n\n  const encoder = new TextEncoder();\n  const basestring = `v0:${timestamp}:${rawBody}`;\n  const key = await crypto.subtle.importKey(\n    \"raw\",\n    encoder.encode(signingSecret),\n    { name: \"HMAC\", hash: \"SHA-256\" },\n    false,\n    [\"sign\"],\n  );\n  const sig = await crypto.subtle.sign(\"HMAC\", key, encoder.encode(basestring));\n  const computed = `v0=${Array.from(new Uint8Array(sig))\n    .map((b) => b.toString(16).padStart(2, \"0\"))\n    .join(\"\")}`;\n\n  if (computed.length !== signature.length) {\n    return c.json({ error: \"Invalid signature\" }, 401);\n  }\n\n  const a = encoder.encode(computed);\n  const b = encoder.encode(signature);\n  let mismatch = 0;\n  for (let i = 0; i < a.length; i++) {\n    mismatch |= a[i] ^ b[i];\n  }\n  if (mismatch !== 0) {\n    return c.json({ error: \"Invalid signature\" }, 401);\n  }\n\n  const contentType = c.req.header(\"content-type\") ?? \"\";\n  if (contentType.includes(\"application/json\")) {\n    c.set(\"slackBody\", JSON.parse(rawBody));\n  } else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n    const params = new URLSearchParams(rawBody);\n    const payload = params.get(\"payload\");\n    c.set(\"slackBody\", payload ? JSON.parse(payload) : {});\n  }\n\n  await next();\n});\n"
  },
  {
    "path": "apps/server/src/routes/slack/workspace-resolver.ts",
    "content": "import { and, db, eq } from \"@openstatus/db\";\nimport {\n  integration,\n  selectWorkspaceSchema,\n  workspace,\n} from \"@openstatus/db/src/schema\";\nimport type { Workspace } from \"@openstatus/db/src/schema/workspaces/validation\";\n\nexport interface SlackWorkspace {\n  workspace: Workspace;\n  botToken: string;\n  botUserId: string;\n}\n\ninterface IntegrationCredential {\n  botToken: string;\n  botUserId: string;\n}\n\nexport async function resolveWorkspace(\n  teamId: string,\n): Promise<SlackWorkspace | null> {\n  const row = await db\n    .select({\n      workspaceId: integration.workspaceId,\n      credential: integration.credential,\n    })\n    .from(integration)\n    .where(\n      and(\n        eq(integration.name, \"slack-agent\"),\n        eq(integration.externalId, teamId),\n      ),\n    )\n    .get();\n\n  if (!row?.workspaceId) return null;\n\n  const credential = row.credential as IntegrationCredential | null;\n  if (!credential?.botToken) return null;\n\n  const ws = await db\n    .select()\n    .from(workspace)\n    .where(eq(workspace.id, row.workspaceId))\n    .get();\n\n  if (!ws) return null;\n\n  const parsed = selectWorkspaceSchema.safeParse(ws);\n  if (!parsed.success) return null;\n\n  return {\n    workspace: parsed.data,\n    botToken: credential.botToken,\n    botUserId: credential.botUserId ?? \"\",\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/check/http/post.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { afterEach, mock } from \"bun:test\";\nimport { app } from \"@/index\";\n\nconst mockFetch = mock();\n\nglobal.fetch = mockFetch as unknown as typeof fetch;\nmock.module(\"node-fetch\", () => mockFetch);\n\nafterEach(() => {\n  mockFetch.mockReset();\n});\n\ntest(\"Create a single check  \", async () => {\n  const data = {\n    url: \"https://www.openstatus.dev\",\n    regions: [\"ams\"],\n    method: \"POST\",\n    body: '{\"hello\":\"world\"}',\n    headers: [{ key: \"key\", value: \"value\" }],\n  };\n  mockFetch.mockReturnValue(\n    Promise.resolve(\n      new Response(\n        '{\"status\":200,\"latency\":100,\"body\":\"Hello World\",\"headers\":{\"Content-Type\":\"application/json\"},\"timestamp\":1234567890,\"timing\":{\"dnsStart\":1,\"dnsDone\":2,\"connectStart\":3,\"connectDone\":4,\"tlsHandshakeStart\":5,\"tlsHandshakeDone\":6,\"firstByteStart\":7,\"firstByteDone\":8,\"transferStart\":9,\"transferDone\":10},\"region\":\"ams\"}',\n        { status: 200, headers: { \"content-type\": \"application/json\" } },\n      ),\n    ),\n  );\n\n  const res = await app.request(\"/v1/check/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify(data),\n  });\n\n  expect(res.status).toBe(200);\n\n  expect(await res.json()).toMatchObject({\n    id: expect.any(Number),\n    raw: [\n      {\n        connectDone: 4,\n        connectStart: 3,\n        dnsDone: 2,\n        dnsStart: 1,\n        firstByteDone: 8,\n        firstByteStart: 7,\n        tlsHandshakeDone: 6,\n        tlsHandshakeStart: 5,\n        transferDone: 10,\n        transferStart: 9,\n      },\n    ],\n    response: {\n      body: \"Hello World\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      latency: 100,\n      region: \"ams\",\n      status: 200,\n      timestamp: 1234567890,\n      timing: {\n        connectDone: 4,\n        connectStart: 3,\n        dnsDone: 2,\n        dnsStart: 1,\n        firstByteDone: 8,\n        firstByteStart: 7,\n        tlsHandshakeDone: 6,\n        tlsHandshakeStart: 5,\n        transferDone: 10,\n        transferStart: 9,\n      },\n    },\n  });\n});\n\ntest(\"Create a multiple check\", async () => {\n  const data = {\n    url: \"https://www.openstatus.dev\",\n    regions: [\"ams\", \"gru\"],\n    method: \"POST\",\n    body: '{\"hello\":\"world\"}',\n    headers: [{ key: \"key\", value: \"value\" }],\n  };\n\n  const amsResponse = {\n    status: 200,\n    latency: 100,\n    body: \"Hello from ams\",\n    headers: { \"Content-Type\": \"application/json\" },\n    timestamp: 1234567890,\n    timing: {\n      dnsStart: 1,\n      dnsDone: 2,\n      connectStart: 3,\n      connectDone: 4,\n      tlsHandshakeStart: 5,\n      tlsHandshakeDone: 6,\n      firstByteStart: 7,\n      firstByteDone: 8,\n      transferStart: 9,\n      transferDone: 10,\n    },\n    region: \"ams\",\n  };\n\n  const gruResponse = {\n    status: 200,\n    latency: 150,\n    body: \"Hello from gru\",\n    headers: { \"Content-Type\": \"application/json\" },\n    timestamp: 1234567891,\n    timing: {\n      dnsStart: 11,\n      dnsDone: 12,\n      connectStart: 13,\n      connectDone: 14,\n      tlsHandshakeStart: 15,\n      tlsHandshakeDone: 16,\n      firstByteStart: 17,\n      firstByteDone: 18,\n      transferStart: 19,\n      transferDone: 20,\n    },\n    region: \"gru\",\n  };\n\n  mockFetch\n    .mockResolvedValueOnce(\n      new Response(JSON.stringify(amsResponse), {\n        status: 200,\n        headers: { \"content-type\": \"application/json\" },\n      }),\n    )\n    .mockResolvedValueOnce(\n      new Response(JSON.stringify(gruResponse), {\n        status: 200,\n        headers: { \"content-type\": \"application/json\" },\n      }),\n    );\n\n  const res = await app.request(\"/v1/check/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify(data),\n  });\n\n  expect(res.status).toBe(200);\n\n  expect(await res.json()).toMatchObject({\n    id: expect.any(Number),\n    raw: [\n      {\n        connectDone: 4,\n        connectStart: 3,\n        dnsDone: 2,\n        dnsStart: 1,\n        firstByteDone: 8,\n        firstByteStart: 7,\n        tlsHandshakeDone: 6,\n        tlsHandshakeStart: 5,\n        transferDone: 10,\n        transferStart: 9,\n      },\n      {\n        connectDone: 14,\n        connectStart: 13,\n        dnsDone: 12,\n        dnsStart: 11,\n        firstByteDone: 18,\n        firstByteStart: 17,\n        tlsHandshakeDone: 16,\n        tlsHandshakeStart: 15,\n        transferDone: 20,\n        transferStart: 19,\n      },\n    ],\n    response: {\n      body: \"Hello from gru\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      latency: 150,\n      region: \"gru\",\n      status: 200,\n      timestamp: 1234567891,\n      timing: {\n        connectDone: 14,\n        connectStart: 13,\n        dnsDone: 12,\n        dnsStart: 11,\n        firstByteDone: 18,\n        firstByteStart: 17,\n        tlsHandshakeDone: 16,\n        tlsHandshakeStart: 15,\n        transferDone: 20,\n        transferStart: 19,\n      },\n    },\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/check/http/post.ts",
    "content": "import { createRoute, type z } from \"@hono/zod-openapi\";\nimport { getLogger } from \"@logtape/logtape\";\n\nimport { env } from \"@/env\";\nimport { openApiErrorResponses } from \"@/libs/errors\";\nimport { db } from \"@openstatus/db\";\nimport { check } from \"@openstatus/db/src/schema/check\";\nimport percentile from \"percentile\";\nimport type { checkApi } from \"../index\";\n\nconst logger = getLogger(\"api-server\");\nimport {\n  AggregatedResponseSchema,\n  AggregatedResult,\n  CheckPostResponseSchema,\n  CheckSchema,\n  ResponseSchema,\n} from \"./schema\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"check\"],\n  summary: \"Run a single check\",\n  path: \"/http\",\n  request: {\n    body: {\n      description: \"The run request to create\",\n      content: {\n        \"application/json\": {\n          schema: CheckSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: CheckPostResponseSchema,\n        },\n      },\n      description: \"Return a run result\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerHTTPPostCheck(api: typeof checkApi) {\n  return api.openapi(postRoute, async (c) => {\n    const data = c.req.valid(\"json\");\n    const workspaceId = c.get(\"workspace\").id;\n    const input = c.req.valid(\"json\");\n\n    const { headers, regions, runCount, aggregated, ...rest } = data;\n\n    const newCheck = await db\n      .insert(check)\n      .values({\n        workspaceId: workspaceId,\n        regions: regions.join(\",\"),\n        countRequests: runCount,\n        ...rest,\n      })\n      .returning()\n      .get();\n\n    const result = [];\n\n    for (let count = 0; count < input.runCount; count++) {\n      const currentFetch = [];\n      for (const region of input.regions) {\n        const r = fetch(`https://openstatus-checker.fly.dev/ping/${region}`, {\n          headers: {\n            Authorization: `Basic ${env.CRON_SECRET}`,\n            \"Content-Type\": \"application/json\",\n            \"fly-prefer-region\": region,\n          },\n          method: \"POST\",\n          body: JSON.stringify({\n            requestId: newCheck.id,\n            workspaceId: workspaceId,\n            url: input.url,\n            method: input.method,\n            headers: input.headers?.reduce((acc, { key, value }) => {\n              if (!key) return acc; // key === \"\" is an invalid header\n\n              return {\n                // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n                ...acc,\n                [key]: value,\n              };\n            }, {}),\n            body: input.body ? input.body : undefined,\n          }),\n        });\n        currentFetch.push(r);\n      }\n\n      const allResults = await Promise.allSettled(currentFetch);\n      result.push(...allResults);\n    }\n\n    const fulfilledRequest: z.infer<typeof ResponseSchema>[] = [];\n\n    const filteredResult = result.filter((r) => r.status === \"fulfilled\");\n    for await (const r of filteredResult) {\n      if (r.status !== \"fulfilled\") throw new Error(\"No value\");\n\n      const json = await r.value.json();\n      const parsed = ResponseSchema.safeParse(json);\n\n      if (!parsed.success) {\n        logger.error(\"Failed to parse check response\", {\n          check_id: newCheck.id,\n          workspace_id: workspaceId,\n          validation_errors: parsed.error,\n        });\n        throw new Error(`Failed to parse response: ${parsed.error.message}`);\n      }\n\n      fulfilledRequest.push(parsed.data);\n    }\n\n    let aggregatedResponse = null;\n\n    if (aggregated) {\n      const { dns, connect, tls, firstByte, transfer, latency } =\n        getTiming(fulfilledRequest);\n\n      aggregatedResponse = AggregatedResult.parse({\n        dns: getAggregate(dns),\n        connect: getAggregate(connect),\n        tls: getAggregate(tls),\n        firstByte: getAggregate(firstByte),\n        transfer: getAggregate(transfer),\n        latency: getAggregate(latency),\n      });\n    }\n\n    const allTimings = fulfilledRequest.map((r) => r.timing);\n\n    const lastResponse = fulfilledRequest[fulfilledRequest.length - 1];\n    const responseResult = CheckPostResponseSchema.parse({\n      id: newCheck.id,\n      raw: allTimings, // TODO: we should return the region here as well!\n      response: lastResponse,\n      aggregated: aggregatedResponse ? aggregatedResponse : undefined,\n    });\n\n    return c.json(responseResult, 200);\n  });\n}\n\n// This is a helper function to get the timing of the request\n\ntype ReturnGetTiming = Record<\n  \"dns\" | \"connect\" | \"tls\" | \"firstByte\" | \"transfer\" | \"latency\",\n  number[]\n>;\n\nfunction getTiming(data: z.infer<typeof ResponseSchema>[]): ReturnGetTiming {\n  return data.reduce(\n    (prev, curr) => {\n      prev.dns.push(curr.timing.dnsDone - curr.timing.dnsStart);\n      prev.connect.push(curr.timing.connectDone - curr.timing.connectStart);\n      prev.tls.push(\n        curr.timing.tlsHandshakeDone - curr.timing.tlsHandshakeStart,\n      );\n      prev.firstByte.push(\n        curr.timing.firstByteDone - curr.timing.firstByteStart,\n      );\n      prev.transfer.push(curr.timing.transferDone - curr.timing.transferStart);\n      prev.latency.push(curr.latency);\n      return prev;\n    },\n    {\n      dns: [],\n      connect: [],\n      tls: [],\n      firstByte: [],\n      transfer: [],\n      latency: [],\n    } as ReturnGetTiming,\n  );\n}\n\nfunction getAggregate(data: number[]) {\n  const parsed = AggregatedResponseSchema.safeParse({\n    p50: percentile(50, data),\n    p75: percentile(75, data),\n    p95: percentile(95, data),\n    p99: percentile(99, data),\n    min: Math.min(...data),\n    max: Math.max(...data),\n  });\n\n  if (!parsed.success) {\n    logger.error(\"Failed to parse aggregated response\", {\n      validation_errors: parsed.error,\n    });\n    throw new Error(`Failed to parse response: ${parsed.error.message}`);\n  }\n\n  return parsed.data;\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/check/http/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\nimport { MonitorSchema } from \"../../monitors/schema\";\n\nexport const CheckSchema = MonitorSchema.pick({\n  url: true,\n  body: true,\n  headers: true,\n  method: true,\n  regions: true,\n})\n  .extend({\n    runCount: z\n      .number()\n      .max(5)\n      .optional()\n      .prefault(1)\n      .openapi({ description: \"The number of times to run the check\" }),\n    aggregated: z\n      .boolean()\n      .optional()\n      .openapi({ description: \"Whether to aggregate the results or not\" }),\n    //   webhook: z\n    //     .string()\n    //     .optional()\n    //     .openapi({ description: \"The webhook to send the result to\" }),\n  })\n  .openapi({\n    description: \"The check request\",\n  });\n\nexport const TimingSchema = z.object({\n  dnsStart: z\n    .number()\n    .openapi({ description: \"DNS timestamp start time in UTC \" }),\n  dnsDone: z\n    .number()\n    .openapi({ description: \"DNS timestamp end time in UTC \" }),\n  connectStart: z\n    .number()\n    .openapi({ description: \"Connect timestamp start time in UTC \" }),\n  connectDone: z\n    .number()\n    .openapi({ description: \"Connect timestamp end time in UTC \" }),\n  tlsHandshakeStart: z\n    .number()\n    .openapi({ description: \"TLS handshake timestamp start time in UTC \" }),\n  tlsHandshakeDone: z\n    .number()\n    .openapi({ description: \"TLS handshake timestamp end time in UTC \" }),\n  firstByteStart: z\n    .number()\n    .openapi({ description: \"First byte timestamp start time in UTC \" }),\n  firstByteDone: z\n    .number()\n    .openapi({ description: \"First byte timestamp end time in UTC \" }),\n  transferStart: z\n    .number()\n    .openapi({ description: \"Transfer timestamp start time in UTC \" }),\n  transferDone: z\n    .number()\n    .openapi({ description: \"Transfer timestamp end time in UTC \" }),\n});\n\nexport const AggregatedResponseSchema = z\n  .object({\n    p50: z.number().openapi({ description: \"The 50th percentile\" }),\n    p75: z.number().openapi({ description: \"The 75th percentile\" }),\n    p95: z.number().openapi({ description: \"The 95th percentile\" }),\n    p99: z.number().openapi({ description: \"The 99th percentile\" }),\n    min: z.number().openapi({ description: \"The minimum value\" }),\n    max: z.number().openapi({ description: \"The maximum value\" }),\n  })\n  .openapi({\n    description: \"The aggregated data of the check\",\n  });\n\nexport const ResponseSchema = z.object({\n  timestamp: z\n    .number()\n    .openapi({ description: \"The timestamp of the response in UTC\" }),\n  status: z\n    .number()\n    .openapi({ description: \"The status code of the response\" }),\n  latency: z.number().openapi({ description: \"The latency of the response\" }),\n  body: z\n    .string()\n    .optional()\n    .openapi({ description: \"The body of the response\" }),\n  headers: z\n    .record(z.string(), z.string())\n    .optional()\n    .openapi({ description: \"The headers of the response\" }),\n  timing: TimingSchema.openapi({\n    description: \"The timing metrics of the response\",\n  }),\n  aggregated: z\n    .object({\n      dns: AggregatedResponseSchema.openapi({\n        description: \"The aggregated DNS timing of the check\",\n      }),\n      connection: AggregatedResponseSchema.openapi({\n        description: \"The aggregated connection timing of the check\",\n      }),\n      tls: AggregatedResponseSchema.openapi({\n        description: \"The aggregated tls timing of the check\",\n      }),\n      firstByte: AggregatedResponseSchema.openapi({\n        description: \"The aggregated first byte timing of the check\",\n      }),\n      transfer: AggregatedResponseSchema.openapi({\n        description: \"The aggregated transfer timing of the check\",\n      }),\n      latency: AggregatedResponseSchema.openapi({\n        description: \"The aggregated latency timing of the check\",\n      }),\n    })\n    .optional()\n    .openapi({\n      description: \"The aggregated data dns timing of the check\",\n    }),\n  region: z.string().openapi({ description: \"The region where the check ran\" }),\n});\n\nexport const AggregatedResult = z.object({\n  dns: AggregatedResponseSchema,\n  connect: AggregatedResponseSchema,\n  tls: AggregatedResponseSchema,\n  firstByte: AggregatedResponseSchema,\n  transfer: AggregatedResponseSchema,\n  latency: AggregatedResponseSchema,\n});\n\nexport const CheckPostResponseSchema = z.object({\n  id: z.int().openapi({ description: \"The id of the check\" }),\n  raw: z.array(TimingSchema).openapi({\n    description: \"The raw data of the check\",\n  }),\n  response: ResponseSchema.openapi({\n    description: \"The last response of the check\",\n  }),\n  aggregated: AggregatedResult.optional().openapi({\n    description: \"The aggregated data of the check\",\n  }),\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/check/index.ts",
    "content": "import { OpenAPIHono } from \"@hono/zod-openapi\";\n\nimport type { Variables } from \"../index\";\n\nimport { handleZodError } from \"@/libs/errors\";\nimport { registerHTTPPostCheck } from \"./http/post\";\n\nconst checkApi = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\nregisterHTTPPostCheck(checkApi);\n\nexport { checkApi };\n"
  },
  {
    "path": "apps/server/src/routes/v1/incidents/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { IncidentSchema } from \"./schema\";\n\ntest(\"return the incident\", async () => {\n  const res = await app.request(\"/v1/incident/2\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n  const result = IncidentSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/incident/2\");\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid incident id should return 400\", async () => {\n  const res = await app.request(\"/v1/incident/invalid-id\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"invalid incident id should return 404\", async () => {\n  const res = await app.request(\"/v1/incident/2\", {\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/incidents/get.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { and, db, eq } from \"@openstatus/db\";\nimport { incidentTable } from \"@openstatus/db/src/schema/incidents\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport type { incidentsApi } from \"./index\";\nimport { IncidentSchema, ParamsSchema } from \"./schema\";\n\nconst getRoute = createRoute({\n  method: \"get\",\n  tags: [\"incident\"],\n  summary: \"Get an incident\",\n  path: \"/{id}\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: IncidentSchema,\n        },\n      },\n      description: \"Get an incident\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetIncident(app: typeof incidentsApi) {\n  return app.openapi(getRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _incident = await db\n      .select()\n      .from(incidentTable)\n      .where(\n        and(\n          eq(incidentTable.workspaceId, workspaceId),\n          eq(incidentTable.id, Number(id)),\n        ),\n      )\n      .get();\n\n    if (!_incident) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Incident ${id} not found`,\n      });\n    }\n\n    const data = IncidentSchema.parse(_incident);\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/incidents/get_all.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport { incidentTable, monitor } from \"@openstatus/db/src/schema\";\n\nimport { app } from \"@/index\";\nimport { IncidentSchema } from \"./schema\";\n\nconst TEST_PREFIX = \"v1-incident-getall-test\";\nlet testMonitorId: number;\nlet testIncidentId: number;\n\nbeforeAll(async () => {\n  await db\n    .delete(incidentTable)\n    .where(eq(incidentTable.title, `${TEST_PREFIX}-incident`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n\n  const mon = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-monitor`,\n      url: \"https://test.example.com\",\n      periodicity: \"1m\",\n      active: true,\n      regions: \"ams\",\n      jobType: \"http\",\n      method: \"GET\",\n      timeout: 30000,\n    })\n    .returning()\n    .get();\n  testMonitorId = mon.id;\n\n  const incident = await db\n    .insert(incidentTable)\n    .values({\n      workspaceId: 1,\n      monitorId: testMonitorId,\n      title: `${TEST_PREFIX}-incident`,\n      status: \"investigating\",\n      startedAt: new Date(\"2099-01-01T00:00:00Z\"),\n    })\n    .returning()\n    .get();\n  testIncidentId = incident.id;\n});\n\nafterAll(async () => {\n  await db\n    .delete(incidentTable)\n    .where(eq(incidentTable.title, `${TEST_PREFIX}-incident`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n});\n\ntest(\"return all incidents\", async () => {\n  const res = await app.request(\"/v1/incident\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = IncidentSchema.array().safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.some((i) => i.id === testIncidentId)).toBe(true);\n});\n\ntest(\"return empty incidents\", async () => {\n  const res = await app.request(\"/v1/incident\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"3\",\n    },\n  });\n\n  const result = IncidentSchema.array().safeParse(await res.json());\n\n  expect(result.success).toBe(true);\n  expect(res.status).toBe(200);\n  expect(result.data?.length).toBe(0);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/incident\", {\n    method: \"GET\",\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/incidents/get_all.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { incidentTable } from \"@openstatus/db/src/schema/incidents\";\n\nimport { openApiErrorResponses } from \"@/libs/errors\";\nimport type { incidentsApi } from \"./index\";\nimport { IncidentSchema } from \"./schema\";\n\nconst getAllRoute = createRoute({\n  method: \"get\",\n  tags: [\"incident\"],\n  summary: \"List all incidents\",\n  path: \"/\",\n  request: {},\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: IncidentSchema.array(),\n        },\n      },\n      description: \"Get all incidents\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetAllIncidents(app: typeof incidentsApi) {\n  app.openapi(getAllRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n\n    const _incidents = await db\n      .select()\n      .from(incidentTable)\n      .where(eq(incidentTable.workspaceId, workspaceId))\n      .all();\n\n    const data = IncidentSchema.array().parse(_incidents);\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/incidents/index.ts",
    "content": "import { OpenAPIHono } from \"@hono/zod-openapi\";\n\nimport { handleZodError } from \"@/libs/errors\";\nimport type { Variables } from \"../index\";\nimport { registerGetIncident } from \"./get\";\nimport { registerGetAllIncidents } from \"./get_all\";\nimport { registerPutIncident } from \"./put\";\n\nconst incidentsApi = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\nregisterGetAllIncidents(incidentsApi);\nregisterGetIncident(incidentsApi);\nregisterPutIncident(incidentsApi);\n\nexport { incidentsApi };\n"
  },
  {
    "path": "apps/server/src/routes/v1/incidents/put.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { IncidentSchema } from \"./schema\";\n\ntest(\"acknlowledge the incident\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/incident/2\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      acknowledgedAt: date.toISOString(),\n    }),\n  });\n\n  const result = IncidentSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.acknowledgedAt?.toISOString()).toBe(date.toISOString());\n});\n\ntest(\"resolve the incident\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/incident/2\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      resolvedAt: date.toISOString(),\n    }),\n  });\n\n  const result = IncidentSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.resolvedAt?.toISOString()).toBe(date.toISOString());\n});\n\ntest(\"invalid payload should return 400\", async () => {\n  const res = await app.request(\"/v1/incident/2\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      acknowledgedAt: \"helloworld\",\n    }),\n  });\n\n  const result = (await res.json()) as Record<string, unknown>;\n  expect(result.message).toBe(\n    \"invalid_type in 'acknowledgedAt': Invalid input: expected date, received Date\",\n  );\n  expect(res.status).toBe(400);\n});\n\ntest(\"invalid incident id should return 400\", async () => {\n  const res = await app.request(\"/v1/incident/invalid-id\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      acknowledgedAt: new Date().toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"empty body should return 400\", async () => {\n  const res = await app.request(\"/v1/incident/2\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({}),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"invalid incident id should return 404\", async () => {\n  const res = await app.request(\"/v1/incident/404\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      acknowledgedAt: new Date().toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/incident/2\", {\n    method: \"PUT\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      acknowledgedAt: new Date().toISOString(),\n    }),\n  });\n  expect(res.status).toBe(401);\n});\n\ntest(\"update the incident with invalid data should return 400\", async () => {\n  const res = await app.request(\"/v1/incident/2\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      acknowledgedAt: \"2023-11-0\",\n    }),\n  });\n  expect(res.status).toBe(400);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/incidents/put.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { and, db, eq } from \"@openstatus/db\";\nimport { incidentTable } from \"@openstatus/db/src/schema/incidents\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { Events } from \"@openstatus/analytics\";\nimport type { incidentsApi } from \"./index\";\nimport { IncidentSchema, ParamsSchema } from \"./schema\";\n\nconst putRoute = createRoute({\n  method: \"put\",\n  tags: [\"incident\"],\n  summary: \"Update an incident\",\n  description: \"Acknowledge or resolve an incident\",\n  path: \"/{id}\",\n  middleware: [trackMiddleware(Events.UpdateIncident)],\n  request: {\n    params: ParamsSchema,\n    body: {\n      description: \"The incident to update\",\n      content: {\n        \"application/json\": {\n          schema: IncidentSchema.pick({\n            acknowledgedAt: true,\n            resolvedAt: true,\n          })\n            .partial()\n            .refine(\n              (data) =>\n                data.acknowledgedAt !== undefined ||\n                data.resolvedAt !== undefined,\n              \"Either acknowledgedAt or resolvedAt must be provided\",\n            ),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: IncidentSchema,\n        },\n      },\n      description: \"Update a monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPutIncident(app: typeof incidentsApi) {\n  return app.openapi(putRoute, async (c) => {\n    const input = c.req.valid(\"json\");\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _incident = await db\n      .select()\n      .from(incidentTable)\n      .where(\n        and(\n          eq(incidentTable.id, Number(id)),\n          eq(incidentTable.workspaceId, workspaceId),\n        ),\n      )\n      .get();\n\n    if (!_incident) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Incident ${id} not found`,\n      });\n    }\n\n    const _newIncident = await db\n      .update(incidentTable)\n      // TODO: we should set the acknowledgedBy and resolvedBy fields\n      .set({ ...input, updatedAt: new Date() })\n      .where(eq(incidentTable.id, Number(id)))\n      .returning()\n      .get();\n\n    const data = IncidentSchema.parse(_newIncident);\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/incidents/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nexport const ParamsSchema = z.object({\n  id: z\n    .string()\n    .min(1)\n    .regex(/^\\d+$/, \"ID must be a numeric string\")\n    .openapi({\n      param: {\n        name: \"id\",\n        in: \"path\",\n      },\n      description: \"The id of the Incident\",\n      example: \"1\",\n    }),\n});\n\nexport const IncidentSchema = z\n  .object({\n    id: z.number().openapi({\n      description: \"The id of the incident\",\n      example: 1,\n    }),\n    startedAt: z.coerce.date().openapi({\n      description: \"The date the incident started\",\n    }),\n    monitorId: z.number().nullable().openapi({\n      description: \"The id of the monitor associated with the incident\",\n      example: 1,\n    }),\n    acknowledgedAt: z.coerce.date().optional().nullable().openapi({\n      description: \"The date the incident was acknowledged\",\n    }),\n    acknowledgedBy: z.number().nullable().openapi({\n      description: \"The user who acknowledged the incident\",\n    }),\n    resolvedAt: z.coerce.date().optional().nullable().openapi({\n      description: \"The date the incident was resolved\",\n    }),\n    resolvedBy: z.number().nullable().openapi({\n      description: \"The user who resolved the incident\",\n    }),\n  })\n  .openapi(\"Incident\");\n\nexport type IncidentSchema = z.infer<typeof IncidentSchema>;\n"
  },
  {
    "path": "apps/server/src/routes/v1/index.ts",
    "content": "import { OpenAPIHono } from \"@hono/zod-openapi\";\nimport { Scalar } from \"@scalar/hono-api-reference\";\nimport { cors } from \"hono/cors\";\nimport type { RequestIdVariables } from \"hono/request-id\";\n\nimport { handleZodError } from \"@/libs/errors\";\nimport { authMiddleware } from \"@/libs/middlewares\";\nimport type { Workspace } from \"@openstatus/db/src/schema\";\nimport { checkApi } from \"./check\";\nimport { incidentsApi } from \"./incidents\";\nimport { maintenancesApi } from \"./maintenances\";\nimport { monitorsApi } from \"./monitors\";\nimport { notificationsApi } from \"./notifications\";\nimport { pageSubscribersApi } from \"./pageSubscribers\";\nimport { pagesApi } from \"./pages\";\nimport { statusReportUpdatesApi } from \"./statusReportUpdates\";\nimport { statusReportsApi } from \"./statusReports\";\nimport { whoamiApi } from \"./whoami\";\n\nexport type Variables = RequestIdVariables & {\n  workspace: Workspace;\n};\n\nexport const api = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\napi.use(\"/openapi\", cors());\n\napi.openAPIRegistry.registerComponent(\"securitySchemes\", \"ApiKeyAuth\", {\n  type: \"apiKey\",\n  in: \"header\",\n  name: \"x-openstatus-key\",\n  \"x-openstatus-key\": \"string\",\n});\n// this is a fix for the memory leak\nif (process.env.NODE_ENV === \"production\") {\n  api.get(\"/openapi\", (c) =>\n    c.redirect(\"https://api.openstatus.dev/openapi-v1.json\"),\n  );\n} else {\n  api.doc(\"/openapi\", {\n    openapi: \"3.0.0\",\n    info: {\n      version: \"1.0.0\",\n      title: \"OpenStatus API\",\n      contact: {\n        email: \"ping@openstatus.dev\",\n        url: \"https://www.openstatus.dev\",\n      },\n      description:\n        \"This version is deprecated please use v2 API: Read more about the new API in the documentation: https://docs.openstatus.dev/reference/api\",\n    },\n    tags: [\n      {\n        name: \"monitor\",\n        description: \"Monitor related endpoints\",\n        \"x-displayName\": \"Monitor\",\n      },\n      {\n        name: \"page\",\n        description: \"Page related endpoints\",\n        \"x-displayName\": \"Page\",\n      },\n      {\n        name: \"status_report\",\n        description: \"Status report related endpoints\",\n        \"x-displayName\": \"Status Report\",\n      },\n      {\n        name: \"status_report_update\",\n        description: \"Status report update related endpoints\",\n        \"x-displayName\": \"Status Report Update\",\n      },\n      {\n        name: \"incident\",\n        description: \"Incident related endpoints\",\n        \"x-displayName\": \"Incident\",\n      },\n      {\n        name: \"maintenance\",\n        description: \"Maintenance related endpoints\",\n        \"x-displayName\": \"Maintenance\",\n      },\n      {\n        name: \"notification\",\n        description: \"Notification related endpoints\",\n        \"x-displayName\": \"Notification\",\n      },\n      {\n        name: \"page_subscriber\",\n        description: \"Page subscriber related endpoints\",\n        \"x-displayName\": \"Page Subscriber\",\n      },\n      {\n        name: \"check\",\n        description: \"Check related endpoints\",\n        \"x-displayName\": \"Check\",\n      },\n      {\n        name: \"whoami\",\n        description: \"WhoAmI related endpoints\",\n        \"x-displayName\": \"WhoAmI\",\n      },\n    ],\n    security: [\n      {\n        ApiKeyAuth: [],\n      },\n    ],\n  });\n}\napi.get(\n  \"/\",\n  Scalar({\n    url: \"/openapi-v1.json\",\n    servers: [\n      {\n        url: \"https://api.openstatus.dev/v1\",\n        description: \"Production server\",\n      },\n      {\n        url: \"http://localhost:3000/v1\",\n        description: \"Dev server\",\n      },\n    ],\n    metaData: {\n      title: \"OpenStatus API\",\n      description: \"Start building with OpenStatus API\",\n      ogDescription: \"API Reference\",\n      ogTitle: \"OpenStatus API\",\n      ogImage:\n        \"https://openstatus.dev/api/og?title=OpenStatus%20API&description=API%20Reference\",\n      twitterCard: \"summary_large_image\",\n    },\n  }),\n);\n/**\n * Middlewares\n */\napi.use(\"/*\", authMiddleware);\n\n/**\n * Routes\n */\napi.route(\"/monitor\", monitorsApi);\napi.route(\"/page\", pagesApi);\napi.route(\"/status_report\", statusReportsApi);\napi.route(\"/status_report_update\", statusReportUpdatesApi);\napi.route(\"/incident\", incidentsApi);\napi.route(\"/maintenance\", maintenancesApi);\napi.route(\"/notification\", notificationsApi);\napi.route(\"/page_subscriber\", pageSubscribersApi);\napi.route(\"/check\", checkApi);\napi.route(\"/whoami\", whoamiApi);\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\nimport { app } from \"@/index\";\nimport { MaintenanceSchema } from \"./schema\";\n\ntest(\"return the maintenance\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"return the maintenance with monitorIds\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitorIds).toBeDefined();\n  expect(Array.isArray(result.data?.monitorIds)).toBe(true);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\");\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid maintenance id should return 400\", async () => {\n  const res = await app.request(\"/v1/maintenance/invalid-id\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"invalid maintenance id should return 404\", async () => {\n  const res = await app.request(\"/v1/maintenance/999\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/get.ts",
    "content": "import { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { notEmpty } from \"@/utils/not-empty\";\nimport { createRoute } from \"@hono/zod-openapi\";\nimport { and, db, eq } from \"@openstatus/db\";\nimport { maintenance } from \"@openstatus/db/src/schema/maintenances\";\nimport type { maintenancesApi } from \"./index\";\nimport { MaintenanceSchema, ParamsSchema } from \"./schema\";\n\nconst getRoute = createRoute({\n  method: \"get\",\n  tags: [\"maintenance\"],\n  summary: \"Get a maintenance\",\n  path: \"/{id}\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MaintenanceSchema,\n        },\n      },\n      description: \"Get a maintenance\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetMaintenance(api: typeof maintenancesApi) {\n  return api.openapi(getRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _maintenance = await db.query.maintenance.findFirst({\n      with: {\n        maintenancesToPageComponents: { with: { pageComponent: true } },\n      },\n      where: and(\n        eq(maintenance.id, Number(id)),\n        eq(maintenance.workspaceId, workspaceId),\n      ),\n    });\n\n    if (!_maintenance) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Maintenance ${id} not found`,\n      });\n    }\n\n    const data = MaintenanceSchema.parse({\n      ..._maintenance,\n      monitorIds: _maintenance.maintenancesToPageComponents\n        .map((m) => m.pageComponent.monitorId)\n        .filter(notEmpty),\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/get_all.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  maintenance,\n  maintenancesToPageComponents,\n  monitor,\n  pageComponent,\n} from \"@openstatus/db/src/schema\";\n\nimport { app } from \"@/index\";\nimport { MaintenanceSchema } from \"./schema\";\n\nconst TEST_PREFIX = \"v1-maint-getall-test\";\nlet testMonitorId: number;\nlet testPageComponentId: number;\nlet testMaintenanceId: number;\n\nbeforeAll(async () => {\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.title, `${TEST_PREFIX}-maint`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n\n  const mon = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-monitor`,\n      url: \"https://test.example.com\",\n      periodicity: \"1m\",\n      active: true,\n      regions: \"ams\",\n      jobType: \"http\",\n      method: \"GET\",\n      timeout: 30000,\n    })\n    .returning()\n    .get();\n  testMonitorId = mon.id;\n\n  const comp = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      monitorId: testMonitorId,\n      type: \"monitor\",\n      name: `${TEST_PREFIX}-component`,\n      order: 200,\n    })\n    .returning()\n    .get();\n  testPageComponentId = comp.id;\n\n  const maint = await db\n    .insert(maintenance)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-maint`,\n      message: \"Test maintenance\",\n      from: new Date(\"2099-01-01T00:00:00Z\"),\n      to: new Date(\"2099-01-02T00:00:00Z\"),\n    })\n    .returning()\n    .get();\n  testMaintenanceId = maint.id;\n\n  await db.insert(maintenancesToPageComponents).values({\n    maintenanceId: testMaintenanceId,\n    pageComponentId: testPageComponentId,\n  });\n});\n\nafterAll(async () => {\n  await db\n    .delete(maintenancesToPageComponents)\n    .where(eq(maintenancesToPageComponents.maintenanceId, testMaintenanceId));\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.title, `${TEST_PREFIX}-maint`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n});\n\ntest(\"return all maintenances\", async () => {\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = MaintenanceSchema.array().safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.some((m) => m.id === testMaintenanceId)).toBe(true);\n});\n\ntest(\"return all maintenances with monitorIds\", async () => {\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = MaintenanceSchema.array().safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  const testMaint = result.data?.find((m) => m.id === testMaintenanceId);\n  expect(testMaint).toBeDefined();\n  expect(testMaint?.monitorIds).toBeDefined();\n  expect(Array.isArray(testMaint?.monitorIds)).toBe(true);\n  expect(testMaint?.monitorIds).toContain(testMonitorId);\n});\n\ntest(\"return empty maintenances\", async () => {\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"3\",\n    },\n  });\n\n  const result = MaintenanceSchema.array().safeParse(await res.json());\n\n  expect(result.success).toBe(true);\n  expect(res.status).toBe(200);\n  expect(result.data?.length).toBe(0);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"GET\",\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/get_all.ts",
    "content": "import { openApiErrorResponses } from \"@/libs/errors\";\nimport { notEmpty } from \"@/utils/not-empty\";\nimport { createRoute } from \"@hono/zod-openapi\";\nimport { db, desc, eq } from \"@openstatus/db\";\nimport { maintenance } from \"@openstatus/db/src/schema/maintenances\";\nimport type { maintenancesApi } from \"./index\";\nimport { MaintenanceSchema } from \"./schema\";\n\nconst getAllRoute = createRoute({\n  method: \"get\",\n  tags: [\"maintenance\"],\n  summary: \"List all maintenances\",\n  path: \"/\",\n  request: {},\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MaintenanceSchema.array(),\n        },\n      },\n      description: \"Get all maintenances\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetAllMaintenances(api: typeof maintenancesApi) {\n  return api.openapi(getAllRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n\n    const _maintenances = await db.query.maintenance.findMany({\n      with: {\n        maintenancesToPageComponents: { with: { pageComponent: true } },\n      },\n      where: eq(maintenance.workspaceId, workspaceId),\n      orderBy: desc(maintenance.createdAt),\n    });\n\n    const data = MaintenanceSchema.array().parse(\n      _maintenances.map((m) => ({\n        ...m,\n        monitorIds: m.maintenancesToPageComponents\n          .map((mtm) => mtm.pageComponent.monitorId)\n          .filter(notEmpty),\n      })),\n    );\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/index.ts",
    "content": "import { handleZodError } from \"@/libs/errors\";\nimport { OpenAPIHono } from \"@hono/zod-openapi\";\nimport type { Variables } from \"../index\";\nimport { registerGetMaintenance } from \"./get\";\nimport { registerGetAllMaintenances } from \"./get_all\";\nimport { registerPostMaintenance } from \"./post\";\nimport { registerPutMaintenance } from \"./put\";\n\nconst maintenancesApi = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\nregisterGetAllMaintenances(maintenancesApi);\nregisterGetMaintenance(maintenancesApi);\nregisterPostMaintenance(maintenancesApi);\nregisterPutMaintenance(maintenancesApi);\n\nexport { maintenancesApi };\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/post.test.ts",
    "content": "import { beforeEach, expect, test } from \"bun:test\";\nimport { app } from \"@/index\";\nimport { db, eq } from \"@openstatus/db\";\nimport { maintenance } from \"@openstatus/db/src/schema\";\nimport { MaintenanceSchema } from \"./schema\";\n\n// biome-ignore lint/suspicious/noExplicitAny: test utility\nconst spies = (globalThis as any).__subscriptionSpies as {\n  dispatchMaintenanceUpdate: {\n    mockClear: () => void;\n    mock: { calls: number[][] };\n  };\n};\n\nbeforeEach(() => {\n  spies.dispatchMaintenanceUpdate.mockClear();\n});\n\ntest(\"create a valid maintenance without monitorIds\", async () => {\n  const from = new Date();\n  const to = new Date(from.getTime() + 3600000); // 1 hour later\n\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Another Maintenance\",\n      message: \"Scheduled maintenance without monitors\",\n      from: from.toISOString(),\n      to: to.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitorIds?.length).toBe(0);\n\n  // Cleanup: delete the created maintenance\n  if (result.success) {\n    await db.delete(maintenance).where(eq(maintenance.id, result.data.id));\n  }\n});\n\ntest(\"create a maintenance with `from` date after `to` date should return 400\", async () => {\n  const to = new Date();\n  const from = new Date(to.getTime() + 3600000); // from is 1 hour after to\n\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Invalid Dates\",\n      message: \"Test message\",\n      from: from.toISOString(),\n      to: to.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create a maintenance with non-existent monitorIds should return 400\", async () => {\n  const from = new Date();\n  const to = new Date(from.getTime() + 3600000);\n\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Invalid Monitors\",\n      message: \"Test message\",\n      from: from.toISOString(),\n      to: to.toISOString(),\n      monitorIds: [9999], // Non-existent monitor ID\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create a maintenance with non-existent pageId should return 400\", async () => {\n  const from = new Date();\n  const to = new Date(from.getTime() + 3600000);\n\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Invalid Page\",\n      message: \"Test message\",\n      from: from.toISOString(),\n      to: to.toISOString(),\n      monitorIds: [1],\n      pageId: 9999, // Non-existent page ID\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create a maintenance with empty body should return 400\", async () => {\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({}),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create a valid maintenance\", async () => {\n  const from = new Date();\n  const to = new Date(from.getTime() + 3600000); // 1 hour later\n\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Database Upgrade\",\n      message: \"Scheduled database maintenance\",\n      from: from.toISOString(),\n      to: to.toISOString(),\n      monitorIds: [1],\n      pageId: 1,\n    }),\n  });\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitorIds?.length).toBe(1);\n  if (result.success) {\n    await db.delete(maintenance).where(eq(maintenance.id, result.data.id));\n  }\n});\n\ntest(\"create a maintenance with multiple monitorIds\", async () => {\n  const from = new Date();\n  const to = new Date(from.getTime() + 3600000); // 1 hour later\n\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Multi-Monitor Maintenance\",\n      message: \"Maintenance affecting multiple monitors\",\n      from: from.toISOString(),\n      to: to.toISOString(),\n      monitorIds: [1, 2],\n      pageId: 1,\n    }),\n  });\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitorIds?.length).toBe(2);\n  expect(result.data?.monitorIds).toEqual(expect.arrayContaining([1, 2]));\n\n  // Cleanup: delete the created maintenance\n  if (result.success) {\n    await db.delete(maintenance).where(eq(maintenance.id, result.data.id));\n  }\n});\n\ntest(\"create a maintenance with invalid dates should return 400\", async () => {\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Invalid Maintenance\",\n      message: \"Test message\",\n      from: \"invalid-date\",\n      to: \"invalid-date\",\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"create a maintenance calls dispatchMaintenanceUpdate\", async () => {\n  const from = new Date();\n  const to = new Date(from.getTime() + 3600000);\n\n  const res = await app.request(\"/v1/maintenance\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Dispatch Test Maintenance\",\n      message: \"Testing dispatcher integration\",\n      from: from.toISOString(),\n      to: to.toISOString(),\n      monitorIds: [1],\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = MaintenanceSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n  expect(spies.dispatchMaintenanceUpdate.mock.calls.length).toBe(1);\n  expect(spies.dispatchMaintenanceUpdate.mock.calls[0][0]).toBeNumber();\n\n  if (result.success) {\n    await db.delete(maintenance).where(eq(maintenance.id, result.data.id));\n  }\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/post.ts",
    "content": "import { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { createRoute } from \"@hono/zod-openapi\";\nimport { Events } from \"@openstatus/analytics\";\nimport { and, db, eq, inArray, isNull } from \"@openstatus/db\";\nimport { monitor, page } from \"@openstatus/db/src/schema\";\nimport { maintenance } from \"@openstatus/db/src/schema/maintenances\";\nimport {\n  maintenancesToPageComponents,\n  pageComponent,\n} from \"@openstatus/db/src/schema/page_components\";\nimport { dispatchMaintenanceUpdate } from \"@openstatus/subscriptions\";\nimport type { maintenancesApi } from \"./index\";\nimport { MaintenanceSchema } from \"./schema\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"maintenance\"],\n  summary: \"Create a maintenance\",\n  path: \"/\",\n  middleware: [trackMiddleware(Events.CreateMaintenance)],\n  request: {\n    body: {\n      content: {\n        \"application/json\": {\n          schema: MaintenanceSchema.omit({ id: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MaintenanceSchema,\n        },\n      },\n      description: \"Create a maintenance\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostMaintenance(api: typeof maintenancesApi) {\n  return api.openapi(postRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const input = c.req.valid(\"json\");\n    const limits = c.get(\"workspace\").limits;\n\n    const { monitorIds, pageId } = input;\n\n    if (input.from > input.to) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message: \"`date.from` cannot be after `date.to`\",\n      });\n    }\n\n    const _newMaintenance = await db.transaction(async (tx) => {\n      const _monitors = await tx\n        .select()\n        .from(monitor)\n        .where(\n          and(\n            inArray(monitor.id, monitorIds),\n            eq(monitor.workspaceId, workspaceId),\n            isNull(monitor.deletedAt),\n          ),\n        )\n        .all();\n\n      if (_monitors.length !== monitorIds.length) {\n        throw new OpenStatusApiError({\n          code: \"BAD_REQUEST\",\n          message: `Some of the monitors ${monitorIds.join(\", \")} not found`,\n        });\n      }\n\n      const _page = await tx\n        .select()\n        .from(page)\n        .where(and(eq(page.id, pageId), eq(page.workspaceId, workspaceId)))\n        .get();\n\n      if (!_page) {\n        throw new OpenStatusApiError({\n          code: \"BAD_REQUEST\",\n          message: `Page ${pageId} not found`,\n        });\n      }\n\n      const newMaintenance = await tx\n        .insert(maintenance)\n        .values({\n          ...input,\n          workspaceId,\n        })\n        .returning()\n        .get();\n\n      if (monitorIds?.length && newMaintenance.pageId) {\n        // Get page components for the given monitors and page\n        const pageComponents = await tx\n          .select({ id: pageComponent.id })\n          .from(pageComponent)\n          .where(\n            and(\n              inArray(pageComponent.monitorId, monitorIds),\n              eq(pageComponent.pageId, newMaintenance.pageId),\n            ),\n          )\n          .all();\n\n        if (pageComponents.length > 0) {\n          // Insert to maintenancesToPageComponents\n          await tx\n            .insert(maintenancesToPageComponents)\n            .values(\n              pageComponents.map((pc) => ({\n                maintenanceId: newMaintenance.id,\n                pageComponentId: pc.id,\n              })),\n            )\n            .run();\n        }\n      }\n\n      return newMaintenance;\n    });\n\n    if (limits[\"status-subscribers\"] && _newMaintenance.pageId) {\n      await dispatchMaintenanceUpdate(_newMaintenance.id);\n    }\n\n    const data = MaintenanceSchema.parse({\n      ..._newMaintenance,\n      monitorIds: input.monitorIds,\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/put.test.ts",
    "content": "import { expect, test } from \"bun:test\";\nimport { app } from \"@/index\";\nimport { MaintenanceSchema } from \"./schema\";\n\ntest(\"update the maintenance\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Updated Maintenance\",\n      message: \"Updated message\",\n    }),\n  });\n\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.title).toBe(\"Updated Maintenance\");\n});\n\ntest(\"update maintenance monitors\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      monitorIds: [1, 2],\n    }),\n  });\n\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitorIds?.length).toBe(2);\n});\n\ntest(\"invalid maintenance id should return 400\", async () => {\n  const res = await app.request(\"/v1/maintenance/invalid-id\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Not Found\",\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"update only the title\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Only Title Updated\",\n    }),\n  });\n\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.title).toBe(\"Only Title Updated\");\n});\n\ntest(\"update only the message\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      message: \"Only Message Updated\",\n    }),\n  });\n\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.message).toBe(\"Only Message Updated\");\n});\n\ntest.todo(\"update only the dates\", async () => {\n  const from = new Date();\n  const to = new Date(from.getTime() + 7200000); // 2 hours later\n\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      from: from.toISOString(),\n      to: to.toISOString(),\n    }),\n  });\n\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.from).toEqual(from);\n  expect(result.data?.to).toEqual(to);\n});\n\ntest.todo(\n  \"update maintenance with `from` date after `to` date should return 400\",\n  async () => {\n    const to = new Date();\n    const from = new Date(to.getTime() + 3600000); // from is 1 hour after to\n\n    const res = await app.request(\"/v1/maintenance/1\", {\n      method: \"PUT\",\n      headers: {\n        \"x-openstatus-key\": \"1\",\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({\n        from: from.toISOString(),\n        to: to.toISOString(),\n      }),\n    });\n\n    expect(res.status).toBe(400);\n  },\n);\n\ntest(\"remove all maintenance monitors\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      monitorIds: [],\n    }),\n  });\n\n  const result = MaintenanceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitorIds?.length).toBe(0);\n});\n\ntest.todo(\"empty body should return 400\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({}),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"invalid maintenance id should return 404\", async () => {\n  const res = await app.request(\"/v1/maintenance/999\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Not Found\",\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({}),\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"update with invalid monitor ids should return 400\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      monitorIds: [999], // Non-existent monitor\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"update with invalid page id should return 400\", async () => {\n  const res = await app.request(\"/v1/maintenance/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      pageId: 999, // Non-existent page\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/put.ts",
    "content": "import { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { createRoute } from \"@hono/zod-openapi\";\nimport { Events } from \"@openstatus/analytics\";\nimport { and, db, eq, inArray, isNull } from \"@openstatus/db\";\nimport { monitor, page } from \"@openstatus/db/src/schema\";\nimport { maintenance } from \"@openstatus/db/src/schema/maintenances\";\nimport {\n  maintenancesToPageComponents,\n  pageComponent,\n} from \"@openstatus/db/src/schema/page_components\";\nimport type { maintenancesApi } from \"./index\";\nimport { MaintenanceSchema, ParamsSchema } from \"./schema\";\n\nconst putRoute = createRoute({\n  method: \"put\",\n  tags: [\"maintenance\"],\n  summary: \"Update a maintenance\",\n  path: \"/{id}\",\n  middleware: [trackMiddleware(Events.UpdateMaintenance)],\n  request: {\n    params: ParamsSchema,\n    body: {\n      content: {\n        \"application/json\": {\n          schema: MaintenanceSchema.omit({ id: true }).partial(),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MaintenanceSchema,\n        },\n      },\n      description: \"Update a maintenance\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPutMaintenance(api: typeof maintenancesApi) {\n  return api.openapi(putRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n    const input = c.req.valid(\"json\");\n\n    const { monitorIds, pageId } = input;\n\n    const _maintenance = await db.query.maintenance.findFirst({\n      with: {\n        maintenancesToPageComponents: {\n          with: {\n            pageComponent: true,\n          },\n        },\n      },\n      where: and(\n        eq(maintenance.id, Number(id)),\n        eq(maintenance.workspaceId, workspaceId),\n      ),\n    });\n\n    if (!_maintenance) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Maintenance ${id} not found`,\n      });\n    }\n\n    if (monitorIds?.length) {\n      const _monitors = await db\n        .select()\n        .from(monitor)\n        .where(\n          and(\n            inArray(monitor.id, monitorIds),\n            eq(monitor.workspaceId, workspaceId),\n            isNull(monitor.deletedAt),\n          ),\n        )\n        .all();\n\n      if (_monitors.length !== monitorIds.length) {\n        throw new OpenStatusApiError({\n          code: \"BAD_REQUEST\",\n          message: `Some of the monitors ${monitorIds.join(\", \")} not found`,\n        });\n      }\n    }\n\n    if (pageId) {\n      const _page = await db\n        .select()\n        .from(page)\n        .where(and(eq(page.id, pageId), eq(page.workspaceId, workspaceId)))\n        .get();\n\n      if (!_page) {\n        throw new OpenStatusApiError({\n          code: \"BAD_REQUEST\",\n          message: `Page ${pageId} not found`,\n        });\n      }\n    }\n\n    const inputFrom = input?.from ?? _maintenance.from;\n    const inputTo = input?.to ?? _maintenance?.to;\n\n    if (inputFrom && inputTo && new Date(inputFrom) > new Date(inputTo)) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message: \"`date.from` cannot be after `date.to`\",\n      });\n    }\n\n    const updatedMaintenance = await db.transaction(async (tx) => {\n      const updated = await tx\n        .update(maintenance)\n        .set({\n          ...input,\n          updatedAt: new Date(),\n        })\n        .where(eq(maintenance.id, Number(id)))\n        .returning()\n        .get();\n\n      if (monitorIds) {\n        // Delete from maintenancesToPageComponents\n        await tx\n          .delete(maintenancesToPageComponents)\n          .where(eq(maintenancesToPageComponents.maintenanceId, Number(id)))\n          .run();\n\n        // Add new associations\n        if (monitorIds.length > 0 && updated.pageId) {\n          // Get page components for the new monitors\n          const pageComponents = await tx\n            .select({ id: pageComponent.id })\n            .from(pageComponent)\n            .where(\n              and(\n                inArray(pageComponent.monitorId, monitorIds),\n                eq(pageComponent.pageId, updated.pageId),\n              ),\n            )\n            .all();\n\n          if (pageComponents.length > 0) {\n            // Insert to maintenancesToPageComponents\n            await tx\n              .insert(maintenancesToPageComponents)\n              .values(\n                pageComponents.map((pc) => ({\n                  maintenanceId: Number(id),\n                  pageComponentId: pc.id,\n                })),\n              )\n              .run();\n          }\n        }\n      }\n\n      return updated;\n    });\n\n    const data = MaintenanceSchema.parse({\n      ...updatedMaintenance,\n      monitorIds:\n        monitorIds ??\n        _maintenance.maintenancesToPageComponents\n          .map((m) => m.pageComponent.monitorId)\n          .filter((id): id is number => id !== null),\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/maintenances/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nexport const ParamsSchema = z.object({\n  id: z\n    .string()\n    .min(1)\n    .regex(/^\\d+$/, \"ID must be a numeric string\")\n    .openapi({\n      param: {\n        name: \"id\",\n        in: \"path\",\n      },\n      description: \"The id of the maintenance\",\n      example: \"1\",\n    }),\n});\n\nexport const MaintenanceSchema = z\n  .object({\n    id: z.number().openapi({\n      description: \"The id of the maintenance\",\n      example: 1,\n    }),\n    title: z.string().openapi({\n      description: \"The title of the maintenance\",\n      example: \"Database Upgrade\",\n    }),\n    message: z.string().openapi({\n      description: \"The message describing the maintenance\",\n      example: \"Upgrading database to improve performance\",\n    }),\n    from: z.coerce.date().openapi({\n      description: \"When the maintenance starts\",\n    }),\n    to: z.coerce.date().openapi({\n      description: \"When the maintenance ends\",\n    }),\n    monitorIds: z\n      .array(z.number())\n      .optional()\n      .prefault([])\n      .openapi({ description: \"IDs of affected monitors\" }),\n    pageId: z.number().openapi({\n      description: \"The id of the status page this maintenance belongs to\",\n    }),\n  })\n  .refine((maintenance) => maintenance.from <= maintenance.to, {\n    error: \"'from' date must be before 'to' date\",\n  })\n  .openapi(\"Maintenance\");\n\nexport type MaintenanceSchema = z.infer<typeof MaintenanceSchema>;\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/delete.test.ts",
    "content": "import { expect, test } from \"bun:test\";\nimport { app } from \"@/index\";\nimport { MonitorSchema } from \"./schema\";\n\ntest(\"delete the monitor\", async () => {\n  // First create a monitor to delete\n  const createRes = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Monitor to delete\",\n      regions: [\"ams\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"GET\",\n      },\n    }),\n  });\n\n  const created = MonitorSchema.safeParse(await createRes.json());\n  expect(createRes.status).toBe(200);\n  expect(created.success).toBe(true);\n\n  // Now delete it\n  const res = await app.request(`/v1/monitor/${created.data?.id}`, {\n    method: \"DELETE\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(200);\n  expect(await res.json()).toMatchObject({});\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/2\", { method: \"DELETE\" });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid monitor id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/404\", {\n    method: \"DELETE\",\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/delete.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq, isNull } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { Events } from \"@openstatus/analytics\";\nimport type { monitorsApi } from \"./index\";\nimport { ParamsSchema } from \"./schema\";\n\nconst deleteRoute = createRoute({\n  method: \"delete\",\n  tags: [\"monitor\"],\n  summary: \"Delete a monitor\",\n  path: \"/{id}\",\n  request: {\n    params: ParamsSchema,\n  },\n  middleware: [trackMiddleware(Events.DeleteMonitor)],\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: z.object({}),\n        },\n      },\n      description: \"The monitor was successfully deleted\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerDeleteMonitor(app: typeof monitorsApi) {\n  return app.openapi(deleteRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _monitor = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(\n          eq(monitor.id, Number(id)),\n          eq(monitor.workspaceId, workspaceId),\n          isNull(monitor.deletedAt),\n        ),\n      )\n      .get();\n\n    if (!_monitor) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    await db\n      .update(monitor)\n      .set({ active: false, deletedAt: new Date() })\n      .where(eq(monitor.id, Number(id)))\n      .run();\n\n    // FIXME: Remove all relations of the monitor from all notifications, pages,....\n\n    return c.json({}, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { MonitorSchema } from \"./schema\";\n\ntest(\"return the monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/1\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/2\");\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid monitor id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/2\", {\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/get.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq, isNull } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport type { monitorsApi } from \"./index\";\nimport { MonitorSchema, ParamsSchema } from \"./schema\";\n\nconst getRoute = createRoute({\n  method: \"get\",\n  tags: [\"monitor\"],\n  summary: \"Get a monitor\",\n  path: \"/{id}\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema,\n        },\n      },\n      description: \"The monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetMonitor(api: typeof monitorsApi) {\n  return api.openapi(getRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _monitor = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(\n          eq(monitor.id, Number(id)),\n          eq(monitor.workspaceId, workspaceId),\n          isNull(monitor.deletedAt),\n        ),\n      )\n      .get();\n\n    if (!_monitor) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n    const otelHeader = _monitor.otelHeaders\n      ? z\n          .array(\n            z.object({\n              key: z.string(),\n              value: z.string(),\n            }),\n          )\n          .parse(JSON.parse(_monitor.otelHeaders))\n          // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n          .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n      : undefined;\n\n    const data = MonitorSchema.parse({\n      ..._monitor,\n      openTelemetry: _monitor.otelEndpoint\n        ? {\n            headers: otelHeader,\n            endpoint: _monitor.otelEndpoint ?? undefined,\n          }\n        : undefined,\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/get_all.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { MonitorSchema } from \"./schema\";\n\ntest(\"return all monitors\", async () => {\n  const res = await app.request(\"/v1/monitor\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = MonitorSchema.array().safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.length).toBeGreaterThan(0);\n});\n\ntest(\"return empty monitors\", async () => {\n  const res = await app.request(\"/v1/monitor\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  const result = MonitorSchema.array().safeParse(await res.json());\n\n  expect(result.success).toBe(true);\n  expect(res.status).toBe(200);\n  expect(result.data?.length).toBe(0);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor\", {\n    method: \"GET\",\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/get_all.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq, isNull } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { openApiErrorResponses } from \"@/libs/errors\";\nimport type { monitorsApi } from \"./index\";\nimport { MonitorSchema } from \"./schema\";\n\nconst getAllRoute = createRoute({\n  method: \"get\",\n  tags: [\"monitor\"],\n  summary: \"List all monitors\",\n  path: \"/\",\n  request: {},\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: z.array(MonitorSchema),\n        },\n      },\n      description: \"All the monitors\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetAllMonitors(app: typeof monitorsApi) {\n  return app.openapi(getAllRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n\n    const _monitors = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(eq(monitor.workspaceId, workspaceId), isNull(monitor.deletedAt)),\n      )\n      .all();\n\n    const data = z.array(MonitorSchema).parse(\n      _monitors.map((monitor) => {\n        const otelHeader = monitor.otelHeaders\n          ? z\n              .array(\n                z.object({\n                  key: z.string(),\n                  value: z.string(),\n                }),\n              )\n              .parse(JSON.parse(monitor.otelHeaders))\n              // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n              .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n          : undefined;\n        return {\n          ...monitor,\n          openTelemetry: monitor.otelEndpoint\n            ? {\n                endpoint: monitor.otelEndpoint ?? undefined,\n                headers: otelHeader,\n              }\n            : undefined,\n        };\n      }),\n    );\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/index.ts",
    "content": "import { OpenAPIHono } from \"@hono/zod-openapi\";\n\nimport { handleZodError } from \"@/libs/errors\";\nimport type { Variables } from \"../index\";\nimport { registerDeleteMonitor } from \"./delete\";\nimport { registerGetMonitor } from \"./get\";\nimport { registerGetAllMonitors } from \"./get_all\";\nimport { registerPostMonitor } from \"./post\";\nimport { registerPostMonitorDNS } from \"./post_dns\";\nimport { registerPostMonitorHTTP } from \"./post_http\";\nimport { registerPostMonitorTCP } from \"./post_tcp\";\nimport { registerPutMonitor } from \"./put\";\nimport { registerPutDNSMonitor } from \"./put_dns\";\nimport { registerPutHTTPMonitor } from \"./put_http\";\nimport { registerPutTCPMonitor } from \"./put_tcp\";\nimport { registerGetMonitorResult } from \"./results/get\";\nimport { registerRunMonitor } from \"./run/post\";\nimport { registerGetMonitorSummary } from \"./summary/get\";\nimport { registerTriggerMonitor } from \"./trigger/post\";\n\nconst monitorsApi = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\nregisterGetAllMonitors(monitorsApi);\nregisterGetMonitor(monitorsApi);\nregisterPutMonitor(monitorsApi);\nregisterDeleteMonitor(monitorsApi);\nregisterPostMonitor(monitorsApi);\nregisterPostMonitorHTTP(monitorsApi);\nregisterPostMonitorTCP(monitorsApi);\nregisterPostMonitorDNS(monitorsApi);\nregisterPutHTTPMonitor(monitorsApi);\nregisterPutTCPMonitor(monitorsApi);\nregisterPutDNSMonitor(monitorsApi);\n//\nregisterGetMonitorSummary(monitorsApi);\nregisterTriggerMonitor(monitorsApi);\nregisterGetMonitorResult(monitorsApi);\nregisterRunMonitor(monitorsApi);\n\nexport { monitorsApi };\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/post.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { createErrorSchema } from \"@/libs/errors\";\nimport { MonitorSchema } from \"./schema\";\n\ntest(\"create a valid monitor\", async () => {\n  const res = await app.request(\"/v1/monitor\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      periodicity: \"10m\",\n      url: \"https://www.openstatus.dev\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      method: \"POST\",\n      body: '{\"hello\":\"world\"}',\n      headers: [{ key: \"key\", value: \"value\" }],\n      active: true,\n      public: true,\n      assertions: [\n        {\n          type: \"status\",\n          compare: \"eq\",\n          target: 200,\n        },\n        { type: \"header\", compare: \"not_eq\", key: \"key\", target: \"value\" },\n      ],\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create a monitor with invalid payload should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      periodicity: 32, //not valid value\n      url: \"https://www.openstatus.dev\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      method: \"POST\",\n      body: '{\"hello\":\"world\"}',\n      headers: [{ key: \"key\", value: \"value\" }],\n      active: true,\n      public: false,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create a monitor with invalid page id should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"New Status Report\",\n      message: \"Message\",\n      monitorIds: [1],\n      pageId: 404,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"create a monitor with deprecated regions should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      regions: [\"ams\", \"jnb\", \"hkg\", \"waw\"],\n      name: \"Testing Deprecated Regions\",\n      description: \"Testing Deprecated Regions\",\n      url: \"https://www.openstatus.dev\",\n      method: \"GET\",\n      active: true,\n    }),\n  });\n\n  const json = await res.json();\n  const errorSchema = createErrorSchema(\"BAD_REQUEST\").safeParse(json);\n\n  expect(res.status).toBe(400);\n  expect(errorSchema.success).toBe(true);\n  expect(errorSchema.data?.message).toMatch(\n    \"Deprecated regions are not allowed: hkg, waw\",\n  );\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/post.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { and, db, eq, isNull, sql } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { serialize } from \"@openstatus/assertions\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport type { monitorsApi } from \"./index\";\nimport { MonitorSchema } from \"./schema\";\nimport { getAssertions } from \"./utils\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"monitor\"],\n  summary: \"Create a monitor\",\n  path: \"/\",\n  middleware: [trackMiddleware(Events.CreateMonitor, [\"url\", \"jobType\"])],\n  request: {\n    body: {\n      description: \"The monitor to create\",\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema.omit({ id: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema,\n        },\n      },\n      description: \"Create a monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostMonitor(api: typeof monitorsApi) {\n  return api.openapi(postRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const input = c.req.valid(\"json\");\n    const count = (\n      await db\n        .select({ count: sql<number>`count(*)` })\n        .from(monitor)\n        .where(\n          and(eq(monitor.workspaceId, workspaceId), isNull(monitor.deletedAt)),\n        )\n        .all()\n    )[0].count;\n\n    if (count >= limits.monitors) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more monitors\",\n      });\n    }\n\n    if (!limits.periodicity.includes(input.periodicity)) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more periodicity\",\n      });\n    }\n\n    if (limits[\"max-regions\"] < input.regions.length) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more regions\",\n      });\n    }\n\n    for (const region of input.regions) {\n      if (!limits.regions.includes(region)) {\n        throw new OpenStatusApiError({\n          code: \"PAYMENT_REQUIRED\",\n          message: \"Upgrade for more regions\",\n        });\n      }\n    }\n\n    if (input.jobType && ![\"http\", \"tcp\"].includes(input.jobType)) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message:\n          \"Invalid jobType, currently only 'http' and 'tcp' are supported\",\n      });\n    }\n\n    const { headers, regions, assertions, ...rest } = input;\n\n    const assert = assertions ? getAssertions(assertions) : [];\n\n    const _newMonitor = await db\n      .insert(monitor)\n      .values({\n        ...rest,\n        workspaceId: workspaceId,\n        regions: regions ? regions.join(\",\") : undefined,\n        description: input.description ?? undefined,\n        headers: input.headers ? JSON.stringify(input.headers) : undefined,\n        assertions: assert.length > 0 ? serialize(assert) : undefined,\n        timeout: input.timeout || 45000,\n      })\n      .returning()\n      .get();\n\n    const otelHeader = _newMonitor.otelHeaders\n      ? z\n          .array(\n            z.object({\n              key: z.string(),\n              value: z.string(),\n            }),\n          )\n          .parse(JSON.parse(_newMonitor.otelHeaders))\n          // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n          .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n      : undefined;\n\n    const data = MonitorSchema.parse({\n      ..._newMonitor,\n      openTelemetry: _newMonitor.otelEndpoint\n        ? {\n            headers: otelHeader,\n            endpoint: _newMonitor.otelEndpoint ?? undefined,\n          }\n        : undefined,\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/post_dns.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { MonitorSchema } from \"./schema\";\n\ntest(\"create a valid monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/dns\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        uri: \"openstatus.dev\",\n      },\n      active: true,\n      public: true,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create a status report with invalid payload should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor/dns\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"21m\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        url: \"openstatus.dev\",\n      },\n      active: true,\n      public: true,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/dns\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/post_dns.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { and, db, eq, isNull, sql } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\n// import { serialize } from \"@openstatus/assertions\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport type { monitorsApi } from \"./index\";\nimport { DNSMonitorSchema, MonitorSchema } from \"./schema\";\n// import { getAssertionNew } from \"./utils\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"monitor\"],\n  summary: \"Create a  dns monitor\",\n  path: \"/dns\",\n  middleware: [trackMiddleware(Events.CreateMonitor, [\"url\", \"jobType\"])],\n  request: {\n    body: {\n      description: \"The monitor to create\",\n      content: {\n        \"application/json\": {\n          schema: DNSMonitorSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema,\n        },\n      },\n      description: \"Create a monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostMonitorDNS(api: typeof monitorsApi) {\n  return api.openapi(postRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const input = c.req.valid(\"json\");\n    const count = (\n      await db\n        .select({ count: sql<number>`count(*)` })\n        .from(monitor)\n        .where(\n          and(eq(monitor.workspaceId, workspaceId), isNull(monitor.deletedAt)),\n        )\n        .all()\n    )[0].count;\n\n    if (count >= limits.monitors) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more monitors\",\n      });\n    }\n\n    if (!limits.periodicity.includes(input.frequency)) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more periodicity\",\n      });\n    }\n\n    if (limits[\"max-regions\"] < input.regions.length) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more regions\",\n      });\n    }\n\n    for (const region of input.regions) {\n      if (!limits.regions.includes(region)) {\n        throw new OpenStatusApiError({\n          code: \"PAYMENT_REQUIRED\",\n          message: \"Upgrade for more regions\",\n        });\n      }\n    }\n\n    const { request, regions, assertions, openTelemetry, ...rest } = input;\n\n    const otelHeadersEntries = openTelemetry?.headers\n      ? Object.entries(openTelemetry.headers).map(([key, value]) => ({\n          key: key,\n          value: value,\n        }))\n      : undefined;\n\n    // const assert = assertions ? getAssertionNew(assertions) : [];\n\n    const _newMonitor = await db\n      .insert(monitor)\n      .values({\n        ...rest,\n        periodicity: input.frequency,\n        jobType: \"dns\",\n        url: input.request.uri,\n        workspaceId: workspaceId,\n        regions: regions ? regions.join(\",\") : undefined,\n        // assertions: assert.length > 0 ? serialize(assert) : undefined,\n        timeout: input.timeout || 45000,\n        otelEndpoint: openTelemetry?.endpoint,\n        otelHeaders: otelHeadersEntries\n          ? JSON.stringify(otelHeadersEntries)\n          : undefined,\n      })\n      .returning()\n      .get();\n\n    const otelHeader = _newMonitor.otelHeaders\n      ? z\n          .array(\n            z.object({\n              key: z.string(),\n              value: z.string(),\n            }),\n          )\n          .parse(JSON.parse(_newMonitor.otelHeaders))\n          // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n          .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n      : undefined;\n\n    const data = MonitorSchema.parse({\n      ..._newMonitor,\n      openTelemetry: _newMonitor.otelEndpoint\n        ? {\n            headers: otelHeader,\n            endpoint: _newMonitor.otelEndpoint ?? undefined,\n          }\n        : undefined,\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/post_http.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { MonitorSchema } from \"./schema\";\n\ntest(\"create a valid monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      active: true,\n      degradedAfter: 60,\n      description: \"This is a test\",\n      frequency: \"10m\",\n      name: \"Test2\",\n      regions: [\"iad\"],\n      request: {\n        url: \"https://api.openstatus.dev/health\",\n        method: \"POST\",\n        body: '{\"hello\":\"world\"}',\n        headers: { \"content-type\": \"application/json\" },\n      },\n      assertions: [\n        {\n          kind: \"statusCode\",\n          compare: \"eq\",\n          target: 200,\n        },\n        { kind: \"header\", compare: \"not_eq\", key: \"key\", target: \"value\" },\n      ],\n      retry: 3,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create a status report with invalid payload should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"21m\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"POST\",\n        body: '{\"hello\":\"world\"}',\n        headers: { \"content-type\": \"application/json\" },\n      },\n      active: true,\n      public: true,\n      assertions: [\n        {\n          kind: \"status\",\n          compare: \"eq\",\n          target: 200,\n        },\n        { kind: \"header\", compare: \"not_eq\", key: \"key\", target: \"value\" },\n      ],\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"create HTTP monitor with GET method should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"5m\",\n      name: \"GET Monitor\",\n      description: \"Monitor with GET method\",\n      regions: [\"ams\"],\n      request: {\n        url: \"https://api.openstatus.dev/health\",\n        method: \"GET\",\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with PUT method should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"PUT Monitor\",\n      description: \"Monitor with PUT method\",\n      regions: [\"gru\"],\n      request: {\n        url: \"https://api.example.com/resource\",\n        method: \"PUT\",\n        body: '{\"data\":\"updated\"}',\n        headers: { authorization: \"Bearer token123\" },\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with textBody assertion should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Text Body Assertion Monitor\",\n      description: \"Monitor with text body assertion\",\n      regions: [\"ams\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"GET\",\n      },\n      assertions: [\n        {\n          kind: \"textBody\",\n          compare: \"contains\",\n          target: \"OpenStatus\",\n        },\n      ],\n      active: true,\n      public: true,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with multiple assertions should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Multi Assertion Monitor\",\n      description: \"Monitor with multiple assertions\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        url: \"https://api.openstatus.dev\",\n        method: \"GET\",\n      },\n      assertions: [\n        {\n          kind: \"statusCode\",\n          compare: \"eq\",\n          target: 200,\n        },\n        {\n          kind: \"header\",\n          compare: \"contains\",\n          key: \"content-type\",\n          target: \"json\",\n        },\n        {\n          kind: \"textBody\",\n          compare: \"contains\",\n          target: \"success\",\n        },\n      ],\n      active: true,\n      public: true,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with timeout and retry configuration should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"HTTP with custom config\",\n      description: \"HTTP monitor with timeout and retry\",\n      regions: [\"ams\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"GET\",\n      },\n      timeout: 60000,\n      retry: 5,\n      degradedAfter: 20000,\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with OpenTelemetry configuration should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"HTTP with OTEL\",\n      description: \"HTTP monitor with OpenTelemetry\",\n      regions: [\"ams\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"GET\",\n      },\n      openTelemetry: {\n        endpoint: \"https://otel.example.com/v1/traces\",\n        headers: {\n          \"x-api-key\": \"otel-key-123\",\n          \"x-tenant-id\": \"tenant-456\",\n        },\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with 30s frequency should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"30s\",\n      name: \"Fast HTTP Check\",\n      description: \"HTTP monitor with 30s frequency\",\n      regions: [\"ams\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"GET\",\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with 1h frequency should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"1h\",\n      name: \"Hourly HTTP Check\",\n      description: \"HTTP monitor with 1h frequency\",\n      regions: [\"gru\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"GET\",\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor without optional fields should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Minimal HTTP Monitor\",\n      regions: [\"ams\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"GET\",\n      },\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with PATCH method should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"PATCH Monitor\",\n      description: \"Monitor with PATCH method\",\n      regions: [\"ams\"],\n      request: {\n        url: \"https://api.example.com/resource\",\n        method: \"PATCH\",\n        body: '{\"field\":\"value\"}',\n        headers: { \"content-type\": \"application/json\" },\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with DELETE method should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"DELETE Monitor\",\n      description: \"Monitor with DELETE method\",\n      regions: [\"ams\"],\n      request: {\n        url: \"https://api.example.com/resource/123\",\n        method: \"DELETE\",\n        headers: { authorization: \"Bearer token\" },\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create HTTP monitor with invalid URL should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Invalid URL Monitor\",\n      regions: [\"ams\"],\n      request: {\n        url: \"not-a-valid-url\",\n        method: \"GET\",\n      },\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create HTTP monitor with deprecated regions should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Deprecated Regions HTTP\",\n      regions: [\"ams\", \"hkg\", \"waw\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"GET\",\n      },\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/post_http.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { and, db, eq, isNull, sql } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { serialize } from \"@openstatus/assertions\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport type { monitorsApi } from \"./index\";\nimport { HTTPMonitorSchema, MonitorSchema } from \"./schema\";\nimport { getAssertionNew } from \"./utils\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"monitor\"],\n  summary: \"Create a  http monitor\",\n  path: \"/http\",\n  middleware: [trackMiddleware(Events.CreateMonitor, [\"url\", \"jobType\"])],\n  request: {\n    body: {\n      description: \"The monitor to create\",\n      content: {\n        \"application/json\": {\n          schema: HTTPMonitorSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema,\n        },\n      },\n      description: \"Create a monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostMonitorHTTP(api: typeof monitorsApi) {\n  return api.openapi(postRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const input = c.req.valid(\"json\");\n    const count = (\n      await db\n        .select({ count: sql<number>`count(*)` })\n        .from(monitor)\n        .where(\n          and(eq(monitor.workspaceId, workspaceId), isNull(monitor.deletedAt)),\n        )\n        .all()\n    )[0].count;\n\n    if (count >= limits.monitors) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more monitors\",\n      });\n    }\n\n    if (!limits.periodicity.includes(input.frequency)) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more periodicity\",\n      });\n    }\n\n    if (limits[\"max-regions\"] < input.regions.length) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more regions\",\n      });\n    }\n\n    for (const region of input.regions) {\n      if (!limits.regions.includes(region)) {\n        throw new OpenStatusApiError({\n          code: \"PAYMENT_REQUIRED\",\n          message: \"Upgrade for more regions\",\n        });\n      }\n    }\n\n    const { request, regions, assertions, openTelemetry, ...rest } = input;\n\n    const headers = input.request.headers\n      ? Object.entries(input.request.headers)\n      : undefined;\n\n    const otelHeadersEntries = openTelemetry?.headers\n      ? Object.entries(openTelemetry.headers).map(([key, value]) => ({\n          key: key,\n          value: value,\n        }))\n      : undefined;\n    const headersEntries = headers\n      ? headers.map(([key, value]) => ({ key: key, value: value }))\n      : undefined;\n    const assert = assertions ? getAssertionNew(assertions) : [];\n\n    const _newMonitor = await db\n      .insert(monitor)\n      .values({\n        ...rest,\n        periodicity: input.frequency,\n        jobType: \"http\",\n        url: request.url,\n        method: request.method,\n        body: request.body,\n        workspaceId: workspaceId,\n        regions: regions ? regions.join(\",\") : undefined,\n        headers: headersEntries ? JSON.stringify(headersEntries) : undefined,\n        assertions: assert.length > 0 ? serialize(assert) : undefined,\n        timeout: input.timeout || 45000,\n        otelEndpoint: openTelemetry?.endpoint,\n        otelHeaders: otelHeadersEntries\n          ? JSON.stringify(otelHeadersEntries)\n          : undefined,\n      })\n      .returning()\n      .get();\n\n    const otelHeader = _newMonitor.otelHeaders\n      ? z\n          .array(\n            z.object({\n              key: z.string(),\n              value: z.string(),\n            }),\n          )\n          .parse(JSON.parse(_newMonitor.otelHeaders))\n          // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n          .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n      : undefined;\n\n    const data = MonitorSchema.parse({\n      ..._newMonitor,\n      openTelemetry: _newMonitor.otelEndpoint\n        ? {\n            headers: otelHeader,\n            endpoint: _newMonitor.otelEndpoint ?? undefined,\n          }\n        : undefined,\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/post_tcp.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { MonitorSchema } from \"./schema\";\n\ntest(\"create a valid monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: 443,\n      },\n      active: true,\n      public: true,\n    }),\n  });\n  const r = await res.json();\n  const result = MonitorSchema.safeParse(r);\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create a status report with invalid payload should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"21m\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: 443,\n      },\n      active: true,\n      public: true,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"create TCP monitor with port 80 should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"5m\",\n      name: \"HTTP Port Monitor\",\n      description: \"Monitor port 80\",\n      regions: [\"ams\"],\n      request: {\n        host: \"example.com\",\n        port: 80,\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create TCP monitor with custom port should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"1m\",\n      name: \"Custom Port Monitor\",\n      description: \"Monitor custom port 8080\",\n      regions: [\"gru\"],\n      request: {\n        host: \"localhost\",\n        port: 8080,\n      },\n      active: false,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create TCP monitor with timeout and retry configuration should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"TCP with custom config\",\n      description: \"TCP monitor with timeout and retry\",\n      regions: [\"ams\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: 443,\n      },\n      timeout: 30000,\n      retry: 5,\n      degradedAfter: 10000,\n      active: true,\n      public: true,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create TCP monitor with OpenTelemetry configuration should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"TCP with OTEL\",\n      description: \"TCP monitor with OpenTelemetry\",\n      regions: [\"ams\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: 443,\n      },\n      openTelemetry: {\n        endpoint: \"https://otel.example.com\",\n        headers: {\n          \"x-api-key\": \"test-key\",\n        },\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create TCP monitor with multiple regions should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"30m\",\n      name: \"Multi-region TCP\",\n      description: \"TCP monitor across multiple regions\",\n      regions: [\"ams\", \"gru\", \"syd\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: 443,\n      },\n      active: true,\n      public: true,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create TCP monitor with 30s frequency should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"30s\",\n      name: \"Fast TCP Check\",\n      description: \"TCP monitor with 30s frequency\",\n      regions: [\"ams\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: 443,\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create TCP monitor with 1h frequency should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"1h\",\n      name: \"Hourly TCP Check\",\n      description: \"TCP monitor with 1h frequency\",\n      regions: [\"gru\"],\n      request: {\n        host: \"example.com\",\n        port: 443,\n      },\n      active: true,\n      public: false,\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create TCP monitor without optional fields should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Minimal TCP Monitor\",\n      regions: [\"ams\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: 443,\n      },\n    }),\n  });\n\n  const result = MonitorSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created monitor\n  if (result.success) {\n    await app.request(`/v1/monitor/${result.data.id}`, {\n      method: \"DELETE\",\n      headers: { \"x-openstatus-key\": \"1\" },\n    });\n  }\n});\n\ntest(\"create TCP monitor with invalid host should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Invalid TCP Monitor\",\n      regions: [\"ams\"],\n      request: {\n        host: \"\",\n        port: 443,\n      },\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create TCP monitor with invalid port should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Invalid Port Monitor\",\n      regions: [\"ams\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: \"not-a-number\",\n      },\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create TCP monitor with deprecated regions should return 400\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"Deprecated Regions TCP\",\n      regions: [\"ams\", \"hkg\", \"waw\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: 443,\n      },\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/post_tcp.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { and, db, eq, isNull, sql } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport type { monitorsApi } from \"./index\";\nimport { MonitorSchema, TCPMonitorSchema } from \"./schema\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"monitor\"],\n  summary: \"Create a  tcp monitor\",\n  path: \"/tcp\",\n  middleware: [trackMiddleware(Events.CreateMonitor, [\"url\", \"jobType\"])],\n  request: {\n    body: {\n      description: \"The monitor to create\",\n      content: {\n        \"application/json\": {\n          schema: TCPMonitorSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema,\n        },\n      },\n      description: \"Create a monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostMonitorTCP(api: typeof monitorsApi) {\n  return api.openapi(postRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const input = c.req.valid(\"json\");\n    const count = (\n      await db\n        .select({ count: sql<number>`count(*)` })\n        .from(monitor)\n        .where(\n          and(eq(monitor.workspaceId, workspaceId), isNull(monitor.deletedAt)),\n        )\n        .all()\n    )[0].count;\n\n    if (count >= limits.monitors) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more monitors\",\n      });\n    }\n\n    if (!limits.periodicity.includes(input.frequency)) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more periodicity\",\n      });\n    }\n\n    if (limits[\"max-regions\"] < input.regions.length) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more regions\",\n      });\n    }\n\n    for (const region of input.regions) {\n      if (!limits.regions.includes(region)) {\n        throw new OpenStatusApiError({\n          code: \"PAYMENT_REQUIRED\",\n          message: \"Upgrade for more regions\",\n        });\n      }\n    }\n\n    const { request, regions, openTelemetry, ...rest } = input;\n    const otelHeadersEntries = openTelemetry?.headers\n      ? Object.entries(openTelemetry.headers).map(([key, value]) => ({\n          key: key,\n          value: value,\n        }))\n      : undefined;\n\n    const _newMonitor = await db\n      .insert(monitor)\n      .values({\n        ...rest,\n        jobType: \"tcp\",\n        periodicity: input.frequency,\n        url: `${request.host}:${request.port}`,\n        workspaceId: workspaceId,\n        regions: regions ? regions.join(\",\") : undefined,\n        headers: undefined,\n        assertions: undefined,\n        timeout: input.timeout || 45000,\n        otelHeaders: otelHeadersEntries\n          ? JSON.stringify(otelHeadersEntries)\n          : undefined,\n        otelEndpoint: openTelemetry?.endpoint,\n      })\n      .returning()\n      .get();\n\n    const otelHeader = _newMonitor.otelHeaders\n      ? z\n          .array(\n            z.object({\n              key: z.string(),\n              value: z.string(),\n            }),\n          )\n          .parse(JSON.parse(_newMonitor.otelHeaders))\n          // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n          .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n      : undefined;\n\n    const data = MonitorSchema.parse({\n      ..._newMonitor,\n      openTelemetry: _newMonitor.otelEndpoint\n        ? {\n            headers: otelHeader,\n            endpoint: _newMonitor.otelEndpoint ?? undefined,\n          }\n        : undefined,\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/put.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { MonitorSchema } from \"./schema\";\n\ntest(\"update the monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      name: \"New Name\",\n    }),\n  });\n  const data = await res.json();\n  const monitor = MonitorSchema.parse(data);\n  expect(res.status).toBe(200);\n  expect(monitor.name).toBe(\"New Name\");\n});\n\ntest(\"invalid monitor id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/404\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      /* */\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/2\", {\n    method: \"PUT\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      /* */\n    }),\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/put.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq, isNull } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { Events } from \"@openstatus/analytics\";\nimport { serialize } from \"@openstatus/assertions\";\nimport type { monitorsApi } from \"./index\";\nimport { MonitorSchema, ParamsSchema } from \"./schema\";\nimport { getAssertions } from \"./utils\";\n\nconst putRoute = createRoute({\n  method: \"put\",\n  tags: [\"monitor\"],\n  summary: \"Update a monitor\",\n  path: \"/{id}\",\n  middleware: [trackMiddleware(Events.UpdateMonitor)],\n  request: {\n    params: ParamsSchema,\n    body: {\n      description: \"The monitor to update\",\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema.omit({ id: true }).partial(),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema,\n        },\n      },\n      description: \"Update a monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPutMonitor(api: typeof monitorsApi) {\n  return api.openapi(putRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const { id } = c.req.valid(\"param\");\n    const input = c.req.valid(\"json\");\n\n    if (input.periodicity && !limits.periodicity.includes(input.periodicity)) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more periodicity\",\n      });\n    }\n\n    if (input.regions) {\n      if (limits[\"max-regions\"] < input.regions.length) {\n        throw new OpenStatusApiError({\n          code: \"PAYMENT_REQUIRED\",\n          message: \"Upgrade for more regions\",\n        });\n      }\n\n      for (const region of input.regions) {\n        if (!limits.regions.includes(region)) {\n          throw new OpenStatusApiError({\n            code: \"PAYMENT_REQUIRED\",\n            message: \"Upgrade for more regions\",\n          });\n        }\n      }\n    }\n\n    const _monitor = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(\n          eq(monitor.id, Number(id)),\n          isNull(monitor.deletedAt),\n          eq(monitor.workspaceId, workspaceId),\n        ),\n      )\n      .get();\n\n    if (!_monitor) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    if (input.jobType && input.jobType !== _monitor.jobType) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message:\n          \"Cannot change jobType. Please delete and create a new monitor instead.\",\n      });\n    }\n\n    const { headers, regions, assertions, ...rest } = input;\n\n    const assert = assertions ? getAssertions(assertions) : [];\n\n    const _newMonitor = await db\n      .update(monitor)\n      .set({\n        ...rest,\n        regions: regions ? regions.join(\",\") : undefined,\n        description: input.description ?? undefined,\n        headers: input.headers ? JSON.stringify(input.headers) : undefined,\n        assertions: assert.length > 0 ? serialize(assert) : undefined,\n        timeout: input.timeout || 45000,\n        updatedAt: new Date(),\n      })\n      .where(eq(monitor.id, Number(_monitor.id)))\n      .returning()\n      .get();\n\n    const otelHeader = _newMonitor.otelHeaders\n      ? z\n          .array(\n            z.object({\n              key: z.string(),\n              value: z.string(),\n            }),\n          )\n          .parse(JSON.parse(_newMonitor.otelHeaders))\n          // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n          .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n      : undefined;\n\n    const data = MonitorSchema.parse({\n      ..._newMonitor,\n      openTelemetry: _newMonitor.otelEndpoint\n        ? {\n            headers: otelHeader,\n            endpoint: _newMonitor.otelEndpoint ?? undefined,\n          }\n        : undefined,\n    });\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/put_dns.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\n\ntest(\"update the monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/dns/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      name: \"New Name\",\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"invalid monitor id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/dns/404\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        uri: \"openstatus.dev\",\n      },\n      active: true,\n      public: true,\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/dns/2\", {\n    method: \"PUT\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      /* */\n    }),\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/put_dns.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq, isNull } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { Events } from \"@openstatus/analytics\";\nimport type { monitorsApi } from \"./index\";\nimport { DNSMonitorSchema, MonitorSchema, ParamsSchema } from \"./schema\";\n\nconst putRoute = createRoute({\n  method: \"put\",\n  tags: [\"monitor\"],\n  summary: \"Update an DNS monitor\",\n  path: \"/dns/{id}\",\n  middleware: [trackMiddleware(Events.UpdateMonitor)],\n  request: {\n    params: ParamsSchema,\n    body: {\n      description: \"The monitor to update\",\n      content: {\n        \"application/json\": {\n          schema: DNSMonitorSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema,\n        },\n      },\n      description: \"Update a monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPutDNSMonitor(api: typeof monitorsApi) {\n  return api.openapi(putRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const { id } = c.req.valid(\"param\");\n    const input = c.req.valid(\"json\");\n\n    if (input.frequency && !limits.periodicity.includes(input.frequency)) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more periodicity\",\n      });\n    }\n\n    if (input.regions) {\n      for (const region of input.regions) {\n        if (!limits.regions.includes(region)) {\n          throw new OpenStatusApiError({\n            code: \"PAYMENT_REQUIRED\",\n            message: \"Upgrade for more regions\",\n          });\n        }\n      }\n    }\n\n    const _monitor = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(\n          eq(monitor.id, Number(id)),\n          isNull(monitor.deletedAt),\n          eq(monitor.workspaceId, workspaceId),\n        ),\n      )\n      .get();\n\n    if (!_monitor) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    if (_monitor.jobType !== \"tcp\") {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    const { request, regions, openTelemetry, assertions, ...rest } = input;\n\n    const otelHeadersEntries = openTelemetry?.headers\n      ? Object.entries(openTelemetry.headers).map(([key, value]) => ({\n          key: key,\n          value: value,\n        }))\n      : undefined;\n\n    const _newMonitor = await db\n      .update(monitor)\n      .set({\n        ...rest,\n        periodicity: input.frequency,\n        url: input.request.uri,\n        regions: regions ? regions.join(\",\") : undefined,\n        otelHeaders: otelHeadersEntries\n          ? JSON.stringify(otelHeadersEntries)\n          : undefined,\n        otelEndpoint: openTelemetry?.endpoint,\n        timeout: input.timeout || 45000,\n        updatedAt: new Date(),\n      })\n      .where(eq(monitor.id, Number(_monitor.id)))\n      .returning()\n      .get();\n    const otelHeader = _newMonitor.otelHeaders\n      ? z\n          .array(\n            z.object({\n              key: z.string(),\n              value: z.string(),\n            }),\n          )\n          .parse(JSON.parse(_newMonitor.otelHeaders))\n          // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n          .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n      : undefined;\n\n    const data = MonitorSchema.parse({\n      ..._newMonitor,\n      openTelemetry: _newMonitor.otelEndpoint\n        ? {\n            headers: otelHeader,\n            endpoint: _newMonitor.otelEndpoint ?? undefined,\n          }\n        : undefined,\n    });\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/put_http.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { MonitorSchema } from \"./schema\";\n\ntest(\"update the monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/http/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      name: \"New Name\",\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"invalid monitor id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/http/404\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"POST\",\n        body: '{\"hello\":\"world\"}',\n        headers: { \"content-type\": \"application/json\" },\n      },\n      active: true,\n      public: true,\n      assertions: [\n        {\n          kind: \"statusCode\",\n          compare: \"eq\",\n          target: 200,\n        },\n        { kind: \"header\", compare: \"not_eq\", key: \"key\", target: \"value\" },\n      ],\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"Update a valid monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/http\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"POST\",\n        body: '{\"hello\":\"world\"}',\n        headers: { \"content-type\": \"application/json\" },\n      },\n      active: true,\n      public: true,\n      assertions: [\n        {\n          kind: \"statusCode\",\n          compare: \"eq\",\n          target: 200,\n        },\n        { kind: \"header\", compare: \"not_eq\", key: \"key\", target: \"value\" },\n      ],\n    }),\n  });\n\n  const result = MonitorSchema.parse(await res.json());\n\n  expect(res.status).toBe(200);\n\n  const updated = await app.request(`/v1/monitor/http/${result.id}`, {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"30m\",\n      name: \"newName\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        url: \"https://www.openstatus.dev\",\n        method: \"POST\",\n        body: '{\"hello\":\"world\"}',\n        headers: { \"content-type\": \"application/json\" },\n      },\n      active: true,\n      public: true,\n    }),\n\n    // expect(r.success).toBe(true);\n  });\n  const r = MonitorSchema.parse(await updated.json());\n  expect(r.assertions?.length).toBe(0);\n  expect(r.periodicity).toBe(\"30m\");\n  expect(r.name).toBe(\"newName\");\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/http/2\", {\n    method: \"PUT\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      /* */\n    }),\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/put_http.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq, isNull } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { Events } from \"@openstatus/analytics\";\nimport { serialize } from \"@openstatus/assertions\";\nimport type { monitorsApi } from \"./index\";\nimport { HTTPMonitorSchema, MonitorSchema, ParamsSchema } from \"./schema\";\nimport { getAssertionNew } from \"./utils\";\n\nconst putRoute = createRoute({\n  method: \"put\",\n  tags: [\"monitor\"],\n  summary: \"Update an HTTP monitor\",\n  path: \"/http/{id}\",\n  middleware: [trackMiddleware(Events.UpdateMonitor)],\n  request: {\n    params: ParamsSchema,\n    body: {\n      description: \"The monitor to update\",\n      content: {\n        \"application/json\": {\n          schema: HTTPMonitorSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema,\n        },\n      },\n      description: \"Update a monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPutHTTPMonitor(api: typeof monitorsApi) {\n  return api.openapi(putRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const { id } = c.req.valid(\"param\");\n    const input = c.req.valid(\"json\");\n\n    if (input.frequency && !limits.periodicity.includes(input.frequency)) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more periodicity\",\n      });\n    }\n\n    if (input.regions) {\n      for (const region of input.regions) {\n        if (!limits.regions.includes(region)) {\n          throw new OpenStatusApiError({\n            code: \"PAYMENT_REQUIRED\",\n            message: \"Upgrade for more regions\",\n          });\n        }\n      }\n    }\n\n    const _monitor = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(\n          eq(monitor.id, Number(id)),\n          isNull(monitor.deletedAt),\n          eq(monitor.workspaceId, workspaceId),\n        ),\n      )\n      .get();\n\n    if (!_monitor) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    if (_monitor.jobType !== \"http\") {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    const { request, regions, assertions, openTelemetry, ...rest } = input;\n\n    const headers = input.request.headers\n      ? Object.entries(input.request.headers)\n      : undefined;\n\n    const otelHeadersEntries = openTelemetry?.headers\n      ? Object.entries(openTelemetry.headers).map(([key, value]) => ({\n          key: key,\n          value: value,\n        }))\n      : undefined;\n    const headersEntries = headers\n      ? headers.map(([key, value]) => ({ key: key, value: value }))\n      : undefined;\n    const assert = assertions ? getAssertionNew(assertions) : [];\n\n    const _newMonitor = await db\n      .update(monitor)\n      .set({\n        ...rest,\n        periodicity: input.frequency,\n        url: input.request.url,\n        method: input.request.method,\n        body: input.request.body,\n        regions: regions ? regions.join(\",\") : undefined,\n        headers: headersEntries ? JSON.stringify(headersEntries) : undefined,\n        otelHeaders: otelHeadersEntries\n          ? JSON.stringify(otelHeadersEntries)\n          : undefined,\n        otelEndpoint: openTelemetry?.endpoint,\n        assertions: assert ? serialize(assert) : \"\",\n        timeout: input.timeout || 45000,\n        updatedAt: new Date(),\n      })\n      .where(eq(monitor.id, Number(_monitor.id)))\n      .returning()\n      .get();\n\n    const otelHeader = _newMonitor.otelHeaders\n      ? z\n          .array(\n            z.object({\n              key: z.string(),\n              value: z.string(),\n            }),\n          )\n          .parse(JSON.parse(_newMonitor.otelHeaders))\n          // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n          .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n      : undefined;\n\n    const data = MonitorSchema.parse({\n      ..._newMonitor,\n      openTelemetry: _newMonitor.otelEndpoint\n        ? {\n            headers: otelHeader,\n            endpoint: _newMonitor.otelEndpoint ?? undefined,\n          }\n        : undefined,\n    });\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/put_tcp.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\n\ntest(\"update the monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      name: \"New Name\",\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"invalid monitor id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp/404\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      frequency: \"10m\",\n      name: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      regions: [\"ams\", \"gru\"],\n      request: {\n        host: \"openstatus.dev\",\n        port: 443,\n      },\n      active: true,\n      public: true,\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/tcp/2\", {\n    method: \"PUT\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      /* */\n    }),\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/put_tcp.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq, isNull } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { Events } from \"@openstatus/analytics\";\nimport type { monitorsApi } from \"./index\";\nimport { MonitorSchema, ParamsSchema, TCPMonitorSchema } from \"./schema\";\n\nconst putRoute = createRoute({\n  method: \"put\",\n  tags: [\"monitor\"],\n  summary: \"Update an TCP monitor\",\n  path: \"/tcp/{id}\",\n  middleware: [trackMiddleware(Events.UpdateMonitor)],\n  request: {\n    params: ParamsSchema,\n    body: {\n      description: \"The monitor to update\",\n      content: {\n        \"application/json\": {\n          schema: TCPMonitorSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: MonitorSchema,\n        },\n      },\n      description: \"Update a monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPutTCPMonitor(api: typeof monitorsApi) {\n  return api.openapi(putRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const { id } = c.req.valid(\"param\");\n    const input = c.req.valid(\"json\");\n\n    if (input.frequency && !limits.periodicity.includes(input.frequency)) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more periodicity\",\n      });\n    }\n\n    if (input.regions) {\n      for (const region of input.regions) {\n        if (!limits.regions.includes(region)) {\n          throw new OpenStatusApiError({\n            code: \"PAYMENT_REQUIRED\",\n            message: \"Upgrade for more regions\",\n          });\n        }\n      }\n    }\n\n    const _monitor = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(\n          eq(monitor.id, Number(id)),\n          isNull(monitor.deletedAt),\n          eq(monitor.workspaceId, workspaceId),\n        ),\n      )\n      .get();\n\n    if (!_monitor) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    if (_monitor.jobType !== \"tcp\") {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    const { request, regions, openTelemetry, ...rest } = input;\n\n    const otelHeadersEntries = openTelemetry?.headers\n      ? Object.entries(openTelemetry.headers).map(([key, value]) => ({\n          key: key,\n          value: value,\n        }))\n      : undefined;\n\n    const _newMonitor = await db\n      .update(monitor)\n      .set({\n        ...rest,\n        periodicity: input.frequency,\n        url: `${request.host}:${request.port}`,\n        regions: regions ? regions.join(\",\") : undefined,\n        otelHeaders: otelHeadersEntries\n          ? JSON.stringify(otelHeadersEntries)\n          : undefined,\n        otelEndpoint: openTelemetry?.endpoint,\n        timeout: input.timeout || 45000,\n        updatedAt: new Date(),\n      })\n      .where(eq(monitor.id, Number(_monitor.id)))\n      .returning()\n      .get();\n    const otelHeader = _newMonitor.otelHeaders\n      ? z\n          .array(\n            z.object({\n              key: z.string(),\n              value: z.string(),\n            }),\n          )\n          .parse(JSON.parse(_newMonitor.otelHeaders))\n          // biome-ignore lint/performance/noAccumulatingSpread: <explanation>\n          .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})\n      : undefined;\n\n    const data = MonitorSchema.parse({\n      ..._newMonitor,\n      openTelemetry: _newMonitor.otelEndpoint\n        ? {\n            headers: otelHeader,\n            endpoint: _newMonitor.otelEndpoint ?? undefined,\n          }\n        : undefined,\n    });\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/results/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { ResultRun } from \"../schema\";\n\ntest.todo(\"get monitor result with valid id should return 200\", async () => {\n  const res = await app.request(\"/v1/monitor/1/result/1\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = ResultRun.array().safeParse(json);\n  expect(result.success).toBe(true);\n});\n\ntest(\"get monitor result with invalid monitor id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/999999/result/1\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"get monitor result with invalid result id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/1/result/999999\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"get monitor result without auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/1/result/1\", {\n    method: \"GET\",\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"get monitor result from different workspace should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/2/result/1\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest.todo(\n  \"get monitor result with valid TCP monitor should return 200\",\n  async () => {\n    const res = await app.request(\"/v1/monitor/4/result/2\", {\n      method: \"GET\",\n      headers: {\n        \"x-openstatus-key\": \"1\",\n      },\n    });\n\n    expect(res.status).toBe(200);\n\n    const json = await res.json();\n    const result = ResultRun.array().safeParse(json);\n    expect(result.success).toBe(true);\n  },\n);\n\ntest(\"get monitor result with non-matching result id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/1/result/2\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/results/get.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq } from \"@openstatus/db\";\nimport { monitor, monitorRun } from \"@openstatus/db/src/schema\";\n\nimport { tb } from \"@/libs/clients\";\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport type { monitorsApi } from \"../index\";\nimport { ParamsSchema, ResultRun } from \"../schema\";\n\nconst getRoute = createRoute({\n  method: \"get\",\n  tags: [\"monitor\"],\n  summary: \"Get a monitor result\",\n  // FIXME: Should work for all types of monitors\n  description:\n    \"**WARNING:** This works only for HTTP monitors. We will add support for other types of monitors soon.\",\n  path: \"/{id}/result/{resultId}\",\n  request: {\n    params: ParamsSchema.extend({\n      resultId: z.string().openapi({\n        description: \"The id of the result\",\n      }),\n    }),\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: ResultRun.array(),\n        },\n      },\n      description: \"All the metrics for the result id from the monitor\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetMonitorResult(api: typeof monitorsApi) {\n  return api.openapi(getRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id, resultId } = c.req.valid(\"param\");\n\n    const _monitorRun = await db\n      .select()\n      .from(monitorRun)\n      .where(\n        and(\n          eq(monitorRun.id, Number(resultId)),\n          eq(monitorRun.monitorId, Number(id)),\n          eq(monitorRun.workspaceId, workspaceId),\n        ),\n      )\n      .get();\n\n    if (!_monitorRun || !_monitorRun?.runnedAt) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor run ${resultId} not found`,\n      });\n    }\n\n    const _monitor = await db\n      .select()\n      .from(monitor)\n      .where(eq(monitor.id, Number(id)))\n      .get();\n\n    if (!_monitor) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    // Fetch result from tb pipe\n    const data = await tb.getResultForOnDemandCheckHttp({\n      monitorId: _monitor.id,\n      timestamp: _monitorRun.runnedAt?.getTime(),\n      url: _monitor.url,\n    });\n\n    return c.json(data.data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/run/post.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { afterEach, mock } from \"bun:test\";\nimport { app } from \"@/index\";\nimport { TriggerResult } from \"../schema\";\n\nconst mockFetch = mock();\n\nglobal.fetch = mockFetch as unknown as typeof fetch;\nmock.module(\"node-fetch\", () => mockFetch);\n\nafterEach(() => {\n  mockFetch.mockReset();\n});\n\ntest(\"run monitor with valid id should return 200\", async () => {\n  mockFetch.mockReturnValue(\n    Promise.resolve(\n      new Response(\n        JSON.stringify({\n          jobType: \"http\",\n          status: 200,\n          latency: 100,\n          region: \"ams\",\n          timestamp: 1234567890,\n          timing: {\n            dnsStart: 1,\n            dnsDone: 2,\n            connectStart: 3,\n            connectDone: 4,\n            tlsHandshakeStart: 5,\n            tlsHandshakeDone: 6,\n            firstByteStart: 7,\n            firstByteDone: 8,\n            transferStart: 9,\n            transferDone: 10,\n          },\n        }),\n        { status: 200, headers: { \"content-type\": \"application/json\" } },\n      ),\n    ),\n  );\n\n  const res = await app.request(\"/v1/monitor/1/run\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = TriggerResult.array().safeParse(json);\n  expect(result.success).toBe(true);\n});\n\ntest(\"run monitor with no-wait parameter should return empty array\", async () => {\n  const res = await app.request(\"/v1/monitor/1/run?no-wait=true\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  expect(json).toEqual([]);\n});\n\ntest(\"run monitor with invalid id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/999999/run\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"run monitor without auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/1/run\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"run monitor from different workspace should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/55555/run\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"run TCP monitor with valid id should return 200\", async () => {\n  mockFetch.mockReturnValue(\n    Promise.resolve(\n      new Response(\n        JSON.stringify({\n          jobType: \"tcp\",\n          latency: 50,\n          region: \"ams\",\n          timestamp: 1234567890,\n          timing: {\n            tcpStart: 1,\n            tcpDone: 2,\n          },\n        }),\n        { status: 200, headers: { \"content-type\": \"application/json\" } },\n      ),\n    ),\n  );\n\n  const res = await app.request(\"/v1/monitor/4/run\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = TriggerResult.array().safeParse(json);\n  expect(result.success).toBe(true);\n  if (result.success && result.data[0]) {\n    expect(result.data[0].jobType).toBe(\"tcp\");\n  }\n});\n\ntest.todo(\n  \"run monitor with multiple regions should return array of results\",\n  async () => {\n    mockFetch.mockReturnValue(\n      Promise.resolve(\n        new Response(\n          JSON.stringify({\n            jobType: \"http\",\n            status: 200,\n            latency: 100,\n            region: \"ams\",\n            timestamp: 1234567890,\n            timing: {\n              dnsStart: 1,\n              dnsDone: 2,\n              connectStart: 3,\n              connectDone: 4,\n              tlsHandshakeStart: 5,\n              tlsHandshakeDone: 6,\n              firstByteStart: 7,\n              firstByteDone: 8,\n              transferStart: 9,\n              transferDone: 10,\n            },\n          }),\n          { status: 200, headers: { \"content-type\": \"application/json\" } },\n        ),\n      ),\n    );\n\n    const res = await app.request(\"/v1/monitor/5/run\", {\n      method: \"POST\",\n      headers: {\n        \"x-openstatus-key\": \"1\",\n        \"content-type\": \"application/json\",\n      },\n    });\n\n    expect(res.status).toBe(200);\n\n    const json = await res.json();\n    expect(Array.isArray(json)).toBe(true);\n  },\n);\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/run/post.ts",
    "content": "import { env } from \"@/env\";\nimport { getCheckerPayload, getCheckerUrl } from \"@/libs/checker\";\nimport { openApiErrorResponses } from \"@/libs/errors\";\nimport { createRoute } from \"@hono/zod-openapi\";\nimport { getLogger } from \"@logtape/logtape\";\nimport { and, eq, gte, isNull, sql } from \"@openstatus/db\";\n\nconst logger = getLogger(\"api-server\");\nimport { db } from \"@openstatus/db/src/db\";\nimport { monitorRun } from \"@openstatus/db/src/schema\";\nimport { monitorStatusTable } from \"@openstatus/db/src/schema/monitor_status/monitor_status\";\nimport { selectMonitorStatusSchema } from \"@openstatus/db/src/schema/monitor_status/validation\";\nimport { monitor } from \"@openstatus/db/src/schema/monitors/monitor\";\nimport { selectMonitorSchema } from \"@openstatus/db/src/schema/monitors/validation\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { monitorsApi } from \"..\";\nimport { ParamsSchema, TriggerResult } from \"../schema\";\nimport { QuerySchema } from \"./schema\";\n\nconst postMonitor = createRoute({\n  method: \"post\",\n  tags: [\"monitor\"],\n  summary: \"Create a monitor run\",\n  description:\n    \"Run a synthetic check for a specific monitor. It will take all configs into account.\",\n  path: \"/{id}/run\",\n  request: {\n    params: ParamsSchema,\n    query: QuerySchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: TriggerResult.array(),\n        },\n      },\n      description: \"All the historical metrics\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerRunMonitor(api: typeof monitorsApi) {\n  return api.openapi(postMonitor, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n    const limits = c.get(\"workspace\").limits;\n    const { \"no-wait\": noWait } = c.req.valid(\"query\");\n    const lastMonth = new Date().setMonth(new Date().getMonth() - 1);\n\n    const count = (\n      await db\n        .select({ count: sql<number>`count(*)` })\n        .from(monitorRun)\n        .where(\n          and(\n            eq(monitorRun.workspaceId, workspaceId),\n            gte(monitorRun.createdAt, new Date(lastMonth)),\n          ),\n        )\n        .all()\n    )[0].count;\n\n    if (count >= limits[\"synthetic-checks\"]) {\n      throw new HTTPException(403, {\n        message: \"Upgrade for more checks\",\n      });\n    }\n\n    const monitorData = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(\n          eq(monitor.id, Number(id)),\n          eq(monitor.workspaceId, workspaceId),\n          isNull(monitor.deletedAt),\n        ),\n      )\n      .get();\n\n    if (!monitorData) {\n      throw new HTTPException(404, { message: \"Not Found\" });\n    }\n\n    const parseMonitor = selectMonitorSchema.safeParse(monitorData);\n\n    if (!parseMonitor.success) {\n      throw new HTTPException(400, { message: \"Something went wrong\" });\n    }\n\n    const row = parseMonitor.data;\n\n    // Maybe later overwrite the region\n\n    const monitorStatusData = await db\n      .select()\n      .from(monitorStatusTable)\n      .where(eq(monitorStatusTable.monitorId, monitorData.id))\n      .all();\n\n    const monitorStatus = selectMonitorStatusSchema\n      .array()\n      .safeParse(monitorStatusData);\n\n    if (!monitorStatus.success) {\n      logger.error(\"Failed to parse monitor status\", {\n        monitor_id: id,\n        workspace_id: workspaceId,\n        error: monitorStatus.error.message,\n      });\n      throw new HTTPException(400, { message: \"Something went wrong\" });\n    }\n\n    const timestamp = Date.now();\n\n    const newRun = await db\n      .insert(monitorRun)\n      .values({\n        monitorId: row.id,\n        workspaceId: row.workspaceId,\n        runnedAt: new Date(timestamp),\n      })\n      .returning();\n\n    if (!newRun[0]) {\n      throw new HTTPException(400, { message: \"Something went wrong\" });\n    }\n\n    const allResult = [];\n    for (const region of parseMonitor.data.regions) {\n      const status =\n        monitorStatus.data.find((m) => region === m.region)?.status || \"active\";\n      const payload = getCheckerPayload(row, status);\n      const url = getCheckerUrl(row, { data: true });\n\n      const result = fetch(url, {\n        headers: {\n          \"Content-Type\": \"application/json\",\n          \"fly-prefer-region\": region, // Specify the region you want the request to be sent to\n          Authorization: `Basic ${env.CRON_SECRET}`,\n        },\n        method: \"POST\",\n        body: JSON.stringify(payload),\n      });\n      allResult.push(result);\n    }\n\n    if (noWait) {\n      return c.json([], 200);\n    }\n\n    const result = await Promise.all(allResult);\n    const bodies = await Promise.all(result.map((r) => r.json()));\n\n    const data = TriggerResult.array().safeParse(bodies);\n\n    if (!data) {\n      throw new HTTPException(400, { message: \"Something went wrong\" });\n    }\n\n    if (!data.success) {\n      logger.error(\"Failed to parse trigger result\", {\n        monitor_id: id,\n        workspace_id: workspaceId,\n        error: data.error.message,\n      });\n      throw new HTTPException(400, { message: \"Something went wrong\" });\n    }\n\n    return c.json(data.data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/run/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nexport const QuerySchema = z\n  .object({\n    \"no-wait\": z.coerce.boolean().optional().prefault(false).openapi({\n      description: \"Don't wait for the result\",\n    }),\n  })\n  .openapi({\n    description: \"Query parameters\",\n  });\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nimport {\n  numberCompare,\n  recordCompare,\n  stringCompare,\n} from \"@openstatus/assertions\";\nimport { monitorJobTypes, monitorMethods } from \"@openstatus/db/src/schema\";\nimport {\n  monitorPeriodicitySchema,\n  monitorRegions,\n} from \"@openstatus/db/src/schema/constants\";\nimport { AVAILABLE_REGIONS } from \"@openstatus/regions\";\nimport { ZodError } from \"zod\";\n\nconst statusAssertion = z\n  .object({\n    type: z.literal(\"status\"),\n    compare: z.enum(numberCompare.options).openapi({\n      description: \"Comparison operator\",\n      examples: [\"eq\", \"not_eq\", \"gt\", \"gte\", \"lt\", \"lte\"],\n    }),\n    target: z.int().positive().openapi({ description: \"The target value\" }),\n  })\n  .openapi({\n    description: \"The status assertion\",\n  });\n\nconst headerAssertion = z\n  .object({\n    type: z.literal(\"header\"),\n    compare: stringCompare,\n    key: z.string().openapi({\n      description: \"The key of the header\",\n    }),\n    target: z.string().openapi({\n      description: \"the header value\",\n    }),\n  })\n  .openapi({ description: \"The header assertion\" });\n\nconst textBodyAssertion = z\n  .object({\n    type: z.literal(\"textBody\"),\n    compare: stringCompare,\n    target: z.string().openapi({\n      description: \"The target value\",\n    }),\n  })\n  .openapi({ description: \"The text body assertion\" });\n\n//   Not used yet\nconst _jsonBodyAssertion = z.object({\n  type: z.literal(\"jsonBody\"),\n  path: z.string(), // https://www.npmjs.com/package/jsonpath-plus\n  compare: stringCompare,\n  target: z.string(),\n});\n\nexport const dnsRecords = [\"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\", \"NS\"] as const;\n\nexport const recordAssertion = z\n  .object({\n    type: z.literal(\"dnsRecord\"),\n    key: z.enum(dnsRecords),\n    compare: recordCompare,\n    target: z.string(),\n  })\n  .openapi({ description: \"The DNS record assertion\" });\n\nexport const assertion = z.discriminatedUnion(\"type\", [\n  statusAssertion,\n  headerAssertion,\n  textBodyAssertion,\n  recordAssertion,\n  // jsonBodyAssertion,\n]);\n\nexport const ParamsSchema = z.object({\n  id: z\n    .string()\n    .min(1)\n    .openapi({\n      param: {\n        name: \"id\",\n        in: \"path\",\n      },\n      description: \"The id of the monitor\",\n      example: \"1\",\n    }),\n});\n\nconst PeriodicityEnumHonoSchema = z.enum([...monitorPeriodicitySchema.options]);\n\nexport const MonitorSchema = z\n  .object({\n    id: z.number().openapi({\n      example: 123,\n      description: \"The id of the monitor\",\n    }),\n    periodicity: PeriodicityEnumHonoSchema.openapi({\n      example: \"1m\",\n      description: \"How often the monitor should run\",\n    }),\n    url: z.string().openapi({\n      example: \"https://www.documenso.co\",\n      description: \"The url to monitor\",\n    }),\n    regions: z\n      .preprocess(\n        (val) => {\n          let parsedRegions: Array<unknown> = [];\n          if (!val) return parsedRegions;\n          if (Array.isArray(val)) {\n            parsedRegions = val;\n          }\n          if (String(val).length > 0) {\n            parsedRegions = String(val).split(\",\");\n          }\n          return parsedRegions;\n        },\n        z.array(z.enum(monitorRegions)),\n      )\n      .superRefine((regions, ctx) => {\n        const deprecatedRegions = regions.filter((r) => {\n          return !AVAILABLE_REGIONS.includes(\n            r as (typeof AVAILABLE_REGIONS)[number],\n          );\n        });\n        if (deprecatedRegions.length > 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            path: [\"regions\"],\n            message: `Deprecated regions are not allowed: ${deprecatedRegions.join(\n              \", \",\n            )}`,\n          });\n        }\n      })\n      .prefault([])\n      .openapi({\n        example: [\"ams\"],\n        description: \"Where we should monitor it\",\n      }),\n    name: z.string().openapi({\n      example: \"documenso-web\",\n      description: \"The name of the monitor\",\n    }),\n    externalName: z.string().nullish().openapi({\n      example: \"Documenso\",\n      description:\n        \"The external name of the monitor, used to display on the status page or in the external notifications\",\n    }),\n    description: z.string().nullish().openapi({\n      example: \"Documenso website\",\n      description: \"The description of your monitor\",\n    }),\n    method: z.enum(monitorMethods).openapi({ example: \"GET\" }),\n    body: z\n      .preprocess((val) => {\n        return String(val);\n      }, z.string())\n      .nullish()\n      .prefault(\"\")\n      .openapi({\n        example: \"Hello World\",\n        description: \"The body\",\n      }),\n    headers: z\n      .preprocess(\n        (val) => {\n          try {\n            if (Array.isArray(val)) return val;\n            if (String(val).length > 0) {\n              return JSON.parse(String(val));\n            }\n            return [];\n          } catch (e) {\n            throw new ZodError([\n              {\n                code: \"custom\",\n                path: [\"headers\"],\n                message: e instanceof Error ? e.message : \"Invalid value\",\n              },\n            ]);\n          }\n        },\n        z.array(z.object({ key: z.string(), value: z.string() })).prefault([]),\n      )\n      .nullish()\n      .openapi({\n        description: \"The headers of your request\",\n        example: [{ key: \"x-apikey\", value: \"supersecrettoken\" }],\n      }),\n    assertions: z\n      .preprocess((val) => {\n        try {\n          if (Array.isArray(val)) return val;\n          if (String(val).length > 0) {\n            return JSON.parse(String(val));\n          }\n          return [];\n        } catch (e) {\n          throw new ZodError([\n            {\n              code: \"custom\",\n              path: [\"assertions\"],\n              message: e instanceof Error ? e.message : \"Invalid value\",\n            },\n          ]);\n        }\n      }, z.array(assertion))\n      .nullish()\n      .prefault([])\n      .openapi({\n        description: \"The assertions to run\",\n      }),\n    active: z\n      .boolean()\n      .prefault(false)\n      .openapi({ description: \"If the monitor is active\" }),\n    public: z\n      .boolean()\n      .prefault(false)\n      .openapi({ description: \"If the monitor is public\" }),\n    degradedAfter: z.number().nullish().openapi({\n      description:\n        \"The time after the monitor is considered degraded in milliseconds\",\n    }),\n    timeout: z.number().nullish().prefault(45000).openapi({\n      description: \"The timeout of the request in milliseconds\",\n    }),\n    retry: z.number().prefault(3).openapi({\n      description: \"The number of retries to attempt\",\n    }),\n    followRedirects: z.boolean().prefault(true).openapi({\n      description: \"If the monitor should follow redirects\",\n    }),\n    jobType: z.enum(monitorJobTypes).optional().prefault(\"http\").openapi({\n      description: \"The type of the monitor\",\n    }),\n    openTelemetry: z\n      .object({\n        endpoint: z.url().optional().prefault(\"http://localhost:4317\").openapi({\n          description: \"The endpoint of the OpenTelemetry collector\",\n        }),\n        headers: z\n          .record(z.string(), z.string())\n          .optional()\n          .prefault({})\n          .openapi({\n            description: \"The headers to send to the OpenTelemetry collector\",\n          }),\n      })\n      .optional()\n      .openapi({\n        description: \"The OpenTelemetry configuration\",\n      }),\n  })\n  .openapi(\"Monitor\");\n\nexport type MonitorSchema = z.infer<typeof MonitorSchema>;\n\n// TODO: Move to @/libs/checker/schema\nconst timingSchema = z.object({\n  dnsStart: z.number(),\n  dnsDone: z.number(),\n  connectStart: z.number(),\n  connectDone: z.number(),\n  tlsHandshakeStart: z.number(),\n  tlsHandshakeDone: z.number(),\n  firstByteStart: z.number(),\n  firstByteDone: z.number(),\n  transferStart: z.number(),\n  transferDone: z.number(),\n});\n\n// Use a baseSchema with 'latency', 'region', 'timestamp'\n\nexport const HTTPTriggerResult = z.object({\n  jobType: z.literal(\"http\"),\n  status: z.number(),\n  latency: z.number(),\n  region: z.enum(monitorRegions),\n  timestamp: z.number(),\n  timing: timingSchema,\n  body: z.string().optional().nullable(),\n  error: z.string().optional().nullable(),\n});\n\nconst tcptimingSchema = z.object({\n  tcpStart: z.number(),\n  tcpDone: z.number(),\n});\n\nexport const TCPTriggerResult = z.object({\n  jobType: z.literal(\"tcp\"),\n  latency: z.number(),\n  region: z.enum(monitorRegions),\n  timestamp: z.number(),\n  timing: tcptimingSchema,\n  // check if it should be z.coerce.boolean()?\n  error: z.number().optional().nullable(),\n  errorMessage: z.string().optional().nullable(),\n});\n\nexport const TriggerResult = z.discriminatedUnion(\"jobType\", [\n  HTTPTriggerResult,\n  TCPTriggerResult,\n]);\n\nexport const ResultRun = z.object({\n  latency: z.int(), // in ms\n  statusCode: z.int().nullable().prefault(null),\n  monitorId: z.string().prefault(\"\"),\n  url: z.string().optional(),\n  error: z.coerce.boolean().prefault(false),\n  region: z.enum(monitorRegions),\n  timestamp: z.int().optional(),\n  message: z.string().nullable().optional(),\n  timing: z\n    .preprocess((val) => {\n      if (!val) return null;\n      const value = timingSchema.safeParse(JSON.parse(String(val)));\n      if (value.success) return value.data;\n      return null;\n    }, timingSchema.nullable())\n    .optional(),\n});\n\nconst baseRequest = z.object({\n  name: z.string().openapi({\n    description: \"Name of the monitor\",\n  }),\n  description: z.string().optional(),\n  retry: z\n    .number()\n    .max(10)\n    .min(1)\n    .optional()\n    .openapi({\n      description: \"Number of retries to attempt\",\n      examples: [1, 3, 5],\n      default: 3,\n    }),\n  degradedAfter: z\n    .number()\n    .min(0)\n    .optional()\n    .openapi({\n      description:\n        \"Time in milliseconds to wait before marking the request as degraded\",\n      examples: [30000],\n      default: 30000,\n    }),\n  timeout: z\n    .number()\n    .min(0)\n    .optional()\n    .openapi({\n      description:\n        \"Time in milliseconds to wait before marking the request as timed out\",\n      examples: [45000],\n      default: 45000,\n    }),\n  frequency: z.enum([\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\"]),\n  active: z.boolean().optional().openapi({\n    description: \"Whether the monitor is active\",\n    default: false,\n  }),\n  public: z.boolean().optional().openapi({\n    description: \"Whether the monitor is public\",\n    default: false,\n  }),\n  regions: z\n    .preprocess(\n      (val) => {\n        let parsedRegions: Array<unknown> = [];\n        if (!val) return parsedRegions;\n        if (Array.isArray(val)) {\n          parsedRegions = val;\n        }\n        if (String(val).length > 0) {\n          parsedRegions = String(val).split(\",\");\n        }\n        return parsedRegions;\n      },\n      z.array(z.enum(monitorRegions)),\n    )\n    .superRefine((regions, ctx) => {\n      const deprecatedRegions = regions.filter((r) => {\n        return !AVAILABLE_REGIONS.includes(\n          r as (typeof AVAILABLE_REGIONS)[number],\n        );\n      });\n      if (deprecatedRegions.length > 0) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: [\"regions\"],\n          message: `Deprecated regions are not allowed: ${deprecatedRegions.join(\n            \", \",\n          )}`,\n        });\n      }\n    })\n    .prefault([])\n    .openapi({\n      example: [\"ams\"],\n      description: \"Where we should monitor it\",\n    }),\n  openTelemetry: z\n    .object({\n      endpoint: z\n        .url()\n        .optional()\n        .openapi({\n          description: \"OTEL endpoint to send metrics to\",\n          examples: [\"https://otel.example.com\"],\n        }),\n      headers: z\n        .record(z.string(), z.string())\n        .optional()\n        .openapi({\n          description: \"Headers to send with the OTEL request\",\n          examples: [{ \"Content-Type\": \"application/json\" }],\n        }),\n    })\n    .nullish(),\n});\n\nconst httpRequestSchema = z.object({\n  method: z.enum(monitorMethods),\n  url: z.url().openapi({\n    description: \"URL to request\",\n    examples: [\"https://openstat.us\", \"https://www.openstatus.dev\"],\n  }),\n  headers: z\n    .record(z.string(), z.string())\n    .optional()\n    .openapi({\n      description: \"Headers to send with the request\",\n      examples: [{ \"Content-Type\": \"application/json\" }],\n    }),\n  body: z\n    .string()\n    .optional()\n    .openapi({\n      description: \"Body to send with the request\",\n      examples: ['{ \"key\": \"value\" }', \"Hello World\"],\n    }),\n});\n\nconst tcpRequestSchema = z.object({\n  host: z\n    .string()\n    .min(1)\n    .openapi({\n      examples: [\"example.com\", \"localhost\"],\n      description: \"Host to connect to\",\n    }),\n  port: z.number().openapi({\n    description: \"Port to connect to\",\n    examples: [80, 443, 1337],\n  }),\n});\n\nconst dnsRequestSchema = z.object({\n  uri: z.string().openapi({\n    description: \"The DNS server to query\",\n    examples: [\"openstatus.dev\"],\n  }),\n});\n\nconst statusCodeAssertion = z\n  .object({\n    kind: z.literal(\"statusCode\"),\n    compare: z.enum(numberCompare.options).openapi({\n      description: \"Comparison operator\",\n      examples: [\"eq\", \"not_eq\", \"gt\", \"gte\", \"lt\", \"lte\"],\n    }),\n    target: z.number().openapi({\n      description: \"Status code to assert\",\n      examples: [200, 404, 418, 500],\n    }),\n  })\n  .openapi({\n    examples: [\n      {\n        kind: \"statusCode\",\n        compare: \"eq\",\n        target: 200,\n      },\n      {\n        kind: \"statusCode\",\n        compare: \"not_eq\",\n        target: 404,\n      },\n      {\n        kind: \"statusCode\",\n        compare: \"gt\",\n        target: 300,\n      },\n    ],\n  });\n\nconst headerAssertions = z.object({\n  kind: z.literal(\"header\"),\n  compare: z.enum(stringCompare.options).openapi({\n    description: \"Comparison operator\",\n    examples: [\"eq\", \"not_eq\", \"contains\", \"not_contains\"],\n  }),\n  key: z.string().openapi({\n    description: \"Header key to assert\",\n    examples: [\"Content-Type\", \"X-Request-ID\"],\n  }),\n  target: z.string().openapi({\n    description: \"Header value to assert\",\n    examples: [\"application/json\", \"text/html\"],\n  }),\n});\n\nconst textBodyAssertions = z.object({\n  kind: z.literal(\"textBody\"),\n  compare: z.enum(stringCompare.options).openapi({\n    description: \"Comparison operator\",\n    examples: [\"eq\", \"not_eq\", \"contains\", \"not_contains\"],\n  }),\n  target: z.string().openapi({\n    description: \"Text body to assert\",\n    examples: [\"Hello, world!\", \"404 Not Found\"],\n  }),\n});\n\nconst dnsRecordAssertion = z.object({\n  kind: z.literal(\"dnsRecord\"),\n  recordType: z.enum([\"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\"]).openapi({\n    description: \"Type of DNS record to check\",\n    examples: [\"A\", \"CNAME\"],\n  }),\n  compare: z.enum(recordCompare.options).openapi({\n    description: \"Comparison operator\",\n    examples: [\"eq\", \"not_eq\", \"contains\", \"not_contains\"],\n  }),\n  target: z.string().openapi({\n    description: \"DNS record value to assert\",\n    examples: [\"example.com\"],\n  }),\n});\nexport const assertionsSchema = z.discriminatedUnion(\"kind\", [\n  statusCodeAssertion,\n  headerAssertions,\n  textBodyAssertions,\n]);\n\nexport const HTTPMonitorSchema = baseRequest\n  .extend({\n    assertions: z.array(assertionsSchema).optional().openapi({\n      description: \"Assertions to run on the response\",\n    }),\n    request: httpRequestSchema.openapi({\n      description: \"The HTTP Request we are sending\",\n    }),\n  })\n  .openapi({\n    title: \"HTTP Monitor Schema\",\n  });\n\nexport const TCPMonitorSchema = baseRequest\n  .extend({\n    request: tcpRequestSchema.openapi({\n      description: \"The TCP Request we are sending\",\n    }),\n  })\n  .openapi({\n    title: \"TCP Monitor Schema\",\n  });\n\nexport const DNSMonitorSchema = baseRequest\n  .extend({\n    request: dnsRequestSchema.openapi({\n      description: \"The DNS Request we are sending\",\n    }),\n    assertions: z.array(dnsRecordAssertion).optional().openapi({\n      description: \"Assertions to run on the DNS response\",\n    }),\n  })\n  .openapi({\n    title: \"DNS Monitor Schema\",\n  });\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/summary/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\nimport { z } from \"@hono/zod-openapi\";\n\nimport { app } from \"@/index\";\nimport { SummarySchema } from \"./schema\";\n\ntest.todo(\"return the summary of the monitor\", async () => {\n  const res = await app.request(\"/v1/monitor/1/summary\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = z\n    .object({ data: SummarySchema.array() })\n    .safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/1/summary\");\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid monitor id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/404/summary\", {\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/summary/get.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq, isNull } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\n\nimport { redis, tb } from \"@/libs/clients\";\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport type { monitorsApi } from \"../index\";\nimport { ParamsSchema, SummarySchema } from \"./schema\";\n\n// TODO: is there another better way to mock Redis/Tinybird?\nif (process.env.NODE_ENV === \"test\") {\n  require(\"@/libs/test/preload\");\n}\n\nconst getMonitorStats = createRoute({\n  method: \"get\",\n  tags: [\"monitor\"],\n  summary: \"Get a monitor summary\",\n  description:\n    \"Get a monitor summary of the last 45 days of data to be used within a status page\",\n  path: \"/{id}/summary\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: z.object({\n            data: SummarySchema.array(),\n          }),\n        },\n      },\n      description: \"All the historical metrics\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetMonitorSummary(api: typeof monitorsApi) {\n  return api.openapi(getMonitorStats, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _monitor = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(\n          eq(monitor.id, Number(id)),\n          eq(monitor.workspaceId, workspaceId),\n          isNull(monitor.deletedAt),\n        ),\n      )\n      .get();\n\n    if (!_monitor) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    const cache = await redis.get<SummarySchema[]>(`${id}-daily-stats`);\n\n    if (cache) {\n      // c.get(\"event\").cache_hit = true;\n      return c.json({ data: cache }, 200);\n    }\n\n    // c.get(\"event\").cache_hit = false;\n    const res =\n      _monitor.jobType === \"http\"\n        ? await tb.legacy_httpStatus45d({ monitorId: id })\n        : await tb.legacy_tcpStatus45d({ monitorId: id });\n\n    await redis.set(`${id}-daily-stats`, res.data, { ex: 600 });\n\n    return c.json({ data: res.data }, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/summary/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\nimport { ParamsSchema } from \"../schema\";\n\nexport { ParamsSchema };\n\nexport const SummarySchema = z.object({\n  ok: z.int().openapi({\n    description:\n      \"The number of ok responses (defined by the assertions - or by default status code 200)\",\n  }),\n  count: z.int().openapi({ description: \"The total number of request\" }),\n  day: z.coerce\n    .date()\n    .openapi({ description: \"The date of the daily stat in ISO8601 format\" }),\n});\n\nexport type SummarySchema = z.infer<typeof SummarySchema>;\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/trigger/post.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { afterEach, mock } from \"bun:test\";\nimport { app } from \"@/index\";\nimport { TriggerSchema } from \"./schema\";\n\nconst mockFetch = mock();\n\nglobal.fetch = mockFetch as unknown as typeof fetch;\nmock.module(\"node-fetch\", () => mockFetch);\n\nafterEach(() => {\n  mockFetch.mockReset();\n});\n\ntest(\"trigger monitor with valid id should return 200\", async () => {\n  mockFetch.mockReturnValue(\n    Promise.resolve(\n      new Response(\n        JSON.stringify({\n          jobType: \"http\",\n          status: 200,\n          latency: 100,\n          region: \"ams\",\n          timestamp: 1234567890,\n          timing: {\n            dnsStart: 1,\n            dnsDone: 2,\n            connectStart: 3,\n            connectDone: 4,\n            tlsHandshakeStart: 5,\n            tlsHandshakeDone: 6,\n            firstByteStart: 7,\n            firstByteDone: 8,\n            transferStart: 9,\n            transferDone: 10,\n          },\n        }),\n        { status: 200, headers: { \"content-type\": \"application/json\" } },\n      ),\n    ),\n  );\n\n  const res = await app.request(\"/v1/monitor/1/trigger\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = TriggerSchema.safeParse(json);\n  expect(result.success).toBe(true);\n  expect(json.resultId).toBeDefined();\n  expect(typeof json.resultId).toBe(\"number\");\n});\n\ntest(\"trigger monitor with invalid id should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/999999/trigger\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"trigger monitor without auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/monitor/1/trigger\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"trigger monitor from different workspace should return 404\", async () => {\n  // Monitor 5 belongs to workspace 3, API key 1 is workspace 1\n  const res = await app.request(\"/v1/monitor/5/trigger\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n  expect(res.status).toBe(404);\n});\n\n// TODO: fix this test create a monitor, delete it, then trigger it\ntest.skip(\"trigger deleted monitor should return 404\", async () => {\n  const res = await app.request(\"/v1/monitor/3/trigger\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"trigger TCP monitor with valid id should return 200\", async () => {\n  mockFetch.mockReturnValue(\n    Promise.resolve(\n      new Response(\n        JSON.stringify({\n          jobType: \"tcp\",\n          latency: 50,\n          region: \"ams\",\n          timestamp: 1234567890,\n          timing: {\n            tcpStart: 1,\n            tcpDone: 2,\n          },\n        }),\n        { status: 200, headers: { \"content-type\": \"application/json\" } },\n      ),\n    ),\n  );\n\n  const res = await app.request(\"/v1/monitor/4/trigger\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = TriggerSchema.safeParse(json);\n  expect(result.success).toBe(true);\n  expect(json.resultId).toBeDefined();\n  expect(typeof json.resultId).toBe(\"number\");\n});\n\ntest(\"trigger monitor with multiple regions should return result id\", async () => {\n  mockFetch.mockReturnValue(\n    Promise.resolve(\n      new Response(\n        JSON.stringify({\n          jobType: \"http\",\n          status: 200,\n          latency: 100,\n          region: \"ams\",\n          timestamp: 1234567890,\n          timing: {\n            dnsStart: 1,\n            dnsDone: 2,\n            connectStart: 3,\n            connectDone: 4,\n            tlsHandshakeStart: 5,\n            tlsHandshakeDone: 6,\n            firstByteStart: 7,\n            firstByteDone: 8,\n            transferStart: 9,\n            transferDone: 10,\n          },\n        }),\n        { status: 200, headers: { \"content-type\": \"application/json\" } },\n      ),\n    ),\n  );\n\n  const res = await app.request(\"/v1/monitor/1/trigger\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = TriggerSchema.safeParse(json);\n  expect(result.success).toBe(true);\n  expect(json.resultId).toBeDefined();\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/trigger/post.ts",
    "content": "import { env } from \"@/env\";\nimport { getCheckerPayload, getCheckerUrl } from \"@/libs/checker\";\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { createRoute, z } from \"@hono/zod-openapi\";\nimport { and, eq, gte, isNull, sql } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db/src/db\";\nimport { monitorRun } from \"@openstatus/db/src/schema\";\nimport { monitorStatusTable } from \"@openstatus/db/src/schema/monitor_status/monitor_status\";\nimport { selectMonitorStatusSchema } from \"@openstatus/db/src/schema/monitor_status/validation\";\nimport { monitor } from \"@openstatus/db/src/schema/monitors/monitor\";\nimport { selectMonitorSchema } from \"@openstatus/db/src/schema/monitors/validation\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { monitorsApi } from \"..\";\nimport { ParamsSchema, TriggerSchema } from \"./schema\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"monitor\"],\n  summary: \"Create a monitor trigger\",\n  description: \"Trigger a monitor check without waiting the result\",\n  path: \"/{id}/trigger\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: TriggerSchema,\n        },\n      },\n      description:\n        \"Returns a result id that can be used to get the result of your trigger\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerTriggerMonitor(api: typeof monitorsApi) {\n  return api.openapi(postRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n    const limits = c.get(\"workspace\").limits;\n\n    const lastMonth = new Date().setMonth(new Date().getMonth() - 1);\n\n    const count = (\n      await db\n        .select({ count: sql<number>`count(*)` })\n        .from(monitorRun)\n        .where(\n          and(\n            eq(monitorRun.workspaceId, workspaceId),\n            gte(monitorRun.createdAt, new Date(lastMonth)),\n          ),\n        )\n        .all()\n    )[0].count;\n\n    if (count >= limits[\"synthetic-checks\"]) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more checks\",\n      });\n    }\n\n    const _monitor = await db\n      .select()\n      .from(monitor)\n      .where(\n        and(\n          eq(monitor.id, Number(id)),\n          eq(monitor.workspaceId, workspaceId),\n          isNull(monitor.deletedAt),\n        ),\n      )\n      .get();\n\n    if (!_monitor) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Monitor ${id} not found`,\n      });\n    }\n\n    const validateMonitor = selectMonitorSchema.safeParse(_monitor);\n\n    if (!validateMonitor.success) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message: \"Invalid monitor, please contact support\",\n      });\n    }\n\n    const row = validateMonitor.data;\n\n    // Maybe later overwrite the region\n\n    const _monitorStatus = await db\n      .select()\n      .from(monitorStatusTable)\n      .where(eq(monitorStatusTable.monitorId, _monitor.id))\n      .all();\n\n    const monitorStatus = z\n      .array(selectMonitorStatusSchema)\n      .safeParse(_monitorStatus);\n\n    if (!monitorStatus.success) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message: \"Invalid monitor status, please contact support\",\n      });\n    }\n\n    const timestamp = Date.now();\n\n    const newRun = await db\n      .insert(monitorRun)\n      .values({\n        monitorId: row.id,\n        workspaceId: row.workspaceId,\n        runnedAt: new Date(timestamp),\n      })\n      .returning();\n\n    if (!newRun[0]) {\n      throw new HTTPException(400, { message: \"Something went wrong\" });\n    }\n\n    const allResult = [];\n\n    for (const region of validateMonitor.data.regions) {\n      const status =\n        monitorStatus.data.find((m) => region === m.region)?.status || \"active\";\n      const payload = getCheckerPayload(row, status);\n      const url = getCheckerUrl(row);\n\n      const result = fetch(url, {\n        headers: {\n          \"Content-Type\": \"application/json\",\n          \"fly-prefer-region\": region, // Specify the region you want the request to be sent to\n          Authorization: `Basic ${env.CRON_SECRET}`,\n        },\n        method: \"POST\",\n        body: JSON.stringify(payload),\n      });\n\n      allResult.push(result);\n    }\n\n    await Promise.all(allResult);\n\n    return c.json({ resultId: newRun[0].id }, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/trigger/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\nimport { ParamsSchema } from \"../schema\";\n\nexport { ParamsSchema };\n\nexport const TriggerSchema = z.object({\n  resultId: z.number().openapi({ description: \"the id of your check result\" }),\n});\n\nexport type TriggerSchema = z.infer<typeof TriggerSchema>;\n"
  },
  {
    "path": "apps/server/src/routes/v1/monitors/utils.ts",
    "content": "import type { Assertion } from \"@openstatus/assertions\";\nimport {\n  HeaderAssertion,\n  StatusAssertion,\n  TextBodyAssertion,\n} from \"@openstatus/assertions\";\nimport type { z } from \"zod\";\nimport type { assertion, assertionsSchema } from \"./schema\";\n\nexport const getAssertions = (\n  assertions: z.infer<typeof assertion>[],\n): Assertion[] => {\n  const assert: Assertion[] = [];\n\n  for (const a of assertions) {\n    if (a.type === \"header\") {\n      assert.push(new HeaderAssertion({ ...a, version: \"v1\" }));\n    }\n    if (a.type === \"textBody\") {\n      assert.push(new TextBodyAssertion({ ...a, version: \"v1\" }));\n    }\n    if (a.type === \"status\") {\n      assert.push(new StatusAssertion({ ...a, version: \"v1\" }));\n    }\n  }\n  return assert;\n};\n\nexport const getAssertionNew = (\n  assertions: z.infer<typeof assertionsSchema>[],\n): Assertion[] => {\n  const assert: Assertion[] = [];\n\n  for (const a of assertions) {\n    if (a.kind === \"header\") {\n      const { kind, ...rest } = a;\n      assert.push(\n        new HeaderAssertion({\n          ...rest,\n          type: \"header\",\n          version: \"v1\",\n        }),\n      );\n    }\n    if (a.kind === \"textBody\") {\n      const { kind, ...rest } = a;\n\n      assert.push(\n        new TextBodyAssertion({ ...rest, type: \"textBody\", version: \"v1\" }),\n      );\n    }\n    if (a.kind === \"statusCode\") {\n      const { kind, ...rest } = a;\n      assert.push(\n        new StatusAssertion({ ...rest, type: \"status\", version: \"v1\" }),\n      );\n    }\n  }\n  return assert;\n};\n"
  },
  {
    "path": "apps/server/src/routes/v1/notifications/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { NotificationSchema } from \"./schema\";\n\ntest(\"return the notification\", async () => {\n  const res = await app.request(\"/v1/notification/1\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n  const result = NotificationSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/notification/1\");\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid notification id should return 404\", async () => {\n  const res = await app.request(\"/v1/notification/404\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"invalid auth key should return 404\", async () => {\n  const res = await app.request(\"/v1/notification/1\", {\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/notifications/get.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { and, db, eq } from \"@openstatus/db\";\nimport {\n  notification,\n  notificationsToMonitors,\n} from \"@openstatus/db/src/schema\";\nimport type { notificationsApi } from \"./index\";\nimport { NotificationSchema, ParamsSchema } from \"./schema\";\n\nconst getRoute = createRoute({\n  method: \"get\",\n  tags: [\"notification\"],\n  summary: \"Get a notification\",\n  path: \"/{id}\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: NotificationSchema,\n        },\n      },\n      description: \"Get an Status page\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetNotification(api: typeof notificationsApi) {\n  return api.openapi(getRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _notification = await db\n      .select()\n      .from(notification)\n      .where(\n        and(\n          eq(notification.workspaceId, workspaceId),\n          eq(notification.id, Number(id)),\n        ),\n      )\n      .get();\n\n    if (!_notification) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Notification ${id} not found`,\n      });\n    }\n\n    const _monitors = await db\n      .select()\n      .from(notificationsToMonitors)\n      .where(eq(notificationsToMonitors.notificationId, Number(id)))\n      .all();\n\n    const data = NotificationSchema.parse({\n      ..._notification,\n      payload: JSON.parse(_notification.data || \"{}\"),\n      monitors: _monitors.map((m) => m.monitorId),\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/notifications/get_all.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport { notification } from \"@openstatus/db/src/schema\";\n\nimport { app } from \"@/index\";\nimport { NotificationSchema } from \"./schema\";\n\nconst TEST_PREFIX = \"v1-notif-getall-test\";\nlet testNotificationId: number;\n\nbeforeAll(async () => {\n  await db\n    .delete(notification)\n    .where(eq(notification.name, `${TEST_PREFIX}-email`));\n\n  const notif = await db\n    .insert(notification)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-email`,\n      provider: \"email\",\n      data: '{\"email\":\"test@test.com\"}',\n    })\n    .returning()\n    .get();\n  testNotificationId = notif.id;\n});\n\nafterAll(async () => {\n  await db\n    .delete(notification)\n    .where(eq(notification.name, `${TEST_PREFIX}-email`));\n});\n\ntest(\"return all notifications\", async () => {\n  const res = await app.request(\"/v1/notification\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = NotificationSchema.array().safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.some((n) => n.id === testNotificationId)).toBe(true);\n});\n\ntest(\"return empty notifications\", async () => {\n  const res = await app.request(\"/v1/notification\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"3\",\n    },\n  });\n\n  const result = NotificationSchema.array().safeParse(await res.json());\n\n  expect(result.success).toBe(true);\n  expect(res.status).toBe(200);\n  expect(result.data?.length).toBe(0);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/notification\", {\n    method: \"GET\",\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/notifications/get_all.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { openApiErrorResponses } from \"@/libs/errors\";\nimport { db, eq, inArray } from \"@openstatus/db\";\nimport {\n  notification,\n  notificationsToMonitors,\n} from \"@openstatus/db/src/schema\";\nimport type { notificationsApi } from \"./index\";\nimport { NotificationSchema } from \"./schema\";\n\nconst getAllRoute = createRoute({\n  method: \"get\",\n  tags: [\"notification\"],\n  summary: \"List all notifications\",\n  path: \"/\",\n\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: NotificationSchema.array(),\n        },\n      },\n      description: \"Get all your workspace notification\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetAllNotifications(app: typeof notificationsApi) {\n  return app.openapi(getAllRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n\n    const _notifications = await db\n      .select()\n      .from(notification)\n      .where(eq(notification.workspaceId, workspaceId))\n      .all();\n\n    const _monitors = await db\n      .select()\n      .from(notificationsToMonitors)\n      .where(\n        inArray(\n          notificationsToMonitors.notificationId,\n          _notifications.map((n) => n.id),\n        ),\n      )\n      .all();\n\n    const data = NotificationSchema.array().parse(\n      _notifications.map((n) => ({\n        ...n,\n        payload: JSON.parse(n.data || \"{}\"),\n        monitors: _monitors\n          .filter((m) => m.notificationId === n.id)\n          .map((m) => m.monitorId),\n      })),\n    );\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/notifications/index.ts",
    "content": "import { OpenAPIHono } from \"@hono/zod-openapi\";\n\nimport { handleZodError } from \"@/libs/errors\";\nimport type { Variables } from \"../index\";\nimport { registerGetNotification } from \"./get\";\nimport { registerGetAllNotifications } from \"./get_all\";\nimport { registerPostNotification } from \"./post\";\n\nexport const notificationsApi = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\nregisterGetAllNotifications(notificationsApi);\nregisterGetNotification(notificationsApi);\nregisterPostNotification(notificationsApi);\n"
  },
  {
    "path": "apps/server/src/routes/v1/notifications/post.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { db, eq } from \"@openstatus/db\";\nimport { notification } from \"@openstatus/db/src/schema\";\nimport { NotificationSchema } from \"./schema\";\n\ntest(\"create a notification\", async () => {\n  const res = await app.request(\"/v1/notification\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      name: \"OpenStatus\",\n      provider: \"email\",\n      payload: { email: \"ping@openstatus.dev\" },\n      monitors: [1],\n    }),\n  });\n\n  const result = NotificationSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created notification\n  if (result.success) {\n    await db.delete(notification).where(eq(notification.id, result.data.id));\n  }\n});\n\ntest(\"create a notification with invalid monitor ids should return a 400\", async () => {\n  const res = await app.request(\"/v1/notification\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      name: \"OpenStatus\",\n      provider: \"email\",\n      payload: { email: \"ping@openstatus.dev\" },\n      monitors: [404],\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create a email notification with invalid payload should return a 400\", async () => {\n  const res = await app.request(\"/v1/notification\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      name: \"OpenStatus\",\n      provider: \"email\",\n      payload: { hello: \"world\" },\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/notification\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/notifications/post.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { Events } from \"@openstatus/analytics\";\nimport { and, db, eq, inArray, isNull, sql } from \"@openstatus/db\";\nimport {\n  NotificationDataSchema,\n  monitor,\n  notification,\n  notificationsToMonitors,\n  selectNotificationSchema,\n} from \"@openstatus/db/src/schema\";\nimport type { notificationsApi } from \"./index\";\nimport { NotificationSchema } from \"./schema\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"notification\"],\n  summary: \"Create a notification\",\n  path: \"/\",\n  middleware: [trackMiddleware(Events.CreateNotification, [\"provider\"])],\n  request: {\n    body: {\n      description: \"The notification to create\",\n      content: {\n        \"application/json\": {\n          schema: NotificationSchema.omit({ id: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: NotificationSchema,\n        },\n      },\n      description: \"Return the created notification\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostNotification(api: typeof notificationsApi) {\n  return api.openapi(postRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const workspacePlan = c.get(\"workspace\").plan;\n    const limits = c.get(\"workspace\").limits;\n    const input = c.req.valid(\"json\");\n\n    if (input.provider === \"sms\" && workspacePlan === \"free\") {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for SMS\",\n      });\n    }\n\n    const count = (\n      await db\n        .select({ count: sql<number>`count(*)` })\n        .from(notification)\n        .where(eq(notification.workspaceId, workspaceId))\n        .all()\n    )[0].count;\n\n    if (count >= limits[\"notification-channels\"]) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more notification channels\",\n      });\n    }\n\n    const { payload, monitors, ...rest } = input;\n\n    if (monitors?.length) {\n      const _monitors = await db\n        .select()\n        .from(monitor)\n        .where(\n          and(\n            inArray(monitor.id, monitors),\n            eq(monitor.workspaceId, workspaceId),\n            isNull(monitor.deletedAt),\n          ),\n        )\n        .all();\n\n      if (_monitors.length !== monitors.length) {\n        throw new OpenStatusApiError({\n          code: \"BAD_REQUEST\",\n          message: `Some of the monitors ${monitors.join(\", \")} not found`,\n        });\n      }\n    }\n\n    const _notification = await db\n      .insert(notification)\n      .values({\n        ...rest,\n        workspaceId: workspaceId,\n        data: JSON.stringify(payload),\n      })\n      .returning()\n      .get();\n\n    if (monitors?.length) {\n      for (const monitorId of monitors) {\n        await db\n          .insert(notificationsToMonitors)\n          .values({ notificationId: _notification.id, monitorId })\n          .run();\n      }\n    }\n\n    // FIXME: too complex\n    const d = selectNotificationSchema.parse(_notification);\n\n    const _payload = NotificationDataSchema.parse(JSON.parse(d.data));\n    const data = NotificationSchema.parse({\n      ..._notification,\n      monitors,\n      payload: _payload,\n    });\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/notifications/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nimport { notificationProvider } from \"@openstatus/db/src/schema/notifications/constants\";\n\nexport const ParamsSchema = z.object({\n  id: z\n    .string()\n    .min(1)\n    .openapi({\n      param: {\n        name: \"id\",\n        in: \"path\",\n      },\n      description: \"The id of the notification\",\n      example: \"1\",\n    }),\n});\n\nexport const NotificationSchema = z\n  .object({\n    id: z\n      .number()\n      .openapi({ description: \"The id of the notification\", example: 1 }),\n    name: z.string().openapi({\n      description: \"The name of the notification\",\n      example: \"OpenStatus Discord\",\n    }),\n    provider: z.enum(notificationProvider).openapi({\n      description: \"The provider of the notification\",\n      example: \"discord\",\n    }),\n    payload: z.any().openapi({\n      description: \"The data of the notification\",\n    }),\n    monitors: z\n      .array(z.number())\n      .nullish()\n      .openapi({\n        description: \"The monitors that the notification is linked to\",\n        example: [1, 2],\n      }),\n  })\n  .openapi(\"Notification\");\n\nexport type NotificationSchema = z.infer<typeof NotificationSchema>;\n"
  },
  {
    "path": "apps/server/src/routes/v1/pageSubscribers/index.ts",
    "content": "import { OpenAPIHono } from \"@hono/zod-openapi\";\n\nimport { handleZodError } from \"@/libs/errors\";\nimport type { Variables } from \"../index\";\nimport { registerPostPageSubscriber } from \"./post\";\n\nexport const pageSubscribersApi = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\nregisterPostPageSubscriber(pageSubscribersApi);\n"
  },
  {
    "path": "apps/server/src/routes/v1/pageSubscribers/post.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { db, eq } from \"@openstatus/db\";\nimport { pageSubscriber } from \"@openstatus/db/src/schema\";\nimport { PageSubscriberSchema } from \"./schema\";\n\ntest(\"create a page subscription\", async () => {\n  const res = await app.request(\"/v1/page_subscriber/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({ email: \"ping@openstatus.dev\" }),\n  });\n\n  const result = PageSubscriberSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created page subscriber\n  if (result.success) {\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.id, result.data.id));\n  }\n});\n\ntest(\"create a scubscriber with invalid email should return a 400\", async () => {\n  const res = await app.request(\"/v1/page_subscriber/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({ email: \"ping\" }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/page_subscriber/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/pageSubscribers/post.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { Events } from \"@openstatus/analytics\";\nimport { and, eq, isNotNull } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db/src/db\";\nimport { page, pageSubscriber } from \"@openstatus/db/src/schema\";\nimport { SubscribeEmail, sendEmail } from \"@openstatus/emails\";\nimport type { pageSubscribersApi } from \"./index\";\nimport { PageSubscriberSchema, ParamsSchema } from \"./schema\";\n\nconst postRouteSubscriber = createRoute({\n  method: \"post\",\n  tags: [\"page_subscriber\"],\n  summary: \"Subscribe to a status page\",\n  path: \"/{id}/update\",\n  middleware: [trackMiddleware(Events.SubscribePage)],\n  description: \"Add a subscriber to a status page\", // TODO: how to define legacy routes\n  request: {\n    params: ParamsSchema,\n    body: {\n      description: \"The subscriber payload\",\n      content: {\n        \"application/json\": {\n          schema: PageSubscriberSchema.pick({ email: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: PageSubscriberSchema,\n        },\n      },\n      description: \"The user has been subscribed\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostPageSubscriber(api: typeof pageSubscribersApi) {\n  return api.openapi(postRouteSubscriber, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const input = c.req.valid(\"json\");\n    const { id } = c.req.valid(\"param\");\n\n    if (!limits[\"status-subscribers\"]) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for status subscribers\",\n      });\n    }\n\n    const _page = await db\n      .select()\n      .from(page)\n      .where(and(eq(page.id, Number(id)), eq(page.workspaceId, workspaceId)))\n      .get();\n\n    if (!_page) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Page ${id} not found`,\n      });\n    }\n\n    const alreadySubscribed = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.email, input.email),\n          eq(pageSubscriber.pageId, Number(id)),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNotNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .get();\n\n    if (alreadySubscribed) {\n      throw new OpenStatusApiError({\n        code: \"CONFLICT\",\n        message: `Email ${input.email} already subscribed`,\n      });\n    }\n\n    const token = crypto.randomUUID();\n    const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7);\n\n    const _statusReportSubscriberUpdate = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: _page.id,\n        email: input.email,\n        token,\n        expiresAt,\n      })\n      .returning()\n      .get();\n\n    const link = `https://${_page.slug}.openstatus.dev/verify/${token}`;\n\n    await sendEmail({\n      react: SubscribeEmail({\n        link,\n        page: _page.title,\n      }),\n      from: \"OpenStatus <notification@notifications.openstatus.dev>\",\n      to: [input.email],\n      subject: \"Verify your subscription\",\n    });\n\n    const data = PageSubscriberSchema.parse(_statusReportSubscriberUpdate);\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/pageSubscribers/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nexport const ParamsSchema = z.object({\n  id: z\n    .string()\n    .min(1)\n    .openapi({\n      param: {\n        name: \"id\",\n        in: \"path\",\n      },\n      description: \"The id of the page\",\n      example: \"1\",\n    }),\n});\n\nexport const PageSubscriberSchema = z\n  .object({\n    id: z.number().openapi({\n      description: \"The id of the subscriber\",\n      example: 1,\n    }),\n    email: z.email().openapi({\n      description: \"The email of the subscriber\",\n    }),\n    pageId: z.number().openapi({\n      description: \"The id of the page to subscribe to\",\n      example: 1,\n    }),\n  })\n  .openapi(\"PageSubscriber\");\n\nexport type PageSubscriberSchema = z.infer<typeof PageSubscriberSchema>;\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { PageSchema } from \"./schema\";\n\ntest(\"return the page\", async () => {\n  const res = await app.request(\"/v1/page/1\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n  const result = PageSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/page/2\");\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid page id should return 404\", async () => {\n  const res = await app.request(\"/v1/page/2\", {\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/get.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { notEmpty } from \"@/utils/not-empty\";\nimport { and, eq } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db/src/db\";\nimport { page } from \"@openstatus/db/src/schema\";\nimport type { pagesApi } from \"./index\";\nimport { PageSchema, ParamsSchema, transformPageData } from \"./schema\";\n\nconst getRoute = createRoute({\n  method: \"get\",\n  tags: [\"page\"],\n  summary: \"Get a status page\",\n  path: \"/{id}\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: PageSchema,\n        },\n      },\n      description: \"Get an Status page\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetPage(api: typeof pagesApi) {\n  return api.openapi(getRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _page = await db.query.page.findFirst({\n      where: and(eq(page.workspaceId, workspaceId), eq(page.id, Number(id))),\n      with: {\n        pageComponents: true,\n      },\n    });\n\n    if (!_page) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Page ${id} not found`,\n      });\n    }\n\n    const monitorIds = _page.pageComponents\n      .map((pc) => pc.monitorId)\n      .filter(notEmpty);\n\n    const data = transformPageData(\n      PageSchema.parse({\n        ..._page,\n        monitors: monitorIds.length > 0 ? monitorIds : undefined,\n      }),\n    );\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/get_all.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport { monitor, page, pageComponent } from \"@openstatus/db/src/schema\";\n\nimport { app } from \"@/index\";\nimport { PageSchema } from \"./schema\";\n\nconst TEST_PREFIX = \"v1-page-getall-test\";\nlet testMonitorId: number;\nlet testPageId: number;\n\nbeforeAll(async () => {\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n\n  const mon = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-monitor`,\n      url: \"https://test.example.com\",\n      periodicity: \"1m\",\n      active: true,\n      regions: \"ams\",\n      jobType: \"http\",\n      method: \"GET\",\n      timeout: 30000,\n    })\n    .returning()\n    .get();\n  testMonitorId = mon.id;\n\n  const p = await db\n    .insert(page)\n    .values({\n      workspaceId: 1,\n      title: `${TEST_PREFIX}-page`,\n      slug: `${TEST_PREFIX}-slug`,\n      description: \"Test page\",\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n  testPageId = p.id;\n\n  await db.insert(pageComponent).values({\n    workspaceId: 1,\n    pageId: testPageId,\n    monitorId: testMonitorId,\n    type: \"monitor\",\n    name: `${TEST_PREFIX}-component`,\n    order: 0,\n  });\n});\n\nafterAll(async () => {\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n  await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n});\n\ntest(\"return all pages\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = PageSchema.array().safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.some((p) => p.id === testPageId)).toBe(true);\n});\n\ntest(\"return empty pages\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"3\",\n    },\n  });\n\n  const result = PageSchema.array().safeParse(await res.json());\n\n  expect(result.success).toBe(true);\n  expect(res.status).toBe(200);\n  expect(result.data?.length).toBe(0);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"GET\",\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/get_all.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { openApiErrorResponses } from \"@/libs/errors\";\nimport { notEmpty } from \"@/utils/not-empty\";\nimport { db, eq } from \"@openstatus/db\";\nimport { page } from \"@openstatus/db/src/schema\";\nimport type { pagesApi } from \"./index\";\nimport { PageSchema, transformPageData } from \"./schema\";\n\nconst getAllRoute = createRoute({\n  method: \"get\",\n  tags: [\"page\"],\n  summary: \"List all status pages\",\n  path: \"/\",\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: PageSchema.array(),\n        },\n      },\n      description: \"A list of your status pages\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetAllPages(api: typeof pagesApi) {\n  return api.openapi(getAllRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n\n    const _pages = await db.query.page.findMany({\n      where: eq(page.workspaceId, workspaceId),\n      with: {\n        pageComponents: true,\n      },\n    });\n\n    const data = PageSchema.array()\n      .parse(\n        _pages.map((p) => {\n          const monitorIds = p.pageComponents\n            .map((pc) => pc.monitorId)\n            .filter(notEmpty);\n          return {\n            ...p,\n            monitors: monitorIds.length > 0 ? monitorIds : undefined,\n          };\n        }),\n      )\n      .map((page) => transformPageData(page));\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/index.ts",
    "content": "import { OpenAPIHono } from \"@hono/zod-openapi\";\n\nimport { handleZodError } from \"@/libs/errors\";\nimport type { Variables } from \"../index\";\nimport { registerGetPage } from \"./get\";\nimport { registerGetAllPages } from \"./get_all\";\nimport { registerPostPage } from \"./post\";\nimport { registerPutPage } from \"./put\";\n\nexport const pagesApi = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\nregisterGetPage(pagesApi);\nregisterGetAllPages(pagesApi);\nregisterPutPage(pagesApi);\nregisterPostPage(pagesApi);\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/post.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { db, eq } from \"@openstatus/db\";\nimport { monitor, page, pageComponent } from \"@openstatus/db/src/schema\";\nimport { PageSchema } from \"./schema\";\n\ntest(\"create a valid page\", async () => {\n  const uniqueSlug = `openstatus-${Date.now()}`;\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      slug: uniqueSlug,\n      monitors: [1],\n    }),\n  });\n\n  const result = PageSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  // Cleanup: delete the created page\n  if (result.success) {\n    await db.delete(page).where(eq(page.id, result.data.id));\n  }\n});\n\ntest(\"create a page with invalid monitor ids should return a 400\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      slug: \"another-openstatus\",\n      monitors: [404],\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create a page with password on free plan should return a 402\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"2\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      slug: \"password-openstatus\",\n      passwordProtected: true,\n    }),\n  });\n\n  expect(res.status).toBe(402);\n});\n\ntest(\"create a email page with invalid payload should return a 400\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      name: \"OpenStatus\",\n      provider: \"email\",\n      payload: { hello: \"world\" },\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"create a page with custom domain without limits should return 402\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"2\", // Free plan\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      slug: `custom-domain-${Date.now()}`,\n      customDomain: \"status.example.com\",\n    }),\n  });\n\n  expect(res.status).toBe(402);\n  const json = await res.json();\n  expect(json.message).toBe(\"Upgrade for custom domains\");\n});\n\ntest(\"create a page with custom domain containing 'openstatus' should return 400\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      slug: `openstatus-domain-${Date.now()}`,\n      customDomain: \"status.openstatus.dev\",\n    }),\n  });\n\n  expect(res.status).toBe(400);\n  const json = await res.json();\n  expect(json.message).toBe(\"Domain cannot contain 'openstatus'\");\n});\n\ntest(\"create a page with reserved slug should return 400\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      slug: \"api\", // Reserved slug\n    }),\n  });\n\n  expect(res.status).toBe(400);\n  const json = await res.json();\n  expect(json.message).toBe(\"Slug is reserved\");\n});\n\ntest(\"create a page with duplicate slug should return 400\", async () => {\n  const uniqueSlug = `duplicate-test-${Date.now()}`;\n\n  // Create first page\n  const res1 = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus First\",\n      description: \"First page\",\n      slug: uniqueSlug,\n    }),\n  });\n\n  expect(res1.status).toBe(200);\n  const result1 = PageSchema.safeParse(await res1.json());\n  expect(result1.success).toBe(true);\n\n  // Try to create second page with same slug\n  const res2 = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus Second\",\n      description: \"Second page\",\n      slug: uniqueSlug,\n    }),\n  });\n\n  expect(res2.status).toBe(400);\n  const json = await res2.json();\n  expect(json.message).toBe(\"Slug has to be unique and has already been taken\");\n\n  // Cleanup\n  if (result1.success) {\n    await db.delete(page).where(eq(page.id, result1.data.id));\n  }\n});\n\ntest(\"create a page with email domain protection on free plan should return 402\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"2\", // Free plan\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      slug: `email-domain-${Date.now()}`,\n      accessType: \"email-domain\",\n      authEmailDomains: [\"example.com\"],\n    }),\n  });\n\n  expect(res.status).toBe(402);\n  const json = await res.json();\n  expect(json.message).toBe(\"Upgrade for email domain protection\");\n});\n\ntest(\"create a page with accessType password on free plan should return 402\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"2\", // Free plan\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      slug: `access-type-password-${Date.now()}`,\n      accessType: \"password\",\n      password: \"secret123\",\n    }),\n  });\n\n  expect(res.status).toBe(402);\n  const json = await res.json();\n  expect(json.message).toBe(\"Upgrade for password protection\");\n});\n\ntest(\"create a page with monitors as objects with order\", async () => {\n  const uniqueSlug = `ordered-monitors-${Date.now()}`;\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus Ordered\",\n      description: \"Page with ordered monitors\",\n      slug: uniqueSlug,\n      monitors: [\n        { monitorId: 1, order: 1 },\n        { monitorId: 2, order: 0 },\n      ],\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = PageSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  if (result.success) {\n    // Verify pageComponent entries were created with correct order\n    const components = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.pageId, result.data.id))\n      .all();\n\n    expect(components.length).toBe(2);\n    expect(components.find((c) => c.monitorId === 1)?.order).toBe(1);\n    expect(components.find((c) => c.monitorId === 2)?.order).toBe(0);\n\n    // Cleanup\n    await db.delete(page).where(eq(page.id, result.data.id));\n  }\n});\n\ntest(\"create a page without monitors should succeed\", async () => {\n  const uniqueSlug = `no-monitors-${Date.now()}`;\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus No Monitors\",\n      description: \"Page without monitors\",\n      slug: uniqueSlug,\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = PageSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  if (result.success) {\n    // Verify no pageComponent entries were created\n    const components = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.pageId, result.data.id))\n      .all();\n\n    expect(components.length).toBe(0);\n\n    // Cleanup\n    await db.delete(page).where(eq(page.id, result.data.id));\n  }\n});\n\ntest(\"create a page with monitors as number array should use index as order\", async () => {\n  const uniqueSlug = `number-array-${Date.now()}`;\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus Number Array\",\n      description: \"Page with monitors as numbers\",\n      slug: uniqueSlug,\n      monitors: [2, 1],\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = PageSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  if (result.success) {\n    // Verify pageComponent entries were created with index as order\n    const components = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.pageId, result.data.id))\n      .all();\n\n    expect(components.length).toBe(2);\n    expect(components.find((c) => c.monitorId === 2)?.order).toBe(0);\n    expect(components.find((c) => c.monitorId === 1)?.order).toBe(1);\n\n    // Cleanup\n    await db.delete(page).where(eq(page.id, result.data.id));\n  }\n});\n\ntest(\"create a page with partial invalid monitors should return 400\", async () => {\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"OpenStatus\",\n      description: \"OpenStatus website\",\n      slug: `partial-invalid-${Date.now()}`,\n      monitors: [1, 999], // 1 exists, 999 doesn't\n    }),\n  });\n\n  expect(res.status).toBe(400);\n  const json = await res.json();\n  expect(json.message).toContain(\"not found\");\n});\n\ntest(\"create a page syncs correctly to pageComponent\", async () => {\n  const uniqueSlug = `sync-test-${Date.now()}`;\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"Sync Test\",\n      description: \"Testing sync to both tables\",\n      slug: uniqueSlug,\n      monitors: [{ monitorId: 1, order: 0 }],\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = PageSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  if (result.success) {\n    // Verify pageComponent (primary table)\n    const components = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.pageId, result.data.id))\n      .all();\n\n    expect(components.length).toBe(1);\n    expect(components[0].monitorId).toBe(1);\n    expect(components[0].type).toBe(\"monitor\");\n\n    // Cleanup\n    await db.delete(page).where(eq(page.id, result.data.id));\n  }\n});\n\ntest(\"create a page uses monitor externalName when available\", async () => {\n  const uniqueSlug = `external-name-${Date.now()}`;\n\n  // First, check if monitor has externalName set\n  const monitorData = await db\n    .select()\n    .from(monitor)\n    .where(eq(monitor.id, 1))\n    .get();\n\n  const res = await app.request(\"/v1/page\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"External Name Test\",\n      description: \"Testing monitor external name\",\n      slug: uniqueSlug,\n      monitors: [1],\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = PageSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  if (result.success) {\n    const components = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.pageId, result.data.id))\n      .all();\n\n    expect(components.length).toBe(1);\n    // Should use externalName if available, otherwise name\n    const expectedName = monitorData?.externalName || monitorData?.name;\n    if (!expectedName) {\n      throw new Error(\"Expected name is undefined\");\n    }\n    expect(components[0].name).toBe(expectedName);\n\n    // Cleanup\n    await db.delete(page).where(eq(page.id, result.data.id));\n  }\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/post.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { and, eq, inArray, isNull, sql } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db/src/db\";\nimport {\n  monitor,\n  page,\n  pageComponent,\n  subdomainSafeList,\n} from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { Events } from \"@openstatus/analytics\";\nimport { isNumberArray } from \"../utils\";\nimport type { pagesApi } from \"./index\";\nimport { PageSchema, transformPageData } from \"./schema\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"page\"],\n  summary: \"Create a status page\",\n  path: \"/\",\n  middleware: [trackMiddleware(Events.CreatePage, [\"slug\"])],\n  request: {\n    body: {\n      description: \"The status page to create\",\n      content: {\n        \"application/json\": {\n          schema: PageSchema.omit({ id: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: PageSchema,\n        },\n      },\n      description: \"Get an Status page\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostPage(api: typeof pagesApi) {\n  return api.openapi(postRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const input = c.req.valid(\"json\");\n\n    if (input.customDomain && !limits[\"custom-domain\"]) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for custom domains\",\n      });\n    }\n\n    if (input.customDomain?.toLowerCase().includes(\"openstatus\")) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message: \"Domain cannot contain 'openstatus'\",\n      });\n    }\n\n    if (\n      !limits[\"password-protection\"] &&\n      (input?.passwordProtected || input?.password)\n    ) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for password protection\",\n      });\n    }\n\n    if (\n      !limits[\"password-protection\"] &&\n      (input?.accessType === \"password\" || input?.password)\n    ) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for password protection\",\n      });\n    }\n\n    if (\n      !limits[\"email-domain-protection\"] &&\n      (input?.accessType === \"email-domain\" || input?.authEmailDomains?.length)\n    ) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for email domain protection\",\n      });\n    }\n\n    const count = (\n      await db\n        .select({ count: sql<number>`count(*)` })\n        .from(page)\n        .where(eq(page.workspaceId, workspaceId))\n        .all()\n    )[0].count;\n\n    if (count >= limits[\"status-pages\"]) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for more status pages\",\n      });\n    }\n\n    if (subdomainSafeList.includes(input.slug)) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message: \"Slug is reserved\",\n      });\n    }\n\n    const countSlug = (\n      await db\n        .select({ count: sql<number>`count(*)` })\n        .from(page)\n        .where(eq(page.slug, input.slug))\n        .all()\n    )[0].count;\n\n    if (countSlug > 0) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message: \"Slug has to be unique and has already been taken\",\n      });\n    }\n\n    const { monitors, ...rest } = input;\n\n    if (monitors?.length) {\n      const monitorIds = isNumberArray(monitors)\n        ? monitors\n        : monitors.map((m) => m.monitorId);\n\n      const _monitors = await db\n        .select()\n        .from(monitor)\n        .where(\n          and(\n            inArray(monitor.id, monitorIds),\n            eq(monitor.workspaceId, workspaceId),\n            isNull(monitor.deletedAt),\n          ),\n        )\n        .all();\n\n      if (_monitors.length !== monitors.length) {\n        throw new OpenStatusApiError({\n          code: \"BAD_REQUEST\",\n          message: `Some of the monitors ${monitorIds.join(\", \")} not found`,\n        });\n      }\n    }\n\n    const _page = await db\n      .insert(page)\n      .values({\n        ...rest,\n        workspaceId: workspaceId,\n        customDomain: rest.customDomain ?? \"\", // TODO  : make database migration to allow null\n        accessType:\n          rest.accessType ?? (rest.passwordProtected ? \"password\" : \"public\"),\n        authEmailDomains: rest.authEmailDomains?.join(\",\"),\n      })\n      .returning()\n      .get();\n\n    // TODO: missing order\n    if (monitors?.length) {\n      for (const [index, m] of monitors.entries()) {\n        const values = typeof m === \"number\" ? { monitorId: m } : m;\n\n        const _monitor = await db.query.monitor.findFirst({\n          where: and(\n            eq(monitor.id, values.monitorId),\n            eq(monitor.workspaceId, workspaceId),\n            isNull(monitor.deletedAt),\n          ),\n        });\n\n        if (!_monitor) {\n          throw new OpenStatusApiError({\n            code: \"BAD_REQUEST\",\n            message: `Monitor ${values.monitorId} not found`,\n          });\n        }\n\n        // Insert to pageComponent (primary table)\n        await db\n          .insert(pageComponent)\n          .values({\n            workspaceId: _page.workspaceId,\n            pageId: _page.id,\n            type: \"monitor\",\n            monitorId: values.monitorId,\n            name: _monitor.externalName || _monitor.name,\n            order: \"order\" in values ? values.order : index,\n            groupId: null,\n            groupOrder: 0,\n          })\n          .run();\n      }\n    }\n    const data = transformPageData(PageSchema.parse(_page));\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/put.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { PageSchema } from \"./schema\";\n\ntest(\"update the page with monitor ids\", async () => {\n  const res = await app.request(\"/v1/page/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      title: \"New Title\",\n      monitors: [1, 2],\n    }),\n  });\n\n  const result = PageSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.title).toBe(\"New Title\");\n  expect(result.data?.monitors).toEqual([1, 2]);\n});\n\ntest(\"update the page with monitor objects\", async () => {\n  const res = await app.request(\"/v1/page/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      monitors: [\n        { monitorId: 1, order: 1 },\n        { monitorId: 2, order: 2 },\n      ],\n    }),\n  });\n\n  const result = PageSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitors).toEqual([\n    { monitorId: 1, order: 1 },\n    { monitorId: 2, order: 2 },\n  ]);\n});\n\ntest(\"update the page with invalid monitors should return 400\", async () => {\n  const res = await app.request(\"/v1/page/1\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      monitors: [404],\n    }),\n  });\n  expect(res.status).toBe(400);\n});\n\ntest(\"invalid page id should return 404\", async () => {\n  const res = await app.request(\"/v1/page/404\", {\n    method: \"PUT\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      acknowledgedAt: new Date().toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/page/2\", {\n    method: \"PUT\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      acknowledgedAt: new Date().toISOString(),\n    }),\n  });\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/put.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { trackMiddleware } from \"@/libs/middlewares\";\nimport { notEmpty } from \"@/utils/not-empty\";\nimport { Events } from \"@openstatus/analytics\";\nimport { and, eq, inArray, isNull, sql } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db/src/db\";\nimport {\n  monitor,\n  page,\n  pageComponent,\n  subdomainSafeList,\n} from \"@openstatus/db/src/schema\";\nimport { isNumberArray } from \"../utils\";\nimport type { pagesApi } from \"./index\";\nimport { PageSchema, ParamsSchema, transformPageData } from \"./schema\";\n\nconst putRoute = createRoute({\n  method: \"put\",\n  tags: [\"page\"],\n  summary: \"Update a status page\",\n  path: \"/{id}\",\n  middleware: [trackMiddleware(Events.UpdatePage)],\n  request: {\n    params: ParamsSchema,\n    body: {\n      description: \"The monitor to update\",\n      content: {\n        \"application/json\": {\n          schema: PageSchema.omit({ id: true }).partial(),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: PageSchema,\n        },\n      },\n      description: \"Get an Status page\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPutPage(api: typeof pagesApi) {\n  return api.openapi(putRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n    const { id } = c.req.valid(\"param\");\n    const input = c.req.valid(\"json\");\n\n    if (input.customDomain && !limits[\"custom-domain\"]) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for custom domain\",\n      });\n    }\n\n    if (input.customDomain?.toLowerCase().includes(\"openstatus\")) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message: \"Domain cannot contain 'openstatus'\",\n      });\n    }\n\n    if (\n      limits[\"password-protection\"] === false &&\n      input?.passwordProtected === true\n    ) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for password protection\",\n      });\n    }\n\n    if (\n      limits[\"email-domain-protection\"] === false &&\n      (input?.accessType === \"email-domain\" || input?.authEmailDomains?.length)\n    ) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for email domain protection\",\n      });\n    }\n\n    if (\n      limits[\"password-protection\"] === false &&\n      (input?.accessType === \"password\" || input?.password)\n    ) {\n      throw new OpenStatusApiError({\n        code: \"PAYMENT_REQUIRED\",\n        message: \"Upgrade for password protection\",\n      });\n    }\n\n    const _page = await db\n      .select()\n      .from(page)\n      .where(and(eq(page.id, Number(id)), eq(page.workspaceId, workspaceId)))\n      .get();\n\n    if (!_page) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Page ${id} not found`,\n      });\n    }\n\n    if (input.slug && _page.slug !== input.slug) {\n      if (subdomainSafeList.includes(input.slug)) {\n        throw new OpenStatusApiError({\n          code: \"BAD_REQUEST\",\n          message: \"Slug is reserved\",\n        });\n      }\n\n      const countSlug = (\n        await db\n          .select({ count: sql<number>`count(*)` })\n          .from(page)\n          .where(eq(page.slug, input.slug))\n          .all()\n      )[0].count;\n\n      if (countSlug > 0) {\n        throw new OpenStatusApiError({\n          code: \"CONFLICT\",\n          message: \"Slug has to be unique and has already been taken\",\n        });\n      }\n    }\n\n    const { monitors, ...rest } = input;\n\n    const monitorIds = monitors\n      ? isNumberArray(monitors)\n        ? monitors\n        : monitors.map((m) => m.monitorId)\n      : [];\n\n    if (monitors?.length) {\n      const monitorsData = await db\n        .select()\n        .from(monitor)\n        .where(\n          and(\n            inArray(monitor.id, monitorIds),\n            eq(monitor.workspaceId, workspaceId),\n            isNull(monitor.deletedAt),\n          ),\n        )\n        .all();\n\n      if (monitorsData.length !== monitors.length) {\n        throw new OpenStatusApiError({\n          code: \"BAD_REQUEST\",\n          message: `Some of the monitors ${monitorIds.join(\", \")} not found`,\n        });\n      }\n    }\n\n    const newPage = await db\n      .update(page)\n      .set({\n        ...rest,\n        customDomain: input.customDomain ?? \"\",\n        accessType:\n          rest.accessType ?? (rest.passwordProtected ? \"password\" : \"public\"),\n        authEmailDomains: rest.authEmailDomains?.join(\",\"),\n        updatedAt: new Date(),\n      })\n      .where(eq(page.id, _page.id))\n      .returning()\n      .get();\n\n    const currentPageComponents = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.pageId, _page.id))\n      .all();\n\n    const currentMonitorIds = currentPageComponents\n      .filter((pc) => pc.type === \"monitor\" && pc.monitorId !== null)\n      .map((pc) => pc.monitorId as number);\n\n    const removedMonitorIds = currentMonitorIds.filter(\n      (id) => !monitorIds?.includes(id),\n    );\n\n    // Delete removed monitors from pageComponent\n    if (removedMonitorIds.length) {\n      await db\n        .delete(pageComponent)\n        .where(\n          and(\n            inArray(pageComponent.monitorId, removedMonitorIds),\n            eq(pageComponent.pageId, newPage.id),\n          ),\n        );\n    }\n\n    // Insert or update pageComponents (primary table)\n    if (monitors) {\n      for (const [index, m] of monitors.entries()) {\n        const values = typeof m === \"number\" ? { monitorId: m } : m;\n\n        const _monitor = await db.query.monitor.findFirst({\n          where: and(\n            eq(monitor.id, values.monitorId),\n            eq(monitor.workspaceId, workspaceId),\n            isNull(monitor.deletedAt),\n          ),\n        });\n\n        if (!_monitor) {\n          throw new OpenStatusApiError({\n            code: \"BAD_REQUEST\",\n            message: `Monitor ${values.monitorId} not found`,\n          });\n        }\n\n        // Insert or update pageComponent\n        await db\n          .insert(pageComponent)\n          .values({\n            workspaceId: newPage.workspaceId,\n            pageId: newPage.id,\n            type: \"monitor\",\n            monitorId: values.monitorId,\n            name: _monitor.externalName || _monitor.name,\n            order: \"order\" in values ? values.order : index,\n            groupId: null,\n            groupOrder: 0,\n          })\n          .onConflictDoUpdate({\n            target: [pageComponent.monitorId, pageComponent.pageId],\n            set: {\n              order: sql.raw(\"excluded.`order`\"),\n              name: sql.raw(\"excluded.`name`\"),\n            },\n          })\n          .run();\n      }\n    }\n\n    const data = transformPageData(\n      PageSchema.parse({\n        ...newPage,\n        monitors:\n          monitors ||\n          currentPageComponents.map((pc) => pc.monitorId).filter(notEmpty),\n      }),\n    );\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/pages/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nexport const ParamsSchema = z.object({\n  id: z\n    .string()\n    .min(1)\n    .openapi({\n      param: {\n        name: \"id\",\n        in: \"path\",\n      },\n      description: \"The id of the page\",\n      example: \"1\",\n    }),\n});\n\nexport const PageSchema = z\n  .object({\n    id: z.number().openapi({\n      description: \"The id of the page\",\n      example: 1,\n    }),\n    title: z.string().openapi({\n      description: \"The title of the page\",\n      example: \"My Page\",\n    }),\n    description: z.string().openapi({\n      description: \"The description of the page\",\n      example: \"My awesome status page\",\n    }),\n    slug: z.string().openapi({\n      description: \"The slug of the page\",\n      example: \"my-page\",\n    }),\n    // REMINDER: needs to be configured on Dashboard UI\n    customDomain: z\n      .string()\n      .transform((val) => (val ? val : undefined))\n      .nullish()\n      .openapi({\n        description:\n          \"The custom domain of the page. To be configured within the dashboard.\",\n        example: \"status.acme.com\",\n      }),\n    icon: z\n      .url()\n      .or(z.literal(\"\"))\n      .transform((val) => (val ? val : undefined))\n      .nullish()\n      .openapi({\n        description: \"The icon of the page\",\n        example: \"https://example.com/icon.png\",\n      }),\n    passwordProtected: z.boolean().optional().prefault(false).openapi({\n      description:\n        \"Deprecated in favor of `accessType`. Used to set the password protection type. Returns true if `accessType` is set to 'password' and false otherwise.\",\n      example: true,\n      deprecated: true,\n    }),\n    accessType: z\n      .enum([\"public\", \"password\", \"email-domain\"])\n      .default(\"public\")\n      .openapi({\n        description: \"The access type of the page\",\n        example: \"public\",\n      }),\n    password: z.string().optional().nullish().openapi({\n      description: \"Your password to protect the page from the public\",\n      example: \"hidden-password\",\n    }),\n    authEmailDomains: z\n      .preprocess((val) => {\n        let parsedDomains: Array<unknown> = [];\n        if (!val) return parsedDomains;\n        if (Array.isArray(val)) {\n          parsedDomains = val;\n        }\n        if (String(val).length > 0) {\n          parsedDomains = String(val).split(\",\");\n        }\n        return parsedDomains;\n      }, z.array(z.string()))\n      .optional()\n      .nullish()\n      .openapi({\n        description: \"The email domains of the page\",\n        example: [\"example.com\", \"example.org\"],\n      }),\n    showMonitorValues: z.boolean().optional().nullish().prefault(true).openapi({\n      description:\n        \"Displays the total and failed request numbers for each monitor. Deprecated and will be removed in the future in favor for `configuration` property.\",\n      example: true,\n      deprecated: true,\n    }),\n    monitors: z\n      .array(z.number())\n      .openapi({\n        description:\n          \"The monitors of the page as an array of ids. We recommend using the object format to include the order.\",\n        deprecated: true,\n        example: [1, 2],\n      })\n      .or(\n        z\n          .array(z.object({ monitorId: z.number(), order: z.number() }))\n          .openapi({\n            description: \"The monitor as object allowing to pass id and order\",\n            example: [\n              { monitorId: 1, order: 0 },\n              { monitorId: 2, order: 1 },\n            ],\n          }),\n      )\n      .optional(),\n  })\n  .openapi(\"Page\");\n\nexport type PageSchema = z.infer<typeof PageSchema>;\n\n/**\n * Transforms page data to ensure passwordProtected reflects accessType\n * This should be used when parsing page data for responses\n *\n * NOTE: cannot be used in `PageSchema` because `.omit` is not supported otherwise\n */\nexport function transformPageData<\n  T extends { accessType?: string; passwordProtected?: boolean },\n>(data: T): T & { passwordProtected: boolean } {\n  return {\n    ...data,\n    passwordProtected:\n      data.accessType === \"password\" ? true : data.passwordProtected ?? false,\n  };\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReportUpdates/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { StatusReportUpdateSchema } from \"./schema\";\n\ntest(\"return the status report update\", async () => {\n  const res = await app.request(\"/v1/status_report_update/2\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/status_report_update/2\");\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid status report id should return 404\", async () => {\n  const res = await app.request(\"/v1/status_report_update/2\", {\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReportUpdates/get.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { and, db, eq } from \"@openstatus/db\";\nimport { statusReport, statusReportUpdate } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport type { statusReportUpdatesApi } from \"./index\";\nimport { ParamsSchema, StatusReportUpdateSchema } from \"./schema\";\n\nconst getRoute = createRoute({\n  method: \"get\",\n  tags: [\"status_report_update\"],\n  summary: \"Get a status report update\",\n  path: \"/{id}\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: StatusReportUpdateSchema,\n        },\n      },\n      description: \"Get a status report update\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetStatusReportUpdate(\n  api: typeof statusReportUpdatesApi,\n) {\n  return api.openapi(getRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _statusReport = await db\n      .select()\n      .from(statusReportUpdate)\n      .innerJoin(\n        statusReport,\n        and(\n          eq(statusReport.id, statusReportUpdate.statusReportId),\n          eq(statusReport.workspaceId, workspaceId),\n        ),\n      )\n      .where(eq(statusReportUpdate.id, Number(id)))\n      .get();\n\n    if (!_statusReport) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Status Report Update ${id} not found`,\n      });\n    }\n\n    const data = StatusReportUpdateSchema.parse(\n      _statusReport.status_report_update,\n    );\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReportUpdates/index.ts",
    "content": "import { OpenAPIHono } from \"@hono/zod-openapi\";\n\nimport { handleZodError } from \"@/libs/errors\";\nimport type { Variables } from \"../index\";\nimport { registerGetStatusReportUpdate } from \"./get\";\nimport { registerPostStatusReportUpdate } from \"./post\";\n\nexport const statusReportUpdatesApi = new OpenAPIHono<{\n  Variables: Variables;\n}>({\n  defaultHook: handleZodError,\n});\n\nregisterGetStatusReportUpdate(statusReportUpdatesApi);\nregisterPostStatusReportUpdate(statusReportUpdatesApi);\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReportUpdates/post.test.ts",
    "content": "import { beforeEach, expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { StatusReportUpdateSchema } from \"./schema\";\n\n// biome-ignore lint/suspicious/noExplicitAny: test utility\nconst spies = (globalThis as any).__subscriptionSpies as {\n  dispatchStatusReportUpdate: {\n    mockClear: () => void;\n    mock: { calls: number[][] };\n  };\n};\n\nbeforeEach(() => {\n  spies.dispatchStatusReportUpdate.mockClear();\n});\n\ntest(\"create a valid status report update\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      date: new Date().toISOString(),\n      message: \"Message\",\n      statusReportId: 1,\n    }),\n  });\n\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create a status report update without valid payload should return 400\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      date: \"test\",\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"create status report update with identified status should return 200\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"identified\",\n      date: new Date().toISOString(),\n      message: \"We have identified the root cause\",\n      statusReportId: 1,\n    }),\n  });\n\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update with monitoring status should return 200\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"monitoring\",\n      date: new Date().toISOString(),\n      message: \"The fix has been deployed and we are monitoring\",\n      statusReportId: 1,\n    }),\n  });\n\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update with resolved status should return 200\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"resolved\",\n      date: new Date().toISOString(),\n      message: \"Issue has been fully resolved\",\n      statusReportId: 1,\n    }),\n  });\n\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update without date should use default\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      message: \"Update without explicit date\",\n      statusReportId: 1,\n    }),\n  });\n\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update with past date should return 200\", async () => {\n  const pastDate = new Date();\n  pastDate.setTime(pastDate.getTime() - 24 * 60 * 60 * 1000);\n\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      date: pastDate.toISOString(),\n      message: \"Update with past date\",\n      statusReportId: 1,\n    }),\n  });\n\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update with long message should return 200\", async () => {\n  const longMessage =\n    \"This is a very detailed status update message that provides comprehensive information about the incident, including what happened, what is being done to resolve it, and what measures are being taken to prevent similar issues in the future. We apologize for any inconvenience this may have caused.\";\n\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"monitoring\",\n      date: new Date().toISOString(),\n      message: longMessage,\n      statusReportId: 1,\n    }),\n  });\n\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update with different status report ID should return 200\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      date: new Date().toISOString(),\n      message: \"Update for different report\",\n      statusReportId: 2,\n    }),\n  });\n\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update with invalid status should return 400\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"invalid_status\",\n      date: new Date().toISOString(),\n      message: \"Test message\",\n      statusReportId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create status report update without message should return 400\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      date: new Date().toISOString(),\n      statusReportId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create status report update without statusReportId should return 400\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      date: new Date().toISOString(),\n      message: \"Test message\",\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create status report update with empty message should return 400\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      date: new Date().toISOString(),\n      message: \"\",\n      statusReportId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create status report update with non-existent statusReportId should return 404\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      date: new Date().toISOString(),\n      message: \"Update for non-existent report\",\n      statusReportId: 9999,\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"create a status report update calls dispatchStatusReportUpdate\", async () => {\n  const res = await app.request(\"/v1/status_report_update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      date: new Date().toISOString(),\n      message: \"Testing dispatcher integration\",\n      statusReportId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = StatusReportUpdateSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n  expect(spies.dispatchStatusReportUpdate.mock.calls.length).toBe(1);\n  expect(spies.dispatchStatusReportUpdate.mock.calls[0][0]).toBeNumber();\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReportUpdates/post.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { and, db, eq } from \"@openstatus/db\";\nimport { statusReport, statusReportUpdate } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { dispatchStatusReportUpdate } from \"@openstatus/subscriptions\";\nimport type { statusReportUpdatesApi } from \"./index\";\nimport { StatusReportUpdateSchema } from \"./schema\";\n\nconst createStatusUpdate = createRoute({\n  method: \"post\",\n  tags: [\"status_report_update\"],\n  summary: \"Create a status report update\",\n  path: \"/\",\n  request: {\n    body: {\n      description: \"The status report update to create\",\n      content: {\n        \"application/json\": {\n          schema: StatusReportUpdateSchema.omit({ id: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: StatusReportUpdateSchema,\n        },\n      },\n      description: \"The created status report update\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostStatusReportUpdate(\n  api: typeof statusReportUpdatesApi,\n) {\n  return api.openapi(createStatusUpdate, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const input = c.req.valid(\"json\");\n    const limits = c.get(\"workspace\").limits;\n\n    const _statusReport = await db.query.statusReport.findFirst({\n      where: and(\n        eq(statusReport.id, input.statusReportId),\n        eq(statusReport.workspaceId, workspaceId),\n      ),\n      with: {\n        statusReportsToPageComponents: {\n          with: {\n            pageComponent: true,\n          },\n        },\n      },\n    });\n\n    if (!_statusReport) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Status Report ${input.statusReportId} not found`,\n      });\n    }\n\n    const _statusReportUpdate = await db.transaction(async (tx) => {\n      const update = await tx\n        .insert(statusReportUpdate)\n        .values({\n          ...input,\n          date: new Date(input.date),\n          statusReportId: _statusReport.id,\n        })\n        .returning()\n        .get();\n\n      await tx\n        .update(statusReport)\n        .set({\n          status: input.status,\n          updatedAt: new Date(),\n        })\n        .where(eq(statusReport.id, _statusReport.id));\n\n      return update;\n    });\n\n    if (limits[\"status-subscribers\"] && _statusReport.pageId) {\n      await dispatchStatusReportUpdate(_statusReportUpdate.id);\n    }\n\n    const data = StatusReportUpdateSchema.parse(_statusReportUpdate);\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReportUpdates/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nimport { statusReportStatus } from \"@openstatus/db/src/schema\";\n\nexport const ParamsSchema = z.object({\n  id: z\n    .string()\n    .min(1)\n    .openapi({\n      param: {\n        name: \"id\",\n        in: \"path\",\n      },\n      description: \"The id of the update\",\n      example: \"1\",\n    }),\n});\n\nexport const StatusReportUpdateSchema = z\n  .object({\n    id: z.coerce.string().openapi({ description: \"The id of the update\" }),\n    status: z.enum(statusReportStatus).openapi({\n      description: \"The status of the update\",\n    }),\n    date: z.coerce.date().prefault(new Date()).openapi({\n      description: \"The date of the update in ISO8601 format\",\n    }),\n    message: z.string().min(1).openapi({\n      description: \"The message of the update\",\n    }),\n    statusReportId: z.number().openapi({\n      description: \"The id of the status report\",\n    }),\n  })\n  .openapi(\"StatusReportUpdate\");\n\nexport type StatusReportUpdateSchema = z.infer<typeof StatusReportUpdateSchema>;\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/delete.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { StatusReportSchema } from \"./schema\";\n\ntest(\"delete the status report\", async () => {\n  // Create a status report we will delete\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"New Status Report\",\n      message: \"Message\",\n      monitorIds: [1],\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  const result = StatusReportSchema.safeParse(await res.json());\n\n  const del = await app.request(`/v1/status_report/${result.data?.id}`, {\n    method: \"DELETE\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  expect(del.status).toBe(200);\n  expect(await del.json()).toMatchObject({});\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/status_report/2\", { method: \"DELETE\" });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid status report id should return 404\", async () => {\n  const res = await app.request(\"/v1/status_report/2\", {\n    method: \"DELETE\",\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/delete.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq } from \"@openstatus/db\";\nimport { statusReport } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport type { statusReportsApi } from \"./index\";\nimport { ParamsSchema } from \"./schema\";\n\nconst deleteRoute = createRoute({\n  method: \"delete\",\n  tags: [\"status_report\"],\n  summary: \"Delete a status report\",\n  path: \"/{id}\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: z.object({}),\n        },\n      },\n      description: \"Status report deleted\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerDeleteStatusReport(api: typeof statusReportsApi) {\n  return api.openapi(deleteRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _statusReport = await db\n      .select()\n      .from(statusReport)\n      .where(\n        and(\n          eq(statusReport.id, Number(id)),\n          eq(statusReport.workspaceId, workspaceId),\n        ),\n      )\n      .get();\n\n    if (!_statusReport) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Status Report ${id} not found`,\n      });\n    }\n\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, Number(id)))\n      .run();\n\n    return c.json({}, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { StatusReportSchema } from \"./schema\";\n\ntest(\"return the status report\", async () => {\n  const res = await app.request(\"/v1/status_report/2\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n  const result = StatusReportSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  // expect(result.data?.statusReportUpdateIds?.length).toBeGreaterThan(0);\n  // expect(result.data?.monitorIds?.length).toBe(0);\n});\n\ntest(\"return the status report with correct monitorIds structure\", async () => {\n  const res = await app.request(\"/v1/status_report/2\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n  const result = StatusReportSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitorIds).toBeDefined();\n  expect(Array.isArray(result.data?.monitorIds)).toBe(true);\n  // Ensure each monitorId is a number\n  for (const monitorId of result.data?.monitorIds || []) {\n    expect(typeof monitorId).toBe(\"number\");\n  }\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/status_report/2\");\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"invalid status report id should return 404\", async () => {\n  const res = await app.request(\"/v1/status_report/2\", {\n    headers: {\n      \"x-openstatus-key\": \"2\",\n    },\n  });\n\n  expect(res.status).toBe(404);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/get.ts",
    "content": "import { createRoute } from \"@hono/zod-openapi\";\n\nimport { and, db, eq } from \"@openstatus/db\";\nimport { statusReport } from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { notEmpty } from \"@/utils/not-empty\";\nimport type { statusReportsApi } from \"./index\";\nimport { ParamsSchema, StatusReportSchema } from \"./schema\";\n\nconst getRoute = createRoute({\n  method: \"get\",\n  tags: [\"status_report\"],\n  summary: \"Get a status report\",\n  path: \"/{id}\",\n  request: {\n    params: ParamsSchema,\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: StatusReportSchema,\n        },\n      },\n      description: \"Get all status reports\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function regsiterGetStatusReport(api: typeof statusReportsApi) {\n  return api.openapi(getRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const { id } = c.req.valid(\"param\");\n\n    const _statusUpdate = await db.query.statusReport.findFirst({\n      with: {\n        statusReportUpdates: true,\n        statusReportsToPageComponents: { with: { pageComponent: true } },\n      },\n      where: and(\n        eq(statusReport.workspaceId, workspaceId),\n        eq(statusReport.id, Number(id)),\n      ),\n    });\n\n    if (!_statusUpdate) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Status Report ${id} not found`,\n      });\n    }\n\n    const { statusReportUpdates, statusReportsToPageComponents } =\n      _statusUpdate;\n\n    // most recent report information\n    const { message, date } =\n      statusReportUpdates[statusReportUpdates.length - 1];\n\n    const data = StatusReportSchema.parse({\n      ..._statusUpdate,\n      message,\n      date,\n      monitorIds: statusReportsToPageComponents\n        .map((sr) => sr.pageComponent.monitorId)\n        .filter(notEmpty),\n\n      statusReportUpdateIds: statusReportUpdates.map((update) => update.id),\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/get_all.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  monitor,\n  pageComponent,\n  statusReport,\n  statusReportUpdate,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\n\nimport { app } from \"@/index\";\nimport { StatusReportSchema } from \"./schema\";\n\nconst TEST_PREFIX = \"v1-sr-getall-test\";\nlet testMonitorId: number;\nlet testPageComponentId: number;\nlet testStatusReportId: number;\n\nbeforeAll(async () => {\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.title, `${TEST_PREFIX}-report`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n\n  const mon = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 1,\n      name: `${TEST_PREFIX}-monitor`,\n      url: \"https://test.example.com\",\n      periodicity: \"1m\",\n      active: true,\n      regions: \"ams\",\n      jobType: \"http\",\n      method: \"GET\",\n      timeout: 30000,\n    })\n    .returning()\n    .get();\n  testMonitorId = mon.id;\n\n  const comp = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      monitorId: testMonitorId,\n      type: \"monitor\",\n      name: `${TEST_PREFIX}-component`,\n      order: 200,\n    })\n    .returning()\n    .get();\n  testPageComponentId = comp.id;\n\n  const report = await db\n    .insert(statusReport)\n    .values({\n      workspaceId: 1,\n      pageId: 1,\n      title: `${TEST_PREFIX}-report`,\n      status: \"investigating\",\n    })\n    .returning()\n    .get();\n  testStatusReportId = report.id;\n\n  await db.insert(statusReportUpdate).values({\n    statusReportId: testStatusReportId,\n    status: \"investigating\",\n    message: \"Test investigating\",\n    date: new Date(\"2099-01-01T00:00:00Z\"),\n  });\n\n  await db.insert(statusReportsToPageComponents).values({\n    statusReportId: testStatusReportId,\n    pageComponentId: testPageComponentId,\n  });\n});\n\nafterAll(async () => {\n  await db\n    .delete(statusReportsToPageComponents)\n    .where(\n      eq(statusReportsToPageComponents.statusReportId, testStatusReportId),\n    );\n  await db\n    .delete(statusReportUpdate)\n    .where(eq(statusReportUpdate.statusReportId, testStatusReportId));\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.title, `${TEST_PREFIX}-report`));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.name, `${TEST_PREFIX}-component`));\n  await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));\n});\n\ntest(\"return all status reports\", async () => {\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = StatusReportSchema.array().safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.some((r) => r.id === testStatusReportId)).toBe(true);\n});\n\ntest(\"return all status reports with monitorIds\", async () => {\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n\n  const result = StatusReportSchema.array().safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n\n  const testReport = result.data?.find((r) => r.id === testStatusReportId);\n  expect(testReport).toBeDefined();\n  expect(testReport?.monitorIds).toBeDefined();\n  expect(Array.isArray(testReport?.monitorIds)).toBe(true);\n  expect(testReport?.monitorIds).toContain(testMonitorId);\n});\n\ntest(\"return empty status reports\", async () => {\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"GET\",\n    headers: {\n      \"x-openstatus-key\": \"3\",\n    },\n  });\n\n  const result = StatusReportSchema.array().safeParse(await res.json());\n\n  expect(result.success).toBe(true);\n  expect(res.status).toBe(200);\n  expect(result.data?.length).toBe(0);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"GET\",\n  });\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/get_all.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { statusReport } from \"@openstatus/db/src/schema\";\n\nimport { openApiErrorResponses } from \"@/libs/errors\";\nimport { notEmpty } from \"@/utils/not-empty\";\nimport type { statusReportsApi } from \"./index\";\nimport { StatusReportSchema } from \"./schema\";\n\nconst getAllRoute = createRoute({\n  method: \"get\",\n  tags: [\"status_report\"],\n  summary: \"List all status reports\",\n  path: \"/\",\n  request: {},\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: z.array(StatusReportSchema),\n        },\n      },\n      description: \"Get all status reports\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetAllStatusReports(api: typeof statusReportsApi) {\n  return api.openapi(getAllRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n\n    const _statusReports = await db.query.statusReport.findMany({\n      with: {\n        statusReportUpdates: true,\n        statusReportsToPageComponents: { with: { pageComponent: true } },\n      },\n      where: eq(statusReport.workspaceId, workspaceId),\n    });\n\n    const data = z.array(StatusReportSchema).parse(\n      _statusReports.map((r) => ({\n        ...r,\n        statusReportUpdateIds: r.statusReportUpdates.map((u) => u.id),\n        monitorIds: r.statusReportsToPageComponents\n          .map((sr) => sr.pageComponent.monitorId)\n          .filter(notEmpty),\n      })),\n    );\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/index.ts",
    "content": "import { OpenAPIHono } from \"@hono/zod-openapi\";\n\nimport { handleZodError } from \"@/libs/errors\";\nimport type { Variables } from \"../index\";\nimport { registerDeleteStatusReport } from \"./delete\";\nimport { regsiterGetStatusReport } from \"./get\";\nimport { registerGetAllStatusReports } from \"./get_all\";\nimport { registerPostStatusReport } from \"./post\";\nimport { registerStatusReportUpdateRoutes } from \"./update/post\";\n\nexport const statusReportsApi = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\nregisterGetAllStatusReports(statusReportsApi);\nregisterDeleteStatusReport(statusReportsApi);\nregsiterGetStatusReport(statusReportsApi);\nregisterPostStatusReport(statusReportsApi);\n\n/**\n * @deprecated in favor of `/status_report_updates`\n */\nregisterStatusReportUpdateRoutes(statusReportsApi);\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/post.test.ts",
    "content": "import { beforeEach, expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  pageComponent,\n  statusReport,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\nimport { StatusReportSchema } from \"./schema\";\n\n// biome-ignore lint/suspicious/noExplicitAny: test utility\nconst spies = (globalThis as any).__subscriptionSpies as {\n  dispatchStatusReportUpdate: {\n    mockClear: () => void;\n    mock: { calls: number[][] };\n  };\n};\n\nbeforeEach(() => {\n  spies.dispatchStatusReportUpdate.mockClear();\n});\n\ntest(\"create a valid status report\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"New Status Report\",\n      message: \"Message\",\n      monitorIds: [1],\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  const result = StatusReportSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.statusReportUpdateIds?.length).toBeGreaterThan(0);\n  expect(result.data?.monitorIds?.length).toBe(1);\n  expect(result.data?.monitorIds).toEqual([1]);\n});\n\ntest(\"create a status report with multiple monitorIds\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"Multi-Monitor Status Report\",\n      message: \"Affecting multiple monitors\",\n      monitorIds: [1, 2],\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  const result = StatusReportSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitorIds?.length).toBe(2);\n  expect(result.data?.monitorIds).toEqual(expect.arrayContaining([1, 2]));\n});\n\ntest(\"create a status report without monitorIds\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"General Status Report\",\n      message: \"No specific monitors affected\",\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  const result = StatusReportSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n  expect(result.data?.monitorIds).toBeDefined();\n  expect(Array.isArray(result.data?.monitorIds)).toBe(true);\n});\n\ntest(\"create a status report with partial invalid monitorIds should return 400\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"Partial Invalid Monitors\",\n      message: \"One valid, one invalid\",\n      monitorIds: [1, 9999],\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create a status report with invalid monitor should return 400\", async () => {\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"New Status Report\",\n      message: \"Message\",\n      monitorIds: [404],\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create a status report with invalid page id should return 400\", async () => {\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"New Status Report\",\n      message: \"Message\",\n      monitorIds: [1],\n      pageId: 404,\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"create a status report calls dispatchStatusReportUpdate\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"Dispatch Test Report\",\n      message: \"Testing dispatcher integration\",\n      monitorIds: [1],\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = StatusReportSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n  expect(spies.dispatchStatusReportUpdate.mock.calls.length).toBe(1);\n  expect(spies.dispatchStatusReportUpdate.mock.calls[0][0]).toBeNumber();\n\n  if (result.success) {\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, result.data.id))\n      .run();\n  }\n});\n\ntest(\"create a status report links correctly to statusReportsToPageComponents\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"Sync Test Status Report\",\n      message: \"Testing link to statusReportsToPageComponents\",\n      monitorIds: [1],\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = StatusReportSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  if (result.success) {\n    const statusReportId = result.data.id;\n\n    // Verify statusReportsToPageComponents\n    const components = await db\n      .select()\n      .from(statusReportsToPageComponents)\n      .where(eq(statusReportsToPageComponents.statusReportId, statusReportId))\n      .all();\n\n    expect(components.length).toBeGreaterThan(0);\n\n    // Get the page component to verify it's linked correctly\n    const pageComponents = await db\n      .select()\n      .from(pageComponent)\n      .where(eq(pageComponent.id, components[0].pageComponentId))\n      .all();\n\n    expect(pageComponents.length).toBe(1);\n    expect(pageComponents[0].monitorId).toBe(1);\n    expect(pageComponents[0].pageId).toBe(1);\n    expect(pageComponents[0].type).toBe(\"monitor\");\n\n    // Cleanup\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, statusReportId))\n      .run();\n  }\n});\n\ntest(\"create a status report with multiple monitors links correctly to statusReportsToPageComponents\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  // First, check which monitors from [1, 2] exist as page components on page 1\n  const existingPageComponents = await db\n    .select()\n    .from(pageComponent)\n    .where(eq(pageComponent.pageId, 1))\n    .all();\n\n  const existingMonitorIds = existingPageComponents\n    .filter(\n      (c) =>\n        c.monitorId !== null &&\n        c.type === \"monitor\" &&\n        [1, 2].includes(c.monitorId),\n    )\n    .map((c) => c.monitorId as number);\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"Multi-Monitor Test\",\n      message: \"Testing with multiple monitors\",\n      monitorIds: [1, 2],\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = StatusReportSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  if (result.success) {\n    const statusReportId = result.data.id;\n\n    // Verify statusReportsToPageComponents\n    const components = await db\n      .select()\n      .from(statusReportsToPageComponents)\n      .where(eq(statusReportsToPageComponents.statusReportId, statusReportId))\n      .all();\n\n    // Should only link monitors that exist as page components on this page\n    expect(components.length).toBe(existingMonitorIds.length);\n\n    // Cleanup\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, statusReportId))\n      .run();\n  }\n});\n\ntest(\"create a status report without monitorIds should not create page component links\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"No Monitors Status Report\",\n      message: \"No specific monitors affected\",\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = StatusReportSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  if (result.success) {\n    const statusReportId = result.data.id;\n\n    // Verify no statusReportsToPageComponents entries\n    const components = await db\n      .select()\n      .from(statusReportsToPageComponents)\n      .where(eq(statusReportsToPageComponents.statusReportId, statusReportId))\n      .all();\n\n    expect(components.length).toBe(0);\n\n    // Cleanup\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, statusReportId))\n      .run();\n  }\n});\n\ntest(\"create a status report only links monitors that exist as page components\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  // First, check which monitors exist as page components on page 1\n  const existingComponents = await db\n    .select()\n    .from(pageComponent)\n    .where(eq(pageComponent.pageId, 1))\n    .all();\n\n  const existingMonitorIds = existingComponents\n    .filter((c) => c.monitorId !== null && c.type === \"monitor\")\n    .map((c) => c.monitorId as number);\n\n  if (existingMonitorIds.length === 0) {\n    // Skip test if no monitors exist on the page\n    return;\n  }\n\n  const res = await app.request(\"/v1/status_report\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"investigating\",\n      title: \"Page Component Link Test\",\n      message: \"Testing page component linking\",\n      monitorIds: existingMonitorIds,\n      date: date.toISOString(),\n      pageId: 1,\n    }),\n  });\n\n  expect(res.status).toBe(200);\n  const result = StatusReportSchema.safeParse(await res.json());\n  expect(result.success).toBe(true);\n\n  if (result.success) {\n    const statusReportId = result.data.id;\n\n    // Verify statusReportsToPageComponents entries match existing components\n    const components = await db\n      .select()\n      .from(statusReportsToPageComponents)\n      .where(eq(statusReportsToPageComponents.statusReportId, statusReportId))\n      .all();\n\n    // Each linked component should correspond to a page component\n    for (const component of components) {\n      const pageComp = await db\n        .select()\n        .from(pageComponent)\n        .where(eq(pageComponent.id, component.pageComponentId))\n        .get();\n\n      expect(pageComp).toBeDefined();\n      expect(pageComp?.pageId).toBe(1);\n      expect(existingMonitorIds).toContain(pageComp?.monitorId as number);\n    }\n\n    // Cleanup\n    await db\n      .delete(statusReport)\n      .where(eq(statusReport.id, statusReportId))\n      .run();\n  }\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/post.ts",
    "content": "import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport { and, db, eq, inArray, isNull } from \"@openstatus/db\";\nimport {\n  monitor,\n  page,\n  pageComponent,\n  statusReport,\n  statusReportUpdate,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\n\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { dispatchStatusReportUpdate } from \"@openstatus/subscriptions\";\nimport type { statusReportsApi } from \"./index\";\nimport { StatusReportSchema } from \"./schema\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"status_report\"],\n  summary: \"Create a status report\",\n  path: \"/\",\n  request: {\n    body: {\n      description: \"The status report to create\",\n      content: {\n        \"application/json\": {\n          schema: StatusReportSchema.omit({\n            id: true,\n            statusReportUpdateIds: true,\n          }).extend({\n            date: z.coerce.date().optional().prefault(new Date()).openapi({\n              description:\n                \"The date of the report in ISO8601 format, defaults to now\",\n            }),\n            message: z.string().openapi({\n              description: \"The message of the current status of incident\",\n            }),\n          }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: StatusReportSchema,\n        },\n      },\n      description: \"The created status report\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostStatusReport(api: typeof statusReportsApi) {\n  return api.openapi(postRoute, async (c) => {\n    const input = c.req.valid(\"json\");\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n\n    if (input.monitorIds?.length) {\n      const _monitors = await db\n        .select()\n        .from(monitor)\n        .where(\n          and(\n            eq(monitor.workspaceId, workspaceId),\n            inArray(monitor.id, input.monitorIds),\n            isNull(monitor.deletedAt),\n          ),\n        )\n        .all();\n\n      if (_monitors.length !== input.monitorIds.length) {\n        throw new OpenStatusApiError({\n          code: \"BAD_REQUEST\",\n          message: `Some of the monitors ${input.monitorIds.join(\n            \", \",\n          )} not found`,\n        });\n      }\n    }\n\n    const _pages = await db\n      .select()\n      .from(page)\n      .where(and(eq(page.workspaceId, workspaceId), eq(page.id, input.pageId)))\n      .all();\n\n    if (_pages.length !== 1) {\n      throw new OpenStatusApiError({\n        code: \"BAD_REQUEST\",\n        message: `Page ${input.pageId} not found`,\n      });\n    }\n\n    const { _newStatusReport, _newStatusReportUpdate } = await db.transaction(\n      async (tx) => {\n        const _newStatusReport = await tx\n          .insert(statusReport)\n          .values({\n            status: input.status,\n            title: input.title,\n            pageId: input.pageId,\n            workspaceId: workspaceId,\n          })\n          .returning()\n          .get();\n\n        const _newStatusReportUpdate = await tx\n          .insert(statusReportUpdate)\n          .values({\n            status: input.status,\n            message: input.message,\n            date: input.date,\n            statusReportId: _newStatusReport.id,\n          })\n          .returning()\n          .get();\n\n        if (!_newStatusReport.pageId) {\n          throw new OpenStatusApiError({\n            code: \"BAD_REQUEST\",\n            message: \"Page ID is required\",\n          });\n        }\n\n        if (input.monitorIds?.length) {\n          // Find matching page_components for the monitors on this page\n          const components = await tx\n            .select({ id: pageComponent.id })\n            .from(pageComponent)\n            .where(\n              and(\n                inArray(pageComponent.monitorId, input.monitorIds),\n                eq(pageComponent.pageId, _newStatusReport.pageId),\n                eq(pageComponent.type, \"monitor\"),\n              ),\n            )\n            .all();\n\n          // Insert to statusReportsToPageComponents\n          if (components.length > 0) {\n            await tx\n              .insert(statusReportsToPageComponents)\n              .values(\n                components.map((c) => ({\n                  statusReportId: _newStatusReport.id,\n                  pageComponentId: c.id,\n                })),\n              )\n              .run();\n          }\n        }\n\n        return { _newStatusReport, _newStatusReportUpdate };\n      },\n    );\n\n    if (limits[\"status-subscribers\"] && _newStatusReport.pageId) {\n      await dispatchStatusReportUpdate(_newStatusReportUpdate.id);\n    }\n\n    const data = StatusReportSchema.parse({\n      ..._newStatusReport,\n      monitorIds: input.monitorIds,\n      statusReportUpdateIds: [_newStatusReportUpdate.id],\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nimport { statusReportStatus } from \"@openstatus/db/src/schema/status_reports/status_reports\";\n\nexport const ParamsSchema = z.object({\n  id: z\n    .string()\n    .min(1)\n    .openapi({\n      param: {\n        name: \"id\",\n        in: \"path\",\n      },\n      description: \"The id of the status report\",\n      example: \"1\",\n    }),\n});\n\nexport const StatusReportSchema = z\n  .object({\n    id: z.number().openapi({ description: \"The id of the status report\" }),\n    title: z.string().openapi({\n      example: \"Documenso\",\n      description: \"The title of the status report\",\n    }),\n    status: z.enum(statusReportStatus).openapi({\n      description: \"The current status of the report\",\n    }),\n    statusReportUpdateIds: z\n      .array(z.number())\n      .optional()\n      .nullable()\n      .prefault([])\n      .openapi({\n        description: \"The ids of the status report updates\",\n      }),\n    monitorIds: z\n      .array(z.number())\n      .optional()\n      .prefault([])\n      .openapi({ description: \"Ids of the monitors the status report.\" }),\n    pageId: z.number().openapi({\n      description: \"The id of the page this status report belongs to\",\n    }),\n  })\n  .openapi(\"StatusReport\");\n\nexport type StatusReportSchema = z.infer<typeof StatusReportSchema>;\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/subscriber-filtering.integration.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\nimport { and, db, eq, isNotNull, isNull } from \"@openstatus/db\";\nimport { page, pageSubscriber } from \"@openstatus/db/src/schema\";\n\n/**\n * Integration tests for subscriber filtering in status report email queries.\n * These tests verify that unsubscribed users are excluded from email notifications.\n */\n\nlet testPageId: number;\nconst testWorkspaceId = 1; // Use existing test workspace from seed data\n\nbeforeAll(async () => {\n  // Clean up any existing test data\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"active-sub@test.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"unsubscribed-sub@test.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"pending-sub@test.com\"));\n  await db.delete(page).where(eq(page.slug, \"test-filtering-page\"));\n\n  // Create a test page\n  const testPage = await db\n    .insert(page)\n    .values({\n      workspaceId: testWorkspaceId,\n      title: \"Test Filtering Page\",\n      description: \"A test page for subscriber filtering tests\",\n      slug: \"test-filtering-page\",\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n\n  testPageId = testPage.id;\n\n  // Create test subscribers with different states\n  // 1. Active subscriber (verified, not unsubscribed)\n  await db.insert(pageSubscriber).values({\n    pageId: testPageId,\n    email: \"active-sub@test.com\",\n    token: crypto.randomUUID(),\n    acceptedAt: new Date(),\n    unsubscribedAt: null,\n    expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n  });\n\n  // 2. Unsubscribed subscriber (verified, then unsubscribed)\n  await db.insert(pageSubscriber).values({\n    pageId: testPageId,\n    email: \"unsubscribed-sub@test.com\",\n    token: crypto.randomUUID(),\n    acceptedAt: new Date(),\n    unsubscribedAt: new Date(),\n    expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n  });\n\n  // 3. Pending subscriber (not verified)\n  await db.insert(pageSubscriber).values({\n    pageId: testPageId,\n    email: \"pending-sub@test.com\",\n    token: crypto.randomUUID(),\n    acceptedAt: null,\n    unsubscribedAt: null,\n    expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n  });\n});\n\nafterAll(async () => {\n  // Clean up test data\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"active-sub@test.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"unsubscribed-sub@test.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"pending-sub@test.com\"));\n  await db.delete(page).where(eq(page.slug, \"test-filtering-page\"));\n});\n\ndescribe(\"Subscriber filtering for email notifications\", () => {\n  test(\"should exclude unsubscribed users from email queries\", async () => {\n    // This query mirrors the exact query used in statusReports/post.ts and statusReportUpdates/post.ts\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    // Should only include active subscriber, not unsubscribed or pending\n    expect(subscribers.length).toBe(1);\n    expect(subscribers[0].email).toBe(\"active-sub@test.com\");\n  });\n\n  test(\"should exclude pending (unverified) users from email queries\", async () => {\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    const pendingEmails = subscribers.filter(\n      (s) => s.email === \"pending-sub@test.com\",\n    );\n    expect(pendingEmails.length).toBe(0);\n  });\n\n  test(\"should not include unsubscribed user even if acceptedAt is set\", async () => {\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    const unsubscribedEmails = subscribers.filter(\n      (s) => s.email === \"unsubscribed-sub@test.com\",\n    );\n    expect(unsubscribedEmails.length).toBe(0);\n  });\n\n  test(\"should return all subscribers without unsubscribedAt filter\", async () => {\n    // Query without the unsubscribedAt filter - should include unsubscribed users\n    const allVerifiedSubscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n        ),\n      )\n      .all();\n\n    // Should include both active and unsubscribed (both have acceptedAt set)\n    expect(allVerifiedSubscribers.length).toBe(2);\n\n    const emails = allVerifiedSubscribers.map((s) => s.email);\n    expect(emails).toContain(\"active-sub@test.com\");\n    expect(emails).toContain(\"unsubscribed-sub@test.com\");\n  });\n\n  test(\"should correctly filter subscribers with valid tokens\", async () => {\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    // Filter for valid tokens (non-null)\n    const validSubscribers = subscribers.filter(\n      (s): s is typeof s & { token: string } => s.token !== null,\n    );\n\n    expect(validSubscribers.length).toBe(1);\n    expect(validSubscribers[0].token).toBeDefined();\n    expect(validSubscribers[0].token).toMatch(\n      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,\n    );\n  });\n});\n\ndescribe(\"Subscriber state transitions\", () => {\n  test(\"should allow re-subscribing after unsubscription\", async () => {\n    // Get the unsubscribed subscriber\n    const unsubscribedSub = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.email, \"unsubscribed-sub@test.com\"),\n    });\n\n    if (!unsubscribedSub) {\n      throw new Error(\"Unsubscribed subscriber not found\");\n    }\n\n    expect(unsubscribedSub?.unsubscribedAt).not.toBeNull();\n\n    // Simulate re-subscription by clearing unsubscribedAt\n    await db\n      .update(pageSubscriber)\n      .set({\n        unsubscribedAt: null,\n        acceptedAt: null, // Reset for re-verification\n        token: crypto.randomUUID(), // Generate new token\n      })\n      .where(eq(pageSubscriber.id, unsubscribedSub?.id));\n\n    // After re-subscription + verification, subscriber should be included\n    // (we need to set acceptedAt for verification)\n    await db\n      .update(pageSubscriber)\n      .set({ acceptedAt: new Date() })\n      .where(eq(pageSubscriber.id, unsubscribedSub?.id));\n\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    // Now should include both active and re-subscribed users\n    expect(subscribers.length).toBe(2);\n\n    // Restore original state for other tests\n    await db\n      .update(pageSubscriber)\n      .set({ unsubscribedAt: new Date() })\n      .where(eq(pageSubscriber.id, unsubscribedSub?.id));\n  });\n\n  test(\"should track unsubscription timestamp\", async () => {\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.email, \"unsubscribed-sub@test.com\"),\n    });\n\n    expect(subscriber?.unsubscribedAt).toBeInstanceOf(Date);\n  });\n});\n\ndescribe(\"Query performance considerations\", () => {\n  test(\"should use proper index-friendly query conditions\", async () => {\n    // This test verifies the query uses conditions that can leverage indexes\n    // The conditions: pageId = X AND acceptedAt IS NOT NULL AND unsubscribedAt IS NULL\n    // can all be optimized with appropriate indexes\n\n    const startTime = performance.now();\n\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    const endTime = performance.now();\n\n    // Query should complete quickly (under 100ms for small datasets)\n    expect(endTime - startTime).toBeLessThan(100);\n    expect(subscribers).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/update/post.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { StatusReportSchema } from \"../schema\";\n\ntest(\"create status report update with valid data should return 200\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"monitoring\",\n      message: \"The issue has been resolved and we are monitoring\",\n      date: date.toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = StatusReportSchema.safeParse(json);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update with different status should return 200\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"identified\",\n      message: \"We have identified the issue\",\n      date: date.toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = StatusReportSchema.safeParse(json);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update with invalid status report id should return 404\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report/999999/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"monitoring\",\n      message: \"The issue has been resolved and we are monitoring\",\n      date: date.toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"create status report update with invalid status should return 400\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"invalid_status\",\n      message: \"Test message\",\n      date: date.toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create status report update without auth key should return 401\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"monitoring\",\n      message: \"Test message\",\n      date: date.toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(401);\n});\n\ntest(\"create status report update from different workspace should return 404\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"2\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"monitoring\",\n      message: \"Test message\",\n      date: date.toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(404);\n});\n\ntest(\"create status report update without message should return 400\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"monitoring\",\n      date: date.toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(400);\n});\n\ntest(\"create status report update with resolved status should return 200\", async () => {\n  const date = new Date();\n  date.setMilliseconds(0);\n\n  const res = await app.request(\"/v1/status_report/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"resolved\",\n      message: \"Issue has been fully resolved\",\n      date: date.toISOString(),\n    }),\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = StatusReportSchema.safeParse(json);\n  expect(result.success).toBe(true);\n});\n\ntest(\"create status report update without date should use default\", async () => {\n  const res = await app.request(\"/v1/status_report/1/update\", {\n    method: \"POST\",\n    headers: {\n      \"x-openstatus-key\": \"1\",\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      status: \"monitoring\",\n      message: \"Test message without explicit date\",\n    }),\n  });\n\n  expect(res.status).toBe(200);\n\n  const json = await res.json();\n  const result = StatusReportSchema.safeParse(json);\n  expect(result.success).toBe(true);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/statusReports/update/post.ts",
    "content": "import { env } from \"@/env\";\nimport { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { notEmpty } from \"@/utils/not-empty\";\nimport { createRoute } from \"@hono/zod-openapi\";\nimport { and, db, eq, isNotNull } from \"@openstatus/db\";\nimport {\n  pageSubscriber,\n  statusReport,\n  statusReportUpdate,\n} from \"@openstatus/db/src/schema\";\nimport { EmailClient } from \"@openstatus/emails/src/client\";\nimport { StatusReportUpdateSchema } from \"../../statusReportUpdates/schema\";\nimport type { statusReportsApi } from \"../index\";\nimport { ParamsSchema, StatusReportSchema } from \"../schema\";\n\nconst emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY });\n\nconst postRouteUpdate = createRoute({\n  method: \"post\",\n  tags: [\"status_report\"],\n  path: \"/{id}/update\",\n  summary: \"Create a status report update\",\n  deprecated: true,\n  description:\n    \"Preferably use [`/status-report-updates`](#tag/status_report_update/POST/status_report_update) instead.\",\n  request: {\n    params: ParamsSchema,\n    body: {\n      description: \"the status report update\",\n      content: {\n        \"application/json\": {\n          schema: StatusReportUpdateSchema.omit({\n            id: true,\n            statusReportId: true,\n          }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: StatusReportSchema,\n        },\n      },\n      description: \"Status report updated\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerStatusReportUpdateRoutes(api: typeof statusReportsApi) {\n  return api.openapi(postRouteUpdate, async (c) => {\n    const input = c.req.valid(\"json\");\n    const { id } = c.req.valid(\"param\");\n    const workspaceId = c.get(\"workspace\").id;\n    const limits = c.get(\"workspace\").limits;\n\n    const _statusReport = await db\n      .update(statusReport)\n      .set({ status: input.status, updatedAt: new Date() })\n      .where(\n        and(\n          eq(statusReport.id, Number(id)),\n          eq(statusReport.workspaceId, workspaceId),\n        ),\n      )\n      .returning()\n      .get();\n\n    if (!_statusReport) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Status Report ${id} not found`,\n      });\n    }\n\n    const _statusReportUpdate = await db\n      .insert(statusReportUpdate)\n      .values({\n        status: input.status,\n        message: input.message,\n        date: input.date,\n        statusReportId: Number(id),\n      })\n      .returning()\n      .get();\n\n    if (limits[\"status-subscribers\"] && _statusReport.pageId) {\n      const _statusReportWithRelations = await db.query.statusReport.findFirst({\n        where: eq(statusReport.id, Number(id)),\n        with: {\n          statusReportsToPageComponents: {\n            with: {\n              pageComponent: true,\n            },\n          },\n          page: true,\n        },\n      });\n\n      const subscribers = await db\n        .select()\n        .from(pageSubscriber)\n        .where(\n          and(\n            eq(pageSubscriber.pageId, _statusReport.pageId),\n            isNotNull(pageSubscriber.acceptedAt),\n          ),\n        )\n        .all();\n\n      const validSubscribers = subscribers.filter(\n        (s): s is typeof s & { token: string } =>\n          s.token !== null &&\n          s.acceptedAt !== null &&\n          s.unsubscribedAt === null,\n      );\n      if (_statusReportWithRelations?.page && validSubscribers.length > 0) {\n        await emailClient.sendStatusReportUpdate({\n          subscribers: validSubscribers.map((subscriber) => ({\n            email: subscriber.email,\n            token: subscriber.token,\n          })),\n          pageTitle: _statusReportWithRelations.page.title,\n          pageSlug: _statusReportWithRelations.page.slug,\n          customDomain: _statusReportWithRelations.page.customDomain,\n          reportTitle: _statusReportWithRelations.title,\n          status: _statusReportUpdate.status,\n          message: _statusReportUpdate.message,\n          date: _statusReportUpdate.date.toISOString(),\n          pageComponents:\n            _statusReportWithRelations.statusReportsToPageComponents.map(\n              (i) => i.pageComponent.name,\n            ),\n        });\n      }\n    }\n\n    // Query the full status report with all its relationships\n    const fullStatusReport = await db.query.statusReport.findFirst({\n      where: eq(statusReport.id, Number(id)),\n      with: {\n        statusReportUpdates: true,\n        statusReportsToPageComponents: {\n          with: {\n            pageComponent: true,\n          },\n        },\n      },\n    });\n\n    if (!fullStatusReport) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Status Report ${id} not found`,\n      });\n    }\n\n    const data = StatusReportSchema.parse({\n      ...fullStatusReport,\n      statusReportUpdateIds: fullStatusReport.statusReportUpdates.map(\n        (u) => u.id,\n      ),\n      monitorIds: fullStatusReport.statusReportsToPageComponents\n        .map((m) => m.pageComponent.monitorId)\n        .filter(notEmpty),\n    });\n\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/utils.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\nimport { ZodError } from \"zod\";\n\nexport const isoDate = z.preprocess((val) => {\n  try {\n    if (val) {\n      return new Date(String(val)).toISOString();\n    }\n    return new Date().toISOString();\n  } catch (_e) {\n    throw new ZodError([\n      {\n        code: \"invalid_type\",\n        message: \"Invalid date\",\n        expected: \"string\",\n        path: [],\n      },\n    ]);\n  }\n}, z.string());\n\nexport function isNumberArray<T>(\n  monitors: number[] | T[],\n): monitors is number[] {\n  return (\n    Array.isArray(monitors) &&\n    monitors.every((item) => typeof item === \"number\")\n  );\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/whoami/get.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { app } from \"@/index\";\nimport { WorkspaceSchema } from \"./schema\";\n\ntest(\"return the whoami\", async () => {\n  const res = await app.request(\"/v1/whoami\", {\n    headers: {\n      \"x-openstatus-key\": \"1\",\n    },\n  });\n  const result = WorkspaceSchema.safeParse(await res.json());\n\n  expect(res.status).toBe(200);\n  expect(result.success).toBe(true);\n});\n\ntest(\"no auth key should return 401\", async () => {\n  const res = await app.request(\"/v1/whoami\");\n\n  expect(res.status).toBe(401);\n});\n"
  },
  {
    "path": "apps/server/src/routes/v1/whoami/get.ts",
    "content": "import { OpenStatusApiError, openApiErrorResponses } from \"@/libs/errors\";\nimport { createRoute } from \"@hono/zod-openapi\";\nimport { eq } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db/src/db\";\nimport { workspace } from \"@openstatus/db/src/schema/workspaces\";\nimport type { whoamiApi } from \".\";\nimport { WorkspaceSchema } from \"./schema\";\n\nconst getRoute = createRoute({\n  method: \"get\",\n  tags: [\"whoami\"],\n  path: \"/\",\n  summary: \"Get your informations\",\n  description: \"Get the current workspace information attached to the API key.\",\n  responses: {\n    200: {\n      content: {\n        \"application/json\": {\n          schema: WorkspaceSchema,\n        },\n      },\n      description: \"The current workspace information with the limits\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerGetWhoami(api: typeof whoamiApi) {\n  return api.openapi(getRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n\n    const _workspace = await db\n      .select()\n      .from(workspace)\n      .where(eq(workspace.id, workspaceId))\n      .get();\n\n    if (!_workspace) {\n      throw new OpenStatusApiError({\n        code: \"NOT_FOUND\",\n        message: `Workspace ${workspaceId} not found`,\n      });\n    }\n\n    const data = WorkspaceSchema.parse(_workspace);\n    return c.json(data, 200);\n  });\n}\n"
  },
  {
    "path": "apps/server/src/routes/v1/whoami/index.ts",
    "content": "import { handleZodError } from \"@/libs/errors\";\nimport { OpenAPIHono } from \"@hono/zod-openapi\";\nimport type { Variables } from \"..\";\nimport { registerGetWhoami } from \"./get\";\n\nexport const whoamiApi = new OpenAPIHono<{ Variables: Variables }>({\n  defaultHook: handleZodError,\n});\n\nregisterGetWhoami(whoamiApi);\n"
  },
  {
    "path": "apps/server/src/routes/v1/whoami/schema.ts",
    "content": "import { z } from \"@hono/zod-openapi\";\n\nimport { workspacePlans } from \"@openstatus/db/src/schema/workspaces/constants\";\n\nexport const WorkspaceSchema = z\n  .object({\n    name: z\n      .string()\n      .optional()\n      .openapi({ description: \"The current workspace name\" }),\n    slug: z.string().openapi({ description: \"The current workspace slug\" }),\n    plan: z.enum(workspacePlans).nullable().prefault(\"free\").openapi({\n      description: \"The current workspace plan\",\n    }),\n  })\n  .openapi(\"Workspace\");\n\nexport type WorkspaceSchema = z.infer<typeof WorkspaceSchema>;\n"
  },
  {
    "path": "apps/server/src/types/index.ts",
    "content": "import type { Workspace } from \"@openstatus/db/src/schema\";\nimport type { RequestIdVariables } from \"hono/request-id\";\n\nexport type Variables = RequestIdVariables & {\n  workspace: Workspace;\n  event: Record<string, unknown>;\n};\n"
  },
  {
    "path": "apps/server/src/utils/audit-log.ts",
    "content": "import { AuditLog, Tinybird } from \"@openstatus/tinybird\";\n\nimport { env } from \"../env\";\n\nconst tb = new Tinybird({ token: env.TINY_BIRD_API_KEY });\n\nexport const checkerAudit = new AuditLog({ tb });\n"
  },
  {
    "path": "apps/server/src/utils/not-empty.ts",
    "content": "export function notEmpty<TValue>(\n  value: TValue | null | undefined,\n): value is TValue {\n  return value !== null && value !== undefined;\n}\n"
  },
  {
    "path": "apps/server/src/utils/page-component.ts",
    "content": "import type { PageComponentType } from \"@openstatus/db/src/schema\";\n\n/**\n * Type guard to check if a pageComponent is a monitor type with a valid monitor relation\n * Filters out static components and ensures the monitor is active and not deleted\n */\nexport function isMonitorComponent(component: {\n  type: PageComponentType;\n  monitor?: { active: boolean | null; deletedAt: Date | null } | null;\n}): component is {\n  type: \"monitor\";\n  monitor: { active: true; deletedAt: null };\n} {\n  return (\n    component.type === \"monitor\" &&\n    component.monitor !== null &&\n    component.monitor !== undefined &&\n    component.monitor.active === true &&\n    component.monitor.deletedAt === null\n  );\n}\n"
  },
  {
    "path": "apps/server/src/utils/random-promise.ts",
    "content": "export function fakePromiseWithRandomResolve() {\n  return new Promise((resolve, reject) => {\n    const randomTime = Math.floor(Math.random() * 1000);\n    setTimeout(() => {\n      const shouldResolve = Math.random() < 1; // 0.5\n      if (shouldResolve) {\n        resolve(\"Promise resolved successfully.\");\n      } else {\n        reject(new Error(\"Promise rejected.\"));\n      }\n    }, randomTime);\n  });\n}\n"
  },
  {
    "path": "apps/server/static/openapi-v1.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"version\": \"1.0.0\",\n    \"title\": \"OpenStatus API\",\n    \"contact\": {\n      \"email\": \"ping@openstatus.dev\",\n      \"url\": \"https://www.openstatus.dev\"\n    },\n    \"description\": \"This version is deprecated please use v2\"\n  },\n  \"tags\": [\n    {\n      \"name\": \"monitor\",\n      \"description\": \"Monitor related endpoints\",\n      \"x-displayName\": \"Monitor\"\n    },\n    {\n      \"name\": \"page\",\n      \"description\": \"Page related endpoints\",\n      \"x-displayName\": \"Page\"\n    },\n    {\n      \"name\": \"status_report\",\n      \"description\": \"Status report related endpoints\",\n      \"x-displayName\": \"Status Report\"\n    },\n    {\n      \"name\": \"status_report_update\",\n      \"description\": \"Status report update related endpoints\",\n      \"x-displayName\": \"Status Report Update\"\n    },\n    {\n      \"name\": \"incident\",\n      \"description\": \"Incident related endpoints\",\n      \"x-displayName\": \"Incident\"\n    },\n    {\n      \"name\": \"maintenance\",\n      \"description\": \"Maintenance related endpoints\",\n      \"x-displayName\": \"Maintenance\"\n    },\n    {\n      \"name\": \"notification\",\n      \"description\": \"Notification related endpoints\",\n      \"x-displayName\": \"Notification\"\n    },\n    {\n      \"name\": \"page_subscriber\",\n      \"description\": \"Page subscriber related endpoints\",\n      \"x-displayName\": \"Page Subscriber\"\n    },\n    {\n      \"name\": \"check\",\n      \"description\": \"Check related endpoints\",\n      \"x-displayName\": \"Check\"\n    },\n    {\n      \"name\": \"whoami\",\n      \"description\": \"WhoAmI related endpoints\",\n      \"x-displayName\": \"WhoAmI\"\n    }\n  ],\n  \"security\": [\n    {\n      \"ApiKeyAuth\": []\n    }\n  ],\n  \"components\": {\n    \"securitySchemes\": {\n      \"ApiKeyAuth\": {\n        \"type\": \"apiKey\",\n        \"in\": \"header\",\n        \"name\": \"x-openstatus-key\",\n        \"x-openstatus-key\": \"string\"\n      }\n    },\n    \"schemas\": {\n      \"Monitor\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"number\",\n            \"example\": 123,\n            \"description\": \"The id of the monitor\"\n          },\n          \"periodicity\": {\n            \"type\": \"string\",\n            \"enum\": [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\", \"other\"],\n            \"example\": \"1m\",\n            \"description\": \"How often the monitor should run\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"example\": \"https://www.documenso.co\",\n            \"description\": \"The url to monitor\"\n          },\n          \"regions\": {\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"ams\",\n                \"arn\",\n                \"atl\",\n                \"bog\",\n                \"bom\",\n                \"bos\",\n                \"cdg\",\n                \"den\",\n                \"dfw\",\n                \"ewr\",\n                \"eze\",\n                \"fra\",\n                \"gdl\",\n                \"gig\",\n                \"gru\",\n                \"hkg\",\n                \"iad\",\n                \"jnb\",\n                \"lax\",\n                \"lhr\",\n                \"mad\",\n                \"mia\",\n                \"nrt\",\n                \"ord\",\n                \"otp\",\n                \"phx\",\n                \"qro\",\n                \"scl\",\n                \"sjc\",\n                \"sea\",\n                \"sin\",\n                \"syd\",\n                \"waw\",\n                \"yul\",\n                \"yyz\",\n                \"koyeb_fra\",\n                \"koyeb_was\",\n                \"koyeb_sin\",\n                \"koyeb_tyo\",\n                \"koyeb_par\",\n                \"koyeb_sfo\",\n                \"railway_europe-west4-drams3a\",\n                \"railway_us-east4-eqdc4a\",\n                \"railway_asia-southeast1-eqsg3a\",\n                \"railway_us-west2\"\n              ]\n            },\n            \"default\": [],\n            \"example\": [\"ams\"],\n            \"description\": \"Where we should monitor it\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"example\": \"documenso-web\",\n            \"description\": \"The name of the monitor\"\n          },\n          \"externalName\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"example\": \"Documenso\",\n            \"description\": \"The external name of the monitor, used to display on the status page or in the external notifications\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"example\": \"Documenso website\",\n            \"description\": \"The description of your monitor\"\n          },\n          \"method\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"GET\",\n              \"POST\",\n              \"HEAD\",\n              \"PUT\",\n              \"PATCH\",\n              \"DELETE\",\n              \"TRACE\",\n              \"CONNECT\",\n              \"OPTIONS\"\n            ],\n            \"example\": \"GET\"\n          },\n          \"body\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"default\": \"\",\n            \"example\": \"Hello World\",\n            \"description\": \"The body\"\n          },\n          \"headers\": {\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"key\": {\n                  \"type\": \"string\"\n                },\n                \"value\": {\n                  \"type\": \"string\"\n                }\n              },\n              \"required\": [\"key\", \"value\"]\n            },\n            \"default\": [],\n            \"description\": \"The headers of your request\",\n            \"example\": [\n              {\n                \"key\": \"x-apikey\",\n                \"value\": \"supersecrettoken\"\n              }\n            ]\n          },\n          \"assertions\": {\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"oneOf\": [\n                {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"type\": {\n                      \"type\": \"string\",\n                      \"enum\": [\"status\"]\n                    },\n                    \"compare\": {\n                      \"type\": \"string\",\n                      \"enum\": [\"eq\", \"not_eq\", \"gt\", \"gte\", \"lt\", \"lte\"],\n                      \"description\": \"Comparison operator\",\n                      \"examples\": [\"eq\", \"not_eq\", \"gt\", \"gte\", \"lt\", \"lte\"]\n                    },\n                    \"target\": {\n                      \"type\": \"integer\",\n                      \"minimum\": 0,\n                      \"exclusiveMinimum\": true,\n                      \"description\": \"The target value\"\n                    }\n                  },\n                  \"required\": [\"type\", \"compare\", \"target\"],\n                  \"description\": \"The status assertion\"\n                },\n                {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"type\": {\n                      \"type\": \"string\",\n                      \"enum\": [\"header\"]\n                    },\n                    \"compare\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"contains\",\n                        \"not_contains\",\n                        \"eq\",\n                        \"not_eq\",\n                        \"empty\",\n                        \"not_empty\",\n                        \"gt\",\n                        \"gte\",\n                        \"lt\",\n                        \"lte\"\n                      ]\n                    },\n                    \"key\": {\n                      \"type\": \"string\",\n                      \"description\": \"The key of the header\"\n                    },\n                    \"target\": {\n                      \"type\": \"string\",\n                      \"description\": \"the header value\"\n                    }\n                  },\n                  \"required\": [\"type\", \"compare\", \"key\", \"target\"],\n                  \"description\": \"The header assertion\"\n                },\n                {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"type\": {\n                      \"type\": \"string\",\n                      \"enum\": [\"textBody\"]\n                    },\n                    \"compare\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"contains\",\n                        \"not_contains\",\n                        \"eq\",\n                        \"not_eq\",\n                        \"empty\",\n                        \"not_empty\",\n                        \"gt\",\n                        \"gte\",\n                        \"lt\",\n                        \"lte\"\n                      ]\n                    },\n                    \"target\": {\n                      \"type\": \"string\",\n                      \"description\": \"The target value\"\n                    }\n                  },\n                  \"required\": [\"type\", \"compare\", \"target\"],\n                  \"description\": \"The text body assertion\"\n                },\n                {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"type\": {\n                      \"type\": \"string\",\n                      \"enum\": [\"dnsRecord\"]\n                    },\n                    \"key\": {\n                      \"type\": \"string\",\n                      \"enum\": [\"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\", \"NS\"]\n                    },\n                    \"compare\": {\n                      \"type\": \"string\",\n                      \"enum\": [\"contains\", \"not_contains\", \"eq\", \"not_eq\"]\n                    },\n                    \"target\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\"type\", \"key\", \"compare\", \"target\"],\n                  \"description\": \"The DNS record assertion\"\n                }\n              ]\n            },\n            \"default\": [],\n            \"description\": \"The assertions to run\"\n          },\n          \"active\": {\n            \"type\": \"boolean\",\n            \"default\": false,\n            \"description\": \"If the monitor is active\"\n          },\n          \"public\": {\n            \"type\": \"boolean\",\n            \"default\": false,\n            \"description\": \"If the monitor is public\"\n          },\n          \"degradedAfter\": {\n            \"type\": \"number\",\n            \"nullable\": true,\n            \"description\": \"The time after the monitor is considered degraded in milliseconds\"\n          },\n          \"timeout\": {\n            \"type\": \"number\",\n            \"nullable\": true,\n            \"default\": 45000,\n            \"description\": \"The timeout of the request in milliseconds\"\n          },\n          \"retry\": {\n            \"type\": \"number\",\n            \"default\": 3,\n            \"description\": \"The number of retries to attempt\"\n          },\n          \"followRedirects\": {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"If the monitor should follow redirects\"\n          },\n          \"jobType\": {\n            \"type\": \"string\",\n            \"enum\": [\"http\", \"tcp\", \"imcp\", \"udp\", \"dns\", \"ssl\"],\n            \"default\": \"http\",\n            \"description\": \"The type of the monitor\"\n          },\n          \"openTelemetry\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"endpoint\": {\n                \"type\": \"string\",\n                \"format\": \"uri\",\n                \"default\": \"http://localhost:4317\",\n                \"description\": \"The endpoint of the OpenTelemetry collector\"\n              },\n              \"headers\": {\n                \"type\": \"object\",\n                \"additionalProperties\": {\n                  \"type\": \"string\"\n                },\n                \"default\": {},\n                \"description\": \"The headers to send to the OpenTelemetry collector\"\n              }\n            },\n            \"description\": \"The OpenTelemetry configuration\"\n          }\n        },\n        \"required\": [\"id\", \"periodicity\", \"url\", \"name\", \"method\"]\n      },\n      \"ErrBadRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"BAD_REQUEST\",\n              \"FORBIDDEN\",\n              \"INTERNAL_SERVER_ERROR\",\n              \"PAYMENT_REQUIRED\",\n              \"CONFLICT\",\n              \"NOT_FOUND\",\n              \"UNAUTHORIZED\",\n              \"METHOD_NOT_ALLOWED\",\n              \"UNPROCESSABLE_ENTITY\"\n            ],\n            \"example\": \"BAD_REQUEST\",\n            \"description\": \"The error code related to the status code.\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"A human readable message describing the issue.\",\n            \"example\": \"<string>\"\n          },\n          \"docs\": {\n            \"type\": \"string\",\n            \"description\": \"A link to the documentation for the error.\",\n            \"example\": \"https://docs.openstatus.dev/api-references/errors/code/BAD_REQUEST\"\n          },\n          \"requestId\": {\n            \"type\": \"string\",\n            \"description\": \"The request id to be used for debugging and error reporting.\",\n            \"example\": \"<uuid>\"\n          }\n        },\n        \"required\": [\"code\", \"message\", \"docs\", \"requestId\"]\n      },\n      \"ErrUnauthorized\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"BAD_REQUEST\",\n              \"FORBIDDEN\",\n              \"INTERNAL_SERVER_ERROR\",\n              \"PAYMENT_REQUIRED\",\n              \"CONFLICT\",\n              \"NOT_FOUND\",\n              \"UNAUTHORIZED\",\n              \"METHOD_NOT_ALLOWED\",\n              \"UNPROCESSABLE_ENTITY\"\n            ],\n            \"example\": \"UNAUTHORIZED\",\n            \"description\": \"The error code related to the status code.\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"A human readable message describing the issue.\",\n            \"example\": \"<string>\"\n          },\n          \"docs\": {\n            \"type\": \"string\",\n            \"description\": \"A link to the documentation for the error.\",\n            \"example\": \"https://docs.openstatus.dev/api-references/errors/code/UNAUTHORIZED\"\n          },\n          \"requestId\": {\n            \"type\": \"string\",\n            \"description\": \"The request id to be used for debugging and error reporting.\",\n            \"example\": \"<uuid>\"\n          }\n        },\n        \"required\": [\"code\", \"message\", \"docs\", \"requestId\"]\n      },\n      \"ErrPaymentRequired\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"BAD_REQUEST\",\n              \"FORBIDDEN\",\n              \"INTERNAL_SERVER_ERROR\",\n              \"PAYMENT_REQUIRED\",\n              \"CONFLICT\",\n              \"NOT_FOUND\",\n              \"UNAUTHORIZED\",\n              \"METHOD_NOT_ALLOWED\",\n              \"UNPROCESSABLE_ENTITY\"\n            ],\n            \"example\": \"PAYMENT_REQUIRED\",\n            \"description\": \"The error code related to the status code.\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"A human readable message describing the issue.\",\n            \"example\": \"<string>\"\n          },\n          \"docs\": {\n            \"type\": \"string\",\n            \"description\": \"A link to the documentation for the error.\",\n            \"example\": \"https://docs.openstatus.dev/api-references/errors/code/PAYMENT_REQUIRED\"\n          },\n          \"requestId\": {\n            \"type\": \"string\",\n            \"description\": \"The request id to be used for debugging and error reporting.\",\n            \"example\": \"<uuid>\"\n          }\n        },\n        \"required\": [\"code\", \"message\", \"docs\", \"requestId\"]\n      },\n      \"ErrForbidden\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"BAD_REQUEST\",\n              \"FORBIDDEN\",\n              \"INTERNAL_SERVER_ERROR\",\n              \"PAYMENT_REQUIRED\",\n              \"CONFLICT\",\n              \"NOT_FOUND\",\n              \"UNAUTHORIZED\",\n              \"METHOD_NOT_ALLOWED\",\n              \"UNPROCESSABLE_ENTITY\"\n            ],\n            \"example\": \"FORBIDDEN\",\n            \"description\": \"The error code related to the status code.\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"A human readable message describing the issue.\",\n            \"example\": \"<string>\"\n          },\n          \"docs\": {\n            \"type\": \"string\",\n            \"description\": \"A link to the documentation for the error.\",\n            \"example\": \"https://docs.openstatus.dev/api-references/errors/code/FORBIDDEN\"\n          },\n          \"requestId\": {\n            \"type\": \"string\",\n            \"description\": \"The request id to be used for debugging and error reporting.\",\n            \"example\": \"<uuid>\"\n          }\n        },\n        \"required\": [\"code\", \"message\", \"docs\", \"requestId\"]\n      },\n      \"ErrNotFound\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"BAD_REQUEST\",\n              \"FORBIDDEN\",\n              \"INTERNAL_SERVER_ERROR\",\n              \"PAYMENT_REQUIRED\",\n              \"CONFLICT\",\n              \"NOT_FOUND\",\n              \"UNAUTHORIZED\",\n              \"METHOD_NOT_ALLOWED\",\n              \"UNPROCESSABLE_ENTITY\"\n            ],\n            \"example\": \"NOT_FOUND\",\n            \"description\": \"The error code related to the status code.\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"A human readable message describing the issue.\",\n            \"example\": \"<string>\"\n          },\n          \"docs\": {\n            \"type\": \"string\",\n            \"description\": \"A link to the documentation for the error.\",\n            \"example\": \"https://docs.openstatus.dev/api-references/errors/code/NOT_FOUND\"\n          },\n          \"requestId\": {\n            \"type\": \"string\",\n            \"description\": \"The request id to be used for debugging and error reporting.\",\n            \"example\": \"<uuid>\"\n          }\n        },\n        \"required\": [\"code\", \"message\", \"docs\", \"requestId\"]\n      },\n      \"ErrConflict\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"BAD_REQUEST\",\n              \"FORBIDDEN\",\n              \"INTERNAL_SERVER_ERROR\",\n              \"PAYMENT_REQUIRED\",\n              \"CONFLICT\",\n              \"NOT_FOUND\",\n              \"UNAUTHORIZED\",\n              \"METHOD_NOT_ALLOWED\",\n              \"UNPROCESSABLE_ENTITY\"\n            ],\n            \"example\": \"CONFLICT\",\n            \"description\": \"The error code related to the status code.\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"A human readable message describing the issue.\",\n            \"example\": \"<string>\"\n          },\n          \"docs\": {\n            \"type\": \"string\",\n            \"description\": \"A link to the documentation for the error.\",\n            \"example\": \"https://docs.openstatus.dev/api-references/errors/code/CONFLICT\"\n          },\n          \"requestId\": {\n            \"type\": \"string\",\n            \"description\": \"The request id to be used for debugging and error reporting.\",\n            \"example\": \"<uuid>\"\n          }\n        },\n        \"required\": [\"code\", \"message\", \"docs\", \"requestId\"]\n      },\n      \"ErrInternalServerError\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"BAD_REQUEST\",\n              \"FORBIDDEN\",\n              \"INTERNAL_SERVER_ERROR\",\n              \"PAYMENT_REQUIRED\",\n              \"CONFLICT\",\n              \"NOT_FOUND\",\n              \"UNAUTHORIZED\",\n              \"METHOD_NOT_ALLOWED\",\n              \"UNPROCESSABLE_ENTITY\"\n            ],\n            \"example\": \"INTERNAL_SERVER_ERROR\",\n            \"description\": \"The error code related to the status code.\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"A human readable message describing the issue.\",\n            \"example\": \"<string>\"\n          },\n          \"docs\": {\n            \"type\": \"string\",\n            \"description\": \"A link to the documentation for the error.\",\n            \"example\": \"https://docs.openstatus.dev/api-references/errors/code/INTERNAL_SERVER_ERROR\"\n          },\n          \"requestId\": {\n            \"type\": \"string\",\n            \"description\": \"The request id to be used for debugging and error reporting.\",\n            \"example\": \"<uuid>\"\n          }\n        },\n        \"required\": [\"code\", \"message\", \"docs\", \"requestId\"]\n      },\n      \"Page\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the page\",\n            \"example\": 1\n          },\n          \"title\": {\n            \"type\": \"string\",\n            \"description\": \"The title of the page\",\n            \"example\": \"My Page\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"description\": \"The description of the page\",\n            \"example\": \"My awesome status page\"\n          },\n          \"slug\": {\n            \"type\": \"string\",\n            \"description\": \"The slug of the page\",\n            \"example\": \"my-page\"\n          },\n          \"customDomain\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"description\": \"The custom domain of the page. To be configured within the dashboard.\",\n            \"example\": \"status.acme.com\"\n          },\n          \"icon\": {\n            \"anyOf\": [\n              {\n                \"type\": \"string\",\n                \"format\": \"uri\"\n              },\n              {\n                \"type\": \"string\",\n                \"enum\": [\"\"]\n              },\n              {\n                \"nullable\": true\n              }\n            ],\n            \"description\": \"The icon of the page\",\n            \"example\": \"https://example.com/icon.png\"\n          },\n          \"passwordProtected\": {\n            \"type\": \"boolean\",\n            \"default\": false,\n            \"description\": \"Deprecated in favor of `accessType`. Used to set the password protection type. Returns true if `accessType` is set to 'password' and false otherwise.\",\n            \"example\": true,\n            \"deprecated\": true\n          },\n          \"accessType\": {\n            \"type\": \"string\",\n            \"enum\": [\"public\", \"password\", \"email-domain\"],\n            \"default\": \"public\",\n            \"description\": \"The access type of the page\",\n            \"example\": \"public\"\n          },\n          \"password\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"description\": \"Your password to protect the page from the public\",\n            \"example\": \"hidden-password\"\n          },\n          \"authEmailDomains\": {\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"The email domains of the page\",\n            \"example\": [\"example.com\", \"example.org\"]\n          },\n          \"showMonitorValues\": {\n            \"type\": \"boolean\",\n            \"nullable\": true,\n            \"default\": true,\n            \"description\": \"Displays the total and failed request numbers for each monitor. Deprecated and will be removed in the future in favor for `configuration` property.\",\n            \"example\": true,\n            \"deprecated\": true\n          },\n          \"monitors\": {\n            \"anyOf\": [\n              {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"number\"\n                },\n                \"description\": \"The monitors of the page as an array of ids. We recommend using the object format to include the order.\",\n                \"deprecated\": true,\n                \"example\": [1, 2]\n              },\n              {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"monitorId\": {\n                      \"type\": \"number\"\n                    },\n                    \"order\": {\n                      \"type\": \"number\"\n                    }\n                  },\n                  \"required\": [\"monitorId\", \"order\"]\n                },\n                \"description\": \"The monitor as object allowing to pass id and order\",\n                \"example\": [\n                  {\n                    \"monitorId\": 1,\n                    \"order\": 0\n                  },\n                  {\n                    \"monitorId\": 2,\n                    \"order\": 1\n                  }\n                ]\n              }\n            ]\n          }\n        },\n        \"required\": [\"id\", \"title\", \"description\", \"slug\"]\n      },\n      \"StatusReport\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the status report\"\n          },\n          \"title\": {\n            \"type\": \"string\",\n            \"example\": \"Documenso\",\n            \"description\": \"The title of the status report\"\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"enum\": [\"investigating\", \"identified\", \"monitoring\", \"resolved\"],\n            \"description\": \"The current status of the report\"\n          },\n          \"statusReportUpdateIds\": {\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"number\"\n            },\n            \"default\": [],\n            \"description\": \"The ids of the status report updates\"\n          },\n          \"monitorIds\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"number\"\n            },\n            \"default\": [],\n            \"description\": \"Ids of the monitors the status report.\"\n          },\n          \"pageId\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the page this status report belongs to\"\n          }\n        },\n        \"required\": [\"id\", \"title\", \"status\", \"pageId\"]\n      },\n      \"StatusReportUpdate\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"description\": \"The id of the update\"\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"enum\": [\"investigating\", \"identified\", \"monitoring\", \"resolved\"],\n            \"description\": \"The status of the update\"\n          },\n          \"date\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"format\": \"date\",\n            \"default\": \"2026-02-12T22:01:46.114Z\",\n            \"description\": \"The date of the update in ISO8601 format\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"minLength\": 1,\n            \"description\": \"The message of the update\"\n          },\n          \"statusReportId\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the status report\"\n          }\n        },\n        \"required\": [\"status\", \"message\", \"statusReportId\"]\n      },\n      \"Incident\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the incident\",\n            \"example\": 1\n          },\n          \"startedAt\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"format\": \"date\",\n            \"description\": \"The date the incident started\"\n          },\n          \"monitorId\": {\n            \"type\": \"number\",\n            \"nullable\": true,\n            \"description\": \"The id of the monitor associated with the incident\",\n            \"example\": 1\n          },\n          \"acknowledgedAt\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"format\": \"date\",\n            \"description\": \"The date the incident was acknowledged\"\n          },\n          \"acknowledgedBy\": {\n            \"type\": \"number\",\n            \"nullable\": true,\n            \"description\": \"The user who acknowledged the incident\"\n          },\n          \"resolvedAt\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"format\": \"date\",\n            \"description\": \"The date the incident was resolved\"\n          },\n          \"resolvedBy\": {\n            \"type\": \"number\",\n            \"nullable\": true,\n            \"description\": \"The user who resolved the incident\"\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"startedAt\",\n          \"monitorId\",\n          \"acknowledgedBy\",\n          \"resolvedBy\"\n        ]\n      },\n      \"Maintenance\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the maintenance\",\n            \"example\": 1\n          },\n          \"title\": {\n            \"type\": \"string\",\n            \"description\": \"The title of the maintenance\",\n            \"example\": \"Database Upgrade\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"The message describing the maintenance\",\n            \"example\": \"Upgrading database to improve performance\"\n          },\n          \"from\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"format\": \"date\",\n            \"description\": \"When the maintenance starts\"\n          },\n          \"to\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"format\": \"date\",\n            \"description\": \"When the maintenance ends\"\n          },\n          \"monitorIds\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"number\"\n            },\n            \"default\": [],\n            \"description\": \"IDs of affected monitors\"\n          },\n          \"pageId\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the status page this maintenance belongs to\"\n          }\n        },\n        \"required\": [\"id\", \"title\", \"message\", \"from\", \"to\", \"pageId\"]\n      },\n      \"Notification\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the notification\",\n            \"example\": 1\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the notification\",\n            \"example\": \"OpenStatus Discord\"\n          },\n          \"provider\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"discord\",\n              \"email\",\n              \"google-chat\",\n              \"grafana-oncall\",\n              \"ntfy\",\n              \"pagerduty\",\n              \"opsgenie\",\n              \"slack\",\n              \"sms\",\n              \"telegram\",\n              \"webhook\",\n              \"whatsapp\"\n            ],\n            \"description\": \"The provider of the notification\",\n            \"example\": \"discord\"\n          },\n          \"payload\": {\n            \"anyOf\": [\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"discord\": {\n                    \"type\": \"string\",\n                    \"format\": \"uri\"\n                  }\n                },\n                \"required\": [\"discord\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"email\": {\n                    \"type\": \"string\",\n                    \"format\": \"email\"\n                  }\n                },\n                \"required\": [\"email\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"grafana-oncall\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"webhookUrl\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\"\n                      }\n                    },\n                    \"required\": [\"webhookUrl\"]\n                  }\n                },\n                \"required\": [\"grafana-oncall\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"ntfy\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"topic\": {\n                        \"type\": \"string\",\n                        \"default\": \"\"\n                      },\n                      \"serverUrl\": {\n                        \"type\": \"string\",\n                        \"default\": \"https://ntfy.sh\"\n                      },\n                      \"token\": {\n                        \"type\": \"string\"\n                      }\n                    }\n                  }\n                },\n                \"required\": [\"ntfy\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"opsgenie\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"apiKey\": {\n                        \"type\": \"string\"\n                      },\n                      \"region\": {\n                        \"type\": \"string\",\n                        \"enum\": [\"us\", \"eu\"]\n                      }\n                    },\n                    \"required\": [\"apiKey\", \"region\"]\n                  }\n                },\n                \"required\": [\"opsgenie\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"pagerduty\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\"pagerduty\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"sms\": {\n                    \"type\": \"string\",\n                    \"pattern\": \"^([+]?[\\\\s0-9]+)?(\\\\d{3}|[(]?[0-9]+[)])?([-]?[\\\\s]?[0-9])+$\"\n                  }\n                },\n                \"required\": [\"sms\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"telegram\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"chatId\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"required\": [\"chatId\"]\n                  }\n                },\n                \"required\": [\"telegram\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"slack\": {\n                    \"type\": \"string\",\n                    \"format\": \"uri\"\n                  }\n                },\n                \"required\": [\"slack\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"webhook\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"endpoint\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\"\n                      },\n                      \"headers\": {\n                        \"type\": \"array\",\n                        \"items\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"key\": {\n                              \"type\": \"string\"\n                            },\n                            \"value\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"required\": [\"key\", \"value\"]\n                        }\n                      }\n                    },\n                    \"required\": [\"endpoint\"]\n                  }\n                },\n                \"required\": [\"webhook\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"whatsapp\": {\n                    \"type\": \"string\",\n                    \"pattern\": \"^([+]?[\\\\s0-9]+)?(\\\\d{3}|[(]?[0-9]+[)])?([-]?[\\\\s]?[0-9])+$\"\n                  }\n                },\n                \"required\": [\"whatsapp\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"google-chat\": {\n                    \"type\": \"string\",\n                    \"format\": \"uri\"\n                  }\n                },\n                \"required\": [\"google-chat\"]\n              }\n            ],\n            \"description\": \"The data of the notification\"\n          },\n          \"monitors\": {\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"number\"\n            },\n            \"description\": \"The monitors that the notification is linked to\",\n            \"example\": [1, 2]\n          }\n        },\n        \"required\": [\"id\", \"name\", \"provider\", \"payload\"]\n      },\n      \"PageSubscriber\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the subscriber\",\n            \"example\": 1\n          },\n          \"email\": {\n            \"type\": \"string\",\n            \"format\": \"email\",\n            \"description\": \"The email of the subscriber\"\n          },\n          \"pageId\": {\n            \"type\": \"number\",\n            \"description\": \"The id of the page to subscribe to\",\n            \"example\": 1\n          }\n        },\n        \"required\": [\"id\", \"email\", \"pageId\"]\n      },\n      \"Workspace\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"The current workspace name\"\n          },\n          \"slug\": {\n            \"type\": \"string\",\n            \"description\": \"The current workspace slug\"\n          },\n          \"plan\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"enum\": [\"free\", \"starter\", \"team\"],\n            \"default\": \"free\",\n            \"description\": \"The current workspace plan\"\n          }\n        },\n        \"required\": [\"slug\"]\n      }\n    },\n    \"parameters\": {}\n  },\n  \"paths\": {\n    \"/monitor\": {\n      \"get\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"List all monitors\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"All the monitors\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/Monitor\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Create a monitor\",\n        \"requestBody\": {\n          \"description\": \"The monitor to create\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"periodicity\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\", \"other\"],\n                    \"example\": \"1m\",\n                    \"description\": \"How often the monitor should run\"\n                  },\n                  \"url\": {\n                    \"type\": \"string\",\n                    \"example\": \"https://www.documenso.co\",\n                    \"description\": \"The url to monitor\"\n                  },\n                  \"regions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"ams\",\n                        \"arn\",\n                        \"atl\",\n                        \"bog\",\n                        \"bom\",\n                        \"bos\",\n                        \"cdg\",\n                        \"den\",\n                        \"dfw\",\n                        \"ewr\",\n                        \"eze\",\n                        \"fra\",\n                        \"gdl\",\n                        \"gig\",\n                        \"gru\",\n                        \"hkg\",\n                        \"iad\",\n                        \"jnb\",\n                        \"lax\",\n                        \"lhr\",\n                        \"mad\",\n                        \"mia\",\n                        \"nrt\",\n                        \"ord\",\n                        \"otp\",\n                        \"phx\",\n                        \"qro\",\n                        \"scl\",\n                        \"sjc\",\n                        \"sea\",\n                        \"sin\",\n                        \"syd\",\n                        \"waw\",\n                        \"yul\",\n                        \"yyz\",\n                        \"koyeb_fra\",\n                        \"koyeb_was\",\n                        \"koyeb_sin\",\n                        \"koyeb_tyo\",\n                        \"koyeb_par\",\n                        \"koyeb_sfo\",\n                        \"railway_europe-west4-drams3a\",\n                        \"railway_us-east4-eqdc4a\",\n                        \"railway_asia-southeast1-eqsg3a\",\n                        \"railway_us-west2\"\n                      ]\n                    },\n                    \"default\": [],\n                    \"example\": [\"ams\"],\n                    \"description\": \"Where we should monitor it\"\n                  },\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"documenso-web\",\n                    \"description\": \"The name of the monitor\"\n                  },\n                  \"externalName\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"example\": \"Documenso\",\n                    \"description\": \"The external name of the monitor, used to display on the status page or in the external notifications\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"example\": \"Documenso website\",\n                    \"description\": \"The description of your monitor\"\n                  },\n                  \"method\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"GET\",\n                      \"POST\",\n                      \"HEAD\",\n                      \"PUT\",\n                      \"PATCH\",\n                      \"DELETE\",\n                      \"TRACE\",\n                      \"CONNECT\",\n                      \"OPTIONS\"\n                    ],\n                    \"example\": \"GET\"\n                  },\n                  \"body\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"default\": \"\",\n                    \"example\": \"Hello World\",\n                    \"description\": \"The body\"\n                  },\n                  \"headers\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"key\": {\n                          \"type\": \"string\"\n                        },\n                        \"value\": {\n                          \"type\": \"string\"\n                        }\n                      },\n                      \"required\": [\"key\", \"value\"]\n                    },\n                    \"default\": [],\n                    \"description\": \"The headers of your request\",\n                    \"example\": [\n                      {\n                        \"key\": \"x-apikey\",\n                        \"value\": \"supersecrettoken\"\n                      }\n                    ]\n                  },\n                  \"assertions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"oneOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"type\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"status\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ],\n                              \"description\": \"Comparison operator\",\n                              \"examples\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"integer\",\n                              \"minimum\": 0,\n                              \"exclusiveMinimum\": true,\n                              \"description\": \"The target value\"\n                            }\n                          },\n                          \"required\": [\"type\", \"compare\", \"target\"],\n                          \"description\": \"The status assertion\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"type\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"header\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\",\n                                \"empty\",\n                                \"not_empty\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ]\n                            },\n                            \"key\": {\n                              \"type\": \"string\",\n                              \"description\": \"The key of the header\"\n                            },\n                            \"target\": {\n                              \"type\": \"string\",\n                              \"description\": \"the header value\"\n                            }\n                          },\n                          \"required\": [\"type\", \"compare\", \"key\", \"target\"],\n                          \"description\": \"The header assertion\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"type\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"textBody\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\",\n                                \"empty\",\n                                \"not_empty\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"string\",\n                              \"description\": \"The target value\"\n                            }\n                          },\n                          \"required\": [\"type\", \"compare\", \"target\"],\n                          \"description\": \"The text body assertion\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"type\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"dnsRecord\"]\n                            },\n                            \"key\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\", \"NS\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"required\": [\"type\", \"key\", \"compare\", \"target\"],\n                          \"description\": \"The DNS record assertion\"\n                        }\n                      ]\n                    },\n                    \"default\": [],\n                    \"description\": \"The assertions to run\"\n                  },\n                  \"active\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"If the monitor is active\"\n                  },\n                  \"public\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"If the monitor is public\"\n                  },\n                  \"degradedAfter\": {\n                    \"type\": \"number\",\n                    \"nullable\": true,\n                    \"description\": \"The time after the monitor is considered degraded in milliseconds\"\n                  },\n                  \"timeout\": {\n                    \"type\": \"number\",\n                    \"nullable\": true,\n                    \"default\": 45000,\n                    \"description\": \"The timeout of the request in milliseconds\"\n                  },\n                  \"retry\": {\n                    \"type\": \"number\",\n                    \"default\": 3,\n                    \"description\": \"The number of retries to attempt\"\n                  },\n                  \"followRedirects\": {\n                    \"type\": \"boolean\",\n                    \"default\": true,\n                    \"description\": \"If the monitor should follow redirects\"\n                  },\n                  \"jobType\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"http\", \"tcp\", \"imcp\", \"udp\", \"dns\", \"ssl\"],\n                    \"default\": \"http\",\n                    \"description\": \"The type of the monitor\"\n                  },\n                  \"openTelemetry\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"endpoint\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"default\": \"http://localhost:4317\",\n                        \"description\": \"The endpoint of the OpenTelemetry collector\"\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"default\": {},\n                        \"description\": \"The headers to send to the OpenTelemetry collector\"\n                      }\n                    },\n                    \"description\": \"The OpenTelemetry configuration\"\n                  }\n                },\n                \"required\": [\"periodicity\", \"url\", \"name\", \"method\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Create a monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Monitor\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/{id}\": {\n      \"get\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Get a monitor\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Monitor\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Update a monitor\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The monitor to update\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"periodicity\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\", \"other\"],\n                    \"example\": \"1m\",\n                    \"description\": \"How often the monitor should run\"\n                  },\n                  \"url\": {\n                    \"type\": \"string\",\n                    \"example\": \"https://www.documenso.co\",\n                    \"description\": \"The url to monitor\"\n                  },\n                  \"regions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"ams\",\n                        \"arn\",\n                        \"atl\",\n                        \"bog\",\n                        \"bom\",\n                        \"bos\",\n                        \"cdg\",\n                        \"den\",\n                        \"dfw\",\n                        \"ewr\",\n                        \"eze\",\n                        \"fra\",\n                        \"gdl\",\n                        \"gig\",\n                        \"gru\",\n                        \"hkg\",\n                        \"iad\",\n                        \"jnb\",\n                        \"lax\",\n                        \"lhr\",\n                        \"mad\",\n                        \"mia\",\n                        \"nrt\",\n                        \"ord\",\n                        \"otp\",\n                        \"phx\",\n                        \"qro\",\n                        \"scl\",\n                        \"sjc\",\n                        \"sea\",\n                        \"sin\",\n                        \"syd\",\n                        \"waw\",\n                        \"yul\",\n                        \"yyz\",\n                        \"koyeb_fra\",\n                        \"koyeb_was\",\n                        \"koyeb_sin\",\n                        \"koyeb_tyo\",\n                        \"koyeb_par\",\n                        \"koyeb_sfo\",\n                        \"railway_europe-west4-drams3a\",\n                        \"railway_us-east4-eqdc4a\",\n                        \"railway_asia-southeast1-eqsg3a\",\n                        \"railway_us-west2\"\n                      ]\n                    },\n                    \"default\": [],\n                    \"example\": [\"ams\"],\n                    \"description\": \"Where we should monitor it\"\n                  },\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"documenso-web\",\n                    \"description\": \"The name of the monitor\"\n                  },\n                  \"externalName\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"example\": \"Documenso\",\n                    \"description\": \"The external name of the monitor, used to display on the status page or in the external notifications\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"example\": \"Documenso website\",\n                    \"description\": \"The description of your monitor\"\n                  },\n                  \"method\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"GET\",\n                      \"POST\",\n                      \"HEAD\",\n                      \"PUT\",\n                      \"PATCH\",\n                      \"DELETE\",\n                      \"TRACE\",\n                      \"CONNECT\",\n                      \"OPTIONS\"\n                    ],\n                    \"example\": \"GET\"\n                  },\n                  \"body\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"default\": \"\",\n                    \"example\": \"Hello World\",\n                    \"description\": \"The body\"\n                  },\n                  \"headers\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"key\": {\n                          \"type\": \"string\"\n                        },\n                        \"value\": {\n                          \"type\": \"string\"\n                        }\n                      },\n                      \"required\": [\"key\", \"value\"]\n                    },\n                    \"default\": [],\n                    \"description\": \"The headers of your request\",\n                    \"example\": [\n                      {\n                        \"key\": \"x-apikey\",\n                        \"value\": \"supersecrettoken\"\n                      }\n                    ]\n                  },\n                  \"assertions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"oneOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"type\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"status\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ],\n                              \"description\": \"Comparison operator\",\n                              \"examples\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"integer\",\n                              \"minimum\": 0,\n                              \"exclusiveMinimum\": true,\n                              \"description\": \"The target value\"\n                            }\n                          },\n                          \"required\": [\"type\", \"compare\", \"target\"],\n                          \"description\": \"The status assertion\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"type\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"header\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\",\n                                \"empty\",\n                                \"not_empty\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ]\n                            },\n                            \"key\": {\n                              \"type\": \"string\",\n                              \"description\": \"The key of the header\"\n                            },\n                            \"target\": {\n                              \"type\": \"string\",\n                              \"description\": \"the header value\"\n                            }\n                          },\n                          \"required\": [\"type\", \"compare\", \"key\", \"target\"],\n                          \"description\": \"The header assertion\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"type\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"textBody\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\",\n                                \"empty\",\n                                \"not_empty\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"string\",\n                              \"description\": \"The target value\"\n                            }\n                          },\n                          \"required\": [\"type\", \"compare\", \"target\"],\n                          \"description\": \"The text body assertion\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"type\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"dnsRecord\"]\n                            },\n                            \"key\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\", \"NS\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"required\": [\"type\", \"key\", \"compare\", \"target\"],\n                          \"description\": \"The DNS record assertion\"\n                        }\n                      ]\n                    },\n                    \"default\": [],\n                    \"description\": \"The assertions to run\"\n                  },\n                  \"active\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"If the monitor is active\"\n                  },\n                  \"public\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"If the monitor is public\"\n                  },\n                  \"degradedAfter\": {\n                    \"type\": \"number\",\n                    \"nullable\": true,\n                    \"description\": \"The time after the monitor is considered degraded in milliseconds\"\n                  },\n                  \"timeout\": {\n                    \"type\": \"number\",\n                    \"nullable\": true,\n                    \"default\": 45000,\n                    \"description\": \"The timeout of the request in milliseconds\"\n                  },\n                  \"retry\": {\n                    \"type\": \"number\",\n                    \"default\": 3,\n                    \"description\": \"The number of retries to attempt\"\n                  },\n                  \"followRedirects\": {\n                    \"type\": \"boolean\",\n                    \"default\": true,\n                    \"description\": \"If the monitor should follow redirects\"\n                  },\n                  \"jobType\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"http\", \"tcp\", \"imcp\", \"udp\", \"dns\", \"ssl\"],\n                    \"default\": \"http\",\n                    \"description\": \"The type of the monitor\"\n                  },\n                  \"openTelemetry\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"endpoint\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"default\": \"http://localhost:4317\",\n                        \"description\": \"The endpoint of the OpenTelemetry collector\"\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"default\": {},\n                        \"description\": \"The headers to send to the OpenTelemetry collector\"\n                      }\n                    },\n                    \"description\": \"The OpenTelemetry configuration\"\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Update a monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Monitor\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Delete a monitor\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The monitor was successfully deleted\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {}\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/http\": {\n      \"post\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Create a  http monitor\",\n        \"requestBody\": {\n          \"description\": \"The monitor to create\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"description\": \"Name of the monitor\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\"\n                  },\n                  \"retry\": {\n                    \"type\": \"number\",\n                    \"maximum\": 10,\n                    \"minimum\": 1,\n                    \"default\": 3,\n                    \"description\": \"Number of retries to attempt\",\n                    \"examples\": [1, 3, 5]\n                  },\n                  \"degradedAfter\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 30000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as degraded\",\n                    \"examples\": [30000]\n                  },\n                  \"timeout\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 45000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as timed out\",\n                    \"examples\": [45000]\n                  },\n                  \"frequency\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\"]\n                  },\n                  \"active\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is active\"\n                  },\n                  \"public\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is public\"\n                  },\n                  \"regions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"ams\",\n                        \"arn\",\n                        \"atl\",\n                        \"bog\",\n                        \"bom\",\n                        \"bos\",\n                        \"cdg\",\n                        \"den\",\n                        \"dfw\",\n                        \"ewr\",\n                        \"eze\",\n                        \"fra\",\n                        \"gdl\",\n                        \"gig\",\n                        \"gru\",\n                        \"hkg\",\n                        \"iad\",\n                        \"jnb\",\n                        \"lax\",\n                        \"lhr\",\n                        \"mad\",\n                        \"mia\",\n                        \"nrt\",\n                        \"ord\",\n                        \"otp\",\n                        \"phx\",\n                        \"qro\",\n                        \"scl\",\n                        \"sjc\",\n                        \"sea\",\n                        \"sin\",\n                        \"syd\",\n                        \"waw\",\n                        \"yul\",\n                        \"yyz\",\n                        \"koyeb_fra\",\n                        \"koyeb_was\",\n                        \"koyeb_sin\",\n                        \"koyeb_tyo\",\n                        \"koyeb_par\",\n                        \"koyeb_sfo\",\n                        \"railway_europe-west4-drams3a\",\n                        \"railway_us-east4-eqdc4a\",\n                        \"railway_asia-southeast1-eqsg3a\",\n                        \"railway_us-west2\"\n                      ]\n                    },\n                    \"default\": [],\n                    \"example\": [\"ams\"],\n                    \"description\": \"Where we should monitor it\"\n                  },\n                  \"openTelemetry\": {\n                    \"type\": \"object\",\n                    \"nullable\": true,\n                    \"properties\": {\n                      \"endpoint\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"description\": \"OTEL endpoint to send metrics to\",\n                        \"examples\": [\"https://otel.example.com\"]\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"description\": \"Headers to send with the OTEL request\",\n                        \"examples\": [\n                          {\n                            \"Content-Type\": \"application/json\"\n                          }\n                        ]\n                      }\n                    }\n                  },\n                  \"assertions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"oneOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"kind\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"statusCode\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ],\n                              \"description\": \"Comparison operator\",\n                              \"examples\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"number\",\n                              \"description\": \"Status code to assert\",\n                              \"examples\": [200, 404, 418, 500]\n                            }\n                          },\n                          \"required\": [\"kind\", \"compare\", \"target\"],\n                          \"examples\": [\n                            {\n                              \"kind\": \"statusCode\",\n                              \"compare\": \"eq\",\n                              \"target\": 200\n                            },\n                            {\n                              \"kind\": \"statusCode\",\n                              \"compare\": \"not_eq\",\n                              \"target\": 404\n                            },\n                            {\n                              \"kind\": \"statusCode\",\n                              \"compare\": \"gt\",\n                              \"target\": 300\n                            }\n                          ]\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"kind\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"header\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\",\n                                \"empty\",\n                                \"not_empty\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ],\n                              \"description\": \"Comparison operator\",\n                              \"examples\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"contains\",\n                                \"not_contains\"\n                              ]\n                            },\n                            \"key\": {\n                              \"type\": \"string\",\n                              \"description\": \"Header key to assert\",\n                              \"examples\": [\"Content-Type\", \"X-Request-ID\"]\n                            },\n                            \"target\": {\n                              \"type\": \"string\",\n                              \"description\": \"Header value to assert\",\n                              \"examples\": [\"application/json\", \"text/html\"]\n                            }\n                          },\n                          \"required\": [\"kind\", \"compare\", \"key\", \"target\"]\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"kind\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"textBody\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\",\n                                \"empty\",\n                                \"not_empty\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ],\n                              \"description\": \"Comparison operator\",\n                              \"examples\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"contains\",\n                                \"not_contains\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"string\",\n                              \"description\": \"Text body to assert\",\n                              \"examples\": [\"Hello, world!\", \"404 Not Found\"]\n                            }\n                          },\n                          \"required\": [\"kind\", \"compare\", \"target\"]\n                        }\n                      ]\n                    },\n                    \"description\": \"Assertions to run on the response\"\n                  },\n                  \"request\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"method\": {\n                        \"type\": \"string\",\n                        \"enum\": [\n                          \"GET\",\n                          \"POST\",\n                          \"HEAD\",\n                          \"PUT\",\n                          \"PATCH\",\n                          \"DELETE\",\n                          \"TRACE\",\n                          \"CONNECT\",\n                          \"OPTIONS\"\n                        ]\n                      },\n                      \"url\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"description\": \"URL to request\",\n                        \"examples\": [\n                          \"https://openstat.us\",\n                          \"https://www.openstatus.dev\"\n                        ]\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"description\": \"Headers to send with the request\",\n                        \"examples\": [\n                          {\n                            \"Content-Type\": \"application/json\"\n                          }\n                        ]\n                      },\n                      \"body\": {\n                        \"type\": \"string\",\n                        \"description\": \"Body to send with the request\",\n                        \"examples\": [\"{ \\\"key\\\": \\\"value\\\" }\", \"Hello World\"]\n                      }\n                    },\n                    \"required\": [\"method\", \"url\"],\n                    \"description\": \"The HTTP Request we are sending\"\n                  }\n                },\n                \"required\": [\"name\", \"frequency\", \"request\"],\n                \"title\": \"HTTP Monitor Schema\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Create a monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Monitor\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/tcp\": {\n      \"post\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Create a  tcp monitor\",\n        \"requestBody\": {\n          \"description\": \"The monitor to create\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"description\": \"Name of the monitor\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\"\n                  },\n                  \"retry\": {\n                    \"type\": \"number\",\n                    \"maximum\": 10,\n                    \"minimum\": 1,\n                    \"default\": 3,\n                    \"description\": \"Number of retries to attempt\",\n                    \"examples\": [1, 3, 5]\n                  },\n                  \"degradedAfter\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 30000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as degraded\",\n                    \"examples\": [30000]\n                  },\n                  \"timeout\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 45000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as timed out\",\n                    \"examples\": [45000]\n                  },\n                  \"frequency\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\"]\n                  },\n                  \"active\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is active\"\n                  },\n                  \"public\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is public\"\n                  },\n                  \"regions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"ams\",\n                        \"arn\",\n                        \"atl\",\n                        \"bog\",\n                        \"bom\",\n                        \"bos\",\n                        \"cdg\",\n                        \"den\",\n                        \"dfw\",\n                        \"ewr\",\n                        \"eze\",\n                        \"fra\",\n                        \"gdl\",\n                        \"gig\",\n                        \"gru\",\n                        \"hkg\",\n                        \"iad\",\n                        \"jnb\",\n                        \"lax\",\n                        \"lhr\",\n                        \"mad\",\n                        \"mia\",\n                        \"nrt\",\n                        \"ord\",\n                        \"otp\",\n                        \"phx\",\n                        \"qro\",\n                        \"scl\",\n                        \"sjc\",\n                        \"sea\",\n                        \"sin\",\n                        \"syd\",\n                        \"waw\",\n                        \"yul\",\n                        \"yyz\",\n                        \"koyeb_fra\",\n                        \"koyeb_was\",\n                        \"koyeb_sin\",\n                        \"koyeb_tyo\",\n                        \"koyeb_par\",\n                        \"koyeb_sfo\",\n                        \"railway_europe-west4-drams3a\",\n                        \"railway_us-east4-eqdc4a\",\n                        \"railway_asia-southeast1-eqsg3a\",\n                        \"railway_us-west2\"\n                      ]\n                    },\n                    \"default\": [],\n                    \"example\": [\"ams\"],\n                    \"description\": \"Where we should monitor it\"\n                  },\n                  \"openTelemetry\": {\n                    \"type\": \"object\",\n                    \"nullable\": true,\n                    \"properties\": {\n                      \"endpoint\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"description\": \"OTEL endpoint to send metrics to\",\n                        \"examples\": [\"https://otel.example.com\"]\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"description\": \"Headers to send with the OTEL request\",\n                        \"examples\": [\n                          {\n                            \"Content-Type\": \"application/json\"\n                          }\n                        ]\n                      }\n                    }\n                  },\n                  \"request\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"host\": {\n                        \"type\": \"string\",\n                        \"minLength\": 1,\n                        \"examples\": [\"example.com\", \"localhost\"],\n                        \"description\": \"Host to connect to\"\n                      },\n                      \"port\": {\n                        \"type\": \"number\",\n                        \"description\": \"Port to connect to\",\n                        \"examples\": [80, 443, 1337]\n                      }\n                    },\n                    \"required\": [\"host\", \"port\"],\n                    \"description\": \"The TCP Request we are sending\"\n                  }\n                },\n                \"required\": [\"name\", \"frequency\", \"request\"],\n                \"title\": \"TCP Monitor Schema\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Create a monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Monitor\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/dns\": {\n      \"post\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Create a  dns monitor\",\n        \"requestBody\": {\n          \"description\": \"The monitor to create\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"description\": \"Name of the monitor\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\"\n                  },\n                  \"retry\": {\n                    \"type\": \"number\",\n                    \"maximum\": 10,\n                    \"minimum\": 1,\n                    \"default\": 3,\n                    \"description\": \"Number of retries to attempt\",\n                    \"examples\": [1, 3, 5]\n                  },\n                  \"degradedAfter\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 30000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as degraded\",\n                    \"examples\": [30000]\n                  },\n                  \"timeout\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 45000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as timed out\",\n                    \"examples\": [45000]\n                  },\n                  \"frequency\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\"]\n                  },\n                  \"active\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is active\"\n                  },\n                  \"public\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is public\"\n                  },\n                  \"regions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"ams\",\n                        \"arn\",\n                        \"atl\",\n                        \"bog\",\n                        \"bom\",\n                        \"bos\",\n                        \"cdg\",\n                        \"den\",\n                        \"dfw\",\n                        \"ewr\",\n                        \"eze\",\n                        \"fra\",\n                        \"gdl\",\n                        \"gig\",\n                        \"gru\",\n                        \"hkg\",\n                        \"iad\",\n                        \"jnb\",\n                        \"lax\",\n                        \"lhr\",\n                        \"mad\",\n                        \"mia\",\n                        \"nrt\",\n                        \"ord\",\n                        \"otp\",\n                        \"phx\",\n                        \"qro\",\n                        \"scl\",\n                        \"sjc\",\n                        \"sea\",\n                        \"sin\",\n                        \"syd\",\n                        \"waw\",\n                        \"yul\",\n                        \"yyz\",\n                        \"koyeb_fra\",\n                        \"koyeb_was\",\n                        \"koyeb_sin\",\n                        \"koyeb_tyo\",\n                        \"koyeb_par\",\n                        \"koyeb_sfo\",\n                        \"railway_europe-west4-drams3a\",\n                        \"railway_us-east4-eqdc4a\",\n                        \"railway_asia-southeast1-eqsg3a\",\n                        \"railway_us-west2\"\n                      ]\n                    },\n                    \"default\": [],\n                    \"example\": [\"ams\"],\n                    \"description\": \"Where we should monitor it\"\n                  },\n                  \"openTelemetry\": {\n                    \"type\": \"object\",\n                    \"nullable\": true,\n                    \"properties\": {\n                      \"endpoint\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"description\": \"OTEL endpoint to send metrics to\",\n                        \"examples\": [\"https://otel.example.com\"]\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"description\": \"Headers to send with the OTEL request\",\n                        \"examples\": [\n                          {\n                            \"Content-Type\": \"application/json\"\n                          }\n                        ]\n                      }\n                    }\n                  },\n                  \"request\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"uri\": {\n                        \"type\": \"string\",\n                        \"description\": \"The DNS server to query\",\n                        \"examples\": [\"openstatus.dev\"]\n                      }\n                    },\n                    \"required\": [\"uri\"],\n                    \"description\": \"The DNS Request we are sending\"\n                  },\n                  \"assertions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"kind\": {\n                          \"type\": \"string\",\n                          \"enum\": [\"dnsRecord\"]\n                        },\n                        \"recordType\": {\n                          \"type\": \"string\",\n                          \"enum\": [\"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\"],\n                          \"description\": \"Type of DNS record to check\",\n                          \"examples\": [\"A\", \"CNAME\"]\n                        },\n                        \"compare\": {\n                          \"type\": \"string\",\n                          \"enum\": [\"contains\", \"not_contains\", \"eq\", \"not_eq\"],\n                          \"description\": \"Comparison operator\",\n                          \"examples\": [\n                            \"eq\",\n                            \"not_eq\",\n                            \"contains\",\n                            \"not_contains\"\n                          ]\n                        },\n                        \"target\": {\n                          \"type\": \"string\",\n                          \"description\": \"DNS record value to assert\",\n                          \"examples\": [\"example.com\"]\n                        }\n                      },\n                      \"required\": [\"kind\", \"recordType\", \"compare\", \"target\"]\n                    },\n                    \"description\": \"Assertions to run on the DNS response\"\n                  }\n                },\n                \"required\": [\"name\", \"frequency\", \"request\"],\n                \"title\": \"DNS Monitor Schema\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Create a monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Monitor\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/http/{id}\": {\n      \"put\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Update an HTTP monitor\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The monitor to update\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"description\": \"Name of the monitor\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\"\n                  },\n                  \"retry\": {\n                    \"type\": \"number\",\n                    \"maximum\": 10,\n                    \"minimum\": 1,\n                    \"default\": 3,\n                    \"description\": \"Number of retries to attempt\",\n                    \"examples\": [1, 3, 5]\n                  },\n                  \"degradedAfter\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 30000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as degraded\",\n                    \"examples\": [30000]\n                  },\n                  \"timeout\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 45000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as timed out\",\n                    \"examples\": [45000]\n                  },\n                  \"frequency\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\"]\n                  },\n                  \"active\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is active\"\n                  },\n                  \"public\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is public\"\n                  },\n                  \"regions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"ams\",\n                        \"arn\",\n                        \"atl\",\n                        \"bog\",\n                        \"bom\",\n                        \"bos\",\n                        \"cdg\",\n                        \"den\",\n                        \"dfw\",\n                        \"ewr\",\n                        \"eze\",\n                        \"fra\",\n                        \"gdl\",\n                        \"gig\",\n                        \"gru\",\n                        \"hkg\",\n                        \"iad\",\n                        \"jnb\",\n                        \"lax\",\n                        \"lhr\",\n                        \"mad\",\n                        \"mia\",\n                        \"nrt\",\n                        \"ord\",\n                        \"otp\",\n                        \"phx\",\n                        \"qro\",\n                        \"scl\",\n                        \"sjc\",\n                        \"sea\",\n                        \"sin\",\n                        \"syd\",\n                        \"waw\",\n                        \"yul\",\n                        \"yyz\",\n                        \"koyeb_fra\",\n                        \"koyeb_was\",\n                        \"koyeb_sin\",\n                        \"koyeb_tyo\",\n                        \"koyeb_par\",\n                        \"koyeb_sfo\",\n                        \"railway_europe-west4-drams3a\",\n                        \"railway_us-east4-eqdc4a\",\n                        \"railway_asia-southeast1-eqsg3a\",\n                        \"railway_us-west2\"\n                      ]\n                    },\n                    \"default\": [],\n                    \"example\": [\"ams\"],\n                    \"description\": \"Where we should monitor it\"\n                  },\n                  \"openTelemetry\": {\n                    \"type\": \"object\",\n                    \"nullable\": true,\n                    \"properties\": {\n                      \"endpoint\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"description\": \"OTEL endpoint to send metrics to\",\n                        \"examples\": [\"https://otel.example.com\"]\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"description\": \"Headers to send with the OTEL request\",\n                        \"examples\": [\n                          {\n                            \"Content-Type\": \"application/json\"\n                          }\n                        ]\n                      }\n                    }\n                  },\n                  \"assertions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"oneOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"kind\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"statusCode\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ],\n                              \"description\": \"Comparison operator\",\n                              \"examples\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"number\",\n                              \"description\": \"Status code to assert\",\n                              \"examples\": [200, 404, 418, 500]\n                            }\n                          },\n                          \"required\": [\"kind\", \"compare\", \"target\"],\n                          \"examples\": [\n                            {\n                              \"kind\": \"statusCode\",\n                              \"compare\": \"eq\",\n                              \"target\": 200\n                            },\n                            {\n                              \"kind\": \"statusCode\",\n                              \"compare\": \"not_eq\",\n                              \"target\": 404\n                            },\n                            {\n                              \"kind\": \"statusCode\",\n                              \"compare\": \"gt\",\n                              \"target\": 300\n                            }\n                          ]\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"kind\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"header\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\",\n                                \"empty\",\n                                \"not_empty\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ],\n                              \"description\": \"Comparison operator\",\n                              \"examples\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"contains\",\n                                \"not_contains\"\n                              ]\n                            },\n                            \"key\": {\n                              \"type\": \"string\",\n                              \"description\": \"Header key to assert\",\n                              \"examples\": [\"Content-Type\", \"X-Request-ID\"]\n                            },\n                            \"target\": {\n                              \"type\": \"string\",\n                              \"description\": \"Header value to assert\",\n                              \"examples\": [\"application/json\", \"text/html\"]\n                            }\n                          },\n                          \"required\": [\"kind\", \"compare\", \"key\", \"target\"]\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"kind\": {\n                              \"type\": \"string\",\n                              \"enum\": [\"textBody\"]\n                            },\n                            \"compare\": {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"contains\",\n                                \"not_contains\",\n                                \"eq\",\n                                \"not_eq\",\n                                \"empty\",\n                                \"not_empty\",\n                                \"gt\",\n                                \"gte\",\n                                \"lt\",\n                                \"lte\"\n                              ],\n                              \"description\": \"Comparison operator\",\n                              \"examples\": [\n                                \"eq\",\n                                \"not_eq\",\n                                \"contains\",\n                                \"not_contains\"\n                              ]\n                            },\n                            \"target\": {\n                              \"type\": \"string\",\n                              \"description\": \"Text body to assert\",\n                              \"examples\": [\"Hello, world!\", \"404 Not Found\"]\n                            }\n                          },\n                          \"required\": [\"kind\", \"compare\", \"target\"]\n                        }\n                      ]\n                    },\n                    \"description\": \"Assertions to run on the response\"\n                  },\n                  \"request\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"method\": {\n                        \"type\": \"string\",\n                        \"enum\": [\n                          \"GET\",\n                          \"POST\",\n                          \"HEAD\",\n                          \"PUT\",\n                          \"PATCH\",\n                          \"DELETE\",\n                          \"TRACE\",\n                          \"CONNECT\",\n                          \"OPTIONS\"\n                        ]\n                      },\n                      \"url\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"description\": \"URL to request\",\n                        \"examples\": [\n                          \"https://openstat.us\",\n                          \"https://www.openstatus.dev\"\n                        ]\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"description\": \"Headers to send with the request\",\n                        \"examples\": [\n                          {\n                            \"Content-Type\": \"application/json\"\n                          }\n                        ]\n                      },\n                      \"body\": {\n                        \"type\": \"string\",\n                        \"description\": \"Body to send with the request\",\n                        \"examples\": [\"{ \\\"key\\\": \\\"value\\\" }\", \"Hello World\"]\n                      }\n                    },\n                    \"required\": [\"method\", \"url\"],\n                    \"description\": \"The HTTP Request we are sending\"\n                  }\n                },\n                \"required\": [\"name\", \"frequency\", \"request\"],\n                \"title\": \"HTTP Monitor Schema\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Update a monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Monitor\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/tcp/{id}\": {\n      \"put\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Update an TCP monitor\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The monitor to update\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"description\": \"Name of the monitor\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\"\n                  },\n                  \"retry\": {\n                    \"type\": \"number\",\n                    \"maximum\": 10,\n                    \"minimum\": 1,\n                    \"default\": 3,\n                    \"description\": \"Number of retries to attempt\",\n                    \"examples\": [1, 3, 5]\n                  },\n                  \"degradedAfter\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 30000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as degraded\",\n                    \"examples\": [30000]\n                  },\n                  \"timeout\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 45000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as timed out\",\n                    \"examples\": [45000]\n                  },\n                  \"frequency\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\"]\n                  },\n                  \"active\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is active\"\n                  },\n                  \"public\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is public\"\n                  },\n                  \"regions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"ams\",\n                        \"arn\",\n                        \"atl\",\n                        \"bog\",\n                        \"bom\",\n                        \"bos\",\n                        \"cdg\",\n                        \"den\",\n                        \"dfw\",\n                        \"ewr\",\n                        \"eze\",\n                        \"fra\",\n                        \"gdl\",\n                        \"gig\",\n                        \"gru\",\n                        \"hkg\",\n                        \"iad\",\n                        \"jnb\",\n                        \"lax\",\n                        \"lhr\",\n                        \"mad\",\n                        \"mia\",\n                        \"nrt\",\n                        \"ord\",\n                        \"otp\",\n                        \"phx\",\n                        \"qro\",\n                        \"scl\",\n                        \"sjc\",\n                        \"sea\",\n                        \"sin\",\n                        \"syd\",\n                        \"waw\",\n                        \"yul\",\n                        \"yyz\",\n                        \"koyeb_fra\",\n                        \"koyeb_was\",\n                        \"koyeb_sin\",\n                        \"koyeb_tyo\",\n                        \"koyeb_par\",\n                        \"koyeb_sfo\",\n                        \"railway_europe-west4-drams3a\",\n                        \"railway_us-east4-eqdc4a\",\n                        \"railway_asia-southeast1-eqsg3a\",\n                        \"railway_us-west2\"\n                      ]\n                    },\n                    \"default\": [],\n                    \"example\": [\"ams\"],\n                    \"description\": \"Where we should monitor it\"\n                  },\n                  \"openTelemetry\": {\n                    \"type\": \"object\",\n                    \"nullable\": true,\n                    \"properties\": {\n                      \"endpoint\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"description\": \"OTEL endpoint to send metrics to\",\n                        \"examples\": [\"https://otel.example.com\"]\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"description\": \"Headers to send with the OTEL request\",\n                        \"examples\": [\n                          {\n                            \"Content-Type\": \"application/json\"\n                          }\n                        ]\n                      }\n                    }\n                  },\n                  \"request\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"host\": {\n                        \"type\": \"string\",\n                        \"minLength\": 1,\n                        \"examples\": [\"example.com\", \"localhost\"],\n                        \"description\": \"Host to connect to\"\n                      },\n                      \"port\": {\n                        \"type\": \"number\",\n                        \"description\": \"Port to connect to\",\n                        \"examples\": [80, 443, 1337]\n                      }\n                    },\n                    \"required\": [\"host\", \"port\"],\n                    \"description\": \"The TCP Request we are sending\"\n                  }\n                },\n                \"required\": [\"name\", \"frequency\", \"request\"],\n                \"title\": \"TCP Monitor Schema\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Update a monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Monitor\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/dns/{id}\": {\n      \"put\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Update an DNS monitor\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The monitor to update\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"description\": \"Name of the monitor\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\"\n                  },\n                  \"retry\": {\n                    \"type\": \"number\",\n                    \"maximum\": 10,\n                    \"minimum\": 1,\n                    \"default\": 3,\n                    \"description\": \"Number of retries to attempt\",\n                    \"examples\": [1, 3, 5]\n                  },\n                  \"degradedAfter\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 30000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as degraded\",\n                    \"examples\": [30000]\n                  },\n                  \"timeout\": {\n                    \"type\": \"number\",\n                    \"minimum\": 0,\n                    \"default\": 45000,\n                    \"description\": \"Time in milliseconds to wait before marking the request as timed out\",\n                    \"examples\": [45000]\n                  },\n                  \"frequency\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\"]\n                  },\n                  \"active\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is active\"\n                  },\n                  \"public\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether the monitor is public\"\n                  },\n                  \"regions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"ams\",\n                        \"arn\",\n                        \"atl\",\n                        \"bog\",\n                        \"bom\",\n                        \"bos\",\n                        \"cdg\",\n                        \"den\",\n                        \"dfw\",\n                        \"ewr\",\n                        \"eze\",\n                        \"fra\",\n                        \"gdl\",\n                        \"gig\",\n                        \"gru\",\n                        \"hkg\",\n                        \"iad\",\n                        \"jnb\",\n                        \"lax\",\n                        \"lhr\",\n                        \"mad\",\n                        \"mia\",\n                        \"nrt\",\n                        \"ord\",\n                        \"otp\",\n                        \"phx\",\n                        \"qro\",\n                        \"scl\",\n                        \"sjc\",\n                        \"sea\",\n                        \"sin\",\n                        \"syd\",\n                        \"waw\",\n                        \"yul\",\n                        \"yyz\",\n                        \"koyeb_fra\",\n                        \"koyeb_was\",\n                        \"koyeb_sin\",\n                        \"koyeb_tyo\",\n                        \"koyeb_par\",\n                        \"koyeb_sfo\",\n                        \"railway_europe-west4-drams3a\",\n                        \"railway_us-east4-eqdc4a\",\n                        \"railway_asia-southeast1-eqsg3a\",\n                        \"railway_us-west2\"\n                      ]\n                    },\n                    \"default\": [],\n                    \"example\": [\"ams\"],\n                    \"description\": \"Where we should monitor it\"\n                  },\n                  \"openTelemetry\": {\n                    \"type\": \"object\",\n                    \"nullable\": true,\n                    \"properties\": {\n                      \"endpoint\": {\n                        \"type\": \"string\",\n                        \"format\": \"uri\",\n                        \"description\": \"OTEL endpoint to send metrics to\",\n                        \"examples\": [\"https://otel.example.com\"]\n                      },\n                      \"headers\": {\n                        \"type\": \"object\",\n                        \"additionalProperties\": {\n                          \"type\": \"string\"\n                        },\n                        \"description\": \"Headers to send with the OTEL request\",\n                        \"examples\": [\n                          {\n                            \"Content-Type\": \"application/json\"\n                          }\n                        ]\n                      }\n                    }\n                  },\n                  \"request\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"uri\": {\n                        \"type\": \"string\",\n                        \"description\": \"The DNS server to query\",\n                        \"examples\": [\"openstatus.dev\"]\n                      }\n                    },\n                    \"required\": [\"uri\"],\n                    \"description\": \"The DNS Request we are sending\"\n                  },\n                  \"assertions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"kind\": {\n                          \"type\": \"string\",\n                          \"enum\": [\"dnsRecord\"]\n                        },\n                        \"recordType\": {\n                          \"type\": \"string\",\n                          \"enum\": [\"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\"],\n                          \"description\": \"Type of DNS record to check\",\n                          \"examples\": [\"A\", \"CNAME\"]\n                        },\n                        \"compare\": {\n                          \"type\": \"string\",\n                          \"enum\": [\"contains\", \"not_contains\", \"eq\", \"not_eq\"],\n                          \"description\": \"Comparison operator\",\n                          \"examples\": [\n                            \"eq\",\n                            \"not_eq\",\n                            \"contains\",\n                            \"not_contains\"\n                          ]\n                        },\n                        \"target\": {\n                          \"type\": \"string\",\n                          \"description\": \"DNS record value to assert\",\n                          \"examples\": [\"example.com\"]\n                        }\n                      },\n                      \"required\": [\"kind\", \"recordType\", \"compare\", \"target\"]\n                    },\n                    \"description\": \"Assertions to run on the DNS response\"\n                  }\n                },\n                \"required\": [\"name\", \"frequency\", \"request\"],\n                \"title\": \"DNS Monitor Schema\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Update a monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Monitor\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/{id}/summary\": {\n      \"get\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Get a monitor summary\",\n        \"description\": \"Get a monitor summary of the last 45 days of data to be used within a status page\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"All the historical metrics\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"data\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"ok\": {\n                            \"type\": \"integer\",\n                            \"description\": \"The number of ok responses (defined by the assertions - or by default status code 200)\"\n                          },\n                          \"count\": {\n                            \"type\": \"integer\",\n                            \"description\": \"The total number of request\"\n                          },\n                          \"day\": {\n                            \"type\": \"string\",\n                            \"nullable\": true,\n                            \"format\": \"date\",\n                            \"description\": \"The date of the daily stat in ISO8601 format\"\n                          }\n                        },\n                        \"required\": [\"ok\", \"count\", \"day\"]\n                      }\n                    }\n                  },\n                  \"required\": [\"data\"]\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/{id}/trigger\": {\n      \"post\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Create a monitor trigger\",\n        \"description\": \"Trigger a monitor check without waiting the result\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Returns a result id that can be used to get the result of your trigger\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"resultId\": {\n                      \"type\": \"number\",\n                      \"description\": \"the id of your check result\"\n                    }\n                  },\n                  \"required\": [\"resultId\"]\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/{id}/result/{resultId}\": {\n      \"get\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Get a monitor result\",\n        \"description\": \"**WARNING:** This works only for HTTP monitors. We will add support for other types of monitors soon.\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"description\": \"The id of the result\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the result\",\n            \"name\": \"resultId\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"All the metrics for the result id from the monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"latency\": {\n                        \"type\": \"integer\"\n                      },\n                      \"statusCode\": {\n                        \"type\": \"integer\",\n                        \"nullable\": true,\n                        \"default\": null\n                      },\n                      \"monitorId\": {\n                        \"type\": \"string\",\n                        \"default\": \"\"\n                      },\n                      \"url\": {\n                        \"type\": \"string\"\n                      },\n                      \"error\": {\n                        \"type\": \"boolean\",\n                        \"nullable\": true,\n                        \"default\": false\n                      },\n                      \"region\": {\n                        \"type\": \"string\",\n                        \"enum\": [\n                          \"ams\",\n                          \"arn\",\n                          \"atl\",\n                          \"bog\",\n                          \"bom\",\n                          \"bos\",\n                          \"cdg\",\n                          \"den\",\n                          \"dfw\",\n                          \"ewr\",\n                          \"eze\",\n                          \"fra\",\n                          \"gdl\",\n                          \"gig\",\n                          \"gru\",\n                          \"hkg\",\n                          \"iad\",\n                          \"jnb\",\n                          \"lax\",\n                          \"lhr\",\n                          \"mad\",\n                          \"mia\",\n                          \"nrt\",\n                          \"ord\",\n                          \"otp\",\n                          \"phx\",\n                          \"qro\",\n                          \"scl\",\n                          \"sjc\",\n                          \"sea\",\n                          \"sin\",\n                          \"syd\",\n                          \"waw\",\n                          \"yul\",\n                          \"yyz\",\n                          \"koyeb_fra\",\n                          \"koyeb_was\",\n                          \"koyeb_sin\",\n                          \"koyeb_tyo\",\n                          \"koyeb_par\",\n                          \"koyeb_sfo\",\n                          \"railway_europe-west4-drams3a\",\n                          \"railway_us-east4-eqdc4a\",\n                          \"railway_asia-southeast1-eqsg3a\",\n                          \"railway_us-west2\"\n                        ]\n                      },\n                      \"timestamp\": {\n                        \"type\": \"integer\"\n                      },\n                      \"message\": {\n                        \"type\": \"string\",\n                        \"nullable\": true\n                      },\n                      \"timing\": {\n                        \"type\": \"object\",\n                        \"nullable\": true,\n                        \"properties\": {\n                          \"dnsStart\": {\n                            \"type\": \"number\"\n                          },\n                          \"dnsDone\": {\n                            \"type\": \"number\"\n                          },\n                          \"connectStart\": {\n                            \"type\": \"number\"\n                          },\n                          \"connectDone\": {\n                            \"type\": \"number\"\n                          },\n                          \"tlsHandshakeStart\": {\n                            \"type\": \"number\"\n                          },\n                          \"tlsHandshakeDone\": {\n                            \"type\": \"number\"\n                          },\n                          \"firstByteStart\": {\n                            \"type\": \"number\"\n                          },\n                          \"firstByteDone\": {\n                            \"type\": \"number\"\n                          },\n                          \"transferStart\": {\n                            \"type\": \"number\"\n                          },\n                          \"transferDone\": {\n                            \"type\": \"number\"\n                          }\n                        },\n                        \"required\": [\n                          \"dnsStart\",\n                          \"dnsDone\",\n                          \"connectStart\",\n                          \"connectDone\",\n                          \"tlsHandshakeStart\",\n                          \"tlsHandshakeDone\",\n                          \"firstByteStart\",\n                          \"firstByteDone\",\n                          \"transferStart\",\n                          \"transferDone\"\n                        ]\n                      }\n                    },\n                    \"required\": [\"latency\", \"region\"]\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/monitor/{id}/run\": {\n      \"post\": {\n        \"tags\": [\"monitor\"],\n        \"summary\": \"Create a monitor run\",\n        \"description\": \"Run a synthetic check for a specific monitor. It will take all configs into account.\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the monitor\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the monitor\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"nullable\": true,\n              \"default\": false,\n              \"description\": \"Don't wait for the result\"\n            },\n            \"required\": false,\n            \"description\": \"Don't wait for the result\",\n            \"name\": \"no-wait\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"All the historical metrics\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"oneOf\": [\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"jobType\": {\n                            \"type\": \"string\",\n                            \"enum\": [\"http\"]\n                          },\n                          \"status\": {\n                            \"type\": \"number\"\n                          },\n                          \"latency\": {\n                            \"type\": \"number\"\n                          },\n                          \"region\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"ams\",\n                              \"arn\",\n                              \"atl\",\n                              \"bog\",\n                              \"bom\",\n                              \"bos\",\n                              \"cdg\",\n                              \"den\",\n                              \"dfw\",\n                              \"ewr\",\n                              \"eze\",\n                              \"fra\",\n                              \"gdl\",\n                              \"gig\",\n                              \"gru\",\n                              \"hkg\",\n                              \"iad\",\n                              \"jnb\",\n                              \"lax\",\n                              \"lhr\",\n                              \"mad\",\n                              \"mia\",\n                              \"nrt\",\n                              \"ord\",\n                              \"otp\",\n                              \"phx\",\n                              \"qro\",\n                              \"scl\",\n                              \"sjc\",\n                              \"sea\",\n                              \"sin\",\n                              \"syd\",\n                              \"waw\",\n                              \"yul\",\n                              \"yyz\",\n                              \"koyeb_fra\",\n                              \"koyeb_was\",\n                              \"koyeb_sin\",\n                              \"koyeb_tyo\",\n                              \"koyeb_par\",\n                              \"koyeb_sfo\",\n                              \"railway_europe-west4-drams3a\",\n                              \"railway_us-east4-eqdc4a\",\n                              \"railway_asia-southeast1-eqsg3a\",\n                              \"railway_us-west2\"\n                            ]\n                          },\n                          \"timestamp\": {\n                            \"type\": \"number\"\n                          },\n                          \"timing\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"dnsStart\": {\n                                \"type\": \"number\"\n                              },\n                              \"dnsDone\": {\n                                \"type\": \"number\"\n                              },\n                              \"connectStart\": {\n                                \"type\": \"number\"\n                              },\n                              \"connectDone\": {\n                                \"type\": \"number\"\n                              },\n                              \"tlsHandshakeStart\": {\n                                \"type\": \"number\"\n                              },\n                              \"tlsHandshakeDone\": {\n                                \"type\": \"number\"\n                              },\n                              \"firstByteStart\": {\n                                \"type\": \"number\"\n                              },\n                              \"firstByteDone\": {\n                                \"type\": \"number\"\n                              },\n                              \"transferStart\": {\n                                \"type\": \"number\"\n                              },\n                              \"transferDone\": {\n                                \"type\": \"number\"\n                              }\n                            },\n                            \"required\": [\n                              \"dnsStart\",\n                              \"dnsDone\",\n                              \"connectStart\",\n                              \"connectDone\",\n                              \"tlsHandshakeStart\",\n                              \"tlsHandshakeDone\",\n                              \"firstByteStart\",\n                              \"firstByteDone\",\n                              \"transferStart\",\n                              \"transferDone\"\n                            ]\n                          },\n                          \"body\": {\n                            \"type\": \"string\",\n                            \"nullable\": true\n                          },\n                          \"error\": {\n                            \"type\": \"string\",\n                            \"nullable\": true\n                          }\n                        },\n                        \"required\": [\n                          \"jobType\",\n                          \"status\",\n                          \"latency\",\n                          \"region\",\n                          \"timestamp\",\n                          \"timing\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"jobType\": {\n                            \"type\": \"string\",\n                            \"enum\": [\"tcp\"]\n                          },\n                          \"latency\": {\n                            \"type\": \"number\"\n                          },\n                          \"region\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"ams\",\n                              \"arn\",\n                              \"atl\",\n                              \"bog\",\n                              \"bom\",\n                              \"bos\",\n                              \"cdg\",\n                              \"den\",\n                              \"dfw\",\n                              \"ewr\",\n                              \"eze\",\n                              \"fra\",\n                              \"gdl\",\n                              \"gig\",\n                              \"gru\",\n                              \"hkg\",\n                              \"iad\",\n                              \"jnb\",\n                              \"lax\",\n                              \"lhr\",\n                              \"mad\",\n                              \"mia\",\n                              \"nrt\",\n                              \"ord\",\n                              \"otp\",\n                              \"phx\",\n                              \"qro\",\n                              \"scl\",\n                              \"sjc\",\n                              \"sea\",\n                              \"sin\",\n                              \"syd\",\n                              \"waw\",\n                              \"yul\",\n                              \"yyz\",\n                              \"koyeb_fra\",\n                              \"koyeb_was\",\n                              \"koyeb_sin\",\n                              \"koyeb_tyo\",\n                              \"koyeb_par\",\n                              \"koyeb_sfo\",\n                              \"railway_europe-west4-drams3a\",\n                              \"railway_us-east4-eqdc4a\",\n                              \"railway_asia-southeast1-eqsg3a\",\n                              \"railway_us-west2\"\n                            ]\n                          },\n                          \"timestamp\": {\n                            \"type\": \"number\"\n                          },\n                          \"timing\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"tcpStart\": {\n                                \"type\": \"number\"\n                              },\n                              \"tcpDone\": {\n                                \"type\": \"number\"\n                              }\n                            },\n                            \"required\": [\"tcpStart\", \"tcpDone\"]\n                          },\n                          \"error\": {\n                            \"type\": \"number\",\n                            \"nullable\": true\n                          },\n                          \"errorMessage\": {\n                            \"type\": \"string\",\n                            \"nullable\": true\n                          }\n                        },\n                        \"required\": [\n                          \"jobType\",\n                          \"latency\",\n                          \"region\",\n                          \"timestamp\",\n                          \"timing\"\n                        ]\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/page/{id}\": {\n      \"get\": {\n        \"tags\": [\"page\"],\n        \"summary\": \"Get a status page\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the page\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the page\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get an Status page\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Page\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\"page\"],\n        \"summary\": \"Update a status page\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the page\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the page\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The monitor to update\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"title\": {\n                    \"type\": \"string\",\n                    \"description\": \"The title of the page\",\n                    \"example\": \"My Page\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\",\n                    \"description\": \"The description of the page\",\n                    \"example\": \"My awesome status page\"\n                  },\n                  \"slug\": {\n                    \"type\": \"string\",\n                    \"description\": \"The slug of the page\",\n                    \"example\": \"my-page\"\n                  },\n                  \"customDomain\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"description\": \"The custom domain of the page. To be configured within the dashboard.\",\n                    \"example\": \"status.acme.com\"\n                  },\n                  \"icon\": {\n                    \"anyOf\": [\n                      {\n                        \"type\": \"string\",\n                        \"format\": \"uri\"\n                      },\n                      {\n                        \"type\": \"string\",\n                        \"enum\": [\"\"]\n                      },\n                      {\n                        \"nullable\": true\n                      }\n                    ],\n                    \"description\": \"The icon of the page\",\n                    \"example\": \"https://example.com/icon.png\"\n                  },\n                  \"passwordProtected\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Deprecated in favor of `accessType`. Used to set the password protection type. Returns true if `accessType` is set to 'password' and false otherwise.\",\n                    \"example\": true,\n                    \"deprecated\": true\n                  },\n                  \"accessType\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"public\", \"password\", \"email-domain\"],\n                    \"default\": \"public\",\n                    \"description\": \"The access type of the page\",\n                    \"example\": \"public\"\n                  },\n                  \"password\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"description\": \"Your password to protect the page from the public\",\n                    \"example\": \"hidden-password\"\n                  },\n                  \"authEmailDomains\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\"\n                    },\n                    \"description\": \"The email domains of the page\",\n                    \"example\": [\"example.com\", \"example.org\"]\n                  },\n                  \"showMonitorValues\": {\n                    \"type\": \"boolean\",\n                    \"nullable\": true,\n                    \"default\": true,\n                    \"description\": \"Displays the total and failed request numbers for each monitor. Deprecated and will be removed in the future in favor for `configuration` property.\",\n                    \"example\": true,\n                    \"deprecated\": true\n                  },\n                  \"monitors\": {\n                    \"anyOf\": [\n                      {\n                        \"type\": \"array\",\n                        \"items\": {\n                          \"type\": \"number\"\n                        },\n                        \"description\": \"The monitors of the page as an array of ids. We recommend using the object format to include the order.\",\n                        \"deprecated\": true,\n                        \"example\": [1, 2]\n                      },\n                      {\n                        \"type\": \"array\",\n                        \"items\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"monitorId\": {\n                              \"type\": \"number\"\n                            },\n                            \"order\": {\n                              \"type\": \"number\"\n                            }\n                          },\n                          \"required\": [\"monitorId\", \"order\"]\n                        },\n                        \"description\": \"The monitor as object allowing to pass id and order\",\n                        \"example\": [\n                          {\n                            \"monitorId\": 1,\n                            \"order\": 0\n                          },\n                          {\n                            \"monitorId\": 2,\n                            \"order\": 1\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get an Status page\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Page\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/page\": {\n      \"get\": {\n        \"tags\": [\"page\"],\n        \"summary\": \"List all status pages\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A list of your status pages\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/Page\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\"page\"],\n        \"summary\": \"Create a status page\",\n        \"requestBody\": {\n          \"description\": \"The status page to create\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"title\": {\n                    \"type\": \"string\",\n                    \"description\": \"The title of the page\",\n                    \"example\": \"My Page\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\",\n                    \"description\": \"The description of the page\",\n                    \"example\": \"My awesome status page\"\n                  },\n                  \"slug\": {\n                    \"type\": \"string\",\n                    \"description\": \"The slug of the page\",\n                    \"example\": \"my-page\"\n                  },\n                  \"customDomain\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"description\": \"The custom domain of the page. To be configured within the dashboard.\",\n                    \"example\": \"status.acme.com\"\n                  },\n                  \"icon\": {\n                    \"anyOf\": [\n                      {\n                        \"type\": \"string\",\n                        \"format\": \"uri\"\n                      },\n                      {\n                        \"type\": \"string\",\n                        \"enum\": [\"\"]\n                      },\n                      {\n                        \"nullable\": true\n                      }\n                    ],\n                    \"description\": \"The icon of the page\",\n                    \"example\": \"https://example.com/icon.png\"\n                  },\n                  \"passwordProtected\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Deprecated in favor of `accessType`. Used to set the password protection type. Returns true if `accessType` is set to 'password' and false otherwise.\",\n                    \"example\": true,\n                    \"deprecated\": true\n                  },\n                  \"accessType\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"public\", \"password\", \"email-domain\"],\n                    \"default\": \"public\",\n                    \"description\": \"The access type of the page\",\n                    \"example\": \"public\"\n                  },\n                  \"password\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"description\": \"Your password to protect the page from the public\",\n                    \"example\": \"hidden-password\"\n                  },\n                  \"authEmailDomains\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\"\n                    },\n                    \"description\": \"The email domains of the page\",\n                    \"example\": [\"example.com\", \"example.org\"]\n                  },\n                  \"showMonitorValues\": {\n                    \"type\": \"boolean\",\n                    \"nullable\": true,\n                    \"default\": true,\n                    \"description\": \"Displays the total and failed request numbers for each monitor. Deprecated and will be removed in the future in favor for `configuration` property.\",\n                    \"example\": true,\n                    \"deprecated\": true\n                  },\n                  \"monitors\": {\n                    \"anyOf\": [\n                      {\n                        \"type\": \"array\",\n                        \"items\": {\n                          \"type\": \"number\"\n                        },\n                        \"description\": \"The monitors of the page as an array of ids. We recommend using the object format to include the order.\",\n                        \"deprecated\": true,\n                        \"example\": [1, 2]\n                      },\n                      {\n                        \"type\": \"array\",\n                        \"items\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"monitorId\": {\n                              \"type\": \"number\"\n                            },\n                            \"order\": {\n                              \"type\": \"number\"\n                            }\n                          },\n                          \"required\": [\"monitorId\", \"order\"]\n                        },\n                        \"description\": \"The monitor as object allowing to pass id and order\",\n                        \"example\": [\n                          {\n                            \"monitorId\": 1,\n                            \"order\": 0\n                          },\n                          {\n                            \"monitorId\": 2,\n                            \"order\": 1\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                },\n                \"required\": [\"title\", \"description\", \"slug\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get an Status page\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Page\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/status_report\": {\n      \"get\": {\n        \"tags\": [\"status_report\"],\n        \"summary\": \"List all status reports\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get all status reports\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/StatusReport\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\"status_report\"],\n        \"summary\": \"Create a status report\",\n        \"requestBody\": {\n          \"description\": \"The status report to create\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"title\": {\n                    \"type\": \"string\",\n                    \"example\": \"Documenso\",\n                    \"description\": \"The title of the status report\"\n                  },\n                  \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"investigating\",\n                      \"identified\",\n                      \"monitoring\",\n                      \"resolved\"\n                    ],\n                    \"description\": \"The current status of the report\"\n                  },\n                  \"monitorIds\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"number\"\n                    },\n                    \"default\": [],\n                    \"description\": \"Ids of the monitors the status report.\"\n                  },\n                  \"pageId\": {\n                    \"type\": \"number\",\n                    \"description\": \"The id of the page this status report belongs to\"\n                  },\n                  \"date\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"format\": \"date\",\n                    \"default\": \"2026-02-12T22:01:46.114Z\",\n                    \"description\": \"The date of the report in ISO8601 format, defaults to now\"\n                  },\n                  \"message\": {\n                    \"type\": \"string\",\n                    \"description\": \"The message of the current status of incident\"\n                  }\n                },\n                \"required\": [\"title\", \"status\", \"pageId\", \"message\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The created status report\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/StatusReport\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/status_report/{id}\": {\n      \"delete\": {\n        \"tags\": [\"status_report\"],\n        \"summary\": \"Delete a status report\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the status report\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the status report\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Status report deleted\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {}\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"get\": {\n        \"tags\": [\"status_report\"],\n        \"summary\": \"Get a status report\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the status report\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the status report\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get all status reports\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/StatusReport\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/status_report/{id}/update\": {\n      \"post\": {\n        \"tags\": [\"status_report\"],\n        \"summary\": \"Create a status report update\",\n        \"deprecated\": true,\n        \"description\": \"Preferably use [`/status-report-updates`](#tag/status_report_update/POST/status_report_update) instead.\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the status report\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the status report\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"the status report update\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"investigating\",\n                      \"identified\",\n                      \"monitoring\",\n                      \"resolved\"\n                    ],\n                    \"description\": \"The status of the update\"\n                  },\n                  \"date\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"format\": \"date\",\n                    \"default\": \"2026-02-12T22:01:46.114Z\",\n                    \"description\": \"The date of the update in ISO8601 format\"\n                  },\n                  \"message\": {\n                    \"type\": \"string\",\n                    \"minLength\": 1,\n                    \"description\": \"The message of the update\"\n                  }\n                },\n                \"required\": [\"status\", \"message\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Status report updated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/StatusReport\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/status_report_update/{id}\": {\n      \"get\": {\n        \"tags\": [\"status_report_update\"],\n        \"summary\": \"Get a status report update\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the update\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the update\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get a status report update\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/StatusReportUpdate\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/status_report_update\": {\n      \"post\": {\n        \"tags\": [\"status_report_update\"],\n        \"summary\": \"Create a status report update\",\n        \"requestBody\": {\n          \"description\": \"The status report update to create\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"investigating\",\n                      \"identified\",\n                      \"monitoring\",\n                      \"resolved\"\n                    ],\n                    \"description\": \"The status of the update\"\n                  },\n                  \"date\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"format\": \"date\",\n                    \"default\": \"2026-02-12T22:01:46.114Z\",\n                    \"description\": \"The date of the update in ISO8601 format\"\n                  },\n                  \"message\": {\n                    \"type\": \"string\",\n                    \"minLength\": 1,\n                    \"description\": \"The message of the update\"\n                  },\n                  \"statusReportId\": {\n                    \"type\": \"number\",\n                    \"description\": \"The id of the status report\"\n                  }\n                },\n                \"required\": [\"status\", \"message\", \"statusReportId\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The created status report update\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/StatusReportUpdate\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/incident\": {\n      \"get\": {\n        \"tags\": [\"incident\"],\n        \"summary\": \"List all incidents\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get all incidents\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/Incident\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/incident/{id}\": {\n      \"get\": {\n        \"tags\": [\"incident\"],\n        \"summary\": \"Get an incident\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"pattern\": \"^\\\\d+$\",\n              \"description\": \"The id of the Incident\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the Incident\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get an incident\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Incident\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\"incident\"],\n        \"summary\": \"Update an incident\",\n        \"description\": \"Acknowledge or resolve an incident\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"pattern\": \"^\\\\d+$\",\n              \"description\": \"The id of the Incident\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the Incident\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The incident to update\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"acknowledgedAt\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"format\": \"date\",\n                    \"description\": \"The date the incident was acknowledged\"\n                  },\n                  \"resolvedAt\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"format\": \"date\",\n                    \"description\": \"The date the incident was resolved\"\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Update a monitor\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Incident\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/maintenance\": {\n      \"get\": {\n        \"tags\": [\"maintenance\"],\n        \"summary\": \"List all maintenances\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get all maintenances\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/Maintenance\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\"maintenance\"],\n        \"summary\": \"Create a maintenance\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"title\": {\n                    \"type\": \"string\",\n                    \"description\": \"The title of the maintenance\",\n                    \"example\": \"Database Upgrade\"\n                  },\n                  \"message\": {\n                    \"type\": \"string\",\n                    \"description\": \"The message describing the maintenance\",\n                    \"example\": \"Upgrading database to improve performance\"\n                  },\n                  \"from\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"format\": \"date\",\n                    \"description\": \"When the maintenance starts\"\n                  },\n                  \"to\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"format\": \"date\",\n                    \"description\": \"When the maintenance ends\"\n                  },\n                  \"monitorIds\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"number\"\n                    },\n                    \"default\": [],\n                    \"description\": \"IDs of affected monitors\"\n                  },\n                  \"pageId\": {\n                    \"type\": \"number\",\n                    \"description\": \"The id of the status page this maintenance belongs to\"\n                  }\n                },\n                \"required\": [\"title\", \"message\", \"from\", \"to\", \"pageId\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Create a maintenance\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Maintenance\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/maintenance/{id}\": {\n      \"get\": {\n        \"tags\": [\"maintenance\"],\n        \"summary\": \"Get a maintenance\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"pattern\": \"^\\\\d+$\",\n              \"description\": \"The id of the maintenance\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the maintenance\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get a maintenance\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Maintenance\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\"maintenance\"],\n        \"summary\": \"Update a maintenance\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"pattern\": \"^\\\\d+$\",\n              \"description\": \"The id of the maintenance\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the maintenance\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"title\": {\n                    \"type\": \"string\",\n                    \"description\": \"The title of the maintenance\",\n                    \"example\": \"Database Upgrade\"\n                  },\n                  \"message\": {\n                    \"type\": \"string\",\n                    \"description\": \"The message describing the maintenance\",\n                    \"example\": \"Upgrading database to improve performance\"\n                  },\n                  \"from\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"format\": \"date\",\n                    \"description\": \"When the maintenance starts\"\n                  },\n                  \"to\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"format\": \"date\",\n                    \"description\": \"When the maintenance ends\"\n                  },\n                  \"monitorIds\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"number\"\n                    },\n                    \"default\": [],\n                    \"description\": \"IDs of affected monitors\"\n                  },\n                  \"pageId\": {\n                    \"type\": \"number\",\n                    \"description\": \"The id of the status page this maintenance belongs to\"\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Update a maintenance\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Maintenance\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/notification\": {\n      \"get\": {\n        \"tags\": [\"notification\"],\n        \"summary\": \"List all notifications\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get all your workspace notification\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/Notification\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\"notification\"],\n        \"summary\": \"Create a notification\",\n        \"requestBody\": {\n          \"description\": \"The notification to create\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"description\": \"The name of the notification\",\n                    \"example\": \"OpenStatus Discord\"\n                  },\n                  \"provider\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"discord\",\n                      \"email\",\n                      \"google-chat\",\n                      \"grafana-oncall\",\n                      \"ntfy\",\n                      \"pagerduty\",\n                      \"opsgenie\",\n                      \"slack\",\n                      \"sms\",\n                      \"telegram\",\n                      \"webhook\",\n                      \"whatsapp\"\n                    ],\n                    \"description\": \"The provider of the notification\",\n                    \"example\": \"discord\"\n                  },\n                  \"payload\": {\n                    \"anyOf\": [\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"discord\": {\n                            \"type\": \"string\",\n                            \"format\": \"uri\"\n                          }\n                        },\n                        \"required\": [\"discord\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"email\": {\n                            \"type\": \"string\",\n                            \"format\": \"email\"\n                          }\n                        },\n                        \"required\": [\"email\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"grafana-oncall\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"webhookUrl\": {\n                                \"type\": \"string\",\n                                \"format\": \"uri\"\n                              }\n                            },\n                            \"required\": [\"webhookUrl\"]\n                          }\n                        },\n                        \"required\": [\"grafana-oncall\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"ntfy\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"topic\": {\n                                \"type\": \"string\",\n                                \"default\": \"\"\n                              },\n                              \"serverUrl\": {\n                                \"type\": \"string\",\n                                \"default\": \"https://ntfy.sh\"\n                              },\n                              \"token\": {\n                                \"type\": \"string\"\n                              }\n                            }\n                          }\n                        },\n                        \"required\": [\"ntfy\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"opsgenie\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"apiKey\": {\n                                \"type\": \"string\"\n                              },\n                              \"region\": {\n                                \"type\": \"string\",\n                                \"enum\": [\"us\", \"eu\"]\n                              }\n                            },\n                            \"required\": [\"apiKey\", \"region\"]\n                          }\n                        },\n                        \"required\": [\"opsgenie\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"pagerduty\": {\n                            \"type\": \"string\"\n                          }\n                        },\n                        \"required\": [\"pagerduty\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"sms\": {\n                            \"type\": \"string\",\n                            \"pattern\": \"^([+]?[\\\\s0-9]+)?(\\\\d{3}|[(]?[0-9]+[)])?([-]?[\\\\s]?[0-9])+$\"\n                          }\n                        },\n                        \"required\": [\"sms\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"telegram\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"chatId\": {\n                                \"type\": \"string\"\n                              }\n                            },\n                            \"required\": [\"chatId\"]\n                          }\n                        },\n                        \"required\": [\"telegram\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"slack\": {\n                            \"type\": \"string\",\n                            \"format\": \"uri\"\n                          }\n                        },\n                        \"required\": [\"slack\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"webhook\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"endpoint\": {\n                                \"type\": \"string\",\n                                \"format\": \"uri\"\n                              },\n                              \"headers\": {\n                                \"type\": \"array\",\n                                \"items\": {\n                                  \"type\": \"object\",\n                                  \"properties\": {\n                                    \"key\": {\n                                      \"type\": \"string\"\n                                    },\n                                    \"value\": {\n                                      \"type\": \"string\"\n                                    }\n                                  },\n                                  \"required\": [\"key\", \"value\"]\n                                }\n                              }\n                            },\n                            \"required\": [\"endpoint\"]\n                          }\n                        },\n                        \"required\": [\"webhook\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"whatsapp\": {\n                            \"type\": \"string\",\n                            \"pattern\": \"^([+]?[\\\\s0-9]+)?(\\\\d{3}|[(]?[0-9]+[)])?([-]?[\\\\s]?[0-9])+$\"\n                          }\n                        },\n                        \"required\": [\"whatsapp\"]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"google-chat\": {\n                            \"type\": \"string\",\n                            \"format\": \"uri\"\n                          }\n                        },\n                        \"required\": [\"google-chat\"]\n                      }\n                    ],\n                    \"description\": \"The data of the notification\"\n                  },\n                  \"monitors\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"number\"\n                    },\n                    \"description\": \"The monitors that the notification is linked to\",\n                    \"example\": [1, 2]\n                  }\n                },\n                \"required\": [\"name\", \"provider\", \"payload\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Return the created notification\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/notification/{id}\": {\n      \"get\": {\n        \"tags\": [\"notification\"],\n        \"summary\": \"Get a notification\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the notification\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the notification\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get an Status page\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/page_subscriber/{id}/update\": {\n      \"post\": {\n        \"tags\": [\"page_subscriber\"],\n        \"summary\": \"Subscribe to a status page\",\n        \"description\": \"Add a subscriber to a status page\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"description\": \"The id of the page\",\n              \"example\": \"1\"\n            },\n            \"required\": true,\n            \"description\": \"The id of the page\",\n            \"name\": \"id\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The subscriber payload\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"email\": {\n                    \"type\": \"string\",\n                    \"format\": \"email\",\n                    \"description\": \"The email of the subscriber\"\n                  }\n                },\n                \"required\": [\"email\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The user has been subscribed\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PageSubscriber\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/check/http\": {\n      \"post\": {\n        \"tags\": [\"check\"],\n        \"summary\": \"Run a single check\",\n        \"requestBody\": {\n          \"description\": \"The run request to create\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"url\": {\n                    \"type\": \"string\",\n                    \"example\": \"https://www.documenso.co\",\n                    \"description\": \"The url to monitor\"\n                  },\n                  \"body\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"default\": \"\",\n                    \"example\": \"Hello World\",\n                    \"description\": \"The body\"\n                  },\n                  \"headers\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"key\": {\n                          \"type\": \"string\"\n                        },\n                        \"value\": {\n                          \"type\": \"string\"\n                        }\n                      },\n                      \"required\": [\"key\", \"value\"]\n                    },\n                    \"default\": [],\n                    \"description\": \"The headers of your request\",\n                    \"example\": [\n                      {\n                        \"key\": \"x-apikey\",\n                        \"value\": \"supersecrettoken\"\n                      }\n                    ]\n                  },\n                  \"method\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"GET\",\n                      \"POST\",\n                      \"HEAD\",\n                      \"PUT\",\n                      \"PATCH\",\n                      \"DELETE\",\n                      \"TRACE\",\n                      \"CONNECT\",\n                      \"OPTIONS\"\n                    ],\n                    \"example\": \"GET\"\n                  },\n                  \"regions\": {\n                    \"type\": \"array\",\n                    \"nullable\": true,\n                    \"items\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"ams\",\n                        \"arn\",\n                        \"atl\",\n                        \"bog\",\n                        \"bom\",\n                        \"bos\",\n                        \"cdg\",\n                        \"den\",\n                        \"dfw\",\n                        \"ewr\",\n                        \"eze\",\n                        \"fra\",\n                        \"gdl\",\n                        \"gig\",\n                        \"gru\",\n                        \"hkg\",\n                        \"iad\",\n                        \"jnb\",\n                        \"lax\",\n                        \"lhr\",\n                        \"mad\",\n                        \"mia\",\n                        \"nrt\",\n                        \"ord\",\n                        \"otp\",\n                        \"phx\",\n                        \"qro\",\n                        \"scl\",\n                        \"sjc\",\n                        \"sea\",\n                        \"sin\",\n                        \"syd\",\n                        \"waw\",\n                        \"yul\",\n                        \"yyz\",\n                        \"koyeb_fra\",\n                        \"koyeb_was\",\n                        \"koyeb_sin\",\n                        \"koyeb_tyo\",\n                        \"koyeb_par\",\n                        \"koyeb_sfo\",\n                        \"railway_europe-west4-drams3a\",\n                        \"railway_us-east4-eqdc4a\",\n                        \"railway_asia-southeast1-eqsg3a\",\n                        \"railway_us-west2\"\n                      ]\n                    },\n                    \"default\": [],\n                    \"example\": [\"ams\"],\n                    \"description\": \"Where we should monitor it\"\n                  },\n                  \"runCount\": {\n                    \"type\": \"number\",\n                    \"maximum\": 5,\n                    \"default\": 1,\n                    \"description\": \"The number of times to run the check\"\n                  },\n                  \"aggregated\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"Whether to aggregate the results or not\"\n                  }\n                },\n                \"required\": [\"url\", \"method\"],\n                \"description\": \"The check request\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Return a run result\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"integer\",\n                      \"description\": \"The id of the check\"\n                    },\n                    \"raw\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"dnsStart\": {\n                            \"type\": \"number\",\n                            \"description\": \"DNS timestamp start time in UTC \"\n                          },\n                          \"dnsDone\": {\n                            \"type\": \"number\",\n                            \"description\": \"DNS timestamp end time in UTC \"\n                          },\n                          \"connectStart\": {\n                            \"type\": \"number\",\n                            \"description\": \"Connect timestamp start time in UTC \"\n                          },\n                          \"connectDone\": {\n                            \"type\": \"number\",\n                            \"description\": \"Connect timestamp end time in UTC \"\n                          },\n                          \"tlsHandshakeStart\": {\n                            \"type\": \"number\",\n                            \"description\": \"TLS handshake timestamp start time in UTC \"\n                          },\n                          \"tlsHandshakeDone\": {\n                            \"type\": \"number\",\n                            \"description\": \"TLS handshake timestamp end time in UTC \"\n                          },\n                          \"firstByteStart\": {\n                            \"type\": \"number\",\n                            \"description\": \"First byte timestamp start time in UTC \"\n                          },\n                          \"firstByteDone\": {\n                            \"type\": \"number\",\n                            \"description\": \"First byte timestamp end time in UTC \"\n                          },\n                          \"transferStart\": {\n                            \"type\": \"number\",\n                            \"description\": \"Transfer timestamp start time in UTC \"\n                          },\n                          \"transferDone\": {\n                            \"type\": \"number\",\n                            \"description\": \"Transfer timestamp end time in UTC \"\n                          }\n                        },\n                        \"required\": [\n                          \"dnsStart\",\n                          \"dnsDone\",\n                          \"connectStart\",\n                          \"connectDone\",\n                          \"tlsHandshakeStart\",\n                          \"tlsHandshakeDone\",\n                          \"firstByteStart\",\n                          \"firstByteDone\",\n                          \"transferStart\",\n                          \"transferDone\"\n                        ]\n                      },\n                      \"description\": \"The raw data of the check\"\n                    },\n                    \"response\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"timestamp\": {\n                          \"type\": \"number\",\n                          \"description\": \"The timestamp of the response in UTC\"\n                        },\n                        \"status\": {\n                          \"type\": \"number\",\n                          \"description\": \"The status code of the response\"\n                        },\n                        \"latency\": {\n                          \"type\": \"number\",\n                          \"description\": \"The latency of the response\"\n                        },\n                        \"body\": {\n                          \"type\": \"string\",\n                          \"description\": \"The body of the response\"\n                        },\n                        \"headers\": {\n                          \"type\": \"object\",\n                          \"additionalProperties\": {\n                            \"type\": \"string\"\n                          },\n                          \"description\": \"The headers of the response\"\n                        },\n                        \"timing\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"dnsStart\": {\n                              \"type\": \"number\",\n                              \"description\": \"DNS timestamp start time in UTC \"\n                            },\n                            \"dnsDone\": {\n                              \"type\": \"number\",\n                              \"description\": \"DNS timestamp end time in UTC \"\n                            },\n                            \"connectStart\": {\n                              \"type\": \"number\",\n                              \"description\": \"Connect timestamp start time in UTC \"\n                            },\n                            \"connectDone\": {\n                              \"type\": \"number\",\n                              \"description\": \"Connect timestamp end time in UTC \"\n                            },\n                            \"tlsHandshakeStart\": {\n                              \"type\": \"number\",\n                              \"description\": \"TLS handshake timestamp start time in UTC \"\n                            },\n                            \"tlsHandshakeDone\": {\n                              \"type\": \"number\",\n                              \"description\": \"TLS handshake timestamp end time in UTC \"\n                            },\n                            \"firstByteStart\": {\n                              \"type\": \"number\",\n                              \"description\": \"First byte timestamp start time in UTC \"\n                            },\n                            \"firstByteDone\": {\n                              \"type\": \"number\",\n                              \"description\": \"First byte timestamp end time in UTC \"\n                            },\n                            \"transferStart\": {\n                              \"type\": \"number\",\n                              \"description\": \"Transfer timestamp start time in UTC \"\n                            },\n                            \"transferDone\": {\n                              \"type\": \"number\",\n                              \"description\": \"Transfer timestamp end time in UTC \"\n                            }\n                          },\n                          \"required\": [\n                            \"dnsStart\",\n                            \"dnsDone\",\n                            \"connectStart\",\n                            \"connectDone\",\n                            \"tlsHandshakeStart\",\n                            \"tlsHandshakeDone\",\n                            \"firstByteStart\",\n                            \"firstByteDone\",\n                            \"transferStart\",\n                            \"transferDone\"\n                          ],\n                          \"description\": \"The timing metrics of the response\"\n                        },\n                        \"aggregated\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"dns\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"p50\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 50th percentile\"\n                                },\n                                \"p75\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 75th percentile\"\n                                },\n                                \"p95\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 95th percentile\"\n                                },\n                                \"p99\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 99th percentile\"\n                                },\n                                \"min\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The minimum value\"\n                                },\n                                \"max\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The maximum value\"\n                                }\n                              },\n                              \"required\": [\n                                \"p50\",\n                                \"p75\",\n                                \"p95\",\n                                \"p99\",\n                                \"min\",\n                                \"max\"\n                              ],\n                              \"description\": \"The aggregated DNS timing of the check\"\n                            },\n                            \"connection\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"p50\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 50th percentile\"\n                                },\n                                \"p75\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 75th percentile\"\n                                },\n                                \"p95\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 95th percentile\"\n                                },\n                                \"p99\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 99th percentile\"\n                                },\n                                \"min\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The minimum value\"\n                                },\n                                \"max\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The maximum value\"\n                                }\n                              },\n                              \"required\": [\n                                \"p50\",\n                                \"p75\",\n                                \"p95\",\n                                \"p99\",\n                                \"min\",\n                                \"max\"\n                              ],\n                              \"description\": \"The aggregated connection timing of the check\"\n                            },\n                            \"tls\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"p50\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 50th percentile\"\n                                },\n                                \"p75\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 75th percentile\"\n                                },\n                                \"p95\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 95th percentile\"\n                                },\n                                \"p99\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 99th percentile\"\n                                },\n                                \"min\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The minimum value\"\n                                },\n                                \"max\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The maximum value\"\n                                }\n                              },\n                              \"required\": [\n                                \"p50\",\n                                \"p75\",\n                                \"p95\",\n                                \"p99\",\n                                \"min\",\n                                \"max\"\n                              ],\n                              \"description\": \"The aggregated tls timing of the check\"\n                            },\n                            \"firstByte\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"p50\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 50th percentile\"\n                                },\n                                \"p75\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 75th percentile\"\n                                },\n                                \"p95\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 95th percentile\"\n                                },\n                                \"p99\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 99th percentile\"\n                                },\n                                \"min\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The minimum value\"\n                                },\n                                \"max\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The maximum value\"\n                                }\n                              },\n                              \"required\": [\n                                \"p50\",\n                                \"p75\",\n                                \"p95\",\n                                \"p99\",\n                                \"min\",\n                                \"max\"\n                              ],\n                              \"description\": \"The aggregated first byte timing of the check\"\n                            },\n                            \"transfer\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"p50\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 50th percentile\"\n                                },\n                                \"p75\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 75th percentile\"\n                                },\n                                \"p95\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 95th percentile\"\n                                },\n                                \"p99\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 99th percentile\"\n                                },\n                                \"min\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The minimum value\"\n                                },\n                                \"max\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The maximum value\"\n                                }\n                              },\n                              \"required\": [\n                                \"p50\",\n                                \"p75\",\n                                \"p95\",\n                                \"p99\",\n                                \"min\",\n                                \"max\"\n                              ],\n                              \"description\": \"The aggregated transfer timing of the check\"\n                            },\n                            \"latency\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"p50\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 50th percentile\"\n                                },\n                                \"p75\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 75th percentile\"\n                                },\n                                \"p95\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 95th percentile\"\n                                },\n                                \"p99\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The 99th percentile\"\n                                },\n                                \"min\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The minimum value\"\n                                },\n                                \"max\": {\n                                  \"type\": \"number\",\n                                  \"description\": \"The maximum value\"\n                                }\n                              },\n                              \"required\": [\n                                \"p50\",\n                                \"p75\",\n                                \"p95\",\n                                \"p99\",\n                                \"min\",\n                                \"max\"\n                              ],\n                              \"description\": \"The aggregated latency timing of the check\"\n                            }\n                          },\n                          \"required\": [\n                            \"dns\",\n                            \"connection\",\n                            \"tls\",\n                            \"firstByte\",\n                            \"transfer\",\n                            \"latency\"\n                          ],\n                          \"description\": \"The aggregated data dns timing of the check\"\n                        },\n                        \"region\": {\n                          \"type\": \"string\",\n                          \"description\": \"The region where the check ran\"\n                        }\n                      },\n                      \"required\": [\n                        \"timestamp\",\n                        \"status\",\n                        \"latency\",\n                        \"timing\",\n                        \"region\"\n                      ],\n                      \"description\": \"The last response of the check\"\n                    },\n                    \"aggregated\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"dns\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"p50\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 50th percentile\"\n                            },\n                            \"p75\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 75th percentile\"\n                            },\n                            \"p95\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 95th percentile\"\n                            },\n                            \"p99\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 99th percentile\"\n                            },\n                            \"min\": {\n                              \"type\": \"number\",\n                              \"description\": \"The minimum value\"\n                            },\n                            \"max\": {\n                              \"type\": \"number\",\n                              \"description\": \"The maximum value\"\n                            }\n                          },\n                          \"required\": [\n                            \"p50\",\n                            \"p75\",\n                            \"p95\",\n                            \"p99\",\n                            \"min\",\n                            \"max\"\n                          ],\n                          \"description\": \"The aggregated data of the check\"\n                        },\n                        \"connect\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"p50\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 50th percentile\"\n                            },\n                            \"p75\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 75th percentile\"\n                            },\n                            \"p95\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 95th percentile\"\n                            },\n                            \"p99\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 99th percentile\"\n                            },\n                            \"min\": {\n                              \"type\": \"number\",\n                              \"description\": \"The minimum value\"\n                            },\n                            \"max\": {\n                              \"type\": \"number\",\n                              \"description\": \"The maximum value\"\n                            }\n                          },\n                          \"required\": [\n                            \"p50\",\n                            \"p75\",\n                            \"p95\",\n                            \"p99\",\n                            \"min\",\n                            \"max\"\n                          ],\n                          \"description\": \"The aggregated data of the check\"\n                        },\n                        \"tls\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"p50\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 50th percentile\"\n                            },\n                            \"p75\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 75th percentile\"\n                            },\n                            \"p95\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 95th percentile\"\n                            },\n                            \"p99\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 99th percentile\"\n                            },\n                            \"min\": {\n                              \"type\": \"number\",\n                              \"description\": \"The minimum value\"\n                            },\n                            \"max\": {\n                              \"type\": \"number\",\n                              \"description\": \"The maximum value\"\n                            }\n                          },\n                          \"required\": [\n                            \"p50\",\n                            \"p75\",\n                            \"p95\",\n                            \"p99\",\n                            \"min\",\n                            \"max\"\n                          ],\n                          \"description\": \"The aggregated data of the check\"\n                        },\n                        \"firstByte\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"p50\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 50th percentile\"\n                            },\n                            \"p75\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 75th percentile\"\n                            },\n                            \"p95\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 95th percentile\"\n                            },\n                            \"p99\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 99th percentile\"\n                            },\n                            \"min\": {\n                              \"type\": \"number\",\n                              \"description\": \"The minimum value\"\n                            },\n                            \"max\": {\n                              \"type\": \"number\",\n                              \"description\": \"The maximum value\"\n                            }\n                          },\n                          \"required\": [\n                            \"p50\",\n                            \"p75\",\n                            \"p95\",\n                            \"p99\",\n                            \"min\",\n                            \"max\"\n                          ],\n                          \"description\": \"The aggregated data of the check\"\n                        },\n                        \"transfer\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"p50\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 50th percentile\"\n                            },\n                            \"p75\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 75th percentile\"\n                            },\n                            \"p95\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 95th percentile\"\n                            },\n                            \"p99\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 99th percentile\"\n                            },\n                            \"min\": {\n                              \"type\": \"number\",\n                              \"description\": \"The minimum value\"\n                            },\n                            \"max\": {\n                              \"type\": \"number\",\n                              \"description\": \"The maximum value\"\n                            }\n                          },\n                          \"required\": [\n                            \"p50\",\n                            \"p75\",\n                            \"p95\",\n                            \"p99\",\n                            \"min\",\n                            \"max\"\n                          ],\n                          \"description\": \"The aggregated data of the check\"\n                        },\n                        \"latency\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"p50\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 50th percentile\"\n                            },\n                            \"p75\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 75th percentile\"\n                            },\n                            \"p95\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 95th percentile\"\n                            },\n                            \"p99\": {\n                              \"type\": \"number\",\n                              \"description\": \"The 99th percentile\"\n                            },\n                            \"min\": {\n                              \"type\": \"number\",\n                              \"description\": \"The minimum value\"\n                            },\n                            \"max\": {\n                              \"type\": \"number\",\n                              \"description\": \"The maximum value\"\n                            }\n                          },\n                          \"required\": [\n                            \"p50\",\n                            \"p75\",\n                            \"p95\",\n                            \"p99\",\n                            \"min\",\n                            \"max\"\n                          ],\n                          \"description\": \"The aggregated data of the check\"\n                        }\n                      },\n                      \"required\": [\n                        \"dns\",\n                        \"connect\",\n                        \"tls\",\n                        \"firstByte\",\n                        \"transfer\",\n                        \"latency\"\n                      ],\n                      \"description\": \"The aggregated data of the check\"\n                    }\n                  },\n                  \"required\": [\"id\", \"raw\", \"response\"]\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/whoami\": {\n      \"get\": {\n        \"tags\": [\"whoami\"],\n        \"summary\": \"Get your informations\",\n        \"description\": \"Get the current workspace information attached to the API key.\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The current workspace information with the limits\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Workspace\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrBadRequest\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"The client must authenticate itself to get the requested response.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrUnauthorized\"\n                }\n              }\n            }\n          },\n          \"402\": {\n            \"description\": \"A higher pricing plan is required to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrPaymentRequired\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"The client does not have the necessary permissions to access the resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrForbidden\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"The server can't find the requested resource.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrNotFound\"\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"The request could not be completed due to a conflict mainly due to unique constraints.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrConflict\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"The server has encountered a situation it doesn't know how to handle.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ErrInternalServerError\"\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/server/static/openapi.yaml",
    "content": "openapi: 3.1.0\ninfo:\n  description: OpenStatus is a open-source status page platform with global uptime monitoring. The OpenStatus API allows you to interact with the OpenStatus platform programmatically. To get started you need to create an account on https://www.openstatus.dev/ and create an api token in your settings.\n  title: OpenStatus API\n  version: v2.0.0\n  contact:\n    email: ping@openstatus.dev\n    url: https://www.openstatus.dev\nexternalDocs:\n  description: OpenStatus Documentation\n  url: https://docs.openstatus.dev\ncomponents:\n  securitySchemes:\n    ApiKeyAuth:\n      type: apiKey\n      in: header\n      name: x-openstatus-key\n  schemas:\n    connect.error:\n      type: object\n      properties:\n        code:\n          type: string\n          examples:\n            - not_found\n          enum:\n            - canceled\n            - unknown\n            - invalid_argument\n            - deadline_exceeded\n            - not_found\n            - already_exists\n            - permission_denied\n            - resource_exhausted\n            - failed_precondition\n            - aborted\n            - out_of_range\n            - unimplemented\n            - internal\n            - unavailable\n            - data_loss\n            - unauthenticated\n          description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].\n        message:\n          type: string\n          description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.\n        details:\n          type: array\n          items:\n            $ref: '#/components/schemas/connect.error_details.Any'\n          description: A list of messages that carry the error details. There is no limit on the number of messages.\n      title: Connect Error\n      additionalProperties: true\n      description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation'\n    connect.error_details.Any:\n      type: object\n      properties:\n        type:\n          type: string\n          description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.'\n        value:\n          type: string\n          format: binary\n          description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field.\n        debug:\n          oneOf:\n            - type: object\n              title: Any\n              additionalProperties: true\n              description: Detailed error information.\n          discriminator:\n            propertyName: type\n          title: Debug\n          description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic.\n      additionalProperties: true\n      description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details.\n    openstatus.health.v1.CheckRequest:\n      type: object\n      properties:\n        service:\n          type: string\n          title: service\n          description: Optional service name to check. If empty, checks overall service health.\n      title: CheckRequest\n      additionalProperties: false\n      description: CheckRequest is the request message for health checks.\n    openstatus.health.v1.CheckResponse:\n      type: object\n      properties:\n        status:\n          title: status\n          description: The serving status of the service.\n          $ref: '#/components/schemas/openstatus.health.v1.CheckResponse.ServingStatus'\n      title: CheckResponse\n      additionalProperties: false\n      description: CheckResponse is the response message for health checks.\n    openstatus.health.v1.CheckResponse.ServingStatus:\n      type: string\n      title: ServingStatus\n      enum:\n        - SERVING_STATUS_UNSPECIFIED\n        - SERVING_STATUS_SERVING\n        - SERVING_STATUS_NOT_SERVING\n      description: ServingStatus represents the health status of the service.\n    openstatus.maintenance.v1.CreateMaintenanceRequest:\n      type: object\n      properties:\n        title:\n          type: string\n          examples:\n            - Database Migration\n          title: title\n          maxLength: 256\n          minLength: 1\n          description: Title of the maintenance (required, 1-256 characters).\n        message:\n          type: string\n          title: message\n          minLength: 1\n          description: Message describing the maintenance (required).\n        from:\n          type: string\n          examples:\n            - \"2024-03-01T02:00:00Z\"\n          title: from\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: Start time of the maintenance window (RFC 3339 format, required).\n        to:\n          type: string\n          examples:\n            - \"2024-03-01T06:00:00Z\"\n          title: to\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: End time of the maintenance window (RFC 3339 format, required).\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: Page ID to associate with this maintenance (required).\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: Page component IDs to associate with this maintenance (optional).\n        notify:\n          type:\n            - boolean\n            - \"null\"\n          title: notify\n          description: Whether to notify subscribers about this maintenance (optional, defaults to false).\n      title: CreateMaintenanceRequest\n      additionalProperties: false\n      description: CreateMaintenanceRequest is the request to create a new maintenance window.\n    openstatus.maintenance.v1.CreateMaintenanceResponse:\n      type: object\n      properties:\n        maintenance:\n          title: maintenance\n          description: The created maintenance.\n          $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n      title: CreateMaintenanceResponse\n      additionalProperties: false\n      description: CreateMaintenanceResponse is the response after creating a maintenance window.\n    openstatus.maintenance.v1.DeleteMaintenanceRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the maintenance to delete (required).\n      title: DeleteMaintenanceRequest\n      additionalProperties: false\n      description: DeleteMaintenanceRequest is the request to delete a maintenance window.\n    openstatus.maintenance.v1.DeleteMaintenanceResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteMaintenanceResponse\n      additionalProperties: false\n      description: DeleteMaintenanceResponse is the response after deleting a maintenance window.\n    openstatus.maintenance.v1.GetMaintenanceRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the maintenance to retrieve (required).\n      title: GetMaintenanceRequest\n      additionalProperties: false\n      description: GetMaintenanceRequest is the request to get a maintenance window by ID.\n    openstatus.maintenance.v1.GetMaintenanceResponse:\n      type: object\n      properties:\n        maintenance:\n          title: maintenance\n          description: The requested maintenance.\n          $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n      title: GetMaintenanceResponse\n      additionalProperties: false\n      description: GetMaintenanceResponse is the response containing the maintenance window.\n    openstatus.maintenance.v1.ListMaintenancesRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of maintenances to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of maintenances to skip for pagination (defaults to 0).\n        pageId:\n          type:\n            - string\n            - \"null\"\n          title: page_id\n          description: Filter by page ID (optional).\n      title: ListMaintenancesRequest\n      additionalProperties: false\n      description: ListMaintenancesRequest is the request to list maintenance windows.\n    openstatus.maintenance.v1.ListMaintenancesResponse:\n      type: object\n      properties:\n        maintenances:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n          title: maintenances\n          description: List of maintenances.\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of maintenances matching the filter.\n      title: ListMaintenancesResponse\n      additionalProperties: false\n      description: ListMaintenancesResponse is the response containing maintenance window summaries.\n    openstatus.maintenance.v1.Maintenance:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the maintenance.\n        title:\n          type: string\n          title: title\n          description: Title of the maintenance.\n        message:\n          type: string\n          title: message\n          description: Message describing the maintenance.\n        from:\n          type: string\n          title: from\n          description: Start time of the maintenance window (RFC 3339 format).\n        to:\n          type: string\n          title: to\n          description: End time of the maintenance window (RFC 3339 format).\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the page this maintenance is associated with.\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: IDs of affected page components.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the maintenance was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the maintenance was last updated (RFC 3339 format).\n      title: Maintenance\n      additionalProperties: false\n      description: Maintenance represents a maintenance window with full details.\n    openstatus.maintenance.v1.MaintenanceSummary:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the maintenance.\n        title:\n          type: string\n          title: title\n          description: Title of the maintenance.\n        message:\n          type: string\n          title: message\n          description: Message describing the maintenance.\n        from:\n          type: string\n          title: from\n          description: Start time of the maintenance window (RFC 3339 format).\n        to:\n          type: string\n          title: to\n          description: End time of the maintenance window (RFC 3339 format).\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the page this maintenance is associated with.\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: IDs of affected page components.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the maintenance was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the maintenance was last updated (RFC 3339 format).\n      title: MaintenanceSummary\n      additionalProperties: false\n      description: MaintenanceSummary represents metadata for a maintenance window (used in list responses).\n    openstatus.maintenance.v1.UpdateMaintenanceRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the maintenance to update (required).\n        title:\n          type:\n            - string\n            - \"null\"\n          title: title\n          maxLength: 256\n          minLength: 1\n          description: New title for the maintenance (optional).\n        message:\n          type:\n            - string\n            - \"null\"\n          title: message\n          description: New message for the maintenance (optional).\n        from:\n          type:\n            - string\n            - \"null\"\n          title: from\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: New start time (RFC 3339 format, optional).\n        to:\n          type:\n            - string\n            - \"null\"\n          title: to\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: New end time (RFC 3339 format, optional).\n        pageId:\n          type:\n            - string\n            - \"null\"\n          title: page_id\n          description: New page ID (optional).\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: New list of page component IDs (optional, replaces existing list).\n      title: UpdateMaintenanceRequest\n      additionalProperties: false\n      description: UpdateMaintenanceRequest is the request to update a maintenance window.\n    openstatus.maintenance.v1.UpdateMaintenanceResponse:\n      type: object\n      properties:\n        maintenance:\n          title: maintenance\n          description: The updated maintenance.\n          $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n      title: UpdateMaintenanceResponse\n      additionalProperties: false\n      description: UpdateMaintenanceResponse is the response after updating a maintenance window.\n    openstatus.monitor.v1.BodyAssertion:\n      type: object\n      properties:\n        target:\n          type: string\n          title: target\n          description: Target value to compare against.\n        comparator:\n          not:\n            enum:\n              - STRING_COMPARATOR_UNSPECIFIED\n          title: comparator\n          description: Comparison operation (required, must not be UNSPECIFIED).\n          $ref: '#/components/schemas/openstatus.monitor.v1.StringComparator'\n      title: BodyAssertion\n      additionalProperties: false\n      description: BodyAssertion defines an assertion for response body content.\n    openstatus.monitor.v1.CreateDNSMonitorRequest:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: Monitor configuration (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n      title: CreateDNSMonitorRequest\n      required:\n        - monitor\n      additionalProperties: false\n      description: CreateDNSMonitorRequest is the request to create a new DNS monitor.\n    openstatus.monitor.v1.CreateDNSMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The created monitor with assigned ID.\n          $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n      title: CreateDNSMonitorResponse\n      additionalProperties: false\n      description: CreateDNSMonitorResponse is the response after creating a DNS monitor.\n    openstatus.monitor.v1.CreateHTTPMonitorRequest:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: Monitor configuration (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n      title: CreateHTTPMonitorRequest\n      required:\n        - monitor\n      additionalProperties: false\n      description: CreateHTTPMonitorRequest is the request to create a new HTTP monitor.\n    openstatus.monitor.v1.CreateHTTPMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The created monitor with assigned ID.\n          $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n      title: CreateHTTPMonitorResponse\n      additionalProperties: false\n      description: CreateHTTPMonitorResponse is the response after creating an HTTP monitor.\n    openstatus.monitor.v1.CreateTCPMonitorRequest:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: Monitor configuration (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n      title: CreateTCPMonitorRequest\n      required:\n        - monitor\n      additionalProperties: false\n      description: CreateTCPMonitorRequest is the request to create a new TCP monitor.\n    openstatus.monitor.v1.CreateTCPMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The created monitor with assigned ID.\n          $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n      title: CreateTCPMonitorResponse\n      additionalProperties: false\n      description: CreateTCPMonitorResponse is the response after creating a TCP monitor.\n    openstatus.monitor.v1.DNSMonitor:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the monitor (output only for create requests).\n        name:\n          type: string\n          examples:\n            - DNS Resolution Check\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Name of the monitor (required, max 256 characters).\n        uri:\n          type: string\n          examples:\n            - example.com\n          title: uri\n          maxLength: 2048\n          minLength: 1\n          description: Domain to resolve (required, max 2048 characters).\n        periodicity:\n          not:\n            enum:\n              - PERIODICITY_UNSPECIFIED\n          title: periodicity\n          description: Check periodicity (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n        timeout:\n          type:\n            - integer\n            - string\n          title: timeout\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Timeout in milliseconds (0-120000, defaults to 45000).\n        degradedAt:\n          type:\n            - integer\n            - string\n            - \"null\"\n          title: degraded_at\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n        retry:\n          type:\n            - integer\n            - string\n          title: retry\n          maximum: 10\n          minimum: 0\n          format: int64\n          description: Number of retry attempts (0-10, defaults to 3).\n        recordAssertions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.RecordAssertion'\n          title: record_assertions\n          maxItems: 10\n          description: DNS record assertions for validation.\n        description:\n          type: string\n          title: description\n          maxLength: 1024\n          description: Description of the monitor (optional).\n        active:\n          type: boolean\n          title: active\n          description: Whether the monitor is active (defaults to false).\n        public:\n          type: boolean\n          title: public\n          description: Whether the monitor is publicly visible (defaults to false).\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          maxItems: 28\n          description: Geographic regions to run checks from.\n        openTelemetry:\n          title: open_telemetry\n          description: OpenTelemetry configuration for exporting metrics.\n          $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n        status:\n          title: status\n          description: Current operational status of the monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n      title: DNSMonitor\n      additionalProperties: false\n      description: DNSMonitor defines the configuration for a DNS monitor.\n    openstatus.monitor.v1.DeleteMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to delete (required).\n      title: DeleteMonitorRequest\n      additionalProperties: false\n      description: DeleteMonitorRequest is the request to delete a monitor.\n    openstatus.monitor.v1.DeleteMonitorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteMonitorResponse\n      additionalProperties: false\n      description: DeleteMonitorResponse is the response after deleting a monitor.\n    openstatus.monitor.v1.GetMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to retrieve (required).\n      title: GetMonitorRequest\n      additionalProperties: false\n      description: GetMonitorRequest is the request to get a single monitor by ID.\n    openstatus.monitor.v1.GetMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The monitor configuration (one of HTTP, TCP, or DNS).\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorConfig'\n      title: GetMonitorResponse\n      additionalProperties: false\n      description: GetMonitorResponse is the response containing the monitor.\n    openstatus.monitor.v1.GetMonitorStatusRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to get status for (required).\n      title: GetMonitorStatusRequest\n      additionalProperties: false\n      description: GetMonitorStatusRequest is the request to get the status of all regions for a monitor.\n    openstatus.monitor.v1.GetMonitorStatusResponse:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Monitor ID.\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.RegionStatus'\n          title: regions\n          description: Status for each region.\n      title: GetMonitorStatusResponse\n      additionalProperties: false\n      description: GetMonitorStatusResponse is the response containing the status of all regions for a monitor.\n    openstatus.monitor.v1.GetMonitorSummaryRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to get summary for (required).\n        timeRange:\n          title: time_range\n          description: Time range for metrics aggregation (defaults to 1 day if unspecified).\n          $ref: '#/components/schemas/openstatus.monitor.v1.TimeRange'\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          maxItems: 28\n          description: Optional filter by regions. If empty, returns metrics for all regions.\n      title: GetMonitorSummaryRequest\n      additionalProperties: false\n      description: GetMonitorSummaryRequest is the request to get aggregated metrics for a monitor.\n    openstatus.monitor.v1.GetMonitorSummaryResponse:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Monitor ID.\n        lastPingAt:\n          type: string\n          title: last_ping_at\n          description: Timestamp of the last check in RFC 3339 format.\n        totalSuccessful:\n          type:\n            - integer\n            - string\n          title: total_successful\n          format: int64\n          description: Total number of successful requests.\n        totalDegraded:\n          type:\n            - integer\n            - string\n          title: total_degraded\n          format: int64\n          description: Total number of degraded requests.\n        totalFailed:\n          type:\n            - integer\n            - string\n          title: total_failed\n          format: int64\n          description: Total number of failed requests.\n        p50:\n          type:\n            - integer\n            - string\n          title: p50\n          format: int64\n          description: 50th percentile (median) latency in milliseconds.\n        p75:\n          type:\n            - integer\n            - string\n          title: p75\n          format: int64\n          description: 75th percentile latency in milliseconds.\n        p90:\n          type:\n            - integer\n            - string\n          title: p90\n          format: int64\n          description: 90th percentile latency in milliseconds.\n        p95:\n          type:\n            - integer\n            - string\n          title: p95\n          format: int64\n          description: 95th percentile latency in milliseconds.\n        p99:\n          type:\n            - integer\n            - string\n          title: p99\n          format: int64\n          description: 99th percentile latency in milliseconds.\n        timeRange:\n          title: time_range\n          description: Time range used for the metrics.\n          $ref: '#/components/schemas/openstatus.monitor.v1.TimeRange'\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          description: Regions included in the metrics.\n      title: GetMonitorSummaryResponse\n      additionalProperties: false\n      description: GetMonitorSummaryResponse is the response containing aggregated metrics for a monitor.\n    openstatus.monitor.v1.HTTPMethod:\n      type: string\n      title: HTTPMethod\n      enum:\n        - HTTP_METHOD_UNSPECIFIED\n        - HTTP_METHOD_GET\n        - HTTP_METHOD_POST\n        - HTTP_METHOD_HEAD\n        - HTTP_METHOD_PUT\n        - HTTP_METHOD_PATCH\n        - HTTP_METHOD_DELETE\n        - HTTP_METHOD_TRACE\n        - HTTP_METHOD_CONNECT\n        - HTTP_METHOD_OPTIONS\n      description: HTTP methods supported for monitors.\n    openstatus.monitor.v1.HTTPMonitor:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the monitor (output only for create requests).\n        name:\n          type: string\n          examples:\n            - Production API Health Check\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Name of the monitor (required, max 256 characters).\n        url:\n          type: string\n          examples:\n            - https://api.example.com/health\n          title: url\n          maxLength: 2048\n          minLength: 1\n          format: uri\n          description: URL to monitor (required, max 2048 characters).\n        periodicity:\n          not:\n            enum:\n              - PERIODICITY_UNSPECIFIED\n          title: periodicity\n          description: Check periodicity (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n        method:\n          not:\n            enum:\n              - HTTP_METHOD_UNSPECIFIED\n          title: method\n          description: HTTP method to use (defaults to GET).\n          $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMethod'\n        body:\n          type: string\n          examples:\n            - map[key:value]\n          title: body\n          description: Request body (optional).\n        timeout:\n          type:\n            - integer\n            - string\n          title: timeout\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Timeout in milliseconds (0-120000, defaults to 45000).\n        degradedAt:\n          type:\n            - integer\n            - string\n            - \"null\"\n          title: degraded_at\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n        retry:\n          type:\n            - integer\n            - string\n          title: retry\n          maximum: 10\n          minimum: 0\n          format: int64\n          description: Number of retry attempts (0-10, defaults to 3).\n        followRedirects:\n          type:\n            - boolean\n            - \"null\"\n          title: follow_redirects\n          description: Whether to follow HTTP redirects (defaults to true when not specified).\n        headers:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n          title: headers\n          maxItems: 20\n          description: Custom headers for the request.\n        statusCodeAssertions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.StatusCodeAssertion'\n          title: status_code_assertions\n          maxItems: 10\n          description: Status code assertions for the response.\n        bodyAssertions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.BodyAssertion'\n          title: body_assertions\n          maxItems: 10\n          description: Body content assertions for the response.\n        headerAssertions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.HeaderAssertion'\n          title: header_assertions\n          maxItems: 10\n          description: Header assertions for the response.\n        description:\n          type: string\n          title: description\n          maxLength: 1024\n          description: Description of the monitor (optional).\n        active:\n          type: boolean\n          title: active\n          description: Whether the monitor is active (defaults to false).\n        public:\n          type: boolean\n          title: public\n          description: Whether the monitor is publicly visible (defaults to false).\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          maxItems: 28\n          description: Geographic regions to run checks from.\n        openTelemetry:\n          title: open_telemetry\n          description: OpenTelemetry configuration for exporting metrics.\n          $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n        status:\n          title: status\n          description: Current operational status of the monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n      title: HTTPMonitor\n      additionalProperties: false\n      description: HTTPMonitor defines the configuration for an HTTP monitor.\n    openstatus.monitor.v1.HeaderAssertion:\n      type: object\n      properties:\n        target:\n          type: string\n          title: target\n          description: Target value to compare against.\n        comparator:\n          not:\n            enum:\n              - STRING_COMPARATOR_UNSPECIFIED\n          title: comparator\n          description: Comparison operation (required, must not be UNSPECIFIED).\n          $ref: '#/components/schemas/openstatus.monitor.v1.StringComparator'\n        key:\n          type: string\n          title: key\n          minLength: 1\n          description: Header key to check (required).\n      title: HeaderAssertion\n      additionalProperties: false\n      description: HeaderAssertion defines an assertion for response headers.\n    openstatus.monitor.v1.Headers:\n      type: object\n      properties:\n        key:\n          type: string\n          examples:\n            - Authorization\n          title: key\n          minLength: 1\n          description: Header name.\n        value:\n          type: string\n          examples:\n            - Bearer token123\n          title: value\n          description: Header value.\n      title: Headers\n      additionalProperties: false\n      description: Headers represents a key-value pair for HTTP headers.\n    openstatus.monitor.v1.ListMonitorsRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of monitors to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of monitors to skip for pagination (defaults to 0).\n      title: ListMonitorsRequest\n      additionalProperties: false\n      description: ListMonitorsRequest is the request to list monitors.\n    openstatus.monitor.v1.ListMonitorsResponse:\n      type: object\n      properties:\n        httpMonitors:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n          title: http_monitors\n          description: HTTP monitors in the workspace.\n        tcpMonitors:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n          title: tcp_monitors\n          description: TCP monitors in the workspace.\n        dnsMonitors:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n          title: dns_monitors\n          description: DNS monitors in the workspace.\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of monitors across all types.\n      title: ListMonitorsResponse\n      additionalProperties: false\n      description: ListMonitorsResponse is the response containing a list of monitors.\n    openstatus.monitor.v1.MonitorConfig:\n      type: object\n      oneOf:\n        - properties:\n            dns:\n              title: dns\n              description: DNS monitor configuration.\n              $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n          title: dns\n          required:\n            - dns\n        - properties:\n            http:\n              title: http\n              description: HTTP monitor configuration.\n              $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n          title: http\n          required:\n            - http\n        - properties:\n            tcp:\n              title: tcp\n              description: TCP monitor configuration.\n              $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n          title: tcp\n          required:\n            - tcp\n      title: MonitorConfig\n      additionalProperties: false\n      description: MonitorConfig represents the type-specific configuration for a monitor.\n    openstatus.monitor.v1.MonitorStatus:\n      type: string\n      title: MonitorStatus\n      enum:\n        - MONITOR_STATUS_UNSPECIFIED\n        - MONITOR_STATUS_ACTIVE\n        - MONITOR_STATUS_DEGRADED\n        - MONITOR_STATUS_ERROR\n      description: MonitorStatus represents the operational status of a monitor.\n    openstatus.monitor.v1.NumberComparator:\n      type: string\n      title: NumberComparator\n      enum:\n        - NUMBER_COMPARATOR_UNSPECIFIED\n        - NUMBER_COMPARATOR_EQUAL\n        - NUMBER_COMPARATOR_NOT_EQUAL\n        - NUMBER_COMPARATOR_GREATER_THAN\n        - NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\n        - NUMBER_COMPARATOR_LESS_THAN\n        - NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\n      description: NumberComparator defines comparison operations for numeric values.\n    openstatus.monitor.v1.OpenTelemetryConfig:\n      type: object\n      properties:\n        endpoint:\n          type: string\n          title: endpoint\n          maxLength: 2048\n          description: OTEL endpoint URL.\n        headers:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n          title: headers\n          maxItems: 20\n          description: Custom headers for OTEL requests.\n      title: OpenTelemetryConfig\n      additionalProperties: false\n      description: OpenTelemetry configuration for exporting metrics.\n    openstatus.monitor.v1.Periodicity:\n      type: string\n      title: Periodicity\n      enum:\n        - PERIODICITY_UNSPECIFIED\n        - PERIODICITY_30S\n        - PERIODICITY_1M\n        - PERIODICITY_5M\n        - PERIODICITY_10M\n        - PERIODICITY_30M\n        - PERIODICITY_1H\n      description: Monitor periodicity options.\n    openstatus.monitor.v1.RecordAssertion:\n      type: object\n      properties:\n        record:\n          type: string\n          title: record\n          enum:\n            - A\n            - AAAA\n            - CNAME\n            - MX\n            - TXT\n          description: DNS record type (e.g., \"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\").\n        comparator:\n          not:\n            enum:\n              - RECORD_COMPARATOR_UNSPECIFIED\n          title: comparator\n          description: Comparison operation (required, must not be UNSPECIFIED).\n          $ref: '#/components/schemas/openstatus.monitor.v1.RecordComparator'\n        target:\n          type: string\n          title: target\n          description: Target value to compare against.\n      title: RecordAssertion\n      additionalProperties: false\n      description: RecordAssertion defines an assertion for DNS records.\n    openstatus.monitor.v1.RecordComparator:\n      type: string\n      title: RecordComparator\n      enum:\n        - RECORD_COMPARATOR_UNSPECIFIED\n        - RECORD_COMPARATOR_EQUAL\n        - RECORD_COMPARATOR_NOT_EQUAL\n        - RECORD_COMPARATOR_CONTAINS\n        - RECORD_COMPARATOR_NOT_CONTAINS\n      description: RecordComparator defines comparison operations for DNS records.\n    openstatus.monitor.v1.Region:\n      type: string\n      title: Region\n      enum:\n        - REGION_UNSPECIFIED\n        - REGION_FLY_AMS\n        - REGION_FLY_ARN\n        - REGION_FLY_BOM\n        - REGION_FLY_CDG\n        - REGION_FLY_DFW\n        - REGION_FLY_EWR\n        - REGION_FLY_FRA\n        - REGION_FLY_GRU\n        - REGION_FLY_IAD\n        - REGION_FLY_JNB\n        - REGION_FLY_LAX\n        - REGION_FLY_LHR\n        - REGION_FLY_NRT\n        - REGION_FLY_ORD\n        - REGION_FLY_SJC\n        - REGION_FLY_SIN\n        - REGION_FLY_SYD\n        - REGION_FLY_YYZ\n        - REGION_KOYEB_FRA\n        - REGION_KOYEB_PAR\n        - REGION_KOYEB_SFO\n        - REGION_KOYEB_SIN\n        - REGION_KOYEB_TYO\n        - REGION_KOYEB_WAS\n        - REGION_RAILWAY_US_WEST2\n        - REGION_RAILWAY_US_EAST4\n        - REGION_RAILWAY_EUROPE_WEST4\n        - REGION_RAILWAY_ASIA_SOUTHEAST1\n      description: Geographic regions where monitors can run checks from.\n    openstatus.monitor.v1.RegionStatus:\n      type: object\n      properties:\n        region:\n          title: region\n          description: The region identifier.\n          $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n        status:\n          title: status\n          description: The status of the monitor in this region.\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n      title: RegionStatus\n      additionalProperties: false\n      description: RegionStatus represents the status of a monitor in a specific region.\n    openstatus.monitor.v1.StatusCodeAssertion:\n      type: object\n      properties:\n        target:\n          type:\n            - integer\n            - string\n          title: target\n          maximum: 599\n          minimum: 100\n          format: int64\n          description: Target status code to compare against (100-599).\n        comparator:\n          not:\n            enum:\n              - NUMBER_COMPARATOR_UNSPECIFIED\n          title: comparator\n          description: Comparison operation (required, must not be UNSPECIFIED).\n          $ref: '#/components/schemas/openstatus.monitor.v1.NumberComparator'\n      title: StatusCodeAssertion\n      additionalProperties: false\n      description: StatusCodeAssertion defines an assertion for HTTP status codes.\n    openstatus.monitor.v1.StringComparator:\n      type: string\n      title: StringComparator\n      enum:\n        - STRING_COMPARATOR_UNSPECIFIED\n        - STRING_COMPARATOR_CONTAINS\n        - STRING_COMPARATOR_NOT_CONTAINS\n        - STRING_COMPARATOR_EQUAL\n        - STRING_COMPARATOR_NOT_EQUAL\n        - STRING_COMPARATOR_EMPTY\n        - STRING_COMPARATOR_NOT_EMPTY\n        - STRING_COMPARATOR_GREATER_THAN\n        - STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\n        - STRING_COMPARATOR_LESS_THAN\n        - STRING_COMPARATOR_LESS_THAN_OR_EQUAL\n      description: StringComparator defines comparison operations for string values.\n    openstatus.monitor.v1.TCPMonitor:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the monitor (output only for create requests).\n        name:\n          type: string\n          examples:\n            - Database Connection Check\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Name of the monitor (required, max 256 characters).\n        uri:\n          type: string\n          examples:\n            - tcp://db.example.com:5432\n          title: uri\n          maxLength: 2048\n          minLength: 1\n          description: URI to monitor in format \"host:port\" (required, max 2048 characters).\n        periodicity:\n          not:\n            enum:\n              - PERIODICITY_UNSPECIFIED\n          title: periodicity\n          description: Check periodicity (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n        timeout:\n          type:\n            - integer\n            - string\n          title: timeout\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Timeout in milliseconds (0-120000, defaults to 45000).\n        degradedAt:\n          type:\n            - integer\n            - string\n            - \"null\"\n          title: degraded_at\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n        retry:\n          type:\n            - integer\n            - string\n          title: retry\n          maximum: 10\n          minimum: 0\n          format: int64\n          description: Number of retry attempts (0-10, defaults to 3).\n        description:\n          type: string\n          title: description\n          maxLength: 1024\n          description: Description of the monitor (optional).\n        active:\n          type: boolean\n          title: active\n          description: Whether the monitor is active (defaults to false).\n        public:\n          type: boolean\n          title: public\n          description: Whether the monitor is publicly visible (defaults to false).\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          maxItems: 28\n          description: Geographic regions to run checks from.\n        openTelemetry:\n          title: open_telemetry\n          description: OpenTelemetry configuration for exporting metrics.\n          $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n        status:\n          title: status\n          description: Current operational status of the monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n      title: TCPMonitor\n      additionalProperties: false\n      description: TCPMonitor defines the configuration for a TCP monitor.\n    openstatus.monitor.v1.TimeRange:\n      type: string\n      title: TimeRange\n      enum:\n        - TIME_RANGE_UNSPECIFIED\n        - TIME_RANGE_1D\n        - TIME_RANGE_7D\n        - TIME_RANGE_14D\n      description: TimeRange represents the time period for metrics aggregation.\n    openstatus.monitor.v1.TriggerMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to trigger (required).\n      title: TriggerMonitorRequest\n      additionalProperties: false\n      description: TriggerMonitorRequest is the request to trigger a monitor check.\n    openstatus.monitor.v1.TriggerMonitorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the trigger was successful.\n      title: TriggerMonitorResponse\n      additionalProperties: false\n      description: TriggerMonitorResponse is the response after triggering a monitor.\n    openstatus.monitor.v1.UpdateDNSMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to update (required).\n        monitor:\n          oneOf:\n            - $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n            - type: \"null\"\n          title: monitor\n          description: Updated monitor configuration (all fields optional for partial updates).\n      title: UpdateDNSMonitorRequest\n      additionalProperties: false\n      description: UpdateDNSMonitorRequest is the request to update an existing DNS monitor.\n    openstatus.monitor.v1.UpdateDNSMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The updated monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n      title: UpdateDNSMonitorResponse\n      additionalProperties: false\n      description: UpdateDNSMonitorResponse is the response after updating a DNS monitor.\n    openstatus.monitor.v1.UpdateHTTPMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to update (required).\n        monitor:\n          oneOf:\n            - $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n            - type: \"null\"\n          title: monitor\n          description: Updated monitor configuration (all fields optional for partial updates).\n      title: UpdateHTTPMonitorRequest\n      additionalProperties: false\n      description: UpdateHTTPMonitorRequest is the request to update an existing HTTP monitor.\n    openstatus.monitor.v1.UpdateHTTPMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The updated monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n      title: UpdateHTTPMonitorResponse\n      additionalProperties: false\n      description: UpdateHTTPMonitorResponse is the response after updating an HTTP monitor.\n    openstatus.monitor.v1.UpdateTCPMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to update (required).\n        monitor:\n          oneOf:\n            - $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n            - type: \"null\"\n          title: monitor\n          description: Updated monitor configuration (all fields optional for partial updates).\n      title: UpdateTCPMonitorRequest\n      additionalProperties: false\n      description: UpdateTCPMonitorRequest is the request to update an existing TCP monitor.\n    openstatus.monitor.v1.UpdateTCPMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The updated monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n      title: UpdateTCPMonitorResponse\n      additionalProperties: false\n      description: UpdateTCPMonitorResponse is the response after updating a TCP monitor.\n    openstatus.notification.v1.CheckNotificationLimitRequest:\n      type: object\n      title: CheckNotificationLimitRequest\n      additionalProperties: false\n      description: CheckNotificationLimitRequest is the request to check notification limits.\n    openstatus.notification.v1.CheckNotificationLimitResponse:\n      type: object\n      properties:\n        limitReached:\n          type: boolean\n          title: limit_reached\n          description: Whether the workspace has reached its notification limit.\n        currentCount:\n          type: integer\n          title: current_count\n          format: int32\n          description: Current number of notification channels.\n        maxCount:\n          type: integer\n          title: max_count\n          format: int32\n          description: Maximum allowed notification channels.\n      title: CheckNotificationLimitResponse\n      additionalProperties: false\n      description: CheckNotificationLimitResponse is the response containing limit information.\n    openstatus.notification.v1.CreateNotificationRequest:\n      type: object\n      properties:\n        name:\n          type: string\n          examples:\n            - Slack Ops Channel\n          title: name\n          minLength: 1\n          description: Display name for the notification channel.\n        provider:\n          not:\n            enum:\n              - NOTIFICATION_PROVIDER_UNSPECIFIED\n          title: provider\n          description: Provider type.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n        data:\n          title: data\n          description: Provider-specific configuration.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n        monitorIds:\n          type: array\n          items:\n            type: string\n          title: monitor_ids\n          description: IDs of monitors to associate with this notification.\n      title: CreateNotificationRequest\n      required:\n        - data\n      additionalProperties: false\n      description: CreateNotificationRequest is the request to create a new notification channel.\n    openstatus.notification.v1.CreateNotificationResponse:\n      type: object\n      properties:\n        notification:\n          title: notification\n          description: The created notification channel.\n          $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n      title: CreateNotificationResponse\n      additionalProperties: false\n      description: CreateNotificationResponse is the response after creating a notification channel.\n    openstatus.notification.v1.DeleteNotificationRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Notification ID to delete (required).\n      title: DeleteNotificationRequest\n      additionalProperties: false\n      description: DeleteNotificationRequest is the request to delete a notification channel.\n    openstatus.notification.v1.DeleteNotificationResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteNotificationResponse\n      additionalProperties: false\n      description: DeleteNotificationResponse is the response after deleting a notification channel.\n    openstatus.notification.v1.DiscordData:\n      type: object\n      properties:\n        webhookUrl:\n          type: string\n          examples:\n            - https://discord.com/api/webhooks/123/abc\n          title: webhook_url\n          format: uri\n          description: Discord webhook URL.\n      title: DiscordData\n      additionalProperties: false\n      description: DiscordData contains configuration for Discord notifications.\n    openstatus.notification.v1.EmailData:\n      type: object\n      properties:\n        email:\n          type: string\n          examples:\n            - ops-team@example.com\n          title: email\n          format: email\n          description: Email address to send notifications to.\n      title: EmailData\n      additionalProperties: false\n      description: EmailData contains configuration for email notifications.\n    openstatus.notification.v1.GetNotificationRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Notification ID to retrieve (required).\n      title: GetNotificationRequest\n      additionalProperties: false\n      description: GetNotificationRequest is the request to get a notification channel.\n    openstatus.notification.v1.GetNotificationResponse:\n      type: object\n      properties:\n        notification:\n          title: notification\n          description: The notification channel.\n          $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n      title: GetNotificationResponse\n      additionalProperties: false\n      description: GetNotificationResponse is the response containing the notification channel.\n    openstatus.notification.v1.GoogleChatData:\n      type: object\n      properties:\n        webhookUrl:\n          type: string\n          title: webhook_url\n          format: uri\n          description: Google Chat webhook URL.\n      title: GoogleChatData\n      additionalProperties: false\n      description: GoogleChatData contains configuration for Google Chat notifications.\n    openstatus.notification.v1.GrafanaOncallData:\n      type: object\n      properties:\n        webhookUrl:\n          type: string\n          title: webhook_url\n          format: uri\n          description: Grafana OnCall webhook URL.\n      title: GrafanaOncallData\n      additionalProperties: false\n      description: GrafanaOncallData contains configuration for Grafana OnCall notifications.\n    openstatus.notification.v1.ListNotificationsRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of notifications to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of notifications to skip for pagination (defaults to 0).\n      title: ListNotificationsRequest\n      additionalProperties: false\n      description: ListNotificationsRequest is the request to list notification channels.\n    openstatus.notification.v1.ListNotificationsResponse:\n      type: object\n      properties:\n        notifications:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.notification.v1.NotificationSummary'\n          title: notifications\n          description: Notification channel summaries.\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of notification channels.\n      title: ListNotificationsResponse\n      additionalProperties: false\n      description: ListNotificationsResponse is the response containing notification channels.\n    openstatus.notification.v1.Notification:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the notification.\n        name:\n          type: string\n          title: name\n          description: Display name for the notification channel.\n        provider:\n          title: provider\n          description: Provider type.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n        data:\n          title: data\n          description: Provider-specific configuration.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n        monitorIds:\n          type: array\n          items:\n            type: string\n          title: monitor_ids\n          description: IDs of monitors associated with this notification.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the notification was created (RFC 3339).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the notification was last updated (RFC 3339).\n      title: Notification\n      additionalProperties: false\n      description: Notification represents a notification channel with full details.\n    openstatus.notification.v1.NotificationData:\n      type: object\n      oneOf:\n        - properties:\n            discord:\n              title: discord\n              description: Discord configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.DiscordData'\n          title: discord\n          required:\n            - discord\n        - properties:\n            email:\n              title: email\n              description: Email configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.EmailData'\n          title: email\n          required:\n            - email\n        - properties:\n            googleChat:\n              title: google_chat\n              description: Google Chat configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.GoogleChatData'\n          title: google_chat\n          required:\n            - googleChat\n        - properties:\n            grafanaOncall:\n              title: grafana_oncall\n              description: Grafana OnCall configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.GrafanaOncallData'\n          title: grafana_oncall\n          required:\n            - grafanaOncall\n        - properties:\n            ntfy:\n              title: ntfy\n              description: Ntfy configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.NtfyData'\n          title: ntfy\n          required:\n            - ntfy\n        - properties:\n            opsgenie:\n              title: opsgenie\n              description: Opsgenie configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.OpsgenieData'\n          title: opsgenie\n          required:\n            - opsgenie\n        - properties:\n            pagerduty:\n              title: pagerduty\n              description: PagerDuty configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.PagerDutyData'\n          title: pagerduty\n          required:\n            - pagerduty\n        - properties:\n            slack:\n              title: slack\n              description: Slack configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.SlackData'\n          title: slack\n          required:\n            - slack\n        - properties:\n            sms:\n              title: sms\n              description: SMS configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.SmsData'\n          title: sms\n          required:\n            - sms\n        - properties:\n            telegram:\n              title: telegram\n              description: Telegram configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.TelegramData'\n          title: telegram\n          required:\n            - telegram\n        - properties:\n            webhook:\n              title: webhook\n              description: Webhook configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.WebhookData'\n          title: webhook\n          required:\n            - webhook\n        - properties:\n            whatsapp:\n              title: whatsapp\n              description: WhatsApp configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.WhatsappData'\n          title: whatsapp\n          required:\n            - whatsapp\n      title: NotificationData\n      additionalProperties: false\n      description: NotificationData is a union of provider-specific configuration.\n    openstatus.notification.v1.NotificationProvider:\n      type: string\n      title: NotificationProvider\n      enum:\n        - NOTIFICATION_PROVIDER_UNSPECIFIED\n        - NOTIFICATION_PROVIDER_DISCORD\n        - NOTIFICATION_PROVIDER_EMAIL\n        - NOTIFICATION_PROVIDER_GOOGLE_CHAT\n        - NOTIFICATION_PROVIDER_GRAFANA_ONCALL\n        - NOTIFICATION_PROVIDER_NTFY\n        - NOTIFICATION_PROVIDER_PAGERDUTY\n        - NOTIFICATION_PROVIDER_OPSGENIE\n        - NOTIFICATION_PROVIDER_SLACK\n        - NOTIFICATION_PROVIDER_SMS\n        - NOTIFICATION_PROVIDER_TELEGRAM\n        - NOTIFICATION_PROVIDER_WEBHOOK\n        - NOTIFICATION_PROVIDER_WHATSAPP\n      description: NotificationProvider represents the supported notification channel types.\n    openstatus.notification.v1.NotificationSummary:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the notification.\n        name:\n          type: string\n          title: name\n          description: Display name for the notification channel.\n        provider:\n          title: provider\n          description: Provider type.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n        monitorCount:\n          type: integer\n          title: monitor_count\n          format: int32\n          description: Number of monitors associated with this notification.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the notification was created (RFC 3339).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the notification was last updated (RFC 3339).\n      title: NotificationSummary\n      additionalProperties: false\n      description: NotificationSummary represents a notification channel summary for list responses.\n    openstatus.notification.v1.NtfyData:\n      type: object\n      properties:\n        topic:\n          type: string\n          examples:\n            - openstatus-alerts\n          title: topic\n          minLength: 1\n          description: Ntfy topic to publish to.\n        serverUrl:\n          type: string\n          title: server_url\n          description: Ntfy server URL (defaults to https://ntfy.sh).\n        token:\n          type:\n            - string\n            - \"null\"\n          title: token\n          description: Optional authentication token.\n      title: NtfyData\n      additionalProperties: false\n      description: NtfyData contains configuration for Ntfy notifications.\n    openstatus.notification.v1.OpsgenieData:\n      type: object\n      properties:\n        apiKey:\n          type: string\n          title: api_key\n          minLength: 1\n          description: Opsgenie API key.\n        region:\n          title: region\n          description: Opsgenie region.\n          $ref: '#/components/schemas/openstatus.notification.v1.OpsgenieRegion'\n      title: OpsgenieData\n      additionalProperties: false\n      description: OpsgenieData contains configuration for Opsgenie notifications.\n    openstatus.notification.v1.OpsgenieRegion:\n      type: string\n      title: OpsgenieRegion\n      enum:\n        - OPSGENIE_REGION_UNSPECIFIED\n        - OPSGENIE_REGION_US\n        - OPSGENIE_REGION_EU\n      description: OpsgenieRegion represents the Opsgenie API region.\n    openstatus.notification.v1.PagerDutyData:\n      type: object\n      properties:\n        integrationKey:\n          type: string\n          examples:\n            - a1b2c3d4e5f6g7h8i9j0\n          title: integration_key\n          minLength: 1\n          description: PagerDuty integration key.\n      title: PagerDutyData\n      additionalProperties: false\n      description: PagerDutyData contains configuration for PagerDuty notifications.\n    openstatus.notification.v1.SendTestNotificationRequest:\n      type: object\n      properties:\n        provider:\n          not:\n            enum:\n              - NOTIFICATION_PROVIDER_UNSPECIFIED\n          title: provider\n          description: Provider type.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n        data:\n          title: data\n          description: Provider-specific configuration.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n      title: SendTestNotificationRequest\n      required:\n        - data\n      additionalProperties: false\n      description: SendTestNotificationRequest is the request to send a test notification.\n    openstatus.notification.v1.SendTestNotificationResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the test was successful.\n        errorMessage:\n          type:\n            - string\n            - \"null\"\n          title: error_message\n          description: Optional error message if the test failed.\n      title: SendTestNotificationResponse\n      additionalProperties: false\n      description: SendTestNotificationResponse is the response after sending a test notification.\n    openstatus.notification.v1.SlackData:\n      type: object\n      properties:\n        webhookUrl:\n          type: string\n          examples:\n            - https://hooks.slack.com/services/T00/B00/xxx\n          title: webhook_url\n          format: uri\n          description: Slack webhook URL.\n      title: SlackData\n      additionalProperties: false\n      description: SlackData contains configuration for Slack notifications.\n    openstatus.notification.v1.SmsData:\n      type: object\n      properties:\n        phoneNumber:\n          type: string\n          examples:\n            - \"+14155551234\"\n          title: phone_number\n          minLength: 1\n          description: Phone number to send SMS to.\n      title: SmsData\n      additionalProperties: false\n      description: SmsData contains configuration for SMS notifications.\n    openstatus.notification.v1.TelegramData:\n      type: object\n      properties:\n        chatId:\n          type: string\n          examples:\n            - \"-1001234567890\"\n          title: chat_id\n          minLength: 1\n          description: Telegram chat ID.\n      title: TelegramData\n      additionalProperties: false\n      description: TelegramData contains configuration for Telegram notifications.\n    openstatus.notification.v1.UpdateNotificationRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Notification ID to update (required).\n        name:\n          type:\n            - string\n            - \"null\"\n          title: name\n          description: Updated display name.\n        data:\n          oneOf:\n            - $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n            - type: \"null\"\n          title: data\n          description: Updated provider-specific configuration.\n        monitorIds:\n          type: array\n          items:\n            type: string\n          title: monitor_ids\n          description: Updated monitor IDs to associate.\n      title: UpdateNotificationRequest\n      additionalProperties: false\n      description: UpdateNotificationRequest is the request to update a notification channel.\n    openstatus.notification.v1.UpdateNotificationResponse:\n      type: object\n      properties:\n        notification:\n          title: notification\n          description: The updated notification channel.\n          $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n      title: UpdateNotificationResponse\n      additionalProperties: false\n      description: UpdateNotificationResponse is the response after updating a notification channel.\n    openstatus.notification.v1.WebhookData:\n      type: object\n      properties:\n        endpoint:\n          type: string\n          examples:\n            - https://api.example.com/webhooks/openstatus\n          title: endpoint\n          format: uri\n          description: Webhook endpoint URL.\n        headers:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.notification.v1.WebhookHeader'\n          title: headers\n          description: Optional custom headers.\n      title: WebhookData\n      additionalProperties: false\n      description: WebhookData contains configuration for custom webhook notifications.\n    openstatus.notification.v1.WebhookHeader:\n      type: object\n      properties:\n        key:\n          type: string\n          title: key\n          minLength: 1\n          description: Header name.\n        value:\n          type: string\n          title: value\n          description: Header value.\n      title: WebhookHeader\n      additionalProperties: false\n      description: WebhookHeader represents a custom header for webhook requests.\n    openstatus.notification.v1.WhatsappData:\n      type: object\n      properties:\n        phoneNumber:\n          type: string\n          title: phone_number\n          minLength: 1\n          description: Phone number to send WhatsApp messages to.\n      title: WhatsappData\n      additionalProperties: false\n      description: WhatsappData contains configuration for WhatsApp notifications.\n    openstatus.status_page.v1.AddMonitorComponentRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to add the component to (required).\n        monitorId:\n          type: string\n          title: monitor_id\n          minLength: 1\n          description: ID of the monitor to associate with this component (required).\n        name:\n          type:\n            - string\n            - \"null\"\n          title: name\n          maxLength: 256\n          description: Display name for the component (optional, defaults to monitor name).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: Description of the component (optional).\n        order:\n          type:\n            - integer\n            - \"null\"\n          title: order\n          format: int32\n          description: Display order of the component (optional).\n        groupId:\n          type:\n            - string\n            - \"null\"\n          title: group_id\n          description: ID of the group to add this component to (optional).\n      title: AddMonitorComponentRequest\n      additionalProperties: false\n      description: AddMonitorComponentRequest is the request to add a monitor-based component to a status page.\n    openstatus.status_page.v1.AddMonitorComponentResponse:\n      type: object\n      properties:\n        component:\n          title: component\n          description: The created component.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n      title: AddMonitorComponentResponse\n      additionalProperties: false\n      description: AddMonitorComponentResponse is the response after adding a monitor component.\n    openstatus.status_page.v1.AddStaticComponentRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to add the component to (required).\n        name:\n          type: string\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Display name for the component (required).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: Description of the component (optional).\n        order:\n          type:\n            - integer\n            - \"null\"\n          title: order\n          format: int32\n          description: Display order of the component (optional).\n        groupId:\n          type:\n            - string\n            - \"null\"\n          title: group_id\n          description: ID of the group to add this component to (optional).\n      title: AddStaticComponentRequest\n      additionalProperties: false\n      description: AddStaticComponentRequest is the request to add a static component to a status page.\n    openstatus.status_page.v1.AddStaticComponentResponse:\n      type: object\n      properties:\n        component:\n          title: component\n          description: The created component.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n      title: AddStaticComponentResponse\n      additionalProperties: false\n      description: AddStaticComponentResponse is the response after adding a static component.\n    openstatus.status_page.v1.ComponentStatus:\n      type: object\n      properties:\n        componentId:\n          type: string\n          title: component_id\n          description: ID of the component.\n        status:\n          title: status\n          description: Current status of the component.\n          $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n      title: ComponentStatus\n      additionalProperties: false\n      description: ComponentStatus represents the status of a single component.\n    openstatus.status_page.v1.CreateComponentGroupRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to create the group in (required).\n        name:\n          type: string\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Display name for the group (required).\n      title: CreateComponentGroupRequest\n      additionalProperties: false\n      description: CreateComponentGroupRequest is the request to create a new component group.\n    openstatus.status_page.v1.CreateComponentGroupResponse:\n      type: object\n      properties:\n        group:\n          title: group\n          description: The created component group.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n      title: CreateComponentGroupResponse\n      additionalProperties: false\n      description: CreateComponentGroupResponse is the response after creating a component group.\n    openstatus.status_page.v1.CreateStatusPageRequest:\n      type: object\n      properties:\n        title:\n          type: string\n          examples:\n            - Acme Corp Status\n          title: title\n          maxLength: 256\n          minLength: 1\n          description: Title of the status page (required).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: Description of the status page (optional).\n        slug:\n          type: string\n          examples:\n            - my-status-page\n          title: slug\n          maxLength: 256\n          minLength: 1\n          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$\n          description: URL-friendly slug for the status page (required). Must be lowercase alphanumeric with hyphens.\n        homepageUrl:\n          type:\n            - string\n            - \"null\"\n          examples:\n            - https://www.example.com\n          title: homepage_url\n          description: URL to the homepage (optional).\n        contactUrl:\n          type:\n            - string\n            - \"null\"\n          title: contact_url\n          description: URL to the contact page (optional).\n      title: CreateStatusPageRequest\n      additionalProperties: false\n      description: CreateStatusPageRequest is the request to create a new status page.\n    openstatus.status_page.v1.CreateStatusPageResponse:\n      type: object\n      properties:\n        statusPage:\n          title: status_page\n          description: The created status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n      title: CreateStatusPageResponse\n      additionalProperties: false\n      description: CreateStatusPageResponse is the response after creating a status page.\n    openstatus.status_page.v1.DeleteComponentGroupRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the component group to delete (required).\n      title: DeleteComponentGroupRequest\n      additionalProperties: false\n      description: DeleteComponentGroupRequest is the request to delete a component group.\n    openstatus.status_page.v1.DeleteComponentGroupResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteComponentGroupResponse\n      additionalProperties: false\n      description: DeleteComponentGroupResponse is the response after deleting a component group.\n    openstatus.status_page.v1.DeleteStatusPageRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status page to delete (required).\n      title: DeleteStatusPageRequest\n      additionalProperties: false\n      description: DeleteStatusPageRequest is the request to delete a status page.\n    openstatus.status_page.v1.DeleteStatusPageResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteStatusPageResponse\n      additionalProperties: false\n      description: DeleteStatusPageResponse is the response after deleting a status page.\n    openstatus.status_page.v1.GetOverallStatusRequest:\n      type: object\n      oneOf:\n        - properties:\n            id:\n              type: string\n              title: id\n              description: ID of the status page.\n          title: id\n          required:\n            - id\n        - properties:\n            slug:\n              type: string\n              title: slug\n              description: Slug of the status page.\n          title: slug\n          required:\n            - slug\n      title: GetOverallStatusRequest\n      additionalProperties: false\n      description: GetOverallStatusRequest is the request to get the overall status of a status page.\n    openstatus.status_page.v1.GetOverallStatusResponse:\n      type: object\n      properties:\n        overallStatus:\n          title: overall_status\n          description: Aggregated status across all components.\n          $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n        componentStatuses:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.ComponentStatus'\n          title: component_statuses\n          description: Status of individual components.\n      title: GetOverallStatusResponse\n      additionalProperties: false\n      description: GetOverallStatusResponse is the response containing the overall status and individual component statuses.\n    openstatus.status_page.v1.GetStatusPageContentRequest:\n      type: object\n      oneOf:\n        - properties:\n            id:\n              type: string\n              title: id\n              description: ID of the status page.\n          title: id\n          required:\n            - id\n        - properties:\n            slug:\n              type: string\n              title: slug\n              description: Slug of the status page.\n          title: slug\n          required:\n            - slug\n      title: GetStatusPageContentRequest\n      additionalProperties: false\n      description: GetStatusPageContentRequest is the request to get the full content of a status page.\n    openstatus.status_page.v1.GetStatusPageContentResponse:\n      type: object\n      properties:\n        statusPage:\n          title: status_page\n          description: The status page details.\n          $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n        components:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n          title: components\n          description: Components on the status page.\n        groups:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n          title: groups\n          description: Component groups on the status page.\n        statusReports:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n          title: status_reports\n          description: Active and recent status reports.\n        maintenances:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n          title: maintenances\n          description: Scheduled maintenances.\n      title: GetStatusPageContentResponse\n      additionalProperties: false\n      description: GetStatusPageContentResponse is the response containing the full status page content.\n    openstatus.status_page.v1.GetStatusPageRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status page to retrieve (required).\n      title: GetStatusPageRequest\n      additionalProperties: false\n      description: GetStatusPageRequest is the request to get a status page by ID.\n    openstatus.status_page.v1.GetStatusPageResponse:\n      type: object\n      properties:\n        statusPage:\n          title: status_page\n          description: The requested status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n      title: GetStatusPageResponse\n      additionalProperties: false\n      description: GetStatusPageResponse is the response containing the status page.\n    openstatus.status_page.v1.ListStatusPagesRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of pages to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of pages to skip for pagination (defaults to 0).\n      title: ListStatusPagesRequest\n      additionalProperties: false\n      description: ListStatusPagesRequest is the request to list status pages.\n    openstatus.status_page.v1.ListStatusPagesResponse:\n      type: object\n      properties:\n        statusPages:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.StatusPageSummary'\n          title: status_pages\n          description: List of status pages (metadata only).\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of status pages.\n      title: ListStatusPagesResponse\n      additionalProperties: false\n      description: ListStatusPagesResponse is the response containing status page summaries.\n    openstatus.status_page.v1.ListSubscribersRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to list subscribers for (required).\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of subscribers to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of subscribers to skip for pagination (defaults to 0).\n        includeUnsubscribed:\n          type:\n            - boolean\n            - \"null\"\n          title: include_unsubscribed\n          description: Whether to include unsubscribed users (defaults to false).\n      title: ListSubscribersRequest\n      additionalProperties: false\n      description: ListSubscribersRequest is the request to list subscribers of a status page.\n    openstatus.status_page.v1.ListSubscribersResponse:\n      type: object\n      properties:\n        subscribers:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n          title: subscribers\n          description: List of subscribers.\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of subscribers matching the filter.\n      title: ListSubscribersResponse\n      additionalProperties: false\n      description: ListSubscribersResponse is the response containing status page subscribers.\n    openstatus.status_page.v1.OverallStatus:\n      type: string\n      title: OverallStatus\n      enum:\n        - OVERALL_STATUS_UNSPECIFIED\n        - OVERALL_STATUS_OPERATIONAL\n        - OVERALL_STATUS_DEGRADED\n        - OVERALL_STATUS_PARTIAL_OUTAGE\n        - OVERALL_STATUS_MAJOR_OUTAGE\n        - OVERALL_STATUS_MAINTENANCE\n        - OVERALL_STATUS_UNKNOWN\n      description: OverallStatus represents the aggregated status of all components on a page.\n    openstatus.status_page.v1.PageAccessType:\n      type: string\n      title: PageAccessType\n      enum:\n        - PAGE_ACCESS_TYPE_UNSPECIFIED\n        - PAGE_ACCESS_TYPE_PUBLIC\n        - PAGE_ACCESS_TYPE_PASSWORD_PROTECTED\n        - PAGE_ACCESS_TYPE_AUTHENTICATED\n      description: PageAccessType defines who can access the status page.\n    openstatus.status_page.v1.PageComponent:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the component.\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the status page this component belongs to.\n        name:\n          type: string\n          title: name\n          description: Display name of the component.\n        description:\n          type: string\n          title: description\n          description: Description of the component (optional).\n        type:\n          title: type\n          description: Type of the component (monitor or static).\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentType'\n        monitorId:\n          type: string\n          title: monitor_id\n          description: ID of the monitor if type is MONITOR (optional).\n        order:\n          type: integer\n          title: order\n          format: int32\n          description: Display order of the component.\n        groupId:\n          type: string\n          title: group_id\n          description: ID of the group this component belongs to (optional).\n        groupOrder:\n          type: integer\n          title: group_order\n          format: int32\n          description: Order within the group if grouped.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the component was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the component was last updated (RFC 3339 format).\n      title: PageComponent\n      additionalProperties: false\n      description: PageComponent represents a component displayed on a status page.\n    openstatus.status_page.v1.PageComponentGroup:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the group.\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the status page this group belongs to.\n        name:\n          type: string\n          title: name\n          description: Display name of the group.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the group was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the group was last updated (RFC 3339 format).\n      title: PageComponentGroup\n      additionalProperties: false\n      description: PageComponentGroup represents a group of components on a status page.\n    openstatus.status_page.v1.PageComponentType:\n      type: string\n      title: PageComponentType\n      enum:\n        - PAGE_COMPONENT_TYPE_UNSPECIFIED\n        - PAGE_COMPONENT_TYPE_MONITOR\n        - PAGE_COMPONENT_TYPE_STATIC\n      description: PageComponentType defines the type of a component on a status page.\n    openstatus.status_page.v1.PageSubscriber:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the subscriber.\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the status page the user is subscribed to.\n        email:\n          type: string\n          title: email\n          description: Email address of the subscriber.\n        acceptedAt:\n          type: string\n          title: accepted_at\n          description: Timestamp when the subscription was accepted/confirmed (RFC 3339 format, optional).\n        unsubscribedAt:\n          type: string\n          title: unsubscribed_at\n          description: Timestamp when the user unsubscribed (RFC 3339 format, optional).\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the subscription was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the subscription was last updated (RFC 3339 format).\n      title: PageSubscriber\n      additionalProperties: false\n      description: PageSubscriber represents a subscriber to a status page.\n    openstatus.status_page.v1.PageTheme:\n      type: string\n      title: PageTheme\n      enum:\n        - PAGE_THEME_UNSPECIFIED\n        - PAGE_THEME_SYSTEM\n        - PAGE_THEME_LIGHT\n        - PAGE_THEME_DARK\n      description: PageTheme defines the visual theme of the status page.\n    openstatus.status_page.v1.RemoveComponentRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the component to remove (required).\n      title: RemoveComponentRequest\n      additionalProperties: false\n      description: RemoveComponentRequest is the request to remove a component from a status page.\n    openstatus.status_page.v1.RemoveComponentResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the removal was successful.\n      title: RemoveComponentResponse\n      additionalProperties: false\n      description: RemoveComponentResponse is the response after removing a component.\n    openstatus.status_page.v1.StatusPage:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the status page.\n        title:\n          type: string\n          title: title\n          description: Title of the status page.\n        description:\n          type: string\n          title: description\n          description: Description of the status page.\n        slug:\n          type: string\n          examples:\n            - acme-corp\n          title: slug\n          description: URL-friendly slug for the status page.\n        customDomain:\n          type: string\n          examples:\n            - status.example.com\n          title: custom_domain\n          description: Custom domain for the status page (optional).\n        published:\n          type: boolean\n          title: published\n          description: Whether the status page is published and visible.\n        accessType:\n          title: access_type\n          description: Access type for the status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageAccessType'\n        theme:\n          title: theme\n          description: Visual theme for the status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageTheme'\n        homepageUrl:\n          type: string\n          title: homepage_url\n          description: URL to the homepage (optional).\n        contactUrl:\n          type: string\n          title: contact_url\n          description: URL to the contact page (optional).\n        icon:\n          type: string\n          title: icon\n          description: Icon URL for the status page (optional).\n        createdAt:\n          type: string\n          examples:\n            - \"2024-01-15T09:00:00Z\"\n          title: created_at\n          description: Timestamp when the page was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          examples:\n            - \"2024-06-20T14:30:00Z\"\n          title: updated_at\n          description: Timestamp when the page was last updated (RFC 3339 format).\n      title: StatusPage\n      additionalProperties: false\n      description: StatusPage represents a full status page with all details.\n    openstatus.status_page.v1.StatusPageSummary:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the status page.\n        title:\n          type: string\n          title: title\n          description: Title of the status page.\n        slug:\n          type: string\n          title: slug\n          description: URL-friendly slug for the status page.\n        published:\n          type: boolean\n          title: published\n          description: Whether the status page is published and visible.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the page was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the page was last updated (RFC 3339 format).\n      title: StatusPageSummary\n      additionalProperties: false\n      description: StatusPageSummary represents metadata for a status page (used in list responses).\n    openstatus.status_page.v1.SubscribeToPageRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to subscribe to (required).\n        email:\n          type: string\n          examples:\n            - user@example.com\n          title: email\n          format: email\n          description: Email address to subscribe (required).\n      title: SubscribeToPageRequest\n      additionalProperties: false\n      description: SubscribeToPageRequest is the request to subscribe an email to a status page.\n    openstatus.status_page.v1.SubscribeToPageResponse:\n      type: object\n      properties:\n        subscriber:\n          title: subscriber\n          description: The created subscriber.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n      title: SubscribeToPageResponse\n      additionalProperties: false\n      description: SubscribeToPageResponse is the response after subscribing to a status page.\n    openstatus.status_page.v1.UnsubscribeFromPageRequest:\n      type: object\n      allOf:\n        - properties:\n            pageId:\n              type: string\n              title: page_id\n              minLength: 1\n              description: ID of the status page to unsubscribe from (required).\n        - oneOf:\n            - properties:\n                email:\n                  type: string\n                  title: email\n                  description: Email address to unsubscribe.\n              title: email\n              required:\n                - email\n            - properties:\n                id:\n                  type: string\n                  title: id\n                  description: Subscriber ID.\n              title: id\n              required:\n                - id\n      title: UnsubscribeFromPageRequest\n      additionalProperties: false\n      description: UnsubscribeFromPageRequest is the request to unsubscribe from a status page.\n    openstatus.status_page.v1.UnsubscribeFromPageResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the unsubscription was successful.\n      title: UnsubscribeFromPageResponse\n      additionalProperties: false\n      description: UnsubscribeFromPageResponse is the response after unsubscribing from a status page.\n    openstatus.status_page.v1.UpdateComponentGroupRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the component group to update (required).\n        name:\n          type:\n            - string\n            - \"null\"\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: New display name for the group (optional).\n      title: UpdateComponentGroupRequest\n      additionalProperties: false\n      description: UpdateComponentGroupRequest is the request to update a component group.\n    openstatus.status_page.v1.UpdateComponentGroupResponse:\n      type: object\n      properties:\n        group:\n          title: group\n          description: The updated component group.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n      title: UpdateComponentGroupResponse\n      additionalProperties: false\n      description: UpdateComponentGroupResponse is the response after updating a component group.\n    openstatus.status_page.v1.UpdateComponentRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the component to update (required).\n        name:\n          type:\n            - string\n            - \"null\"\n          title: name\n          maxLength: 256\n          description: New display name for the component (optional).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: New description for the component (optional).\n        order:\n          type:\n            - integer\n            - \"null\"\n          title: order\n          format: int32\n          description: New display order (optional).\n        groupId:\n          type:\n            - string\n            - \"null\"\n          title: group_id\n          description: New group ID (optional, set to empty string to remove from group).\n        groupOrder:\n          type:\n            - integer\n            - \"null\"\n          title: group_order\n          format: int32\n          description: New order within the group (optional).\n      title: UpdateComponentRequest\n      additionalProperties: false\n      description: UpdateComponentRequest is the request to update a component.\n    openstatus.status_page.v1.UpdateComponentResponse:\n      type: object\n      properties:\n        component:\n          title: component\n          description: The updated component.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n      title: UpdateComponentResponse\n      additionalProperties: false\n      description: UpdateComponentResponse is the response after updating a component.\n    openstatus.status_page.v1.UpdateStatusPageRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status page to update (required).\n        title:\n          type:\n            - string\n            - \"null\"\n          title: title\n          maxLength: 256\n          minLength: 1\n          description: New title for the status page (optional).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: New description for the status page (optional).\n        slug:\n          type:\n            - string\n            - \"null\"\n          title: slug\n          maxLength: 256\n          minLength: 1\n          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$\n          description: New slug for the status page (optional).\n        homepageUrl:\n          type:\n            - string\n            - \"null\"\n          title: homepage_url\n          description: New homepage URL (optional).\n        contactUrl:\n          type:\n            - string\n            - \"null\"\n          title: contact_url\n          description: New contact URL (optional).\n      title: UpdateStatusPageRequest\n      additionalProperties: false\n      description: UpdateStatusPageRequest is the request to update a status page.\n    openstatus.status_page.v1.UpdateStatusPageResponse:\n      type: object\n      properties:\n        statusPage:\n          title: status_page\n          description: The updated status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n      title: UpdateStatusPageResponse\n      additionalProperties: false\n      description: UpdateStatusPageResponse is the response after updating a status page.\n    openstatus.status_report.v1.AddStatusReportUpdateRequest:\n      type: object\n      properties:\n        statusReportId:\n          type: string\n          title: status_report_id\n          minLength: 1\n          description: ID of the status report to update (required).\n        status:\n          title: status\n          description: New status for the report (required).\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        message:\n          type: string\n          title: message\n          minLength: 1\n          description: Message describing what changed (required).\n        date:\n          type:\n            - string\n            - \"null\"\n          title: date\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: Optional date for the update (RFC 3339 format). Defaults to current time if not provided.\n        notify:\n          type:\n            - boolean\n            - \"null\"\n          title: notify\n          description: Whether to notify subscribers about this update (optional, defaults to false).\n      title: AddStatusReportUpdateRequest\n      additionalProperties: false\n      description: AddStatusReportUpdateRequest is the request to add a new update to a status report.\n    openstatus.status_report.v1.AddStatusReportUpdateResponse:\n      type: object\n      properties:\n        statusReport:\n          title: status_report\n          description: The updated status report with the new update included.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n      title: AddStatusReportUpdateResponse\n      additionalProperties: false\n      description: AddStatusReportUpdateResponse is the response after adding an update to a status report.\n    openstatus.status_report.v1.CreateStatusReportRequest:\n      type: object\n      properties:\n        title:\n          type: string\n          examples:\n            - API Degradation Investigation\n          title: title\n          minLength: 1\n          description: Title of the status report (required).\n        status:\n          title: status\n          description: Initial status (required).\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        message:\n          type: string\n          examples:\n            - We are investigating reports of increased API latency.\n          title: message\n          minLength: 1\n          description: Initial message describing the incident (required).\n        date:\n          type: string\n          examples:\n            - \"2024-03-15T10:30:00Z\"\n          title: date\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: Date when the event occurred (RFC 3339 format, required).\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: Page ID to associate with this report (required).\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: Page component IDs to associate with this report (optional).\n        notify:\n          type:\n            - boolean\n            - \"null\"\n          title: notify\n          description: Whether to notify subscribers about this status report (optional, defaults to false).\n      title: CreateStatusReportRequest\n      additionalProperties: false\n      description: CreateStatusReportRequest is the request to create a new status report.\n    openstatus.status_report.v1.CreateStatusReportResponse:\n      type: object\n      properties:\n        statusReport:\n          title: status_report\n          description: The created status report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n      title: CreateStatusReportResponse\n      additionalProperties: false\n      description: CreateStatusReportResponse is the response after creating a status report.\n    openstatus.status_report.v1.DeleteStatusReportRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status report to delete (required).\n      title: DeleteStatusReportRequest\n      additionalProperties: false\n      description: DeleteStatusReportRequest is the request to delete a status report.\n    openstatus.status_report.v1.DeleteStatusReportResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteStatusReportResponse\n      additionalProperties: false\n      description: DeleteStatusReportResponse is the response after deleting a status report.\n    openstatus.status_report.v1.GetStatusReportRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status report to retrieve (required).\n      title: GetStatusReportRequest\n      additionalProperties: false\n      description: GetStatusReportRequest is the request to get a status report by ID.\n    openstatus.status_report.v1.GetStatusReportResponse:\n      type: object\n      properties:\n        statusReport:\n          title: status_report\n          description: The requested status report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n      title: GetStatusReportResponse\n      additionalProperties: false\n      description: GetStatusReportResponse is the response containing the status report with its full update timeline.\n    openstatus.status_report.v1.ListStatusReportsRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of reports to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of reports to skip for pagination (defaults to 0).\n        statuses:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n          title: statuses\n          description: Filter by status (optional). If empty, returns all statuses.\n      title: ListStatusReportsRequest\n      additionalProperties: false\n      description: ListStatusReportsRequest is the request to list status reports.\n    openstatus.status_report.v1.ListStatusReportsResponse:\n      type: object\n      properties:\n        statusReports:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportSummary'\n          title: status_reports\n          description: List of status reports (metadata only, use GetStatusReport for full details).\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of reports matching the filter.\n      title: ListStatusReportsResponse\n      additionalProperties: false\n      description: ListStatusReportsResponse is the response containing status report summaries.\n    openstatus.status_report.v1.StatusReport:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the status report.\n        status:\n          title: status\n          description: Current status of the report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        title:\n          type: string\n          title: title\n          description: Title of the status report.\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: IDs of affected page components.\n        updates:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportUpdate'\n          title: updates\n          description: Timeline of updates for this report (only included in GetStatusReport).\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the report was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the report was last updated (RFC 3339 format).\n      title: StatusReport\n      additionalProperties: false\n      description: StatusReport represents an incident or maintenance report with full details.\n    openstatus.status_report.v1.StatusReportStatus:\n      type: string\n      title: StatusReportStatus\n      enum:\n        - STATUS_REPORT_STATUS_UNSPECIFIED\n        - STATUS_REPORT_STATUS_INVESTIGATING\n        - STATUS_REPORT_STATUS_IDENTIFIED\n        - STATUS_REPORT_STATUS_MONITORING\n        - STATUS_REPORT_STATUS_RESOLVED\n      description: StatusReportStatus represents the current state of a status report.\n    openstatus.status_report.v1.StatusReportSummary:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the status report.\n        status:\n          title: status\n          description: Current status of the report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        title:\n          type: string\n          title: title\n          description: Title of the status report.\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: IDs of affected page components.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the report was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the report was last updated (RFC 3339 format).\n      title: StatusReportSummary\n      additionalProperties: false\n      description: StatusReportSummary represents metadata for a status report (used in list responses).\n    openstatus.status_report.v1.StatusReportUpdate:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the update.\n        status:\n          title: status\n          description: Status at the time of this update.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        date:\n          type: string\n          title: date\n          description: Timestamp when this update occurred (RFC 3339 format).\n        message:\n          type: string\n          title: message\n          description: Message describing the update.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the update was created (RFC 3339 format).\n      title: StatusReportUpdate\n      additionalProperties: false\n      description: StatusReportUpdate represents a single update entry in a status report timeline.\n    openstatus.status_report.v1.UpdateStatusReportRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status report to update (required).\n        title:\n          type:\n            - string\n            - \"null\"\n          title: title\n          description: New title for the report (optional).\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: New list of page component IDs (optional, replaces existing list).\n      title: UpdateStatusReportRequest\n      additionalProperties: false\n      description: UpdateStatusReportRequest is the request to update a status report's metadata.\n    openstatus.status_report.v1.UpdateStatusReportResponse:\n      type: object\n      properties:\n        statusReport:\n          title: status_report\n          description: The updated status report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n      title: UpdateStatusReportResponse\n      additionalProperties: false\n      description: UpdateStatusReportResponse is the response after updating a status report.\nsecurity:\n  - ApiKeyAuth: []\ntags:\n  - name: MonitorService\n    description: |\n      Create, update, delete, and query monitors. Supports HTTP, TCP, and DNS monitor types\n      with configurable check intervals, regions, assertions, and alerting thresholds.\n  - name: StatusPageService\n    description: |\n      Manage public status pages with components, component groups, and email subscribers.\n      Includes endpoints for retrieving full page content and aggregated status.\n  - name: NotificationService\n    description: |\n      Configure notification channels (Slack, Discord, PagerDuty, email, webhooks, etc.)\n      and associate them with monitors. Supports 12 notification providers.\n  - name: StatusReportService\n    description: |\n      Create and manage incident reports with status updates. Reports follow a lifecycle:\n      investigating -> identified -> monitoring -> resolved.\n  - name: MaintenanceService\n    description: |\n      Schedule maintenance windows for status page components. Subscribers can be\n      notified automatically when maintenance is created.\n  - name: HealthService\n    description: Health check endpoint for load balancer probes. No authentication required.\npaths:\n  /rpc/openstatus.health.v1.HealthService/Check:\n    get:\n      tags:\n        - HealthService\n      summary: Check\n      description: Check returns the current serving status of the service.\n      operationId: HealthService_Check.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.health.v1.CheckRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.health.v1.CheckResponse'\n    post:\n      tags:\n        - HealthService\n      summary: Check\n      description: Check returns the current serving status of the service.\n      operationId: HealthService_Check\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.health.v1.CheckRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.health.v1.CheckResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/CreateMaintenance:\n    post:\n      tags:\n        - MaintenanceService\n      summary: CreateMaintenance\n      description: CreateMaintenance creates a new maintenance window.\n      operationId: MaintenanceService_CreateMaintenance\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.CreateMaintenanceRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.CreateMaintenanceResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/DeleteMaintenance:\n    post:\n      tags:\n        - MaintenanceService\n      summary: DeleteMaintenance\n      description: DeleteMaintenance removes a maintenance window.\n      operationId: MaintenanceService_DeleteMaintenance\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/GetMaintenance:\n    get:\n      tags:\n        - MaintenanceService\n      summary: GetMaintenance\n      description: GetMaintenance retrieves a specific maintenance window by ID.\n      operationId: MaintenanceService_GetMaintenance.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceResponse'\n    post:\n      tags:\n        - MaintenanceService\n      summary: GetMaintenance\n      description: GetMaintenance retrieves a specific maintenance window by ID.\n      operationId: MaintenanceService_GetMaintenance\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/ListMaintenances:\n    get:\n      tags:\n        - MaintenanceService\n      summary: ListMaintenances\n      description: ListMaintenances returns all maintenance windows for the workspace.\n      operationId: MaintenanceService_ListMaintenances.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesResponse'\n    post:\n      tags:\n        - MaintenanceService\n      summary: ListMaintenances\n      description: ListMaintenances returns all maintenance windows for the workspace.\n      operationId: MaintenanceService_ListMaintenances\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/UpdateMaintenance:\n    post:\n      tags:\n        - MaintenanceService\n      summary: UpdateMaintenance\n      description: UpdateMaintenance updates a maintenance window.\n      operationId: MaintenanceService_UpdateMaintenance\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/CreateDNSMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: CreateDNSMonitor\n      description: CreateDNSMonitor creates a new DNS monitor.\n      operationId: MonitorService_CreateDNSMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.CreateDNSMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.CreateDNSMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/CreateHTTPMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: CreateHTTPMonitor\n      description: Creates a new HTTP monitor in the authenticated workspace. Configure the target URL, HTTP method, request headers and body, response assertions (status code, body content, headers), check periodicity, geographic regions, and optional OpenTelemetry export. The monitor starts checking immediately if set to active.\n      operationId: MonitorService_CreateHTTPMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.CreateHTTPMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.CreateHTTPMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/CreateTCPMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: CreateTCPMonitor\n      description: CreateTCPMonitor creates a new TCP monitor.\n      operationId: MonitorService_CreateTCPMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.CreateTCPMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.CreateTCPMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/DeleteMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: DeleteMonitor\n      description: DeleteMonitor removes a monitor.\n      operationId: MonitorService_DeleteMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.DeleteMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.DeleteMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/GetMonitor:\n    get:\n      tags:\n        - MonitorService\n      summary: GetMonitor\n      description: |-\n        GetMonitor returns a single monitor by ID within the authenticated workspace.\n         Returns the monitor configuration (HTTP, TCP, or DNS) using the MonitorConfig oneof type.\n      operationId: MonitorService_GetMonitor.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorResponse'\n    post:\n      tags:\n        - MonitorService\n      summary: GetMonitor\n      description: |-\n        GetMonitor returns a single monitor by ID within the authenticated workspace.\n         Returns the monitor configuration (HTTP, TCP, or DNS) using the MonitorConfig oneof type.\n      operationId: MonitorService_GetMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/GetMonitorStatus:\n    get:\n      tags:\n        - MonitorService\n      summary: GetMonitorStatus\n      description: GetMonitorStatus returns the current status of all regions for a monitor.\n      operationId: MonitorService_GetMonitorStatus.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusResponse'\n    post:\n      tags:\n        - MonitorService\n      summary: GetMonitorStatus\n      description: GetMonitorStatus returns the current status of all regions for a monitor.\n      operationId: MonitorService_GetMonitorStatus\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/GetMonitorSummary:\n    get:\n      tags:\n        - MonitorService\n      summary: GetMonitorSummary\n      description: Returns aggregated metrics for a monitor including latency percentiles (p50, p75, p90, p95, p99), request counts by status (successful, degraded, failed), and the timestamp of the last check. Metrics can be scoped to a time range (1 day, 7 days, or 14 days) and filtered by specific regions.\n      operationId: MonitorService_GetMonitorSummary.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryResponse'\n    post:\n      tags:\n        - MonitorService\n      summary: GetMonitorSummary\n      description: Returns aggregated metrics for a monitor including latency percentiles (p50, p75, p90, p95, p99), request counts by status (successful, degraded, failed), and the timestamp of the last check. Metrics can be scoped to a time range (1 day, 7 days, or 14 days) and filtered by specific regions.\n      operationId: MonitorService_GetMonitorSummary\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/ListMonitors:\n    get:\n      tags:\n        - MonitorService\n      summary: ListMonitors\n      description: ListMonitors returns a paginated list of all monitors in the workspace.\n      operationId: MonitorService_ListMonitors.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsResponse'\n    post:\n      tags:\n        - MonitorService\n      summary: ListMonitors\n      description: ListMonitors returns a paginated list of all monitors in the workspace.\n      operationId: MonitorService_ListMonitors\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/TriggerMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: TriggerMonitor\n      description: Manually triggers an immediate check for the specified monitor across all configured regions. This operation is rate-limited under the synthetic-checks quota. A monitor run record is created and the check is dispatched to the checker service.\n      operationId: MonitorService_TriggerMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.TriggerMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.TriggerMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/UpdateDNSMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: UpdateDNSMonitor\n      description: UpdateDNSMonitor updates an existing DNS monitor.\n      operationId: MonitorService_UpdateDNSMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.UpdateDNSMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.UpdateDNSMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/UpdateHTTPMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: UpdateHTTPMonitor\n      description: UpdateHTTPMonitor updates an existing HTTP monitor.\n      operationId: MonitorService_UpdateHTTPMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.UpdateHTTPMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.UpdateHTTPMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/UpdateTCPMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: UpdateTCPMonitor\n      description: UpdateTCPMonitor updates an existing TCP monitor.\n      operationId: MonitorService_UpdateTCPMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.UpdateTCPMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.UpdateTCPMonitorResponse'\n  /rpc/openstatus.notification.v1.NotificationService/CheckNotificationLimit:\n    get:\n      tags:\n        - NotificationService\n      summary: CheckNotificationLimit\n      description: CheckNotificationLimit checks if the workspace has reached its notification limit.\n      operationId: NotificationService_CheckNotificationLimit.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitResponse'\n    post:\n      tags:\n        - NotificationService\n      summary: CheckNotificationLimit\n      description: CheckNotificationLimit checks if the workspace has reached its notification limit.\n      operationId: NotificationService_CheckNotificationLimit\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitResponse'\n  /rpc/openstatus.notification.v1.NotificationService/CreateNotification:\n    post:\n      tags:\n        - NotificationService\n      summary: CreateNotification\n      description: CreateNotification creates a new notification channel.\n      operationId: NotificationService_CreateNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.CreateNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.CreateNotificationResponse'\n  /rpc/openstatus.notification.v1.NotificationService/DeleteNotification:\n    post:\n      tags:\n        - NotificationService\n      summary: DeleteNotification\n      description: DeleteNotification removes a notification channel.\n      operationId: NotificationService_DeleteNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.DeleteNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.DeleteNotificationResponse'\n  /rpc/openstatus.notification.v1.NotificationService/GetNotification:\n    get:\n      tags:\n        - NotificationService\n      summary: GetNotification\n      description: GetNotification retrieves a notification channel by ID.\n      operationId: NotificationService_GetNotification.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationResponse'\n    post:\n      tags:\n        - NotificationService\n      summary: GetNotification\n      description: GetNotification retrieves a notification channel by ID.\n      operationId: NotificationService_GetNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationResponse'\n  /rpc/openstatus.notification.v1.NotificationService/ListNotifications:\n    get:\n      tags:\n        - NotificationService\n      summary: ListNotifications\n      description: ListNotifications returns a list of notification channels.\n      operationId: NotificationService_ListNotifications.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsResponse'\n    post:\n      tags:\n        - NotificationService\n      summary: ListNotifications\n      description: ListNotifications returns a list of notification channels.\n      operationId: NotificationService_ListNotifications\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsResponse'\n  /rpc/openstatus.notification.v1.NotificationService/SendTestNotification:\n    post:\n      tags:\n        - NotificationService\n      summary: SendTestNotification\n      description: Sends a test notification to the specified provider to verify that the configuration is correct. This does not require an existing notification channel - just provide the provider type and its configuration data. Returns success status and an error message if the test failed.\n      operationId: NotificationService_SendTestNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.SendTestNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.SendTestNotificationResponse'\n  /rpc/openstatus.notification.v1.NotificationService/UpdateNotification:\n    post:\n      tags:\n        - NotificationService\n      summary: UpdateNotification\n      description: UpdateNotification updates an existing notification channel.\n      operationId: NotificationService_UpdateNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.UpdateNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.UpdateNotificationResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/AddMonitorComponent:\n    post:\n      tags:\n        - StatusPageService\n      summary: AddMonitorComponent\n      description: AddMonitorComponent adds a monitor-based component to a status page.\n      operationId: StatusPageService_AddMonitorComponent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.AddMonitorComponentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.AddMonitorComponentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/AddStaticComponent:\n    post:\n      tags:\n        - StatusPageService\n      summary: AddStaticComponent\n      description: AddStaticComponent adds a static component to a status page.\n      operationId: StatusPageService_AddStaticComponent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.AddStaticComponentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.AddStaticComponentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/CreateComponentGroup:\n    post:\n      tags:\n        - StatusPageService\n      summary: CreateComponentGroup\n      description: CreateComponentGroup creates a new component group.\n      operationId: StatusPageService_CreateComponentGroup\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.CreateComponentGroupRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.CreateComponentGroupResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/CreateStatusPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: CreateStatusPage\n      description: CreateStatusPage creates a new status page.\n      operationId: StatusPageService_CreateStatusPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.CreateStatusPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.CreateStatusPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/DeleteComponentGroup:\n    post:\n      tags:\n        - StatusPageService\n      summary: DeleteComponentGroup\n      description: DeleteComponentGroup removes a component group.\n      operationId: StatusPageService_DeleteComponentGroup\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.DeleteComponentGroupRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.DeleteComponentGroupResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/DeleteStatusPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: DeleteStatusPage\n      description: DeleteStatusPage removes a status page.\n      operationId: StatusPageService_DeleteStatusPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.DeleteStatusPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.DeleteStatusPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/GetOverallStatus:\n    get:\n      tags:\n        - StatusPageService\n      summary: GetOverallStatus\n      description: 'Returns the overall status of a status page along with individual component statuses. The overall status is computed from active status reports and maintenances with the following priority: degraded (from active status reports) > maintenance (from active maintenance windows) > operational.'\n      operationId: StatusPageService_GetOverallStatus.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: GetOverallStatus\n      description: 'Returns the overall status of a status page along with individual component statuses. The overall status is computed from active status reports and maintenances with the following priority: degraded (from active status reports) > maintenance (from active maintenance windows) > operational.'\n      operationId: StatusPageService_GetOverallStatus\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPage:\n    get:\n      tags:\n        - StatusPageService\n      summary: GetStatusPage\n      description: GetStatusPage retrieves a specific status page by ID.\n      operationId: StatusPageService_GetStatusPage.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: GetStatusPage\n      description: GetStatusPage retrieves a specific status page by ID.\n      operationId: StatusPageService_GetStatusPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPageContent:\n    get:\n      tags:\n        - StatusPageService\n      summary: GetStatusPageContent\n      description: 'Returns the full content of a status page including its components, component groups, active status reports, and scheduled maintenances. Supports two access paths: by ID (requires authentication, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n      operationId: StatusPageService_GetStatusPageContent.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: GetStatusPageContent\n      description: 'Returns the full content of a status page including its components, component groups, active status reports, and scheduled maintenances. Supports two access paths: by ID (requires authentication, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n      operationId: StatusPageService_GetStatusPageContent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/ListStatusPages:\n    get:\n      tags:\n        - StatusPageService\n      summary: ListStatusPages\n      description: ListStatusPages returns all status pages for the workspace.\n      operationId: StatusPageService_ListStatusPages.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: ListStatusPages\n      description: ListStatusPages returns all status pages for the workspace.\n      operationId: StatusPageService_ListStatusPages\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/ListSubscribers:\n    get:\n      tags:\n        - StatusPageService\n      summary: ListSubscribers\n      description: ListSubscribers returns all subscribers for a status page.\n      operationId: StatusPageService_ListSubscribers.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: ListSubscribers\n      description: ListSubscribers returns all subscribers for a status page.\n      operationId: StatusPageService_ListSubscribers\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/RemoveComponent:\n    post:\n      tags:\n        - StatusPageService\n      summary: RemoveComponent\n      description: RemoveComponent removes a component from a status page.\n      operationId: StatusPageService_RemoveComponent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.RemoveComponentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.RemoveComponentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/SubscribeToPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: SubscribeToPage\n      description: Subscribes an email address to receive notifications from a status page. If the email was previously unsubscribed, the subscription is reactivated instead of creating a duplicate.\n      operationId: StatusPageService_SubscribeToPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.SubscribeToPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.SubscribeToPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/UnsubscribeFromPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: UnsubscribeFromPage\n      description: UnsubscribeFromPage removes a subscription from a status page.\n      operationId: StatusPageService_UnsubscribeFromPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.UnsubscribeFromPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.UnsubscribeFromPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/UpdateComponent:\n    post:\n      tags:\n        - StatusPageService\n      summary: UpdateComponent\n      description: UpdateComponent updates an existing component.\n      operationId: StatusPageService_UpdateComponent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/UpdateComponentGroup:\n    post:\n      tags:\n        - StatusPageService\n      summary: UpdateComponentGroup\n      description: UpdateComponentGroup updates an existing component group.\n      operationId: StatusPageService_UpdateComponentGroup\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentGroupRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentGroupResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/UpdateStatusPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: UpdateStatusPage\n      description: UpdateStatusPage updates an existing status page.\n      operationId: StatusPageService_UpdateStatusPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.UpdateStatusPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.UpdateStatusPageResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/AddStatusReportUpdate:\n    post:\n      tags:\n        - StatusReportService\n      summary: AddStatusReportUpdate\n      description: 'Adds a new update entry to an existing status report and transitions the report to the specified status. Status reports follow a lifecycle: investigating -> identified -> monitoring -> resolved. If notify is true, subscribers of the associated page are notified by email about the update.'\n      operationId: StatusReportService_AddStatusReportUpdate\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.AddStatusReportUpdateRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.AddStatusReportUpdateResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/CreateStatusReport:\n    post:\n      tags:\n        - StatusReportService\n      summary: CreateStatusReport\n      description: Creates a new status report with an initial update entry. The report is associated with a status page and optionally specific page components. An initial StatusReportUpdate is created automatically with the provided status, message, and date. If notify is true, subscribers of the associated page are notified by email.\n      operationId: StatusReportService_CreateStatusReport\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.CreateStatusReportRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.CreateStatusReportResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/DeleteStatusReport:\n    post:\n      tags:\n        - StatusReportService\n      summary: DeleteStatusReport\n      description: DeleteStatusReport removes a status report and all its updates.\n      operationId: StatusReportService_DeleteStatusReport\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/GetStatusReport:\n    get:\n      tags:\n        - StatusReportService\n      summary: GetStatusReport\n      description: GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n      operationId: StatusReportService_GetStatusReport.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportResponse'\n    post:\n      tags:\n        - StatusReportService\n      summary: GetStatusReport\n      description: GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n      operationId: StatusReportService_GetStatusReport\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/ListStatusReports:\n    get:\n      tags:\n        - StatusReportService\n      summary: ListStatusReports\n      description: ListStatusReports returns all status reports for the workspace (metadata only).\n      operationId: StatusReportService_ListStatusReports.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsResponse'\n    post:\n      tags:\n        - StatusReportService\n      summary: ListStatusReports\n      description: ListStatusReports returns all status reports for the workspace (metadata only).\n      operationId: StatusReportService_ListStatusReports\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/UpdateStatusReport:\n    post:\n      tags:\n        - StatusReportService\n      summary: UpdateStatusReport\n      description: UpdateStatusReport updates the metadata of a status report (title, page components).\n      operationId: StatusReportService_UpdateStatusReport\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportResponse'\n"
  },
  {
    "path": "apps/server/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\", \"**/*.ts\"],\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"target\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"jsxImportSource\": \"react\",\n    \"allowJs\": true,\n    \"types\": [\"bun-types\"],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "apps/ssh-server/.dockerignore",
    "content": "**/.ssh\n"
  },
  {
    "path": "apps/ssh-server/.gitignore",
    "content": ".ssh/\n"
  },
  {
    "path": "apps/ssh-server/Dockerfile",
    "content": "ARG GO_VERSION=1\nFROM golang:1.25.1-alpine as builder\n\nWORKDIR /usr/src/app\nCOPY go.mod go.sum ./\nRUN go mod download && go mod verify\nCOPY . .\nRUN go build -v -o /ssh-status-server .\n\n\nFROM debian:bookworm\n\nCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\nCOPY --from=builder /ssh-status-server /app/ssh-status-server\n\nCMD [\"/app/ssh-status-server\"]\n"
  },
  {
    "path": "apps/ssh-server/banner.txt",
    "content": "                            _        _\n                           | |      | |\n  ___  _ __   ___ _ __  ___| |_ __ _| |_ _   _ ___\n / _ \\| '_ \\ / _ \\ '_ \\/ __| __/ _` | __| | | / __|\n| (_) | |_) |  __/ | | \\__ \\ || (_| | |_| |_| \\__ \\\n \\___/| .__/ \\___|_| |_|___/\\__\\__,_|\\__|\\__,_|___/\n      | |\n      |_|\n"
  },
  {
    "path": "apps/ssh-server/fly.toml",
    "content": "# fly.toml app configuration file generated for ssh-server-status on 2025-09-16T08:46:39+02:00\n#\n# See https://fly.io/docs/reference/configuration/ for information about how to use this file.\n#\n\napp = 'ssh-server-status'\nprimary_region = 'cdg'\n\n[build]\n  dockerfile = './Dockerfile'\n\n\n\n[[services]]\n    internal_port = 2222\n    protocol = \"tcp\"\n    auto_stop_machines = true\n    auto_start_machines = true\n    [[services.ports]]\n      port = 22\n\n\n[env]\n    PORT = \"2222\"\n\n[mounts]\n    source = \"ssh_key\"\n    destination = \"/data\"\n\n\n[[vm]]\n  size = 'shared-cpu-1x'\n  memory = '256MB'\n"
  },
  {
    "path": "apps/ssh-server/go.mod",
    "content": "module github.com/openstatusHQ/openstatus/apps/ssh-server\n\ngo 1.25.1\n\nrequire github.com/gliderlabs/ssh v0.3.8\n\nrequire (\n\tgithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect\n\tgolang.org/x/crypto v0.42.0 // indirect\n\tgolang.org/x/sys v0.36.0 // indirect\n)\n"
  },
  {
    "path": "apps/ssh-server/go.sum",
    "content": "github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=\ngithub.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=\ngolang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=\ngolang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=\ngolang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=\ngolang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=\ngolang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=\n"
  },
  {
    "path": "apps/ssh-server/main.go",
    "content": "package main\n\nimport (\n\t_ \"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/gliderlabs/ssh\"\n)\n\n//go:embed banner.txt\nvar banner string\n\nfunc bannerfunc(ctx ssh.Context) string {\n\treturn banner\n}\n\nvar statusOk = `\n+----------------------------------+\n|                                  |\n|       All Systems Operational    |\n|                                  |\n+----------------------------------+\n`\nvar statusDegraded = `\n+----------------------------------+\n|                                  |\n|       System is degraded         |\n|                                  |\n+----------------------------------+\n`\nvar statusPartialOutage = `\n+----------------------------------+\n|                                  |\n|   System is partially out of     |\n|       service                    |\n|                                  |\n+----------------------------------+\n`\nvar statusMajorOutage = `\n+----------------------------------+\n|                                  |\n|       System is out of service   |\n|                                  |\n+----------------------------------+\n`\nvar statusUnderMaintenance = `\n+----------------------------------+\n|                                  |\n|       System is under            |\n|       maintenance                |\n|                                  |\n+----------------------------------+\n`\nvar statusIncident = `\n+----------------------------------+\n|                                  |\n|   System is partially out of     |\n|       service                    |\n|                                  |\n+----------------------------------+\n`\n\ntype status struct {\n\tStatus string `json:\"status\"`\n}\n\nfunc handler(s ssh.Session) {\n\turl := fmt.Sprintf(\"https://api.openstatus.dev/public/status/%s\", s.User())\n\tres, err := http.Get(url)\n\tif err != nil {\n\t\tfmt.Fprintf(s, \"Error fetching status: %v\\n\", err)\n\t\treturn\n\t}\n\tdefer res.Body.Close()\n\tvar status status\n\tjson.NewDecoder(res.Body).Decode(&status)\n\n\tvar currentStatus string\n\tswitch status.Status {\n\tcase \"operational\":\n\t\tcurrentStatus = statusOk\n\tcase \"degraded_performance\":\n\t\tcurrentStatus = statusDegraded\n\tcase \"partial_outage\":\n\tcurrentStatus = statusPartialOutage\n\tcase \"major_outage\":\n\t\tcurrentStatus = statusMajorOutage\n\tcase \"under_maintenance\":\n\t\tcurrentStatus = statusUnderMaintenance\n\tcase \"incident\":\n\t\tcurrentStatus = statusIncident\n\tdefault:\n\t\tcurrentStatus = \"\"\n\t}\n\n\tif currentStatus == \"\" {\n\t\tio.WriteString(s, \"Unknown status page\")\n\t\treturn\n\t}\n\n\tio.WriteString(s, fmt.Sprintf(\"\\nCurrent Status for: %s\\n\\n%s\\n\\nVisit the status page at https://%s.openstatus.dev/\\n\\n\", s.User(), currentStatus, s.User()))\n}\n\nfunc main() {\n\n\tserver := &ssh.Server{\n\t\tAddr:          \":2222\",\n\t\tBannerHandler: bannerfunc,\n\t\tHandler:       handler,\n\n\t}\n\tssh.HostKeyFile(\"/data/id_rsa\")\n\n\tlog.Println(\"starting ssh server on port 2222...\")\n\tlog.Fatal(server.ListenAndServe())\n\n\n}\n"
  },
  {
    "path": "apps/status-page/.dockerignore",
    "content": "# This file is generated by Dofigen v2.5.1\n# See https://github.com/lenra-io/dofigen\n\n"
  },
  {
    "path": "apps/status-page/.gitignore",
    "content": ".vercel\n"
  },
  {
    "path": "apps/status-page/Dockerfile",
    "content": "# syntax=docker/dockerfile:1.11\n# This file is generated by Dofigen v2.5.1\n# See https://github.com/lenra-io/dofigen\n\n# builder\nFROM node@sha256:0afb7822fac7bf9d7c1bf3b6e6c496dee6b2b64d8dfa365501a3c68e8eba94b2 AS builder\nLABEL \\\n    org.opencontainers.image.base.digest=\"sha256:0afb7822fac7bf9d7c1bf3b6e6c496dee6b2b64d8dfa365501a3c68e8eba94b2\" \\\n    org.opencontainers.image.base.name=\"docker.io/node:24-slim\"\nENV \\\n    PATH=\"$PNPM_HOME:$PATH\" \\\n    CRON_SECRET=\"test\" \\\n    RESEND_API_KEY=\"test\" \\\n    STRIPE_SECRET_KEY=\"test\" \\\n    TINY_BIRD_API_KEY=\"test\" \\\n    TEAM_ID_VERCEL=\"test\" \\\n    PROJECT_ID_VERCEL=\"test\" \\\n    SELF_HOST=\"true\" \\\n    NODE_ENV=\"production\" \\\n    NEXT_PUBLIC_OPENPANEL_CLIENT_ID=\"test\" \\\n    OPENPANEL_CLIENT_SECRET=\"test\" \\\n    NEXT_PUBLIC_URL=\"http://localhost:3002\" \\\n    UNKEY_TOKEN=\"test\" \\\n    RANDOM_YOLO=\"YOLO\" \\\n    UPSTASH_REDIS_REST_TOKEN=\"test\" \\\n    UNKEY_API_ID=\"test\" \\\n    DATABASE_URL=\"http://libsql:8080\" \\\n    UPSTASH_REDIS_REST_URL=\"test\" \\\n    VERCEL_AUTH_BEARER_TOKEN=\"test\" \\\n    PNPM_HOME=\"/pnpm\" \\\n    DATABASE_AUTH_TOKEN=\"test\"\nWORKDIR /app\nCOPY \\\n    --link \\\n    \".\" \"/app/\"\nRUN <<EOF\ncorepack enable\npnpm install --frozen-lockfile\npnpm turbo run build --filter=@openstatus/status-page\nEOF\n\n# runtime\nFROM node@sha256:0afb7822fac7bf9d7c1bf3b6e6c496dee6b2b64d8dfa365501a3c68e8eba94b2 AS runtime\nLABEL \\\n    io.dofigen.version=\"2.5.1\" \\\n    org.opencontainers.image.base.digest=\"sha256:0afb7822fac7bf9d7c1bf3b6e6c496dee6b2b64d8dfa365501a3c68e8eba94b2\" \\\n    org.opencontainers.image.base.name=\"docker.io/node:24-slim\"\nWORKDIR /app/apps/status-page\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --chmod=555 \\\n    --link \\\n    \"/app/apps/status-page/.next/standalone/apps/status-page/\" \"./\"\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/app/node_modules/\" \"/app/node_modules/\"\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/app/apps/status-page/.next/static/\" \"./.next/static/\"\nCOPY \\\n    --from=builder \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/app/apps/status-page/public/\" \"./public/\"\nUSER 0:0\nRUN <<EOF\napt-get update\napt-get install -y --no-install-recommends curl\nrm -rf /var/lib/apt/lists/*\nEOF\nUSER 1000:1000\nEXPOSE 3000\nHEALTHCHECK \\\n    --interval=30s \\\n    --timeout=10s \\\n    --start-period=45s \\\n    --retries=3 \\\n    CMD curl -f http://localhost:3000/ || exit 1\nCMD [\"node\", \"server.js\"]\n"
  },
  {
    "path": "apps/status-page/README.md",
    "content": "# openstatus status page\n\n\n## Theme Store dev\n\n```sh\npnpm install\n```\n\n\n\n```sh\npnpm run env\n```\n\n```sh\npnpm dev\n```\n"
  },
  {
    "path": "apps/status-page/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"src/app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@openstatus/ui/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}\n"
  },
  {
    "path": "apps/status-page/docker-compose.yaml",
    "content": "name: server\nservices:\n    server:\n        build:\n         context: ../..\n         dockerfile: apps/status-page/Dockerfile\n        ports:\n            - 3000:3000\n        image: status-page\n        env_file:\n            - ../../.env.docker\n        command: .\n"
  },
  {
    "path": "apps/status-page/dofigen.yml",
    "content": "builders:\n  # Stage 1: Next.js build with Node.js\n  builder:\n    fromImage: node:24-slim\n    workdir: /app\n    copy:\n      - . /app/\n    env:\n      NODE_ENV: production\n      PNPM_HOME: /pnpm\n      PATH: $PNPM_HOME:$PATH\n      # Build-time environment variables (placeholder values, overwritten by .env.docker at runtime)\n      DATABASE_URL: http://libsql:8080\n      DATABASE_AUTH_TOKEN: test\n      NEXT_PUBLIC_OPENPANEL_CLIENT_ID: test\n      NEXT_PUBLIC_URL: http://localhost:3002\n      TEAM_ID_VERCEL: test\n      PROJECT_ID_VERCEL: test\n      VERCEL_AUTH_BEARER_TOKEN: test\n      OPENPANEL_CLIENT_SECRET: test\n      RESEND_API_KEY: test\n      UPSTASH_REDIS_REST_URL: test\n      UPSTASH_REDIS_REST_TOKEN: test\n      UNKEY_TOKEN: test\n      UNKEY_API_ID: test\n      TINY_BIRD_API_KEY: test\n      CRON_SECRET: test\n      STRIPE_SECRET_KEY: test\n      SELF_HOST: \"true\"\n\n    run:\n      - corepack enable\n      - pnpm install --frozen-lockfile\n      - pnpm turbo run build --filter=@openstatus/status-page\n\n# Runtime stage\nfromImage: node:24-slim\nworkdir: /app/apps/status-page\n\n# Copy artifacts from builder\ncopy:\n  # Copy Next.js standalone output\n  - fromBuilder: builder\n    source: /app/apps/status-page/.next/standalone/apps/status-page/\n    target: ./\n    chmod: \"555\"\n  # Copy root node_modules (required for pnpm symlinks)\n  - fromBuilder: builder\n    source: /app/node_modules/\n    target: /app/node_modules/\n  # Copy static assets\n  - fromBuilder: builder\n    source: /app/apps/status-page/.next/static/\n    target: ./.next/static/\n  # Copy public directory\n  - fromBuilder: builder\n    source: /app/apps/status-page/public/\n    target: ./public/\n\n# Install curl for health checks\nroot:\n  run:\n    - apt-get update\n    - apt-get install -y --no-install-recommends curl\n    - rm -rf /var/lib/apt/lists/*\n\n# Security: run as non-root user\nuser: \"1000:1000\"\n\n# Expose port\nexpose: \"3000\"\n\n# Health check\nhealthcheck:\n  interval: 30s\n  timeout: 10s\n  start: 45s\n  retries: 3\n  cmd: curl -f http://localhost:3000/ || exit 1\n\n# Start application\ncmd:\n  - node\n  - server.js\n"
  },
  {
    "path": "apps/status-page/env.ts",
    "content": "const file = Bun.file(\"./.env.example\");\nawait Bun.write(\"./.env\", file);\n"
  },
  {
    "path": "apps/status-page/instrumentation-client.ts",
    "content": "// This file configures the initialization of Sentry on the client.\n// The config you add here will be used whenever a users loads a page in their browser.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\n\nimport * as Sentry from \"@sentry/nextjs\";\n\nSentry.init({\n  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_FRONTEND,\n\n  // Adjust this value in production, or use tracesSampler for greater control\n  tracesSampleRate: 0.5,\n\n  // Setting this option to true will print useful information to the console while you're setting up Sentry.\n  debug: false,\n\n  replaysOnErrorSampleRate: 1.0,\n\n  // This sets the sample rate to be 10%. You may want this to be 100% while\n  // in development and sample at a lower rate in production\n  replaysSessionSampleRate: 0.1,\n\n  // You can remove this option if you're not planning to use the Sentry Session Replay feature:\n  integrations: [\n    Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),\n    Sentry.captureConsoleIntegration({ levels: [\"error\"] }),\n  ],\n});\n\nexport const onRouterTransitionStart = Sentry.captureRouterTransitionStart;\n\nexport const onRequestError = Sentry.captureRequestError;\n"
  },
  {
    "path": "apps/status-page/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "apps/status-page/next.config.ts",
    "content": "import { withSentryConfig } from \"@sentry/nextjs\";\n\nimport type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  output: process.env.SELF_HOST === \"true\" ? \"standalone\" : undefined,\n  experimental: {\n    authInterrupts: true,\n  },\n  images: {\n    remotePatterns: [\n      new URL(\"https://openstatus.dev/**\"),\n      new URL(\"https://**.public.blob.vercel-storage.com/**\"),\n    ],\n  },\n  logging: {\n    fetches: {\n      fullUrl: true,\n    },\n  },\n  async rewrites() {\n    return {\n      beforeFiles: [\n        {\n          source:\n            \"/:path((?!api|assets|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)\",\n          has: [\n            {\n              type: \"host\",\n              value:\n                process.env.NODE_ENV === \"production\"\n                  ? \"(?<subdomain>[^.]+).stpg.dev\"\n                  : \"(?<subdomain>[^.]+).localhost\",\n            },\n          ],\n          missing: [\n            // Skip this rewrite when the request came via proxy from web app\n            {\n              type: \"header\",\n              key: \"x-proxy\",\n              value: \"1\",\n            },\n            {\n              type: \"host\",\n              value:\n                process.env.NODE_ENV === \"production\"\n                  ? \"www.stpg.dev\"\n                  : \"localhost\",\n            },\n          ],\n          destination: \"/:subdomain/:path*\",\n        },\n      ],\n    };\n  },\n};\n\n// For detailed options, refer to the official documentation:\n// - Webpack plugin options: https://github.com/getsentry/sentry-webpack-plugin#options\n// - Next.js Sentry setup guide: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/\nconst sentryConfig = {\n  // Prevent log output unless running in a CI environment (helps reduce noise in logs)\n  silent: !process.env.CI,\n  org: \"openstatus\",\n  project: \"openstatus\",\n  authToken: process.env.SENTRY_AUTH_TOKEN,\n\n  // Upload a larger set of source maps for improved stack trace accuracy (increases build time)\n  widenClientFileUpload: true,\n\n  // If set to true, transpiles Sentry SDK to be compatible with IE11 (increases bundle size)\n  transpileClientSDK: false,\n\n  // Tree-shake Sentry logger statements to reduce bundle size\n  webpack: {\n    treeshake: {\n      removeDebugLogging: true,\n    },\n  },\n};\n\nexport default withSentryConfig(nextConfig, sentryConfig);\n"
  },
  {
    "path": "apps/status-page/package.json",
    "content": "{\n  \"name\": \"@openstatus/status-page\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"env\": \"bun env.ts\",\n    \"dev\": \"next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@auth/core\": \"0.40.0\",\n    \"@auth/drizzle-adapter\": \"1.10.0\",\n    \"@date-fns/tz\": \"1.2.0\",\n    \"@date-fns/utc\": \"2.1.0\",\n    \"@dnd-kit/core\": \"6.3.1\",\n    \"@dnd-kit/modifiers\": \"9.0.0\",\n    \"@dnd-kit/sortable\": \"10.0.0\",\n    \"@dnd-kit/utilities\": \"3.2.2\",\n    \"@hookform/devtools\": \"4.4.0\",\n    \"@hookform/resolvers\": \"5.1.0\",\n    \"@libsql/client\": \"0.15.15\",\n    \"@openpanel/nextjs\": \"1.2.0\",\n    \"@openstatus/analytics\": \"workspace:*\",\n    \"@openstatus/api\": \"workspace:*\",\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/emails\": \"workspace:*\",\n    \"@openstatus/error\": \"workspace:*\",\n    \"@openstatus/react\": \"workspace:*\",\n    \"@openstatus/theme-store\": \"workspace:*\",\n    \"@openstatus/tinybird\": \"workspace:*\",\n    \"@openstatus/tracker\": \"workspace:*\",\n    \"@openstatus/ui\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:*\",\n    \"@radix-ui/react-dropdown-menu\": \"2.1.15\",\n    \"@radix-ui/react-hover-card\": \"1.1.14\",\n    \"@sentry/nextjs\": \"10.31.0\",\n    \"@stripe/stripe-js\": \"2.1.6\",\n    \"@tanstack/react-query\": \"5.81.5\",\n    \"@tanstack/react-table\": \"8.21.3\",\n    \"@trpc/client\": \"11.4.4\",\n    \"@trpc/next\": \"11.4.4\",\n    \"@trpc/react-query\": \"11.4.4\",\n    \"@trpc/server\": \"11.4.4\",\n    \"@trpc/tanstack-react-query\": \"11.4.4\",\n    \"class-variance-authority\": \"0.7.1\",\n    \"clsx\": \"2.1.1\",\n    \"cmdk\": \"1.1.1\",\n    \"date-fns\": \"3.6.0\",\n    \"feed\": \"4.2.2\",\n    \"lucide-react\": \"0.525.0\",\n    \"next\": \"16.1.6\",\n    \"next-auth\": \"5.0.0-beta.29\",\n    \"next-plausible\": \"3.12.5\",\n    \"next-themes\": \"0.4.6\",\n    \"nuqs\": \"2.8.5\",\n    \"react\": \"19.2.3\",\n    \"react-day-picker\": \"8.10.1\",\n    \"react-dom\": \"19.2.3\",\n    \"react-hook-form\": \"7.68.0\",\n    \"recharts\": \"2.15.0\",\n    \"rehype-react\": \"8.0.0\",\n    \"remark-gfm\": \"4.0.1\",\n    \"remark-parse\": \"11.0.0\",\n    \"remark-rehype\": \"11.1.2\",\n    \"sonner\": \"2.0.5\",\n    \"superjson\": \"2.2.2\",\n    \"tailwind-merge\": \"3.3.1\",\n    \"unified\": \"11.0.5\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"4.1.11\",\n    \"@tailwindcss/typography\": \"0.5.10\",\n    \"@types/dom-speech-recognition\": \"0.0.6\",\n    \"@types/node\": \"24.0.8\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"shadcn\": \"3.8.4\",\n    \"tailwindcss\": \"4.1.11\",\n    \"tw-animate-css\": \"1.3.4\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "apps/status-page/postcss.config.mjs",
    "content": "const config = {\n  plugins: [\"@tailwindcss/postcss\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "apps/status-page/sentry.edge.config.ts",
    "content": "// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).\n// The config you add here will be used whenever one of the edge features is loaded.\n// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\nimport * as Sentry from \"@sentry/nextjs\";\nimport { TRPCError } from \"@trpc/server\";\n\n// tRPC error codes that should not be reported to Sentry (expected client errors)\nconst IGNORED_TRPC_CODES: TRPCError[\"code\"][] = [\n  \"UNAUTHORIZED\",\n  \"NOT_FOUND\",\n  \"BAD_REQUEST\",\n];\n\nSentry.init({\n  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,\n\n  // Adjust this value in production, or use tracesSampler for greater control\n  tracesSampleRate: 0,\n\n  // Setting this option to true will print useful information to the console while you're setting up Sentry.\n  debug: false,\n  integrations: [Sentry.captureConsoleIntegration({ levels: [\"error\"] })],\n\n  beforeSend(event, hint) {\n    if (\n      hint.originalException instanceof TRPCError &&\n      IGNORED_TRPC_CODES.includes(hint.originalException.code)\n    ) {\n      return null;\n    }\n    return event;\n  },\n});\n"
  },
  {
    "path": "apps/status-page/sentry.server.config.ts",
    "content": "// This file configures the initialization of Sentry on the server.\n// The config you add here will be used whenever the server handles a request.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\n\nimport * as Sentry from \"@sentry/nextjs\";\nimport { TRPCError } from \"@trpc/server\";\n\n// tRPC error codes that should not be reported to Sentry (expected client errors)\nconst IGNORED_TRPC_CODES: TRPCError[\"code\"][] = [\n  \"UNAUTHORIZED\",\n  \"NOT_FOUND\",\n  \"BAD_REQUEST\",\n];\n\nSentry.init({\n  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,\n\n  // Adjust this value in production, or use tracesSampler for greater control\n  tracesSampleRate: 0.2,\n\n  // Setting this option to true will print useful information to the console while you're setting up Sentry.\n  debug: false,\n  integrations: [Sentry.captureConsoleIntegration({ levels: [\"error\"] })],\n\n  beforeSend(event, hint) {\n    if (\n      hint.originalException instanceof TRPCError &&\n      IGNORED_TRPC_CODES.includes(hint.originalException.code)\n    ) {\n      return null;\n    }\n    return event;\n  },\n});\n"
  },
  {
    "path": "apps/status-page/src/app/(public)/client.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  Section,\n  SectionDescription,\n  SectionGroup,\n  SectionGroupHeader,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { recomputeStyles } from \"@/components/status-page/floating-button\";\nimport {\n  Status,\n  StatusContent,\n  StatusDescription,\n  StatusHeader,\n  StatusTitle,\n} from \"@/components/status-page/status\";\nimport { StatusBanner } from \"@/components/status-page/status-banner\";\nimport { StatusMonitor } from \"@/components/status-page/status-monitor\";\nimport { ThemePalettePicker } from \"@/components/themes/theme-palette-picker\";\nimport { ThemeSelect } from \"@/components/themes/theme-select\";\nimport { monitors } from \"@/data/monitors\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { THEMES, THEME_KEYS } from \"@openstatus/theme-store\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { useSidebar } from \"@openstatus/ui/components/ui/sidebar\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useTheme } from \"next-themes\";\nimport { useQueryStates } from \"nuqs\";\nimport { useEffect, useState } from \"react\";\nimport { searchParamsParsers } from \"./search-params\";\n\nconst MAIN_COLORS = [\n  { key: \"--primary\", label: \"Primary\" },\n  { key: \"--success\", label: \"Operational\" },\n  { key: \"--destructive\", label: \"Error\" },\n  { key: \"--warning\", label: \"Degraded\" },\n  { key: \"--info\", label: \"Maintenance\" },\n] as const;\n\n// TODO: add keyboard navigation for selection?\n\nexport function Client() {\n  const { resolvedTheme } = useTheme();\n  const [isMounted, setIsMounted] = useState(false);\n  const [{ q, t }, setSearchParams] = useQueryStates(searchParamsParsers);\n  const theme = t ? THEMES[t as keyof typeof THEMES] : undefined;\n  const { toggleSidebar } = useSidebar();\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  useEffect(() => {\n    if (isMounted && t) {\n      recomputeStyles(t);\n    }\n  }, [t, isMounted]);\n\n  return (\n    <SectionGroup>\n      <SectionGroupHeader>\n        <h1 className=\"font-bold text-2xl md:text-4xl\">\n          Status Page Theme Explorer\n        </h1>\n        <h2 className=\"font-medium text-muted-foreground md:text-lg\">\n          View all the openstatus themes for your status page and learn how to\n          create your own theme.\n        </h2>\n      </SectionGroupHeader>\n      <Section>\n        <SectionHeader>\n          <SectionTitle>Explorer</SectionTitle>\n          <SectionDescription>\n            Search for your favorite status page theme.{\" \"}\n            <Link href=\"#contribute-theme\">Contribute your own?</Link>\n          </SectionDescription>\n        </SectionHeader>\n        <div className=\"sticky top-0 z-10 overflow-hidden rounded-lg border border-border bg-background outline-[3px] outline-background sm:relative\">\n          <div className=\"relative\">\n            <div className=\"absolute top-0 right-0 rounded-bl-lg border-border border-b border-l bg-muted/50 px-2 py-0.5 text-[10px]\">\n              {theme?.name}\n            </div>\n            <div className=\"sm:p-8\">\n              <ThemePlaygroundStatus className=\"scale-80 sm:scale-100\" />\n            </div>\n          </div>\n        </div>\n        <div className=\"flex gap-3\">\n          <ThemeSelect className=\"min-w-[125px] max-w-[125px]\" />\n          <Input\n            placeholder={`Search from ${THEME_KEYS.length} themes`}\n            value={q ?? \"\"}\n            onChange={(e) => {\n              if (e.target.value.length === 0) {\n                setSearchParams({ q: null });\n              }\n              setSearchParams({ q: e.target.value.trim().toLowerCase() });\n            }}\n          />\n          <ThemePalettePicker />\n        </div>\n        <ul className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n          {THEME_KEYS.filter((k) => {\n            const theme = THEMES[k];\n            return (\n              theme.author.name\n                .toLowerCase()\n                .includes(q?.toLowerCase() ?? \"\") ||\n              theme.name.toLowerCase().includes(q?.toLowerCase() ?? \"\")\n            );\n          }).map((k) => {\n            const theme = THEMES[k];\n            const style = isMounted\n              ? theme[resolvedTheme as \"dark\" | \"light\"]\n              : undefined;\n\n            return (\n              <li key={k} className=\"group/theme-card space-y-1.5\">\n                <div\n                  data-active={k === t}\n                  data-slot=\"theme-card\"\n                  data-theme={k}\n                  className=\"relative h-40 cursor-pointer overflow-hidden rounded-md border border-border outline-none transition-all focus:outline-ring/50 focus:ring-2 focus:ring-ring/50 data-[active=true]:border-ring data-[active=true]:outline-[3px] data-[active=true]:outline-ring/50\"\n                  onClick={() => setSearchParams({ t: k })}\n                  role=\"button\"\n                  tabIndex={0}\n                  onKeyDown={(e) => {\n                    if (e.key === \"Enter\" || e.key === \" \") {\n                      setSearchParams({ t: k });\n                    }\n                  }}\n                >\n                  {isMounted ? (\n                    <div\n                      className=\"absolute h-full w-full bg-background text-foreground\"\n                      style={style as React.CSSProperties}\n                      inert\n                    >\n                      <ThemePlaygroundStatus className=\"pointer-events-none scale-80\" />\n                    </div>\n                  ) : (\n                    <Skeleton className=\"absolute h-full w-full\" />\n                  )}\n                </div>\n                <div className=\"flex items-start justify-between gap-2\">\n                  <div className=\"space-y-0.5 truncate\">\n                    <div className=\"truncate font-medium text-foreground text-sm leading-none\">\n                      {theme.name}\n                    </div>\n                    <div className=\"font-mono text-xs\">\n                      <Link\n                        href={theme.author.url}\n                        target=\"_blank\"\n                        rel=\"noopener noreferrer\"\n                        className=\"text-muted-foreground\"\n                      >\n                        by {theme.author.name}\n                      </Link>\n                    </div>\n                  </div>\n                  <div className=\"flex gap-0.5\">\n                    {MAIN_COLORS.map((color) => {\n                      const backgroundColor = style\n                        ? style[color.key]\n                        : undefined;\n\n                      if (!isMounted) {\n                        return (\n                          <Skeleton\n                            key={color.key}\n                            className=\"size-3.5 rounded-sm\"\n                          />\n                        );\n                      }\n                      return (\n                        <TooltipProvider key={color.key}>\n                          <Tooltip>\n                            <TooltipTrigger>\n                              <div\n                                className=\"size-3.5 rounded-sm border bg-muted-foreground\"\n                                style={{ backgroundColor }}\n                              />\n                            </TooltipTrigger>\n                            <TooltipContent>{color.label}</TooltipContent>\n                          </Tooltip>\n                        </TooltipProvider>\n                      );\n                    })}\n                  </div>\n                </div>\n              </li>\n            );\n          })}\n        </ul>\n      </Section>\n      <Separator />\n      <Section>\n        <SectionHeader id=\"contribute-theme\">\n          <SectionTitle>Contribute Theme</SectionTitle>\n          <SectionDescription>\n            Contribute your own theme to the community.\n          </SectionDescription>\n        </SectionHeader>\n        <div className=\"prose dark:prose-invert prose-sm max-w-none\">\n          <p>\n            You can contribute your own theme by creating a new file in the{\" \"}\n            <code>@openstatus/theme-store</code> package. You&apos;ll only need\n            to override css variables. If you are familiar with shadcn, you'll\n            know the trick (it also allows you to override `--radius`). Make\n            sure your object is satisfying the <code>Theme</code> interface. We\n            provide a theme builder to help you with the process.\n          </p>\n          <Button onClick={toggleSidebar}>Toggle Theme Builder</Button>\n          <p>\n            Go to the{\" \"}\n            <Link href=\"https://github.com/openstatusHQ/openstatus/tree/main/packages/theme-store\">\n              GitHub directory\n            </Link>{\" \"}\n            to see the existing themes and create a new one by forking and\n            creating a pull request.\n          </p>\n          <p>\n            Once you're done, you can test it by adding the following snippet to\n            your status page:\n          </p>\n          <pre>\n            <code>sessionStorage.setItem(\"community-theme\", \"true\");</code>\n          </pre>\n          <p>\n            Or use the following button to test it on the `status` page slug:\n          </p>\n          <Button\n            onClick={() => {\n              // NOTE: we use it to display the 'floating-theme' component\n              sessionStorage.setItem(\"community-theme\", \"true\");\n              window.location.href = \"/status\";\n            }}\n          >\n            Test it\n          </Button>\n          {/* TODO: OR go to the status-page config and click on the View and Configure button */}\n        </div>\n      </Section>\n      <Separator />\n      <Section>\n        <div className=\"prose dark:prose-invert prose-sm max-w-none\">\n          <p>\n            Why don't we allow custom css styles to be overridden and only\n            support themes?\n          </p>\n          <ul>\n            <li>Keep it simple for the user</li>\n            <li>Don't end up with a xmas tree</li>\n            <li>Keep the theme consistent</li>\n            <li>Avoid conflicts with other styles</li>\n            <li>\n              Keep the theme maintainable (but this will also mean, a change\n              will affect all users)\n            </li>\n          </ul>\n        </div>\n      </Section>\n    </SectionGroup>\n  );\n}\n\nfunction ThemePlaygroundStatus({\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {}) {\n  const trpc = useTRPC();\n  const { data: uptimeData, isLoading } = useQuery(\n    trpc.statusPage.getNoopUptime.queryOptions(),\n  );\n  return (\n    // NOTE: we use pointer-events-none to prevent the hover card or tooltip from being interactive - the Portal container is document body and we loose the styles\n    <div className={cn(\"h-full w-full\", className)} {...props}>\n      <Status variant=\"success\">\n        <StatusHeader>\n          <StatusTitle>Acme Inc.</StatusTitle>\n          <StatusDescription>\n            Get informed about our services.\n          </StatusDescription>\n        </StatusHeader>\n        <StatusBanner status=\"success\" />\n        <StatusContent>\n          {/* TODO: create mock data */}\n          <StatusMonitor\n            status=\"success\"\n            data={uptimeData?.data || []}\n            monitor={monitors[0]}\n            showUptime={true}\n            uptime={uptimeData?.uptime}\n            isLoading={isLoading}\n          />\n        </StatusContent>\n      </Status>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(public)/layout.tsx",
    "content": "import { Link } from \"@/components/common/link\";\nimport { ThemeProvider } from \"@/components/themes/theme-provider\";\nimport {\n  SidebarTrigger,\n  ThemeSidebar,\n} from \"@/components/themes/theme-sidebar\";\nimport { generateThemeStyles } from \"@openstatus/theme-store\";\nimport {\n  SidebarInset,\n  SidebarProvider,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport { Toaster } from \"@openstatus/ui/components/ui/sonner\";\nimport PlausibleProvider from \"next-plausible\";\nimport { Suspense } from \"react\";\n\nconst SIDEBAR_WIDTH = \"20rem\";\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <PlausibleProvider domain=\"themes.openstatus.dev\">\n      <style\n        id=\"theme-styles\"\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n        dangerouslySetInnerHTML={{ __html: generateThemeStyles() }}\n      />\n      <ThemeProvider attribute=\"class\" enableSystem disableTransitionOnChange>\n        <SidebarProvider\n          defaultOpen={true}\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH,\n              \"--sidebar-width-mobile\": SIDEBAR_WIDTH_MOBILE,\n            } as React.CSSProperties\n          }\n        >\n          <SidebarInset className=\"relative\">\n            <SidebarTrigger className=\"absolute top-2 right-2\" />\n            <main className=\"mx-auto\">{children}</main>\n            <footer className=\"flex items-center justify-center gap-4 p-4 text-center font-mono text-muted-foreground text-sm\">\n              <p>\n                powered by <Link href=\"https://openstatus.dev\">openstatus</Link>\n              </p>\n            </footer>\n          </SidebarInset>\n          <Suspense>\n            <ThemeSidebar />\n          </Suspense>\n        </SidebarProvider>\n        <Toaster richColors expand />\n      </ThemeProvider>\n    </PlausibleProvider>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(public)/page.tsx",
    "content": "import type { SearchParams } from \"nuqs\";\nimport { Client } from \"./client\";\nimport { searchParamsCache } from \"./search-params\";\n\nexport default async function Page({\n  searchParams,\n}: {\n  searchParams: Promise<SearchParams>;\n}) {\n  await searchParamsCache.parse(searchParams);\n  return <Client />;\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(public)/search-params.ts",
    "content": "import { THEME_KEYS } from \"@openstatus/theme-store\";\nimport {\n  createSearchParamsCache,\n  parseAsBoolean,\n  parseAsString,\n  parseAsStringEnum,\n} from \"nuqs/server\";\n\nexport const searchParamsParsers = {\n  q: parseAsString, // q = query\n  t: parseAsStringEnum(THEME_KEYS).withDefault(\"default\"), // t = theme\n  b: parseAsBoolean.withDefault(false), // b = builder\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(auth)/layout.tsx",
    "content": "import { Footer } from \"@/components/nav/footer\";\nimport { getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Suspense } from \"react\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ domain: string }>;\n}) {\n  const queryClient = getQueryClient();\n  const { domain } = await params;\n  await queryClient.prefetchQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  return (\n    <Suspense>\n      <div className=\"flex min-h-screen flex-col gap-4\">\n        <main className=\"mx-auto flex w-full max-w-2xl flex-1 flex-col px-3 py-2\">\n          {children}\n        </main>\n        <Footer className=\"w-full border-t\" />\n      </div>\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(auth)/login/_components/section-magic-link.tsx",
    "content": "\"use client\";\n\nimport {\n  EmptyStateContainer,\n  EmptyStateDescription,\n  EmptyStateTitle,\n} from \"@/components/content/empty-state\";\nimport {\n  Section,\n  SectionDescription,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormEmail, type FormValues } from \"@/components/forms/form-email\";\nimport { generateServerActionPromise } from \"@/lib/server-actions\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Inbox } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\nimport { useState } from \"react\";\nimport { flushSync } from \"react-dom\";\nimport { signInWithResendAction } from \"../actions\";\n\nexport function SectionMagicLink() {\n  const { domain } = useParams<{ domain: string }>();\n  const [state, setState] = useState<\"idle\" | \"pending\" | \"success\">(\"idle\");\n\n  async function submitAction(values: FormValues) {\n    // NOTE: we can improve a bit if we use pathname instead of subdomain/hostname\n    // like http://localhost:3000/hello, the redirectTo should be http://localhost:3000/hello\n    // this only affects local development if not using chrome and subdomain\n    const redirectTo =\n      process.env.NODE_ENV === \"development\"\n        ? `http://${window.location.hostname}:${window.location.port}`\n        : `https://${window.location.hostname}`;\n\n    const formData = new FormData();\n    formData.append(\"redirectTo\", redirectTo);\n    formData.append(\"email\", values.email);\n    formData.append(\"domain\", domain);\n\n    // we need this because submitAction is called  in a startTransition and we need to update the state immediately\n    flushSync(() => setState(\"pending\"));\n\n    try {\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n      await generateServerActionPromise(signInWithResendAction(formData));\n      setState(\"success\");\n    } catch (error) {\n      setState(\"idle\");\n      throw error;\n    }\n  }\n\n  return (\n    <Section className=\"m-auto w-full max-w-lg rounded-lg border bg-card p-4\">\n      <SectionHeader>\n        <SectionTitle>Authenticate</SectionTitle>\n        <SectionDescription>\n          Enter your email to receive a magic link for accessing the status\n          page. Note: Only emails from approved domains are accepted.\n        </SectionDescription>\n      </SectionHeader>\n      {state !== \"success\" ? (\n        <div className=\"flex flex-col gap-2\">\n          <FormEmail id=\"email-form\" onSubmit={submitAction} />\n          <Button\n            type=\"submit\"\n            form=\"email-form\"\n            disabled={state === \"pending\"}\n          >\n            {state === \"pending\" ? \"Submitting...\" : \"Submit\"}\n          </Button>\n        </div>\n      ) : (\n        <SuccessState />\n      )}\n    </Section>\n  );\n}\n\nfunction SuccessState() {\n  return (\n    <EmptyStateContainer>\n      <Inbox className=\"size-4 shrink-0\" />\n      <EmptyStateTitle>Check your inbox!</EmptyStateTitle>\n      <EmptyStateDescription>\n        Access the status page by clicking the link in the email.\n      </EmptyStateDescription>\n    </EmptyStateContainer>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(auth)/login/_components/section-password.tsx",
    "content": "\"use client\";\n\nimport {\n  Section,\n  SectionDescription,\n  SectionHeader,\n  SectionTitle,\n} from \"@/components/content/section\";\nimport { FormPassword } from \"@/components/forms/form-password\";\nimport { createProtectedCookieKey } from \"@/lib/protected\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useCookieState } from \"@openstatus/ui/hooks/use-cookie-state\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { useParams, useRouter, useSearchParams } from \"next/navigation\";\n\nexport function SectionPassword() {\n  const { domain } = useParams<{ domain: string }>();\n  const searchParams = useSearchParams();\n  const trpc = useTRPC();\n  const [_, setPassword] = useCookieState(createProtectedCookieKey(domain));\n  const router = useRouter();\n  const verifyPasswordMutation = useMutation(\n    trpc.statusPage.verifyPassword.mutationOptions({}),\n  );\n\n  return (\n    <Section className=\"m-auto w-full max-w-lg rounded-lg border bg-card p-4\">\n      <SectionHeader>\n        <SectionTitle>Protected Page</SectionTitle>\n        <SectionDescription>\n          Enter the password to access the status page.\n        </SectionDescription>\n      </SectionHeader>\n      <div className=\"flex flex-col gap-2\">\n        <FormPassword\n          id=\"password-form\"\n          onSubmit={async (values) => {\n            const result = await verifyPasswordMutation.mutateAsync({\n              slug: domain,\n              password: values.password,\n            });\n            if (result) {\n              setPassword(values.password);\n              const redirect = searchParams.get(\"redirect\");\n              // Only allow safe relative paths to prevent XSS via javascript: URLs\n              if (redirect?.startsWith(\"/\") && !redirect.startsWith(\"//\")) {\n                router.push(redirect);\n              } else {\n                router.push(\"/\");\n              }\n            }\n          }}\n        />\n        <Button type=\"submit\" form=\"password-form\">\n          Submit\n        </Button>\n      </div>\n    </Section>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(auth)/login/actions.ts",
    "content": "\"use server\";\n\nimport { signIn } from \"@/lib/auth\";\nimport { getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport { AuthError } from \"next-auth\";\nimport { isRedirectError } from \"next/dist/client/components/redirect-error\";\n\nexport async function signInWithResendAction(formData: FormData) {\n  try {\n    const email = formData.get(\"email\") as string;\n    const redirectTo = formData.get(\"redirectTo\") as string;\n    const domain = formData.get(\"domain\") as string;\n\n    if (!email || !redirectTo) {\n      return {\n        success: false,\n        error: \"Email and redirectTo are required\",\n      };\n    }\n\n    const queryClient = getQueryClient();\n    // NOTE: throws an error if the email domain is not allowed\n    try {\n      await queryClient.fetchQuery(\n        trpc.statusPage.validateEmailDomain.queryOptions({\n          slug: domain,\n          email,\n        }),\n      );\n    } catch (error) {\n      console.error(\"[SignIn] Email validation failed\", error);\n      if (error instanceof TRPCClientError) {\n        return { success: false, error: error.message };\n      }\n      if (error instanceof Error) {\n        return { success: false, error: error.message };\n      }\n      return {\n        success: false,\n        error: \"An unexpected error occurred during sign in\",\n      };\n    }\n\n    await signIn(\"resend\", {\n      email,\n      redirectTo,\n    });\n\n    return { success: true };\n  } catch (e) {\n    // NOTE: https://github.com/nextauthjs/next-auth/discussions/9389\n    if (isRedirectError(e)) {\n      return { success: true };\n    }\n    console.error(\"[SignIn] Error:\", e);\n    if (e instanceof AuthError) {\n      return { success: false, error: e.type };\n    }\n    if (e instanceof Error) {\n      return { success: false, error: e.message };\n    }\n\n    return {\n      success: false,\n      error: \"An unexpected error occurred during sign in\",\n    };\n  }\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(auth)/login/page.tsx",
    "content": "\"use client\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { notFound, useParams } from \"next/navigation\";\nimport { SectionMagicLink } from \"./_components/section-magic-link\";\nimport { SectionPassword } from \"./_components/section-password\";\n\nexport default function LoginPage() {\n  const { domain } = useParams<{ domain: string }>();\n  const trpc = useTRPC();\n  const { data: page } = useQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  if (page?.accessType === \"password\") {\n    return <SectionPassword />;\n  }\n\n  if (page?.accessType === \"email-domain\") {\n    return <SectionMagicLink />;\n  }\n\n  return notFound();\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/badge/route.tsx",
    "content": "import { ImageResponse } from \"next/og\";\nimport type { NextRequest } from \"next/server\";\n\nimport type { Status } from \"@openstatus/react\";\nimport { getStatus } from \"@openstatus/react\";\n\n// Keep the `label` size within a maximum of 'Operational' to stay within the `SIZE` restriction\nconst statusDictionary: Record<Status, { label: string; color: string }> = {\n  operational: {\n    label: \"Operational\",\n    color: \"bg-green-500\",\n  },\n  degraded_performance: {\n    label: \"Degraded\",\n    color: \"bg-yellow-500\",\n  },\n  partial_outage: {\n    label: \"Outage\",\n    color: \"bg-yellow-500\",\n  },\n  major_outage: {\n    label: \"Outage\",\n    color: \"bg-red-500\",\n  },\n  unknown: {\n    label: \"Unknown\",\n    color: \"bg-gray-500\",\n  },\n  incident: {\n    label: \"Incident\",\n    color: \"bg-yellow-500\",\n  },\n  under_maintenance: {\n    label: \"Maintenance\",\n    color: \"bg-blue-500\",\n  },\n} as const;\n\n// const SIZE = { width: 120, height: 34 };\nconst SIZE: Record<string, { width: number; height: number }> = {\n  sm: { width: 120, height: 34 },\n  md: { width: 160, height: 46 },\n  lg: { width: 200, height: 56 },\n  xl: { width: 240, height: 68 },\n};\nexport async function GET(\n  req: NextRequest,\n  props: { params: Promise<{ domain: string }> },\n) {\n  const params = await props.params;\n  const { status } = await getStatus(params.domain);\n  const theme = req.nextUrl.searchParams.get(\"theme\");\n  const size = req.nextUrl.searchParams.get(\"size\");\n  const s = SIZE[size ?? \"sm\"] ?? SIZE.sm;\n  const { label, color } = statusDictionary[status];\n  const light = \"border-gray-200 text-gray-700 bg-white\";\n  const dark = \"border-gray-800 text-gray-300 bg-gray-900\";\n\n  return new ImageResponse(\n    <div\n      tw={`flex items-center justify-center rounded-md border px-3 py-1\n        ${size === \"sm\" && \"text-sm\"}${size === \"md\" && \"text-md\"} ${\n          size === \"lg\" && \"text-lg\"\n        } ${size === \"xl\" && \"text-xl\"} ${!size && \"text-sm\"} ${\n          theme === \"dark\" ? dark : light\n        }`}\n      style={{ ...s }}\n    >\n      {label}\n      <div tw={`flex h-2 w-2 rounded-full ml-2 ${color}`} />\n    </div>,\n    { ...s },\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/badge/v2/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\n\nimport type { Status } from \"@openstatus/react\";\nimport { getStatus } from \"@openstatus/react\";\n\nconst statusDictionary: Record<Status, { label: string; hexColor: string }> = {\n  operational: {\n    label: \"All Systems Operational\",\n    hexColor: \"#10b981\",\n  },\n  degraded_performance: {\n    label: \"Degraded Performance\",\n    hexColor: \"#f59e0b\",\n  },\n  partial_outage: {\n    label: \"Partial Outage\",\n    hexColor: \"#f59e0b\",\n  },\n  major_outage: {\n    label: \"Major Outage\",\n    hexColor: \"#ef4444\",\n  },\n  unknown: {\n    label: \"Unknown\",\n    hexColor: \"#6b7280\",\n  },\n  incident: {\n    label: \"Ongoing Incident\",\n    hexColor: \"#f59e0b\",\n  },\n  under_maintenance: {\n    label: \"Under Maintenance\",\n    hexColor: \"#3b82f6\",\n  },\n} as const;\n\nconst SIZE: Record<\n  string,\n  {\n    height: number;\n    padding: number;\n    gap: number;\n    radius: number;\n    fontSize: number;\n  }\n> = {\n  sm: { height: 34, padding: 8, gap: 12, radius: 4, fontSize: 12 },\n  md: { height: 46, padding: 8, gap: 12, radius: 4, fontSize: 14 },\n  lg: { height: 56, padding: 12, gap: 16, radius: 6, fontSize: 16 },\n  xl: { height: 68, padding: 12, gap: 16, radius: 6, fontSize: 18 },\n};\n\nfunction getTextWidth(text: string, fontSize: number): number {\n  const monoCharWidthRatio = 0.6;\n  return text.length * monoCharWidthRatio * fontSize;\n}\n\nexport async function GET(\n  req: NextRequest,\n  props: { params: Promise<{ domain: string }> },\n) {\n  const params = await props.params;\n  const { status } = await getStatus(params.domain);\n  const theme = req.nextUrl.searchParams.get(\"theme\") ?? \"light\";\n  const variant = req.nextUrl.searchParams.get(\"variant\") ?? \"default\";\n  const size = req.nextUrl.searchParams.get(\"size\") ?? \"sm\";\n\n  const { height, padding, gap, radius, fontSize } = SIZE[size] ?? SIZE.sm;\n  const { label, hexColor } = statusDictionary[status];\n  const textWidth = getTextWidth(label, fontSize);\n  const width = Math.ceil(padding + textWidth + gap + radius * 2 + padding);\n\n  const textColor = theme === \"dark\" ? \"#d1d5db\" : \"#374151\";\n  const bgColor = theme === \"dark\" ? \"#111827\" : \"#ffffff\";\n  const borderColor = variant === \"outline\" ? \"#d1d5db\" : \"transparent\";\n\n  const svg = `\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n    <rect x=\"0.5\" y=\"0.5\" width=\"${width - 1}\" height=\"${\n      height - 1\n    }\"  fill=\"${bgColor}\" stroke=\"${borderColor}\" stroke-width=\"1\" rx=\"${radius}\" ry=\"${radius}\" />\n      <text x=\"${padding}\" y=\"50%\" dominant-baseline=\"middle\"\n            font-family=\"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace\" font-size=\"${fontSize}\" font-weight=\"600\" fill=\"${textColor}\">\n        ${label}\n      </text>\n      <circle cx=\"${width - padding - radius}\" cy=\"${\n        height / 2\n      }\" r=\"${radius}\" fill=\"${hexColor}\"/>\n    </svg>\n  `;\n\n  return new Response(svg, {\n    headers: { \"Content-Type\": \"image/svg+xml\" },\n  });\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/events/(list)/page.tsx",
    "content": "\"use client\";\n\nimport { StatusBlankEvents } from \"@/components/status-page/status-blank\";\nimport {\n  StatusEvent,\n  StatusEventAffected,\n  StatusEventAffectedBadge,\n  StatusEventAside,\n  StatusEventContent,\n  StatusEventDate,\n  StatusEventGroup,\n  StatusEventTimelineMaintenance,\n  StatusEventTimelineReport,\n  StatusEventTitle,\n  StatusEventTitleCheck,\n} from \"@/components/status-page/status-events\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Link from \"next/link\";\nimport { useParams } from \"next/navigation\";\nimport { useQueryStates } from \"nuqs\";\nimport { searchParamsParsers } from \"./search-params\";\n\nexport default function Page() {\n  const [{ tab }, setSearchParams] = useQueryStates(searchParamsParsers);\n  const { domain } = useParams<{ domain: string }>();\n  const trpc = useTRPC();\n  const { data: page } = useQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  if (!page) return null;\n\n  const { statusReports, maintenances } = page;\n\n  return (\n    <Tabs\n      defaultValue={tab}\n      onValueChange={(value) =>\n        setSearchParams({ tab: value as \"reports\" | \"maintenances\" })\n      }\n      className=\"gap-4\"\n    >\n      <TabsList>\n        <TabsTrigger value=\"reports\">Reports</TabsTrigger>\n        <TabsTrigger value=\"maintenances\">Maintenances</TabsTrigger>\n      </TabsList>\n      <TabsContent value=\"reports\">\n        <StatusEventGroup>\n          {statusReports.length > 0 ? (\n            statusReports.map((report) => {\n              const updates = report.statusReportUpdates.sort(\n                (a, b) => b.date.getTime() - a.date.getTime(),\n              );\n              const firstUpdate = updates[updates.length - 1];\n              const lastUpdate = updates[0];\n              // NOTE: updates are sorted descending by date\n              const startedAt = firstUpdate.date;\n              // HACKY: LEGACY: only resolved via report and not via report update\n              const isReportResolvedOnly =\n                report.status === \"resolved\" &&\n                lastUpdate.status !== \"resolved\";\n              return (\n                <StatusEvent key={report.id}>\n                  <StatusEventAside>\n                    <StatusEventDate date={startedAt} />\n                  </StatusEventAside>\n                  <Link\n                    href={`./events/report/${report.id}`}\n                    className=\"rounded-lg\"\n                  >\n                    <StatusEventContent>\n                      <StatusEventTitle className=\"inline-flex gap-1\">\n                        {report.title}\n                        {isReportResolvedOnly ? (\n                          <StatusEventTitleCheck />\n                        ) : null}\n                      </StatusEventTitle>\n                      {report.statusReportsToPageComponents.length > 0 ? (\n                        <StatusEventAffected>\n                          {report.statusReportsToPageComponents.map(\n                            (affected) => (\n                              <StatusEventAffectedBadge\n                                key={affected.pageComponent.id}\n                              >\n                                {affected.pageComponent.name}\n                              </StatusEventAffectedBadge>\n                            ),\n                          )}\n                        </StatusEventAffected>\n                      ) : null}\n                      <StatusEventTimelineReport\n                        updates={report.statusReportUpdates}\n                        reportId={report.id}\n                      />\n                    </StatusEventContent>\n                  </Link>\n                </StatusEvent>\n              );\n            })\n          ) : (\n            <StatusBlankEvents />\n          )}\n        </StatusEventGroup>\n      </TabsContent>\n      <TabsContent value=\"maintenances\">\n        <StatusEventGroup>\n          {maintenances.length > 0 ? (\n            maintenances.map((maintenance) => {\n              return (\n                <StatusEvent key={maintenance.id}>\n                  <StatusEventAside>\n                    <StatusEventDate date={maintenance.from} />\n                  </StatusEventAside>\n                  <Link\n                    href={`./events/maintenance/${maintenance.id}`}\n                    className=\"rounded-lg\"\n                  >\n                    <StatusEventContent>\n                      <StatusEventTitle>{maintenance.title}</StatusEventTitle>\n                      {maintenance.maintenancesToPageComponents.length > 0 ? (\n                        <StatusEventAffected>\n                          {maintenance.maintenancesToPageComponents.map(\n                            (affected) => (\n                              <StatusEventAffectedBadge\n                                key={affected.pageComponent.id}\n                              >\n                                {affected.pageComponent.name}\n                              </StatusEventAffectedBadge>\n                            ),\n                          )}\n                        </StatusEventAffected>\n                      ) : null}\n                      <StatusEventTimelineMaintenance\n                        maintenance={maintenance}\n                      />\n                    </StatusEventContent>\n                  </Link>\n                </StatusEvent>\n              );\n            })\n          ) : (\n            <StatusBlankEvents\n              title=\"No maintenances found\"\n              description=\"No maintenances found for this status page.\"\n            />\n          )}\n        </StatusEventGroup>\n      </TabsContent>\n    </Tabs>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/events/(list)/search-params.ts",
    "content": "import { createSearchParamsCache, parseAsStringEnum } from \"nuqs/server\";\n\nexport const searchParamsParsers = {\n  tab: parseAsStringEnum([\"reports\", \"maintenances\"]).withDefault(\"reports\"),\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/maintenance/[id]/layout.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string; domain: string }>;\n}) {\n  const { id, domain } = await params;\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(\n    trpc.statusPage.getMaintenance.queryOptions({\n      id: Number(id),\n      slug: domain,\n    }),\n  );\n  return <HydrateClient>{children}</HydrateClient>;\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/maintenance/[id]/page.tsx",
    "content": "\"use client\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { ButtonBack } from \"@/components/button/button-back\";\nimport { ButtonCopyLink } from \"@/components/button/button-copy-link\";\nimport { StatusBlankEvents } from \"@/components/status-page/status-blank\";\nimport {\n  StatusEvent,\n  StatusEventAffected,\n  StatusEventAffectedBadge,\n  StatusEventAside,\n  StatusEventContent,\n  StatusEventDate,\n  StatusEventTimelineMaintenance,\n  StatusEventTitle,\n} from \"@/components/status-page/status-events\";\nimport { useParams } from \"next/navigation\";\n\nexport default function MaintenancePage() {\n  const trpc = useTRPC();\n  const { id, domain } = useParams<{ id: string; domain: string }>();\n  const { data: maintenance } = useQuery(\n    trpc.statusPage.getMaintenance.queryOptions({\n      id: Number(id),\n      slug: domain,\n    }),\n  );\n\n  if (!maintenance) {\n    return (\n      <StatusBlankEvents\n        title=\"Maintenance not found\"\n        description=\"The maintenance you are looking for does not exist.\"\n      />\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex w-full flex-row items-center justify-between gap-2 py-0.5\">\n        <ButtonBack href=\"../\" />\n        <ButtonCopyLink />\n      </div>\n      <StatusEvent>\n        <StatusEventAside>\n          <StatusEventDate date={maintenance.from} />\n        </StatusEventAside>\n        <StatusEventContent hoverable={false}>\n          <StatusEventTitle>{maintenance.title}</StatusEventTitle>\n          <StatusEventAffected>\n            {maintenance.maintenancesToPageComponents.map((affected) => (\n              <StatusEventAffectedBadge key={affected.pageComponent.id}>\n                {affected.pageComponent.name}\n              </StatusEventAffectedBadge>\n            ))}\n          </StatusEventAffected>\n          <StatusEventTimelineMaintenance maintenance={maintenance} />\n        </StatusEventContent>\n      </StatusEvent>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/report/[id]/layout.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ id: string; domain: string }>;\n}) {\n  const { id, domain } = await params;\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(\n    trpc.statusPage.getReport.queryOptions({\n      id: Number(id),\n      slug: domain,\n    }),\n  );\n  return <HydrateClient>{children}</HydrateClient>;\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/report/[id]/page.tsx",
    "content": "\"use client\";\n\nimport { ButtonBack } from \"@/components/button/button-back\";\nimport { ButtonCopyLink } from \"@/components/button/button-copy-link\";\nimport { StatusBlankEvents } from \"@/components/status-page/status-blank\";\nimport {\n  StatusEvent,\n  StatusEventAffected,\n  StatusEventAffectedBadge,\n  StatusEventAside,\n  StatusEventContent,\n  StatusEventDate,\n  StatusEventTimelineReport,\n  StatusEventTitle,\n  StatusEventTitleCheck,\n} from \"@/components/status-page/status-events\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function ReportPage() {\n  const trpc = useTRPC();\n  const { id, domain } = useParams<{ id: string; domain: string }>();\n  const { data: report } = useQuery(\n    trpc.statusPage.getReport.queryOptions({ id: Number(id), slug: domain }),\n  );\n\n  if (!report) {\n    return (\n      <StatusBlankEvents\n        title=\"Report not found\"\n        description=\"The report you are looking for does not exist.\"\n      />\n    );\n  }\n\n  const updates = report.statusReportUpdates.sort(\n    (a, b) => b.date.getTime() - a.date.getTime(),\n  );\n  const firstUpdate = updates[updates.length - 1];\n  const lastUpdate = updates[0];\n\n  // HACKY: LEGACY: only resolved via report and not via report update\n  const isReportResolvedOnly =\n    report.status === \"resolved\" && lastUpdate.status !== \"resolved\";\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex w-full flex-row items-center justify-between gap-2 py-0.5\">\n        <ButtonBack href=\"../\" />\n        <ButtonCopyLink />\n      </div>\n      <StatusEvent>\n        <StatusEventAside>\n          <StatusEventDate date={firstUpdate.date} />\n        </StatusEventAside>\n        <StatusEventContent hoverable={false}>\n          <StatusEventTitle className=\"inline-flex gap-1\">\n            {report.title}\n            {isReportResolvedOnly ? <StatusEventTitleCheck /> : null}\n          </StatusEventTitle>\n          {report.statusReportsToPageComponents.length > 0 ? (\n            <StatusEventAffected>\n              {report.statusReportsToPageComponents.map((affected) => (\n                <StatusEventAffectedBadge key={affected.pageComponent.id}>\n                  {affected.pageComponent.name}\n                </StatusEventAffectedBadge>\n              ))}\n            </StatusEventAffected>\n          ) : null}\n          <StatusEventTimelineReport\n            updates={report.statusReportUpdates}\n            reportId={report.id}\n          />\n        </StatusEventContent>\n      </StatusEvent>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/events/layout.tsx",
    "content": "\"use client\";\n\nimport {\n  Status,\n  StatusContent,\n  StatusDescription,\n  StatusHeader,\n  StatusTitle,\n} from \"@/components/status-page/status\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function EventLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const { domain } = useParams<{ domain: string }>();\n  const trpc = useTRPC();\n  const { data: page } = useQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  if (!page) return null;\n\n  return (\n    <Status>\n      <StatusHeader>\n        <StatusTitle>{page.title}</StatusTitle>\n        <StatusDescription>{page.description}</StatusDescription>\n      </StatusHeader>\n      <StatusContent>{children}</StatusContent>\n    </Status>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/feed/[type]/route.ts",
    "content": "import { auth } from \"@/lib/auth\";\nimport { getBaseUrl } from \"@/lib/base-url\";\nimport { getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { Feed } from \"feed\";\nimport { notFound, unauthorized } from \"next/navigation\";\n\nconst STATUS_LABELS = {\n  investigating: \"Investigating\",\n  identified: \"Identified\",\n  monitoring: \"Monitoring\",\n  resolved: \"Resolved\",\n  maintenance: \"Maintenance\",\n} as const;\n\nexport const revalidate = 60;\n\nexport async function GET(\n  _request: Request,\n  props: { params: Promise<{ domain: string; type: string }> },\n) {\n  try {\n    const queryClient = getQueryClient();\n    const { domain, type } = await props.params;\n\n    if (![\"rss\", \"atom\"].includes(type)) return notFound();\n\n    const _page = await queryClient.fetchQuery(\n      trpc.statusPage.getLight.queryOptions({ slug: domain }),\n    );\n    if (!_page) return notFound();\n\n    if (_page.accessType === \"password\") {\n      const url = new URL(_request.url);\n      const password = url.searchParams.get(\"pw\");\n      console.log({ url, _page, password });\n      if (password !== _page.password) return unauthorized();\n    }\n\n    if (_page.accessType === \"email-domain\") {\n      const session = await auth();\n      const user = session?.user;\n      const allowedDomains = _page.authEmailDomains ?? [];\n      if (!user || !user.email) return unauthorized();\n      if (!allowedDomains.includes(user.email.split(\"@\")[1]))\n        return unauthorized();\n    }\n\n    const page = await queryClient.fetchQuery(\n      trpc.statusPage.get.queryOptions({ slug: domain }),\n    );\n    if (!page) return notFound();\n\n    const baseUrl = getBaseUrl({\n      slug: page.slug,\n      customDomain: page.customDomain,\n    });\n\n    const feed = new Feed({\n      id: `${baseUrl}/feed/${type}`,\n      title: page.title,\n      description: page.description,\n      generator: \"OpenStatus - Status Page Updates\",\n      feedLinks: {\n        rss: `${baseUrl}/feed/rss`,\n        atom: `${baseUrl}/feed/atom`,\n      },\n      link: baseUrl,\n      author: {\n        name: page.title,\n        email:\n          page.contactUrl?.startsWith(\"mailto:\") && page.contactUrl !== null\n            ? page.contactUrl.slice(7)\n            : undefined,\n        link: page.homepageUrl || baseUrl,\n      },\n      copyright: `Copyright ${new Date()\n        .getFullYear()\n        .toString()} openstatus.dev`,\n      language: \"en-US\",\n      updated: new Date(),\n      ttl: 60,\n    });\n\n    for (const maintenance of page.maintenances ?? []) {\n      const maintenanceUrl = `${baseUrl}/events/maintenance/${maintenance.id}`;\n      feed.addItem({\n        id: maintenanceUrl,\n        title: `Maintenance - ${maintenance.title}`,\n        link: maintenanceUrl,\n        description: maintenance.message,\n        date: maintenance.updatedAt ?? maintenance.createdAt ?? new Date(),\n      });\n    }\n\n    for (const statusReport of page.statusReports ?? []) {\n      const statusReportUrl = `${baseUrl}/events/report/${statusReport.id}`;\n      const status = STATUS_LABELS[statusReport.status] ?? statusReport.status;\n      const statusReportUpdates = (statusReport.statusReportUpdates ?? [])\n        .map((update) => {\n          const updateStatus = STATUS_LABELS[update.status] ?? update.status;\n          return `${updateStatus}: ${update.message}.`;\n        })\n        .join(\"\\n\\n\");\n\n      feed.addItem({\n        id: statusReportUrl,\n        title: `${status} - ${statusReport.title}`,\n        link: statusReportUrl,\n        description: statusReportUpdates,\n        date: statusReport.updatedAt ?? statusReport.createdAt ?? new Date(),\n      });\n    }\n\n    feed.items.sort((a, b) => a.date.getTime() - b.date.getTime());\n\n    const res = type === \"atom\" ? feed.atom1() : feed.rss2();\n\n    return new Response(res, {\n      headers: {\n        \"Content-Type\": \"application/xml; charset=utf-8\",\n      },\n    });\n  } catch (error) {\n    console.error(\"Error generating feed:\", error);\n    throw error;\n  }\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/feed/json/route.ts",
    "content": "import { auth } from \"@/lib/auth\";\nimport { getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { notFound, unauthorized } from \"next/navigation\";\n\nexport const revalidate = 60;\n\nexport async function GET(\n  _request: Request,\n  props: { params: Promise<{ domain: string }> },\n) {\n  try {\n    const queryClient = getQueryClient();\n    const { domain } = await props.params;\n\n    const _page = await queryClient.fetchQuery(\n      trpc.statusPage.getLight.queryOptions({ slug: domain }),\n    );\n\n    if (!_page) return notFound();\n\n    if (_page.accessType === \"password\") {\n      const url = new URL(_request.url);\n      const password = url.searchParams.get(\"pw\");\n      console.log({ url, _page, password });\n      if (password !== _page.password) return unauthorized();\n    }\n\n    if (_page.accessType === \"email-domain\") {\n      const session = await auth();\n      const user = session?.user;\n      const allowedDomains = _page.authEmailDomains ?? [];\n      if (!user || !user.email) return unauthorized();\n      if (!allowedDomains.includes(user.email.split(\"@\")[1]))\n        return unauthorized();\n    }\n\n    const page = await queryClient.fetchQuery(\n      trpc.statusPage.get.queryOptions({ slug: domain }),\n    );\n\n    if (!page) return notFound();\n\n    const res = {\n      title: page.title,\n      description: page.description,\n      status: page.status,\n      updatedAt: new Date(),\n      // @deprecated Use pageComponents instead\n      monitors: page.monitors.map((monitor) => ({\n        id: monitor.id,\n        name: monitor.name,\n        description: monitor.description,\n        status: monitor.status,\n      })),\n      // New field - exposes the page component structure\n      pageComponents: page.pageComponents.map((component) => ({\n        id: component.id,\n        name: component.name,\n        description: component.description,\n        monitorId: component.monitorId,\n        order: component.order,\n        groupId: component.groupId,\n        groupOrder: component.groupOrder,\n      })),\n      pageComponentGroups: page.pageComponentGroups.map((group) => ({\n        id: group.id,\n        name: group.name,\n      })),\n      maintenances: page.maintenances.map((maintenance) => ({\n        id: maintenance.id,\n        name: maintenance.title,\n        message: maintenance.message,\n        from: maintenance.from,\n        to: maintenance.to,\n        updatedAt: maintenance.updatedAt,\n        // @deprecated Use components instead - returning monitor IDs for backwards compatibility\n        monitors: maintenance.maintenancesToPageComponents\n          .map((item) => item.pageComponent.monitorId)\n          .filter((id): id is number => id !== null),\n        // New field - references page component IDs\n        pageComponents: maintenance.maintenancesToPageComponents.map(\n          (item) => item.pageComponentId,\n        ),\n      })),\n      statusReports: page.statusReports.map((report) => ({\n        id: report.id,\n        title: report.title,\n        updatedAt: report.updatedAt,\n        status: report.status,\n        // @deprecated Use components instead - returning monitor IDs for backwards compatibility\n        monitors: report.statusReportsToPageComponents\n          .map((item) => item.pageComponent.monitorId)\n          .filter((id): id is number => id !== null),\n        // New field - references page component IDs\n        pageComponents: report.statusReportsToPageComponents.map(\n          (item) => item.pageComponentId,\n        ),\n        statusReportUpdates: report.statusReportUpdates.map((update) => ({\n          id: update.id,\n          status: update.status,\n          message: update.message,\n          date: update.date,\n          updatedAt: update.updatedAt,\n        })),\n      })),\n    };\n\n    return new Response(JSON.stringify(res), {\n      headers: {\n        \"Content-Type\": \"application/json; charset=utf-8\",\n      },\n    });\n  } catch (error) {\n    console.error(\"Error generating feed:\", error);\n    throw error;\n  }\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/layout.tsx",
    "content": "import { Footer } from \"@/components/nav/footer\";\nimport { Header } from \"@/components/nav/header\";\nimport { Suspense } from \"react\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <Suspense>\n      <div className=\"flex min-h-screen flex-col gap-4\">\n        <Header className=\"w-full border-b\" />\n        <main className=\"mx-auto flex w-full max-w-2xl flex-1 flex-col px-3 py-2\">\n          {children}\n        </main>\n        <Footer className=\"w-full border-t\" />\n      </div>\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/manage/[token]/layout.tsx",
    "content": "import { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ token: string; domain: string }>;\n}) {\n  const { token, domain } = await params;\n  const queryClient = getQueryClient();\n  await queryClient.prefetchQuery(\n    trpc.statusPage.getSubscriptionByToken.queryOptions({\n      token,\n      slug: domain,\n    }),\n  );\n  return <HydrateClient>{children}</HydrateClient>;\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/manage/[token]/page.tsx",
    "content": "\"use client\";\n\nimport { ButtonBack } from \"@/components/button/button-back\";\nimport {\n  FormCard,\n  FormCardContent,\n  FormCardDescription,\n  FormCardFooter,\n  FormCardFooterInfo,\n  FormCardHeader,\n  FormCardTitle,\n} from \"@/components/forms/form-card\";\nimport { FormManageSubscription } from \"@/components/forms/form-manage-subscription\";\nimport {\n  StatusBlankContainer,\n  StatusBlankContent,\n  StatusBlankDescription,\n  StatusBlankLink,\n  StatusBlankTitle,\n} from \"@/components/status-page/status-blank\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@openstatus/ui/components/ui/alert-dialog\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useParams } from \"next/navigation\";\nimport { toast } from \"sonner\";\n\nexport default function VerifyPage() {\n  const trpc = useTRPC();\n  const { token, domain } = useParams<{ token: string; domain: string }>();\n  const { data: page } = useQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n  const { data: subscription, refetch } = useQuery(\n    trpc.statusPage.getSubscriptionByToken.queryOptions({\n      slug: domain,\n      token,\n    }),\n  );\n  const manageSubscriptionMutation = useMutation(\n    trpc.statusPage.updateSubscription.mutationOptions({}),\n  );\n  const unsubscribeMutation = useMutation(\n    trpc.statusPage.unsubscribe.mutationOptions({\n      onSuccess: () => {\n        refetch();\n        toast.success(\"Unsubscribed successfully\");\n      },\n      onError: (error) => {\n        if (isTRPCClientError(error)) {\n          toast.error(error.message);\n        } else {\n          toast.error(\"Failed to unsubscribe\");\n        }\n      },\n    }),\n  );\n\n  if (!subscription)\n    return (\n      <StatusBlankContainer>\n        <StatusBlankContent>\n          <StatusBlankTitle>Invalid subscription token</StatusBlankTitle>\n          <StatusBlankDescription>\n            This subscription token is no longer valid. You may have already\n            unsubscribed or the link has expired.\n          </StatusBlankDescription>\n          <StatusBlankLink href=\"../\">Go back</StatusBlankLink>\n        </StatusBlankContent>\n      </StatusBlankContainer>\n    );\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex w-full flex-row items-center justify-between gap-2 py-0.5\">\n        <ButtonBack href=\"../\" />\n      </div>\n      <FormCard>\n        <FormCardHeader>\n          <FormCardTitle>{subscription.email}</FormCardTitle>\n          <FormCardDescription>\n            Manage your subscription to receive updates on the status page.\n          </FormCardDescription>\n        </FormCardHeader>\n        <FormCardContent className=\"px-0\">\n          <FormManageSubscription\n            id=\"manage-subscription-form\"\n            defaultValues={{\n              pageComponents: subscription?.componentIds ?? [],\n              subscribeComponents:\n                (subscription?.componentIds?.length ?? 0) > 0,\n            }}\n            page={page}\n            onSubmit={async (values) => {\n              await manageSubscriptionMutation.mutateAsync({\n                slug: domain,\n                token,\n                ...values,\n              });\n            }}\n          />\n        </FormCardContent>\n        <FormCardFooter>\n          <FormCardFooterInfo>\n            {subscription.unsubscribedAt ? (\n              <span className=\"text-destructive\">\n                Unsubscribed on{\" \"}\n                {Intl.DateTimeFormat(\"en-US\", {\n                  year: \"numeric\",\n                  month: \"long\",\n                  day: \"numeric\",\n                }).format(subscription.unsubscribedAt)}\n              </span>\n            ) : null}\n          </FormCardFooterInfo>\n          <div className=\"flex flex-row gap-2\">\n            <AlertDialog>\n              <AlertDialogTrigger asChild>\n                <Button\n                  size=\"sm\"\n                  variant=\"ghost\"\n                  className=\"text-destructive hover:bg-destructive/10 hover:text-destructive focus-visible:ring-destructive/20 dark:hover:bg-destructive/10\"\n                  disabled={\n                    unsubscribeMutation.isPending ||\n                    !!subscription.unsubscribedAt\n                  }\n                >\n                  {unsubscribeMutation.isPending\n                    ? \"Unsubscribing...\"\n                    : \"Unsubscribe\"}\n                </Button>\n              </AlertDialogTrigger>\n              <AlertDialogContent>\n                <AlertDialogHeader>\n                  <AlertDialogTitle>Unsubscribe</AlertDialogTitle>\n                  <AlertDialogDescription>\n                    Are you sure you want to unsubscribe from this status page?\n                    You will no longer receive updates.\n                  </AlertDialogDescription>\n                </AlertDialogHeader>\n                <AlertDialogFooter>\n                  <AlertDialogCancel>Cancel</AlertDialogCancel>\n                  <AlertDialogAction\n                    onClick={() =>\n                      unsubscribeMutation.mutate({ token, domain })\n                    }\n                  >\n                    Unsubscribe\n                  </AlertDialogAction>\n                </AlertDialogFooter>\n              </AlertDialogContent>\n            </AlertDialog>\n            <Button\n              size=\"sm\"\n              variant=\"outline\"\n              type=\"submit\"\n              form=\"manage-subscription-form\"\n              disabled={\n                manageSubscriptionMutation.isPending ||\n                !!subscription.unsubscribedAt\n              }\n            >\n              {manageSubscriptionMutation.isPending\n                ? \"Submitting...\"\n                : \"Submit\"}\n            </Button>\n          </div>\n        </FormCardFooter>\n      </FormCard>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/manage/layout.tsx",
    "content": "\"use client\";\n\nimport {\n  Status,\n  StatusContent,\n  StatusDescription,\n  StatusHeader,\n  StatusTitle,\n} from \"@/components/status-page/status\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function EventLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const { domain } = useParams<{ domain: string }>();\n  const trpc = useTRPC();\n  const { data: page } = useQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  if (!page) return null;\n\n  return (\n    <Status>\n      <StatusHeader>\n        <StatusTitle>{page.title}</StatusTitle>\n        <StatusDescription>{page.description}</StatusDescription>\n      </StatusHeader>\n      <StatusContent>{children}</StatusContent>\n    </Status>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/monitors/[id]/page.tsx",
    "content": "\"use client\";\n\nimport { ButtonBack } from \"@/components/button/button-back\";\nimport { ButtonCopyLink } from \"@/components/button/button-copy-link\";\nimport {\n  ChartAreaPercentiles,\n  ChartAreaPercentilesSkeleton,\n} from \"@/components/chart/chart-area-percentiles\";\nimport {\n  ChartBarUptime,\n  ChartBarUptimeSkeleton,\n} from \"@/components/chart/chart-bar-uptime\";\nimport {\n  ChartLineRegions,\n  ChartLineRegionsSkeleton,\n} from \"@/components/chart/chart-line-regions\";\nimport { PopoverQuantile } from \"@/components/popover/popover-quantile\";\nimport {\n  Status,\n  StatusContent,\n  StatusDescription,\n  StatusHeader,\n  StatusTitle,\n} from \"@/components/status-page/status\";\nimport { StatusBlankMonitors } from \"@/components/status-page/status-blank\";\nimport {\n  StatusChartContent,\n  StatusChartDescription,\n  StatusChartHeader,\n  StatusChartTitle,\n} from \"@/components/status-page/status-charts\";\nimport {\n  StatusMonitorTabs,\n  StatusMonitorTabsContent,\n  StatusMonitorTabsList,\n  StatusMonitorTabsTrigger,\n  StatusMonitorTabsTriggerLabel,\n  StatusMonitorTabsTriggerValue,\n  StatusMonitorTabsTriggerValueSkeleton,\n} from \"@/components/status-page/status-monitor-tabs\";\nimport {\n  formatMillisecondsRange,\n  formatNumber,\n  formatPercentage,\n} from \"@/lib/formatter\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { TrendingUp } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\nimport { useQueryStates } from \"nuqs\";\nimport { useMemo } from \"react\";\nimport { searchParamsParsers } from \"./search-params\";\n\nexport default function Page() {\n  const [{ tab }, setSearchParams] = useQueryStates(searchParamsParsers);\n  const trpc = useTRPC();\n  const { id, domain } = useParams<{ id: string; domain: string }>();\n  const { data: page } = useQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  const tempMonitor = useMemo(() => {\n    return page?.monitors.find((monitor) => monitor.id === Number(id));\n  }, [page, id]);\n\n  if (!page) return null;\n\n  const { data: monitor, isLoading } = useQuery(\n    trpc.statusPage.getMonitor.queryOptions({ id: Number(id), slug: domain }),\n  );\n\n  const globalLatencyData = useMemo(() => {\n    if (!monitor?.data.latency?.data) return [];\n\n    return monitor.data.latency.data\n      .sort((a, b) => a.timestamp - b.timestamp)\n      .map((item) => ({\n        ...item,\n        timestamp: new Date(item.timestamp).toLocaleString(\"default\", {\n          day: \"numeric\",\n          month: \"short\",\n          hour: \"numeric\",\n          minute: \"numeric\",\n          timeZoneName: \"short\",\n        }),\n      }));\n  }, [monitor?.data.latency?.data]);\n\n  const regionLatencyData = useMemo(() => {\n    if (!monitor?.data.regions?.data) return [];\n\n    const grouped = monitor.data.regions.data\n      .sort((a, b) => a.timestamp - b.timestamp)\n      .reduce(\n        (acc, item) => {\n          const timestamp = new Date(item.timestamp).toLocaleString(\"default\", {\n            day: \"numeric\",\n            month: \"short\",\n            hour: \"numeric\",\n            minute: \"numeric\",\n            timeZoneName: \"short\",\n          });\n\n          if (!acc[timestamp]) {\n            acc[timestamp] = { timestamp };\n          }\n          acc[timestamp][item.region] = item.p75Latency;\n          return acc;\n        },\n        {} as Record<\n          string,\n          { timestamp: string; [region: string]: number | string | null }\n        >,\n      );\n\n    return Object.values(grouped);\n  }, [monitor?.data.regions?.data]);\n\n  const uptimeData = useMemo(() => {\n    if (!monitor?.data.uptime?.data) return [];\n    return monitor.data.uptime.data\n      .sort((a, b) => a.interval.getTime() - b.interval.getTime())\n      .map((item) => ({\n        timestamp: item.interval.toLocaleString(\"default\", {\n          day: \"numeric\",\n          month: \"short\",\n          hour: \"numeric\",\n          minute: \"numeric\",\n          timeZoneName: \"short\",\n        }),\n        ...item,\n      }));\n  }, [monitor?.data.uptime?.data]);\n\n  const { totalChecks, uptimePercentage, slowestRegion, p75Range } =\n    useMemo(() => {\n      const p75Range = globalLatencyData.reduce(\n        (acc, item) => ({\n          min: Math.min(acc.min, item.p75Latency),\n          max: Math.max(acc.max, item.p75Latency),\n        }),\n        {\n          min: Number.POSITIVE_INFINITY,\n          max: Number.NEGATIVE_INFINITY,\n        },\n      );\n\n      const uptimeStats = uptimeData.reduce(\n        (acc, item) => {\n          return {\n            total: acc.total + item.success + item.degraded + item.error,\n            success: acc.success + item.success,\n            degraded: acc.degraded + item.degraded,\n            error: acc.error + item.error,\n          };\n        },\n        { total: 0, success: 0, degraded: 0, error: 0 },\n      );\n\n      const uptimePercentage =\n        uptimeStats.total > 0\n          ? (uptimeStats.success + uptimeStats.degraded) / uptimeStats.total\n          : 0;\n\n      const regionAverages = regionLatencyData.reduce(\n        (acc, item) => {\n          Object.keys(item).forEach((key) => {\n            if (key !== \"timestamp\" && typeof item[key] === \"number\") {\n              if (!acc[key]) {\n                acc[key] = { sum: 0, count: 0 };\n              }\n              acc[key].sum += item[key] as number;\n              acc[key].count += 1;\n            }\n          });\n          return acc;\n        },\n        {} as Record<string, { sum: number; count: number }>,\n      );\n\n      const slowestRegion = Object.entries(regionAverages)\n        .map(([region, stats]) => ({\n          region,\n          avgLatency: stats.count > 0 ? stats.sum / stats.count : 0,\n        }))\n        .sort((a, b) => b.avgLatency - a.avgLatency)[0];\n\n      return {\n        totalChecks: formatNumber(uptimeStats.total, {\n          notation: \"compact\",\n          compactDisplay: \"short\",\n        }).replace(\"K\", \"k\"),\n        uptimePercentage:\n          uptimeStats.total > 0 ? formatPercentage(uptimePercentage) : \"N/A\",\n        slowestRegion: slowestRegion?.region || \"N/A\",\n        p75Range:\n          p75Range.min !== Number.POSITIVE_INFINITY ||\n          p75Range.max !== Number.NEGATIVE_INFINITY\n            ? formatMillisecondsRange(p75Range.min, p75Range.max)\n            : \"N/A\",\n      };\n    }, [uptimeData, regionLatencyData, globalLatencyData]);\n\n  if (!isLoading && !monitor) {\n    return (\n      <StatusBlankMonitors\n        title=\"Monitor not found\"\n        description=\"The monitor you are looking for does not exist.\"\n      />\n    );\n  }\n\n  return (\n    <Status>\n      <StatusHeader>\n        <StatusTitle>{tempMonitor?.name}</StatusTitle>\n        <StatusDescription>{tempMonitor?.description}</StatusDescription>\n      </StatusHeader>\n      <StatusContent className=\"flex flex-col gap-6\">\n        <div className=\"flex w-full flex-row items-center justify-between gap-2 py-0.5\">\n          <ButtonBack href=\"./\" />\n          <ButtonCopyLink />\n        </div>\n        <StatusMonitorTabs\n          defaultValue={tab}\n          onValueChange={(value) =>\n            setSearchParams({ tab: value as \"global\" | \"region\" | \"uptime\" })\n          }\n        >\n          <StatusMonitorTabsList className=\"grid grid-cols-3\">\n            <StatusMonitorTabsTrigger value=\"global\">\n              <StatusMonitorTabsTriggerLabel>\n                Global Latency\n              </StatusMonitorTabsTriggerLabel>\n              {isLoading ? (\n                <StatusMonitorTabsTriggerValueSkeleton />\n              ) : (\n                <StatusMonitorTabsTriggerValue>\n                  {p75Range}{\" \"}\n                  <Badge variant=\"outline\" className=\"py-px text-[10px]\">\n                    p75\n                  </Badge>\n                </StatusMonitorTabsTriggerValue>\n              )}\n            </StatusMonitorTabsTrigger>\n            <StatusMonitorTabsTrigger value=\"region\">\n              <StatusMonitorTabsTriggerLabel>\n                Region Latency\n              </StatusMonitorTabsTriggerLabel>\n              {isLoading ? (\n                <StatusMonitorTabsTriggerValueSkeleton />\n              ) : (\n                <StatusMonitorTabsTriggerValue>\n                  {tempMonitor?.regions.length} regions{\" \"}\n                  <Badge\n                    variant=\"outline\"\n                    className=\"py-px font-mono text-[10px]\"\n                  >\n                    {slowestRegion} <TrendingUp className=\"size-3\" />\n                  </Badge>\n                </StatusMonitorTabsTriggerValue>\n              )}\n            </StatusMonitorTabsTrigger>\n            <StatusMonitorTabsTrigger value=\"uptime\">\n              <StatusMonitorTabsTriggerLabel>\n                Uptime\n              </StatusMonitorTabsTriggerLabel>\n              {isLoading ? (\n                <StatusMonitorTabsTriggerValueSkeleton />\n              ) : (\n                <StatusMonitorTabsTriggerValue>\n                  {uptimePercentage}{\" \"}\n                  <Badge variant=\"outline\" className=\"py-px text-[10px]\">\n                    {totalChecks} checks\n                  </Badge>\n                </StatusMonitorTabsTriggerValue>\n              )}\n            </StatusMonitorTabsTrigger>\n          </StatusMonitorTabsList>\n          <StatusMonitorTabsContent value=\"global\">\n            <StatusChartContent>\n              <StatusChartHeader>\n                <StatusChartTitle>Global Latency</StatusChartTitle>\n                <StatusChartDescription>\n                  The aggregated latency from all active regions based on\n                  different <PopoverQuantile>quantiles</PopoverQuantile>.\n                </StatusChartDescription>\n              </StatusChartHeader>\n              {isLoading ? (\n                <ChartAreaPercentilesSkeleton className=\"h-[250px]\" />\n              ) : (\n                <ChartAreaPercentiles\n                  className=\"h-[250px]\"\n                  legendClassName=\"justify-start pt-1 ps-1\"\n                  legendVerticalAlign=\"top\"\n                  xAxisHide={false}\n                  data={globalLatencyData}\n                  yAxisDomain={[0, \"dataMax\"]}\n                />\n              )}\n            </StatusChartContent>\n          </StatusMonitorTabsContent>\n          <StatusMonitorTabsContent value=\"region\">\n            <StatusChartContent>\n              <StatusChartHeader>\n                <StatusChartTitle>Latency by Region</StatusChartTitle>\n                <StatusChartDescription>\n                  {/* TODO: we could add an information to p95 that it takes the highest selected global latency percentile */}\n                  Region latency per{\" \"}\n                  <code className=\"font-medium text-foreground\">p75</code>{\" \"}\n                  <PopoverQuantile>quantile</PopoverQuantile>, sorted by slowest\n                  region. Compare up to{\" \"}\n                  <code className=\"font-medium text-foreground\">6</code>{\" \"}\n                  regions.\n                </StatusChartDescription>\n              </StatusChartHeader>\n              {isLoading ? (\n                <ChartLineRegionsSkeleton className=\"h-[250px]\" />\n              ) : (\n                <ChartLineRegions\n                  className=\"h-[250px]\"\n                  data={regionLatencyData}\n                  defaultRegions={tempMonitor?.regions}\n                />\n              )}\n            </StatusChartContent>\n          </StatusMonitorTabsContent>\n          <StatusMonitorTabsContent value=\"uptime\">\n            <StatusChartContent>\n              <StatusChartHeader>\n                <StatusChartTitle>Total Uptime</StatusChartTitle>\n                <StatusChartDescription>\n                  Main values of uptime and availability, transparent.\n                </StatusChartDescription>\n              </StatusChartHeader>\n              {isLoading ? (\n                <ChartBarUptimeSkeleton className=\"h-[250px]\" />\n              ) : (\n                <ChartBarUptime className=\"h-[250px]\" data={uptimeData} />\n              )}\n            </StatusChartContent>\n          </StatusMonitorTabsContent>\n        </StatusMonitorTabs>\n      </StatusContent>\n    </Status>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/monitors/[id]/search-params.ts",
    "content": "import { createSearchParamsCache, parseAsStringEnum } from \"nuqs/server\";\n\nexport const searchParamsParsers = {\n  tab: parseAsStringEnum([\"global\", \"region\", \"uptime\"]).withDefault(\"global\"),\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/monitors/page.tsx",
    "content": "\"use client\";\n\nimport {\n  ChartAreaPercentiles,\n  ChartAreaPercentilesSkeleton,\n} from \"@/components/chart/chart-area-percentiles\";\nimport {\n  Status,\n  StatusContent,\n  StatusDescription,\n  StatusHeader,\n  StatusTitle,\n} from \"@/components/status-page/status\";\nimport { StatusBlankMonitors } from \"@/components/status-page/status-blank\";\nimport { StatusMonitorTitle } from \"@/components/status-page/status-monitor\";\nimport { StatusMonitorDescription } from \"@/components/status-page/status-monitor\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Link from \"next/link\";\nimport { useParams } from \"next/navigation\";\n\nexport default function Page() {\n  const { domain } = useParams<{ domain: string }>();\n  const trpc = useTRPC();\n  const { data: page } = useQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n  const { data: monitors, isLoading } = useQuery(\n    trpc.statusPage.getMonitors.queryOptions({ slug: domain }),\n  );\n\n  if (!page) return null;\n\n  const publicMonitors = page.monitors.filter((monitor) => monitor.public);\n\n  return (\n    <Status>\n      <StatusHeader>\n        <StatusTitle>{page.title}</StatusTitle>\n        <StatusDescription>{page.description}</StatusDescription>\n      </StatusHeader>\n      <StatusContent className=\"flex flex-col gap-6\">\n        {publicMonitors.length > 0 ? (\n          publicMonitors.map((monitor) => {\n            const data =\n              monitors\n                ?.find((item) => item.id === monitor.id)\n                ?.data?.map((item) => ({\n                  ...item,\n                  // TODO: create formatter\n                  timestamp: new Date(item.timestamp).toLocaleString(\n                    \"default\",\n                    {\n                      day: \"numeric\",\n                      month: \"short\",\n                      hour: \"numeric\",\n                      minute: \"numeric\",\n                      timeZoneName: \"short\",\n                    },\n                  ),\n                })) ?? [];\n\n            return (\n              <Link\n                key={monitor.id}\n                href={`./monitors/${monitor.id}`}\n                className=\"rounded-lg\"\n              >\n                <div className=\"group -mx-3 -my-2 flex flex-col gap-2 rounded-lg border border-transparent px-3 py-2 hover:border-border/50 hover:bg-muted/50\">\n                  <div className=\"flex flex-row items-center justify-start gap-2\">\n                    <StatusMonitorTitle>{monitor.name}</StatusMonitorTitle>\n                    <StatusMonitorDescription>\n                      {monitor.description}\n                    </StatusMonitorDescription>\n                  </div>\n                  {isLoading ? (\n                    <ChartAreaPercentilesSkeleton className=\"h-[80px]\" />\n                  ) : (\n                    <ChartAreaPercentiles\n                      className=\"h-[80px]\"\n                      legendClassName=\"pb-1 justify-start\"\n                      data={data}\n                      singleSeries\n                    />\n                  )}\n                </div>\n              </Link>\n            );\n          })\n        ) : (\n          <StatusBlankMonitors />\n        )}\n      </StatusContent>\n    </Status>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/page.tsx",
    "content": "\"use client\";\n\nimport { useStatusPage } from \"@/components/status-page/floating-button\";\nimport {\n  Status,\n  StatusContent,\n  StatusDescription,\n  StatusHeader,\n  StatusTitle,\n} from \"@/components/status-page/status\";\nimport {\n  StatusBanner,\n  StatusBannerContainer,\n  StatusBannerContent,\n  StatusBannerTabs,\n  StatusBannerTabsContent,\n  StatusBannerTabsList,\n  StatusBannerTabsTrigger,\n} from \"@/components/status-page/status-banner\";\nimport {\n  StatusEventAffected,\n  StatusEventAffectedBadge,\n  StatusEventTimelineMaintenance,\n  StatusEventTimelineReportUpdate,\n} from \"@/components/status-page/status-events\";\nimport { StatusFeed } from \"@/components/status-page/status-feed\";\nimport { StatusMonitor } from \"@/components/status-page/status-monitor\";\nimport { StatusTrackerGroup } from \"@/components/status-page/status-tracker-group\";\nimport { usePathnamePrefix } from \"@/hooks/use-pathname-prefix\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Link from \"next/link\";\nimport { notFound, useParams } from \"next/navigation\";\nimport { useMemo } from \"react\";\n\nexport default function Page() {\n  const prefix = usePathnamePrefix();\n  const { domain } = useParams<{ domain: string }>();\n  const { cardType, barType, showUptime } = useStatusPage();\n  const trpc = useTRPC();\n\n  // NOTE: we cannot use `cardType` and `barType` here because of queryKey changes\n  // It wouldn't match the server prefetch keys and we would have to refetch the page here\n  const { data: pageInitial, error } = useQuery({\n    ...trpc.statusPage.get.queryOptions({\n      slug: domain,\n    }),\n    enabled: !!domain,\n  });\n\n  // Handle case where page doesn't exist or query fails\n  if (error || (!pageInitial && domain)) {\n    notFound();\n  }\n\n  const hasCustomConfig = pageInitial?.configuration\n    ? pageInitial.configuration.type !== barType ||\n      pageInitial.configuration.value !== cardType\n    : false;\n\n  // NOTE: instead, we use the `enabled` flag to only fetch the page if the configuration differs\n  const { data: pageWithCustomConfiguration } = useQuery({\n    ...trpc.statusPage.get.queryOptions({\n      slug: domain,\n      cardType,\n      barType,\n    }),\n    enabled: !!domain && hasCustomConfig,\n  });\n\n  // NOTE: we can prefetch that to avoid loading state\n  const { data: uptimeData, isLoading } = useQuery({\n    ...trpc.statusPage.getUptime.queryOptions({\n      slug: domain,\n      pageComponentIds:\n        pageInitial?.pageComponents?.map((c) => c.id.toString()) || [],\n      cardType,\n      barType,\n    }),\n    enabled: !!pageInitial && pageInitial.pageComponents.length > 0,\n  });\n\n  // NOTE: we need to filter out the incidents as we don't want to show all of them in the banner - a single one is enough\n  // REMINDER: we could move that to the server - but we might wanna have the info of all openEvents actually\n  const events = useMemo(() => {\n    let hasIncident = false;\n    return (\n      pageInitial?.openEvents.filter((e) => {\n        if (e.type !== \"incident\") return true;\n        if (hasIncident) return false;\n        hasIncident = true;\n        return true;\n      }) ?? []\n    );\n  }, [pageInitial]);\n\n  if (!pageInitial) return null;\n\n  // REMINDER: if we are using the custom configuration, we need to use the pageWithCustomConfiguration\n  const page = pageWithCustomConfiguration ?? pageInitial;\n\n  const firstGroupIndex = useMemo(\n    () => page.trackers.findIndex((tracker) => tracker.type === \"group\"),\n    [page.trackers],\n  );\n\n  return (\n    <div className=\"flex flex-col gap-6\">\n      <Status variant={page.status}>\n        <StatusHeader>\n          <StatusTitle>{page.title}</StatusTitle>\n          <StatusDescription>{page.description}</StatusDescription>\n        </StatusHeader>\n        {events.length > 0 ? (\n          <StatusContent>\n            <StatusBannerTabs\n              defaultValue={`${events[0].type}-${events[0].id}`}\n            >\n              <StatusBannerTabsList>\n                {events.map((e, i) => {\n                  return (\n                    <StatusBannerTabsTrigger\n                      value={`${e.type}-${e.id}`}\n                      status={e.status}\n                      key={`${e.type}-${e.id}`}\n                      className={cn(\n                        i === 0 && \"rounded-tl-lg\",\n                        i === events.length - 1 && \"rounded-tr-lg\",\n                      )}\n                    >\n                      {e.name}\n                    </StatusBannerTabsTrigger>\n                  );\n                })}\n              </StatusBannerTabsList>\n              {events.map((e) => {\n                if (e.type === \"report\") {\n                  const report = page.statusReports.find(\n                    (report) => report.id === e.id,\n                  );\n                  if (!report) return null;\n                  const lastUpdate = report.statusReportUpdates.sort(\n                    (a, b) => b.date.getTime() - a.date.getTime(),\n                  )[0];\n                  if (!lastUpdate) return null;\n                  return (\n                    <StatusBannerTabsContent\n                      value={`${e.type}-${e.id}`}\n                      key={`${e.type}-${e.id}`}\n                    >\n                      <Link\n                        href={`${prefix ? `/${prefix}` : \"\"}/events/report/${report.id}`}\n                        className=\"rounded-lg\"\n                      >\n                        <StatusBannerContainer status={e.status}>\n                          <StatusBannerContent>\n                            <StatusEventTimelineReportUpdate\n                              report={lastUpdate}\n                              withDot={false}\n                              isLast={true}\n                              withSeparator={false}\n                            />\n                            {report.statusReportsToPageComponents.length > 0 ? (\n                              <StatusEventAffected>\n                                {report.statusReportsToPageComponents.map(\n                                  (affected) => (\n                                    <StatusEventAffectedBadge\n                                      key={affected.pageComponent.id}\n                                    >\n                                      {affected.pageComponent.name}\n                                    </StatusEventAffectedBadge>\n                                  ),\n                                )}\n                              </StatusEventAffected>\n                            ) : null}\n                          </StatusBannerContent>\n                        </StatusBannerContainer>\n                      </Link>\n                    </StatusBannerTabsContent>\n                  );\n                }\n                if (e.type === \"maintenance\") {\n                  const maintenance = page.maintenances.find(\n                    (maintenance) => maintenance.id === e.id,\n                  );\n                  if (!maintenance) return null;\n                  return (\n                    <StatusBannerTabsContent\n                      value={`${e.type}-${e.id}`}\n                      key={e.id}\n                    >\n                      <Link\n                        href={`${prefix ? `/${prefix}` : \"\"}/events/maintenance/${maintenance.id}`}\n                        className=\"rounded-lg\"\n                      >\n                        <StatusBannerContainer status={e.status}>\n                          <StatusBannerContent>\n                            <StatusEventTimelineMaintenance\n                              maintenance={maintenance}\n                              withDot={false}\n                            />\n                            {maintenance.maintenancesToPageComponents.length >\n                            0 ? (\n                              <StatusEventAffected>\n                                {maintenance.maintenancesToPageComponents.map(\n                                  (affected) => (\n                                    <StatusEventAffectedBadge\n                                      key={affected.pageComponent.id}\n                                    >\n                                      {affected.pageComponent.name}\n                                    </StatusEventAffectedBadge>\n                                  ),\n                                )}\n                              </StatusEventAffected>\n                            ) : null}\n                          </StatusBannerContent>\n                        </StatusBannerContainer>\n                      </Link>\n                    </StatusBannerTabsContent>\n                  );\n                }\n                if (e.type === \"incident\") {\n                  return (\n                    <StatusBannerTabsContent\n                      value={`${e.type}-${e.id}`}\n                      key={e.id}\n                    >\n                      <StatusBanner status={e.status} />\n                    </StatusBannerTabsContent>\n                  );\n                }\n                return null;\n              })}\n            </StatusBannerTabs>\n          </StatusContent>\n        ) : (\n          <StatusBanner status={page.status} />\n        )}\n        {/* NOTE: check what gap feels right */}\n        {page.trackers.length > 0 ? (\n          <StatusContent className=\"gap-5\">\n            {page.trackers.map((tracker, index) => {\n              if (tracker.type === \"component\") {\n                const component = tracker.component;\n\n                // Fetch uptime data by component ID\n                const { data, uptime } =\n                  uptimeData?.find((u) => u.pageComponentId === component.id) ??\n                  {};\n\n                return (\n                  <StatusMonitor\n                    key={`component-${component.id}`}\n                    status={component.status}\n                    data={data}\n                    monitor={{\n                      name: component.name,\n                      description: component.description,\n                    }}\n                    uptime={uptime}\n                    showUptime={showUptime}\n                    isLoading={isLoading}\n                  />\n                );\n              }\n\n              return (\n                <StatusTrackerGroup\n                  key={`group-${tracker.groupId}`}\n                  title={tracker.groupName}\n                  status={tracker.status}\n                  // NOTE: we only want to open the first group if it is the first one\n                  defaultOpen={firstGroupIndex === index && index === 0}\n                >\n                  {tracker.components.map((component) => {\n                    const { data, uptime } =\n                      uptimeData?.find(\n                        (u) => u.pageComponentId === component.id,\n                      ) ?? {};\n\n                    return (\n                      <StatusMonitor\n                        key={`component-${component.id}`}\n                        status={component.status}\n                        data={data}\n                        monitor={{\n                          name: component.name,\n                          description: component.description,\n                        }}\n                        uptime={uptime}\n                        showUptime={showUptime}\n                        isLoading={isLoading}\n                      />\n                    );\n                  })}\n                </StatusTrackerGroup>\n              );\n            })}\n          </StatusContent>\n        ) : null}\n        <Separator />\n        <StatusContent>\n          <StatusFeed\n            statusReports={page.statusReports\n              .filter((report) =>\n                page.lastEvents.some(\n                  (event) => event.id === report.id && event.type === \"report\",\n                ),\n              )\n              .map((report) => ({\n                ...report,\n                affected: report.statusReportsToPageComponents.map(\n                  (component) => component.pageComponent.name,\n                ),\n                updates: report.statusReportUpdates,\n              }))}\n            maintenances={page.maintenances\n              .filter((maintenance) =>\n                page.lastEvents.some(\n                  (event) =>\n                    event.id === maintenance.id && event.type === \"maintenance\",\n                ),\n              )\n              .map((maintenance) => ({\n                ...maintenance,\n                affected: maintenance.maintenancesToPageComponents.map(\n                  (component) => component.pageComponent.name,\n                ),\n              }))}\n          />\n        </StatusContent>\n      </Status>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/unsubscribe/[token]/layout.tsx",
    "content": "\"use client\";\n\nimport {\n  Status,\n  StatusContent,\n  StatusDescription,\n  StatusHeader,\n  StatusTitle,\n} from \"@/components/status-page/status\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function UnsubscribeLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const { domain } = useParams<{ domain: string }>();\n  const trpc = useTRPC();\n  const { data: page } = useQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  if (!page) return null;\n\n  return (\n    <Status>\n      <StatusHeader>\n        <StatusTitle>{page.title}</StatusTitle>\n        <StatusDescription>{page.description}</StatusDescription>\n      </StatusHeader>\n      <StatusContent>{children}</StatusContent>\n    </Status>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/unsubscribe/[token]/page.tsx",
    "content": "\"use client\";\n\nimport {\n  StatusBlankContainer,\n  StatusBlankContent,\n  StatusBlankDescription,\n  StatusBlankLink,\n  StatusBlankTitle,\n} from \"@/components/status-page/status-blank\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function UnsubscribePage() {\n  const trpc = useTRPC();\n  const { token, domain } = useParams<{ token: string; domain: string }>();\n\n  const subscriberQuery = useQuery(\n    trpc.statusPage.getSubscriberByToken.queryOptions({ token, domain }),\n  );\n\n  const unsubscribeMutation = useMutation(\n    trpc.statusPage.unsubscribe.mutationOptions({}),\n  );\n\n  const handleUnsubscribe = () => {\n    unsubscribeMutation.mutate({ token, domain });\n  };\n\n  // Loading state\n  if (subscriberQuery.isLoading) {\n    return (\n      <StatusBlankContainer>\n        <StatusBlankContent>\n          <StatusBlankTitle>Loading...</StatusBlankTitle>\n        </StatusBlankContent>\n      </StatusBlankContainer>\n    );\n  }\n\n  // Invalid/expired token or already unsubscribed\n  if (!subscriberQuery.data) {\n    return (\n      <StatusBlankContainer>\n        <StatusBlankContent>\n          <StatusBlankTitle className=\"text-destructive\">\n            Invalid or expired link\n          </StatusBlankTitle>\n          <StatusBlankDescription>\n            This unsubscribe link is no longer valid. You may have already\n            unsubscribed.\n          </StatusBlankDescription>\n          <StatusBlankLink href=\"../\">Go back</StatusBlankLink>\n        </StatusBlankContent>\n      </StatusBlankContainer>\n    );\n  }\n\n  // Success state after unsubscribing\n  if (unsubscribeMutation.isSuccess) {\n    return (\n      <StatusBlankContainer>\n        <StatusBlankContent>\n          <StatusBlankTitle className=\"text-success\">\n            Successfully unsubscribed\n          </StatusBlankTitle>\n          <StatusBlankDescription>\n            You will no longer receive email notifications from{\" \"}\n            {subscriberQuery.data.pageName}.\n          </StatusBlankDescription>\n          <StatusBlankLink href=\"../\">Go back</StatusBlankLink>\n        </StatusBlankContent>\n      </StatusBlankContainer>\n    );\n  }\n\n  // Error state\n  if (unsubscribeMutation.isError) {\n    return (\n      <StatusBlankContainer>\n        <StatusBlankContent>\n          <StatusBlankTitle className=\"text-destructive\">\n            {unsubscribeMutation.error?.message || \"Something went wrong\"}\n          </StatusBlankTitle>\n          <StatusBlankDescription>\n            Please try again or contact support if the issue persists.\n          </StatusBlankDescription>\n          <StatusBlankLink href=\"../\">Go back</StatusBlankLink>\n        </StatusBlankContent>\n      </StatusBlankContainer>\n    );\n  }\n\n  // Confirmation state (initial view)\n  return (\n    <StatusBlankContainer>\n      <StatusBlankContent>\n        <StatusBlankTitle>Unsubscribe from notifications</StatusBlankTitle>\n        <StatusBlankDescription>\n          You are about to unsubscribe{\" \"}\n          <span className=\"font-semibold\">\n            {subscriberQuery.data.maskedEmail}\n          </span>{\" \"}\n          from{\" \"}\n          <span className=\"font-semibold\">{subscriberQuery.data.pageName}</span>{\" \"}\n          status updates.\n        </StatusBlankDescription>\n        <div className=\"flex justify-center gap-2\">\n          <StatusBlankLink href=\"../\">Cancel</StatusBlankLink>\n          <Button\n            variant=\"destructive\"\n            size=\"sm\"\n            onClick={handleUnsubscribe}\n            disabled={unsubscribeMutation.isPending}\n          >\n            {unsubscribeMutation.isPending ? \"Unsubscribing...\" : \"Unsubscribe\"}\n          </Button>\n        </div>\n      </StatusBlankContent>\n    </StatusBlankContainer>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/verify/[token]/layout.tsx",
    "content": "\"use client\";\n\nimport {\n  Status,\n  StatusContent,\n  StatusDescription,\n  StatusHeader,\n  StatusTitle,\n} from \"@/components/status-page/status\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\n\nexport default function EventLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const { domain } = useParams<{ domain: string }>();\n  const trpc = useTRPC();\n  const { data: page } = useQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  if (!page) return null;\n\n  return (\n    <Status>\n      <StatusHeader>\n        <StatusTitle>{page.title}</StatusTitle>\n        <StatusDescription>{page.description}</StatusDescription>\n      </StatusHeader>\n      <StatusContent>{children}</StatusContent>\n    </Status>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/(public)/verify/[token]/page.tsx",
    "content": "\"use client\";\n\nimport {\n  StatusBlankContainer,\n  StatusBlankContent,\n  StatusBlankLink,\n  StatusBlankTitle,\n} from \"@/components/status-page/status-blank\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\nimport { useEffect } from \"react\";\n\nexport default function VerifyPage() {\n  const trpc = useTRPC();\n  const { token, domain } = useParams<{ token: string; domain: string }>();\n  const verifyEmailMutation = useMutation(\n    trpc.statusPage.verifyEmail.mutationOptions({}),\n  );\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  useEffect(() => {\n    verifyEmailMutation.mutate({ slug: domain, token });\n  }, [domain, token]);\n\n  const title = verifyEmailMutation.isSuccess\n    ? `All set to receive updates to ${verifyEmailMutation.data?.email}!`\n    : verifyEmailMutation.isError\n      ? verifyEmailMutation.error?.message || \"Something went wrong\"\n      : \"Hang tight - we're confirming your subscription\";\n\n  return (\n    <StatusBlankContainer>\n      <StatusBlankContent>\n        <StatusBlankTitle\n          className={cn({\n            \"text-destructive\": verifyEmailMutation.isError,\n            \"text-success\": verifyEmailMutation.isSuccess,\n          })}\n        >\n          {title}\n        </StatusBlankTitle>\n        <div className=\"flex justify-center gap-2\">\n          <StatusBlankLink\n            href=\"/\"\n            disabled={\n              verifyEmailMutation.isPending || !verifyEmailMutation.data\n            }\n          >\n            Go back\n          </StatusBlankLink>\n          {verifyEmailMutation.isSuccess && (\n            <StatusBlankLink\n              href={`/manage/${token}`}\n              disabled={\n                verifyEmailMutation.isPending || !verifyEmailMutation.data\n              }\n            >\n              Manage\n            </StatusBlankLink>\n          )}\n        </div>\n      </StatusBlankContent>\n    </StatusBlankContainer>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/(status-page)/[domain]/layout.tsx",
    "content": "import { defaultMetadata, ogMetadata, twitterMetadata } from \"@/app/metadata\";\nimport { PasswordWrapper } from \"@/components/password-wrapper\";\nimport {\n  FloatingButton,\n  StatusPageProvider,\n} from \"@/components/status-page/floating-button\";\nimport { FloatingTheme } from \"@/components/status-page/floating-theme\";\nimport { ThemeProvider } from \"@/components/themes/theme-provider\";\nimport { HydrateClient, getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport {\n  THEME_KEYS,\n  type ThemeKey,\n  generateThemeStyles,\n} from \"@openstatus/theme-store\";\nimport { Toaster } from \"@openstatus/ui/components/ui/sonner\";\nimport type { Metadata } from \"next\";\nimport { notFound } from \"next/navigation\";\nimport { z } from \"zod\";\n\nexport const schema = z.object({\n  value: z.enum([\"duration\", \"requests\", \"manual\"]).prefault(\"duration\"),\n  type: z.enum([\"absolute\", \"manual\"]).prefault(\"absolute\"),\n  uptime: z.coerce.boolean().prefault(true),\n  theme: z.enum(THEME_KEYS as [ThemeKey, ...ThemeKey[]]).prefault(\"default\"),\n});\n\nexport default async function Layout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ domain: string }>;\n}) {\n  const queryClient = getQueryClient();\n  const { domain } = await params;\n  const page = await queryClient.fetchQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  if (!page) return notFound();\n\n  const validation = schema.safeParse(page?.configuration);\n  const communityTheme = validation.data?.theme;\n\n  return (\n    <HydrateClient>\n      <style\n        id=\"theme-styles\"\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n        dangerouslySetInnerHTML={{\n          __html: generateThemeStyles(communityTheme),\n        }}\n      />\n      <ThemeProvider\n        attribute=\"class\"\n        defaultTheme={page?.forceTheme ?? \"system\"}\n        enableSystem\n        disableTransitionOnChange\n      >\n        <StatusPageProvider\n          defaultBarType={validation.data?.type}\n          defaultCardType={validation.data?.value}\n          defaultShowUptime={validation.data?.uptime}\n          defaultCommunityTheme={validation.data?.theme}\n        >\n          {children}\n          <FloatingButton\n            pageId={page?.id}\n            // NOTE: token to avoid showing the floating button to random users\n            // timestamp is our token - it is hard to guess\n            token={page?.createdAt?.getTime().toString()}\n          />\n          <FloatingTheme />\n          <Toaster\n            toastOptions={{\n              classNames: {},\n              style: { borderRadius: \"var(--radius-lg)\" },\n            }}\n            richColors\n            expand\n          />\n          <PasswordWrapper />\n        </StatusPageProvider>\n      </ThemeProvider>\n    </HydrateClient>\n  );\n}\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ domain: string }>;\n}): Promise<Metadata> {\n  const queryClient = getQueryClient();\n  const { domain } = await params;\n  const page = await queryClient.fetchQuery(\n    trpc.statusPage.get.queryOptions({ slug: domain }),\n  );\n\n  if (!page) return notFound();\n\n  return {\n    ...defaultMetadata,\n    title: {\n      template: `%s | ${page.title}`,\n      default: page?.title,\n    },\n    description: page?.description,\n    icons: page?.icon,\n    alternates: {\n      canonical: page?.customDomain\n        ? `https://${page.customDomain}`\n        : `https://${page.slug}.openstatus.dev`,\n    },\n    twitter: {\n      ...twitterMetadata,\n      images: [`/api/og/page?slug=${page?.slug}`],\n      title: page?.title,\n      description: page?.description,\n    },\n    openGraph: {\n      ...ogMetadata,\n      images: [`/api/og/page?slug=${page?.slug}`],\n      title: page?.title,\n      description: page?.description,\n    },\n  };\n}\n"
  },
  {
    "path": "apps/status-page/src/app/api/auth/[...nextauth]/route.ts",
    "content": "import { handlers } from \"@/lib/auth\";\n\nexport const { GET, POST } = handlers;\n"
  },
  {
    "path": "apps/status-page/src/app/api/trpc/edge/[trpc]/route.ts",
    "content": "import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport type { NextRequest } from \"next/server\";\n\nimport { auth } from \"@/lib/auth\";\nimport { createTRPCContext } from \"@openstatus/api\";\nimport { edgeRouter } from \"@openstatus/api/src/edge\";\n\nexport const runtime = \"edge\";\n\nconst handler = (req: NextRequest) =>\n  fetchRequestHandler({\n    endpoint: \"/api/trpc/edge\",\n    router: edgeRouter,\n    req: req,\n    createContext: () => createTRPCContext({ req, auth }),\n    onError: ({ error }) => {\n      console.log(\"Error in tRPC handler (edge)\");\n      console.error(error);\n    },\n  });\n\nexport { handler as GET, handler as POST };\n"
  },
  {
    "path": "apps/status-page/src/app/api/trpc/lambda/[trpc]/route.ts",
    "content": "import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport type { NextRequest } from \"next/server\";\n\nimport { auth } from \"@/lib/auth\";\nimport { createTRPCContext } from \"@openstatus/api\";\nimport { lambdaRouter } from \"@openstatus/api/src/lambda\";\n\n// Stripe is incompatible with Edge runtimes due to using Node.js events\n// export const runtime = \"edge\";\n\nconst handler = (req: NextRequest) =>\n  fetchRequestHandler({\n    endpoint: \"/api/trpc/lambda\",\n    router: lambdaRouter,\n    req: req,\n    createContext: () => createTRPCContext({ req, auth }),\n    onError: ({ error }) => {\n      console.log(\"Error in tRPC handler (lambda)\");\n      console.error(error);\n    },\n  });\n\nexport { handler as GET, handler as POST };\n"
  },
  {
    "path": "apps/status-page/src/app/global-error.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport * as Sentry from \"@sentry/nextjs\";\nimport { useEffect } from \"react\";\n\nexport default function GlobalError({\n  error,\n  reset,\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  useEffect(() => {\n    Sentry.captureException(error);\n  }, [error]);\n\n  return (\n    <html lang=\"en\">\n      <body>\n        <main className=\"flex min-h-screen w-full flex-col space-y-6 bg-background p-4 md:p-8\">\n          <div className=\"flex flex-1 flex-col items-center justify-center gap-8\">\n            <div className=\"mx-auto max-w-xl border bg-card text-center\">\n              <div className=\"flex flex-col gap-4 p-6 sm:p-12\">\n                <div className=\"flex flex-col gap-1\">\n                  <h2 className=\"font-cal text-2xl text-foreground\">\n                    Application Error\n                  </h2>\n                  <p className=\"text-muted-foreground text-sm sm:text-base\">\n                    An unexpected error occurred. This has been reported and\n                    we&apos;re working on it.{\" \"}\n                    <Link href=\"mailto:ping@openstatus.dev\">Contact us</Link> if\n                    it persists.\n                  </p>\n                </div>\n                <div className=\"flex flex-col items-center justify-center gap-4 sm:flex-row\">\n                  <Button\n                    variant=\"outline\"\n                    size=\"lg\"\n                    onClick={reset}\n                    className=\"cursor-pointer\"\n                  >\n                    Try Again\n                  </Button>\n                  <Button size=\"lg\" asChild>\n                    <Link href=\"/\">Go Home</Link>\n                  </Button>\n                </div>\n              </div>\n            </div>\n          </div>\n        </main>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"@openstatus/ui/globals\";\n@import \"tw-animate-css\";\n@plugin \"@tailwindcss/typography\";\n\n/* safelist */\n@source inline(\"has-data-[slot=slider-range]:bg-red-500\");\n\n@theme {\n  --breakpoint-xs: 30rem;\n}\n\n@theme inline {\n  --font-sans: var(--font-geist-sans);\n  --font-mono: var(--font-commit-mono, var(--font-geist-mono));\n  --radius-xs: calc(var(--radius) - 8px);\n}\n\n:root {\n  /* Override the base radius for status-page */\n  --radius: 0rem;\n}\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n@layer utilities {\n  /* NOTE: allows us to --radius: 0px and avoid rounding issues - otherwise it is 'infinite * 1px' */\n  .rounded-full {\n    border-radius: calc(var(--radius) * 99999999);\n  }\n  .rounded-b-full {\n    border-bottom-left-radius: calc(var(--radius) * 99999999);\n    border-bottom-right-radius: calc(var(--radius) * 99999999);\n  }\n  .rounded-t-full {\n    border-top-left-radius: calc(var(--radius) * 99999999);\n    border-top-right-radius: calc(var(--radius) * 99999999);\n  }\n}\n"
  },
  {
    "path": "apps/status-page/src/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Geist, Geist_Mono } from \"next/font/google\";\nimport \"./globals.css\";\nimport { TailwindIndicator } from \"@/components/tailwind-indicator\";\nimport { TRPCReactProvider } from \"@/lib/trpc/client\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport LocalFont from \"next/font/local\";\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\nimport { ogMetadata, twitterMetadata } from \"./metadata\";\nimport { defaultMetadata } from \"./metadata\";\n\nconst cal = LocalFont({\n  src: \"../../public/fonts/CalSans-SemiBold.ttf\",\n  variable: \"--font-cal-sans\",\n});\n\nconst geistSans = Geist({\n  variable: \"--font-geist-sans\",\n  subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n});\n\nconst commitMono = LocalFont({\n  src: [\n    {\n      path: \"../../public/fonts/CommitMono-400-Regular.otf\",\n      weight: \"400\",\n      style: \"normal\",\n    },\n    {\n      path: \"../../public/fonts/CommitMono-400-Italic.otf\",\n      weight: \"400\",\n      style: \"italic\",\n    },\n    {\n      path: \"../../public/fonts/CommitMono-700-Regular.otf\",\n      weight: \"700\",\n      style: \"normal\",\n    },\n    {\n      path: \"../../public/fonts/CommitMono-700-Italic.otf\",\n      weight: \"700\",\n      style: \"italic\",\n    },\n  ],\n  variable: \"--font-commit-mono\",\n});\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  twitter: {\n    ...twitterMetadata,\n  },\n  openGraph: {\n    ...ogMetadata,\n  },\n};\n\n// export const dynamic = \"error\";\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body\n        className={cn(\n          geistSans.variable,\n          geistMono.variable,\n          cal.variable,\n          commitMono.variable,\n          \"antialiased\",\n        )}\n      >\n        <NuqsAdapter>\n          <TRPCReactProvider>\n            {children}\n            <TailwindIndicator />\n          </TRPCReactProvider>\n        </NuqsAdapter>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/metadata.ts",
    "content": "import type { Metadata } from \"next\";\n\nexport const TITLE = \"Status Page\";\nexport const DESCRIPTION =\n  \"Status page customization with built-in themes. Explore all themes and contribute your own theme.\";\n\nconst OG_TITLE = \"Theme Explorer\";\nconst OG_DESCRIPTION =\n  \"Explore all themes for your status page and contribute new ones to the community.\";\nconst FOOTER = \"themes.openstatus.dev\";\nconst IMAGE = \"assets/og/theme-explorer.png\";\n\nexport const defaultMetadata: Metadata = {\n  title: {\n    template: `%s | ${TITLE}`,\n    default: TITLE,\n  },\n  icons: \"https://www.openstatus.dev/favicon.ico\",\n  description: DESCRIPTION,\n  metadataBase: new URL(\"https://www.openstatus.dev\"),\n};\n\nexport const twitterMetadata: Metadata[\"twitter\"] = {\n  title: TITLE,\n  description: DESCRIPTION,\n  card: \"summary_large_image\",\n  images: [\n    `/api/og?title=${OG_TITLE}&description=${OG_DESCRIPTION}&footer=${FOOTER}&image=${IMAGE}`,\n  ],\n};\n\nexport const ogMetadata: Metadata[\"openGraph\"] = {\n  title: TITLE,\n  description: DESCRIPTION,\n  type: \"website\",\n  images: [\n    `/api/og?title=${OG_TITLE}&description=${OG_DESCRIPTION}&footer=${FOOTER}&image=${IMAGE}`,\n  ],\n};\n"
  },
  {
    "path": "apps/status-page/src/app/not-found.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\n\nimport { ThemeProvider } from \"@/components/themes/theme-provider\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\n\nexport default function NotFound() {\n  const router = useRouter();\n\n  return (\n    <ThemeProvider attribute=\"class\" enableSystem disableTransitionOnChange>\n      <main className=\"flex min-h-screen w-full flex-col space-y-6 bg-background p-4 md:p-8\">\n        <div className=\"flex flex-1 flex-col items-center justify-center gap-8\">\n          <div className=\"mx-auto max-w-xl border bg-card text-center\">\n            <div className=\"flex flex-col gap-4 p-6 sm:p-12\">\n              <div className=\"flex flex-col gap-1\">\n                <p className=\"font-mono text-foreground\">404 Page not found</p>\n                <h2 className=\"font-cal text-2xl text-foreground\">\n                  Oops, something went wrong.\n                </h2>\n                <p className=\"text-muted-foreground text-sm sm:text-base\">\n                  The page you are looking for doesn&apos;t exist.\n                </p>\n              </div>\n              <div className=\"flex flex-col items-center justify-center gap-4 sm:flex-row\">\n                <Button\n                  variant=\"outline\"\n                  size=\"lg\"\n                  onClick={router.back}\n                  className=\"cursor-pointer\"\n                >\n                  Go Back\n                </Button>\n                <Button size=\"lg\" asChild>\n                  <Link href=\"/\">Home</Link>\n                </Button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </main>\n    </ThemeProvider>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/app/react-table.d.ts",
    "content": "import \"@tanstack/react-table\";\n\ndeclare module \"@tanstack/react-table\" {\n  interface ColumnMeta {\n    headerClassName?: string;\n    cellClassName?: string;\n  }\n}\n"
  },
  {
    "path": "apps/status-page/src/components/button/button-back.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { ArrowLeft } from \"lucide-react\";\nimport Link from \"next/link\";\n\nexport function ButtonBack({\n  className,\n  href = \"/\",\n  ...props\n}: React.ComponentProps<typeof Button> & { href?: string }) {\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"sm\"\n      className={cn(\"text-muted-foreground\", className)}\n      asChild\n      {...props}\n    >\n      <Link href={href}>\n        <ArrowLeft />\n        Back\n      </Link>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/button/button-copy-link.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Check, Copy } from \"lucide-react\";\n\nexport function ButtonCopyLink({\n  className,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { copy, isCopied } = useCopyToClipboard();\n\n  return (\n    <Button\n      variant=\"outline\"\n      size=\"icon\"\n      onClick={() =>\n        copy(window.location.href, {\n          successMessage: \"Link copied to clipboard\",\n          withToast: true,\n        })\n      }\n      className={cn(\"size-8\", className)}\n      {...props}\n    >\n      {isCopied ? <Check /> : <Copy />}\n      <span className=\"sr-only\">Copy Link</span>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/chart/chart-area-percentiles.tsx",
    "content": "\"use client\";\n\nimport { formatMilliseconds } from \"@/lib/formatter\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartLegend,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useState } from \"react\";\nimport { Area, AreaChart, CartesianGrid, XAxis, YAxis } from \"recharts\";\nimport type { AxisDomain } from \"recharts/types/util/types\";\nimport { ChartLegendBadge } from \"./chart-legend-badge\";\nimport { ChartTooltipNumber } from \"./chart-tooltip-number\";\n\nconst chartConfig = {\n  p50Latency: {\n    label: \"p50\",\n    color: \"var(--chart-1)\",\n  },\n  p75Latency: {\n    label: \"p75\",\n    color: \"var(--chart-2)\",\n  },\n  p90Latency: {\n    label: \"p90\",\n    color: \"var(--chart-4)\",\n  },\n  p95Latency: {\n    label: \"p95\",\n    color: \"var(--chart-3)\",\n  },\n  p99Latency: {\n    label: \"p99\",\n    color: \"var(--chart-5)\",\n  },\n} satisfies ChartConfig;\n\nfunction avg(values: number[]) {\n  return Math.round(\n    values.reduce((acc, curr) => acc + curr, 0) / values.length,\n  );\n}\n\nfunction formatAnnotation(values: number[]) {\n  if (values.length === 0) return \"N/A\";\n  return formatMilliseconds(avg(values));\n}\n\nexport function ChartAreaPercentiles({\n  className,\n  singleSeries,\n  xAxisHide = true,\n  legendVerticalAlign = \"bottom\",\n  legendClassName,\n  yAxisDomain = [\"dataMin\", \"dataMax\"],\n  data,\n}: {\n  className?: string;\n  singleSeries?: boolean;\n  xAxisHide?: boolean;\n  legendVerticalAlign?: \"top\" | \"bottom\";\n  legendClassName?: string;\n  yAxisDomain?: AxisDomain;\n  data: {\n    timestamp: string;\n    p50Latency: number;\n    p75Latency: number;\n    p90Latency: number;\n    p95Latency: number;\n    p99Latency: number;\n  }[];\n}) {\n  const [activeSeries, setActiveSeries] = useState<\n    Array<keyof typeof chartConfig>\n  >([\"p75Latency\"]);\n\n  const annotation = {\n    p50Latency: formatAnnotation(data.map((item) => item.p50Latency)),\n    p75Latency: formatAnnotation(data.map((item) => item.p75Latency)),\n    p90Latency: formatAnnotation(data.map((item) => item.p90Latency)),\n    p95Latency: formatAnnotation(data.map((item) => item.p95Latency)),\n    p99Latency: formatAnnotation(data.map((item) => item.p99Latency)),\n  };\n\n  return (\n    <ChartContainer\n      config={chartConfig}\n      className={cn(\"h-[100px] w-full\", className)}\n    >\n      <AreaChart\n        accessibilityLayer\n        data={data}\n        margin={{\n          left: 0,\n          right: 0,\n          // NOTE: otherwise the line is cut off\n          top: 2,\n          bottom: 2,\n        }}\n      >\n        <ChartLegend\n          verticalAlign={legendVerticalAlign}\n          content={\n            <ChartLegendBadge\n              handleActive={(item) => {\n                setActiveSeries((prev) => {\n                  if (item.dataKey) {\n                    const key = item.dataKey as keyof typeof chartConfig;\n                    if (singleSeries) {\n                      return [key];\n                    }\n                    if (prev.includes(key)) {\n                      return prev.filter((item) => item !== key);\n                    }\n                    return [...prev, key];\n                  }\n                  return prev;\n                });\n              }}\n              active={activeSeries}\n              annotation={annotation}\n              className={cn(\"overflow-x-scroll\", legendClassName)}\n            />\n          }\n        />\n        <CartesianGrid vertical={false} />\n        <XAxis dataKey=\"timestamp\" hide={xAxisHide} />\n        <ChartTooltip\n          cursor={false}\n          content={\n            <ChartTooltipContent\n              className=\"w-[200px]\"\n              formatter={(value, name) => (\n                <ChartTooltipNumber\n                  chartConfig={chartConfig}\n                  value={value}\n                  name={name}\n                />\n              )}\n            />\n          }\n        />\n        <defs>\n          <linearGradient id=\"fillP50\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop\n              offset=\"5%\"\n              stopColor=\"var(--color-p50Latency)\"\n              stopOpacity={0.8}\n            />\n            <stop\n              offset=\"95%\"\n              stopColor=\"var(--color-p50Latency)\"\n              stopOpacity={0.1}\n            />\n          </linearGradient>\n          <linearGradient id=\"fillP75\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop\n              offset=\"5%\"\n              stopColor=\"var(--color-p75Latency)\"\n              stopOpacity={0.8}\n            />\n            <stop\n              offset=\"95%\"\n              stopColor=\"var(--color-p75Latency)\"\n              stopOpacity={0.1}\n            />\n          </linearGradient>\n          <linearGradient id=\"fillP90\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop\n              offset=\"5%\"\n              stopColor=\"var(--color-p90Latency)\"\n              stopOpacity={0.8}\n            />\n            <stop\n              offset=\"95%\"\n              stopColor=\"var(--color-p90Latency)\"\n              stopOpacity={0.1}\n            />\n          </linearGradient>\n          <linearGradient id=\"fillP95\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop\n              offset=\"5%\"\n              stopColor=\"var(--color-p95Latency)\"\n              stopOpacity={0.8}\n            />\n            <stop\n              offset=\"95%\"\n              stopColor=\"var(--color-p95Latency)\"\n              stopOpacity={0.1}\n            />\n          </linearGradient>\n          <linearGradient id=\"fillP99\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop\n              offset=\"5%\"\n              stopColor=\"var(--color-p99Latency)\"\n              stopOpacity={0.8}\n            />\n            <stop\n              offset=\"95%\"\n              stopColor=\"var(--color-p99Latency)\"\n              stopOpacity={0.1}\n            />\n          </linearGradient>\n        </defs>\n        <Area\n          hide={!activeSeries.includes(\"p50Latency\")}\n          dataKey=\"p50Latency\"\n          type=\"monotone\"\n          stroke=\"var(--color-p50Latency)\"\n          fill=\"url(#fillP50)\"\n          fillOpacity={0.4}\n          dot={false}\n          yAxisId=\"percentile\"\n          connectNulls\n        />\n        <Area\n          hide={!activeSeries.includes(\"p75Latency\")}\n          dataKey=\"p75Latency\"\n          type=\"monotone\"\n          stroke=\"var(--color-p75Latency)\"\n          fill=\"url(#fillP75)\"\n          fillOpacity={0.4}\n          dot={false}\n          yAxisId=\"percentile\"\n          connectNulls\n        />\n        {/* <Area\n          hide={!activeSeries.includes(\"p90Latency\")}\n          dataKey=\"p90Latency\"\n          type=\"monotone\"\n          stroke=\"var(--color-p90Latency)\"\n          fill=\"url(#fillP90)\"\n          fillOpacity={0.4}\n          dot={false}\n          yAxisId=\"percentile\"\n          connectNulls\n        /> */}\n        <Area\n          hide={!activeSeries.includes(\"p95Latency\")}\n          dataKey=\"p95Latency\"\n          type=\"monotone\"\n          stroke=\"var(--color-p95Latency)\"\n          fill=\"url(#fillP95)\"\n          fillOpacity={0.4}\n          dot={false}\n          yAxisId=\"percentile\"\n          connectNulls\n        />\n        <Area\n          hide={!activeSeries.includes(\"p99Latency\")}\n          dataKey=\"p99Latency\"\n          type=\"monotone\"\n          stroke=\"var(--color-p99Latency)\"\n          fill=\"url(#fillP99)\"\n          fillOpacity={0.4}\n          dot={false}\n          yAxisId=\"percentile\"\n          connectNulls\n        />\n        <YAxis\n          domain={yAxisDomain}\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          orientation=\"right\"\n          yAxisId=\"percentile\"\n          tickFormatter={(value) => `${value}ms`}\n        />\n      </AreaChart>\n    </ChartContainer>\n  );\n}\n\nexport function ChartAreaPercentilesSkeleton({\n  className,\n  ...props\n}: React.ComponentProps<typeof Skeleton>) {\n  return (\n    <Skeleton\n      className={cn(\"h-[100px] w-full rounded-lg\", className)}\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/chart/chart-bar-uptime.tsx",
    "content": "\"use client\";\n\nimport { Bar, BarChart, CartesianGrid, XAxis, YAxis } from \"recharts\";\n\nimport { formatNumber } from \"@/lib/formatter\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartLegend,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useState } from \"react\";\nimport { ChartLegendBadge } from \"./chart-legend-badge\";\n\nconst chartConfig = {\n  success: {\n    label: \"success\",\n    // WTF: why is var(--color-success) not working\n    color: \"var(--success)\",\n  },\n  degraded: {\n    label: \"degraded\",\n    color: \"var(--color-warning)\",\n  },\n  error: {\n    label: \"failed\",\n    color: \"var(--color-destructive)\",\n  },\n} satisfies ChartConfig;\n\nexport function ChartBarUptime({\n  className,\n  data,\n}: {\n  className?: string;\n  data: {\n    timestamp: string;\n    success: number;\n    error: number;\n    degraded: number;\n  }[];\n}) {\n  const [activeSeries, setActiveSeries] = useState<\n    Array<keyof typeof chartConfig>\n  >([\"success\", \"error\", \"degraded\"]);\n\n  const annotation = {\n    success: formatNumber(data.reduce((acc, item) => acc + item.success, 0)),\n    error: formatNumber(data.reduce((acc, item) => acc + item.error, 0)),\n    degraded: formatNumber(data.reduce((acc, item) => acc + item.degraded, 0)),\n  };\n\n  return (\n    <ChartContainer\n      config={chartConfig}\n      className={cn(\"h-[130px] w-full\", className)}\n    >\n      <BarChart accessibilityLayer data={data} barCategoryGap={2}>\n        <CartesianGrid vertical={false} />\n        <ChartTooltip\n          cursor={false}\n          content={<ChartTooltipContent indicator=\"dot\" />}\n        />\n        <Bar\n          dataKey=\"success\"\n          fill=\"var(--color-success)\"\n          stackId=\"a\"\n          hide={!activeSeries.includes(\"success\")}\n        />\n        <Bar\n          dataKey=\"degraded\"\n          fill=\"var(--color-degraded)\"\n          stackId=\"a\"\n          hide={!activeSeries.includes(\"degraded\")}\n        />\n        <Bar\n          dataKey=\"error\"\n          fill=\"var(--color-error)\"\n          stackId=\"a\"\n          hide={!activeSeries.includes(\"error\")}\n        />\n        <YAxis\n          domain={[\"dataMin\", \"dataMax\"]}\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          orientation=\"right\"\n        />\n        <XAxis\n          dataKey=\"timestamp\"\n          tickLine={false}\n          tickMargin={8}\n          minTickGap={10}\n          axisLine={false}\n        />\n        <ChartLegend\n          verticalAlign=\"top\"\n          content={\n            <ChartLegendBadge\n              active={activeSeries}\n              handleActive={(item) => {\n                setActiveSeries((prev) => {\n                  if (item.dataKey) {\n                    const key = item.dataKey as keyof typeof chartConfig;\n                    if (prev.includes(key)) {\n                      return prev.filter((item) => item !== key);\n                    }\n                    return [...prev, key];\n                  }\n                  return prev;\n                });\n              }}\n              annotation={annotation}\n              className=\"justify-start overflow-x-scroll ps-1 pt-1\"\n            />\n          }\n        />\n      </BarChart>\n    </ChartContainer>\n  );\n}\n\nexport function ChartBarUptimeSkeleton({ className }: { className?: string }) {\n  return <Skeleton className={cn(\"h-[130px] w-full\", className)} />;\n}\n"
  },
  {
    "path": "apps/status-page/src/components/chart/chart-legend-badge.tsx",
    "content": "import { getPayloadConfigFromPayload } from \"@/lib/chart\";\nimport { badgeVariants } from \"@openstatus/ui/components/ui/badge\";\nimport { useChart } from \"@openstatus/ui/components/ui/chart\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport * as React from \"react\";\nimport type * as RechartsPrimitive from \"recharts\";\nimport type { Payload } from \"recharts/types/component/DefaultLegendContent\";\n\nexport function ChartLegendBadge({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = \"bottom\",\n  nameKey,\n  handleActive,\n  active,\n  maxActive,\n  annotation,\n  tooltip,\n}: React.ComponentProps<\"div\"> &\n  Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n    hideIcon?: boolean;\n    nameKey?: string;\n    // NOTE: additional props compared to default shadcn/ui Legend component\n    handleActive?: (item: Payload) => void;\n    active?: Payload[\"dataKey\"][];\n    maxActive?: number;\n    annotation?: Record<string, string | number | undefined>;\n    tooltip?: Record<string, string | undefined>;\n  }) {\n  const { config } = useChart();\n  const [focusedIndex, setFocusedIndex] = React.useState(0);\n  const buttonRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n\n  if (!payload?.length) {\n    return null;\n  }\n\n  const filteredPayload = payload.filter((item) => item.type !== \"none\");\n  const hasMaxActive = active && maxActive ? active.length >= maxActive : false;\n\n  const handleKeyDown = (event: React.KeyboardEvent) => {\n    if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n      event.preventDefault();\n      const direction = event.key === \"ArrowLeft\" ? -1 : 1;\n      let nextIndex = 0;\n      nextIndex =\n        (focusedIndex + direction + filteredPayload.length) %\n        filteredPayload.length;\n      setFocusedIndex(nextIndex);\n      while (buttonRefs.current[nextIndex]?.disabled === true) {\n        nextIndex =\n          (nextIndex + direction + filteredPayload.length) %\n          filteredPayload.length;\n      }\n      buttonRefs.current[nextIndex]?.focus();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center justify-center gap-1.5\",\n        verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n        className,\n      )}\n      onKeyDown={handleKeyDown}\n      role=\"group\"\n      aria-label=\"Chart legend\"\n    >\n      {filteredPayload.map((item, index) => {\n        const key = `${nameKey || item.dataKey || \"value\"}`;\n        const itemConfig = getPayloadConfigFromPayload(config, item, key);\n        const suffix = annotation?.[item.dataKey as string];\n        const tooltipLabel = tooltip?.[item.dataKey as string];\n        const isActive = active ? active?.includes(item.dataKey) : true;\n        const isFocused = index === focusedIndex;\n\n        const badge = (\n          <button\n            key={item.value}\n            type=\"button\"\n            ref={(el) => {\n              buttonRefs.current[index] = el;\n            }}\n            className={cn(\n              badgeVariants({ variant: \"outline\" }),\n              \"outline-none\",\n              \"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground\",\n              !isActive && \"opacity-60\",\n              !isActive && hasMaxActive && \"cursor-not-allowed opacity-40\",\n            )}\n            onClick={(e) => {\n              e.stopPropagation();\n              e.preventDefault();\n              setFocusedIndex(index);\n              handleActive?.(item);\n            }}\n            onFocus={() => setFocusedIndex(index)}\n            disabled={!isActive && hasMaxActive}\n            tabIndex={isFocused ? 0 : -1}\n          >\n            {itemConfig?.icon && !hideIcon ? (\n              <itemConfig.icon />\n            ) : (\n              <div\n                className=\"h-2 w-2 shrink-0 rounded-(--radius-xs)\"\n                style={{\n                  backgroundColor: item.color,\n                }}\n              />\n            )}\n            {itemConfig?.label}\n            {suffix !== undefined ? (\n              <span className=\"font-mono text-[10px] text-muted-foreground\">\n                {suffix}\n              </span>\n            ) : null}\n          </button>\n        );\n\n        if (tooltipLabel) {\n          return (\n            <ChartLegendTooltip key={item.value} tooltip={tooltipLabel}>\n              {badge}\n            </ChartLegendTooltip>\n          );\n        }\n\n        return badge;\n      })}\n    </div>\n  );\n}\n\nfunction ChartLegendTooltip({\n  children,\n  tooltip,\n  ...props\n}: React.ComponentProps<typeof TooltipTrigger> & { tooltip: string }) {\n  return (\n    <TooltipProvider>\n      <Tooltip delayDuration={0}>\n        <TooltipTrigger className=\"rounded-md\" asChild {...props}>\n          {children}\n        </TooltipTrigger>\n        <TooltipContent>{tooltip}</TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/chart/chart-line-region.tsx",
    "content": "\"use client\";\n\nimport { CartesianGrid, Line, LineChart, XAxis, YAxis } from \"recharts\";\n\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { ChartTooltipNumber } from \"./chart-tooltip-number\";\n\nconst chartConfig = {\n  latency: {\n    label: \"Latency\",\n    color: \"var(--success)\",\n  },\n} satisfies ChartConfig;\n\nexport type TrendPoint = {\n  timestamp: number; // unix millis\n  latency: number; // milliseconds\n};\n\nexport function ChartLineRegion({\n  className,\n  data,\n}: {\n  className?: string;\n  data: TrendPoint[];\n}) {\n  const trendData = data ?? [];\n\n  const chartData = trendData.map((d) => ({\n    timestamp: new Date(d.timestamp).toLocaleString(\"default\", {\n      hour: \"numeric\",\n      minute: \"numeric\",\n      day: \"numeric\",\n      month: \"short\",\n    }),\n    latency: d.latency,\n  }));\n\n  return (\n    <ChartContainer\n      config={chartConfig}\n      className={cn(\"h-[100px] w-full\", className)}\n    >\n      <LineChart\n        accessibilityLayer\n        data={chartData}\n        margin={{\n          left: 12,\n          right: 12,\n        }}\n      >\n        <CartesianGrid vertical={false} />\n        <XAxis dataKey=\"timestamp\" hide />\n        <ChartTooltip\n          cursor={false}\n          content={\n            <ChartTooltipContent\n              className=\"w-[180px]\"\n              formatter={(value, name) => (\n                <ChartTooltipNumber\n                  chartConfig={chartConfig}\n                  value={value}\n                  name={name}\n                />\n              )}\n            />\n          }\n        />\n        <Line\n          dataKey=\"latency\"\n          type=\"monotone\"\n          stroke=\"var(--color-latency)\"\n          strokeWidth={2}\n          dot={false}\n        />\n        <YAxis\n          domain={[\"dataMin\", \"dataMax\"]}\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          orientation=\"right\"\n          tickFormatter={(value) => `${value}ms`}\n        />\n      </LineChart>\n    </ChartContainer>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/chart/chart-line-regions.tsx",
    "content": "\"use client\";\n\nimport {\n  CartesianGrid,\n  Line,\n  LineChart,\n  XAxis,\n  // XAxis,\n  YAxis,\n} from \"recharts\";\n\nimport { regions } from \"@/data/regions\";\nimport { formatMilliseconds } from \"@/lib/formatter\";\nimport type { MonitorRegion } from \"@openstatus/db/src/schema\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartLegend,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useState } from \"react\";\nimport { ChartLegendBadge } from \"./chart-legend-badge\";\nimport { ChartTooltipNumber } from \"./chart-tooltip-number\";\n\nfunction avg(values: (number | null | string)[]) {\n  const n = values.filter((val): val is number => typeof val === \"number\");\n  return Math.round(n.reduce((acc, curr) => acc + curr, 0) / n.length);\n}\n\nfunction formatAnnotation(values: (number | null | string)[]) {\n  if (values.length === 0) return \"N/A\";\n  return formatMilliseconds(avg(values));\n}\n\nfunction getChartConfig(\n  data: {\n    timestamp: string;\n    [key: string]: string | number | null;\n  }[],\n): ChartConfig {\n  const regions =\n    data.length > 0\n      ? Object.keys(data[0]).filter((item) => item !== \"timestamp\")\n      : [];\n\n  return regions\n    .sort((a, b) => {\n      return (\n        avg(data.map((item) => item[b])) - avg(data.map((item) => item[a]))\n      );\n    })\n    .map((region, index) => ({\n      code: region,\n      color: `var(--rainbow-${((index + 5) % 17) + 1})`,\n    }))\n    .reduce(\n      (acc, item) => {\n        acc[item.code] = {\n          label: item.code,\n          color: item.color,\n        };\n        return acc;\n      },\n      {} as Record<string, { label: string; color: string }>,\n    ) satisfies ChartConfig;\n}\n\nfunction getChartConfigDefault(regions: MonitorRegion[]) {\n  return regions.reduce(\n    (acc, region, index) => {\n      acc[region] = {\n        label: region,\n        color: `var(--rainbow-${((index + 5) % 17) + 1})`,\n      };\n      return acc;\n    },\n    {} as Record<string, { label: string; color: string }>,\n  ) satisfies ChartConfig;\n}\n\nexport function ChartLineRegions({\n  className,\n  data,\n  defaultRegions,\n}: {\n  className?: string;\n  data: {\n    timestamp: string;\n    [key: string]: string | number | null;\n  }[];\n  defaultRegions?: MonitorRegion[];\n}) {\n  const chartConfig =\n    data.length > 0\n      ? getChartConfig(data)\n      : getChartConfigDefault(defaultRegions ?? []);\n  const [activeSeries, setActiveSeries] = useState<\n    Array<keyof typeof chartConfig>\n  >(Object.keys(chartConfig).slice(0, 2));\n\n  const annotation = Object.keys(chartConfig).reduce(\n    (acc, region) => {\n      acc[region] = formatAnnotation(data.map((item) => item[region]));\n      return acc;\n    },\n    {} as Record<string, string>,\n  );\n\n  const tooltip = regions.reduce(\n    (acc, region) => {\n      acc[region.code] = region.location;\n      return acc;\n    },\n    {} as Record<string, string>,\n  );\n\n  return (\n    <ChartContainer\n      config={chartConfig}\n      className={cn(\"h-[100px] w-full\", className)}\n    >\n      <LineChart\n        accessibilityLayer\n        data={data}\n        margin={{\n          left: 0,\n          right: 0,\n          top: 2,\n          bottom: 2,\n        }}\n      >\n        <CartesianGrid vertical={false} />\n        <XAxis dataKey=\"timestamp\" />\n        <ChartTooltip\n          cursor={false}\n          content={\n            <ChartTooltipContent\n              formatter={(value, name) => (\n                <ChartTooltipNumber\n                  chartConfig={chartConfig}\n                  value={value}\n                  name={name}\n                  labelFormatter={(_, name) => {\n                    const region = regions.find((r) => r.code === name);\n                    return (\n                      <>\n                        <span className=\"font-mono\">{name}</span>{\" \"}\n                        <span className=\"text-muted-foreground text-xs\">\n                          {region?.location}\n                        </span>\n                      </>\n                    );\n                  }}\n                />\n              )}\n            />\n          }\n        />\n        {Object.keys(chartConfig).map((item) => (\n          <Line\n            key={item}\n            dataKey={item}\n            type=\"monotone\"\n            stroke={`var(--color-${item})`}\n            dot={false}\n            hide={!activeSeries.includes(item)}\n            connectNulls\n          />\n        ))}\n        <YAxis\n          domain={[\"dataMin\", \"dataMax\"]}\n          tickLine={false}\n          axisLine={false}\n          tickMargin={8}\n          orientation=\"right\"\n          tickFormatter={(value) => `${value}ms`}\n        />\n        <ChartLegend\n          verticalAlign=\"top\"\n          content={\n            <ChartLegendBadge\n              handleActive={(item) => {\n                setActiveSeries((prev) => {\n                  if (item.dataKey) {\n                    const key = item.dataKey as keyof typeof chartConfig;\n                    if (prev.includes(key)) {\n                      return prev.filter((item) => item !== key);\n                    }\n                    return [...prev, key];\n                  }\n                  return prev;\n                });\n              }}\n              active={activeSeries}\n              annotation={annotation}\n              tooltip={tooltip}\n              maxActive={6}\n              className=\"justify-start overflow-x-scroll ps-1 pt-1 font-mono\"\n            />\n          }\n        />\n      </LineChart>\n    </ChartContainer>\n  );\n}\n\nexport function ChartLineRegionsSkeleton({\n  className,\n  ...props\n}: React.ComponentProps<typeof Skeleton>) {\n  return (\n    <Skeleton\n      className={cn(\"h-[100px] w-full rounded-lg\", className)}\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/chart/chart-tooltip-number.tsx",
    "content": "import type { ChartConfig } from \"@openstatus/ui/components/ui/chart\";\nimport type {\n  NameType,\n  ValueType,\n} from \"recharts/types/component/DefaultTooltipContent\";\n\ninterface ChartTooltipNumberProps {\n  chartConfig: ChartConfig;\n  value: ValueType;\n  name: NameType;\n  labelFormatter?: (value: ValueType, name: NameType) => React.ReactNode;\n}\n\nexport function ChartTooltipNumber({\n  value,\n  name,\n  chartConfig,\n  labelFormatter,\n}: ChartTooltipNumberProps) {\n  const label: React.ReactNode = labelFormatter\n    ? labelFormatter(value, name)\n    : chartConfig[name as keyof typeof chartConfig]?.label || name;\n\n  return (\n    <>\n      <div\n        className=\"h-2.5 w-2.5 shrink-0 rounded-(--radius-xs) bg-(--color-bg)\"\n        style={\n          {\n            \"--color-bg\": `var(--color-${name})`,\n          } as React.CSSProperties\n        }\n      />\n      <span>{label}</span>\n      <div className=\"ml-auto flex items-baseline gap-0.5 font-medium font-mono text-foreground tabular-nums\">\n        {value}\n        <span className=\"font-normal text-muted-foreground\">ms</span>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/common/kbd.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function Kbd({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"kbd\">) {\n  return (\n    <kbd\n      className={cn(\n        \"-me-1 ms-2 inline-flex h-5 max-h-full items-center rounded border bg-background px-1 font-[inherit] font-medium text-[0.625rem] text-muted-foreground/70\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </kbd>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/common/link.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport NextLink from \"next/link\";\n\nexport const linkVariants = cva(\n  // NOTE: use same ring styles as the button\n  \"outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] rounded-sm\",\n  {\n    variants: {\n      variant: {\n        default: \"text-foreground font-medium\",\n        container: \"focus-visible:border-ring\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport function Link({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<typeof NextLink> & VariantProps<typeof linkVariants>) {\n  return (\n    <NextLink className={cn(linkVariants({ variant, className }))} {...props}>\n      {children}\n    </NextLink>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/content/empty-state.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function EmptyStateContainer({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex h-full flex-col items-center justify-center gap-2 rounded-lg border border-border border-dashed p-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function EmptyStateTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"text-foreground\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function EmptyStateDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      className={cn(\"text-center text-muted-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </p>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/content/metric-card.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport { cva } from \"class-variance-authority\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nconst metricCardVariants = cva(\n  \"flex flex-col gap-1 border rounded-lg px-3 py-2 text-card-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"border-input bg-card\",\n        ghost: \"border-transparent\",\n        destructive: \"border-destructive/80 bg-destructive/10\",\n        success: \"border-success/80 bg-success/10\",\n        warning: \"border-warning/80 bg-warning/10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport function MetricCard({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof metricCardVariants>) {\n  return (\n    <div\n      data-variant={variant}\n      className={cn(metricCardVariants({ variant, className }), \"group/metric\")}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function MetricCardTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"font-medium text-sm\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function MetricCardHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"text-muted-foreground\",\n        \"group-data-[variant=destructive]/metric:text-destructive\",\n        \"group-data-[variant=success]/metric:text-success\",\n        \"group-data-[variant=warning]/metric:text-warning\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function MetricCardValue({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"font-semibold text-foreground\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function MetricCardGroup({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nconst badgeVariants = cva(\"px-1.5 font-mono text-[10px]\", {\n  variants: {\n    variant: {\n      default: \"border-border\",\n      increase:\n        \"border-destructive/20 bg-destructive/10 hover:bg-destructive/10 text-destructive\",\n      decrease:\n        \"border-success/20 bg-success/10 hover:bg-success/10 text-success\",\n    },\n  },\n  defaultVariants: {\n    variant: \"default\",\n  },\n});\n\nexport function MetricCardBadge({\n  value,\n  decimal = 1,\n  className,\n  ...props\n}: React.ComponentProps<typeof Badge> & {\n  value: number;\n  decimal?: number;\n}) {\n  const round = 10 ** decimal; // 10^1 = 10 (1 decimal), 10^2 = 100 (2 decimals), etc.\n  const percentage = Math.round((value - 1) * 100 * round) / round;\n\n  const variant: VariantProps<typeof badgeVariants>[\"variant\"] =\n    percentage > 0 ? \"increase\" : percentage < 0 ? \"decrease\" : \"default\";\n\n  return (\n    <Badge\n      variant=\"secondary\"\n      className={badgeVariants({ variant, className })}\n      {...props}\n    >\n      {percentage !== 0 ? (\n        <span>\n          {percentage > 0 ? <ChevronUp className=\"mr-px size-2.5\" /> : null}\n          {percentage < 0 ? <ChevronDown className=\"mr-px size-2.5\" /> : null}\n        </span>\n      ) : null}\n      {Math.abs(percentage)}%\n    </Badge>\n  );\n}\n\nconst metricCardButtonVariants = cva(\n  \"group w-full text-left transition-all rounded-md outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 cursor-pointer\",\n  // TODO: discuss if we want rings\n);\n\nexport function MetricCardButton({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"button\"> & VariantProps<typeof metricCardVariants>) {\n  return (\n    <button\n      type=\"button\"\n      data-variant={variant}\n      className={cn(\n        metricCardVariants({ variant, className }),\n        metricCardButtonVariants(),\n      )}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/content/process-message.tsx",
    "content": "import type { AnchorHTMLAttributes, HTMLAttributes } from \"react\";\nimport { Fragment, createElement } from \"react\";\nimport { jsx, jsxs } from \"react/jsx-runtime\";\nimport rehypeReact from \"rehype-react\";\nimport remarkParse from \"remark-parse\";\nimport remarkRehype from \"remark-rehype\";\nimport { unified } from \"unified\";\n\nexport function ProcessMessage({ value }: { value: string }) {\n  const result = unified()\n    .use(remarkParse)\n    .use(remarkRehype)\n    .use(rehypeReact, {\n      createElement,\n      Fragment,\n      jsx,\n      jsxs,\n      components: {\n        ul: (props: HTMLAttributes<HTMLUListElement>) => {\n          return (\n            <ul\n              className=\"list-inside list-disc marker:text-muted-foreground/50\"\n              {...props}\n            />\n          );\n        },\n        ol: (_props: HTMLAttributes<HTMLOListElement>) => {\n          return (\n            <ol className=\"list-inside list-decimal marker:text-muted-foreground/50\" />\n          );\n        },\n        a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {\n          return (\n            <a\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"rounded-sm underline outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n              {...props}\n            />\n          );\n        },\n      } as { [key: string]: React.ComponentType<unknown> },\n    })\n    .processSync(value).result;\n\n  return result;\n}\n"
  },
  {
    "path": "apps/status-page/src/components/content/section.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function Section({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"section\">) {\n  return (\n    <section className={cn(\"space-y-4\", className)} {...props}>\n      {children}\n    </section>\n  );\n}\n\nexport function SectionHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function SectionHeaderRow({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col gap-1.5 sm:flex-row sm:items-end sm:justify-between\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function SectionDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"text-muted-foreground text-sm\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function SectionTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"font-medium text-lg\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function SectionGroup({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"mx-auto w-full max-w-4xl space-y-8 px-4 py-8\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function SectionGroupHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"space-y-1.5\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function SectionGroupTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"font-bold text-4xl\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/content/timestamp-hover-card.tsx",
    "content": "import { UTCDate } from \"@date-fns/utc\";\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@openstatus/ui/components/ui/hover-card\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { useMediaQuery } from \"@openstatus/ui/hooks/use-media-query\";\nimport {\n  type HoverCardContentProps,\n  HoverCardPortal,\n} from \"@radix-ui/react-hover-card\";\nimport { format } from \"date-fns\";\nimport { formatDistanceToNowStrict } from \"date-fns\";\nimport { Check, Copy } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\n\nexport function TimestampHoverCard({\n  date,\n  side = \"right\",\n  align = \"start\",\n  alignOffset = -4,\n  sideOffset,\n  children,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof HoverCardTrigger> & {\n  date: Date;\n  side?: HoverCardContentProps[\"side\"];\n  align?: HoverCardContentProps[\"align\"];\n  alignOffset?: HoverCardContentProps[\"alignOffset\"];\n  sideOffset?: HoverCardContentProps[\"sideOffset\"];\n}) {\n  const [open, setOpen] = useState(false);\n  const isTouch = useMediaQuery(\"(hover: none)\");\n  const [_, setRerender] = useState(0);\n\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const relative = formatDistanceToNowStrict(date, { addSuffix: true });\n  const formatted = format(date, \"LLL dd, y HH:mm:ss\");\n  const utc = format(new UTCDate(date), \"LLL dd, y HH:mm:ss\");\n\n  useEffect(() => {\n    // only setInterval if open\n    if (!open) return;\n\n    const interval = setInterval(() => {\n      setRerender((prev) => prev + 1);\n    }, 1000);\n\n    return () => clearInterval(interval);\n  }, [open]);\n\n  return (\n    <HoverCard openDelay={0} closeDelay={0} open={open} onOpenChange={setOpen}>\n      {/* NOTE: the trigger is an `a` tag per default */}\n      <HoverCardTrigger\n        onClick={(e) => {\n          // NOTE: support touch devices\n          if (isTouch) setOpen((prev) => !prev);\n          onClick?.(e);\n        }}\n        {...props}\n      >\n        {children}\n      </HoverCardTrigger>\n      <HoverCardPortal>\n        <HoverCardContent\n          className=\"z-10 w-auto p-2\"\n          {...{ side, align, alignOffset, sideOffset }}\n        >\n          <dl className=\"flex flex-col gap-1\">\n            <Row value={formatted} label={timezone} />\n            <Row value={utc} label=\"UTC\" />\n            {/* <Row value={date.toISOString()} label=\"ISO\" /> */}\n            {/* <Row value={String(date.getTime())} label=\"Timestamp\" /> */}\n            <Row value={relative} label=\"Relative\" />\n          </dl>\n        </HoverCardContent>\n      </HoverCardPortal>\n    </HoverCard>\n  );\n}\n\nfunction Row({ value, label }: { value: string; label: string }) {\n  const { copy, isCopied } = useCopyToClipboard();\n\n  return (\n    <div\n      className=\"group flex items-center justify-between gap-4 text-sm\"\n      onClick={(e) => {\n        e.stopPropagation();\n        copy(value, { withToast: true });\n      }}\n    >\n      <dt className=\"text-muted-foreground\">{label}</dt>\n      <dd className=\"flex items-center gap-1 truncate font-mono\">\n        <span className=\"invisible group-hover:visible\">\n          {!isCopied ? (\n            <Copy className=\"h-3 w-3\" />\n          ) : (\n            <Check className=\"h-3 w-3\" />\n          )}\n        </span>\n        {value}\n      </dd>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/date-picker.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport type { DateRange } from \"react-day-picker\";\n\nimport { Kbd } from \"@/components/common/kbd\";\nimport { formatDateForInput } from \"@/lib/formatter\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Calendar } from \"@openstatus/ui/components/ui/calendar\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { endOfDay } from \"date-fns\";\n\ntype DatePickerProps = {\n  range: DateRange;\n  onSelect: (range: DateRange) => void;\n  presets: { id: string; label: string; values: DateRange; shortcut: string }[];\n};\n\nexport function DatePicker({ range, onSelect, presets }: DatePickerProps) {\n  const [today] = useState(new Date());\n  const disableBefore = presets[presets.length - 1]?.values?.from;\n\n  return (\n    <div>\n      <div className=\"flex flex-row\">\n        <div className=\"relative py-4\">\n          <div className=\"h-full\">\n            <div className=\"flex flex-col px-1\">\n              <div className=\"px-3 py-1 font-medium text-muted-foreground text-xs\">\n                Presets\n              </div>\n              {presets.map((preset) => {\n                const isSelected =\n                  range.from?.getTime() === preset.values.from?.getTime() &&\n                  range.to?.getTime() === preset.values.to?.getTime();\n\n                return (\n                  <Button\n                    key={preset.id}\n                    variant={isSelected ? \"outline\" : \"ghost\"}\n                    size=\"sm\"\n                    className=\"w-full justify-between border border-transparent\"\n                    onClick={() => {\n                      onSelect(preset.values);\n                    }}\n                  >\n                    <span>{preset.label}</span>\n                    <Kbd className=\"font-mono uppercase\">{preset.shortcut}</Kbd>\n                  </Button>\n                );\n              })}\n            </div>\n          </div>\n        </div>\n        <Separator orientation=\"vertical\" className=\"h-auto! w-px\" />\n        <div className=\"flex flex-1 items-center justify-center\">\n          <Calendar\n            mode=\"range\"\n            selected={range}\n            onSelect={(newDate) => {\n              if (newDate) {\n                onSelect({\n                  ...newDate,\n                  to: newDate.to ? endOfDay(newDate.to) : undefined,\n                });\n              }\n            }}\n            className=\"p-2\"\n            disabled={[\n              { after: today }, // Dates before today\n              { before: disableBefore ?? today }, // Dates before last action\n            ]}\n          />\n        </div>\n      </div>\n      <Separator />\n      <div className=\"flex flex-col gap-2 px-3 py-4\">\n        <p className=\"px-1 font-medium text-muted-foreground text-xs\">\n          Custom Range\n        </p>\n        <div className=\"grid gap-2 sm:grid-cols-2\">\n          <div className=\"grid w-full gap-1.5\">\n            <Label htmlFor=\"from\" className=\"px-1\">\n              Start\n            </Label>\n            <Input\n              type=\"datetime-local\"\n              id=\"from\"\n              name=\"from\"\n              min={formatDateForInput(disableBefore ?? today)}\n              max={formatDateForInput(today)}\n              value={range.from ? formatDateForInput(range.from) : \"\"}\n              onChange={(e) => {\n                const newDate = new Date(e.target.value);\n                if (!Number.isNaN(newDate.getTime())) {\n                  onSelect({ ...range, from: newDate });\n                }\n              }}\n              disabled={!range.from}\n            />\n          </div>\n          <div className=\"grid w-full gap-1.5\">\n            <Label htmlFor=\"to\" className=\"px-1\">\n              End\n            </Label>\n            <Input\n              type=\"datetime-local\"\n              id=\"to\"\n              name=\"to\"\n              min={formatDateForInput(range.from ?? today)}\n              max={formatDateForInput(today)}\n              value={range.to ? formatDateForInput(range.to) : \"\"}\n              onChange={(e) => {\n                const newDate = new Date(e.target.value);\n                if (!Number.isNaN(newDate.getTime())) {\n                  onSelect({ ...range, to: newDate });\n                }\n              }}\n              disabled={!range.to}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/forms/form-card.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@openstatus/ui/components/ui/card\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nimport { type VariantProps, cva } from \"class-variance-authority\";\n\nconst formCardVariants = cva(\n  \"group relative w-full overflow-hidden py-0 shadow-none gap-4 rounded-lg\",\n  {\n    variants: {\n      variant: {\n        default: \"\",\n        destructive: \"border-destructive\",\n        info: \"border-info\",\n      },\n      defaultVariants: {\n        variant: \"default\",\n      },\n    },\n  },\n);\n\n// NOTE: Add a formcardprovider to share the variant prop\n\nexport function FormCard({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof formCardVariants>) {\n  return (\n    <Card className={cn(formCardVariants({ variant }), className)} {...props}>\n      {children}\n    </Card>\n  );\n}\n\nexport function FormCardHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <CardHeader\n      className={cn(\n        \"px-4 pt-4 group-has-data-[slot=card-upgrade]:pointer-events-none group-has-data-[slot=card-upgrade]:opacity-50 [.border-b]:pb-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </CardHeader>\n  );\n}\n\nexport function FormCardTitle({ children }: { children: React.ReactNode }) {\n  return <CardTitle>{children}</CardTitle>;\n}\n\nexport function FormCardDescription({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return <CardDescription>{children}</CardDescription>;\n}\n\nexport function FormCardContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <CardContent\n      className={cn(\n        \"px-4 group-has-data-[slot=card-upgrade]:pointer-events-none group-has-data-[slot=card-upgrade]:opacity-50\",\n        \"has-data-[slot=card-content-upgrade]:pointer-events-none has-data-[slot=card-content-upgrade]:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </CardContent>\n  );\n}\n\nexport function FormCardSeparator({\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return <Separator {...props} />;\n}\n\nconst formCardFooterVariants = cva(\n  \"border-t flex items-center gap-2 pb-4 px-4 [&>:last-child]:ml-auto [.border-t]:pt-4\",\n  {\n    variants: {\n      variant: {\n        default: \"\",\n        destructive: \"border-destructive bg-destructive/5\",\n        info: \"border-info bg-info/5\",\n      },\n      defaultVariants: {\n        variant: \"default\",\n      },\n    },\n  },\n);\n\nexport function FormCardFooter({\n  children,\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof formCardFooterVariants>) {\n  return (\n    <CardFooter\n      className={cn(formCardFooterVariants({ variant }), className)}\n      {...props}\n    >\n      {children}\n    </CardFooter>\n  );\n}\n\nexport function FormCardFooterInfo({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer-info\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function FormCardGroup({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-group\"\n      className={cn(\"flex flex-col gap-4\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function FormCardUpgrade({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-upgrade\"\n      className={cn(\"hidden\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n// NOTE; this is for a very specific case where we don't want to disable the whole content\n// and instead disable specpfic card content (e.g. for add-ons)\nexport function FormCardContentUpgrade({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content-upgrade\"\n      className={cn(\"hidden\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function FormCardEmpty({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-empty\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 z-10 bg-background opacity-70 blur\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/forms/form-email.tsx",
    "content": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Form, FormMessage } from \"@openstatus/ui/components/ui/form\";\nimport {\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  email: z.string().email(),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function FormEmail({\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      email: \"\",\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Confirming...\",\n          success: \"Confirmed\",\n          error: (error) => {\n            console.error(error);\n            if (isTRPCClientError(error)) {\n              form.setError(\"email\", { message: error.message });\n              return error.message;\n            }\n            if (error instanceof Error) {\n              form.setError(\"email\", { message: error.message });\n              return error.message;\n            }\n            return \"Failed to confirm\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormField\n          control={form.control}\n          name=\"email\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Email</FormLabel>\n              <FormControl>\n                <Input type=\"email\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/forms/form-manage-subscription.tsx",
    "content": "\"use client\";\n\nimport {\n  StatusBlankContainer,\n  StatusBlankDescription,\n  StatusBlankTitle,\n} from \"@/components/status-page/status-blank\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport {\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\ntype Page = NonNullable<RouterOutputs[\"statusPage\"][\"get\"]>;\n\nconst schema = z.object({\n  pageComponents: z.array(z.number().int().positive()),\n  subscribeComponents: z.boolean(),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function FormManageSubscription({\n  page,\n  defaultValues,\n  onSubmit,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  onSubmit: (values: FormValues) => Promise<void>;\n  onSubmitCallback?: () => void;\n  page?: Page | null;\n  defaultValues?: FormValues;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      pageComponents: defaultValues?.pageComponents ?? [],\n      subscribeComponents: defaultValues?.subscribeComponents ?? true,\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Updating subscription...\",\n          success: \"Subscription updated\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to update subscription\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        onSubmit={form.handleSubmit(submitAction)}\n        className={cn(\"flex flex-col gap-2\", className)}\n        {...props}\n      >\n        <FormField\n          control={form.control}\n          name=\"subscribeComponents\"\n          render={({ field }) => (\n            <FormItem className=\"flex items-center gap-2 px-4\">\n              <FormControl>\n                <Checkbox\n                  checked={field.value}\n                  onCheckedChange={(checked) => {\n                    field.onChange(checked);\n                  }}\n                />\n              </FormControl>\n              <FormLabel>Subscribe to specific components</FormLabel>\n            </FormItem>\n          )}\n        />\n        {form.watch(\"subscribeComponents\") ? (\n          <>\n            <Separator className=\"my-2\" />\n            {page?.trackers && page.trackers.length > 0 ? (\n              page.trackers.map((tracker) => {\n                if (tracker.type === \"group\") {\n                  const groupIds = tracker.components.map((c) => c.id);\n                  return (\n                    <div\n                      key={tracker.groupId}\n                      className=\"flex flex-col gap-2 px-4\"\n                    >\n                      <FormField\n                        control={form.control}\n                        name=\"pageComponents\"\n                        render={({ field }) => {\n                          const allChecked = groupIds.every((id) =>\n                            field.value?.includes(id),\n                          );\n                          const someChecked = groupIds.some((id) =>\n                            field.value?.includes(id),\n                          );\n                          return (\n                            <FormItem className=\"flex items-center gap-2\">\n                              <FormControl>\n                                <Checkbox\n                                  checked={\n                                    allChecked\n                                      ? true\n                                      : someChecked\n                                        ? \"indeterminate\"\n                                        : false\n                                  }\n                                  onCheckedChange={(checked) => {\n                                    const value = field.value ?? [];\n                                    if (checked) {\n                                      field.onChange([\n                                        ...new Set([...value, ...groupIds]),\n                                      ]);\n                                    } else {\n                                      field.onChange(\n                                        value.filter(\n                                          (id) => !groupIds.includes(id),\n                                        ),\n                                      );\n                                    }\n                                  }}\n                                />\n                              </FormControl>\n                              <FormLabel>{tracker.groupName}</FormLabel>\n                            </FormItem>\n                          );\n                        }}\n                      />\n                      {tracker.components.map((component) => (\n                        <FormField\n                          key={component.id}\n                          control={form.control}\n                          name=\"pageComponents\"\n                          render={({ field }) => (\n                            <FormItem className=\"flex items-center gap-2 pl-6\">\n                              <FormControl>\n                                <Checkbox\n                                  checked={field.value?.includes(component.id)}\n                                  onCheckedChange={(checked) => {\n                                    const value = field.value ?? [];\n                                    if (checked) {\n                                      field.onChange([...value, component.id]);\n                                    } else {\n                                      field.onChange(\n                                        value.filter(\n                                          (id) => id !== component.id,\n                                        ),\n                                      );\n                                    }\n                                  }}\n                                />\n                              </FormControl>\n                              <FormLabel>{component.name}</FormLabel>\n                            </FormItem>\n                          )}\n                        />\n                      ))}\n                    </div>\n                  );\n                }\n                return (\n                  <FormField\n                    key={tracker.component.id}\n                    control={form.control}\n                    name=\"pageComponents\"\n                    render={({ field }) => (\n                      <FormItem className=\"flex items-center gap-2 px-4\">\n                        <FormControl>\n                          <Checkbox\n                            checked={field.value?.includes(\n                              tracker.component.id,\n                            )}\n                            onCheckedChange={(checked) => {\n                              const value = field.value ?? [];\n                              if (checked) {\n                                field.onChange([\n                                  ...value,\n                                  tracker.component.id,\n                                ]);\n                              } else {\n                                field.onChange(\n                                  value.filter(\n                                    (id) => id !== tracker.component.id,\n                                  ),\n                                );\n                              }\n                            }}\n                          />\n                        </FormControl>\n                        <FormLabel>{tracker.component.name}</FormLabel>\n                      </FormItem>\n                    )}\n                  />\n                );\n              })\n            ) : (\n              <StatusBlankContainer className=\"px-4\">\n                <StatusBlankTitle>\n                  No components to subscribe to\n                </StatusBlankTitle>\n                <StatusBlankDescription>\n                  This status page has no components to subscribe to.\n                </StatusBlankDescription>\n              </StatusBlankContainer>\n            )}\n          </>\n        ) : null}\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/forms/form-password.tsx",
    "content": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport {\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  password: z.string().min(1),\n});\n\ntype FormValues = z.infer<typeof schema>;\n\nexport function FormPassword({\n  onSubmit,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  onSubmit: (values: FormValues) => Promise<void>;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      password: \"\",\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Confirming...\",\n          success: \"Confirmed\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              form.setError(\"password\", { message: error.message });\n              return error.message;\n            }\n            return \"Failed to confirm\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(submitAction)} {...props}>\n        <FormField\n          control={form.control}\n          name=\"password\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Password</FormLabel>\n              <FormControl>\n                <Input type=\"password\" {...field} />\n              </FormControl>\n            </FormItem>\n          )}\n        />\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/forms/form-subscribe-email.tsx",
    "content": "\"use client\";\n\nimport {\n  StatusBlankContainer,\n  StatusBlankDescription,\n  StatusBlankTitle,\n} from \"@/components/status-page/status-blank\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport { Form } from \"@openstatus/ui/components/ui/form\";\nimport {\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n} from \"@openstatus/ui/components/ui/form\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { useTransition } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\ntype Page = NonNullable<RouterOutputs[\"statusPage\"][\"get\"]>;\n\nconst schema = z.object({\n  email: z.email(),\n  subscribeComponents: z.boolean(),\n  pageComponents: z.array(z.number().int().positive()),\n});\n\nexport type FormValues = z.infer<typeof schema>;\n\nexport function FormSubscribeEmail({\n  page,\n  onSubmit,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  onSubmit: (values: FormValues) => Promise<void>;\n  onSubmitCallback?: () => void;\n  page?: Page | null;\n}) {\n  const form = useForm<FormValues>({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      email: \"\",\n      subscribeComponents: false,\n      pageComponents: [],\n    },\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function submitAction(values: FormValues) {\n    if (isPending) return;\n\n    startTransition(async () => {\n      try {\n        const promise = onSubmit(values);\n        toast.promise(promise, {\n          loading: \"Subscribing...\",\n          success: \"Subscribed\",\n          error: (error) => {\n            if (isTRPCClientError(error)) {\n              return error.message;\n            }\n            return \"Failed to subscribe\";\n          },\n        });\n        await promise;\n      } catch (error) {\n        console.error(error);\n      }\n    });\n  }\n\n  return (\n    <Form {...form}>\n      <form\n        onSubmit={form.handleSubmit(submitAction)}\n        className={cn(\"flex flex-col gap-2\", className)}\n        {...props}\n      >\n        <FormField\n          control={form.control}\n          name=\"email\"\n          render={({ field }) => (\n            <FormItem className=\"px-2\">\n              <FormLabel className=\"sr-only\">Email</FormLabel>\n              <FormControl>\n                <Input placeholder=\"subscribe@me.com\" {...field} />\n              </FormControl>\n            </FormItem>\n          )}\n        />\n\n        <FormField\n          control={form.control}\n          name=\"subscribeComponents\"\n          render={({ field }) => (\n            <FormItem className=\"flex items-center gap-2 px-2\">\n              <FormControl>\n                <Checkbox\n                  checked={field.value}\n                  onCheckedChange={field.onChange}\n                />\n              </FormControl>\n              <FormLabel>Subscribe to specific components</FormLabel>\n            </FormItem>\n          )}\n        />\n        {form.watch(\"subscribeComponents\") && (\n          <>\n            <Separator />\n            <div className=\"-my-2 flex max-h-56 flex-col gap-2 overflow-y-auto bg-muted p-2 px-2\">\n              {page?.trackers && page.trackers.length > 0 ? (\n                page.trackers.map((tracker) => {\n                  if (tracker.type === \"group\") {\n                    const groupIds = tracker.components.map((c) => c.id);\n                    return (\n                      <div\n                        key={tracker.groupId}\n                        className=\"flex flex-col gap-2\"\n                      >\n                        <FormField\n                          control={form.control}\n                          name=\"pageComponents\"\n                          render={({ field }) => {\n                            const allChecked = groupIds.every((id) =>\n                              field.value?.includes(id),\n                            );\n                            const someChecked = groupIds.some((id) =>\n                              field.value?.includes(id),\n                            );\n                            return (\n                              <FormItem className=\"flex items-center gap-2\">\n                                <FormControl>\n                                  <Checkbox\n                                    checked={\n                                      allChecked\n                                        ? true\n                                        : someChecked\n                                          ? \"indeterminate\"\n                                          : false\n                                    }\n                                    onCheckedChange={(checked) => {\n                                      const value = field.value ?? [];\n                                      if (checked) {\n                                        field.onChange([\n                                          ...new Set([...value, ...groupIds]),\n                                        ]);\n                                      } else {\n                                        field.onChange(\n                                          value.filter(\n                                            (id) => !groupIds.includes(id),\n                                          ),\n                                        );\n                                      }\n                                    }}\n                                  />\n                                </FormControl>\n                                <FormLabel>{tracker.groupName}</FormLabel>\n                              </FormItem>\n                            );\n                          }}\n                        />\n                        {tracker.components.map((component) => (\n                          <FormField\n                            key={component.id}\n                            control={form.control}\n                            name=\"pageComponents\"\n                            render={({ field }) => (\n                              <FormItem className=\"flex items-center gap-2 pl-6\">\n                                <FormControl>\n                                  <Checkbox\n                                    checked={field.value?.includes(\n                                      component.id,\n                                    )}\n                                    onCheckedChange={(checked) => {\n                                      const value = field.value ?? [];\n                                      if (checked) {\n                                        field.onChange([\n                                          ...value,\n                                          component.id,\n                                        ]);\n                                      } else {\n                                        field.onChange(\n                                          value.filter(\n                                            (id) => id !== component.id,\n                                          ),\n                                        );\n                                      }\n                                    }}\n                                  />\n                                </FormControl>\n                                <FormLabel>{component.name}</FormLabel>\n                              </FormItem>\n                            )}\n                          />\n                        ))}\n                      </div>\n                    );\n                  }\n                  return (\n                    <FormField\n                      key={tracker.component.id}\n                      control={form.control}\n                      name=\"pageComponents\"\n                      render={({ field }) => (\n                        <FormItem className=\"flex items-center gap-2\">\n                          <FormControl>\n                            <Checkbox\n                              checked={field.value?.includes(\n                                tracker.component.id,\n                              )}\n                              onCheckedChange={(checked) => {\n                                const value = field.value ?? [];\n                                if (checked) {\n                                  field.onChange([\n                                    ...value,\n                                    tracker.component.id,\n                                  ]);\n                                } else {\n                                  field.onChange(\n                                    value.filter(\n                                      (id) => id !== tracker.component.id,\n                                    ),\n                                  );\n                                }\n                              }}\n                            />\n                          </FormControl>\n                          <FormLabel>{tracker.component.name}</FormLabel>\n                        </FormItem>\n                      )}\n                    />\n                  );\n                })\n              ) : (\n                <StatusBlankContainer>\n                  <StatusBlankTitle>\n                    No components to subscribe to\n                  </StatusBlankTitle>\n                  <StatusBlankDescription>\n                    This page has no components to subscribe to.\n                  </StatusBlankDescription>\n                </StatusBlankContainer>\n              )}\n            </div>\n          </>\n        )}\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/icons/discord.tsx",
    "content": "export function DiscordIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Discord</title>\n      <path d=\"M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/icons/github.tsx",
    "content": "export function GitHubIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>GitHub</title>\n      <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/icons/google.tsx",
    "content": "export function GoogleIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Google</title>\n      <path d=\"M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/icons/opsgenie.tsx",
    "content": "export function OpsGenieIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Opsgenie</title>\n      <path d=\"M12.002 0a5.988 5.988 0 1 1 0 11.975 5.988 5.988 0 0 1 0-11.975zm9.723 13.026h-.03l-4.527-2.242a.671.671 0 0 0-.876.268 22.408 22.408 0 0 1-4.306 5.217 22.407 22.407 0 0 1-4.286-5.2.671.671 0 0 0-.876-.269l-4.535 2.226h-.03a.671.671 0 0 0-.248.902 28.85 28.85 0 0 0 4.55 5.933l-.002.001c.024.025.05.048.075.072.335.335.676.664 1.027.981.081.074.165.144.247.217.315.278.632.555.96.82.144.117.295.227.441.341.277.216.552.434.837.639.44.318.888.625 1.346.917a.963.963 0 0 0 1.007.017c.487-.312.962-.64 1.428-.98.068-.05.132-.103.2-.153.358-.266.713-.537 1.06-.82.234-.19.46-.39.688-.588.17-.147.34-.291.506-.442.295-.268.58-.545.864-.825.061-.06.127-.118.188-.179l-.004-.002a28.852 28.852 0 0 0 4.565-5.949.671.671 0 0 0-.269-.902z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/icons/pagerduty.tsx",
    "content": "export function PagerDutyIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>PagerDuty</title>\n      <path d=\"M16.965 1.18C15.085.164 13.769 0 10.683 0H3.73v14.55h6.926c2.743 0 4.8-.164 6.61-1.37 1.975-1.303 3.004-3.484 3.004-6.007 0-2.716-1.262-4.896-3.305-5.994zm-5.5 10.326h-4.21V3.113l3.977-.027c3.62-.028 5.43 1.234 5.43 4.128 0 3.113-2.248 4.292-5.197 4.292zM3.73 17.61h3.525V24H3.73Z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/icons/slack.tsx",
    "content": "export function SlackIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Slack</title>\n      <path d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/nav/footer.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport { TimestampHoverCard } from \"@/components/content/timestamp-hover-card\";\nimport { ThemeDropdown } from \"@/components/themes/theme-dropdown\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Clock } from \"lucide-react\";\nimport { useParams } from \"next/navigation\";\nimport { useEffect, useState } from \"react\";\n\nexport function Footer(props: React.ComponentProps<\"footer\">) {\n  const { domain } = useParams<{ domain: string }>();\n  const [isMounted, setIsMounted] = useState(false);\n  const trpc = useTRPC();\n  const { data: page, dataUpdatedAt } = useQuery({\n    ...trpc.statusPage.get.queryOptions({ slug: domain }),\n  });\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  if (!page) return null;\n\n  return (\n    <footer {...props}>\n      <div className=\"mx-auto flex max-w-2xl items-center justify-between gap-4 px-3 py-2\">\n        <div>\n          {!page.whiteLabel ? (\n            <p className=\"font-mono text-muted-foreground text-xs leading-none sm:text-sm\">\n              powered by{\" \"}\n              <Link\n                href={`https://openstatus.dev?utm_medium=status-page&utm_source=${page.slug}`}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                openstatus.dev\n              </Link>\n            </p>\n          ) : null}\n        </div>\n        <div className=\"flex items-center gap-4\">\n          <TimestampHoverCard\n            date={new Date(dataUpdatedAt)}\n            side=\"top\"\n            align=\"end\"\n            className=\"flex items-center gap-1.5 text-muted-foreground/70\"\n          >\n            {isMounted ? (\n              <>\n                <Clock className=\"size-3\" />\n                <span className=\"font-mono text-xs\">{timezone}</span>\n              </>\n            ) : (\n              <Skeleton className=\"h-4 w-28\" />\n            )}\n          </TimestampHoverCard>\n          <ThemeDropdown />\n        </div>\n      </div>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/nav/header.tsx",
    "content": "\"use client\";\n\nimport { Link } from \"@/components/common/link\";\nimport {\n  type StatusUpdateType,\n  StatusUpdates,\n} from \"@/components/status-page/status-updates\";\nimport { usePathnamePrefix } from \"@/hooks/use-pathname-prefix\";\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Sheet,\n  SheetContent,\n  SheetHeader,\n  SheetTitle,\n  SheetTrigger,\n} from \"@openstatus/ui/components/ui/sheet\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { isTRPCClientError } from \"@trpc/client\";\nimport { Menu, MessageCircleMore } from \"lucide-react\";\nimport NextLink from \"next/link\";\nimport { useParams, usePathname } from \"next/navigation\";\nimport { useState } from \"react\";\nimport { toast } from \"sonner\";\n\ntype Page = RouterOutputs[\"statusPage\"][\"get\"];\n\nfunction useNav() {\n  const pathname = usePathname();\n  const prefix = usePathnamePrefix();\n\n  return [\n    {\n      label: \"Status\",\n      href: `/${prefix}`,\n      isActive: pathname === `/${prefix}`,\n    },\n    {\n      label: \"Events\",\n      href: `${prefix ? `/${prefix}` : \"\"}/events`,\n      isActive: pathname.startsWith(`${prefix ? `/${prefix}` : \"\"}/events`),\n    },\n    {\n      label: \"Monitors\",\n      href: `${prefix ? `/${prefix}` : \"\"}/monitors`,\n      isActive: pathname.startsWith(`${prefix ? `/${prefix}` : \"\"}/monitors`),\n    },\n  ];\n}\n\nfunction getStatusUpdateTypes(page: Page): StatusUpdateType[] {\n  if (!page) return [];\n\n  // NOTE: rss or json are not supported because of authentication\n  if (page?.accessType === \"email-domain\") {\n    return [\"email\"] as const;\n  }\n\n  if (page?.workspacePlan === \"free\") {\n    return [\"slack\", \"rss\", \"json\"] as const;\n  }\n\n  return [\"email\", \"slack\", \"rss\", \"json\"] as const;\n}\n\nexport function Header(props: React.ComponentProps<\"header\">) {\n  const trpc = useTRPC();\n  const { domain } = useParams<{ domain: string }>();\n  const { data: page } = useQuery({\n    ...trpc.statusPage.get.queryOptions({ slug: domain }),\n  });\n\n  const sendPageSubscriptionMutation = useMutation(\n    trpc.emailRouter.sendPageSubscriptionVerification.mutationOptions({}),\n  );\n\n  const subscribeMutation = useMutation(\n    trpc.statusPage.subscribe.mutationOptions({\n      onSuccess: (data) => {\n        if (!data?.id || !data?.token) return;\n        sendPageSubscriptionMutation.mutate(\n          { id: data.id, token: data.token },\n          {\n            onError: (error) => {\n              if (isTRPCClientError(error)) {\n                toast.error(error.message);\n              } else {\n                toast.error(\"Failed to subscribe\");\n              }\n            },\n          },\n        );\n      },\n    }),\n  );\n\n  return (\n    <header {...props}>\n      <nav className=\"mx-auto flex max-w-2xl items-center justify-between gap-3 px-3 py-2\">\n        {/* NOTE: same width as the `StatusUpdates` button */}\n        <div className=\"flex w-[150px] shrink-0\">\n          <div className=\"flex items-center justify-center\">\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              className=\"size-8 overflow-hidden\"\n              asChild\n            >\n              <Link\n                href={page?.homepageUrl || \"/\"}\n                target={page?.homepageUrl ? \"_blank\" : undefined}\n                rel={page?.homepageUrl ? \"noreferrer\" : undefined}\n              >\n                {page?.icon ? (\n                  <img\n                    src={page.icon}\n                    alt={`${page.title} status page`}\n                    className=\"size-8\"\n                  />\n                ) : (\n                  <div className=\"flex size-8 items-center justify-center font-mono\">\n                    {/* NOTE: show the first two letters of the title and if its multiple words, show the first letter of the first two words */}\n                    {page?.title\n                      ?.split(\" \")\n                      .map((word) => word.charAt(0))\n                      .slice(0, 2)\n                      .join(\"\")\n                      .toUpperCase()}\n                  </div>\n                )}\n              </Link>\n            </Button>\n          </div>\n        </div>\n        <NavDesktop className=\"hidden md:flex\" />\n        <div className=\"flex min-w-[150px] items-center justify-end gap-2\">\n          {page?.contactUrl ? (\n            <GetInTouch buttonType=\"icon\" link={page.contactUrl} />\n          ) : null}\n          <StatusUpdates\n            types={getStatusUpdateTypes(page)}\n            onSubscribe={async (values) => {\n              await subscribeMutation.mutateAsync({ slug: domain, ...values });\n            }}\n            page={page}\n          />\n          <NavMobile className=\"md:hidden\" />\n        </div>\n      </nav>\n    </header>\n  );\n}\n\nfunction NavDesktop({ className, ...props }: React.ComponentProps<\"ul\">) {\n  const nav = useNav();\n  return (\n    <ul className={cn(\"flex flex-row gap-0.5\", className)} {...props}>\n      {nav.map((item) => {\n        return (\n          <li key={item.label}>\n            <Button\n              variant={item.isActive ? \"secondary\" : \"ghost\"}\n              className={cn(\n                \"border\",\n                item.isActive ? \"border-input\" : \"border-transparent\",\n              )}\n              size=\"sm\"\n              asChild\n            >\n              <NextLink href={item.href}>{item.label}</NextLink>\n            </Button>\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n\nfunction NavMobile({\n  className,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const [open, setOpen] = useState(false);\n  const nav = useNav();\n  return (\n    <Sheet open={open} onOpenChange={setOpen}>\n      <SheetTrigger asChild>\n        <Button\n          variant=\"secondary\"\n          size=\"sm\"\n          className={cn(\"size-8 border\", className)}\n          {...props}\n        >\n          <Menu />\n        </Button>\n      </SheetTrigger>\n      <SheetContent side=\"top\">\n        <SheetHeader className=\"border-b\">\n          <SheetTitle>Menu</SheetTitle>\n        </SheetHeader>\n        <div className=\"px-1 pb-4\">\n          <ul className=\"flex flex-col gap-1\">\n            {nav.map((item) => {\n              return (\n                <li key={item.label} className=\"w-full\">\n                  <Button\n                    variant={item.isActive ? \"secondary\" : \"ghost\"}\n                    onClick={() => setOpen(false)}\n                    className=\"w-full justify-start\"\n                    size=\"sm\"\n                    asChild\n                  >\n                    <NextLink href={item.href}>{item.label}</NextLink>\n                  </Button>\n                </li>\n              );\n            })}\n          </ul>\n        </div>\n      </SheetContent>\n    </Sheet>\n  );\n}\n\nfunction GetInTouch({\n  buttonType,\n  className,\n  link,\n  ...props\n}: React.ComponentProps<typeof Button> & {\n  buttonType: \"icon\" | \"text\";\n  link: string;\n}) {\n  if (buttonType === \"text\") {\n    return (\n      <Button\n        variant=\"outline\"\n        size=\"sm\"\n        type=\"button\"\n        className={className}\n        asChild\n        {...props}\n      >\n        <a href={link} target=\"_blank\" rel=\"noreferrer\">\n          Get in touch\n        </a>\n      </Button>\n    );\n  }\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            type=\"button\"\n            className={cn(\"size-8\", className)}\n            asChild\n            {...props}\n          >\n            <a href={link} target=\"_blank\" rel=\"noreferrer\">\n              <MessageCircleMore />\n            </a>\n          </Button>\n        </TooltipTrigger>\n        <TooltipContent>\n          <p>Get in touch</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/password-wrapper.tsx",
    "content": "\"use client\";\n\nimport { createProtectedCookieKey } from \"@/lib/protected\";\nimport { useParams } from \"next/navigation\";\nimport { parseAsString, useQueryState } from \"nuqs\";\nimport { useEffect } from \"react\";\n\nexport function PasswordWrapper({ children }: { children?: React.ReactNode }) {\n  const [password, setPassword] = useQueryState(\"pw\", parseAsString);\n  const { domain } = useParams<{ domain: string }>();\n\n  useEffect(() => {\n    if (password) {\n      const key = createProtectedCookieKey(domain);\n      document.cookie = `${key}=${password}; path=/; expires=${new Date(\n        Date.now() + 1000 * 60 * 60 * 24 * 30,\n      ).toUTCString()}`;\n      setPassword(null);\n    }\n  }, [password, domain, setPassword]);\n\n  return children;\n}\n"
  },
  {
    "path": "apps/status-page/src/components/popover/popover-quantile.tsx",
    "content": "import {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function PopoverQuantile({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof PopoverTrigger>) {\n  return (\n    <Popover>\n      <PopoverTrigger\n        className={cn(\n          \"shrink-0 rounded-md p-0 underline decoration-muted-foreground/70 decoration-dotted underline-offset-2 outline-none transition-all hover:decoration-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=open]:decoration-foreground dark:aria-invalid:ring-destructive/40\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </PopoverTrigger>\n      <PopoverContent side=\"top\" className=\"p-0 text-sm\">\n        <p className=\"px-3 py-2 font-medium\">\n          A quantile represents a specific percentile in your dataset.\n        </p>\n        <Separator />\n        <p className=\"px-3 py-2 text-muted-foreground\">\n          For example, p50 is the 50th percentile - the point below which 50% of\n          data falls. Higher percentiles include more data and highlight the\n          upper range.\n        </p>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/floating-button.tsx",
    "content": "\"use client\";\n\nimport { ThemeSelect } from \"@/components/themes/theme-select\";\nimport {\n  THEMES,\n  THEME_KEYS,\n  type ThemeDefinition,\n  generateThemeStyles,\n} from \"@openstatus/theme-store\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@openstatus/ui/components/ui/command\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Check, ChevronsUpDown, Settings } from \"lucide-react\";\nimport { parseAsString, useQueryState } from \"nuqs\";\nimport type React from \"react\";\nimport { createContext, useContext, useEffect, useState } from \"react\";\n\nexport const IS_DEV = process.env.NODE_ENV === \"development\";\n\nexport const VARIANT = [\"success\", \"degraded\", \"error\", \"info\"] as const;\nexport type VariantType = (typeof VARIANT)[number];\n\nexport const CARD_TYPE = [\"duration\", \"requests\", \"manual\"] as const;\nexport type CardType = (typeof CARD_TYPE)[number];\n\nexport const BAR_TYPE = [\"absolute\", \"manual\"] as const;\nexport type BarType = (typeof BAR_TYPE)[number];\n\nexport const COMMUNITY_THEME = THEME_KEYS;\nexport type CommunityTheme = (typeof COMMUNITY_THEME)[number];\n\ninterface StatusPageContextType {\n  cardType: CardType;\n  setCardType: (cardType: CardType) => void;\n  barType: BarType;\n  setBarType: (barType: BarType) => void;\n  showUptime: boolean;\n  setShowUptime: (showUptime: boolean) => void;\n  communityTheme: CommunityTheme;\n  setCommunityTheme: (communityTheme: CommunityTheme) => void;\n}\n\nconst StatusPageContext = createContext<StatusPageContextType | null>(null);\n\nexport function useStatusPage() {\n  const context = useContext(StatusPageContext);\n  if (!context) {\n    throw new Error(\"useStatusPage must be used within a StatusPageProvider\");\n  }\n  return context;\n}\n\nexport function StatusPageProvider({\n  children,\n  defaultCardType = \"duration\",\n  defaultBarType = \"absolute\",\n  defaultShowUptime = true,\n  defaultCommunityTheme = \"default\",\n}: {\n  children: React.ReactNode;\n  defaultCardType?: CardType;\n  defaultBarType?: BarType;\n  defaultShowUptime?: boolean;\n  defaultCommunityTheme?: CommunityTheme;\n}) {\n  const [cardType, setCardType] = useState<CardType>(defaultCardType);\n  const [barType, setBarType] = useState<BarType>(defaultBarType);\n  const [showUptime, setShowUptime] = useState<boolean>(defaultShowUptime);\n  const [communityTheme, setCommunityTheme] = useState<CommunityTheme>(\n    defaultCommunityTheme,\n  );\n  const [isMounted, setIsMounted] = useState(false);\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  useEffect(() => {\n    if (isMounted) {\n      recomputeStyles(communityTheme);\n    }\n  }, [communityTheme, isMounted]);\n\n  return (\n    <StatusPageContext.Provider\n      value={{\n        cardType,\n        setCardType,\n        barType,\n        setBarType,\n        showUptime,\n        setShowUptime,\n        communityTheme,\n        setCommunityTheme,\n      }}\n    >\n      {children}\n    </StatusPageContext.Provider>\n  );\n}\n\nexport function FloatingButton({\n  className,\n  pageId,\n  token,\n}: {\n  className?: string;\n  pageId?: number;\n  token?: string;\n}) {\n  const {\n    cardType,\n    setCardType,\n    barType,\n    setBarType,\n    showUptime,\n    setShowUptime,\n    communityTheme,\n    setCommunityTheme,\n  } = useStatusPage();\n  const [display, setDisplay] = useState(false);\n  const [configToken, setConfigToken] = useQueryState(\n    \"configuration-token\",\n    parseAsString,\n  );\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  useEffect(() => {\n    const enabled =\n      localStorage.getItem(\"configuration-token\") === token ||\n      configToken === token;\n    const host = window.location.host;\n    if (\n      (host.includes(\"localhost\") ||\n        host.includes(\"stpg.dev\") ||\n        host.includes(\"openstatus.dev\") ||\n        host.includes(\"vercel.app\")) &&\n      enabled\n    ) {\n      setDisplay(true);\n      localStorage.setItem(\"configuration-token\", token);\n    } else if (IS_DEV) {\n      setDisplay(true);\n    }\n\n    if (configToken) setConfigToken(null);\n  }, [token]);\n\n  if (!display) return null;\n\n  return (\n    <div className={cn(\"fixed right-4 bottom-4 z-50\", className)}>\n      <Popover>\n        <PopoverTrigger asChild>\n          <Button\n            size=\"icon\"\n            variant=\"secondary\"\n            className=\"size-12 rounded-full border\"\n          >\n            <Settings className=\"size-5\" />\n            <span className=\"sr-only\">Open status page settings</span>\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-80 p-0\" align=\"end\">\n          <div className=\"space-y-4 p-4\">\n            <div className=\"space-y-2\">\n              <h4 className=\"font-medium leading-none\">Status Page Settings</h4>\n              <p className=\"text-muted-foreground text-sm\">\n                Configure the status page appearance\n              </p>\n            </div>\n            <div className=\"grid grid-cols-2 gap-4\">\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"bar-type\">Bar Type</Label>\n                <Select\n                  value={barType}\n                  onValueChange={(v) => {\n                    setBarType(v as BarType);\n                    if (v !== \"absolute\") {\n                      setCardType(v as CardType);\n                    } else {\n                      setCardType(\"requests\");\n                    }\n                  }}\n                >\n                  <SelectTrigger id=\"bar-type\" className=\"w-full capitalize\">\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {BAR_TYPE.map((v) => (\n                      <SelectItem key={v} value={v} className=\"capitalize\">\n                        {v}\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"card-type\">Card Type</Label>\n                <Select\n                  value={cardType}\n                  onValueChange={(v) => setCardType(v as CardType)}\n                  disabled={barType !== \"absolute\"}\n                >\n                  <SelectTrigger id=\"card-type\" className=\"w-full capitalize\">\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {CARD_TYPE.map((v) => (\n                      <SelectItem\n                        key={v}\n                        value={v}\n                        className=\"capitalize\"\n                        disabled={[\"dominant\", \"manual\"].includes(v)}\n                      >\n                        {v}\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"show-uptime\">Show Uptime</Label>\n                <Select\n                  value={showUptime ? \"true\" : \"false\"}\n                  onValueChange={(v) => setShowUptime(v === \"true\")}\n                >\n                  <SelectTrigger id=\"show-uptime\" className=\"w-full capitalize\">\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {[\"true\", \"false\"].map((v) => (\n                      <SelectItem key={v} value={v} className=\"capitalize\">\n                        {v}\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"theme\">Theme</Label>\n                <ThemeSelect id=\"theme\" className=\"max-w-full\" />\n              </div>\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"community-theme\">Community Theme</Label>\n                <Popover>\n                  <PopoverTrigger asChild>\n                    <Button\n                      id=\"community-theme\"\n                      variant=\"outline\"\n                      role=\"combobox\"\n                      className=\"w-full justify-between font-normal\"\n                    >\n                      <span className=\"truncate\">\n                        {THEMES[communityTheme].name}\n                      </span>\n                      <ChevronsUpDown className=\"opacity-50\" />\n                    </Button>\n                  </PopoverTrigger>\n                  <PopoverContent className=\"p-0\">\n                    <Command>\n                      <CommandInput\n                        placeholder=\"Search themes...\"\n                        className=\"h-9\"\n                      />\n                      <CommandList>\n                        <CommandEmpty>No themes found.</CommandEmpty>\n                        <CommandGroup>\n                          {COMMUNITY_THEME.map((theme) => (\n                            <CommandItem\n                              value={theme}\n                              key={theme}\n                              onSelect={(v) =>\n                                setCommunityTheme(v as CommunityTheme)\n                              }\n                            >\n                              <span className=\"truncate\">\n                                {THEMES[theme].name}\n                              </span>\n                              <span className=\"truncate font-commit-mono text-muted-foreground text-xs\">\n                                by {THEMES[theme].author.name}\n                              </span>\n                              <Check\n                                className={cn(\n                                  \"ml-auto\",\n                                  theme === communityTheme\n                                    ? \"opacity-100\"\n                                    : \"opacity-0\",\n                                )}\n                              />\n                            </CommandItem>\n                          ))}\n                        </CommandGroup>\n                      </CommandList>\n                    </Command>\n                  </PopoverContent>\n                </Popover>\n              </div>\n            </div>\n          </div>\n          <Separator />\n          <div className=\"p-4\">\n            <Button className=\"w-full\" size=\"sm\" asChild>\n              <a\n                href={\n                  pageId\n                    ? `https://app.openstatus.dev/status-pages/${pageId}/edit?type=${barType}&value=${cardType}&uptime=${showUptime}&theme=${communityTheme}`\n                    : \"https://app.openstatus.dev/status-pages\"\n                }\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                Save Configuration\n              </a>\n            </Button>\n          </div>\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n\nexport function recomputeStyles(\n  newTheme: CommunityTheme,\n  overrides?: Partial<ThemeDefinition>,\n) {\n  try {\n    // Only update the text content of existing style tags, don't remove them\n    // This prevents React hydration errors during navigation\n    const allThemeStyles = document.querySelectorAll(\n      \"style[id='theme-styles']\",\n    );\n    const newStyles = generateThemeStyles(newTheme, overrides);\n\n    // Update all style elements with the same content\n    // This way React can manage the DOM without conflicts\n    allThemeStyles.forEach((style) => {\n      style.textContent = newStyles;\n    });\n  } catch (error) {\n    console.error(error);\n  }\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/floating-theme.tsx",
    "content": "\"use client\";\n\nimport { ThemeSelect } from \"@/components/themes/theme-select\";\nimport { THEMES, THEME_KEYS } from \"@openstatus/theme-store\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@openstatus/ui/components/ui/command\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Check, ChevronsUpDown, Palette } from \"lucide-react\";\nimport { useEffect } from \"react\";\nimport { useState } from \"react\";\nimport { useStatusPage } from \"./floating-button\";\n\nexport const COMMUNITY_THEME = THEME_KEYS;\nexport type CommunityTheme = (typeof COMMUNITY_THEME)[number];\n\nexport function FloatingTheme({ className }: { className?: string }) {\n  const { communityTheme, setCommunityTheme } = useStatusPage();\n  const [display, setDisplay] = useState(false);\n  const [open, setOpen] = useState(false);\n\n  useEffect(() => {\n    const enabled = sessionStorage.getItem(\"community-theme\") === \"true\";\n    const host = window.location.host;\n    if (\n      (host.includes(\"localhost\") ||\n        host.includes(\"stpg.dev\") ||\n        host.includes(\"openstatus.dev\") ||\n        host.includes(\"vercel.app\")) &&\n      enabled\n    ) {\n      setDisplay(true);\n      setOpen(true);\n    }\n  }, []);\n\n  if (!display) return null;\n\n  return (\n    <div className={cn(\"fixed right-4 bottom-4 z-50\", className)}>\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <Button size=\"icon\" className=\"size-12 rounded-full\">\n            <Palette className=\"size-5\" />\n            <span className=\"sr-only\">Open theme settings</span>\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-80 p-0\" align=\"end\">\n          <div className=\"space-y-4 p-4\">\n            <div className=\"space-y-2\">\n              <h4 className=\"font-medium leading-none\">Theme Settings</h4>\n              <p className=\"text-muted-foreground text-sm\">\n                Test community themes on the status page.\n              </p>\n            </div>\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"theme\">Theme Mode</Label>\n              <ThemeSelect id=\"theme\" className=\"w-full\" />\n            </div>\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"community-theme\">Community Theme</Label>\n              <Popover>\n                <PopoverTrigger asChild>\n                  <Button\n                    id=\"community-theme\"\n                    variant=\"outline\"\n                    role=\"combobox\"\n                    className=\"w-full justify-between font-normal\"\n                  >\n                    <span className=\"truncate\">\n                      {THEMES[communityTheme].name}\n                    </span>\n                    <ChevronsUpDown className=\"opacity-50\" />\n                  </Button>\n                </PopoverTrigger>\n                <PopoverContent className=\"p-0\">\n                  <Command>\n                    <CommandInput\n                      placeholder=\"Search themes...\"\n                      className=\"h-9\"\n                    />\n                    <CommandList>\n                      <CommandEmpty>No themes found.</CommandEmpty>\n                      <CommandGroup>\n                        {COMMUNITY_THEME.map((theme) => (\n                          <CommandItem\n                            value={theme}\n                            key={theme}\n                            onSelect={(v) =>\n                              setCommunityTheme(v as CommunityTheme)\n                            }\n                          >\n                            <span className=\"truncate\">\n                              {THEMES[theme].name}\n                            </span>\n                            <span className=\"truncate font-commit-mono text-muted-foreground text-xs\">\n                              by {THEMES[theme].author.name}\n                            </span>\n                            <Check\n                              className={cn(\n                                \"ml-auto\",\n                                theme === communityTheme\n                                  ? \"opacity-100\"\n                                  : \"opacity-0\",\n                              )}\n                            />\n                          </CommandItem>\n                        ))}\n                      </CommandGroup>\n                    </CommandList>\n                  </Command>\n                </PopoverContent>\n              </Popover>\n            </div>\n          </div>\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/messages.ts",
    "content": "export const messages = {\n  long: {\n    success: \"All Systems Operational\",\n    degraded: \"Degraded Performance\",\n    error: \"Downtime Performance\",\n    info: \"Maintenance\",\n    empty: \"No Data\",\n  },\n  short: {\n    success: \"Operational\",\n    degraded: \"Degraded\",\n    error: \"Downtime\",\n    info: \"Maintenance\",\n    empty: \"No Data\",\n  },\n};\n\nexport const requests = {\n  success: \"Normal\",\n  degraded: \"Degraded\",\n  error: \"Error\",\n  info: \"Maintenance\",\n  empty: \"No Data\",\n};\n\nexport const status = {\n  resolved: \"Resolved\",\n  monitoring: \"Monitoring\",\n  identified: \"Identified\",\n  investigating: \"Investigating\",\n};\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-banner.tsx",
    "content": "import {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport {\n  AlertCircleIcon,\n  CheckIcon,\n  TriangleAlertIcon,\n  WrenchIcon,\n} from \"lucide-react\";\nimport { messages } from \"./messages\";\nimport { StatusTimestamp } from \"./status\";\n\nexport function StatusBanner({\n  className,\n  status,\n}: React.ComponentProps<\"div\"> & {\n  status?: \"success\" | \"degraded\" | \"error\" | \"info\";\n}) {\n  return (\n    <StatusBannerContainer\n      status={status}\n      className={cn(\n        \"flex items-center gap-3 px-3 py-2 sm:px-4 sm:py-3\",\n        \"data-[status=success]:bg-success/20\",\n        \"data-[status=degraded]:bg-warning/20\",\n        \"data-[status=error]:bg-destructive/20\",\n        \"data-[status=info]:bg-info/20\",\n        className,\n      )}\n    >\n      <StatusBannerIcon className=\"flex-shrink-0\" />\n      <div className=\"flex flex-1 flex-wrap items-center justify-between gap-2\">\n        <StatusBannerMessage className=\"font-semibold text-xl\" />\n        <StatusTimestamp date={new Date()} className=\"text-xs\" />\n      </div>\n    </StatusBannerContainer>\n  );\n}\n\nexport function StatusBannerContainer({\n  className,\n  children,\n  status,\n}: React.ComponentProps<\"div\"> & {\n  status?: \"success\" | \"degraded\" | \"error\" | \"info\";\n}) {\n  return (\n    <div\n      data-slot=\"status-banner\"\n      data-status={status}\n      className={cn(\n        \"group/status-banner overflow-hidden rounded-lg border\",\n        \"data-[status=success]:border-success data-[status=success]:bg-success/5 dark:data-[status=success]:bg-success/10\",\n        \"data-[status=degraded]:border-warning data-[status=degraded]:bg-warning/5 dark:data-[status=degraded]:bg-warning/10\",\n        \"data-[status=error]:border-destructive data-[status=error]:bg-destructive/5 dark:data-[status=error]:bg-destructive/10\",\n        \"data-[status=info]:border-info data-[status=info]:bg-info/5 dark:data-[status=info]:bg-info/10\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBannerMessage({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(className)} {...props}>\n      <span className=\"hidden group-data-[status=success]/status-banner:block\">\n        {messages.long.success}\n      </span>\n      <span className=\"hidden group-data-[status=degraded]/status-banner:block\">\n        {messages.long.degraded}\n      </span>\n      <span className=\"hidden group-data-[status=error]/status-banner:block\">\n        {messages.long.error}\n      </span>\n      <span className=\"hidden group-data-[status=info]/status-banner:block\">\n        {messages.long.info}\n      </span>\n    </div>\n  );\n}\n\nexport function StatusBannerTitle({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"px-3 py-2 font-medium text-background\",\n        \"group-data-[status=success]/status-banner:bg-success\",\n        \"group-data-[status=degraded]/status-banner:bg-warning\",\n        \"group-data-[status=error]/status-banner:bg-destructive\",\n        \"group-data-[status=info]/status-banner:bg-info\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBannerContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"flex flex-col gap-2 px-3 py-2 sm:px-4 sm:py-3\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBannerIcon({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex size-7 items-center justify-center rounded-full bg-muted text-background [&>svg]:size-4\",\n        \"group-data-[status=success]/status-banner:bg-success\",\n        \"group-data-[status=degraded]/status-banner:bg-warning\",\n        \"group-data-[status=error]/status-banner:bg-destructive\",\n        \"group-data-[status=info]/status-banner:bg-info\",\n        className,\n      )}\n      {...props}\n    >\n      <CheckIcon className=\"hidden group-data-[status=success]/status-banner:block\" />\n      <TriangleAlertIcon className=\"hidden group-data-[status=degraded]/status-banner:block\" />\n      <AlertCircleIcon className=\"hidden group-data-[status=error]/status-banner:block\" />\n      <WrenchIcon className=\"hidden group-data-[status=info]/status-banner:block\" />\n    </div>\n  );\n}\n\n// Tabs Components\n\nexport function StatusBannerTabs({\n  className,\n  children,\n  status,\n  ...props\n}: React.ComponentProps<typeof Tabs> & {\n  status?: \"success\" | \"degraded\" | \"error\" | \"info\";\n}) {\n  return (\n    <Tabs\n      data-slot=\"status-banner-tabs\"\n      data-status={status}\n      className={cn(\n        \"gap-0\",\n        \"data-[status=success]:bg-success/20\",\n        \"data-[status=degraded]:bg-warning/20\",\n        \"data-[status=error]:bg-destructive/20\",\n        \"data-[status=info]:bg-info/20\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </Tabs>\n  );\n}\n\nexport function StatusBannerTabsList({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof TabsList>) {\n  return (\n    <div className={cn(\"rounded-t-lg\", \"w-full overflow-x-auto\")}>\n      <TabsList\n        className={cn(\n          \"rounded-none rounded-t-lg p-0\",\n          \"border-none\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </TabsList>\n    </div>\n  );\n}\n\nexport function StatusBannerTabsTrigger({\n  className,\n  children,\n  status,\n  ...props\n}: React.ComponentProps<typeof TabsTrigger> & {\n  status?: \"success\" | \"degraded\" | \"error\" | \"info\";\n}) {\n  return (\n    <TabsTrigger\n      data-slot=\"status-banner-tabs-trigger\"\n      data-status={status}\n      className={cn(\n        \"font-mono\",\n        \"rounded-none border-none focus-visible:ring-inset\",\n        \"h-full text-foreground data-[state=active]:text-background dark:text-foreground dark:data-[state=active]:text-background\",\n        \"data-[state=active]:data-[status=success]:bg-success data-[status=success]:bg-success/50 dark:data-[state=active]:data-[status=success]:bg-success dark:data-[status=success]:bg-success/50\",\n        \"data-[state=active]:data-[status=degraded]:bg-warning data-[status=degraded]:bg-warning/50 dark:data-[state=active]:data-[status=degraded]:bg-warning dark:data-[status=degraded]:bg-warning/50\",\n        \"data-[state=active]:data-[status=error]:bg-destructive data-[status=error]:bg-destructive/50 dark:data-[state=active]:data-[status=error]:bg-destructive dark:data-[status=error]:bg-destructive/50\",\n        \"data-[state=active]:data-[status=info]:bg-info data-[status=info]:bg-info/50 dark:data-[state=active]:data-[status=info]:bg-info dark:data-[status=info]:bg-info/50\",\n        \"data-[state=active]:shadow-none\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </TabsTrigger>\n  );\n}\n\n// NOTE: tabing into content is not being highlighted\nexport function StatusBannerTabsContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof TabsContent>) {\n  return (\n    <TabsContent className={cn(\"-mx-3\", className)} {...props}>\n      {children}\n    </TabsContent>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-blank.tsx",
    "content": "import { Button } from \"@openstatus/ui/components/ui/button\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport Link from \"next/link\";\n\nexport function StatusBlankContainer({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col items-center justify-center gap-2.5 rounded-lg border bg-muted/30 px-3 py-2 text-center sm:px-8 sm:py-6\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBlankTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"font-medium\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBlankDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"font-mono text-muted-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBlankLink({\n  children,\n  className,\n  href,\n  ...props\n}: React.ComponentProps<typeof Button> & { href: string }) {\n  return (\n    <Button\n      variant=\"outline\"\n      size=\"sm\"\n      className={cn(\"text-foreground\", className)}\n      asChild\n      {...props}\n    >\n      <Link href={href}>{children}</Link>\n    </Button>\n  );\n}\n\nexport function StatusBlankContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"space-y-1\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBlankReport({ ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <StatusBlankPage {...props}>\n      <StatusBlankPageHeader />\n      <div className=\"flex w-full flex-col\">\n        <StatusBlankReportUpdate />\n        <StatusBlankReportUpdate />\n      </div>\n      <StatusBlankOverlay />\n    </StatusBlankPage>\n  );\n}\n\nexport function StatusBlankMonitor({ ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <StatusBlankPage {...props}>\n      <StatusBlankPageHeader />\n      <StatusBlankMonitorUptime />\n      <StatusBlankOverlay />\n    </StatusBlankPage>\n  );\n}\n\nexport function StatusBlankPage({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"relative flex w-full max-w-xs flex-1 flex-col items-center justify-center gap-4 overflow-hidden rounded-lg border border-border/70 bg-background px-3 py-2 text-center\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBlankPageHeader({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex w-full items-center justify-between gap-4\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"size-3 rounded-sm bg-accent/60\" />\n      <div className=\"flex flex-row gap-1\">\n        <div className=\"h-3 w-8 rounded-sm bg-accent/60\" />\n        <div className=\"h-3 w-8 rounded-sm bg-accent/60\" />\n        <div className=\"h-3 w-8 rounded-sm bg-accent/60\" />\n      </div>\n      <div className=\"h-3 w-8 rounded-sm bg-accent/60\" />\n    </div>\n  );\n}\n\nexport function StatusBlankMonitorUptime({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex w-full flex-col items-center justify-between gap-1\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex w-full flex-row gap-1\">\n        <div className=\"h-3 w-8 rounded-sm bg-accent\" />\n        <div className=\"h-3 w-12 rounded-sm bg-accent\" />\n        <div className=\"h-3 w-10 rounded-sm bg-accent\" />\n      </div>\n      <div className=\"flex w-full flex-row items-end gap-0.5\">\n        {Array.from({ length: 30 }).map((_, index) => (\n          <div\n            key={index}\n            className={cn(\n              \"h-12 flex-1 rounded-sm bg-accent\",\n              [10, 20].includes(index) && \"h-8\",\n              [25].includes(index) && \"h-10\",\n            )}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport function StatusBlankReportUpdate({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"flex w-full items-start gap-2\", className)} {...props}>\n      <div className=\"flex h-full flex-col items-center gap-0.5\">\n        <div className=\"size-3 rounded-sm bg-accent\" />\n      </div>\n      <div className=\"flex flex-1 flex-col gap-1 pb-2\">\n        <div className=\"flex items-center gap-1\">\n          <div className=\"h-3 w-12 rounded-sm bg-accent\" />\n          <div className=\"h-3 w-16 rounded-sm bg-accent\" />\n        </div>\n        <div className=\"h-3 w-full rounded-sm bg-accent\" />\n        <div className=\"h-3 w-full rounded-sm bg-accent\" />\n      </div>\n    </div>\n  );\n}\n\nexport function StatusBlankOverlay({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"absolute inset-0 flex flex-col items-center justify-center gap-2 bg-gradient-to-b from-40% from-transparent to-background p-2\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBlankEvents({\n  title = \"No reports found\",\n  description = \"No reports found for this status page.\",\n  ...props\n}: React.ComponentProps<typeof StatusBlankContainer> & {\n  title?: string;\n  description?: string;\n}) {\n  return (\n    <StatusBlankContainer {...props}>\n      <div className=\"relative mt-8 flex w-full flex-col items-center justify-center\">\n        <StatusBlankReport className=\"-top-16 absolute scale-60 opacity-50\" />\n        <StatusBlankReport className=\"-top-8 absolute scale-80 opacity-80\" />\n        <StatusBlankReport />\n      </div>\n      <StatusBlankContent>\n        <StatusBlankTitle>{title}</StatusBlankTitle>\n        <StatusBlankDescription>{description}</StatusBlankDescription>\n      </StatusBlankContent>\n    </StatusBlankContainer>\n  );\n}\n\nexport function StatusBlankMonitors({\n  title = \"No public monitors\",\n  description = \"No public monitors have been added to this page.\",\n  ...props\n}: React.ComponentProps<typeof StatusBlankContainer> & {\n  title?: string;\n  description?: string;\n}) {\n  return (\n    <StatusBlankContainer {...props}>\n      <div className=\"relative mt-8 flex w-full flex-col items-center justify-center\">\n        <StatusBlankMonitor className=\"-top-16 absolute scale-60 opacity-50\" />\n        <StatusBlankMonitor className=\"-top-8 absolute scale-80 opacity-80\" />\n        <StatusBlankMonitor />\n      </div>\n      <StatusBlankContent>\n        <StatusBlankTitle>{title}</StatusBlankTitle>\n        <StatusBlankDescription>{description}</StatusBlankDescription>\n      </StatusBlankContent>\n    </StatusBlankContainer>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-charts.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function StatusChartContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-chart-content\"\n      className={cn(\"flex flex-col gap-3\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusChartHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-chart-header\"\n      className={cn(\"flex flex-col\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusChartTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-chart-title\"\n      className={cn(\"font-medium text-base text-foreground\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusChartDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-chart-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-events.tsx",
    "content": "import { ProcessMessage } from \"@/components/content/process-message\";\nimport { TimestampHoverCard } from \"@/components/content/timestamp-hover-card\";\nimport { usePathnamePrefix } from \"@/hooks/use-pathname-prefix\";\nimport { formatDate, formatDateRange, formatDateTime } from \"@/lib/formatter\";\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { formatDistanceStrict } from \"date-fns\";\nimport { Check } from \"lucide-react\";\nimport { status } from \"./messages\";\n\nexport function StatusEventGroup({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"flex flex-col gap-4\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function StatusEvent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"relative flex flex-col gap-2\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function StatusEventContent({\n  className,\n  hoverable = true,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  hoverable?: boolean;\n}) {\n  // TODO: add Link\n  return (\n    <div\n      data-hoverable={hoverable}\n      className={cn(\n        \"group -mx-3 -my-2 flex flex-col gap-2 rounded-lg border border-transparent px-3 py-2\",\n        \"data-[hoverable=true]:hover:cursor-pointer data-[hoverable=true]:hover:border-border/50 data-[hoverable=true]:hover:bg-muted/50\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusEventTitle({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"font-medium\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function StatusEventTitleCheck({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"flex items-center pl-1\", className)} {...props}>\n      <TooltipProvider>\n        <Tooltip>\n          <TooltipTrigger>\n            <div className=\"rounded-full border border-success/20 bg-success/10 p-0.5 text-success\">\n              <Check className=\"size-3 shrink-0\" />\n            </div>\n          </TooltipTrigger>\n          <TooltipContent>\n            <p>Report resolved</p>\n          </TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n    </div>\n  );\n}\n\n// TODO: affected monitors\nexport function StatusEventAffected({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"flex flex-wrap gap-1\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function StatusEventAffectedBadge({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <Badge\n      variant=\"secondary\"\n      className={cn(\"text-[10px]\", className)}\n      {...props}\n    >\n      {children}\n    </Badge>\n  );\n}\n\nexport function StatusEventDate({\n  className,\n  date,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  date: Date;\n}) {\n  const isFuture = date > new Date();\n  const distance = formatDistanceStrict(date, new Date(), { addSuffix: true });\n  return (\n    <div className={cn(\"flex gap-2 lg:flex-col\", className)} {...props}>\n      <div className=\"font-medium text-foreground\">\n        {formatDate(date, { month: \"short\" })}\n      </div>{\" \"}\n      <Badge\n        variant=\"secondary\"\n        className={cn(\n          \"text-[10px]\",\n          isFuture ? \"bg-info text-background dark:text-foreground\" : \"\",\n        )}\n      >\n        {distance}\n      </Badge>\n    </div>\n  );\n}\n\nexport function StatusEventAside({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className=\"lg:-left-32 border border-transparent lg:absolute lg:top-0 lg:h-full\">\n      <div className={cn(\"lg:sticky lg:top-0 lg:left-0\", className)} {...props}>\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport function StatusEventTimelineReport({\n  className,\n  updates,\n  withDot = true,\n  maxUpdates,\n  reportId,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  // TODO: remove unused props\n  reportId: number;\n  updates: {\n    date: Date;\n    message: string;\n    status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\";\n  }[];\n  withDot?: boolean;\n  maxUpdates?: number;\n}) {\n  const _prefix = usePathnamePrefix();\n  const sortedUpdates = [...updates].sort(\n    (a, b) => b.date.getTime() - a.date.getTime(),\n  );\n  const _hasMoreUpdates = maxUpdates && sortedUpdates.length > maxUpdates;\n  const displayedUpdates = maxUpdates\n    ? sortedUpdates.slice(0, maxUpdates)\n    : sortedUpdates;\n\n  return (\n    <div className={cn(\"text-muted-foreground text-sm\", className)} {...props}>\n      {/* NOTE: make sure they are sorted by date */}\n      {displayedUpdates.map((update, index) => {\n        const updateDate = new Date(update.date);\n        let durationText: string | undefined;\n\n        if (index === 0) {\n          const startedAt = new Date(\n            sortedUpdates[sortedUpdates.length - 1].date,\n          );\n          const duration = formatDistanceStrict(startedAt, updateDate);\n\n          if (duration !== \"0 seconds\" && update.status === \"resolved\") {\n            durationText = `(in ${duration})`;\n          }\n        } else {\n          const lastUpdateDate = new Date(displayedUpdates[index - 1].date);\n          const timeFromLast = formatDistanceStrict(updateDate, lastUpdateDate);\n          durationText = `(${timeFromLast} earlier)`;\n        }\n\n        return (\n          <StatusEventTimelineReportUpdate\n            key={index}\n            report={update}\n            duration={durationText}\n            withSeparator={index !== displayedUpdates.length - 1}\n            withDot={withDot}\n            isLast={index === displayedUpdates.length - 1}\n          />\n        );\n      })}\n    </div>\n  );\n}\n\nexport function StatusEventTimelineReportUpdate({\n  report,\n  duration,\n  withSeparator = true,\n  withDot = true,\n  isLast = false,\n}: {\n  report: {\n    date: Date;\n    message: string;\n    status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\";\n  };\n  withSeparator?: boolean;\n  duration?: string;\n  withDot?: boolean;\n  isLast?: boolean;\n}) {\n  return (\n    <div data-variant={report.status} className=\"group\">\n      <div className=\"flex flex-row items-center justify-between gap-2\">\n        <div className=\"flex flex-row gap-4\">\n          {withDot ? (\n            <div className=\"flex flex-col\">\n              <div className=\"flex h-5 flex-col items-center justify-center\">\n                <StatusEventTimelineDot />\n              </div>\n              {withSeparator ? <StatusEventTimelineSeparator /> : null}\n            </div>\n          ) : null}\n          <div className={cn(isLast ? \"mb-0\" : \"mb-2\")}>\n            <StatusEventTimelineTitle>\n              <span>{status[report.status]}</span>{\" \"}\n              <span className=\"text-muted-foreground/70\">·</span>{\" \"}\n              <span className=\"font-mono text-muted-foreground text-xs\">\n                <TimestampHoverCard date={new Date(report.date)} asChild>\n                  <span>{formatDateTime(report.date)}</span>\n                </TimestampHoverCard>\n              </span>{\" \"}\n              {duration ? (\n                <span className=\"font-mono text-muted-foreground/70 text-xs\">\n                  {duration}\n                </span>\n              ) : null}\n            </StatusEventTimelineTitle>\n            <StatusEventTimelineMessage>\n              {report.message.trim() === \"\" ? (\n                <span className=\"text-muted-foreground/70\">-</span>\n              ) : (\n                <ProcessMessage value={report.message} />\n              )}\n            </StatusEventTimelineMessage>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function StatusEventTimelineMaintenance({\n  maintenance,\n  withDot = true,\n}: {\n  maintenance: {\n    title: string;\n    message: string;\n    from: Date;\n    to: Date;\n  };\n  withDot?: boolean;\n}) {\n  const duration = formatDistanceStrict(maintenance.from, maintenance.to);\n  const range = formatDateRange(maintenance.from, maintenance.to);\n  // NOTE: because formatDateRange is sure to return a range, we can split it into two dates\n  const [from, to] = range.split(\" - \");\n  return (\n    <div data-variant=\"maintenance\" className=\"group\">\n      <div className=\"flex flex-row items-center justify-between gap-2\">\n        <div className=\"flex flex-row gap-4\">\n          {withDot ? (\n            <div className=\"flex flex-col\">\n              <div className=\"flex h-5 flex-col items-center justify-center\">\n                <StatusEventTimelineDot />\n              </div>\n            </div>\n          ) : null}\n          {/* NOTE: is always last, no need for className=\"mb-2\" */}\n          <div>\n            <StatusEventTimelineTitle>\n              <span>Maintenance</span>{\" \"}\n              <span className=\"text-muted-foreground/70\">·</span>{\" \"}\n              <span className=\"font-mono text-muted-foreground text-xs\">\n                <TimestampHoverCard date={maintenance.from} asChild>\n                  <span>{from}</span>\n                </TimestampHoverCard>\n                {\" - \"}\n                <TimestampHoverCard date={maintenance.to} asChild>\n                  <span>{to}</span>\n                </TimestampHoverCard>\n              </span>{\" \"}\n              {duration ? (\n                <span className=\"font-mono text-muted-foreground/70 text-xs\">\n                  (for {duration})\n                </span>\n              ) : null}\n            </StatusEventTimelineTitle>\n            <StatusEventTimelineMessage>\n              {maintenance.message.trim() === \"\" ? (\n                <span className=\"text-muted-foreground/70\">-</span>\n              ) : (\n                <ProcessMessage value={maintenance.message} />\n              )}\n            </StatusEventTimelineMessage>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function StatusEventTimelineTitle({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"font-medium text-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusEventTimelineMessage({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"py-1.5 font-mono text-muted-foreground text-sm\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusEventTimelineDot({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"size-2.5 shrink-0 rounded-full bg-muted\",\n        \"group-data-[variant=resolved]:bg-success\",\n        \"group-data-[variant=monitoring]:bg-info\",\n        \"group-data-[variant=identified]:bg-warning\",\n        \"group-data-[variant=investigating]:bg-destructive\",\n        \"group-data-[variant=maintenance]:bg-info\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function StatusEventTimelineSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      orientation=\"vertical\"\n      className={cn(\n        \"mx-auto flex-1\",\n        \"group-data-[variant=resolved]:bg-success\",\n        \"group-data-[variant=monitoring]:bg-info\",\n        \"group-data-[variant=identified]:bg-warning\",\n        \"group-data-[variant=investigating]:bg-destructive\",\n        \"group-data-[variant=maintenance]:bg-info\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-feed.tsx",
    "content": "\"use client\";\nimport { usePathnamePrefix } from \"@/hooks/use-pathname-prefix\";\nimport Link from \"next/link\";\nimport {\n  StatusBlankContainer,\n  StatusBlankContent,\n  StatusBlankDescription,\n  StatusBlankLink,\n  StatusBlankReport,\n  StatusBlankTitle,\n} from \"./status-blank\";\nimport {\n  StatusEvent,\n  StatusEventAffected,\n  StatusEventAffectedBadge,\n  StatusEventAside,\n  StatusEventContent,\n  StatusEventDate,\n  StatusEventGroup,\n  StatusEventTimelineMaintenance,\n  StatusEventTimelineReport,\n  StatusEventTitle,\n} from \"./status-events\";\n\ntype StatusReport = {\n  id: number;\n  title: string;\n  affected: string[];\n  updates: {\n    date: Date;\n    message: string;\n    status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\";\n  }[];\n};\n\ntype Maintenance = {\n  id: number;\n  title: string;\n  message: string;\n  from: Date;\n  to: Date;\n  affected: string[];\n};\n\ntype UnifiedEvent = {\n  id: number;\n  title: string;\n  type: \"report\" | \"maintenance\";\n  startDate: Date;\n  data: StatusReport | Maintenance;\n};\n\nexport function StatusFeed({\n  statusReports = [],\n  maintenances = [],\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  statusReports?: StatusReport[];\n  maintenances?: Maintenance[];\n  showLinks?: boolean;\n}) {\n  const prefix = usePathnamePrefix();\n  const unifiedEvents: UnifiedEvent[] = [\n    ...statusReports.map((report) => ({\n      id: report.id,\n      title: report.title,\n      type: \"report\" as const,\n      // FIXME: we have a flicker here when the report is updated\n      startDate: report.updates[report.updates.length - 1]?.date || new Date(),\n      data: report,\n    })),\n    ...maintenances.map((maintenance) => ({\n      id: maintenance.id,\n      title: maintenance.title,\n      type: \"maintenance\" as const,\n      startDate: maintenance.from,\n      data: maintenance,\n    })),\n  ].sort((a, b) => b.startDate.getTime() - a.startDate.getTime());\n\n  if (unifiedEvents.length === 0) {\n    return (\n      <StatusBlankContainer>\n        <div className=\"relative mt-8 flex w-full flex-col items-center justify-center\">\n          <StatusBlankReport className=\"-top-16 absolute scale-60 opacity-50\" />\n          <StatusBlankReport className=\"-top-8 absolute scale-80 opacity-80\" />\n          <StatusBlankReport />\n        </div>\n        <StatusBlankContent>\n          <StatusBlankTitle>No recent notifications</StatusBlankTitle>\n          <StatusBlankDescription>\n            There have been no reports within the last 7 days.\n          </StatusBlankDescription>\n          <StatusBlankLink href={`${prefix ? `/${prefix}` : \"\"}/events`}>\n            View events history\n          </StatusBlankLink>\n        </StatusBlankContent>\n      </StatusBlankContainer>\n    );\n  }\n\n  return (\n    <StatusEventGroup {...props}>\n      {unifiedEvents.map((event) => {\n        if (event.type === \"report\") {\n          const report = event.data as StatusReport;\n          return (\n            <StatusEvent key={`report-${event.id}`}>\n              <StatusEventAside>\n                <StatusEventDate date={event.startDate} />\n              </StatusEventAside>\n              <Link\n                href={`${prefix ? `/${prefix}` : \"\"}/events/report/${\n                  report.id\n                }`}\n                className=\"rounded-lg\"\n              >\n                <StatusEventContent>\n                  <StatusEventTitle>{report.title}</StatusEventTitle>\n                  {report.affected.length > 0 && (\n                    <StatusEventAffected>\n                      {report.affected.map((affected, index) => (\n                        <StatusEventAffectedBadge key={index}>\n                          {affected}\n                        </StatusEventAffectedBadge>\n                      ))}\n                    </StatusEventAffected>\n                  )}\n                  <StatusEventTimelineReport\n                    updates={report.updates}\n                    reportId={report.id}\n                  />\n                </StatusEventContent>\n              </Link>\n            </StatusEvent>\n          );\n        }\n\n        if (event.type === \"maintenance\") {\n          const maintenance = event.data as Maintenance;\n          return (\n            <StatusEvent key={`maintenance-${event.id}`}>\n              <StatusEventAside>\n                <StatusEventDate date={event.startDate} />\n              </StatusEventAside>\n              <Link\n                href={`${prefix ? `/${prefix}` : \"\"}/events/maintenance/${\n                  maintenance.id\n                }`}\n                className=\"rounded-lg\"\n              >\n                <StatusEventContent>\n                  <StatusEventTitle>{maintenance.title}</StatusEventTitle>\n                  {maintenance.affected.length > 0 && (\n                    <StatusEventAffected>\n                      {maintenance.affected.map((affected, index) => (\n                        <StatusEventAffectedBadge key={index}>\n                          {affected}\n                        </StatusEventAffectedBadge>\n                      ))}\n                    </StatusEventAffected>\n                  )}\n                  <StatusEventTimelineMaintenance\n                    maintenance={{\n                      title: maintenance.title,\n                      message: maintenance.message,\n                      from: maintenance.from,\n                      to: maintenance.to,\n                    }}\n                  />\n                </StatusEventContent>\n              </Link>\n            </StatusEvent>\n          );\n        }\n        return null;\n      })}\n      <StatusBlankLink\n        className=\"mx-auto\"\n        href={`${prefix ? `/${prefix}` : \"\"}/events`}\n      >\n        View events history\n      </StatusBlankLink>\n    </StatusEventGroup>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-monitor-tabs.tsx",
    "content": "import { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport function StatusMonitorTabs({\n  className,\n  ...props\n}: React.ComponentProps<typeof Tabs>) {\n  return <Tabs className={cn(\"gap-6\", className)} {...props} />;\n}\n\nexport function StatusMonitorTabsList({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsList>) {\n  return (\n    <TabsList\n      className={cn(\"flex h-auto min-h-fit w-full\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function StatusMonitorTabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsTrigger>) {\n  return (\n    <TabsTrigger\n      className={cn(\n        \"min-w-0 flex-1 flex-col items-start gap-0.5 text-foreground dark:text-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function StatusMonitorTabsTriggerLabel({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"w-full truncate text-left\", className)} {...props} />\n  );\n}\n\nexport function StatusMonitorTabsTriggerValue({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex min-h-5 flex-row flex-wrap items-center gap-1 text-left text-muted-foreground text-xs\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function StatusMonitorTabsTriggerValueSkeleton({\n  className,\n  ...props\n}: React.ComponentProps<typeof Skeleton>) {\n  return <Skeleton className={cn(\"h-5 w-24\", className)} {...props} />;\n}\n\nexport function StatusMonitorTabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsContent>) {\n  return (\n    <TabsContent\n      className={cn(\n        \"flex flex-col gap-2 rounded-lg focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-monitor.tsx",
    "content": "\"use client\";\n\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useMediaQuery } from \"@openstatus/ui/hooks/use-media-query\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { formatDistanceToNowStrict } from \"date-fns\";\nimport {\n  AlertCircleIcon,\n  CheckIcon,\n  InfoIcon,\n  TriangleAlertIcon,\n  WrenchIcon,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport type { VariantType } from \"./floating-button\";\nimport { StatusTracker, StatusTrackerSkeleton } from \"./status-tracker\";\n\n// TODO: use status instead of variant\n\ntype Data = NonNullable<\n  RouterOutputs[\"statusPage\"][\"getUptime\"]\n>[number][\"data\"];\n\nexport function StatusMonitor({\n  className,\n  status = \"success\",\n  showUptime = true,\n  data = [],\n  monitor,\n  uptime,\n  isLoading = false,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  status?: VariantType;\n  showUptime?: boolean;\n  uptime?: string;\n  monitor: {\n    name: string;\n    description?: string | null;\n  };\n  data?: Data;\n  isLoading?: boolean;\n}) {\n  return (\n    <div\n      data-slot=\"status-monitor\"\n      data-variant={status}\n      className={cn(\"group/monitor flex flex-col gap-1\", className)}\n      {...props}\n    >\n      <div className=\"flex flex-row items-center justify-between gap-4\">\n        <div className=\"flex min-w-0 flex-row items-center gap-2\">\n          <StatusMonitorTitle>{monitor.name}</StatusMonitorTitle>\n          <StatusMonitorDescription>\n            {monitor.description}\n          </StatusMonitorDescription>\n        </div>\n        <div className=\"flex flex-row items-center gap-2\">\n          {/* TODO: check if we can improve that cuz its looking ugly */}\n          {showUptime ? (\n            <>\n              {isLoading ? (\n                <StatusMonitorUptimeSkeleton />\n              ) : (\n                <StatusMonitorUptime>{uptime}</StatusMonitorUptime>\n              )}\n              <StatusMonitorIcon />\n            </>\n          ) : (\n            <StatusMonitorStatus />\n          )}\n        </div>\n      </div>\n      {isLoading ? <StatusTrackerSkeleton /> : <StatusTracker data={data} />}\n      <StatusMonitorFooter data={data} isLoading={isLoading} />\n    </div>\n  );\n}\n\nexport function StatusMonitorTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"truncate font-medium font-mono text-base text-foreground leading-5\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusMonitorDescription({\n  onClick,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipTrigger>) {\n  const isTouch = useMediaQuery(\"(hover: none)\");\n  const [open, setOpen] = useState(false);\n\n  if (!children) return null;\n\n  return (\n    <TooltipProvider delayDuration={0}>\n      <Tooltip open={open} onOpenChange={setOpen}>\n        <TooltipTrigger\n          onClick={(e) => {\n            if (isTouch) setOpen((prev) => !prev);\n            onClick?.(e);\n          }}\n          className=\"rounded-full\"\n          {...props}\n        >\n          <InfoIcon className=\"size-4 text-muted-foreground\" />\n        </TooltipTrigger>\n        <TooltipContent>\n          <p>{children}</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n\nexport function StatusMonitorIcon({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex size-[12.5px] items-center justify-center rounded-full bg-muted text-background [&>svg]:size-[9px]\",\n        \"group-data-[variant=success]/monitor:bg-success\",\n        \"group-data-[variant=degraded]/monitor:bg-warning\",\n        \"group-data-[variant=error]/monitor:bg-destructive\",\n        \"group-data-[variant=info]/monitor:bg-info\",\n        className,\n      )}\n      {...props}\n    >\n      <CheckIcon className=\"hidden group-data-[variant=success]/monitor:block\" />\n      <TriangleAlertIcon className=\"hidden group-data-[variant=degraded]/monitor:block\" />\n      <AlertCircleIcon className=\"hidden group-data-[variant=error]/monitor:block\" />\n      <WrenchIcon className=\"hidden group-data-[variant=info]/monitor:block\" />\n    </div>\n  );\n}\n\nexport function StatusMonitorFooter({\n  data,\n  isLoading,\n}: {\n  data: Data;\n  isLoading?: boolean;\n}) {\n  return (\n    <div className=\"flex flex-row items-center justify-between font-mono text-muted-foreground text-xs leading-none\">\n      <div>\n        {isLoading ? (\n          <Skeleton className=\"h-3 w-18\" />\n        ) : data.length > 0 ? (\n          formatDistanceToNowStrict(new Date(data[0].day), {\n            unit: \"day\",\n            addSuffix: true,\n          })\n        ) : (\n          \"-\"\n        )}\n      </div>\n      <div>today</div>\n    </div>\n  );\n}\n\nexport function StatusMonitorUptime({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      {...props}\n      className={cn(\n        \"font-mono text-foreground/80 text-sm leading-none\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusMonitorUptimeSkeleton({\n  className,\n  ...props\n}: React.ComponentProps<typeof Skeleton>) {\n  return <Skeleton className={cn(\"h-4 w-16\", className)} {...props} />;\n}\n\nexport function StatusMonitorStatus({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"font-mono text-sm leading-none\",\n        \"group-data-[variant=success]/monitor:text-success\",\n        \"group-data-[variant=degraded]/monitor:text-warning\",\n        \"group-data-[variant=error]/monitor:text-destructive\",\n        \"group-data-[variant=info]/monitor:text-info\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"hidden group-data-[variant=success]/monitor:block\">\n        Operational\n      </span>\n      <span className=\"hidden group-data-[variant=degraded]/monitor:block\">\n        Degraded\n      </span>\n      <span className=\"hidden group-data-[variant=error]/monitor:block\">\n        Downtime\n      </span>\n      <span className=\"hidden group-data-[variant=info]/monitor:block\">\n        Maintenance\n      </span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-tracker-group.tsx",
    "content": "\"use client\";\n\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@openstatus/ui/components/ui/collapsible\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useEffect, useState } from \"react\";\nimport type { VariantType } from \"./floating-button\";\nimport { StatusMonitorIcon, StatusMonitorStatus } from \"./status-monitor\";\n\nexport function StatusTrackerGroup({\n  children,\n  title,\n  status,\n  className,\n  defaultOpen = false,\n  ...props\n}: React.ComponentProps<typeof CollapsibleTrigger> & {\n  title: string;\n  status?: VariantType;\n  children?: React.ReactNode;\n  defaultOpen?: boolean;\n}) {\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  return (\n    <Collapsible\n      defaultOpen={defaultOpen}\n      className={cn(\n        \"-mx-3\",\n        \"rounded-lg border border-transparent bg-muted/50 hover:border-border/50 data-[state=open]:border-border/50 data-[state=open]:bg-muted/50\",\n        className,\n      )}\n    >\n      <CollapsibleTrigger\n        className={cn(\n          \"group/monitor flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 font-medium font-mono\",\n          \"cursor-pointer\",\n          className,\n        )}\n        data-variant={status}\n        {...props}\n      >\n        {title}\n        <div className=\"flex items-center gap-2\">\n          <StatusMonitorStatus className=\"text-sm\" />\n          <StatusMonitorIcon />\n        </div>\n      </CollapsibleTrigger>\n      <CollapsibleContent\n        data-animate={mounted}\n        className={cn(\n          \"flex flex-col gap-3 border-border/50 border-t px-3 py-2\",\n          \"overflow-hidden\",\n          // REMINDER: otherwise, if defaultOpen is true, the animation will be triggered and we have a layout shift\n          \"data-[animate=true]:data-[state=closed]:animate-collapsible-up data-[animate=true]:data-[state=open]:animate-collapsible-down\",\n        )}\n      >\n        {children}\n      </CollapsibleContent>\n    </Collapsible>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-tracker.tsx",
    "content": "\"use client\";\n\nimport { Kbd } from \"@/components/common/kbd\";\nimport { usePathnamePrefix } from \"@/hooks/use-pathname-prefix\";\nimport { formatDateRange } from \"@/lib/formatter\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@openstatus/ui/components/ui/hover-card\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { useMediaQuery } from \"@openstatus/ui/hooks/use-media-query\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { formatDistanceStrict } from \"date-fns\";\nimport Link from \"next/link\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { requests } from \"./messages\";\nimport { chartConfig } from \"./utils\";\n\ntype UptimeData = NonNullable<\n  RouterOutputs[\"statusPage\"][\"getUptime\"]\n>[number][\"data\"];\n\n// TODO: keyboard arrow navigation\n// FIXME: on small screens, avoid pinned state\n// TODO: only on real mobile devices, use click events\n// TODO: improve status reports -> add duration and time\n// TODO: support headless mode -> both card and bar type share only maintenance or degraded mode\n// TODO: support status page logo + onClick to homepage\n// TODO: widget type -> current status only | with status history\n\nexport function StatusTracker({ data }: { data: UptimeData }) {\n  const [pinnedIndex, setPinnedIndex] = useState<number | null>(null);\n  const [focusedIndex, setFocusedIndex] = useState<number | null>(null);\n  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n  const isTouch = useMediaQuery(\"(hover: none)\");\n  const prefix = usePathnamePrefix();\n\n  useEffect(() => {\n    const handleOutsideClick = (e: MouseEvent) => {\n      if (\n        pinnedIndex !== null &&\n        containerRef.current &&\n        !containerRef.current.contains(e.target as Node)\n      ) {\n        setPinnedIndex(null);\n      }\n    };\n\n    if (pinnedIndex !== null) {\n      document.addEventListener(\"mousedown\", handleOutsideClick);\n      return () =>\n        document.removeEventListener(\"mousedown\", handleOutsideClick);\n    }\n  }, [pinnedIndex]);\n\n  useEffect(() => {\n    if (focusedIndex !== null && containerRef.current) {\n      const buttons = containerRef.current.querySelectorAll('[role=\"button\"]');\n      const targetButton = buttons[focusedIndex] as HTMLElement;\n      if (targetButton) {\n        targetButton.focus();\n      }\n    }\n  }, [focusedIndex]);\n\n  useEffect(() => {\n    return () => {\n      if (hoverTimeoutRef.current) {\n        clearTimeout(hoverTimeoutRef.current);\n      }\n    };\n  }, []);\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Escape\") {\n      setPinnedIndex(null);\n      setFocusedIndex(null);\n      setHoveredIndex(null);\n\n      if (focusedIndex !== null) {\n        const buttons =\n          containerRef.current?.querySelectorAll('[role=\"button\"]');\n        const button = buttons?.[focusedIndex] as HTMLElement;\n        if (button) {\n          button.blur();\n        }\n      }\n\n      if (hoverTimeoutRef.current) {\n        clearTimeout(hoverTimeoutRef.current);\n        hoverTimeoutRef.current = null;\n      }\n      return;\n    }\n\n    if (focusedIndex !== null) {\n      switch (e.key) {\n        case \"ArrowLeft\":\n          e.preventDefault();\n          setFocusedIndex((prev) =>\n            prev !== null && prev > 0 ? prev - 1 : data.length - 1,\n          );\n          break;\n        case \"ArrowRight\":\n          e.preventDefault();\n          setFocusedIndex((prev) =>\n            prev !== null && prev < data.length - 1 ? prev + 1 : 0,\n          );\n          break;\n        case \"ArrowUp\":\n          e.preventDefault();\n          const prevMonitor = containerRef.current?.closest(\n            '[data-slot=\"status-monitor\"]',\n          )?.previousElementSibling;\n          if (prevMonitor) {\n            const prevTracker = prevMonitor.querySelector('[role=\"toolbar\"]');\n            if (prevTracker) {\n              const buttons = prevTracker.querySelectorAll('[role=\"button\"]');\n              const button = buttons?.[focusedIndex] as HTMLElement;\n              if (button) {\n                button.focus();\n              }\n            }\n          }\n          break;\n        case \"ArrowDown\":\n          e.preventDefault();\n          const nextMonitor = containerRef.current?.closest(\n            '[data-slot=\"status-monitor\"]',\n          )?.nextElementSibling;\n          if (nextMonitor) {\n            const nextTracker = nextMonitor.querySelector('[role=\"toolbar\"]');\n            if (nextTracker) {\n              const buttons = nextTracker.querySelectorAll('[role=\"button\"]');\n              const button = buttons?.[focusedIndex] as HTMLElement;\n              if (button) {\n                button.focus();\n              }\n            }\n          }\n          break;\n        case \"Enter\":\n        case \"Escape\":\n        case \" \":\n          e.preventDefault();\n          handleBarClick(focusedIndex);\n          break;\n      }\n    }\n  };\n\n  const handleBarClick = (index: number) => {\n    // Clear any pending hover timeout\n    if (hoverTimeoutRef.current) {\n      clearTimeout(hoverTimeoutRef.current);\n      hoverTimeoutRef.current = null;\n    }\n    if (pinnedIndex === index) {\n      setPinnedIndex(null);\n    } else {\n      setPinnedIndex(index);\n    }\n  };\n\n  const handleBarFocus = (index: number) => {\n    setFocusedIndex(index);\n  };\n\n  const handleBarBlur = (e: React.FocusEvent, _currentIndex: number) => {\n    const relatedTarget = e.relatedTarget as HTMLElement;\n    const isMovingToAnotherBar =\n      relatedTarget &&\n      relatedTarget.closest('[role=\"toolbar\"]') === containerRef.current &&\n      relatedTarget.getAttribute(\"role\") === \"button\";\n\n    if (!isMovingToAnotherBar) {\n      setFocusedIndex(null);\n    }\n  };\n\n  const handleBarMouseEnter = (index: number) => {\n    if (hoverTimeoutRef.current) {\n      clearTimeout(hoverTimeoutRef.current);\n      hoverTimeoutRef.current = null;\n    }\n    setHoveredIndex(index);\n  };\n\n  const handleBarMouseLeave = () => {\n    hoverTimeoutRef.current = setTimeout(() => {\n      setHoveredIndex(null);\n    }, 100);\n  };\n\n  const handleHoverCardMouseEnter = () => {\n    if (hoverTimeoutRef.current) {\n      clearTimeout(hoverTimeoutRef.current);\n      hoverTimeoutRef.current = null;\n    }\n  };\n\n  const handleHoverCardMouseLeave = () => {\n    setHoveredIndex(null);\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"flex h-[50px] w-full items-end\"\n      onKeyDown={handleKeyDown}\n      role=\"toolbar\"\n      aria-label=\"Status tracker\"\n    >\n      {data.map((item, index) => {\n        const isPinned = pinnedIndex === index;\n        const isFocused = focusedIndex === index;\n        const isHovered = hoveredIndex === index;\n\n        return (\n          <HoverCard\n            key={item.day}\n            openDelay={0}\n            closeDelay={0}\n            open={isPinned || isFocused || isHovered}\n          >\n            <HoverCardTrigger asChild>\n              <div\n                className={cn(\n                  \"group relative mx-px flex h-full w-full cursor-pointer flex-col rounded-full outline-none first:ml-0 last:mr-0 hover:opacity-80 focus-visible:opacity-80 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[aria-pressed=true]:opacity-80\",\n                  \"overflow-hidden rounded-full\",\n                )}\n                onClick={() => handleBarClick(index)}\n                onFocus={() => handleBarFocus(index)}\n                onBlur={(e) => handleBarBlur(e, index)}\n                onMouseEnter={() => handleBarMouseEnter(index)}\n                onMouseLeave={handleBarMouseLeave}\n                tabIndex={\n                  index === data.length - 1 && focusedIndex === null\n                    ? 0\n                    : isFocused\n                      ? 0\n                      : -1\n                }\n                role=\"button\"\n                aria-label={`Day ${index + 1} status`}\n                aria-pressed={isPinned}\n              >\n                {/* Render processed bar segments from backend */}\n                {item.bar.map((segment, segmentIndex) => (\n                  <div\n                    key={`${item.day}-${segment.status}-${segmentIndex}`}\n                    className={cn(\"w-full transition-all\", {\n                      \"rounded-t-full\": segmentIndex === 0,\n                      \"rounded-b-full\": segmentIndex === item.bar.length - 1,\n                    })}\n                    style={{\n                      height: `${segment.height}%`,\n                      backgroundColor: chartConfig[segment.status].color,\n                    }}\n                  />\n                ))}\n              </div>\n            </HoverCardTrigger>\n            <HoverCardContent\n              side=\"top\"\n              align=\"center\"\n              // NOTE: remove animation and transition to avoid flickering\n              className=\"![animation-duration:0ms] ![transition-duration:0ms] w-auto min-w-40 p-0\"\n              onMouseEnter={handleHoverCardMouseEnter}\n              onMouseLeave={handleHoverCardMouseLeave}\n            >\n              <div>\n                <div className=\"p-2 text-xs\">\n                  {new Date(item.day).toLocaleDateString(\"default\", {\n                    day: \"numeric\",\n                    month: \"short\",\n                    year: \"numeric\",\n                  })}\n                </div>\n                <Separator />\n                <div className=\"space-y-1 p-2 text-sm\">\n                  {/* Render processed card data from backend */}\n                  {item.card.map((cardItem, cardIndex) => (\n                    <StatusTrackerContent\n                      key={`${item.day}-card-${cardIndex}`}\n                      status={cardItem.status}\n                      value={cardItem.value}\n                    />\n                  ))}\n                </div>\n                {item.events.length > 0 && (\n                  <>\n                    <Separator />\n                    <div className=\"p-2\">\n                      {item.events.map((event) => {\n                        const eventStatus =\n                          event.type === \"incident\"\n                            ? \"error\"\n                            : event.type === \"report\"\n                              ? \"degraded\"\n                              : \"info\";\n\n                        const content = (\n                          <StatusTrackerEvent\n                            key={`${event.id}-${event.type}`}\n                            status={eventStatus}\n                            name={event.name}\n                            from={event.from}\n                            to={event.to}\n                          />\n                        );\n\n                        // Wrap reports and maintenances with links\n                        if (\n                          event.type === \"report\" ||\n                          event.type === \"maintenance\"\n                        ) {\n                          return (\n                            <Link\n                              key={`${event.id}-${event.type}`}\n                              href={`${prefix ? `/${prefix}` : \"\"}/events/${\n                                event.type\n                              }/${event.id}`}\n                            >\n                              {content}\n                            </Link>\n                          );\n                        }\n\n                        // Incidents don't have links\n                        return content;\n                      })}\n                    </div>\n                  </>\n                )}\n                {isPinned && !isTouch && (\n                  <>\n                    <Separator />\n                    <div className=\"flex cursor-pointer items-center p-2 text-muted-foreground text-xs\">\n                      <span>Click again to unpin</span>\n                      <Kbd>Esc</Kbd>\n                    </div>\n                  </>\n                )}\n              </div>\n            </HoverCardContent>\n          </HoverCard>\n        );\n      })}\n    </div>\n  );\n}\n\nexport function StatusTrackerSkeleton({\n  className,\n  ...props\n}: React.ComponentProps<typeof Skeleton>) {\n  return (\n    <Skeleton\n      className={cn(\"h-[50px] w-full rounded-none bg-muted\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction StatusTrackerContent({\n  status,\n  value,\n}: {\n  status: \"success\" | \"degraded\" | \"error\" | \"info\" | \"empty\";\n  value: string;\n}) {\n  return (\n    <div className=\"flex items-baseline gap-4\">\n      <div className=\"flex items-center gap-2\">\n        <div\n          className=\"h-2.5 w-2.5 rounded-sm\"\n          style={{\n            backgroundColor: chartConfig[status].color,\n          }}\n        />\n        <div className=\"text-sm\">{requests[status]}</div>\n      </div>\n      <div className=\"ml-auto font-mono text-muted-foreground text-xs tracking-tight\">\n        {value}\n      </div>\n    </div>\n  );\n}\n\nfunction StatusTrackerEvent({\n  name,\n  from,\n  to,\n  status,\n}: {\n  name: string;\n  from?: Date | null;\n  to?: Date | null;\n  status: \"success\" | \"degraded\" | \"error\" | \"info\" | \"empty\";\n}) {\n  if (!from) return null;\n\n  return (\n    <div className=\"group relative text-sm\">\n      {/* NOTE: this is to make the text truncate based on the with of the sibling element */}\n      {/* REMINDER: height needs to be equal the text height */}\n      <div className=\"h-4 w-full\" />\n      <div className=\"absolute inset-0 text-muted-foreground hover:text-foreground\">\n        <div className=\"flex items-center gap-2\">\n          <div\n            className=\"h-2.5 w-2.5 shrink-0 rounded-sm\"\n            style={{\n              backgroundColor: chartConfig[status].color,\n            }}\n          />\n          <div className=\"truncate\">{name}</div>\n        </div>\n      </div>\n      <div className=\"mt-1 text-muted-foreground text-xs\">\n        {formatDateRange(from, to ?? undefined)}{\" \"}\n        <span className=\"ml-1.5 font-mono text-muted-foreground/70\">\n          {formatDuration({ from, to, name, status })}\n        </span>\n      </div>\n    </div>\n  );\n}\n\nconst formatDuration = ({\n  from,\n  to,\n  name,\n}: React.ComponentProps<typeof StatusTrackerEvent>) => {\n  if (!from) return null;\n  if (!to) return \"ongoing\";\n  const duration = formatDistanceStrict(from, to);\n  const isMultipleIncidents = name.includes(\"Downtime (\");\n  if (isMultipleIncidents) return `across ${duration}`;\n  if (duration === \"0 seconds\") return null;\n  return duration;\n};\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status-updates.tsx",
    "content": "\"use client\";\n\nimport {\n  FormSubscribeEmail,\n  type FormValues,\n} from \"@/components/forms/form-subscribe-email\";\nimport { getBaseUrl } from \"@/lib/base-url\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Check, Copy, Inbox } from \"lucide-react\";\nimport { useState } from \"react\";\n\nexport type StatusUpdateType = \"email\" | \"rss\" | \"ssh\" | \"json\" | \"slack\";\n\ntype Page = NonNullable<RouterOutputs[\"statusPage\"][\"get\"]>;\n\nfunction getUpdateLink(type: \"rss\" | \"json\" | \"atom\", page?: Page | null) {\n  const baseUrl = getBaseUrl({\n    slug: page?.slug,\n    customDomain: page?.customDomain,\n  });\n\n  return `${baseUrl}/feed/${type}${\n    page?.accessType === \"password\" ? `?pw=${page?.password}` : \"\"\n  }`;\n}\n\n// TODO: use domain instead of openstatus subdomain if available\n\ninterface StatusUpdatesProps extends React.ComponentProps<typeof Button> {\n  types?: StatusUpdateType[];\n  page?: Page | null;\n  onSubscribe?: (values: FormValues) => Promise<void> | void;\n}\n\nexport function StatusUpdates({\n  className,\n  types = [\"rss\", \"ssh\", \"json\", \"slack\"],\n  page,\n  onSubscribe,\n  ...props\n}: StatusUpdatesProps) {\n  const [success, setSuccess] = useState(false);\n\n  if (types.length === 0) return null;\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button\n          size=\"sm\"\n          variant=\"outline\"\n          className={cn(className)}\n          {...props}\n        >\n          Get updates\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align=\"end\" className=\"w-80 overflow-hidden p-0\">\n        <Tabs defaultValue={types[0]}>\n          <TabsList className=\"w-full rounded-none border-b\">\n            {types.includes(\"email\") ? (\n              <TabsTrigger value=\"email\">Email</TabsTrigger>\n            ) : null}\n            {types.includes(\"slack\") ? (\n              <TabsTrigger value=\"slack\">Slack</TabsTrigger>\n            ) : null}\n            {types.includes(\"rss\") ? (\n              <TabsTrigger value=\"rss\">RSS</TabsTrigger>\n            ) : null}\n            {types.includes(\"json\") ? (\n              <TabsTrigger value=\"json\">JSON</TabsTrigger>\n            ) : null}\n            {types.includes(\"ssh\") ? (\n              <TabsTrigger value=\"ssh\">SSH</TabsTrigger>\n            ) : null}\n          </TabsList>\n          <TabsContent value=\"email\" className=\"flex flex-col gap-2\">\n            {success ? (\n              <SuccessMessage />\n            ) : (\n              <>\n                <div className=\"flex flex-col gap-2\">\n                  <p className=\"px-2 text-sm\">\n                    Get email notifications whenever a report has been created\n                    or resolved\n                  </p>\n                  <FormSubscribeEmail\n                    id=\"email-form\"\n                    page={page}\n                    onSubmit={async (values) => {\n                      await onSubscribe?.(values);\n                      setSuccess(true);\n                    }}\n                  />\n                </div>\n                <Separator />\n                <div className=\"px-2 pb-2\">\n                  <Button className=\"w-full\" type=\"submit\" form=\"email-form\">\n                    Subscribe\n                  </Button>\n                </div>{\" \"}\n              </>\n            )}\n          </TabsContent>\n          <TabsContent value=\"rss\" className=\"flex flex-col gap-2\">\n            <div className=\"flex flex-col gap-2 px-2\">\n              <p className=\"text-sm\">Get the RSS feed</p>\n              <CopyInputButton\n                className=\"w-full\"\n                id=\"rss\"\n                value={getUpdateLink(\"rss\", page)}\n              />\n            </div>\n            <Separator />\n            <div className=\"flex flex-col gap-2 px-2 pb-2\">\n              <p className=\"text-sm\">Get the Atom feed</p>\n              <CopyInputButton\n                className=\"w-full\"\n                id=\"atom\"\n                value={getUpdateLink(\"atom\", page)}\n              />\n            </div>\n          </TabsContent>\n          <TabsContent value=\"json\" className=\"flex flex-col gap-2\">\n            <div className=\"flex flex-col gap-2 px-2 pb-2\">\n              <p className=\"text-sm\">Get the JSON updates</p>\n              <CopyInputButton\n                className=\"w-full\"\n                id=\"json\"\n                value={getUpdateLink(\"json\", page)}\n              />\n            </div>\n          </TabsContent>\n          <TabsContent value=\"ssh\" className=\"flex flex-col gap-2\">\n            <div className=\"flex flex-col gap-2 px-2 pb-2\">\n              <p className=\"text-sm\">Get status via SSH</p>\n              <CopyInputButton\n                className=\"w-full\"\n                id=\"ssh\"\n                value={`ssh ${page?.slug}@ssh.openstatus.dev`}\n              />\n            </div>\n          </TabsContent>\n          <TabsContent value=\"slack\" className=\"flex flex-col gap-2\">\n            <div className=\"flex flex-col gap-2 px-2 pb-2\">\n              <p className=\"text-sm\">\n                For status updates in Slack, paste the text below into any\n                channel.\n              </p>\n              <CopyInputButton\n                className=\"w-full\"\n                id=\"slack\"\n                value={`/feed subscribe ${getUpdateLink(\"rss\", page)}`}\n              />\n            </div>\n          </TabsContent>\n        </Tabs>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nfunction CopyInputButton({\n  value,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof Input> & {\n  value: string;\n}) {\n  const { copy, isCopied } = useCopyToClipboard();\n  return (\n    <div className=\"relative w-full\">\n      <Input\n        placeholder={value}\n        readOnly\n        onClick={(e) => {\n          copy(value, {\n            successMessage: \"Link copied to clipboard\",\n            withToast: true,\n          });\n          onClick?.(e);\n        }}\n        {...props}\n      />\n      <Button\n        variant=\"outline\"\n        size=\"icon\"\n        onClick={() =>\n          copy(value, {\n            successMessage: \"Link copied to clipboard\",\n          })\n        }\n        className=\"-translate-y-1/2 absolute top-1/2 right-2 size-6\"\n      >\n        {isCopied ? <Check /> : <Copy />}\n        <span className=\"sr-only\">Copy Link</span>\n      </Button>\n    </div>\n  );\n}\n\nfunction SuccessMessage() {\n  return (\n    <div className=\"flex flex-col items-center justify-center gap-1 p-3\">\n      <Inbox className=\"size-4 shrink-0\" />\n      <p className=\"text-center font-medium\">Check your inbox!</p>\n      <p className=\"text-center text-muted-foreground text-sm\">\n        Validate your email to receive updates and you are all set.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/status.tsx",
    "content": "import { UTCDate } from \"@date-fns/utc\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { format } from \"date-fns\";\nimport {\n  AlertCircleIcon,\n  CheckIcon,\n  TriangleAlertIcon,\n  WrenchIcon,\n} from \"lucide-react\";\n\nexport function Status({\n  children,\n  className,\n  variant = \"success\",\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  variant?: \"success\" | \"degraded\" | \"error\" | \"info\";\n}) {\n  return (\n    <div\n      data-variant={variant}\n      data-slot=\"status\"\n      className={cn(\"group peer flex flex-col gap-8\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusBrand({\n  src,\n  alt,\n  className,\n  ...props\n}: React.ComponentProps<\"img\">) {\n  return (\n    // biome-ignore lint/a11y/useAltText: <explanation>\n    <img src={src} alt={alt} className={cn(\"size-8\", className)} {...props} />\n  );\n}\n\nexport function StatusHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-header\"\n      className={cn(\"@container/status-header\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"font-semibold text-foreground text-lg leading-none\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusDescription({\n  children,\n  className,\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"text-muted-foreground\", className)}>{children}</div>\n  );\n}\n\nexport function StatusContent({\n  children,\n  className,\n}: React.ComponentProps<\"div\">) {\n  return <div className={cn(\"flex flex-col gap-3\", className)}>{children}</div>;\n}\n\nexport function StatusIcon({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex size-7 items-center justify-center rounded-full bg-muted text-background [&>svg]:size-4\",\n        \"group-data-[variant=success]:bg-success\",\n        \"group-data-[variant=degraded]:bg-warning\",\n        \"group-data-[variant=error]:bg-destructive\",\n        \"group-data-[variant=info]:bg-info\",\n        className,\n      )}\n      {...props}\n    >\n      <CheckIcon className=\"hidden group-data-[variant=success]:block\" />\n      <TriangleAlertIcon className=\"hidden group-data-[variant=degraded]:block\" />\n      <AlertCircleIcon className=\"hidden group-data-[variant=error]:block\" />\n      <WrenchIcon className=\"hidden group-data-[variant=info]:block\" />\n    </div>\n  );\n}\n\nexport function StatusTimestamp({\n  date,\n  className,\n  ...props\n}: React.ComponentProps<typeof TooltipTrigger> & { date: Date }) {\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger\n          className={cn(\n            \"font-mono text-muted-foreground underline decoration-muted-foreground/30 decoration-dashed underline-offset-4\",\n            className,\n          )}\n          {...props}\n        >\n          {format(new UTCDate(date), \"LLL dd, y HH:mm (z)\")}\n        </TooltipTrigger>\n        <TooltipContent>\n          <p className=\"font-mono\">{format(date, \"LLL dd, y HH:mm (z)\")}</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n\nexport function StatusEmptyState({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col items-center justify-center gap-0.5 rounded-lg border border-dashed bg-muted/30 px-3 py-2 text-center sm:px-8 sm:py-6\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function StatusEmptyStateTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"font-medium\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function StatusEmptyStateDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"font-mono text-muted-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/status-page/utils.ts",
    "content": "import type { ChartConfig } from \"@openstatus/ui/components/ui/chart\";\nimport { VARIANT, type VariantType } from \"./floating-button\";\n\nexport const chartData = Array.from({ length: 45 }, (_, i) => {\n  const date = new Date();\n  date.setDate(date.getDate() - i);\n\n  // Simulate realistic daily status distribution that sums to 1440 minutes\n  let error = 0;\n  let degraded = 0;\n  let success = 1440; // Start with all minutes as ok\n  let info = 0;\n\n  // Simulate some incidents on certain days\n  if (i === 3) {\n    // Day 3: Major incident for 2 hours (120 minutes)\n    error = 120;\n    success -= error;\n  } else if (i === 16) {\n    // Day 16: Degraded performance for 4 hours (240 minutes)\n    degraded = 240;\n    success -= degraded;\n  } else if (i === 8) {\n    // Day 8: Brief outage (30 minutes) + some degraded performance (60 minutes)\n    error = 30;\n    degraded = 60;\n    success -= error + degraded;\n  } else if (i === 13) {\n    info = 120;\n    success -= info;\n  } else if (i === 22) {\n    // Day 22: Extended degraded performance (6 hours = 360 minutes)\n    degraded = 360;\n    success -= degraded;\n  } else if (Math.random() > 0.85) {\n    // Random minor issues on some days (5-15 minutes of degraded performance)\n    degraded = Math.floor(Math.random() * 10) + 5;\n    success -= degraded;\n  }\n\n  return {\n    timestamp: date.getTime(),\n    info,\n    degraded,\n    error,\n    success,\n  };\n}).reverse();\n\nexport type ChartData = (typeof chartData)[number];\n\nexport const chartConfig = {\n  success: {\n    label: \"success\",\n    color: \"var(--success)\",\n  },\n  degraded: {\n    label: \"degraded\",\n    color: \"var(--warning)\",\n  },\n  error: {\n    label: \"error\",\n    color: \"var(--destructive)\",\n  },\n  info: {\n    label: \"info\",\n    color: \"var(--info)\",\n  },\n  empty: {\n    label: \"empty\",\n    color: \"var(--muted)\",\n  },\n} satisfies ChartConfig;\n\nexport const PRIORITY = {\n  error: 3,\n  degraded: 2,\n  info: 1,\n  success: 0,\n} as const; // satisfies Record<XXX, number>;\n\nexport function getHighestPriorityStatus(item: ChartData) {\n  const total = item.success + item.degraded + item.info + item.error;\n  if (total === 0) return \"empty\";\n  return (\n    VARIANT.filter((status) => item[status] > 0).sort(\n      (a, b) => PRIORITY[b] - PRIORITY[a],\n    )[0] || \"empty\"\n  );\n}\n\nexport const PERCENTAGE_PRIORITY = {\n  info: -1,\n  error: 0,\n  degraded: 0.75,\n  success: 0.95,\n} as const;\n\nexport function getPercentagePriorityStatus(item: ChartData) {\n  const total = item.success + item.degraded + item.info + item.error;\n  if (total === 0) return \"empty\";\n\n  const percentage = item.success / total;\n  if (percentage >= PERCENTAGE_PRIORITY.success) return \"success\";\n  if (percentage >= PERCENTAGE_PRIORITY.degraded) return \"degraded\";\n  if (percentage >= PERCENTAGE_PRIORITY.error) return \"error\";\n  if (percentage >= PERCENTAGE_PRIORITY.info) return \"info\";\n  return \"info\";\n}\n\nexport function getHighestStatus(items: VariantType[]) {\n  if (items.some((item) => item === \"error\")) return \"error\";\n  if (items.some((item) => item === \"degraded\")) return \"degraded\";\n  if (items.some((item) => item === \"info\")) return \"info\";\n  return \"success\";\n}\n\nexport function getTotalUptime(item: ChartData[]) {\n  const { ok, total } = item.reduce(\n    (acc, item) => ({\n      ok: acc.ok + item.success + item.degraded + item.info,\n      total: acc.total + item.success + item.degraded + item.info + item.error,\n    }),\n    {\n      ok: 0,\n      total: 0,\n    },\n  );\n\n  if (total === 0) return 100;\n  return Math.round((ok / total) * 10000) / 100;\n}\n\nexport function getManualUptime(\n  items: { from: Date | null; to: Date | null }[],\n  days: number,\n) {\n  const duration = items.reduce((acc, item) => {\n    if (!item.from) return acc;\n    return acc + ((item.to || new Date()).getTime() - item.from.getTime());\n  }, 0);\n\n  const total = days * 24 * 60 * 60 * 1000;\n\n  return Math.round(((total - duration) / total) * 10000) / 100;\n}\n"
  },
  {
    "path": "apps/status-page/src/components/tailwind-indicator.tsx",
    "content": "export function TailwindIndicator() {\n  if (process.env.NODE_ENV === \"production\") return null;\n\n  return (\n    <div className=\"fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-foreground p-3 font-mono text-background text-xs\">\n      <div className=\"block sm:hidden\">xs</div>\n      <div className=\"hidden sm:block md:hidden\">sm</div>\n      <div className=\"hidden md:block lg:hidden\">md</div>\n      <div className=\"hidden lg:block xl:hidden\">lg</div>\n      <div className=\"hidden xl:block 2xl:hidden\">xl</div>\n      <div className=\"hidden 2xl:block\">2xl</div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/themes/theme-dropdown.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport type * as React from \"react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Laptop, Moon, Sun } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { useEffect } from \"react\";\n\nfunction getThemeIcon(theme?: string | null) {\n  if (theme === \"light\") return <Sun className=\"h-4 w-4\" />;\n  if (theme === \"dark\") return <Moon className=\"h-4 w-4\" />;\n  if (theme === \"system\") return <Laptop className=\"h-4 w-4\" />;\n  return null;\n}\n\nexport function ThemeDropdown({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuTrigger>) {\n  const { setTheme, theme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return <Skeleton className={cn(\"size-9\", className)} />;\n  }\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger className={cn(className)} asChild {...props}>\n        <Button variant=\"outline\" size=\"icon\">\n          {getThemeIcon(theme ?? \"system\")}\n          <span className=\"sr-only\">{theme ?? \"system\"}</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        {[\"light\", \"dark\", \"system\"].map((theme) => (\n          <DropdownMenuItem key={theme} onClick={() => setTheme(theme)}>\n            {getThemeIcon(theme)}\n            <span className=\"capitalize\">{theme}</span>\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/themes/theme-palette-picker.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Kbd, KbdGroup } from \"@openstatus/ui/components/ui/kbd\";\nimport { useSidebar } from \"@openstatus/ui/components/ui/sidebar\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { Palette } from \"lucide-react\";\n\nexport function ThemePalettePicker() {\n  const { toggleSidebar } = useSidebar();\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button size=\"icon\" variant=\"outline\" onClick={toggleSidebar}>\n          <Palette className=\"size-4\" />\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent className=\"flex items-center gap-2\">\n        Toggle Sidebar{\" \"}\n        <KbdGroup>\n          <Kbd>⌘</Kbd>\n          <span>+</span>\n          <Kbd>B</Kbd>\n        </KbdGroup>\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/themes/theme-provider.tsx",
    "content": "\"use client\";\n\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport type * as React from \"react\";\n\nexport function ThemeProvider({\n  children,\n  ...props\n}: React.ComponentProps<typeof NextThemesProvider>) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "apps/status-page/src/components/themes/theme-select.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport type * as React from \"react\";\n\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { Laptop, Moon, Sun } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { useEffect } from \"react\";\n\nexport function ThemeSelect({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectTrigger>) {\n  const { setTheme, theme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return (\n      <Skeleton\n        className={cn(\"h-9 rounded-md border border-border\", className)}\n      />\n    );\n  }\n\n  return (\n    <Select value={theme} onValueChange={setTheme}>\n      <SelectTrigger className={cn(\"w-full\", className)} {...props}>\n        <SelectValue\n          className=\"w-full\"\n          defaultValue={theme}\n          placeholder=\"Select theme\"\n        />\n      </SelectTrigger>\n      <SelectContent>\n        <SelectItem value=\"light\">\n          <div className=\"flex items-center gap-2\">\n            <Sun className=\"h-4 w-4\" />\n            <span>Light</span>\n          </div>\n        </SelectItem>\n        <SelectItem value=\"dark\">\n          <div className=\"flex items-center gap-2\">\n            <Moon className=\"h-4 w-4\" />\n            <span>Dark</span>\n          </div>\n        </SelectItem>\n        <SelectItem value=\"system\">\n          <div className=\"flex items-center gap-2\">\n            <Laptop className=\"h-4 w-4\" />\n            <span>System</span>\n          </div>\n        </SelectItem>\n      </SelectContent>\n    </Select>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/themes/theme-sidebar.tsx",
    "content": "\"use client\";\n\nimport { searchParamsParsers } from \"@/app/(public)/search-params\";\nimport { recomputeStyles } from \"@/components/status-page/floating-button\";\nimport {\n  THEMES,\n  type Theme,\n  type ThemeKey,\n  type ThemeVarName,\n} from \"@openstatus/theme-store\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  ButtonGroup,\n  ButtonGroupText,\n} from \"@openstatus/ui/components/ui/button-group\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@openstatus/ui/components/ui/collapsible\";\nimport {\n  InputGroup,\n  InputGroupInput,\n} from \"@openstatus/ui/components/ui/input-group\";\nimport { Kbd, KbdGroup } from \"@openstatus/ui/components/ui/kbd\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@openstatus/ui/components/ui/sidebar\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { Tabs, TabsList, TabsTrigger } from \"@openstatus/ui/components/ui/tabs\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { useDebounce } from \"@openstatus/ui/hooks/use-debounce\";\nimport { useDebounceCallback } from \"@openstatus/ui/hooks/use-debounce-callback\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport {\n  Check,\n  ChevronDown,\n  Copy,\n  PanelRightIcon,\n  RotateCcw,\n} from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { useQueryStates } from \"nuqs\";\nimport { useEffect, useState } from \"react\";\n\ntype ThemeBuilderColor = {\n  label: string;\n  type: \"color\";\n  values: { id: ThemeVarName; label: string }[];\n};\n\ntype ThemeBuilderCheckbox = {\n  label: string;\n  type: \"checkbox\";\n  values: { id: ThemeVarName; label: string }[];\n  options: { value: string; label: boolean }[];\n};\n\nconst THEME_BUILDER_INFO = {\n  id: {\n    label: \"ID\",\n    id: \"id\",\n    type: \"text\",\n  },\n  name: {\n    label: \"Name\",\n    id: \"name\",\n    type: \"text\",\n  },\n  author: {\n    label: \"Author\",\n    id: \"author.name\",\n    type: \"text\",\n  },\n  authorUrl: {\n    label: \"Link\",\n    id: \"author.url\",\n    type: \"text\",\n  },\n} as const;\n\nconst THEME_STYLE_BUILDER = {\n  base: {\n    label: \"Base Colors\",\n    type: \"color\",\n    values: [\n      { id: \"--foreground\", label: \"Foreground\" },\n      { id: \"--background\", label: \"Background\" },\n      // consider linking both border and input to the same color\n      { id: \"--border\", label: \"Border\" },\n      { id: \"--input\", label: \"Input\" },\n    ],\n  },\n  status: {\n    label: \"Status Colors\",\n    type: \"color\",\n    values: [\n      { id: \"--success\", label: \"Operational\" },\n      { id: \"--destructive\", label: \"Error\" },\n      { id: \"--warning\", label: \"Degraded\" },\n      { id: \"--info\", label: \"Maintenance\" },\n    ],\n  },\n  brand: {\n    label: \"Brand Colors\",\n    type: \"color\",\n    values: [\n      { id: \"--primary\", label: \"Primary\" },\n      { id: \"--primary-foreground\", label: \"Primary Foreground\" },\n      // consider linking both secondary, muted, accent to the same color\n      { id: \"--secondary\", label: \"Secondary\" },\n      { id: \"--muted\", label: \"Muted\" },\n      { id: \"--muted-foreground\", label: \"Muted Foreground\" },\n      { id: \"--accent\", label: \"Accent\" },\n      { id: \"--accent-foreground\", label: \"Accent Foreground\" },\n    ],\n  },\n  \"border-radius\": {\n    label: \"Border Radius\",\n    type: \"checkbox\",\n    values: [{ id: \"--radius\", label: \"Border Radius\" }],\n    options: [\n      { value: \"0rem\", label: false },\n      { value: \"0.625rem\", label: true },\n    ],\n  },\n} satisfies Record<string, ThemeBuilderColor | ThemeBuilderCheckbox>;\n\n// Helper function to get nested property value from an object\n// biome-ignore lint/suspicious/noExplicitAny: <explanation>\nfunction getNestedValue(obj: any, path: string): string | undefined {\n  const keys = path.split(\".\");\n  let value = obj;\n  for (const key of keys) {\n    if (value === undefined || value === null) return undefined;\n    value = value[key];\n  }\n  return value;\n}\n\nexport function ThemeSidebar(props: React.ComponentProps<typeof Sidebar>) {\n  const [{ t, b }, setSearchParams] = useQueryStates(searchParamsParsers);\n  const [newTheme, setNewTheme] = useState<Theme>(THEMES[t]);\n  const { resolvedTheme, setTheme } = useTheme();\n  const { copy, isCopied } = useCopyToClipboard();\n  const [isMounted, setIsMounted] = useState(false);\n  const debouncedNewTheme = useDebounce(newTheme, 100);\n  const { setOpen } = useSidebar();\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  useEffect(() => {\n    setNewTheme(THEMES[t]);\n  }, [t]);\n\n  useEffect(() => {\n    if (b) {\n      setOpen(true);\n      setSearchParams({ b: null });\n    }\n  }, [b, setOpen, setSearchParams]);\n\n  useEffect(() => {\n    if (!resolvedTheme || !isMounted) return;\n    recomputeStyles(debouncedNewTheme.id as ThemeKey, { ...debouncedNewTheme });\n  }, [resolvedTheme, isMounted, debouncedNewTheme]);\n\n  return (\n    <Sidebar side=\"right\" {...props}>\n      <SidebarHeader className=\"border-border border-b px-3 font-medium\">\n        <div className=\"flex items-center justify-between gap-2\">\n          <div>Theme Builder</div>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"size-7\"\n                onClick={() => setNewTheme(THEMES[t])}\n              >\n                <span className=\"sr-only\">Reset</span>\n                <RotateCcw />\n              </Button>\n            </TooltipTrigger>\n            <TooltipContent side=\"left\">\n              <p>Reset theme</p>\n            </TooltipContent>\n          </Tooltip>\n        </div>\n      </SidebarHeader>\n      <SidebarContent>\n        <Collapsible key=\"info\" defaultOpen className=\"group/collapsible\">\n          <SidebarGroup className=\"pt-2 pb-0\">\n            <SidebarGroupLabel asChild>\n              <CollapsibleTrigger>\n                Information\n                <ChevronDown className=\"ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180\" />\n              </CollapsibleTrigger>\n            </SidebarGroupLabel>\n            <CollapsibleContent>\n              <SidebarGroupContent>\n                <SidebarMenu>\n                  {Object.entries(THEME_BUILDER_INFO).map(([key, config]) => (\n                    <SidebarMenuItem key={key}>\n                      <SidebarMenuButton asChild>\n                        <div>\n                          <ButtonGroup className=\"w-full\">\n                            <ButtonGroupText className=\"w-24\">\n                              <Label htmlFor={config.id}>{config.label}</Label>\n                            </ButtonGroupText>\n                            <InputGroup className=\"h-7\">\n                              <InputGroupInput\n                                id={config.id}\n                                defaultValue={\n                                  newTheme\n                                    ? getNestedValue(newTheme, config.id)\n                                    : \"\"\n                                }\n                              />\n                            </InputGroup>\n                          </ButtonGroup>\n                        </div>\n                      </SidebarMenuButton>\n                    </SidebarMenuItem>\n                  ))}\n                </SidebarMenu>\n              </SidebarGroupContent>\n            </CollapsibleContent>\n          </SidebarGroup>\n        </Collapsible>\n        <SidebarGroup className=\"py-0\">\n          <SidebarGroupLabel>Theme Mode</SidebarGroupLabel>\n          <SidebarGroupContent>\n            <SidebarMenu>\n              <SidebarMenuItem>\n                <SidebarMenuButton asChild>\n                  <div>\n                    {!isMounted ? (\n                      <Skeleton className=\"h-8 w-full\" />\n                    ) : (\n                      <Tabs\n                        value={resolvedTheme}\n                        onValueChange={(value) =>\n                          setTheme(value as \"light\" | \"dark\")\n                        }\n                        className=\"w-full\"\n                      >\n                        <TabsList className=\"h-8 w-full\">\n                          <TabsTrigger value=\"light\">Light</TabsTrigger>\n                          <TabsTrigger value=\"dark\">Dark</TabsTrigger>\n                        </TabsList>\n                      </Tabs>\n                    )}\n                  </div>\n                </SidebarMenuButton>\n              </SidebarMenuItem>\n            </SidebarMenu>\n          </SidebarGroupContent>\n        </SidebarGroup>\n        {Object.entries(THEME_STYLE_BUILDER).map(([key, config], index) => (\n          <Collapsible key={key} defaultOpen className=\"group/collapsible\">\n            <SidebarGroup\n              className={cn(\n                index !== Object.entries(THEME_STYLE_BUILDER).length - 1\n                  ? \"py-0\"\n                  : \"pt-0\",\n              )}\n            >\n              <SidebarGroupLabel asChild>\n                <CollapsibleTrigger>\n                  {config.label}\n                  <ChevronDown className=\"ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180\" />\n                </CollapsibleTrigger>\n              </SidebarGroupLabel>\n              <CollapsibleContent>\n                <SidebarGroupContent>\n                  <SidebarMenu>\n                    {config.values.map((value) => {\n                      return (\n                        <SidebarMenuItem key={value.id}>\n                          <SidebarMenuButton asChild>\n                            <div>\n                              <span className=\"truncate\">{value.label}</span>\n                              <ThemeValueSelector\n                                config={config}\n                                id={value.id}\n                                theme={newTheme}\n                                setTheme={setNewTheme}\n                                isMounted={isMounted}\n                              />\n                            </div>\n                          </SidebarMenuButton>\n                        </SidebarMenuItem>\n                      );\n                    })}\n                  </SidebarMenu>\n                </SidebarGroupContent>\n              </CollapsibleContent>\n            </SidebarGroup>\n          </Collapsible>\n        ))}\n      </SidebarContent>\n      <SidebarFooter className=\"border-border border-t\">\n        <Button\n          size=\"sm\"\n          onClick={() => copy(JSON.stringify(newTheme), { withToast: false })}\n        >\n          {isCopied ? \"Configuration Copied!\" : \"Copy Configuration\"}\n          {isCopied ? (\n            <Check className=\"size-4\" />\n          ) : (\n            <Copy className=\"size-4\" />\n          )}\n        </Button>\n      </SidebarFooter>\n    </Sidebar>\n  );\n}\n\nfunction ThemeValueSelector(props: {\n  config: ThemeBuilderColor | ThemeBuilderCheckbox;\n  id: ThemeVarName;\n  theme: Theme;\n  setTheme: (theme: Theme) => void;\n  isMounted: boolean;\n}) {\n  const { resolvedTheme } = useTheme();\n\n  const handleChange = useDebounceCallback((value: string) => {\n    const mode = resolvedTheme as \"light\" | \"dark\";\n    props.setTheme({\n      ...props.theme,\n      [mode]: {\n        ...props.theme[mode],\n        [props.id]: value,\n      },\n    });\n  }, 100);\n\n  if (!props.isMounted || !resolvedTheme)\n    return <Skeleton className=\"ml-auto size-4 border border-foreground/70\" />;\n\n  const value = props.theme[resolvedTheme as \"light\" | \"dark\"][props.id];\n\n  if (props.config.type === \"color\") {\n    return (\n      <label\n        className=\"ml-auto size-4 rounded-full border border-foreground/70\"\n        style={{ backgroundColor: value }}\n        htmlFor={props.id}\n      >\n        <input\n          type=\"color\"\n          id={props.id}\n          name={props.id}\n          value={value}\n          className=\"sr-only\"\n          onChange={(e) => handleChange(e.target.value)}\n        />\n      </label>\n    );\n  }\n\n  if (props.config.type === \"checkbox\") {\n    const { options } = props.config;\n    const checked =\n      options.find((option) => option.value === value)?.label ?? false;\n    return (\n      <label htmlFor={props.id} className=\"ml-auto flex items-center gap-2\">\n        <span className=\"font-mono text-muted-foreground text-xs\">\n          {value ??\n            options.find((option) => option.label === false)?.value ??\n            \"\"}\n        </span>\n        <Checkbox\n          id={props.id}\n          name={props.id}\n          checked={checked}\n          className=\"size-4 bg-background\"\n          onCheckedChange={(checked) =>\n            props.setTheme({\n              ...props.theme,\n              [resolvedTheme as \"light\" | \"dark\"]: {\n                ...props.theme[resolvedTheme as \"light\" | \"dark\"],\n                [props.id]: checked\n                  ? options.find((option) => option.label === true)?.value ?? \"\"\n                  : options.find((option) => option.label === false)?.value ??\n                    \"\",\n              },\n            })\n          }\n        />\n      </label>\n    );\n  }\n\n  return (\n    <span className=\"ml-auto font-mono text-muted-foreground text-xs\">\n      {value}\n    </span>\n  );\n}\n\nexport function SidebarTrigger({\n  className,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { toggleSidebar } = useSidebar();\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button\n          data-sidebar=\"trigger\"\n          data-slot=\"sidebar-trigger\"\n          variant=\"ghost\"\n          size=\"icon\"\n          className={cn(\"size-7\", className)}\n          onClick={(event) => {\n            onClick?.(event);\n            toggleSidebar();\n          }}\n          {...props}\n        >\n          <PanelRightIcon />\n          <span className=\"sr-only\">Toggle Sidebar</span>\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent side=\"left\" className=\"flex items-center gap-2\">\n        Toggle Sidebar{\" \"}\n        <KbdGroup>\n          <Kbd>⌘</Kbd>\n          <span>+</span>\n          <Kbd>B</Kbd>\n        </KbdGroup>\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/ui/data-table/data-table-action-bar.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport type { Table } from \"@tanstack/react-table\";\nimport { Loader, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\nexport interface DataTableActionBarProps<TData>\n  extends React.ComponentProps<\"div\"> {\n  table: Table<TData>;\n  visible?: boolean;\n  container?: Element | DocumentFragment | null;\n}\n\nfunction DataTableActionBar<TData>({\n  table,\n  visible: visibleProp,\n  container: containerProp,\n  children,\n  className,\n  ...props\n}: DataTableActionBarProps<TData>) {\n  const [mounted, setMounted] = React.useState(false);\n\n  React.useLayoutEffect(() => {\n    setMounted(true);\n  }, []);\n\n  React.useEffect(() => {\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key === \"Escape\") {\n        table.toggleAllRowsSelected(false);\n      }\n    }\n\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => window.removeEventListener(\"keydown\", onKeyDown);\n  }, [table]);\n\n  const container =\n    containerProp ?? (mounted ? globalThis.document?.body : null);\n\n  if (!container) return null;\n\n  const visible =\n    visibleProp ?? table.getFilteredSelectedRowModel().rows.length > 0;\n\n  return ReactDOM.createPortal(\n    <div>\n      {visible && (\n        <div\n          role=\"toolbar\"\n          aria-orientation=\"horizontal\"\n          className={cn(\n            \"fixed inset-x-0 bottom-6 z-50 mx-auto flex w-fit flex-wrap items-center justify-center gap-2 rounded-md border bg-background p-2 text-foreground shadow-sm\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </div>\n      )}\n    </div>,\n    container,\n  );\n}\n\ninterface DataTableActionBarActionProps\n  extends React.ComponentProps<typeof Button> {\n  tooltip?: string;\n  isPending?: boolean;\n}\n\nfunction DataTableActionBarAction({\n  size = \"sm\",\n  tooltip,\n  isPending,\n  disabled,\n  className,\n  children,\n  ...props\n}: DataTableActionBarActionProps) {\n  const trigger = (\n    <Button\n      variant=\"secondary\"\n      size={size}\n      className={cn(\n        \"gap-1.5 border border-secondary bg-secondary/50 hover:bg-secondary/70 [&>svg]:size-3.5\",\n        size === \"icon\" ? \"size-7\" : \"h-7\",\n        className,\n      )}\n      disabled={disabled || isPending}\n      {...props}\n    >\n      {isPending ? <Loader className=\"animate-spin\" /> : children}\n    </Button>\n  );\n\n  if (!tooltip) return trigger;\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{trigger}</TooltipTrigger>\n      <TooltipContent\n        sideOffset={6}\n        className=\"border bg-accent font-semibold text-foreground dark:bg-zinc-900 [&>span]:hidden\"\n      >\n        <p>{tooltip}</p>\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n\ninterface DataTableActionBarSelectionProps<TData> {\n  table: Table<TData>;\n}\n\nfunction DataTableActionBarSelection<TData>({\n  table,\n}: DataTableActionBarSelectionProps<TData>) {\n  const onClearSelection = React.useCallback(() => {\n    table.toggleAllRowsSelected(false);\n  }, [table]);\n\n  return (\n    <div className=\"flex h-7 items-center rounded-md border pr-1 pl-2.5\">\n      <span className=\"whitespace-nowrap text-xs\">\n        {table.getFilteredSelectedRowModel().rows.length} selected\n      </span>\n      <Separator\n        orientation=\"vertical\"\n        className=\"mr-1 ml-2 data-[orientation=vertical]:h-4\"\n      />\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            className=\"size-5\"\n            onClick={onClearSelection}\n          >\n            <X className=\"size-3.5\" />\n          </Button>\n        </TooltipTrigger>\n        <TooltipContent\n          sideOffset={10}\n          className=\"flex items-center gap-2 border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-zinc-900 [&>span]:hidden\"\n        >\n          <p>Clear selection</p>\n          <kbd className=\"select-none rounded border bg-background px-1.5 py-px font-mono font-normal text-[0.7rem] text-foreground shadow-xs\">\n            <abbr title=\"Escape\" className=\"no-underline\">\n              Esc\n            </abbr>\n          </kbd>\n        </TooltipContent>\n      </Tooltip>\n    </div>\n  );\n}\n\nexport {\n  DataTableActionBar,\n  DataTableActionBarAction,\n  DataTableActionBarSelection,\n};\n"
  },
  {
    "path": "apps/status-page/src/components/ui/data-table/data-table-column-header.tsx",
    "content": "import type { Column } from \"@tanstack/react-table\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\ninterface DataTableColumnHeaderProps<TData, TValue>\n  extends React.ComponentProps<\"button\"> {\n  column: Column<TData, TValue>;\n  title: string;\n}\n\nexport function DataTableColumnHeader<TData, TValue>({\n  column,\n  title,\n  className,\n  ...props\n}: DataTableColumnHeaderProps<TData, TValue>) {\n  if (!column.getCanSort()) {\n    return <div className={cn(className)}>{title}</div>;\n  }\n\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"sm\"\n      onClick={() => {\n        column.toggleSorting(undefined);\n      }}\n      className={cn(\n        \"flex h-7 w-full items-center justify-between gap-2 px-0 py-0 hover:bg-transparent dark:hover:bg-transparent\",\n        className,\n      )}\n      {...props}\n    >\n      <span>{title}</span>\n      <span className=\"flex flex-col\">\n        <ChevronUp\n          className={cn(\n            \"-mb-0.5 size-3\",\n            column.getIsSorted() === \"asc\"\n              ? \"text-accent-foreground\"\n              : \"text-muted-foreground\",\n          )}\n        />\n        <ChevronDown\n          className={cn(\n            \"-mt-0.5 size-3\",\n            column.getIsSorted() === \"desc\"\n              ? \"text-accent-foreground\"\n              : \"text-muted-foreground\",\n          )}\n        />\n      </span>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/ui/data-table/data-table-faceted-filter.tsx",
    "content": "import type { Column } from \"@tanstack/react-table\";\nimport { Check, PlusCircle } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from \"@openstatus/ui/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@openstatus/ui/components/ui/popover\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\ninterface DataTableFacetedFilterProps<TData, TValue> {\n  column?: Column<TData, TValue>;\n  title?: string;\n  options: {\n    label: string;\n    value: string | number;\n    icon?: React.ComponentType<{ className?: string }>;\n  }[];\n}\n\nexport function DataTableFacetedFilter<TData, TValue>({\n  column,\n  title,\n  options,\n}: DataTableFacetedFilterProps<TData, TValue>) {\n  const facets = column?.getFacetedUniqueValues();\n  const selectedValues = new Set(\n    column?.getFilterValue() as (string | number)[],\n  );\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\" className=\"h-8 border-dashed\">\n          <PlusCircle />\n          {title}\n          {selectedValues?.size > 0 && (\n            <>\n              <Separator orientation=\"vertical\" className=\"mx-2 h-4\" />\n              <Badge\n                variant=\"secondary\"\n                className=\"rounded-sm px-1 font-normal lg:hidden\"\n              >\n                {selectedValues.size}\n              </Badge>\n              <div className=\"hidden space-x-1 lg:flex\">\n                {selectedValues.size > 2 ? (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"rounded-sm px-1 font-normal\"\n                  >\n                    {selectedValues.size} selected\n                  </Badge>\n                ) : (\n                  options\n                    .filter((option) => selectedValues.has(option.value))\n                    .map((option) => (\n                      <Badge\n                        variant=\"secondary\"\n                        key={option.value}\n                        className=\"rounded-sm px-1 font-normal\"\n                      >\n                        {option.label}\n                      </Badge>\n                    ))\n                )}\n              </div>\n            </>\n          )}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[200px] p-0\" align=\"start\">\n        <Command>\n          <CommandInput placeholder={title} />\n          <CommandList>\n            <CommandEmpty>No results found.</CommandEmpty>\n            <CommandGroup>\n              {options.map((option) => {\n                const isSelected = selectedValues.has(option.value);\n                return (\n                  <CommandItem\n                    key={option.value}\n                    onSelect={() => {\n                      if (isSelected) {\n                        selectedValues.delete(option.value);\n                      } else {\n                        selectedValues.add(option.value);\n                      }\n                      const filterValues = Array.from(selectedValues);\n                      column?.setFilterValue(\n                        filterValues.length ? filterValues : undefined,\n                      );\n                    }}\n                  >\n                    <div\n                      className={cn(\n                        \"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary\",\n                        isSelected\n                          ? \"bg-primary text-primary-foreground\"\n                          : \"opacity-50 [&_svg]:invisible\",\n                      )}\n                    >\n                      <Check />\n                    </div>\n                    {option.icon && (\n                      <option.icon className=\"mr-2 h-4 w-4 text-muted-foreground\" />\n                    )}\n                    <span>{option.label}</span>\n                    {facets?.get(option.value) && (\n                      <span className=\"ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs\">\n                        {facets.get(option.value)}\n                      </span>\n                    )}\n                  </CommandItem>\n                );\n              })}\n            </CommandGroup>\n            {selectedValues.size > 0 && (\n              <>\n                <CommandSeparator />\n                <CommandGroup>\n                  <CommandItem\n                    onSelect={() => column?.setFilterValue(undefined)}\n                    className=\"justify-center text-center\"\n                  >\n                    Clear filters\n                  </CommandItem>\n                </CommandGroup>\n              </>\n            )}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/ui/data-table/data-table-pagination.tsx",
    "content": "import type { Table } from \"@tanstack/react-table\";\nimport {\n  ChevronLeft,\n  ChevronRight,\n  ChevronsLeft,\n  ChevronsRight,\n} from \"lucide-react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\n\nexport interface DataTablePaginationProps<TData> {\n  table: Table<TData>;\n}\n\nexport function DataTablePagination<TData>({\n  table,\n}: DataTablePaginationProps<TData>) {\n  return (\n    <div className=\"flex flex-wrap items-center justify-between gap-2\">\n      <div className=\"flex-1 text-muted-foreground text-sm\">\n        {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n        {table.getFilteredRowModel().rows.length} row(s) selected.\n      </div>\n      <div className=\"flex items-center space-x-6 lg:space-x-8\">\n        <div className=\"flex items-center space-x-2\">\n          <p className=\"font-medium text-sm\">Rows per page</p>\n          <Select\n            value={`${table.getState().pagination.pageSize}`}\n            onValueChange={(value) => {\n              table.setPageSize(Number(value));\n            }}\n          >\n            <SelectTrigger className=\"h-8 w-[70px]\">\n              <SelectValue placeholder={table.getState().pagination.pageSize} />\n            </SelectTrigger>\n            <SelectContent side=\"top\">\n              {[10, 20, 30, 40, 50].map((pageSize) => (\n                <SelectItem key={pageSize} value={`${pageSize}`}>\n                  {pageSize}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n        <div className=\"flex items-center justify-center font-medium text-sm\">\n          Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n          {table.getPageCount()}\n        </div>\n        <div className=\"flex items-center space-x-2\">\n          <Button\n            variant=\"outline\"\n            className=\"hidden h-8 w-8 p-0 lg:flex\"\n            onClick={() => table.setPageIndex(0)}\n            disabled={!table.getCanPreviousPage()}\n          >\n            <span className=\"sr-only\">Go to first page</span>\n            <ChevronsLeft />\n          </Button>\n          <Button\n            variant=\"outline\"\n            className=\"h-8 w-8 p-0\"\n            onClick={() => table.previousPage()}\n            disabled={!table.getCanPreviousPage()}\n          >\n            <span className=\"sr-only\">Go to previous page</span>\n            <ChevronLeft />\n          </Button>\n          <Button\n            variant=\"outline\"\n            className=\"h-8 w-8 p-0\"\n            onClick={() => table.nextPage()}\n            disabled={!table.getCanNextPage()}\n          >\n            <span className=\"sr-only\">Go to next page</span>\n            <ChevronRight />\n          </Button>\n          <Button\n            variant=\"outline\"\n            className=\"hidden h-8 w-8 p-0 lg:flex\"\n            onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n            disabled={!table.getCanNextPage()}\n          >\n            <span className=\"sr-only\">Go to last page</span>\n            <ChevronsRight />\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function DataTablePaginationSimple<TData>({\n  table,\n}: DataTablePaginationProps<TData>) {\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex-1 text-muted-foreground text-sm\">\n        {table.getFilteredRowModel().rows.length} of{\" \"}\n        {table.getPreFilteredRowModel().rows.length} row(s) filtered.\n      </div>\n      <div className=\"flex items-center space-x-2\">\n        <Button\n          variant=\"outline\"\n          className=\"hidden h-8 w-8 p-0 lg:flex\"\n          onClick={() => table.setPageIndex(0)}\n          disabled={!table.getCanPreviousPage()}\n        >\n          <span className=\"sr-only\">Go to first page</span>\n          <ChevronsLeft />\n        </Button>\n        <Button\n          variant=\"outline\"\n          className=\"h-8 w-8 p-0\"\n          onClick={() => table.previousPage()}\n          disabled={!table.getCanPreviousPage()}\n        >\n          <span className=\"sr-only\">Go to previous page</span>\n          <ChevronLeft />\n        </Button>\n        <Button\n          variant=\"outline\"\n          className=\"h-8 w-8 p-0\"\n          onClick={() => table.nextPage()}\n          disabled={!table.getCanNextPage()}\n        >\n          <span className=\"sr-only\">Go to next page</span>\n          <ChevronRight />\n        </Button>\n        <Button\n          variant=\"outline\"\n          className=\"hidden h-8 w-8 p-0 lg:flex\"\n          onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n          disabled={!table.getCanNextPage()}\n        >\n          <span className=\"sr-only\">Go to last page</span>\n          <ChevronsRight />\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/ui/data-table/data-table-skeleton.tsx",
    "content": "import { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\n\ninterface DataTableSkeletonProps {\n  /**\n   * Number of rows to render\n   * @default 10\n   */\n  rows?: number;\n}\n\n// TODO: add checkbox skeleton (for MonitorTable e.g.)\n\nexport function DataTableSkeleton({ rows = 3 }: DataTableSkeletonProps) {\n  return (\n    <Table>\n      <TableHeader className=\"bg-muted/50\">\n        <TableRow className=\"hover:bg-transparent\">\n          <TableHead>\n            <Skeleton className=\"my-1.5 h-4 w-24\" />\n          </TableHead>\n          <TableHead className=\"hidden sm:table-cell\">\n            <Skeleton className=\"my-1.5 h-4 w-32\" />\n          </TableHead>\n          <TableHead className=\"hidden md:table-cell\">\n            <Skeleton className=\"my-1.5 h-4 w-16\" />\n          </TableHead>\n          <TableHead>\n            <Skeleton className=\"my-1.5 h-4 w-20\" />\n          </TableHead>\n          <TableHead className=\"flex items-center justify-end\" />\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        {new Array(rows).fill(0).map((_, i) => (\n          <TableRow key={i} className=\"hover:bg-transparent\">\n            <TableCell>\n              <Skeleton className=\"my-1.5 h-4 w-full max-w-40\" />\n            </TableCell>\n            <TableCell className=\"hidden sm:table-cell\">\n              <Skeleton className=\"my-1.5 h-4 w-full max-w-52\" />\n            </TableCell>\n            <TableCell className=\"hidden md:table-cell\">\n              <Skeleton className=\"my-1.5 h-4 w-24\" />\n            </TableCell>\n            <TableCell>\n              <Skeleton className=\"my-1.5 h-4 w-full max-w-40\" />\n            </TableCell>\n            <TableCell className=\"flex justify-end\">\n              <Skeleton className=\"my-1.5 h-5 w-5\" />\n            </TableCell>\n          </TableRow>\n        ))}\n      </TableBody>\n    </Table>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/ui/data-table/data-table-toobar.tsx",
    "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\n\nimport { DataTableViewOptions } from \"@/components/ui/data-table/data-table-view-options\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\n\nimport { DataTableFacetedFilter } from \"@/components/ui/data-table/data-table-faceted-filter\";\n\nexport interface DataTableToolbarProps<TData> {\n  table: Table<TData>;\n}\n\nexport function DataTableToolbar<TData>({\n  table,\n}: DataTableToolbarProps<TData>) {\n  const isFiltered = table.getState().columnFilters.length > 0;\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex flex-1 items-center space-x-2\">\n        <Input\n          placeholder=\"Filter entries...\"\n          value={(table.getColumn(\"title\")?.getFilterValue() as string) ?? \"\"}\n          onChange={(event) =>\n            table.getColumn(\"title\")?.setFilterValue(event.target.value)\n          }\n          className=\"h-8 w-[150px] lg:w-[250px]\"\n        />\n        {table.getColumn(\"status\") && (\n          <DataTableFacetedFilter\n            column={table.getColumn(\"status\")}\n            title=\"Status\"\n            options={[]}\n          />\n        )}\n        {table.getColumn(\"tags\") && (\n          <DataTableFacetedFilter\n            column={table.getColumn(\"tags\")}\n            title=\"Tags\"\n            options={[]}\n          />\n        )}\n        {isFiltered && (\n          <Button\n            variant=\"ghost\"\n            onClick={() => table.resetColumnFilters()}\n            className=\"h-8 px-2 lg:px-3\"\n          >\n            Reset\n            <X />\n          </Button>\n        )}\n      </div>\n      <DataTableViewOptions table={table} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/ui/data-table/data-table-view-options.tsx",
    "content": "\"use client\";\n\nimport { DropdownMenuTrigger } from \"@radix-ui/react-dropdown-menu\";\nimport type { Table } from \"@tanstack/react-table\";\nimport { Settings2 } from \"lucide-react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\n\ninterface DataTableViewOptionsProps<TData> {\n  table: Table<TData>;\n}\n\nexport function DataTableViewOptions<TData>({\n  table,\n}: DataTableViewOptionsProps<TData>) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          className=\"ml-auto hidden h-8 lg:flex\"\n        >\n          <Settings2 />\n          View\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"w-[150px]\">\n        <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        {table\n          .getAllColumns()\n          .filter(\n            (column) =>\n              typeof column.accessorFn !== \"undefined\" && column.getCanHide(),\n          )\n          .map((column) => {\n            return (\n              <DropdownMenuCheckboxItem\n                key={column.id}\n                className=\"capitalize\"\n                checked={column.getIsVisible()}\n                onCheckedChange={(value) => column.toggleVisibility(!!value)}\n              >\n                {column.id}\n              </DropdownMenuCheckboxItem>\n            );\n          })}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/components/ui/data-table/data-table.tsx",
    "content": "\"use client\";\n\nimport {\n  type ColumnDef,\n  type ColumnFiltersState,\n  type PaginationState,\n  type Row,\n  type SortingState,\n  type VisibilityState,\n  flexRender,\n  getCoreRowModel,\n  getExpandedRowModel,\n  getFacetedRowModel,\n  getFacetedUniqueValues,\n  getFilteredRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  useReactTable,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\n\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@openstatus/ui/components/ui/table\";\nimport { Fragment } from \"react\";\nimport type { DataTableActionBarProps } from \"./data-table-action-bar\";\nimport type { DataTablePaginationProps } from \"./data-table-pagination\";\nimport type { DataTableToolbarProps } from \"./data-table-toobar\";\n\nexport interface DataTableProps<TData, TValue> {\n  columns: ColumnDef<TData, TValue>[];\n  data: TData[];\n  rowComponent?: React.ComponentType<{ row: Row<TData> }>;\n  toolbarComponent?: React.ComponentType<DataTableToolbarProps<TData>>;\n  actionBar?: React.ComponentType<DataTableActionBarProps<TData>>;\n  paginationComponent?: React.ComponentType<DataTablePaginationProps<TData>>;\n  onRowClick?: (row: Row<TData>) => void;\n  defaultSorting?: SortingState;\n  defaultColumnVisibility?: VisibilityState;\n  defaultColumnFilters?: ColumnFiltersState;\n  defaultPagination?: PaginationState;\n  autoResetPageIndex?: boolean;\n\n  /** access the state from the parent component */\n  columnFilters?: ColumnFiltersState;\n  setColumnFilters?: React.Dispatch<React.SetStateAction<ColumnFiltersState>>;\n  sorting?: SortingState;\n  setSorting?: React.Dispatch<React.SetStateAction<SortingState>>;\n  pagination?: PaginationState;\n  setPagination?: React.Dispatch<React.SetStateAction<PaginationState>>;\n}\n\nexport function DataTable<TData, TValue>({\n  columns,\n  data,\n  rowComponent,\n  toolbarComponent,\n  actionBar,\n  paginationComponent,\n  onRowClick,\n  defaultSorting = [],\n  defaultColumnVisibility = {},\n  defaultColumnFilters = [],\n  defaultPagination = { pageIndex: 0, pageSize: 10 },\n  autoResetPageIndex = true,\n  columnFilters,\n  setColumnFilters,\n  sorting,\n  setSorting,\n  pagination,\n  setPagination,\n}: DataTableProps<TData, TValue>) {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  const [globalFilter, setGlobalFilter] = React.useState<any>();\n  const [rowSelection, setRowSelection] = React.useState({});\n  const [columnVisibility, setColumnVisibility] =\n    React.useState<VisibilityState>(defaultColumnVisibility);\n  const [internalPagination, setInternalPagination] =\n    React.useState<PaginationState>(defaultPagination);\n  const [internalColumnFilters, setInternalColumnFilters] =\n    React.useState<ColumnFiltersState>(defaultColumnFilters);\n  const [internalSorting, setInternalSorting] =\n    React.useState<SortingState>(defaultSorting);\n\n  // Use controlled or uncontrolled column filters\n  const columnFiltersState = columnFilters ?? internalColumnFilters;\n  const setColumnFiltersState = setColumnFilters ?? setInternalColumnFilters;\n  const sortingState = sorting ?? internalSorting;\n  const setSortingState = setSorting ?? setInternalSorting;\n  const paginationState = pagination ?? internalPagination;\n  const setPaginationState = setPagination ?? setInternalPagination;\n\n  const table = useReactTable({\n    data,\n    columns,\n    state: {\n      sorting: sortingState,\n      columnVisibility,\n      rowSelection,\n      pagination: paginationState,\n      columnFilters: columnFiltersState,\n      globalFilter,\n    },\n    enableRowSelection: true,\n    onRowSelectionChange: setRowSelection,\n    onSortingChange: setSortingState,\n    onColumnFiltersChange: setColumnFiltersState,\n    onColumnVisibilityChange: setColumnVisibility,\n    onPaginationChange: setPaginationState,\n    onGlobalFilterChange: setGlobalFilter,\n    getCoreRowModel: getCoreRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getFacetedRowModel: getFacetedRowModel(),\n    getFacetedUniqueValues: getFacetedUniqueValues(),\n    getExpandedRowModel: getExpandedRowModel(),\n    autoResetPageIndex,\n    // @ts-expect-error as we have an id in the data\n    getRowCanExpand: (row) => Boolean(row.original.id),\n  });\n\n  return (\n    <div className=\"grid gap-2\">\n      {toolbarComponent\n        ? React.createElement(toolbarComponent, { table })\n        : null}\n      <Table>\n        <TableHeader>\n          {table.getHeaderGroups().map((headerGroup) => (\n            <TableRow key={headerGroup.id}>\n              {headerGroup.headers.map((header) => {\n                return (\n                  <TableHead\n                    key={header.id}\n                    colSpan={header.colSpan}\n                    className={header.column.columnDef.meta?.headerClassName}\n                  >\n                    {header.isPlaceholder\n                      ? null\n                      : flexRender(\n                          header.column.columnDef.header,\n                          header.getContext(),\n                        )}\n                  </TableHead>\n                );\n              })}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {table.getRowModel().rows?.length ? (\n            table.getRowModel().rows.map((row) => (\n              <Fragment key={row.id}>\n                <TableRow\n                  data-state={\n                    (row.getIsSelected() || row.getIsExpanded()) && \"selected\"\n                  }\n                  onClick={() => onRowClick?.(row)}\n                  className=\"data-[state=selected]:bg-muted/50\"\n                >\n                  {row.getVisibleCells().map((cell) => (\n                    <TableCell\n                      key={cell.id}\n                      className={cell.column.columnDef.meta?.cellClassName}\n                    >\n                      {flexRender(\n                        cell.column.columnDef.cell,\n                        cell.getContext(),\n                      )}\n                    </TableCell>\n                  ))}\n                </TableRow>\n                {row.getIsExpanded() && (\n                  <TableRow className=\"hover:bg-background\">\n                    <TableCell\n                      className=\"p-0\"\n                      colSpan={row.getVisibleCells().length}\n                    >\n                      {rowComponent\n                        ? React.createElement(rowComponent, { row })\n                        : null}\n                    </TableCell>\n                  </TableRow>\n                )}\n              </Fragment>\n            ))\n          ) : (\n            <TableRow>\n              <TableCell colSpan={columns.length} className=\"h-24 text-center\">\n                No results.\n              </TableCell>\n            </TableRow>\n          )}\n        </TableBody>\n        {actionBar ? React.createElement(actionBar, { table }) : null}\n      </Table>\n      {paginationComponent\n        ? React.createElement(paginationComponent, { table })\n        : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/data/icons.ts",
    "content": "\"use client\";\n\nimport { Activity, AlertCircle, SearchCheck } from \"lucide-react\";\n\nexport const status = {\n  operational: SearchCheck,\n  investigating: AlertCircle,\n  identified: AlertCircle,\n  monitoring: Activity,\n} as const;\n\nexport const icons = {\n  status,\n};\n"
  },
  {
    "path": "apps/status-page/src/data/incidents.client.ts",
    "content": "import { Bookmark, Check, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"acknowledge\",\n    label: \"Acknowledge\",\n    icon: Bookmark,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"resolve\",\n    label: \"Resolve\",\n    icon: Check,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type IncidentAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<IncidentAction[\"id\"], () => Promise<void> | void>>,\n): (IncidentAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/status-page/src/data/incidents.ts",
    "content": "export const incidents = [\n  {\n    id: 1,\n    startedAt: new Date(\"2025-05-05 12:00:00\"),\n    acknowledged: null,\n    resolvedAt: new Date(\"2025-05-05 14:00:00\"),\n    monitor: \"OpenStatus API\",\n  },\n];\n\nexport type Incident = (typeof incidents)[number];\n"
  },
  {
    "path": "apps/status-page/src/data/invitations.ts",
    "content": "export const invitations = [\n  {\n    id: 1,\n    email: \"thibault@openstatus.dev\",\n    role: \"member\",\n    createdAt: \"2021-01-01\",\n    expiresAt: \"2021-01-07\",\n    acceptedAt: \"2021-01-02\",\n  },\n];\n\nexport type Invitation = (typeof invitations)[number];\n"
  },
  {
    "path": "apps/status-page/src/data/maintenances.client.ts",
    "content": "import { Pencil, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Edit\",\n    icon: Pencil,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type MaintenanceAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<MaintenanceAction[\"id\"], () => Promise<void> | void>>,\n): (MaintenanceAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/status-page/src/data/maintenances.ts",
    "content": "const today = new Date();\nconst week = new Date(today);\nweek.setDate(week.getDate() + 7);\nconst hour = new Date(week);\nhour.setHours(hour.getHours() + 1);\n\nexport const maintenances = [\n  {\n    id: 1,\n    title: \"DB Migration\",\n    message:\n      \"We are performing a db migration on our system and will be down for a an hour.\",\n    startDate: week,\n    endDate: hour,\n    affected: [\"OpenStatus API\"],\n  },\n  {\n    id: 2,\n    title: \"System Upgrade\",\n    message:\n      \"We will be upgrading our core infrastructure to improve performance and reliability. Service interruptions may occur.\",\n    startDate: new Date(\"2025-03-01 11:00:00\"),\n    endDate: new Date(\"2025-03-01 15:30:00\"),\n    affected: [\"OpenStatus API\", \"OpenStatus Web\"],\n  },\n];\n\nexport type Maintenance = (typeof maintenances)[number];\n"
  },
  {
    "path": "apps/status-page/src/data/members.ts",
    "content": "export const members = [\n  {\n    id: 1,\n    name: \"Maximilian Kaske\",\n    email: \"max@openstatus.dev\",\n    role: \"admin\",\n    createdAt: \"2021-01-01\",\n  },\n];\n\nexport type Member = (typeof members)[number];\n"
  },
  {
    "path": "apps/status-page/src/data/metrics.client.ts",
    "content": "\"use client\";\n\nimport type { MetricCard } from \"@/components/content/metric-card\";\nimport { formatDateTime, formatMilliseconds } from \"@/lib/formatter\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { monitorRegions } from \"@openstatus/db/src/schema/constants\";\nimport type { RegionMetric } from \"./region-metrics\";\nimport type { Region } from \"./regions\";\n\nexport const STATUS = [\"success\", \"error\", \"degraded\"] as const;\nexport const PERIODS = [\"1d\", \"7d\", \"14d\"] as const;\nexport const REGIONS =\n  monitorRegions as unknown as (typeof monitorRegions)[number][];\nexport const PERCENTILES = [\"p50\", \"p75\", \"p90\", \"p95\", \"p99\"] as const;\nexport const INTERVALS = [5, 15, 30, 60, 120, 240, 480, 1440] as const;\nexport const TRIGGER = [\"api\", \"cron\"] as const;\n\nconst PERCENTILE_MAP = {\n  p50: \"p50Latency\",\n  p75: \"p75Latency\",\n  p90: \"p90Latency\",\n  p95: \"p95Latency\",\n  p99: \"p99Latency\",\n} as const;\n\n// FIXME: rename pipe return values\n\nexport function mapMetrics(metrics: RouterOutputs[\"tinybird\"][\"metrics\"]) {\n  return metrics.data?.map((metric) => {\n    return {\n      p50: metric.p50Latency,\n      p75: metric.p75Latency,\n      p90: metric.p90Latency,\n      p95: metric.p95Latency,\n      p99: metric.p99Latency,\n      total: metric.count,\n      uptime: (metric.success + metric.degraded) / metric.count,\n      degraded: metric.degraded,\n      error: metric.error,\n      lastTimestamp: metric.lastTimestamp,\n    };\n  });\n}\n\nexport const metricsCards = {\n  uptime: {\n    label: \"UPTIME\",\n    variant: \"success\",\n  },\n  degraded: {\n    label: \"DEGRADED\",\n    variant: \"warning\",\n  },\n  error: {\n    label: \"FAILING\",\n    variant: \"destructive\",\n  },\n  total: {\n    label: \"REQUESTS\",\n    variant: \"default\",\n  },\n  lastTimestamp: {\n    label: \"LAST CHECKED\",\n    variant: \"ghost\",\n  },\n  p50: {\n    label: \"P50\",\n    variant: \"default\",\n  },\n  p75: {\n    label: \"P75\",\n    variant: \"default\",\n  },\n  p90: {\n    label: \"P90\",\n    variant: \"default\",\n  },\n  p95: {\n    label: \"P95\",\n    variant: \"default\",\n  },\n  p99: {\n    label: \"P99\",\n    variant: \"default\",\n  },\n} as const satisfies Record<\n  keyof ReturnType<typeof mapMetrics>[number],\n  {\n    label: string;\n    variant: React.ComponentProps<typeof MetricCard>[\"variant\"];\n  }\n>;\n\nexport function mapUptime(status: RouterOutputs[\"tinybird\"][\"uptime\"]) {\n  return status.data\n    .map((status) => {\n      return {\n        ...status,\n        ok: status.success,\n        interval: formatDateTime(status.interval),\n        total: status.success + status.error + status.degraded,\n      };\n    })\n    .reverse();\n}\n\n/**\n * Transform Tinybird `metricsRegions` response into RegionMetric[] for UI.\n */\nexport function mapRegionMetrics(\n  timeline: RouterOutputs[\"tinybird\"][\"metricsRegions\"] | undefined,\n  regions: Region[],\n  percentile: (typeof PERCENTILES)[number],\n): RegionMetric[] {\n  if (!timeline)\n    return (regions\n      .sort((a, b) => a.localeCompare(b))\n      .map((region) => ({\n        region,\n        p50: 0,\n        p90: 0,\n        p99: 0,\n        trend: [] as {\n          latency: number;\n          timestamp: number;\n          [key: string]: number;\n        }[],\n      })) ?? []) as RegionMetric[];\n\n  type TimelineRow = (typeof timeline.data)[number];\n\n  const map = new Map<\n    Region,\n    {\n      region: Region;\n      p50: number;\n      p90: number;\n      p99: number;\n      trend: {\n        latency: number;\n        timestamp: number;\n        [key: string]: number;\n      }[];\n    }\n  >();\n\n  (timeline.data as TimelineRow[])\n    .filter((row) => regions.includes(row.region as Region))\n    .sort((a, b) => a.region.localeCompare(b.region))\n    .forEach((row) => {\n      const region = row.region as Region;\n      const entry = map.get(region) ?? {\n        region,\n        p50: 0,\n        p90: 0,\n        p99: 0,\n        trend: [],\n      };\n\n      entry.trend.push({\n        latency: row[PERCENTILE_MAP[percentile]] ?? 0,\n        timestamp: row.timestamp,\n        [region]: row[PERCENTILE_MAP[percentile]] ?? 0,\n      });\n\n      entry.p50 += row.p50Latency ?? 0;\n      entry.p90 += row.p90Latency ?? 0;\n      entry.p99 += row.p99Latency ?? 0;\n\n      map.set(region, entry);\n    });\n\n  map.forEach((entry) => {\n    const count = entry.trend.length || 1;\n    entry.trend.reverse();\n    entry.p50 = Math.round(entry.p50 / count);\n    entry.p90 = Math.round(entry.p90 / count);\n    entry.p99 = Math.round(entry.p99 / count);\n  });\n\n  return Array.from(map.values()) as RegionMetric[];\n}\n\nexport function mapGlobalMetrics(\n  metrics: RouterOutputs[\"tinybird\"][\"globalMetrics\"],\n) {\n  return metrics.data?.map((metric) => {\n    return {\n      p50: metric.p50Latency,\n      p75: metric.p75Latency,\n      p90: metric.p90Latency,\n      p95: metric.p95Latency,\n      p99: metric.p99Latency,\n      total: metric.count,\n      monitorId: metric.monitorId,\n    };\n  });\n}\n\nexport type MonitorListMetric = {\n  title: string;\n  key: \"degraded\" | \"error\" | \"active\" | \"inactive\" | \"p95\";\n  value: number | string | undefined;\n  variant: React.ComponentProps<typeof MetricCard>[\"variant\"];\n};\n\nexport const globalCards = [\n  \"active\",\n  \"degraded\",\n  \"error\",\n  \"inactive\",\n  \"p95\",\n] as const;\n\nexport const metricsGlobalCards: Record<\n  (typeof globalCards)[number],\n  {\n    title: string;\n    key: (typeof globalCards)[number];\n  }\n> = {\n  active: {\n    title: \"Normal\",\n    key: \"active\" as const,\n  },\n  degraded: {\n    title: \"Degraded\",\n    key: \"degraded\" as const,\n  },\n  error: {\n    title: \"Failing\",\n    key: \"error\" as const,\n  },\n  inactive: {\n    title: \"Inactive\",\n    key: \"inactive\" as const,\n  },\n  p95: {\n    title: \"Slowest P95\",\n    key: \"p95\" as const,\n  },\n};\n\n/**\n * Build the metric cards data that is shown on the monitors list page.\n */\nexport function getMonitorListMetrics(\n  monitors: RouterOutputs[\"monitor\"][\"list\"] = [],\n  data: {\n    p95Latency: number;\n    monitorId: string;\n  }[] = [],\n): readonly MonitorListMetric[] {\n  const variantMap: Record<\n    (typeof globalCards)[number],\n    React.ComponentProps<typeof MetricCard>[\"variant\"]\n  > = {\n    active: \"success\",\n    degraded: \"warning\",\n    error: \"destructive\",\n    inactive: \"default\",\n    p95: \"ghost\",\n  } as const;\n\n  return globalCards.map((key) => {\n    let value: number | string | undefined;\n    switch (key) {\n      case \"active\":\n        value = monitors.filter(\n          (m) => m.status === \"active\" && m.active,\n        ).length;\n        break;\n      case \"degraded\":\n        value = monitors.filter(\n          (m) => m.status === \"degraded\" && m.active,\n        ).length;\n        break;\n      case \"error\":\n        value = monitors.filter((m) => m.status === \"error\" && m.active).length;\n        break;\n      case \"inactive\":\n        value = monitors.filter((m) => m.active === false).length;\n        break;\n      case \"p95\":\n        const p95 = data.sort((a, b) => b.p95Latency - a.p95Latency)[0]\n          ?.p95Latency;\n        value = p95 ? formatMilliseconds(p95) : \"N/A\";\n        break;\n    }\n\n    return {\n      title: metricsGlobalCards[key].title,\n      key,\n      value,\n      variant: variantMap[key],\n    } as const;\n  }) as readonly MonitorListMetric[];\n}\n\nexport function mapLatency(\n  latency: RouterOutputs[\"tinybird\"][\"metricsLatency\"],\n  percentile: (typeof PERCENTILES)[number],\n) {\n  return latency.data?.map((metric) => {\n    return {\n      timestamp: formatDateTime(new Date(metric.timestamp)),\n      latency: metric[PERCENTILE_MAP[percentile]],\n    };\n  });\n}\n\nexport function mapTimingPhases(\n  timingPhases: RouterOutputs[\"tinybird\"][\"metricsTimingPhases\"],\n  percentile: (typeof PERCENTILES)[number],\n) {\n  return timingPhases.data?.map((metric) => {\n    return {\n      timestamp: formatDateTime(new Date(metric.timestamp)),\n      dns: metric[`${percentile}Dns`],\n      ttfb: metric[`${percentile}Ttfb`],\n      transfer: metric[`${percentile}Transfer`],\n      connect: metric[`${percentile}Connect`],\n      tls: metric[`${percentile}Tls`],\n    };\n  });\n}\n"
  },
  {
    "path": "apps/status-page/src/data/monitor-tags.ts",
    "content": "export const monitorTags = [\n  {\n    value: \"production\",\n    label: \"Production\",\n    color: \"bg-green-500\",\n  },\n  {\n    value: \"development\",\n    label: \"Development\",\n    color: \"bg-blue-500\",\n  },\n  {\n    value: \"staging\",\n    label: \"Staging\",\n    color: \"bg-yellow-500\",\n  },\n  {\n    value: \"testing\",\n    label: \"Testing\",\n    color: \"bg-purple-500\",\n  },\n  {\n    value: \"api\",\n    label: \"API\",\n    color: \"bg-red-500\",\n  },\n  {\n    value: \"database\",\n    label: \"Database\",\n    color: \"bg-orange-500\",\n  },\n];\n\nexport type MonitorTag = (typeof monitorTags)[number];\n"
  },
  {
    "path": "apps/status-page/src/data/monitors.client.ts",
    "content": "import { Code, Copy, CopyPlus, Pencil, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Edit\",\n    icon: Pencil,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"copy-id\",\n    label: \"Copy ID\",\n    icon: Copy,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"clone\",\n    label: \"Clone\",\n    icon: CopyPlus,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"export\",\n    label: \"Export Code\",\n    icon: Code,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type MonitorAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<MonitorAction[\"id\"], () => Promise<void> | void>>,\n): (MonitorAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/status-page/src/data/monitors.ts",
    "content": "export const monitors = [\n  {\n    id: 1,\n    name: \"OpenStatus Marketing\",\n    description: \"Marketing website for OpenStatus\",\n    public: true,\n    active: true,\n    status: \"Normal\" as const,\n    url: \"https://openstatus.dev\",\n    tags: [\"Production\"],\n    lastIncident: undefined,\n    p50: 110,\n    p90: 200,\n    p99: 250,\n  },\n  {\n    id: 2,\n    name: \"OpenStatus API\",\n    description: \"API for OpenStatus\",\n    public: true,\n    active: true,\n    status: \"Normal\" as const,\n    url: \"https://api.openstatus.dev/v1/ping\",\n    tags: [\"Production\", \"API\"],\n    lastIncident: undefined,\n    p50: 34,\n    p90: 201,\n    p99: 530,\n  },\n  {\n    id: 3,\n    name: \"OpenStatus App\",\n    description: \"Dashboard for OpenStatus\",\n    public: true,\n    active: true,\n    status: \"Failing\" as const,\n    url: \"https://openstatus.dev/app\",\n    tags: [\"Production\"],\n    lastIncident: \"10 minutes ago\",\n    p50: 130,\n    p90: 200,\n    p99: 250,\n  },\n  {\n    id: 4,\n    name: \"Lightweight OS\",\n    description: \"Lightweight Operations System\",\n    public: false,\n    active: false,\n    status: \"Inactive\" as const,\n    url: \"https://data-table.openstatus.dev/light\",\n    tags: [\"Development\"],\n    lastIncident: undefined,\n    p50: undefined,\n    p90: undefined,\n    p99: undefined,\n  },\n  {\n    id: 5,\n    name: \"Astro Status Page\",\n    description: \"Status page for Astro\",\n    public: false,\n    active: true,\n    status: \"Degraded\" as const,\n    url: \"https://status.openstat.us\",\n    tags: [\"Development\"],\n    lastIncident: undefined,\n    p50: 130,\n    p90: 201,\n    p99: 250,\n  },\n  {\n    id: 6,\n    name: \"Vercel Edge Ping\",\n    description: \"Ping for Vercel Edge\",\n    public: false,\n    active: true,\n    status: \"Normal\" as const,\n    url: \"https://light.openstatus.dev\",\n    tags: [\"Staging\"],\n    lastIncident: \"15 days ago\",\n    p50: 30,\n    p90: 240,\n    p99: 400,\n  },\n];\n\nexport type Monitor = (typeof monitors)[number];\n"
  },
  {
    "path": "apps/status-page/src/data/plans.ts",
    "content": "export const plans = [\n  {\n    title: \"Hobby\",\n    id: \"hobby\",\n    description: \"Perfect for personal projects\",\n    price: 0,\n    limits: {\n      monitors: 1,\n      regions: 35,\n      periodicity: \"10m\",\n      \"status-pages\": 1,\n      members: 1,\n      \"notification-channels\": 1,\n      \"custom-domain\": false,\n      \"password-protection\": false,\n      \"status-subscribers\": false,\n      \"audit-log\": false,\n    },\n  },\n  {\n    title: \"Starter\",\n    id: \"starter\",\n    description: \"Perfect for uptime monitoring\",\n    price: 30,\n    limits: {\n      monitors: 10,\n      regions: 35,\n      periodicity: \"1m\",\n      \"status-pages\": 1,\n      members: Number.POSITIVE_INFINITY,\n      \"notification-channels\": 10,\n      \"custom-domain\": true,\n      \"password-protection\": true,\n      \"status-subscribers\": true,\n      \"audit-log\": false,\n    },\n  },\n  {\n    title: \"Pro\",\n    id: \"team\",\n    description: \"Perfect for global synthetic monitoring\",\n    price: 100,\n    limits: {\n      monitors: 100,\n      regions: 35,\n      periodicity: \"30s\",\n      \"status-pages\": 5,\n      members: Number.POSITIVE_INFINITY,\n      \"notification-channels\": 20,\n      \"custom-domain\": true,\n      \"password-protection\": true,\n      \"status-subscribers\": true,\n      \"audit-log\": true,\n    },\n  },\n];\n"
  },
  {
    "path": "apps/status-page/src/data/region-metrics.client.ts",
    "content": "import { Filter, Zap } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"filter\",\n    label: \"Filter\",\n    icon: Filter,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"trigger\",\n    label: \"Trigger\",\n    icon: Zap,\n    variant: \"default\" as const,\n  },\n] as const;\n\nexport type RegionMetricAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<RegionMetricAction[\"id\"], () => Promise<void> | void>>,\n): (RegionMetricAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/status-page/src/data/region-metrics.ts",
    "content": "import type { Region } from \"./regions\";\n\nexport const regionMetrics = [\n  {\n    region: \"ams\" as const satisfies Region,\n    p50: 100,\n    p90: 150,\n    p99: 200,\n    trend: [{ latency: 100, timestamp: 1716729600 }],\n  },\n  {\n    region: \"fra\" as const satisfies Region,\n    p50: 110,\n    p90: 155,\n    p99: 220,\n    trend: [{ latency: 100, timestamp: 1716729600 }],\n  },\n  {\n    region: \"gru\" as const satisfies Region,\n    p50: 120,\n    p90: 160,\n    p99: 230,\n    trend: [{ latency: 100, timestamp: 1716729600 }],\n  },\n];\n\nexport type RegionMetric = (typeof regionMetrics)[number];\n"
  },
  {
    "path": "apps/status-page/src/data/region-percentile.ts",
    "content": "const randomizer = Math.random() * 10;\n\nexport const regionPercentile = Array.from({ length: 30 }, (_, i) => ({\n  timestamp: new Date(\n    new Date().setMinutes(new Date().getMinutes() - i),\n  ).toLocaleString(\"default\", {\n    hour: \"numeric\",\n    minute: \"numeric\",\n  }),\n  latency: Math.floor(Math.random() * randomizer + 1) * 100,\n})).map((item, i) => {\n  const baseLatency = item.latency;\n  const randomFactor = () => 0.85 + Math.random() * 0.3; // Random factor between 0.85-1.15\n\n  return {\n    ...item,\n    // More realistic percentile distribution with randomness\n    p50: Math.round(baseLatency * 0.7 * randomFactor()),\n    p75: Math.round(baseLatency * 0.85 * randomFactor()),\n    p90: Math.round(baseLatency * 1.1 * randomFactor()),\n    p95: Math.round(baseLatency * 1.3 * randomFactor()),\n    p99: Math.round(baseLatency * 1.8 * randomFactor()),\n    // REMINDER: for error bars\n    error: [4, 5, 6].includes(i) ? 1 : undefined,\n  };\n});\n\nexport type RegionPercentile = (typeof regionPercentile)[number];\n"
  },
  {
    "path": "apps/status-page/src/data/regions.ts",
    "content": "export const regions = [\n  {\n    code: \"ams\",\n    location: \"Amsterdam, Netherlands\",\n    flag: \"🇳🇱\",\n    continent: \"Europe\",\n  },\n  {\n    code: \"arn\",\n    location: \"Stockholm, Sweden\",\n    flag: \"🇸🇪\",\n    continent: \"Europe\",\n  },\n  {\n    code: \"atl\",\n    location: \"Atlanta, Georgia, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"bog\",\n    location: \"Bogotá, Colombia\",\n    flag: \"🇨🇴\",\n    continent: \"South America\",\n  },\n  {\n    code: \"bom\",\n    location: \"Mumbai, India\",\n    flag: \"🇮🇳\",\n    continent: \"Asia\",\n  },\n  {\n    code: \"bos\",\n    location: \"Boston, Massachusetts, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"cdg\",\n    location: \"Paris, France\",\n    flag: \"🇫🇷\",\n    continent: \"Europe\",\n  },\n  {\n    code: \"den\",\n    location: \"Denver, Colorado, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"dfw\",\n    location: \"Dallas, Texas, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"ewr\",\n    location: \"Secaucus, New Jersey, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"eze\",\n    location: \"Ezeiza, Argentina\",\n    flag: \"🇦🇷\",\n    continent: \"South America\",\n  },\n  {\n    code: \"fra\",\n    location: \"Frankfurt, Germany\",\n    flag: \"🇩🇪\",\n    continent: \"Europe\",\n  },\n  {\n    code: \"gdl\",\n    location: \"Guadalajara, Mexico\",\n    flag: \"🇲🇽\",\n    continent: \"North America\",\n  },\n  {\n    code: \"gig\",\n    location: \"Rio de Janeiro, Brazil\",\n    flag: \"🇧🇷\",\n    continent: \"South America\",\n  },\n  {\n    code: \"gru\",\n    location: \"Sao Paulo, Brazil\",\n    flag: \"🇧🇷\",\n    continent: \"South America\",\n  },\n  {\n    code: \"hkg\",\n    location: \"Hong Kong, Hong Kong\",\n    flag: \"🇭🇰\",\n    continent: \"Asia\",\n  },\n  {\n    code: \"iad\",\n    location: \"Ashburn, Virginia, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"jnb\",\n    location: \"Johannesburg, South Africa\",\n    flag: \"🇿🇦\",\n    continent: \"Africa\",\n  },\n  {\n    code: \"lax\",\n    location: \"Los Angeles, California, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"lhr\",\n    location: \"London, United Kingdom\",\n    flag: \"🇬🇧\",\n    continent: \"Europe\",\n  },\n  {\n    code: \"mad\",\n    location: \"Madrid, Spain\",\n    flag: \"🇪🇸\",\n    continent: \"Europe\",\n  },\n  {\n    code: \"mia\",\n    location: \"Miami, Florida, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"nrt\",\n    location: \"Tokyo, Japan\",\n    flag: \"🇯🇵\",\n    continent: \"Asia\",\n  },\n  {\n    code: \"ord\",\n    location: \"Chicago, Illinois, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"otp\",\n    location: \"Bucharest, Romania\",\n    flag: \"🇷🇴\",\n    continent: \"Europe\",\n  },\n  {\n    code: \"phx\",\n    location: \"Phoenix, Arizona, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"qro\",\n    location: \"Querétaro, Mexico\",\n    flag: \"🇲🇽\",\n    continent: \"North America\",\n  },\n  {\n    code: \"scl\",\n    location: \"Santiago, Chile\",\n    flag: \"🇨🇱\",\n    continent: \"South America\",\n  },\n  {\n    code: \"sjc\",\n    location: \"San Jose, California, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"sea\",\n    location: \"Seattle, Washington, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n  },\n  {\n    code: \"sin\",\n    location: \"Singapore, Singapore\",\n    flag: \"🇸🇬\",\n    continent: \"Asia\",\n  },\n  {\n    code: \"syd\",\n    location: \"Sydney, Australia\",\n    flag: \"🇦🇺\",\n    continent: \"Oceania\",\n  },\n  {\n    code: \"waw\",\n    location: \"Warsaw, Poland\",\n    flag: \"🇵🇱\",\n    continent: \"Europe\",\n  },\n  {\n    code: \"yul\",\n    location: \"Montreal, Canada\",\n    flag: \"🇨🇦\",\n    continent: \"North America\",\n  },\n  {\n    code: \"yyz\",\n    location: \"Toronto, Canada\",\n    flag: \"🇨🇦\",\n    continent: \"North America\",\n  },\n] as const;\n\nexport type Region = (typeof regions)[number][\"code\"];\n\nexport const groupedRegions = regions.reduce(\n  (acc, region) => {\n    const continent = region.continent;\n    if (!acc[continent]) {\n      acc[continent] = [];\n    }\n    acc[continent].push(region.code);\n    return acc;\n  },\n  {} as Record<string, Region[]>,\n);\n"
  },
  {
    "path": "apps/status-page/src/data/response-logs.ts",
    "content": "export const responseLogs = [\n  {\n    id: 1,\n    url: \"https://api.openstatus.dev\",\n    method: \"GET\",\n    status: 200 as const,\n    latency: 150,\n    timing: {\n      dns: 10,\n      connect: 20,\n      tls: 30,\n      ttfb: 40,\n      transfer: 50,\n    },\n    assertions: [],\n    region: \"ams\" as const,\n    error: false,\n    timestamp: new Date().getTime(),\n    headers: {\n      \"Cache-Control\":\n        \"private, no-cache, no-store, max-age=0, must-revalidate\",\n      \"Content-Type\": \"text/html; charset=utf-8\",\n      Date: \"Sun, 28 Jan 2024 08:50:13 GMT\",\n      Server: \"Vercel\",\n    },\n    type: \"scheduled\" as const satisfies \"scheduled\" | \"on-demand\",\n  },\n  {\n    id: 2,\n    url: \"https://api.openstatus.dev\",\n    method: \"GET\",\n    status: 500 as const,\n    latency: 150,\n    timing: {\n      dns: 4,\n      connect: 120,\n      tls: 12,\n      ttfb: 20,\n      transfer: 40,\n    },\n    assertions: [],\n    region: \"ams\" as const,\n    error: true,\n    timestamp: new Date().getTime(),\n    headers: {\n      \"Cache-Control\":\n        \"private, no-cache, no-store, max-age=0, must-revalidate\",\n      \"Content-Type\": \"text/html; charset=utf-8\",\n      Date: \"Sun, 28 Jan 2024 08:50:13 GMT\",\n      Server: \"Vercel\",\n    },\n    type: \"scheduled\" as const satisfies \"scheduled\" | \"on-demand\",\n    // error message\n    message:\n      \"Environment variable 'NEXT_PUBLIC_TEST_KEY' is missing. Please add and redeploy your project.\",\n  },\n];\n\nexport type ResponseLog = (typeof responseLogs)[number];\nexport type Timing = ResponseLog[\"timing\"];\n"
  },
  {
    "path": "apps/status-page/src/data/status-codes.ts",
    "content": "export const statusCodes = [\n  {\n    code: 200 as const,\n    bg: \"bg-success\",\n    text: \"text-success\",\n    name: \"OK\",\n  },\n  {\n    code: 500 as const,\n    bg: \"bg-destructive\",\n    text: \"text-destructive\",\n    name: \"Internal Server Error\",\n  },\n];\n\nexport type StatusCode = (typeof statusCodes)[number][\"code\"];\n"
  },
  {
    "path": "apps/status-page/src/data/status-pages.client.ts",
    "content": "import { Copy, Pencil, Tag, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Edit\",\n    icon: Pencil,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"copy-id\",\n    label: \"Copy ID\",\n    icon: Copy,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"create-badge\",\n    label: \"Create Badge\",\n    icon: Tag,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type StatusPageAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<Record<StatusPageAction[\"id\"], () => Promise<void> | void>>,\n): (StatusPageAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/status-page/src/data/status-pages.ts",
    "content": "export const statusPages = [\n  {\n    id: 1,\n    name: \"OpenStatus Status\",\n    description: \"See our uptime history and status reports.\",\n    slug: \"status\",\n    favicon: \"https://openstatus.dev/favicon.ico\",\n    domain: \"status.openstatus.dev\",\n    protected: true,\n    showValues: false,\n    // NOTE: the worst status of a report\n    status: \"degraded\" as const,\n    monitors: [],\n  },\n];\n\nexport type StatusPage = (typeof statusPages)[number];\n"
  },
  {
    "path": "apps/status-page/src/data/status-report-updates.client.ts",
    "content": "import { Pencil, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Edit\",\n    icon: Pencil,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type StatusReportUpdateAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<\n    Record<StatusReportUpdateAction[\"id\"], () => Promise<void> | void>\n  >,\n): (StatusReportUpdateAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/status-page/src/data/status-reports.client.ts",
    "content": "import { Pencil, Plus, Trash2 } from \"lucide-react\";\n\nexport const actions = [\n  {\n    id: \"edit\",\n    label: \"Edit\",\n    icon: Pencil,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"create-update\",\n    label: \"Create Update\",\n    icon: Plus,\n    variant: \"default\" as const,\n  },\n  {\n    id: \"delete\",\n    label: \"Delete\",\n    icon: Trash2,\n    variant: \"destructive\" as const,\n  },\n] as const;\n\nexport type StatusReportUpdateAction = (typeof actions)[number];\n\nexport const getActions = (\n  props: Partial<\n    Record<StatusReportUpdateAction[\"id\"], () => Promise<void> | void>\n  >,\n): (StatusReportUpdateAction & { onClick?: () => Promise<void> | void })[] => {\n  return actions.map((action) => ({\n    ...action,\n    onClick: props[action.id as keyof typeof props],\n  }));\n};\n"
  },
  {
    "path": "apps/status-page/src/data/status-reports.ts",
    "content": "const today = new Date();\nconst lastHour = new Date(new Date().setHours(new Date().getHours() - 1));\nconst yesterday = new Date(new Date().setDate(new Date().getDate() - 1));\n\nconsole.log({ today, lastHour, yesterday });\n\nexport const statusReports = [\n  {\n    id: 1,\n    name: \"Downtime API due to hosting provider with 400 errors\",\n    startedAt: yesterday,\n    updatedAt: today,\n    status: \"operational\",\n    updates: [\n      {\n        id: 2,\n        status: \"operational\" as const,\n        message:\n          \"Everything is under control, we continue to monitor the situation.\",\n        date: today,\n        updatedAt: today,\n        monitors: [1],\n      },\n      {\n        id: 1,\n        status: \"investigating\" as const,\n        message:\n          \"Our hosting provider is having an increase of 400 errors. We are aware of the dependency and will be working on a solution to reduce the risk.\",\n        date: lastHour,\n        updatedAt: lastHour,\n        monitors: [1],\n      },\n    ],\n    affected: [\"OpenStatus API\"],\n  },\n  {\n    id: 3,\n    name: \"Downtime API due to hosting provider with 400 errors\",\n    startedAt: new Date(\"2025-08-05 12:10:00\"),\n    updatedAt: new Date(\"2025-08-05 12:30:00\"),\n    status: \"operational\",\n    updates: [\n      {\n        id: 4,\n        status: \"operational\" as const,\n        message:\n          \"Everything is under control, we continue to monitor the situation.\",\n        date: new Date(\"2025-08-06 03:30:00\"),\n        updatedAt: new Date(\"2025-08-06 03:30:00\"),\n        monitors: [1],\n      },\n      {\n        id: 3,\n        status: \"monitoring\" as const,\n        message:\n          \"We are continuing to monitor the situation to ensure that the issue is resolved.\",\n        date: new Date(\"2025-08-05 16:00:00\"),\n        updatedAt: new Date(\"2025-08-05 16:00:00\"),\n        monitors: [1],\n      },\n      {\n        id: 2,\n        status: \"identified\" as const,\n        message:\n          \"We have identified the root cause of the issue. It is due to a configuration error on our part.\",\n        date: new Date(\"2025-08-05 14:00:00\"),\n        updatedAt: new Date(\"2025-08-05 14:00:00\"),\n        monitors: [1],\n      },\n      {\n        id: 1,\n        status: \"investigating\" as const,\n        message:\n          \"Our hosting provider is having an increase of 400 errors. We are working on a solution to reduce the risk.\",\n        date: new Date(\"2025-08-05 12:00:00\"),\n        updatedAt: new Date(\"2025-08-05 12:00:00\"),\n        monitors: [1],\n      },\n    ],\n    affected: [\"OpenStatus API\"],\n  },\n];\n\nexport type StatusReport = (typeof statusReports)[number];\n"
  },
  {
    "path": "apps/status-page/src/hooks/use-pathname-prefix.ts",
    "content": "\"use client\";\n\nimport { useTRPC } from \"@/lib/trpc/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useParams } from \"next/navigation\";\nimport { useEffect, useState } from \"react\";\n\nexport function usePathnamePrefix() {\n  const trpc = useTRPC();\n  const { domain } = useParams<{ domain: string }>();\n  const { data: page } = useQuery({\n    ...trpc.statusPage.get.queryOptions({ slug: domain }),\n  });\n  const [prefix, setPrefix] = useState(\"\");\n\n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      const hostnames = window.location.hostname.split(\".\");\n      const pathnames = window.location.pathname.split(\"/\");\n      const isCustomDomain = window.location.hostname === page?.customDomain;\n\n      if (\n        isCustomDomain ||\n        (hostnames.length > 2 &&\n          hostnames[0] !== \"www\" &&\n          !window.location.hostname.endsWith(\".vercel.app\"))\n      ) {\n        setPrefix(\"\");\n      } else {\n        setPrefix(pathnames[1] || \"\");\n      }\n    }\n  }, [page?.customDomain]);\n\n  return prefix;\n}\n"
  },
  {
    "path": "apps/status-page/src/instrumentation.ts",
    "content": "import * as Sentry from \"@sentry/nextjs\";\n\nexport async function register() {\n  if (process.env.NEXT_RUNTIME === \"nodejs\") {\n    await import(\"../sentry.server.config\");\n  }\n\n  if (process.env.NEXT_RUNTIME === \"edge\") {\n    await import(\"../sentry.edge.config\");\n  }\n}\n\nexport const onRequestError = Sentry.captureRequestError;\n"
  },
  {
    "path": "apps/status-page/src/lib/auth/adapter.ts",
    "content": "import { DrizzleAdapter } from \"@auth/drizzle-adapter\";\nimport type { Adapter } from \"next-auth/adapters\";\n\nimport { db } from \"@openstatus/db\";\nimport {\n  verificationToken,\n  viewer,\n  viewerAccounts,\n  viewerSession,\n} from \"@openstatus/db/src/schema\";\n\nexport const adapter: Adapter = {\n  ...DrizzleAdapter(db, {\n    // @ts-ignore\n    usersTable: viewer,\n    // NOTE: only need accounts for external providers\n    // @ts-ignore\n    accountsTable: viewerAccounts,\n    // @ts-ignore\n    sessionsTable: viewerSession,\n    // @ts-ignore\n    verificationTokensTable: verificationToken,\n  }),\n};\n"
  },
  {
    "path": "apps/status-page/src/lib/auth/index.ts",
    "content": "import type { DefaultSession } from \"next-auth\";\nimport NextAuth, { AuthError } from \"next-auth\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { viewer } from \"@openstatus/db/src/schema\";\n\nimport { getValidCustomDomain } from \"@/lib/domain\";\nimport { getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { headers } from \"next/headers\";\nimport { adapter } from \"./adapter\";\nimport { ResendProvider } from \"./providers\";\n\nexport type { DefaultSession };\n\nexport const { handlers, signIn, signOut, auth } = NextAuth({\n  debug: process.env.NODE_ENV === \"development\",\n  adapter,\n  providers: [ResendProvider],\n  callbacks: {\n    async signIn(params) {\n      const _headers = await headers();\n      const host = _headers.get(\"host\");\n\n      if (!host) throw new AuthError(\"No host found\");\n\n      const protocol = _headers.get(\"x-forwarded-proto\") || \"https\";\n      const req = new Request(`${protocol}://${host}`, {\n        headers: new Headers(_headers),\n      });\n      const { prefix } = getValidCustomDomain(req);\n\n      if (!prefix || !params.user.email) return false;\n\n      const queryClient = getQueryClient();\n      // NOTE: throws an error if the email domain is not allowed\n      const query = await queryClient.fetchQuery(\n        trpc.statusPage.validateEmailDomain.queryOptions({\n          slug: prefix,\n          email: params.user.email,\n        }),\n      );\n\n      if (!query) return false;\n\n      if (params.account?.provider === \"resend\") {\n        // if the user is new, the id is the verification_token and not the viewer id, so we cannot update the viewer\n        if (Number.isNaN(Number(params.user.id))) return true;\n        await db\n          .update(viewer)\n          .set({ updatedAt: new Date() })\n          .where(eq(viewer.id, Number(params.user.id)))\n          .run();\n\n        return true;\n      }\n\n      return false;\n    },\n    redirect: async (params) => {\n      return params.url;\n    },\n    async session(params) {\n      return params.session;\n    },\n  },\n});\n"
  },
  {
    "path": "apps/status-page/src/lib/auth/providers.ts",
    "content": "import { getQueryClient, trpc } from \"@/lib/trpc/server\";\nimport { EmailClient } from \"@openstatus/emails\";\nimport Resend from \"next-auth/providers/resend\";\nimport { getValidCustomDomain } from \"../domain\";\n\nexport const ResendProvider = Resend({\n  apiKey: undefined,\n  async sendVerificationRequest(params) {\n    const url = params.url;\n    const email = params.identifier;\n\n    const emailClient = new EmailClient({\n      apiKey: process.env.RESEND_API_KEY ?? \"\",\n    });\n\n    const { prefix } = getValidCustomDomain(params.request);\n\n    if (!prefix) return;\n\n    const queryClient = getQueryClient();\n    const query = await queryClient.fetchQuery(\n      trpc.statusPage.validateEmailDomain.queryOptions({ slug: prefix, email }),\n    );\n\n    if (!query) return;\n\n    await emailClient.sendStatusPageMagicLink({\n      page: query.page.title,\n      link: url,\n      to: params.identifier,\n    });\n  },\n});\n"
  },
  {
    "path": "apps/status-page/src/lib/base-url.ts",
    "content": "export function getBaseUrl({\n  slug,\n  customDomain,\n}: {\n  slug?: string;\n  customDomain?: string;\n}) {\n  if (process.env.NODE_ENV === \"development\") {\n    return `http://localhost:3000/${slug}`;\n  }\n  if (customDomain) {\n    return `https://${customDomain}`;\n  }\n  return `https://${slug}.openstatus.dev`;\n}\n"
  },
  {
    "path": "apps/status-page/src/lib/chart.ts",
    "content": "import type { ChartConfig } from \"@openstatus/ui/components/ui/chart\";\n\n// Helper to extract item config from a payload.\nexport function getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string,\n) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined;\n  }\n\n  const payloadPayload =\n    \"payload\" in payload &&\n    typeof payload.payload === \"object\" &&\n    payload.payload !== null\n      ? payload.payload\n      : undefined;\n\n  let configLabelKey: string = key;\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === \"string\"\n  ) {\n    configLabelKey = payload[key as keyof typeof payload] as string;\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[\n      key as keyof typeof payloadPayload\n    ] as string;\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config];\n}\n"
  },
  {
    "path": "apps/status-page/src/lib/composition.ts",
    "content": "import * as React from \"react\";\n\n/**\n * A utility to compose multiple event handlers into a single event handler.\n * Run originalEventHandler first, then ourEventHandler unless prevented.\n */\nfunction composeEventHandlers<E>(\n  originalEventHandler?: (event: E) => void,\n  ourEventHandler?: (event: E) => void,\n  { checkForDefaultPrevented = true } = {},\n) {\n  return function handleEvent(event: E) {\n    originalEventHandler?.(event);\n\n    if (\n      checkForDefaultPrevented === false ||\n      !(event as unknown as Event).defaultPrevented\n    ) {\n      return ourEventHandler?.(event);\n    }\n  };\n}\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value.\n * This utility takes care of different types of refs: callback refs and RefObject(s).\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together.\n * Accepts callback refs and RefObject(s).\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs.\n * Accepts callback refs and RefObject(s).\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeEventHandlers, composeRefs, useComposedRefs };\n"
  },
  {
    "path": "apps/status-page/src/lib/domain.ts",
    "content": "import type { NextRequest } from \"next/server\";\n\nexport const getValidSubdomain = (host?: string | null) => {\n  let subdomain: string | null = null;\n  if (!host && typeof window !== \"undefined\") {\n    // On client side, get the host from window\n    // biome-ignore lint: to fix later\n    host = window.location.host;\n  }\n\n  // Exclude localhost and IP addresses from being treated as subdomains\n  if (\n    host?.match(/^(localhost|127\\\\.0\\\\.0\\\\.1|::1|\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/)\n  ) {\n    return null;\n  }\n\n  // Handle subdomains of localhost (e.g., hello.localhost:3000)\n  if (host?.match(/^([^.]+)\\.localhost(:\\d+)?$/)) {\n    const match = host.match(/^([^.]+)\\.localhost(:\\d+)?$/);\n    return match?.[1] || null;\n  }\n\n  // we should improve here for custom vercel deploy page\n  if (host?.includes(\".\") && !host.includes(\".vercel.app\")) {\n    const candidate = host.split(\".\")[0];\n    if (candidate && !candidate.includes(\"www\")) {\n      // Valid candidate\n      subdomain = candidate;\n    }\n  }\n\n  // In case the host is a custom domain\n  if (\n    host &&\n    !(\n      host?.includes(\"stpg.dev\") ||\n      host?.includes(\"openstatus.dev\") ||\n      host?.endsWith(\".vercel.app\")\n    )\n  ) {\n    subdomain = host;\n  }\n  return subdomain;\n};\n\nexport const getValidCustomDomain = (req: NextRequest | Request) => {\n  const url = \"nextUrl\" in req ? req.nextUrl.clone() : new URL(req.url);\n  const headers = req.headers;\n  const host = headers.get(\"x-forwarded-host\");\n\n  let prefix = \"\";\n  let type: \"hostname\" | \"pathname\";\n\n  const hostnames = host?.split(/[.:]/) ?? url.host.split(/[.:]/);\n  const pathnames = url.pathname.split(\"/\");\n\n  const subdomain = getValidSubdomain(url.host);\n  console.log({\n    hostnames,\n    pathnames,\n    host,\n    urlHost: url.host,\n    subdomain,\n  });\n\n  if (\n    hostnames.length > 2 &&\n    hostnames[0] !== \"www\" &&\n    !url.host.endsWith(\".vercel.app\")\n  ) {\n    prefix = hostnames[0].toLowerCase();\n    type = \"hostname\";\n  } else {\n    prefix = pathnames[1].toLowerCase();\n    type = \"pathname\";\n  }\n\n  if (subdomain !== null) {\n    prefix = subdomain.toLowerCase();\n  }\n\n  console.log({ type, prefix });\n\n  return { type, prefix };\n};\n"
  },
  {
    "path": "apps/status-page/src/lib/formatter.ts",
    "content": "import { endOfDay, isSameDay, startOfDay } from \"date-fns\";\n\nexport function formatMilliseconds(ms: number) {\n  if (ms > 1000) {\n    return `${Intl.NumberFormat(\"en-US\", {\n      style: \"unit\",\n      unit: \"second\",\n      maximumFractionDigits: 2,\n    }).format(ms / 1000)}`;\n  }\n\n  return `${Intl.NumberFormat(\"en-US\", {\n    style: \"unit\",\n    unit: \"millisecond\",\n  }).format(ms)}`;\n}\n\nexport function formatMillisecondsRange(min: number, max: number) {\n  if ((min > 1000 && max > 1000) || (min < 1000 && max < 1000)) {\n    return `${formatNumber(min / 1000)} - ${formatMilliseconds(max)}`;\n  }\n\n  return `${formatMilliseconds(min)} - ${formatMilliseconds(max)}`;\n}\n\nexport function formatPercentage(value: number) {\n  if (Number.isNaN(value)) return \"100%\";\n  return `${Intl.NumberFormat(\"en-US\", {\n    style: \"percent\",\n    minimumFractionDigits: 2,\n    maximumFractionDigits: 2,\n  }).format(value)}`;\n}\n\nexport function formatNumber(\n  value: number,\n  options?: Intl.NumberFormatOptions,\n) {\n  return `${Intl.NumberFormat(\"en-US\", options).format(value)}`;\n}\n\n// TODO: think of supporting custom formats\n\nexport function formatDate(date: Date, options?: Intl.DateTimeFormatOptions) {\n  return date.toLocaleDateString(\"en-US\", {\n    year: \"numeric\",\n    month: \"long\",\n    day: \"numeric\",\n    ...options,\n  });\n}\n\nexport function formatDateTime(date: Date) {\n  return date.toLocaleDateString(\"en-US\", {\n    month: \"long\",\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"numeric\",\n  });\n}\n\nexport function formatTime(date: Date) {\n  return date.toLocaleTimeString(\"en-US\", {\n    hour: \"numeric\",\n    minute: \"numeric\",\n  });\n}\n\nexport function formatDateRange(from?: Date, to?: Date) {\n  const sameDay = from && to && isSameDay(from, to);\n  const isFromStartDay = from && startOfDay(from).getTime() === from.getTime();\n  const isToEndDay = to && endOfDay(to).getTime() === to.getTime();\n\n  if (sameDay) {\n    if (from.getTime() === to.getTime()) {\n      return formatDateTime(from);\n    }\n    if (from && to) {\n      return `${formatDateTime(from)} - ${formatTime(to)}`;\n    }\n  }\n\n  if (from && to) {\n    if (isFromStartDay && isToEndDay) {\n      return `${formatDate(from)} - ${formatDate(to)}`;\n    }\n    return `${formatDateTime(from)} - ${formatDateTime(to)}`;\n  }\n\n  if (to) {\n    return `Until ${formatDateTime(to)}`;\n  }\n\n  if (from) {\n    return `Since ${formatDateTime(from)}`;\n  }\n\n  return \"All time\";\n}\n\nexport function formatDateForInput(date: Date): string {\n  const year = date.getFullYear();\n  const month = String(date.getMonth() + 1).padStart(2, \"0\");\n  const day = String(date.getDate()).padStart(2, \"0\");\n  const hours = String(date.getHours()).padStart(2, \"0\");\n  const minutes = String(date.getMinutes()).padStart(2, \"0\");\n\n  return `${year}-${month}-${day}T${hours}:${minutes}`;\n}\n"
  },
  {
    "path": "apps/status-page/src/lib/protected.ts",
    "content": "export function createProtectedCookieKey(value: string) {\n  return `secured-${value}`;\n}\n"
  },
  {
    "path": "apps/status-page/src/lib/server-actions.ts",
    "content": "export async function generateServerActionPromise<T>(\n  promise: Promise<{ success: boolean; error?: string; data?: T }>,\n): Promise<T | undefined> {\n  const { success, data, error } = await promise;\n  if (!success) {\n    throw new Error(error);\n  }\n  return data;\n}\n"
  },
  {
    "path": "apps/status-page/src/lib/trpc/client.tsx",
    "content": "\"use client\";\n\nimport { endingLink } from \"@/lib/trpc/shared\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { createTRPCClient, loggerLink } from \"@trpc/client\";\nimport { createTRPCContext } from \"@trpc/tanstack-react-query\";\nimport { useState } from \"react\";\n\nimport type { AppRouter } from \"@openstatus/api\";\n\nexport const { TRPCProvider, useTRPC, useTRPCClient } =\n  createTRPCContext<AppRouter>();\n\nfunction makeQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        // With SSR, we usually want to set some default staleTime\n        // above 0 to avoid refetching immediately on the client\n        staleTime: 60 * 1000,\n      },\n    },\n  });\n}\nlet browserQueryClient: QueryClient | undefined = undefined;\nfunction getQueryClient() {\n  if (typeof window === \"undefined\") {\n    // Server: always make a new query client\n    return makeQueryClient();\n  }\n  // Browser: make a new query client if we don't already have one\n  // This is very important, so we don't re-make a new client if React\n  // suspends during the initial render. This may not be needed if we\n  // have a suspense boundary BELOW the creation of the query client\n  if (!browserQueryClient) browserQueryClient = makeQueryClient();\n  return browserQueryClient;\n}\n\nexport function TRPCReactProvider({ children }: { children: React.ReactNode }) {\n  const queryClient = getQueryClient();\n  const [trpcClient] = useState(() =>\n    createTRPCClient<AppRouter>({\n      links: [\n        loggerLink({\n          enabled: (opts) =>\n            process.env.NODE_ENV === \"development\" ||\n            (opts.direction === \"down\" && opts.result instanceof Error),\n        }),\n        endingLink({\n          headers: {\n            \"x-trpc-source\": \"client\",\n          },\n        }),\n      ],\n    }),\n  );\n\n  return (\n    <QueryClientProvider client={queryClient}>\n      <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>\n        {children}\n      </TRPCProvider>\n    </QueryClientProvider>\n  );\n}\n"
  },
  {
    "path": "apps/status-page/src/lib/trpc/query-client.ts",
    "content": "import {\n  QueryClient,\n  defaultShouldDehydrateQuery,\n} from \"@tanstack/react-query\";\n\nexport function makeQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        staleTime: 60 * 1000,\n      },\n      dehydrate: {\n        shouldDehydrateQuery: (query) =>\n          defaultShouldDehydrateQuery(query) ||\n          query.state.status === \"pending\",\n      },\n      hydrate: {},\n    },\n  });\n}\n"
  },
  {
    "path": "apps/status-page/src/lib/trpc/server.tsx",
    "content": "import \"server-only\";\n\nimport type { AppRouter } from \"@openstatus/api\";\n\nimport { HydrationBoundary } from \"@tanstack/react-query\";\nimport { dehydrate } from \"@tanstack/react-query\";\nimport { createTRPCClient, loggerLink } from \"@trpc/client\";\nimport {\n  type TRPCQueryOptions,\n  createTRPCOptionsProxy,\n} from \"@trpc/tanstack-react-query\";\nimport { cookies } from \"next/headers\";\nimport { cache } from \"react\";\nimport { makeQueryClient } from \"./query-client\";\nimport { endingLink } from \"./shared\";\n\n// IMPORTANT: Create a stable getter for the query client that\n//            will return the same client during the same request.\nexport const getQueryClient = cache(makeQueryClient);\n\nexport const trpc = createTRPCOptionsProxy<AppRouter>({\n  queryClient: getQueryClient,\n  client: createTRPCClient({\n    links: [\n      loggerLink({\n        enabled: (opts) =>\n          process.env.NODE_ENV === \"development\" ||\n          (opts.direction === \"down\" && opts.result instanceof Error),\n      }),\n      endingLink({\n        headers: {\n          \"x-trpc-source\": \"server\",\n        },\n        fetch: async (url, options) => {\n          const cookieStore = await cookies();\n          return fetch(url, {\n            ...options,\n            credentials: \"include\",\n            headers: {\n              ...options?.headers,\n              cookie: cookieStore.toString(),\n            },\n          });\n        },\n      }),\n    ],\n  }),\n});\n\nexport function HydrateClient(props: { children: React.ReactNode }) {\n  const queryClient = getQueryClient();\n\n  return (\n    <HydrationBoundary state={dehydrate(queryClient)}>\n      {props.children}\n    </HydrationBoundary>\n  );\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\nexport function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(\n  queryOptions: T,\n) {\n  const queryClient = getQueryClient();\n\n  if (queryOptions.queryKey[1]?.type === \"infinite\") {\n    // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\n    void queryClient.prefetchInfiniteQuery(queryOptions as any);\n  } else {\n    void queryClient.prefetchQuery(queryOptions);\n  }\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\nexport function batchPrefetch<T extends ReturnType<TRPCQueryOptions<any>>>(\n  queryOptionsArray: T[],\n) {\n  const queryClient = getQueryClient();\n\n  for (const queryOptions of queryOptionsArray) {\n    if (queryOptions.queryKey[1]?.type === \"infinite\") {\n      // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\n      void queryClient.prefetchInfiniteQuery(queryOptions as any);\n    } else {\n      void queryClient.prefetchQuery(queryOptions);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/status-page/src/lib/trpc/shared.ts",
    "content": "import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from \"@trpc/client\";\nimport { httpBatchLink } from \"@trpc/client\";\n\nimport type { AppRouter } from \"@openstatus/api\";\nimport superjson from \"superjson\";\n\nconst getBaseUrl = () => {\n  if (typeof window !== \"undefined\") return \"\";\n  // Note: status-page has its own tRPC API routes\n  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // Vercel\n  return \"http://localhost:3000\"; // Local dev and Docker (internal calls)\n};\n\nconst lambdas = [\"stripeRouter\", \"emailRouter\"];\n\nexport const endingLink = (opts?: {\n  fetch?: typeof fetch;\n  headers?: HTTPHeaders | (() => HTTPHeaders | Promise<HTTPHeaders>);\n}) =>\n  ((runtime) => {\n    const sharedOpts = {\n      headers: opts?.headers,\n      fetch: opts?.fetch,\n      transformer: superjson,\n      // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\n    } satisfies Partial<HTTPBatchLinkOptions<any>>;\n\n    const edgeLink = httpBatchLink({\n      ...sharedOpts,\n      url: `${getBaseUrl()}/api/trpc/edge`,\n    })(runtime);\n    const lambdaLink = httpBatchLink({\n      ...sharedOpts,\n      url: `${getBaseUrl()}/api/trpc/lambda`,\n    })(runtime);\n\n    return (ctx) => {\n      const path = ctx.op.path.split(\".\") as [string, ...string[]];\n      const endpoint = lambdas.includes(path[0]) ? \"lambda\" : \"edge\";\n\n      const newCtx = {\n        ...ctx,\n        op: { ...ctx.op, path: path.join(\".\") },\n      };\n      return endpoint === \"edge\" ? edgeLink(newCtx) : lambdaLink(newCtx);\n    };\n  }) satisfies TRPCLink<AppRouter>;\n"
  },
  {
    "path": "apps/status-page/src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/status-page/src/next-auth.d.ts",
    "content": "import type { Viewer as DefaultViewerSchema } from \"@openstatus/db/src/schema\";\n\ndeclare module \"next-auth\" {\n  interface User extends DefaultViewerSchema {}\n}\n"
  },
  {
    "path": "apps/status-page/src/proxy.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport { auth } from \"@/lib/auth\";\n\nimport { db, sql } from \"@openstatus/db\";\nimport { page, selectPageSchema } from \"@openstatus/db/src/schema\";\nimport { getValidSubdomain } from \"./lib/domain\";\nimport { createProtectedCookieKey } from \"./lib/protected\";\n\nexport default auth(async (req) => {\n  const url = req.nextUrl.clone();\n  const response = NextResponse.next();\n  const cookies = req.cookies;\n  const headers = req.headers;\n  const host = headers.get(\"x-forwarded-host\");\n\n  let prefix = \"\";\n  let type: \"hostname\" | \"pathname\";\n\n  const hostnames = host?.split(/[.:]/) ?? url.host.split(/[.:]/);\n  const pathnames = url.pathname.split(\"/\");\n\n  const subdomain = getValidSubdomain(url.host);\n  console.log({\n    hostnames,\n    pathnames,\n    host,\n    urlHost: url.host,\n    subdomain,\n  });\n\n  if (\n    hostnames.length > 2 &&\n    hostnames[0] !== \"www\" &&\n    !url.host.endsWith(\".vercel.app\")\n  ) {\n    prefix = hostnames[0].toLowerCase();\n    type = \"hostname\";\n  } else {\n    prefix = pathnames[1].toLowerCase();\n    type = \"pathname\";\n  }\n\n  if (subdomain !== null) {\n    prefix = subdomain.toLowerCase();\n  }\n\n  console.log({ pathname: url.pathname, type, prefix, subdomain });\n\n  if (url.pathname === \"/\" && type !== \"hostname\" && subdomain === null) {\n    return response;\n  }\n\n  const query = await db\n    .select()\n    .from(page)\n    .where(\n      sql`lower(${page.slug}) = ${prefix} OR lower(${page.customDomain}) = ${prefix}`,\n    )\n    .get();\n\n  const validation = selectPageSchema.safeParse(query);\n\n  if (!validation.success) {\n    return response;\n  }\n\n  const _page = validation.data;\n\n  console.log({ slug: _page?.slug, customDomain: _page?.customDomain });\n\n  if (_page?.accessType === \"password\") {\n    const protectedCookie = cookies.get(createProtectedCookieKey(_page.slug));\n    const cookiePassword = protectedCookie ? protectedCookie.value : undefined;\n    const queryPassword = url.searchParams.get(\"pw\");\n    const password = queryPassword || cookiePassword;\n\n    if (password !== _page.password && !url.pathname.endsWith(\"/login\")) {\n      const { pathname, origin } = req.nextUrl;\n\n      // custom domain redirect\n      if (_page.customDomain && host !== `${_page.slug}.stpg.dev`) {\n        const redirect = pathname.replace(`/${_page.customDomain}`, \"\");\n        const url = new URL(\n          `https://${_page.customDomain}/login?redirect=${encodeURIComponent(\n            redirect,\n          )}`,\n        );\n        console.log(\"redirect to /login\", url.toString());\n        return NextResponse.redirect(url);\n      }\n\n      const url = new URL(\n        `${origin}${\n          type === \"pathname\" ? `/${prefix}` : \"\"\n        }/login?redirect=${encodeURIComponent(pathname)}`,\n      );\n      return NextResponse.redirect(url);\n    }\n    if (password === _page.password && url.pathname.endsWith(\"/login\")) {\n      const redirect = url.searchParams.get(\"redirect\");\n\n      // custom domain redirect\n      if (_page.customDomain && host !== `${_page.slug}.stpg.dev`) {\n        const url = new URL(`https://${_page.customDomain}${redirect ?? \"/\"}`);\n        console.log(\"redirect to /\", url.toString());\n        return NextResponse.redirect(url);\n      }\n\n      return NextResponse.redirect(\n        new URL(\n          `${req.nextUrl.origin}${\n            redirect ?? type === \"pathname\" ? `/${prefix}` : \"/\"\n          }`,\n        ),\n      );\n    }\n  }\n\n  if (_page.accessType === \"email-domain\") {\n    const { origin, pathname } = req.nextUrl;\n    const email = req.auth?.user?.email;\n    const emailDomain = email?.split(\"@\")[1];\n    if (\n      !pathname.endsWith(\"/login\") &&\n      (!emailDomain || !_page.authEmailDomains.includes(emailDomain))\n    ) {\n      const url = new URL(\n        `${origin}${type === \"pathname\" ? `/${prefix}` : \"\"}/login`,\n      );\n      return NextResponse.redirect(url);\n    }\n    if (\n      pathname.endsWith(\"/login\") &&\n      emailDomain &&\n      _page.authEmailDomains.includes(emailDomain)\n    ) {\n      const url = new URL(\n        `${origin}${type === \"pathname\" ? `/${prefix}` : \"\"}`,\n      );\n      return NextResponse.redirect(url);\n    }\n  }\n\n  const proxy = req.headers.get(\"x-proxy\");\n  console.log({ proxy });\n\n  if (proxy) {\n    const rewriteUrl = new URL(`/${prefix}${url.pathname}`, req.url);\n    // Preserve search params from original request\n    rewriteUrl.search = url.search;\n    return NextResponse.rewrite(rewriteUrl);\n  }\n\n  console.log({\n    customDomain: _page.customDomain,\n    host,\n    expectedHost: `${_page.slug}.stpg.dev`,\n  });\n  if (_page.customDomain && host !== `${_page.slug}.stpg.dev`) {\n    if (pathnames.length > 2 && !subdomain) {\n      const pathname = pathnames.slice(2).join(\"/\");\n      const rewriteUrl = new URL(`/${_page.slug}/${pathname}`, req.url);\n      rewriteUrl.search = url.search;\n      return NextResponse.rewrite(rewriteUrl);\n    }\n    if (_page.customDomain && subdomain) {\n      console.log({ url: req.url });\n      // const vercelURL = process.env.VERCEL_URL || \"www.stpg.dev\";\n      // console.log({newUrl: vercelURL})\n      if (pathnames.length > 2) {\n        const pathname = pathnames.slice(1).join(\"/\");\n\n        const rewriteUrl = new URL(\n          `${pathname}`,\n          `https://${_page.slug}.stpg.dev`,\n        );\n        console.log({ rewriteUrl });\n        rewriteUrl.search = url.search;\n        return NextResponse.rewrite(rewriteUrl);\n      }\n      const rewriteUrl = new URL(\n        `${url.pathname}`,\n        `https://${_page.slug}.stpg.dev`,\n      );\n      console.log({ rewriteUrl });\n      rewriteUrl.search = url.search;\n      return NextResponse.rewrite(rewriteUrl);\n    }\n    const rewriteUrl = new URL(`/${_page.slug}`, req.url);\n    console.log({ rewriteUrl });\n    rewriteUrl.search = url.search;\n    return NextResponse.rewrite(rewriteUrl);\n  }\n  if (host?.includes(\"openstatus.dev\")) {\n    const rewriteUrl = new URL(`/${prefix}${url.pathname}`, req.url);\n    // Preserve search params from original request\n    rewriteUrl.search = url.search;\n    return NextResponse.rewrite(rewriteUrl);\n  }\n  return response;\n});\n\nexport const config = {\n  matcher: [\n    \"/((?!api|assets|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)\",\n  ],\n};\n"
  },
  {
    "path": "apps/status-page/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"compilerOptions\": {\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"strictNullChecks\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"bundler\"\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"env.ts\"]\n}\n"
  },
  {
    "path": "apps/status-page/turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"extends\": [\"//\"],\n  \"tasks\": {\n    \"build\": {\n      \"dependsOn\": [\"@openstatus/react#build\"]\n    }\n  }\n}\n"
  },
  {
    "path": "apps/web/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# vercel\n.vercel\n\n# content-collections\n.content-collections\n\n# Sentry Auth Token\n.sentryclirc\n"
  },
  {
    "path": "apps/web/README.md",
    "content": "## Getting Started\n\nFirst, run the development server:\n\n```bash\npnpm dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the\nresult.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js\n  features and API.\n- [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an\n  interactive Next.js tutorial.\n\nYou can check out\n[the Next.js GitHub repository](https://github.com/vercel/next.js/) - your\nfeedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the\n[Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme)\nfrom the creators of Next.js.\n\nCheck out our\n[Next.js deployment documentation](https://nextjs.org/docs/deployment) for more\ndetails.\n"
  },
  {
    "path": "apps/web/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"src/styles/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\"\n  }\n}\n"
  },
  {
    "path": "apps/web/env.ts",
    "content": "const file = Bun.file(\"./.env.example\");\nawait Bun.write(\"./.env\", file);\n"
  },
  {
    "path": "apps/web/instrumentation-client.ts",
    "content": "// This file configures the initialization of Sentry on the client.\n// The config you add here will be used whenever a users loads a page in their browser.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\n\nimport * as Sentry from \"@sentry/nextjs\";\n\nSentry.init({\n  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_FRONTEND,\n\n  // Adjust this value in production, or use tracesSampler for greater control\n  tracesSampleRate: 0.5,\n\n  // Setting this option to true will print useful information to the console while you're setting up Sentry.\n  debug: false,\n\n  replaysOnErrorSampleRate: 1.0,\n\n  // This sets the sample rate to be 10%. You may want this to be 100% while\n  // in development and sample at a lower rate in production\n  replaysSessionSampleRate: 0.1,\n\n  // You can remove this option if you're not planning to use the Sentry Session Replay feature:\n  integrations: [\n    Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),\n    Sentry.captureConsoleIntegration({ levels: [\"error\"] }),\n  ],\n});\n\nexport const onRouterTransitionStart = Sentry.captureRouterTransitionStart;\n\nexport const onRequestError = Sentry.captureRequestError;\n"
  },
  {
    "path": "apps/web/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "apps/web/next.config.ts",
    "content": "import { withSentryConfig } from \"@sentry/nextjs\";\nimport type { NextConfig } from \"next\";\n\n// REMINDER: avoid Clickjacking attacks by setting the frame-ancestors directive\nconst securityHeaders = [\n  {\n    key: \"Content-Security-Policy\",\n    value: \"frame-ancestors 'self' https://shoogle.dev\",\n  },\n];\n\n/** @type {import('next').NextConfig} */\nconst nextConfig: NextConfig = {\n  reactStrictMode: true,\n  transpilePackages: [\"@openstatus/ui\", \"@openstatus/api\", \"next-mdx-remote\"],\n  outputFileTracingIncludes: {\n    \"/\": [\n      \"./node_modules/.pnpm/@google-cloud/tasks/build/esm/src/**/*.json\",\n      \"./node_modules/@google-cloud/tasks/build/esm/src/**/*.js\",\n    ],\n  },\n  experimental: {\n    turbopackScopeHoisting: false,\n    // serverMinification:false,\n  },\n  serverExternalPackages: [\"@google-cloud/tasks\"],\n  expireTime: 180, // 3 minutes\n  logging: {\n    fetches: {\n      fullUrl: true,\n    },\n  },\n  images: {\n    remotePatterns: [\n      {\n        protocol: \"https\",\n        hostname: \"**.public.blob.vercel-storage.com\",\n      },\n      {\n        protocol: \"https\",\n        hostname: \"screenshot.openstat.us\",\n      },\n      {\n        protocol: \"https\",\n        hostname: \"www.openstatus.dev\",\n      },\n    ],\n  },\n  async headers() {\n    return [{ source: \"/(.*)\", headers: securityHeaders }];\n  },\n  async redirects() {\n    return [\n      {\n        source: \"/legal/terms\",\n        destination: \"/terms\",\n        permanent: true,\n      },\n      {\n        source: \"/legal/privacy\",\n        destination: \"/privacy\",\n        permanent: true,\n      },\n      {\n        source: \"/features/monitoring\",\n        destination: \"/uptime-monitoring\",\n        permanent: true,\n      },\n      {\n        source: \"/features/status-page\",\n        destination: \"/status-page\",\n        permanent: true,\n      },\n      {\n        source: \"/api-monitoring\",\n        destination: \"/uptime-monitoring\",\n        permanent: true,\n      },\n      {\n        source: \"/monitoring-as-code\",\n        destination: \"/uptime-monitoring\",\n        permanent: true,\n      },\n      {\n        source: \"/private-locations\",\n        destination: \"/uptime-monitoring\",\n        permanent: true,\n      },\n      {\n        source: \"/app/:path*\",\n        destination: \"https://app.openstatus.dev/\",\n        permanent: true,\n      },\n    ];\n  },\n  async rewrites() {\n    return {\n      beforeFiles: [\n        {\n          source: \"/status-page/themes/:path*\",\n          destination: \"https://www.stpg.dev/:path*\",\n        },\n        {\n          source: \"/:path*\",\n          has: [\n            {\n              type: \"host\",\n              value: \"themes.openstatus.dev\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/:path*\",\n        },\n        // New design: proxy app routes to external host with slug prefix\n        {\n          source: \"/:path*\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value: \"(?<slug>[^.]+)\\\\.(openstatus\\\\.dev|localhost)\",\n            },\n          ],\n          destination: \"https://:slug.stpg.dev/:path*\",\n        },\n        // Handle custom domains (e.g., status.mxkaske.dev)\n        {\n          source:\n            \"/:path((?!api|assets|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value: \"^(?!.*\\\\.openstatus\\\\.dev$)(?!openstatus\\\\.dev$)$\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/:path*\",\n        },\n        // enfore routes to avoid infinite redirects - https://github.com/vercel/vercel/issues/6126#issuecomment-823523122\n        // testing with https://validator.w3.org/feed/check.cgi\n        {\n          source: \"/feed/rss\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value: \"^(?!.*\\\\.openstatus\\\\.dev$)(?!openstatus\\\\.dev$)$\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/:domain/feed/rss\",\n        },\n        {\n          source: \"/feed/atom\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value: \"^(?!.*\\\\.openstatus\\\\.dev$)(?!openstatus\\\\.dev$)$\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/:domain/feed/atom\",\n        },\n        {\n          source: \"/feed/rss\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value: \"^(?<domain>.+)$\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/:domain/feed/rss\",\n        },\n        {\n          source: \"/feed/atom\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value: \"^(?<domain>.+)$\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/:domain/feed/atom\",\n        },\n        {\n          source:\n            \"/:path((?!api|assets|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|badge|feed|events|monitors|protected|verify).*)\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value: \"^(?<domain>.+)$\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/:domain*\",\n        },\n        {\n          source:\n            \"/:path((?!api|assets|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value: \"^(?<domain>.+)$\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/:domain/:path*\",\n        },\n        // Handle API routes for custom domains\n        {\n          source: \"/api/:path*\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value:\n                \"^(?!.*\\\\.openstatus\\\\.dev$)(?!openstatus\\\\.dev$)(?<domain>.+)$\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/api/:path*\",\n        },\n        // Handle static assets for custom domains\n        {\n          source: \"/_next/:path*\",\n          has: [\n            { type: \"cookie\", key: \"sp_mode\", value: \"new\" },\n            {\n              type: \"host\",\n              value:\n                \"^(?!.*\\\\.openstatus\\\\.dev$)(?!openstatus\\\\.dev$)(?<domain>.+)$\",\n            },\n          ],\n          destination: \"https://www.stpg.dev/_next/:path*\",\n        },\n        // Markdown content negotiation for AI tools\n        {\n          source: \"/:path*\",\n          destination: \"/api/markdown/:path*\",\n          has: [\n            {\n              type: \"header\",\n              key: \"accept\",\n              value: \"(.*)text/markdown(.*)\",\n            },\n          ],\n        },\n      ],\n    };\n  },\n};\n\n// For detailed options, refer to the official documentation:\n// - Webpack plugin options: https://github.com/getsentry/sentry-webpack-plugin#options\n// - Next.js Sentry setup guide: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/\nconst sentryConfig = {\n  // Prevent log output unless running in a CI environment (helps reduce noise in logs)\n  silent: !process.env.CI,\n  org: \"openstatus\",\n  project: \"openstatus\",\n  authToken: process.env.SENTRY_AUTH_TOKEN,\n\n  // Upload a larger set of source maps for improved stack trace accuracy (increases build time)\n  widenClientFileUpload: true,\n\n  // If set to true, transpiles Sentry SDK to be compatible with IE11 (increases bundle size)\n  transpileClientSDK: false,\n\n  // Tree-shake Sentry logger statements to reduce bundle size\n  webpack: {\n    treeshake: {\n      removeDebugLogging: true,\n    },\n  },\n};\n\nexport default withSentryConfig(nextConfig, sentryConfig);\n"
  },
  {
    "path": "apps/web/package.json",
    "content": "{\n  \"name\": \"@openstatus/web\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"env\": \"bun env.ts\",\n    \"dev\": \"next dev\",\n    \"build:registry\": \"turbo run registry:build --filter=@openstatus/ui\",\n    \"build\": \"pnpm build:registry && next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@auth/core\": \"0.40.0\",\n    \"@auth/drizzle-adapter\": \"1.10.0\",\n    \"@google-cloud/tasks\": \"4.0.1\",\n    \"@hookform/resolvers\": \"5.1.0\",\n    \"@libsql/client\": \"0.15.15\",\n    \"@openpanel/nextjs\": \"1.2.0\",\n    \"@openstatus/analytics\": \"workspace:*\",\n    \"@openstatus/api\": \"workspace:*\",\n    \"@openstatus/assertions\": \"workspace:*\",\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/emails\": \"workspace:*\",\n    \"@openstatus/error\": \"workspace:*\",\n    \"@openstatus/header-analysis\": \"workspace:*\",\n    \"@openstatus/icons\": \"workspace:*\",\n    \"@openstatus/notification-discord\": \"workspace:*\",\n    \"@openstatus/notification-emails\": \"workspace:*\",\n    \"@openstatus/notification-ntfy\": \"workspace:*\",\n    \"@openstatus/notification-opsgenie\": \"workspace:*\",\n    \"@openstatus/notification-pagerduty\": \"workspace:*\",\n    \"@openstatus/notification-slack\": \"workspace:*\",\n    \"@openstatus/notification-webhook\": \"workspace:*\",\n    \"@openstatus/react\": \"workspace:*\",\n    \"@openstatus/regions\": \"workspace:*\",\n    \"@openstatus/theme-store\": \"workspace:*\",\n    \"@openstatus/tinybird\": \"workspace:*\",\n    \"@openstatus/tracker\": \"workspace:*\",\n    \"@openstatus/ui\": \"workspace:*\",\n    \"@openstatus/upstash\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:*\",\n    \"@sentry/nextjs\": \"10.31.0\",\n    \"@stripe/stripe-js\": \"2.1.6\",\n    \"@t3-oss/env-nextjs\": \"0.7.0\",\n    \"@tailwindcss/container-queries\": \"0.1.1\",\n    \"@tailwindcss/typography\": \"0.5.10\",\n    \"@tanstack/react-query\": \"5.81.5\",\n    \"@tanstack/react-query-devtools\": \"5.80.7\",\n    \"@tanstack/react-table\": \"8.21.3\",\n    \"@trpc/client\": \"11.4.4\",\n    \"@trpc/next\": \"11.4.4\",\n    \"@trpc/react-query\": \"11.4.4\",\n    \"@trpc/server\": \"11.4.4\",\n    \"@upstash/qstash\": \"2.6.2\",\n    \"@upstash/redis\": \"1.22.1\",\n    \"@vercel/blob\": \"0.23.3\",\n    \"class-variance-authority\": \"0.7.1\",\n    \"clsx\": \"2.1.1\",\n    \"cmdk\": \"1.1.1\",\n    \"cobe\": \"0.6.3\",\n    \"date-fns\": \"3.6.0\",\n    \"date-fns-tz\": \"2.0.0\",\n    \"feed\": \"4.2.2\",\n    \"gray-matter\": \"4.0.3\",\n    \"lucide-react\": \"0.525.0\",\n    \"nanoid\": \"5.0.7\",\n    \"next\": \"16.1.6\",\n    \"next-auth\": \"5.0.0-beta.29\",\n    \"next-mdx-remote\": \"6.0.0\",\n    \"next-plausible\": \"3.12.5\",\n    \"next-themes\": \"0.4.6\",\n    \"nuqs\": \"2.8.5\",\n    \"random-word-slugs\": \"0.1.7\",\n    \"react\": \"19.2.3\",\n    \"react-day-picker\": \"8.10.1\",\n    \"react-dom\": \"19.2.3\",\n    \"react-hook-form\": \"7.68.0\",\n    \"react-medium-image-zoom\": \"5.4.0\",\n    \"react-tweet\": \"3.2.1\",\n    \"reading-time\": \"1.5.0\",\n    \"recharts\": \"2.15.0\",\n    \"remark-gfm\": \"4.0.1\",\n    \"resend\": \"6.6.0\",\n    \"sanitize-html\": \"2.17.0\",\n    \"schema-dts\": \"1.1.5\",\n    \"shiki\": \"3.23.0\",\n    \"slugify\": \"1.6.6\",\n    \"sonner\": \"2.0.5\",\n    \"stripe\": \"13.8.0\",\n    \"sugar-high\": \"0.9.5\",\n    \"superjson\": \"2.2.2\",\n    \"tailwind-merge\": \"3.3.1\",\n    \"tailwindcss-animate\": \"1.0.7\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"4.1.11\",\n    \"@types/node\": \"24.0.8\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"@types/sanitize-html\": \"2.16.0\",\n    \"postcss\": \"8.4.38\",\n    \"rehype-autolink-headings\": \"7.1.0\",\n    \"rehype-slug\": \"5.1.0\",\n    \"tailwindcss\": \"4.1.11\",\n    \"typescript\": \"5.9.3\",\n    \"unified\": \"11.0.5\"\n  }\n}\n"
  },
  {
    "path": "apps/web/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "apps/web/public/assets/posts/global-latency-monitoring-benchmark-hono-hetzner/hetzner.json",
    "content": "{\n  \"regions\": [\n    \"ams\",\n    \"railway_europe-west4-drams3a\",\n    \"cdg\",\n    \"koyeb_par\",\n    \"fra\",\n    \"koyeb_fra\",\n    \"iad\",\n    \"koyeb_was\",\n    \"railway_us-east4-eqdc4a\",\n    \"koyeb_sfo\",\n    \"railway_us-west2\",\n    \"lax\",\n    \"koyeb_sin\",\n    \"railway_asia-southeast1-eqsg3a\",\n    \"sin\",\n    \"koyeb_tyo\",\n    \"nrt\"\n  ],\n  \"data\": {\n    \"regions\": [\n      \"koyeb_fra\",\n      \"cdg\",\n      \"ams\",\n      \"railway_europe-west4-drams3a\",\n      \"fra\",\n      \"koyeb_par\",\n      \"koyeb_sfo\",\n      \"railway_us-west2\",\n      \"lax\",\n      \"iad\",\n      \"koyeb_was\",\n      \"railway_us-east4-eqdc4a\",\n      \"koyeb_sin\",\n      \"railway_asia-southeast1-eqsg3a\",\n      \"koyeb_tyo\",\n      \"sin\",\n      \"nrt\"\n    ],\n    \"data\": [\n      {\n        \"timestamp\": \"2025-10-16T12:00:00.000Z\",\n        \"koyeb_tyo\": 338,\n        \"railway_europe-west4-drams3a\": 96,\n        \"koyeb_sfo\": 243,\n        \"koyeb_sin\": 344,\n        \"koyeb_was\": 252,\n        \"sin\": 396,\n        \"nrt\": 338,\n        \"lax\": 197,\n        \"ams\": 82,\n        \"iad\": 244,\n        \"railway_us-west2\": 390,\n        \"koyeb_fra\": 68,\n        \"fra\": 67,\n        \"cdg\": 113,\n        \"railway_asia-southeast1-eqsg3a\": 348,\n        \"koyeb_par\": 117,\n        \"railway_us-east4-eqdc4a\": 241\n      },\n      {\n        \"timestamp\": \"2025-10-16T13:00:00.000Z\",\n        \"fra\": 65,\n        \"cdg\": 114,\n        \"koyeb_fra\": 67,\n        \"ams\": 77,\n        \"iad\": 242,\n        \"railway_us-west2\": 247,\n        \"koyeb_par\": 107,\n        \"railway_asia-southeast1-eqsg3a\": 404,\n        \"lax\": 210,\n        \"railway_europe-west4-drams3a\": 96,\n        \"nrt\": 348,\n        \"koyeb_tyo\": 352,\n        \"sin\": 422,\n        \"koyeb_sfo\": 248,\n        \"koyeb_sin\": 402,\n        \"koyeb_was\": 245,\n        \"railway_us-east4-eqdc4a\": 252\n      },\n      {\n        \"timestamp\": \"2025-10-16T14:00:00.000Z\",\n        \"koyeb_tyo\": 366,\n        \"nrt\": 334,\n        \"sin\": 418,\n        \"railway_asia-southeast1-eqsg3a\": 392,\n        \"koyeb_was\": 245,\n        \"koyeb_sin\": 394,\n        \"koyeb_sfo\": 336,\n        \"railway_us-west2\": 253,\n        \"railway_us-east4-eqdc4a\": 251,\n        \"cdg\": 112,\n        \"fra\": 66,\n        \"iad\": 242,\n        \"ams\": 84,\n        \"railway_europe-west4-drams3a\": 95,\n        \"koyeb_par\": 116,\n        \"lax\": 221,\n        \"koyeb_fra\": 67\n      },\n      {\n        \"timestamp\": \"2025-10-16T15:00:00.000Z\",\n        \"fra\": 64,\n        \"koyeb_tyo\": 334,\n        \"cdg\": 114,\n        \"railway_us-west2\": 320,\n        \"railway_asia-southeast1-eqsg3a\": 420,\n        \"lax\": 376,\n        \"koyeb_was\": 252,\n        \"koyeb_sfo\": 407,\n        \"ams\": 82,\n        \"iad\": 242,\n        \"koyeb_sin\": 467,\n        \"sin\": 440,\n        \"nrt\": 319,\n        \"railway_europe-west4-drams3a\": 94,\n        \"railway_us-east4-eqdc4a\": 254,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 64\n      },\n      {\n        \"timestamp\": \"2025-10-16T16:00:00.000Z\",\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 68,\n        \"railway_asia-southeast1-eqsg3a\": 396,\n        \"sin\": 408,\n        \"nrt\": 436,\n        \"railway_us-east4-eqdc4a\": 254,\n        \"lax\": 390,\n        \"railway_europe-west4-drams3a\": 98,\n        \"railway_us-west2\": 462,\n        \"koyeb_was\": 247,\n        \"koyeb_sfo\": 424,\n        \"iad\": 250,\n        \"koyeb_sin\": 394,\n        \"ams\": 83,\n        \"cdg\": 114,\n        \"fra\": 66,\n        \"koyeb_tyo\": 322\n      },\n      {\n        \"timestamp\": \"2025-10-16T17:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 94,\n        \"cdg\": 114,\n        \"fra\": 68,\n        \"koyeb_was\": 248,\n        \"koyeb_sfo\": 422,\n        \"koyeb_sin\": 372,\n        \"railway_us-west2\": 374,\n        \"koyeb_tyo\": 313,\n        \"iad\": 249,\n        \"ams\": 86,\n        \"lax\": 378,\n        \"nrt\": 312,\n        \"railway_us-east4-eqdc4a\": 254,\n        \"sin\": 399,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 67,\n        \"railway_asia-southeast1-eqsg3a\": 380\n      },\n      {\n        \"timestamp\": \"2025-10-16T18:00:00.000Z\",\n        \"koyeb_fra\": 67,\n        \"nrt\": 426,\n        \"koyeb_par\": 118,\n        \"sin\": 380,\n        \"railway_europe-west4-drams3a\": 92,\n        \"railway_us-west2\": 361,\n        \"koyeb_sin\": 369,\n        \"koyeb_sfo\": 404,\n        \"koyeb_was\": 248,\n        \"lhr\": 113,\n        \"railway_asia-southeast1-eqsg3a\": 372,\n        \"fra\": 66,\n        \"cdg\": 114,\n        \"ams\": 84,\n        \"iad\": 250,\n        \"railway_us-east4-eqdc4a\": 249,\n        \"lax\": 364,\n        \"koyeb_tyo\": 341\n      },\n      {\n        \"timestamp\": \"2025-10-16T19:00:00.000Z\",\n        \"nrt\": 300,\n        \"railway_us-west2\": 366,\n        \"koyeb_fra\": 68,\n        \"railway_us-east4-eqdc4a\": 253,\n        \"sin\": 370,\n        \"railway_europe-west4-drams3a\": 96,\n        \"koyeb_par\": 117,\n        \"railway_asia-southeast1-eqsg3a\": 374,\n        \"ams\": 84,\n        \"koyeb_tyo\": 548,\n        \"iad\": 247,\n        \"lax\": 340,\n        \"fra\": 67,\n        \"koyeb_sfo\": 338,\n        \"cdg\": 114,\n        \"koyeb_sin\": 372,\n        \"koyeb_was\": 248\n      },\n      {\n        \"timestamp\": \"2025-10-16T20:00:00.000Z\",\n        \"nrt\": 307,\n        \"railway_asia-southeast1-eqsg3a\": 228,\n        \"sin\": 369,\n        \"koyeb_fra\": 66,\n        \"koyeb_par\": 118,\n        \"fra\": 66,\n        \"cdg\": 100,\n        \"koyeb_tyo\": 311,\n        \"koyeb_sin\": 366,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_sfo\": 308,\n        \"railway_us-east4-eqdc4a\": 250,\n        \"koyeb_was\": 243,\n        \"railway_us-west2\": 302,\n        \"ams\": 82,\n        \"iad\": 240,\n        \"lax\": 307\n      },\n      {\n        \"timestamp\": \"2025-10-16T21:00:00.000Z\",\n        \"ams\": 83,\n        \"iad\": 242,\n        \"railway_us-east4-eqdc4a\": 244,\n        \"koyeb_tyo\": 537,\n        \"lax\": 306,\n        \"railway_asia-southeast1-eqsg3a\": 376,\n        \"koyeb_was\": 245,\n        \"koyeb_sfo\": 320,\n        \"koyeb_sin\": 366,\n        \"fra\": 64,\n        \"cdg\": 113,\n        \"railway_us-west2\": 303,\n        \"koyeb_par\": 117,\n        \"railway_europe-west4-drams3a\": 94,\n        \"koyeb_fra\": 65,\n        \"nrt\": 312,\n        \"sin\": 377\n      },\n      {\n        \"timestamp\": \"2025-10-16T22:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 90,\n        \"sin\": 372,\n        \"koyeb_par\": 115,\n        \"koyeb_fra\": 63,\n        \"nrt\": 428,\n        \"lax\": 309,\n        \"railway_us-east4-eqdc4a\": 249,\n        \"koyeb_tyo\": 557,\n        \"iad\": 236,\n        \"ams\": 80,\n        \"railway_asia-southeast1-eqsg3a\": 214,\n        \"cdg\": 84,\n        \"fra\": 63,\n        \"koyeb_was\": 245,\n        \"koyeb_sfo\": 309,\n        \"railway_us-west2\": 298,\n        \"koyeb_sin\": 368\n      },\n      {\n        \"timestamp\": \"2025-10-16T23:00:00.000Z\",\n        \"cdg\": 112,\n        \"fra\": 63,\n        \"koyeb_tyo\": 316,\n        \"railway_europe-west4-drams3a\": 90,\n        \"railway_us-east4-eqdc4a\": 246,\n        \"lax\": 313,\n        \"koyeb_sin\": 367,\n        \"koyeb_sfo\": 314,\n        \"koyeb_was\": 245,\n        \"iad\": 238,\n        \"ams\": 80,\n        \"sin\": 370,\n        \"railway_us-west2\": 306,\n        \"nrt\": 326,\n        \"railway_asia-southeast1-eqsg3a\": 375,\n        \"koyeb_fra\": 64,\n        \"koyeb_par\": 116\n      },\n      {\n        \"timestamp\": \"2025-10-17T00:00:00.000Z\",\n        \"cdg\": 113,\n        \"fra\": 63,\n        \"railway_us-east4-eqdc4a\": 267,\n        \"railway_asia-southeast1-eqsg3a\": 371,\n        \"koyeb_tyo\": 458,\n        \"koyeb_sfo\": 323,\n        \"lax\": 329,\n        \"railway_us-west2\": 302,\n        \"koyeb_sin\": 370,\n        \"koyeb_was\": 265,\n        \"iad\": 257,\n        \"ams\": 81,\n        \"sin\": 371,\n        \"nrt\": 307,\n        \"koyeb_fra\": 63,\n        \"railway_europe-west4-drams3a\": 94,\n        \"koyeb_par\": 116\n      },\n      {\n        \"timestamp\": \"2025-10-17T01:00:00.000Z\",\n        \"railway_us-west2\": 296,\n        \"railway_asia-southeast1-eqsg3a\": 387,\n        \"koyeb_par\": 115,\n        \"sin\": 386,\n        \"koyeb_fra\": 64,\n        \"nrt\": 311,\n        \"lax\": 318,\n        \"koyeb_tyo\": 308,\n        \"iad\": 259,\n        \"ams\": 82,\n        \"cdg\": 111,\n        \"koyeb_was\": 260,\n        \"fra\": 63,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_sin\": 380,\n        \"koyeb_sfo\": 323,\n        \"railway_us-east4-eqdc4a\": 182\n      },\n      {\n        \"timestamp\": \"2025-10-17T02:00:00.000Z\",\n        \"koyeb_tyo\": 326,\n        \"ams\": 77,\n        \"iad\": 168,\n        \"railway_europe-west4-drams3a\": 91,\n        \"lax\": 314,\n        \"railway_us-west2\": 374,\n        \"koyeb_was\": 268,\n        \"fra\": 62,\n        \"cdg\": 110,\n        \"railway_us-east4-eqdc4a\": 264,\n        \"koyeb_sin\": 387,\n        \"koyeb_sfo\": 320,\n        \"railway_asia-southeast1-eqsg3a\": 384,\n        \"nrt\": 316,\n        \"sin\": 386,\n        \"koyeb_par\": 114,\n        \"koyeb_fra\": 62\n      },\n      {\n        \"timestamp\": \"2025-10-17T03:00:00.000Z\",\n        \"nrt\": 316,\n        \"railway_us-west2\": 306,\n        \"sin\": 388,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_fra\": 62,\n        \"koyeb_par\": 114,\n        \"koyeb_tyo\": 326,\n        \"railway_us-east4-eqdc4a\": 264,\n        \"fra\": 64,\n        \"cdg\": 112,\n        \"ams\": 65,\n        \"iad\": 262,\n        \"railway_asia-southeast1-eqsg3a\": 390,\n        \"koyeb_sfo\": 324,\n        \"lax\": 318,\n        \"koyeb_sin\": 378,\n        \"koyeb_was\": 260\n      },\n      {\n        \"timestamp\": \"2025-10-17T04:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 264,\n        \"koyeb_fra\": 65,\n        \"koyeb_par\": 116,\n        \"nrt\": 548,\n        \"railway_asia-southeast1-eqsg3a\": 386,\n        \"sin\": 392,\n        \"railway_europe-west4-drams3a\": 83,\n        \"ams\": 83,\n        \"iad\": 252,\n        \"railway_us-west2\": 288,\n        \"lax\": 374,\n        \"koyeb_tyo\": 322,\n        \"koyeb_sin\": 308,\n        \"koyeb_sfo\": 308,\n        \"koyeb_was\": 165,\n        \"fra\": 63,\n        \"cdg\": 111\n      },\n      {\n        \"timestamp\": \"2025-10-17T05:00:00.000Z\",\n        \"nrt\": 324,\n        \"koyeb_fra\": 64,\n        \"sin\": 386,\n        \"koyeb_par\": 115,\n        \"railway_us-west2\": 264,\n        \"railway_asia-southeast1-eqsg3a\": 406,\n        \"fra\": 64,\n        \"koyeb_sfo\": 282,\n        \"cdg\": 113,\n        \"koyeb_sin\": 376,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"koyeb_was\": 260,\n        \"railway_europe-west4-drams3a\": 91,\n        \"ams\": 81,\n        \"iad\": 256,\n        \"koyeb_tyo\": 317,\n        \"lax\": 346\n      },\n      {\n        \"timestamp\": \"2025-10-17T06:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 393,\n        \"koyeb_was\": 261,\n        \"koyeb_sin\": 390,\n        \"koyeb_sfo\": 276,\n        \"cdg\": 112,\n        \"fra\": 63,\n        \"iad\": 262,\n        \"ams\": 84,\n        \"koyeb_tyo\": 334,\n        \"lax\": 221,\n        \"railway_us-west2\": 226,\n        \"koyeb_par\": 116,\n        \"nrt\": 530,\n        \"koyeb_fra\": 64,\n        \"sin\": 390,\n        \"railway_europe-west4-drams3a\": 93,\n        \"railway_us-east4-eqdc4a\": 259\n      },\n      {\n        \"timestamp\": \"2025-10-17T07:00:00.000Z\",\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 66,\n        \"sin\": 404,\n        \"railway_europe-west4-drams3a\": 91,\n        \"railway_us-west2\": 376,\n        \"nrt\": 332,\n        \"lax\": 218,\n        \"koyeb_was\": 255,\n        \"koyeb_sin\": 386,\n        \"koyeb_sfo\": 249,\n        \"iad\": 252,\n        \"ams\": 80,\n        \"railway_asia-southeast1-eqsg3a\": 400,\n        \"cdg\": 113,\n        \"fra\": 62,\n        \"railway_us-east4-eqdc4a\": 265,\n        \"koyeb_tyo\": 322\n      },\n      {\n        \"timestamp\": \"2025-10-17T08:00:00.000Z\",\n        \"fra\": 66,\n        \"cdg\": 114,\n        \"koyeb_tyo\": 321,\n        \"koyeb_was\": 258,\n        \"lax\": 212,\n        \"koyeb_sin\": 354,\n        \"railway_europe-west4-drams3a\": 91,\n        \"koyeb_sfo\": 252,\n        \"ams\": 82,\n        \"iad\": 248,\n        \"sin\": 424,\n        \"railway_us-east4-eqdc4a\": 266,\n        \"nrt\": 324,\n        \"railway_asia-southeast1-eqsg3a\": 396,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 66,\n        \"railway_us-west2\": 214\n      },\n      {\n        \"timestamp\": \"2025-10-17T09:00:00.000Z\",\n        \"cdg\": 115,\n        \"fra\": 64,\n        \"koyeb_sfo\": 240,\n        \"railway_europe-west4-drams3a\": 93,\n        \"koyeb_sin\": 388,\n        \"koyeb_was\": 257,\n        \"lax\": 200,\n        \"koyeb_tyo\": 329,\n        \"railway_us-west2\": 371,\n        \"iad\": 261,\n        \"ams\": 81,\n        \"sin\": 396,\n        \"koyeb_fra\": 68,\n        \"nrt\": 324,\n        \"koyeb_par\": 114,\n        \"railway_us-east4-eqdc4a\": 260,\n        \"railway_asia-southeast1-eqsg3a\": 397\n      },\n      {\n        \"timestamp\": \"2025-10-17T10:00:00.000Z\",\n        \"koyeb_par\": 116,\n        \"sin\": 406,\n        \"koyeb_fra\": 65,\n        \"nrt\": 317,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_was\": 256,\n        \"fra\": 66,\n        \"cdg\": 113,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"railway_us-west2\": 214,\n        \"railway_asia-southeast1-eqsg3a\": 402,\n        \"koyeb_sin\": 385,\n        \"koyeb_sfo\": 244,\n        \"lax\": 206,\n        \"koyeb_tyo\": 323,\n        \"ams\": 85,\n        \"iad\": 254\n      },\n      {\n        \"timestamp\": \"2025-10-17T11:00:00.000Z\",\n        \"koyeb_was\": 254,\n        \"koyeb_sin\": 387,\n        \"ams\": 82,\n        \"koyeb_sfo\": 246,\n        \"iad\": 254,\n        \"railway_asia-southeast1-eqsg3a\": 394,\n        \"lax\": 206,\n        \"koyeb_tyo\": 559,\n        \"fra\": 66,\n        \"cdg\": 113,\n        \"railway_us-west2\": 366,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 64,\n        \"railway_us-east4-eqdc4a\": 263,\n        \"railway_europe-west4-drams3a\": 94,\n        \"nrt\": 567,\n        \"sin\": 395\n      },\n      {\n        \"timestamp\": \"2025-10-17T12:00:00.000Z\",\n        \"fra\": 66,\n        \"railway_us-east4-eqdc4a\": 261,\n        \"cdg\": 114,\n        \"koyeb_was\": 256,\n        \"koyeb_sfo\": 246,\n        \"koyeb_sin\": 393,\n        \"koyeb_tyo\": 338,\n        \"ams\": 88,\n        \"iad\": 258,\n        \"railway_europe-west4-drams3a\": 94,\n        \"lax\": 209,\n        \"nrt\": 582,\n        \"railway_asia-southeast1-eqsg3a\": 404,\n        \"koyeb_par\": 118,\n        \"sin\": 414,\n        \"koyeb_fra\": 68,\n        \"railway_us-west2\": 289\n      },\n      {\n        \"timestamp\": \"2025-10-17T13:00:00.000Z\",\n        \"koyeb_fra\": 67,\n        \"koyeb_par\": 116,\n        \"nrt\": 352,\n        \"railway_europe-west4-drams3a\": 94,\n        \"sin\": 413,\n        \"railway_asia-southeast1-eqsg3a\": 400,\n        \"ams\": 84,\n        \"iad\": 239,\n        \"railway_us-west2\": 370,\n        \"koyeb_sin\": 408,\n        \"koyeb_sfo\": 260,\n        \"lax\": 212,\n        \"koyeb_was\": 244,\n        \"koyeb_tyo\": 344,\n        \"railway_us-east4-eqdc4a\": 251,\n        \"fra\": 67,\n        \"cdg\": 114\n      },\n      {\n        \"timestamp\": \"2025-10-17T14:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 92,\n        \"nrt\": 330,\n        \"koyeb_fra\": 68,\n        \"sin\": 432,\n        \"koyeb_par\": 117,\n        \"iad\": 238,\n        \"ams\": 86,\n        \"koyeb_tyo\": 354,\n        \"lax\": 226,\n        \"railway_us-east4-eqdc4a\": 250,\n        \"koyeb_sfo\": 248,\n        \"cdg\": 115,\n        \"fra\": 64,\n        \"koyeb_sin\": 418,\n        \"railway_us-west2\": 375,\n        \"koyeb_was\": 244,\n        \"railway_asia-southeast1-eqsg3a\": 410\n      },\n      {\n        \"timestamp\": \"2025-10-17T15:00:00.000Z\",\n        \"koyeb_tyo\": 536,\n        \"railway_europe-west4-drams3a\": 94,\n        \"railway_us-east4-eqdc4a\": 248,\n        \"cdg\": 115,\n        \"fra\": 64,\n        \"iad\": 245,\n        \"ams\": 85,\n        \"koyeb_was\": 248,\n        \"lax\": 364,\n        \"koyeb_sin\": 512,\n        \"koyeb_sfo\": 322,\n        \"railway_us-west2\": 330,\n        \"nrt\": 318,\n        \"sin\": 418,\n        \"railway_asia-southeast1-eqsg3a\": 462,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 65\n      },\n      {\n        \"timestamp\": \"2025-10-17T16:00:00.000Z\",\n        \"fra\": 64,\n        \"cdg\": 116,\n        \"koyeb_tyo\": 316,\n        \"koyeb_was\": 242,\n        \"lax\": 355,\n        \"koyeb_sfo\": 349,\n        \"koyeb_sin\": 486,\n        \"ams\": 85,\n        \"iad\": 239,\n        \"railway_europe-west4-drams3a\": 94,\n        \"railway_us-east4-eqdc4a\": 247,\n        \"railway_asia-southeast1-eqsg3a\": 474,\n        \"sin\": 474,\n        \"railway_us-west2\": 338,\n        \"nrt\": 536,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 67\n      },\n      {\n        \"timestamp\": \"2025-10-17T17:00:00.000Z\",\n        \"cdg\": 114,\n        \"koyeb_tyo\": 304,\n        \"fra\": 66,\n        \"railway_us-east4-eqdc4a\": 252,\n        \"lax\": 328,\n        \"koyeb_was\": 238,\n        \"koyeb_sin\": 442,\n        \"railway_us-west2\": 450,\n        \"iad\": 241,\n        \"railway_asia-southeast1-eqsg3a\": 460,\n        \"koyeb_sfo\": 322,\n        \"ams\": 86,\n        \"sin\": 456,\n        \"railway_europe-west4-drams3a\": 98,\n        \"nrt\": 314,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 66\n      },\n      {\n        \"timestamp\": \"2025-10-17T18:00:00.000Z\",\n        \"nrt\": 311,\n        \"koyeb_was\": 243,\n        \"koyeb_sfo\": 328,\n        \"koyeb_sin\": 400,\n        \"sin\": 388,\n        \"railway_us-east4-eqdc4a\": 248,\n        \"koyeb_tyo\": 315,\n        \"railway_asia-southeast1-eqsg3a\": 404,\n        \"railway_europe-west4-drams3a\": 92,\n        \"railway_us-west2\": 302,\n        \"koyeb_par\": 116,\n        \"fra\": 66,\n        \"koyeb_fra\": 66,\n        \"cdg\": 116,\n        \"ams\": 85,\n        \"iad\": 240,\n        \"lax\": 333\n      },\n      {\n        \"timestamp\": \"2025-10-17T19:00:00.000Z\",\n        \"koyeb_tyo\": 558,\n        \"railway_us-west2\": 369,\n        \"ams\": 85,\n        \"iad\": 156,\n        \"lax\": 309,\n        \"railway_europe-west4-drams3a\": 91,\n        \"fra\": 64,\n        \"koyeb_was\": 161,\n        \"cdg\": 113,\n        \"koyeb_sfo\": 333,\n        \"koyeb_sin\": 395,\n        \"nrt\": 305,\n        \"railway_asia-southeast1-eqsg3a\": 393,\n        \"sin\": 406,\n        \"railway_us-east4-eqdc4a\": 161,\n        \"koyeb_par\": 118,\n        \"koyeb_fra\": 68\n      },\n      {\n        \"timestamp\": \"2025-10-17T20:00:00.000Z\",\n        \"lax\": 311,\n        \"iad\": 152,\n        \"ams\": 84,\n        \"cdg\": 115,\n        \"fra\": 66,\n        \"koyeb_par\": 114,\n        \"railway_europe-west4-drams3a\": 94,\n        \"koyeb_fra\": 66,\n        \"railway_asia-southeast1-eqsg3a\": 388,\n        \"koyeb_tyo\": 318,\n        \"railway_us-east4-eqdc4a\": 226,\n        \"railway_us-west2\": 364,\n        \"sin\": 382,\n        \"koyeb_was\": 154,\n        \"nrt\": 547,\n        \"koyeb_sin\": 376,\n        \"koyeb_sfo\": 354\n      },\n      {\n        \"timestamp\": \"2025-10-17T21:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 86,\n        \"koyeb_par\": 116,\n        \"sin\": 382,\n        \"railway_us-east4-eqdc4a\": 171,\n        \"koyeb_fra\": 64,\n        \"nrt\": 549,\n        \"railway_us-west2\": 376,\n        \"lax\": 348,\n        \"koyeb_tyo\": 311,\n        \"ams\": 84,\n        \"iad\": 160,\n        \"koyeb_was\": 162,\n        \"fra\": 65,\n        \"cdg\": 114,\n        \"railway_asia-southeast1-eqsg3a\": 382,\n        \"koyeb_sin\": 376,\n        \"koyeb_sfo\": 345\n      },\n      {\n        \"timestamp\": \"2025-10-17T22:00:00.000Z\",\n        \"lax\": 370,\n        \"railway_us-east4-eqdc4a\": 245,\n        \"iad\": 238,\n        \"koyeb_par\": 116,\n        \"railway_asia-southeast1-eqsg3a\": 386,\n        \"ams\": 83,\n        \"koyeb_fra\": 63,\n        \"cdg\": 112,\n        \"fra\": 62,\n        \"railway_us-west2\": 383,\n        \"koyeb_was\": 239,\n        \"koyeb_sin\": 376,\n        \"koyeb_sfo\": 398,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_tyo\": 548,\n        \"sin\": 388,\n        \"nrt\": 318\n      },\n      {\n        \"timestamp\": \"2025-10-17T23:00:00.000Z\",\n        \"koyeb_sin\": 386,\n        \"cdg\": 114,\n        \"koyeb_sfo\": 308,\n        \"fra\": 64,\n        \"koyeb_was\": 243,\n        \"railway_europe-west4-drams3a\": 87,\n        \"lax\": 354,\n        \"iad\": 232,\n        \"railway_us-west2\": 350,\n        \"koyeb_tyo\": 312,\n        \"ams\": 81,\n        \"sin\": 386,\n        \"koyeb_fra\": 62,\n        \"koyeb_par\": 116,\n        \"nrt\": 310,\n        \"railway_asia-southeast1-eqsg3a\": 390,\n        \"railway_us-east4-eqdc4a\": 244\n      },\n      {\n        \"timestamp\": \"2025-10-18T00:00:00.000Z\",\n        \"cdg\": 114,\n        \"fra\": 64,\n        \"koyeb_tyo\": 315,\n        \"railway_us-east4-eqdc4a\": 240,\n        \"railway_asia-southeast1-eqsg3a\": 374,\n        \"railway_us-west2\": 242,\n        \"lax\": 338,\n        \"koyeb_was\": 239,\n        \"koyeb_sin\": 382,\n        \"koyeb_sfo\": 274,\n        \"iad\": 238,\n        \"ams\": 83,\n        \"sin\": 378,\n        \"nrt\": 540,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_par\": 115,\n        \"koyeb_fra\": 64\n      },\n      {\n        \"timestamp\": \"2025-10-18T01:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 383,\n        \"railway_us-west2\": 378,\n        \"fra\": 64,\n        \"cdg\": 96,\n        \"koyeb_tyo\": 533,\n        \"koyeb_sin\": 366,\n        \"koyeb_sfo\": 346,\n        \"koyeb_was\": 240,\n        \"ams\": 79,\n        \"iad\": 237,\n        \"lax\": 224,\n        \"nrt\": 312,\n        \"sin\": 390,\n        \"koyeb_fra\": 62,\n        \"koyeb_par\": 114,\n        \"railway_europe-west4-drams3a\": 90,\n        \"railway_us-east4-eqdc4a\": 242\n      },\n      {\n        \"timestamp\": \"2025-10-18T02:00:00.000Z\",\n        \"railway_us-west2\": 334,\n        \"railway_europe-west4-drams3a\": 90,\n        \"nrt\": 315,\n        \"koyeb_sin\": 304,\n        \"koyeb_sfo\": 404,\n        \"koyeb_was\": 245,\n        \"sin\": 388,\n        \"koyeb_tyo\": 318,\n        \"railway_us-east4-eqdc4a\": 250,\n        \"koyeb_fra\": 62,\n        \"cdg\": 110,\n        \"koyeb_par\": 114,\n        \"fra\": 63,\n        \"iad\": 256,\n        \"ams\": 79,\n        \"lax\": 380,\n        \"railway_asia-southeast1-eqsg3a\": 388\n      },\n      {\n        \"timestamp\": \"2025-10-18T03:00:00.000Z\",\n        \"fra\": 62,\n        \"cdg\": 110,\n        \"railway_asia-southeast1-eqsg3a\": 390,\n        \"lax\": 368,\n        \"ams\": 70,\n        \"koyeb_par\": 114,\n        \"iad\": 214,\n        \"railway_us-west2\": 383,\n        \"koyeb_fra\": 61,\n        \"koyeb_tyo\": 313,\n        \"sin\": 389,\n        \"nrt\": 312,\n        \"railway_us-east4-eqdc4a\": 260,\n        \"railway_europe-west4-drams3a\": 82,\n        \"koyeb_was\": 264,\n        \"koyeb_sin\": 382,\n        \"koyeb_sfo\": 389\n      },\n      {\n        \"timestamp\": \"2025-10-18T04:00:00.000Z\",\n        \"cdg\": 114,\n        \"fra\": 64,\n        \"koyeb_tyo\": 445,\n        \"railway_europe-west4-drams3a\": 90,\n        \"lax\": 224,\n        \"koyeb_sin\": 390,\n        \"railway_us-west2\": 226,\n        \"koyeb_sfo\": 266,\n        \"koyeb_was\": 260,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"iad\": 257,\n        \"ams\": 82,\n        \"sin\": 390,\n        \"nrt\": 323,\n        \"railway_asia-southeast1-eqsg3a\": 396,\n        \"koyeb_fra\": 63,\n        \"koyeb_par\": 114\n      },\n      {\n        \"timestamp\": \"2025-10-18T05:00:00.000Z\",\n        \"sin\": 418,\n        \"nrt\": 560,\n        \"koyeb_tyo\": 321,\n        \"koyeb_sin\": 390,\n        \"koyeb_sfo\": 258,\n        \"koyeb_was\": 258,\n        \"railway_europe-west4-drams3a\": 86,\n        \"railway_asia-southeast1-eqsg3a\": 394,\n        \"fra\": 63,\n        \"cdg\": 113,\n        \"railway_us-west2\": 377,\n        \"railway_us-east4-eqdc4a\": 266,\n        \"koyeb_fra\": 63,\n        \"koyeb_par\": 115,\n        \"lax\": 220,\n        \"ams\": 83,\n        \"iad\": 250\n      },\n      {\n        \"timestamp\": \"2025-10-18T06:00:00.000Z\",\n        \"railway_us-west2\": 225,\n        \"cdg\": 113,\n        \"fra\": 61,\n        \"koyeb_was\": 258,\n        \"railway_europe-west4-drams3a\": 90,\n        \"railway_us-east4-eqdc4a\": 263,\n        \"koyeb_sfo\": 252,\n        \"koyeb_sin\": 403,\n        \"koyeb_tyo\": 544,\n        \"iad\": 251,\n        \"ams\": 75,\n        \"lax\": 214,\n        \"nrt\": 320,\n        \"sin\": 409,\n        \"koyeb_par\": 114,\n        \"koyeb_fra\": 64,\n        \"railway_asia-southeast1-eqsg3a\": 396\n      },\n      {\n        \"timestamp\": \"2025-10-18T07:00:00.000Z\",\n        \"koyeb_tyo\": 545,\n        \"nrt\": 320,\n        \"railway_asia-southeast1-eqsg3a\": 396,\n        \"koyeb_was\": 208,\n        \"koyeb_sfo\": 248,\n        \"koyeb_sin\": 417,\n        \"sin\": 407,\n        \"railway_europe-west4-drams3a\": 92,\n        \"iad\": 256,\n        \"ams\": 79,\n        \"lax\": 206,\n        \"railway_us-west2\": 366,\n        \"railway_us-east4-eqdc4a\": 252,\n        \"koyeb_par\": 116,\n        \"cdg\": 112,\n        \"fra\": 64,\n        \"koyeb_fra\": 63\n      },\n      {\n        \"timestamp\": \"2025-10-18T08:00:00.000Z\",\n        \"sin\": 396,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"koyeb_was\": 253,\n        \"nrt\": 558,\n        \"koyeb_sfo\": 254,\n        \"koyeb_sin\": 404,\n        \"railway_europe-west4-drams3a\": 93,\n        \"koyeb_tyo\": 548,\n        \"fra\": 64,\n        \"cdg\": 114,\n        \"railway_asia-southeast1-eqsg3a\": 395,\n        \"koyeb_par\": 115,\n        \"railway_us-west2\": 358,\n        \"koyeb_fra\": 63,\n        \"lax\": 202,\n        \"ams\": 81,\n        \"iad\": 254\n      },\n      {\n        \"timestamp\": \"2025-10-18T09:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 258,\n        \"koyeb_was\": 252,\n        \"koyeb_sin\": 426,\n        \"sin\": 388,\n        \"koyeb_sfo\": 248,\n        \"nrt\": 318,\n        \"railway_asia-southeast1-eqsg3a\": 386,\n        \"koyeb_tyo\": 324,\n        \"railway_us-west2\": 370,\n        \"koyeb_par\": 116,\n        \"cdg\": 114,\n        \"fra\": 64,\n        \"koyeb_fra\": 64,\n        \"railway_europe-west4-drams3a\": 90,\n        \"lax\": 206,\n        \"iad\": 256,\n        \"ams\": 84\n      },\n      {\n        \"timestamp\": \"2025-10-18T10:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 400,\n        \"koyeb_tyo\": 326,\n        \"fra\": 65,\n        \"cdg\": 112,\n        \"ams\": 83,\n        \"iad\": 258,\n        \"koyeb_sfo\": 227,\n        \"koyeb_sin\": 404,\n        \"lax\": 200,\n        \"koyeb_was\": 254,\n        \"railway_us-east4-eqdc4a\": 261,\n        \"nrt\": 334,\n        \"sin\": 400,\n        \"railway_us-west2\": 366,\n        \"koyeb_fra\": 66,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_par\": 117\n      },\n      {\n        \"timestamp\": \"2025-10-18T11:00:00.000Z\",\n        \"koyeb_sfo\": 238,\n        \"koyeb_sin\": 394,\n        \"koyeb_was\": 165,\n        \"railway_europe-west4-drams3a\": 92,\n        \"nrt\": 332,\n        \"railway_us-west2\": 370,\n        \"koyeb_tyo\": 330,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"sin\": 406,\n        \"ams\": 80,\n        \"iad\": 256,\n        \"koyeb_fra\": 64,\n        \"railway_asia-southeast1-eqsg3a\": 397,\n        \"lax\": 209,\n        \"koyeb_par\": 116,\n        \"fra\": 65,\n        \"cdg\": 114\n      },\n      {\n        \"timestamp\": \"2025-10-18T12:00:00.000Z\",\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 65,\n        \"railway_europe-west4-drams3a\": 94,\n        \"sin\": 396,\n        \"nrt\": 334,\n        \"koyeb_was\": 260,\n        \"railway_us-west2\": 368,\n        \"lax\": 202,\n        \"koyeb_sin\": 404,\n        \"koyeb_sfo\": 241,\n        \"iad\": 251,\n        \"ams\": 84,\n        \"railway_asia-southeast1-eqsg3a\": 390,\n        \"cdg\": 114,\n        \"fra\": 63,\n        \"railway_us-east4-eqdc4a\": 260,\n        \"koyeb_tyo\": 548\n      },\n      {\n        \"timestamp\": \"2025-10-18T13:00:00.000Z\",\n        \"lax\": 209,\n        \"railway_europe-west4-drams3a\": 94,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"ams\": 86,\n        \"koyeb_par\": 116,\n        \"iad\": 209,\n        \"koyeb_fra\": 67,\n        \"fra\": 64,\n        \"cdg\": 114,\n        \"railway_us-west2\": 368,\n        \"koyeb_was\": 259,\n        \"koyeb_sin\": 419,\n        \"koyeb_sfo\": 240,\n        \"koyeb_tyo\": 322,\n        \"sin\": 520,\n        \"railway_asia-southeast1-eqsg3a\": 413,\n        \"nrt\": 545\n      },\n      {\n        \"timestamp\": \"2025-10-18T14:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 444,\n        \"sin\": 436,\n        \"koyeb_par\": 117,\n        \"railway_us-east4-eqdc4a\": 260,\n        \"koyeb_fra\": 66,\n        \"nrt\": 540,\n        \"railway_us-west2\": 374,\n        \"lax\": 218,\n        \"koyeb_tyo\": 338,\n        \"iad\": 254,\n        \"ams\": 84,\n        \"koyeb_was\": 255,\n        \"cdg\": 114,\n        \"fra\": 64,\n        \"koyeb_sin\": 422,\n        \"koyeb_sfo\": 248,\n        \"railway_europe-west4-drams3a\": 89\n      },\n      {\n        \"timestamp\": \"2025-10-18T15:00:00.000Z\",\n        \"koyeb_fra\": 64,\n        \"railway_asia-southeast1-eqsg3a\": 443,\n        \"koyeb_par\": 117,\n        \"cdg\": 113,\n        \"fra\": 65,\n        \"railway_us-east4-eqdc4a\": 245,\n        \"iad\": 233,\n        \"railway_us-west2\": 375,\n        \"ams\": 84,\n        \"lax\": 215,\n        \"koyeb_sfo\": 284,\n        \"koyeb_sin\": 448,\n        \"nrt\": 552,\n        \"koyeb_was\": 242,\n        \"sin\": 436,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_tyo\": 324\n      },\n      {\n        \"timestamp\": \"2025-10-18T16:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 92,\n        \"cdg\": 113,\n        \"fra\": 65,\n        \"iad\": 232,\n        \"ams\": 84,\n        \"koyeb_par\": 116,\n        \"lax\": 219,\n        \"koyeb_fra\": 66,\n        \"railway_us-west2\": 375,\n        \"nrt\": 344,\n        \"koyeb_tyo\": 550,\n        \"sin\": 462,\n        \"koyeb_was\": 239,\n        \"railway_asia-southeast1-eqsg3a\": 452,\n        \"koyeb_sfo\": 248,\n        \"railway_us-east4-eqdc4a\": 244,\n        \"koyeb_sin\": 444\n      },\n      {\n        \"timestamp\": \"2025-10-18T17:00:00.000Z\",\n        \"railway_us-west2\": 378,\n        \"nrt\": 544,\n        \"koyeb_sin\": 384,\n        \"koyeb_sfo\": 258,\n        \"koyeb_was\": 238,\n        \"railway_asia-southeast1-eqsg3a\": 394,\n        \"sin\": 402,\n        \"koyeb_tyo\": 547,\n        \"koyeb_fra\": 66,\n        \"railway_us-east4-eqdc4a\": 240,\n        \"fra\": 67,\n        \"cdg\": 94,\n        \"koyeb_par\": 117,\n        \"ams\": 86,\n        \"iad\": 233,\n        \"railway_europe-west4-drams3a\": 93,\n        \"lax\": 228\n      },\n      {\n        \"timestamp\": \"2025-10-18T18:00:00.000Z\",\n        \"railway_us-west2\": 371,\n        \"cdg\": 115,\n        \"fra\": 67,\n        \"railway_europe-west4-drams3a\": 91,\n        \"koyeb_tyo\": 426,\n        \"koyeb_sin\": 380,\n        \"koyeb_sfo\": 259,\n        \"koyeb_was\": 201,\n        \"iad\": 235,\n        \"ams\": 87,\n        \"lax\": 220,\n        \"nrt\": 312,\n        \"sin\": 392,\n        \"koyeb_fra\": 65,\n        \"railway_asia-southeast1-eqsg3a\": 390,\n        \"koyeb_par\": 115,\n        \"railway_us-east4-eqdc4a\": 246\n      },\n      {\n        \"timestamp\": \"2025-10-18T19:00:00.000Z\",\n        \"koyeb_was\": 240,\n        \"koyeb_sfo\": 263,\n        \"koyeb_sin\": 382,\n        \"railway_us-west2\": 377,\n        \"railway_europe-west4-drams3a\": 92,\n        \"sin\": 392,\n        \"nrt\": 552,\n        \"koyeb_tyo\": 310,\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 66,\n        \"lax\": 207,\n        \"iad\": 234,\n        \"ams\": 86,\n        \"cdg\": 115,\n        \"fra\": 66,\n        \"railway_asia-southeast1-eqsg3a\": 284,\n        \"railway_us-east4-eqdc4a\": 245\n      },\n      {\n        \"timestamp\": \"2025-10-18T20:00:00.000Z\",\n        \"koyeb_fra\": 67,\n        \"koyeb_par\": 116,\n        \"railway_asia-southeast1-eqsg3a\": 384,\n        \"railway_us-west2\": 372,\n        \"railway_us-east4-eqdc4a\": 241,\n        \"sin\": 384,\n        \"nrt\": 312,\n        \"koyeb_sfo\": 250,\n        \"koyeb_sin\": 382,\n        \"lax\": 216,\n        \"koyeb_was\": 241,\n        \"ams\": 82,\n        \"iad\": 237,\n        \"railway_europe-west4-drams3a\": 94,\n        \"fra\": 66,\n        \"cdg\": 114,\n        \"koyeb_tyo\": 314\n      },\n      {\n        \"timestamp\": \"2025-10-18T21:00:00.000Z\",\n        \"lax\": 214,\n        \"iad\": 235,\n        \"koyeb_fra\": 66,\n        \"ams\": 82,\n        \"koyeb_par\": 116,\n        \"cdg\": 114,\n        \"fra\": 65,\n        \"railway_asia-southeast1-eqsg3a\": 382,\n        \"railway_us-east4-eqdc4a\": 244,\n        \"railway_europe-west4-drams3a\": 92,\n        \"koyeb_sfo\": 250,\n        \"koyeb_sin\": 380,\n        \"koyeb_was\": 242,\n        \"sin\": 386,\n        \"railway_us-west2\": 374,\n        \"koyeb_tyo\": 316,\n        \"nrt\": 309\n      },\n      {\n        \"timestamp\": \"2025-10-18T22:00:00.000Z\",\n        \"koyeb_was\": 237,\n        \"koyeb_sfo\": 256,\n        \"railway_us-west2\": 293,\n        \"sin\": 384,\n        \"koyeb_sin\": 308,\n        \"railway_asia-southeast1-eqsg3a\": 382,\n        \"nrt\": 326,\n        \"koyeb_tyo\": 547,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 64,\n        \"fra\": 62,\n        \"cdg\": 114,\n        \"railway_us-east4-eqdc4a\": 245,\n        \"lax\": 308,\n        \"railway_europe-west4-drams3a\": 91,\n        \"ams\": 83,\n        \"iad\": 235\n      },\n      {\n        \"timestamp\": \"2025-10-18T23:00:00.000Z\",\n        \"lax\": 222,\n        \"koyeb_tyo\": 548,\n        \"ams\": 80,\n        \"iad\": 234,\n        \"fra\": 62,\n        \"cdg\": 113,\n        \"koyeb_was\": 238,\n        \"koyeb_sfo\": 263,\n        \"koyeb_sin\": 382,\n        \"railway_us-east4-eqdc4a\": 238,\n        \"railway_europe-west4-drams3a\": 90,\n        \"railway_asia-southeast1-eqsg3a\": 306,\n        \"railway_us-west2\": 368,\n        \"koyeb_par\": 114,\n        \"sin\": 384,\n        \"koyeb_fra\": 62,\n        \"nrt\": 308\n      },\n      {\n        \"timestamp\": \"2025-10-19T00:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 250,\n        \"koyeb_tyo\": 318,\n        \"railway_europe-west4-drams3a\": 90,\n        \"iad\": 248,\n        \"ams\": 79,\n        \"lax\": 217,\n        \"koyeb_was\": 243,\n        \"cdg\": 114,\n        \"fra\": 63,\n        \"koyeb_sin\": 382,\n        \"koyeb_sfo\": 263,\n        \"railway_us-west2\": 372,\n        \"nrt\": 550,\n        \"koyeb_par\": 115,\n        \"sin\": 386,\n        \"railway_asia-southeast1-eqsg3a\": 382,\n        \"koyeb_fra\": 61\n      },\n      {\n        \"timestamp\": \"2025-10-19T01:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 74,\n        \"nrt\": 315,\n        \"sin\": 390,\n        \"koyeb_fra\": 63,\n        \"koyeb_par\": 116,\n        \"koyeb_tyo\": 320,\n        \"cdg\": 113,\n        \"fra\": 61,\n        \"railway_us-east4-eqdc4a\": 264,\n        \"iad\": 254,\n        \"ams\": 79,\n        \"koyeb_sfo\": 258,\n        \"lax\": 221,\n        \"koyeb_sin\": 384,\n        \"railway_asia-southeast1-eqsg3a\": 312,\n        \"railway_us-west2\": 375,\n        \"koyeb_was\": 260\n      },\n      {\n        \"timestamp\": \"2025-10-19T02:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 264,\n        \"iad\": 253,\n        \"ams\": 76,\n        \"lax\": 234,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_fra\": 61,\n        \"koyeb_par\": 115,\n        \"cdg\": 112,\n        \"fra\": 62,\n        \"koyeb_tyo\": 316,\n        \"nrt\": 434,\n        \"koyeb_sfo\": 260,\n        \"koyeb_sin\": 386,\n        \"railway_asia-southeast1-eqsg3a\": 382,\n        \"koyeb_was\": 256,\n        \"railway_us-west2\": 232,\n        \"sin\": 385\n      },\n      {\n        \"timestamp\": \"2025-10-19T03:00:00.000Z\",\n        \"nrt\": 548,\n        \"koyeb_tyo\": 310,\n        \"railway_europe-west4-drams3a\": 86,\n        \"sin\": 390,\n        \"koyeb_was\": 260,\n        \"koyeb_sin\": 308,\n        \"koyeb_sfo\": 294,\n        \"railway_us-west2\": 378,\n        \"cdg\": 112,\n        \"fra\": 63,\n        \"koyeb_par\": 115,\n        \"iad\": 252,\n        \"ams\": 82,\n        \"railway_asia-southeast1-eqsg3a\": 390,\n        \"railway_us-east4-eqdc4a\": 167,\n        \"koyeb_fra\": 62,\n        \"lax\": 223\n      },\n      {\n        \"timestamp\": \"2025-10-19T04:00:00.000Z\",\n        \"koyeb_sin\": 388,\n        \"koyeb_sfo\": 258,\n        \"koyeb_was\": 264,\n        \"sin\": 392,\n        \"nrt\": 438,\n        \"railway_asia-southeast1-eqsg3a\": 390,\n        \"koyeb_tyo\": 321,\n        \"koyeb_fra\": 62,\n        \"koyeb_par\": 115,\n        \"fra\": 63,\n        \"cdg\": 114,\n        \"railway_us-east4-eqdc4a\": 170,\n        \"lax\": 228,\n        \"railway_us-west2\": 374,\n        \"railway_europe-west4-drams3a\": 90,\n        \"ams\": 82,\n        \"iad\": 254\n      },\n      {\n        \"timestamp\": \"2025-10-19T05:00:00.000Z\",\n        \"koyeb_par\": 115,\n        \"koyeb_fra\": 62,\n        \"lax\": 222,\n        \"railway_asia-southeast1-eqsg3a\": 396,\n        \"ams\": 84,\n        \"iad\": 209,\n        \"fra\": 62,\n        \"cdg\": 113,\n        \"railway_us-east4-eqdc4a\": 260,\n        \"koyeb_was\": 263,\n        \"koyeb_sfo\": 258,\n        \"koyeb_sin\": 394,\n        \"railway_us-west2\": 378,\n        \"railway_europe-west4-drams3a\": 92,\n        \"sin\": 390,\n        \"nrt\": 328,\n        \"koyeb_tyo\": 328\n      },\n      {\n        \"timestamp\": \"2025-10-19T06:00:00.000Z\",\n        \"sin\": 408,\n        \"railway_us-east4-eqdc4a\": 271,\n        \"nrt\": 322,\n        \"railway_us-west2\": 373,\n        \"koyeb_tyo\": 554,\n        \"koyeb_sin\": 392,\n        \"koyeb_sfo\": 250,\n        \"koyeb_was\": 260,\n        \"railway_europe-west4-drams3a\": 91,\n        \"fra\": 63,\n        \"cdg\": 114,\n        \"railway_asia-southeast1-eqsg3a\": 404,\n        \"koyeb_fra\": 63,\n        \"koyeb_par\": 116,\n        \"lax\": 216,\n        \"ams\": 82,\n        \"iad\": 252\n      },\n      {\n        \"timestamp\": \"2025-10-19T07:00:00.000Z\",\n        \"ams\": 83,\n        \"iad\": 252,\n        \"lax\": 214,\n        \"koyeb_par\": 116,\n        \"railway_asia-southeast1-eqsg3a\": 390,\n        \"fra\": 63,\n        \"cdg\": 114,\n        \"koyeb_fra\": 62,\n        \"koyeb_tyo\": 326,\n        \"railway_europe-west4-drams3a\": 92,\n        \"railway_us-west2\": 372,\n        \"nrt\": 318,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"koyeb_was\": 264,\n        \"koyeb_sin\": 400,\n        \"sin\": 394,\n        \"koyeb_sfo\": 252\n      },\n      {\n        \"timestamp\": \"2025-10-19T08:00:00.000Z\",\n        \"ams\": 84,\n        \"iad\": 250,\n        \"lax\": 211,\n        \"koyeb_tyo\": 328,\n        \"railway_asia-southeast1-eqsg3a\": 396,\n        \"koyeb_was\": 254,\n        \"koyeb_sfo\": 244,\n        \"koyeb_sin\": 401,\n        \"fra\": 65,\n        \"cdg\": 114,\n        \"railway_us-west2\": 364,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"koyeb_par\": 116,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_fra\": 65,\n        \"nrt\": 334,\n        \"sin\": 398\n      },\n      {\n        \"timestamp\": \"2025-10-19T09:00:00.000Z\",\n        \"sin\": 406,\n        \"nrt\": 307,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 65,\n        \"railway_europe-west4-drams3a\": 92,\n        \"railway_us-east4-eqdc4a\": 252,\n        \"railway_asia-southeast1-eqsg3a\": 402,\n        \"fra\": 64,\n        \"cdg\": 114,\n        \"railway_us-west2\": 362,\n        \"koyeb_tyo\": 318,\n        \"koyeb_was\": 256,\n        \"lax\": 205,\n        \"koyeb_sfo\": 254,\n        \"koyeb_sin\": 398,\n        \"ams\": 85,\n        \"iad\": 260\n      },\n      {\n        \"timestamp\": \"2025-10-19T10:00:00.000Z\",\n        \"koyeb_sfo\": 244,\n        \"koyeb_sin\": 244,\n        \"koyeb_was\": 253,\n        \"railway_us-west2\": 354,\n        \"sin\": 394,\n        \"railway_europe-west4-drams3a\": 92,\n        \"nrt\": 322,\n        \"koyeb_tyo\": 326,\n        \"railway_asia-southeast1-eqsg3a\": 401,\n        \"koyeb_fra\": 64,\n        \"koyeb_par\": 117,\n        \"lax\": 200,\n        \"ams\": 84,\n        \"iad\": 256,\n        \"fra\": 66,\n        \"cdg\": 114,\n        \"railway_us-east4-eqdc4a\": 264\n      },\n      {\n        \"timestamp\": \"2025-10-19T11:00:00.000Z\",\n        \"koyeb_tyo\": 569,\n        \"sin\": 402,\n        \"nrt\": 336,\n        \"railway_europe-west4-drams3a\": 96,\n        \"koyeb_sin\": 390,\n        \"railway_us-east4-eqdc4a\": 256,\n        \"koyeb_sfo\": 245,\n        \"koyeb_was\": 262,\n        \"fra\": 65,\n        \"cdg\": 114,\n        \"railway_asia-southeast1-eqsg3a\": 359,\n        \"lax\": 209,\n        \"ams\": 76,\n        \"iad\": 162,\n        \"koyeb_fra\": 66,\n        \"koyeb_par\": 117,\n        \"railway_us-west2\": 368\n      },\n      {\n        \"timestamp\": \"2025-10-19T12:00:00.000Z\",\n        \"sin\": 394,\n        \"railway_europe-west4-drams3a\": 92,\n        \"railway_us-west2\": 366,\n        \"nrt\": 338,\n        \"koyeb_sfo\": 244,\n        \"koyeb_sin\": 396,\n        \"koyeb_was\": 254,\n        \"koyeb_tyo\": 440,\n        \"cdg\": 94,\n        \"fra\": 66,\n        \"koyeb_fra\": 66,\n        \"koyeb_par\": 117,\n        \"railway_us-east4-eqdc4a\": 264,\n        \"lax\": 208,\n        \"railway_asia-southeast1-eqsg3a\": 399,\n        \"iad\": 258,\n        \"ams\": 84\n      },\n      {\n        \"timestamp\": \"2025-10-19T13:00:00.000Z\",\n        \"iad\": 164,\n        \"ams\": 84,\n        \"koyeb_fra\": 66,\n        \"lax\": 352,\n        \"koyeb_par\": 119,\n        \"railway_asia-southeast1-eqsg3a\": 398,\n        \"cdg\": 116,\n        \"fra\": 64,\n        \"koyeb_sfo\": 245,\n        \"koyeb_sin\": 406,\n        \"railway_europe-west4-drams3a\": 86,\n        \"koyeb_was\": 260,\n        \"nrt\": 326,\n        \"railway_us-west2\": 360,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"koyeb_tyo\": 336,\n        \"sin\": 446\n      },\n      {\n        \"timestamp\": \"2025-10-19T14:00:00.000Z\",\n        \"koyeb_tyo\": 336,\n        \"nrt\": 558,\n        \"railway_europe-west4-drams3a\": 94,\n        \"sin\": 422,\n        \"railway_us-west2\": 370,\n        \"koyeb_was\": 258,\n        \"koyeb_sin\": 398,\n        \"koyeb_sfo\": 251,\n        \"railway_us-east4-eqdc4a\": 240,\n        \"cdg\": 114,\n        \"fra\": 66,\n        \"iad\": 152,\n        \"ams\": 85,\n        \"koyeb_par\": 119,\n        \"railway_asia-southeast1-eqsg3a\": 412,\n        \"lax\": 206,\n        \"koyeb_fra\": 65\n      },\n      {\n        \"timestamp\": \"2025-10-19T15:00:00.000Z\",\n        \"iad\": 235,\n        \"ams\": 83,\n        \"railway_europe-west4-drams3a\": 94,\n        \"lax\": 201,\n        \"railway_us-west2\": 370,\n        \"railway_us-east4-eqdc4a\": 241,\n        \"koyeb_fra\": 66,\n        \"cdg\": 116,\n        \"fra\": 64,\n        \"koyeb_par\": 119,\n        \"koyeb_tyo\": 320,\n        \"nrt\": 320,\n        \"koyeb_sfo\": 381,\n        \"koyeb_sin\": 400,\n        \"railway_asia-southeast1-eqsg3a\": 434,\n        \"sin\": 490,\n        \"koyeb_was\": 241\n      },\n      {\n        \"timestamp\": \"2025-10-19T16:00:00.000Z\",\n        \"koyeb_was\": 238,\n        \"koyeb_sin\": 432,\n        \"koyeb_sfo\": 253,\n        \"railway_europe-west4-drams3a\": 94,\n        \"railway_us-east4-eqdc4a\": 246,\n        \"nrt\": 544,\n        \"koyeb_tyo\": 314,\n        \"sin\": 451,\n        \"koyeb_par\": 119,\n        \"iad\": 236,\n        \"ams\": 80,\n        \"railway_us-west2\": 378,\n        \"koyeb_fra\": 69,\n        \"lax\": 348,\n        \"railway_asia-southeast1-eqsg3a\": 428,\n        \"cdg\": 80,\n        \"fra\": 66\n      },\n      {\n        \"timestamp\": \"2025-10-19T17:00:00.000Z\",\n        \"koyeb_fra\": 68,\n        \"koyeb_par\": 117,\n        \"cdg\": 114,\n        \"railway_europe-west4-drams3a\": 94,\n        \"fra\": 68,\n        \"iad\": 236,\n        \"ams\": 83,\n        \"lax\": 354,\n        \"koyeb_sfo\": 257,\n        \"nrt\": 310,\n        \"koyeb_sin\": 439,\n        \"railway_us-east4-eqdc4a\": 242,\n        \"koyeb_was\": 242,\n        \"sin\": 430,\n        \"railway_asia-southeast1-eqsg3a\": 434,\n        \"railway_us-west2\": 381,\n        \"koyeb_tyo\": 542\n      },\n      {\n        \"timestamp\": \"2025-10-19T18:00:00.000Z\",\n        \"lax\": 280,\n        \"koyeb_tyo\": 316,\n        \"iad\": 234,\n        \"ams\": 84,\n        \"cdg\": 115,\n        \"koyeb_sin\": 427,\n        \"fra\": 68,\n        \"koyeb_sfo\": 249,\n        \"koyeb_was\": 238,\n        \"railway_asia-southeast1-eqsg3a\": 420,\n        \"railway_europe-west4-drams3a\": 93,\n        \"railway_us-west2\": 379,\n        \"sin\": 422,\n        \"koyeb_fra\": 67,\n        \"koyeb_par\": 119,\n        \"nrt\": 307,\n        \"railway_us-east4-eqdc4a\": 244\n      },\n      {\n        \"timestamp\": \"2025-10-19T19:00:00.000Z\",\n        \"sin\": 404,\n        \"nrt\": 312,\n        \"railway_us-east4-eqdc4a\": 238,\n        \"koyeb_par\": 119,\n        \"koyeb_fra\": 68,\n        \"railway_asia-southeast1-eqsg3a\": 388,\n        \"cdg\": 114,\n        \"railway_europe-west4-drams3a\": 94,\n        \"fra\": 67,\n        \"koyeb_tyo\": 551,\n        \"railway_us-west2\": 378,\n        \"lax\": 352,\n        \"koyeb_was\": 239,\n        \"koyeb_sfo\": 246,\n        \"koyeb_sin\": 410,\n        \"iad\": 234,\n        \"ams\": 87\n      },\n      {\n        \"timestamp\": \"2025-10-19T20:00:00.000Z\",\n        \"fra\": 67,\n        \"railway_asia-southeast1-eqsg3a\": 320,\n        \"cdg\": 116,\n        \"koyeb_tyo\": 310,\n        \"lax\": 208,\n        \"koyeb_sin\": 236,\n        \"koyeb_sfo\": 384,\n        \"ams\": 84,\n        \"koyeb_was\": 239,\n        \"iad\": 234,\n        \"sin\": 400,\n        \"nrt\": 532,\n        \"railway_us-west2\": 376,\n        \"railway_us-east4-eqdc4a\": 244,\n        \"koyeb_fra\": 66,\n        \"railway_europe-west4-drams3a\": 95,\n        \"koyeb_par\": 116\n      },\n      {\n        \"timestamp\": \"2025-10-19T21:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 391,\n        \"railway_us-east4-eqdc4a\": 247,\n        \"sin\": 388,\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 66,\n        \"nrt\": 309,\n        \"lax\": 206,\n        \"koyeb_tyo\": 306,\n        \"ams\": 83,\n        \"iad\": 204,\n        \"railway_us-west2\": 378,\n        \"fra\": 64,\n        \"cdg\": 114,\n        \"koyeb_was\": 243,\n        \"koyeb_sfo\": 385,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_sin\": 379\n      },\n      {\n        \"timestamp\": \"2025-10-19T22:00:00.000Z\",\n        \"fra\": 62,\n        \"cdg\": 113,\n        \"koyeb_sin\": 369,\n        \"koyeb_sfo\": 386,\n        \"railway_us-west2\": 374,\n        \"koyeb_was\": 160,\n        \"lax\": 206,\n        \"railway_europe-west4-drams3a\": 89,\n        \"koyeb_tyo\": 310,\n        \"ams\": 80,\n        \"iad\": 234,\n        \"railway_us-east4-eqdc4a\": 244,\n        \"sin\": 385,\n        \"railway_asia-southeast1-eqsg3a\": 378,\n        \"koyeb_fra\": 65,\n        \"koyeb_par\": 116,\n        \"nrt\": 308\n      },\n      {\n        \"timestamp\": \"2025-10-19T23:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 386,\n        \"koyeb_fra\": 63,\n        \"koyeb_par\": 116,\n        \"nrt\": 306,\n        \"railway_us-east4-eqdc4a\": 180,\n        \"sin\": 384,\n        \"ams\": 82,\n        \"iad\": 233,\n        \"koyeb_sin\": 373,\n        \"koyeb_sfo\": 299,\n        \"lax\": 210,\n        \"koyeb_was\": 218,\n        \"railway_us-west2\": 365,\n        \"koyeb_tyo\": 438,\n        \"fra\": 62,\n        \"cdg\": 98,\n        \"railway_europe-west4-drams3a\": 88\n      },\n      {\n        \"timestamp\": \"2025-10-20T00:00:00.000Z\",\n        \"koyeb_sfo\": 388,\n        \"koyeb_sin\": 378,\n        \"koyeb_was\": 242,\n        \"railway_us-west2\": 245,\n        \"railway_asia-southeast1-eqsg3a\": 376,\n        \"nrt\": 316,\n        \"sin\": 372,\n        \"koyeb_tyo\": 314,\n        \"koyeb_fra\": 62,\n        \"ams\": 80,\n        \"iad\": 236,\n        \"koyeb_par\": 114,\n        \"railway_europe-west4-drams3a\": 90,\n        \"lax\": 211,\n        \"railway_us-east4-eqdc4a\": 246,\n        \"fra\": 62,\n        \"cdg\": 113\n      },\n      {\n        \"timestamp\": \"2025-10-20T01:00:00.000Z\",\n        \"fra\": 63,\n        \"cdg\": 112,\n        \"koyeb_par\": 114,\n        \"koyeb_fra\": 63,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"railway_europe-west4-drams3a\": 92,\n        \"lax\": 214,\n        \"ams\": 79,\n        \"iad\": 254,\n        \"railway_us-west2\": 258,\n        \"sin\": 371,\n        \"koyeb_was\": 256,\n        \"nrt\": 314,\n        \"koyeb_sfo\": 267,\n        \"railway_asia-southeast1-eqsg3a\": 377,\n        \"koyeb_sin\": 378,\n        \"koyeb_tyo\": 314\n      },\n      {\n        \"timestamp\": \"2025-10-20T02:00:00.000Z\",\n        \"lax\": 347,\n        \"koyeb_tyo\": 439,\n        \"ams\": 78,\n        \"iad\": 260,\n        \"railway_us-west2\": 245,\n        \"fra\": 61,\n        \"railway_europe-west4-drams3a\": 84,\n        \"cdg\": 113,\n        \"koyeb_sfo\": 250,\n        \"koyeb_sin\": 382,\n        \"koyeb_was\": 260,\n        \"railway_us-east4-eqdc4a\": 265,\n        \"railway_asia-southeast1-eqsg3a\": 381,\n        \"sin\": 384,\n        \"koyeb_fra\": 62,\n        \"koyeb_par\": 114,\n        \"nrt\": 322\n      },\n      {\n        \"timestamp\": \"2025-10-20T03:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 388,\n        \"nrt\": 318,\n        \"sin\": 389,\n        \"koyeb_fra\": 62,\n        \"koyeb_par\": 114,\n        \"railway_us-east4-eqdc4a\": 256,\n        \"koyeb_tyo\": 548,\n        \"fra\": 62,\n        \"cdg\": 112,\n        \"ams\": 81,\n        \"iad\": 256,\n        \"railway_us-west2\": 248,\n        \"koyeb_sfo\": 240,\n        \"railway_europe-west4-drams3a\": 88,\n        \"lax\": 207,\n        \"koyeb_sin\": 387,\n        \"koyeb_was\": 266\n      },\n      {\n        \"timestamp\": \"2025-10-20T04:00:00.000Z\",\n        \"railway_us-west2\": 252,\n        \"nrt\": 556,\n        \"sin\": 384,\n        \"koyeb_fra\": 62,\n        \"railway_us-east4-eqdc4a\": 164,\n        \"koyeb_par\": 116,\n        \"railway_asia-southeast1-eqsg3a\": 385,\n        \"cdg\": 100,\n        \"koyeb_sin\": 386,\n        \"fra\": 63,\n        \"koyeb_sfo\": 252,\n        \"koyeb_was\": 262,\n        \"railway_europe-west4-drams3a\": 92,\n        \"koyeb_tyo\": 552,\n        \"iad\": 156,\n        \"ams\": 82,\n        \"lax\": 210\n      },\n      {\n        \"timestamp\": \"2025-10-20T05:00:00.000Z\",\n        \"koyeb_fra\": 63,\n        \"nrt\": 318,\n        \"koyeb_par\": 114,\n        \"railway_asia-southeast1-eqsg3a\": 387,\n        \"sin\": 390,\n        \"railway_us-east4-eqdc4a\": 263,\n        \"iad\": 250,\n        \"ams\": 80,\n        \"railway_europe-west4-drams3a\": 94,\n        \"lax\": 220,\n        \"koyeb_tyo\": 316,\n        \"koyeb_sfo\": 246,\n        \"koyeb_sin\": 390,\n        \"koyeb_was\": 262,\n        \"railway_us-west2\": 260,\n        \"cdg\": 112,\n        \"fra\": 64\n      },\n      {\n        \"timestamp\": \"2025-10-20T06:00:00.000Z\",\n        \"koyeb_was\": 260,\n        \"koyeb_sin\": 388,\n        \"koyeb_sfo\": 263,\n        \"cdg\": 113,\n        \"fra\": 62,\n        \"railway_europe-west4-drams3a\": 90,\n        \"iad\": 248,\n        \"railway_us-west2\": 368,\n        \"ams\": 81,\n        \"koyeb_tyo\": 324,\n        \"lax\": 334,\n        \"koyeb_par\": 118,\n        \"nrt\": 316,\n        \"koyeb_fra\": 63,\n        \"sin\": 392,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"railway_asia-southeast1-eqsg3a\": 392\n      },\n      {\n        \"timestamp\": \"2025-10-20T07:00:00.000Z\",\n        \"nrt\": 612,\n        \"sin\": 477,\n        \"iad\": 264,\n        \"ams\": 94,\n        \"lax\": 388,\n        \"cdg\": 62,\n        \"fra\": 72\n      },\n      {\n        \"timestamp\": \"2025-10-20T08:00:00.000Z\",\n        \"railway_us-west2\": 384,\n        \"koyeb_fra\": 76,\n        \"koyeb_par\": 138,\n        \"sin\": 844,\n        \"railway_europe-west4-drams3a\": 107,\n        \"nrt\": 578,\n        \"railway_us-east4-eqdc4a\": 282,\n        \"lax\": 359,\n        \"koyeb_sin\": 896,\n        \"koyeb_sfo\": 452,\n        \"koyeb_was\": 286,\n        \"railway_asia-southeast1-eqsg3a\": 488,\n        \"ams\": 97,\n        \"iad\": 276,\n        \"fra\": 72,\n        \"cdg\": 142,\n        \"koyeb_tyo\": 587\n      },\n      {\n        \"timestamp\": \"2025-10-20T09:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 319,\n        \"fra\": 48,\n        \"cdg\": 71,\n        \"lax\": 191,\n        \"ams\": 59,\n        \"iad\": 153,\n        \"koyeb_fra\": 52,\n        \"koyeb_par\": 74,\n        \"koyeb_tyo\": 303,\n        \"sin\": 331,\n        \"railway_us-west2\": 207,\n        \"railway_us-east4-eqdc4a\": 157,\n        \"nrt\": 313,\n        \"railway_europe-west4-drams3a\": 66,\n        \"koyeb_sin\": 318,\n        \"koyeb_sfo\": 229,\n        \"koyeb_was\": 156\n      },\n      {\n        \"timestamp\": \"2025-10-20T10:00:00.000Z\",\n        \"lax\": 206,\n        \"koyeb_tyo\": 319,\n        \"ams\": 85,\n        \"iad\": 256,\n        \"railway_us-east4-eqdc4a\": 263,\n        \"fra\": 66,\n        \"cdg\": 116,\n        \"koyeb_sin\": 396,\n        \"koyeb_sfo\": 244,\n        \"koyeb_was\": 165,\n        \"railway_us-west2\": 362,\n        \"railway_europe-west4-drams3a\": 88,\n        \"railway_asia-southeast1-eqsg3a\": 402,\n        \"sin\": 424,\n        \"koyeb_fra\": 64,\n        \"koyeb_par\": 108,\n        \"nrt\": 331\n      },\n      {\n        \"timestamp\": \"2025-10-20T11:00:00.000Z\",\n        \"lax\": 208,\n        \"iad\": 252,\n        \"koyeb_tyo\": 338,\n        \"ams\": 82,\n        \"koyeb_sfo\": 240,\n        \"cdg\": 114,\n        \"fra\": 67,\n        \"koyeb_sin\": 390,\n        \"koyeb_was\": 264,\n        \"railway_us-east4-eqdc4a\": 261,\n        \"railway_asia-southeast1-eqsg3a\": 402,\n        \"railway_europe-west4-drams3a\": 93,\n        \"railway_us-west2\": 286,\n        \"koyeb_fra\": 66,\n        \"sin\": 407,\n        \"koyeb_par\": 118,\n        \"nrt\": 333\n      },\n      {\n        \"timestamp\": \"2025-10-20T12:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 251,\n        \"ams\": 84,\n        \"iad\": 248,\n        \"railway_asia-southeast1-eqsg3a\": 452,\n        \"railway_us-west2\": 370,\n        \"lax\": 280,\n        \"koyeb_fra\": 66,\n        \"koyeb_par\": 119,\n        \"fra\": 66,\n        \"cdg\": 113,\n        \"koyeb_tyo\": 352,\n        \"koyeb_sin\": 406,\n        \"nrt\": 349,\n        \"railway_europe-west4-drams3a\": 88,\n        \"koyeb_sfo\": 300,\n        \"koyeb_was\": 256,\n        \"sin\": 458\n      },\n      {\n        \"timestamp\": \"2025-10-20T13:00:00.000Z\",\n        \"koyeb_fra\": 66,\n        \"koyeb_par\": 118,\n        \"nrt\": 343,\n        \"sin\": 562,\n        \"railway_europe-west4-drams3a\": 96,\n        \"koyeb_sin\": 522,\n        \"koyeb_sfo\": 252,\n        \"koyeb_was\": 156,\n        \"railway_us-west2\": 370,\n        \"railway_asia-southeast1-eqsg3a\": 525,\n        \"fra\": 65,\n        \"cdg\": 115,\n        \"ams\": 84,\n        \"iad\": 244,\n        \"railway_us-east4-eqdc4a\": 247,\n        \"lax\": 207,\n        \"koyeb_tyo\": 347\n      },\n      {\n        \"timestamp\": \"2025-10-20T14:00:00.000Z\",\n        \"nrt\": 340,\n        \"koyeb_was\": 250,\n        \"koyeb_sin\": 418,\n        \"koyeb_sfo\": 250,\n        \"sin\": 456,\n        \"railway_us-west2\": 368,\n        \"railway_asia-southeast1-eqsg3a\": 432,\n        \"koyeb_tyo\": 354,\n        \"koyeb_par\": 118,\n        \"koyeb_fra\": 64,\n        \"cdg\": 115,\n        \"railway_europe-west4-drams3a\": 94,\n        \"fra\": 67,\n        \"iad\": 247,\n        \"ams\": 84,\n        \"lax\": 214,\n        \"railway_us-east4-eqdc4a\": 206\n      },\n      {\n        \"timestamp\": \"2025-10-20T15:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 428,\n        \"koyeb_was\": 161,\n        \"fra\": 67,\n        \"cdg\": 115,\n        \"koyeb_sin\": 457,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"koyeb_sfo\": 243,\n        \"koyeb_tyo\": 317,\n        \"ams\": 88,\n        \"iad\": 151,\n        \"lax\": 210,\n        \"nrt\": 559,\n        \"koyeb_par\": 108,\n        \"sin\": 532,\n        \"koyeb_fra\": 68,\n        \"railway_us-west2\": 264,\n        \"railway_europe-west4-drams3a\": 92\n      },\n      {\n        \"timestamp\": \"2025-10-20T16:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 93,\n        \"fra\": 67,\n        \"cdg\": 87,\n        \"koyeb_tyo\": 535,\n        \"koyeb_sin\": 461,\n        \"koyeb_sfo\": 259,\n        \"ams\": 89,\n        \"koyeb_was\": 256,\n        \"iad\": 159,\n        \"lax\": 215,\n        \"railway_us-east4-eqdc4a\": 256,\n        \"nrt\": 316,\n        \"sin\": 476,\n        \"koyeb_fra\": 68,\n        \"koyeb_par\": 119,\n        \"railway_asia-southeast1-eqsg3a\": 444,\n        \"railway_us-west2\": 255\n      },\n      {\n        \"timestamp\": \"2025-10-20T18:00:00.000Z\",\n        \"lax\": 221,\n        \"koyeb_tyo\": 337,\n        \"railway_asia-southeast1-eqsg3a\": 443,\n        \"iad\": 249,\n        \"ams\": 81,\n        \"cdg\": 114,\n        \"fra\": 69,\n        \"koyeb_was\": 253,\n        \"koyeb_sfo\": 251,\n        \"koyeb_sin\": 456,\n        \"railway_us-west2\": 243,\n        \"sin\": 497,\n        \"railway_us-east4-eqdc4a\": 255,\n        \"railway_europe-west4-drams3a\": 95,\n        \"koyeb_par\": 119,\n        \"koyeb_fra\": 66,\n        \"nrt\": 307\n      },\n      {\n        \"timestamp\": \"2025-10-20T19:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 422,\n        \"koyeb_tyo\": 434,\n        \"ams\": 85,\n        \"iad\": 252,\n        \"lax\": 217,\n        \"fra\": 66,\n        \"cdg\": 115,\n        \"koyeb_was\": 206,\n        \"koyeb_sfo\": 253,\n        \"koyeb_sin\": 415,\n        \"railway_us-west2\": 249,\n        \"nrt\": 302,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_par\": 117,\n        \"sin\": 440,\n        \"koyeb_fra\": 66\n      },\n      {\n        \"timestamp\": \"2025-10-20T20:00:00.000Z\",\n        \"iad\": 149,\n        \"ams\": 61,\n        \"railway_europe-west4-drams3a\": 68,\n        \"lax\": 198,\n        \"koyeb_fra\": 48,\n        \"railway_us-east4-eqdc4a\": 157,\n        \"cdg\": 73,\n        \"fra\": 51,\n        \"koyeb_par\": 78,\n        \"koyeb_tyo\": 301,\n        \"railway_us-west2\": 231,\n        \"nrt\": 299,\n        \"koyeb_sfo\": 232,\n        \"koyeb_sin\": 231,\n        \"sin\": 296,\n        \"railway_asia-southeast1-eqsg3a\": 237,\n        \"koyeb_was\": 155\n      },\n      {\n        \"timestamp\": \"2025-10-20T21:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 67,\n        \"sin\": 286,\n        \"koyeb_par\": 74,\n        \"nrt\": 304,\n        \"koyeb_fra\": 50,\n        \"railway_us-east4-eqdc4a\": 153,\n        \"cdg\": 74,\n        \"fra\": 52,\n        \"koyeb_was\": 150,\n        \"koyeb_sin\": 231,\n        \"koyeb_sfo\": 237,\n        \"koyeb_tyo\": 302,\n        \"lax\": 204,\n        \"iad\": 239,\n        \"railway_asia-southeast1-eqsg3a\": 234,\n        \"ams\": 60,\n        \"railway_us-west2\": 235\n      },\n      {\n        \"timestamp\": \"2025-10-20T22:00:00.000Z\",\n        \"fra\": 61,\n        \"cdg\": 76,\n        \"railway_asia-southeast1-eqsg3a\": 248,\n        \"railway_us-east4-eqdc4a\": 246,\n        \"koyeb_par\": 115,\n        \"koyeb_fra\": 61,\n        \"lax\": 214,\n        \"ams\": 65,\n        \"iad\": 335,\n        \"sin\": 344,\n        \"koyeb_tyo\": 316,\n        \"nrt\": 306,\n        \"koyeb_was\": 156,\n        \"koyeb_sin\": 388,\n        \"koyeb_sfo\": 263,\n        \"railway_us-west2\": 235,\n        \"railway_europe-west4-drams3a\": 85\n      },\n      {\n        \"timestamp\": \"2025-10-20T23:00:00.000Z\",\n        \"sin\": 391,\n        \"railway_us-west2\": 251,\n        \"nrt\": 320,\n        \"railway_asia-southeast1-eqsg3a\": 398,\n        \"koyeb_par\": 115,\n        \"koyeb_fra\": 63,\n        \"koyeb_tyo\": 542,\n        \"cdg\": 113,\n        \"fra\": 63,\n        \"railway_europe-west4-drams3a\": 88,\n        \"lax\": 356,\n        \"koyeb_was\": 252,\n        \"railway_us-east4-eqdc4a\": 253,\n        \"iad\": 246,\n        \"ams\": 82,\n        \"koyeb_sin\": 394,\n        \"koyeb_sfo\": 390\n      },\n      {\n        \"timestamp\": \"2025-10-21T00:00:00.000Z\",\n        \"railway_us-west2\": 240,\n        \"koyeb_par\": 116,\n        \"railway_us-east4-eqdc4a\": 259,\n        \"koyeb_fra\": 62,\n        \"nrt\": 318,\n        \"railway_europe-west4-drams3a\": 92,\n        \"sin\": 388,\n        \"ams\": 80,\n        \"iad\": 248,\n        \"railway_asia-southeast1-eqsg3a\": 240,\n        \"koyeb_was\": 252,\n        \"lax\": 366,\n        \"koyeb_sin\": 388,\n        \"koyeb_sfo\": 390,\n        \"koyeb_tyo\": 555,\n        \"fra\": 62,\n        \"cdg\": 112\n      },\n      {\n        \"timestamp\": \"2025-10-21T01:00:00.000Z\",\n        \"fra\": 61,\n        \"cdg\": 112,\n        \"railway_asia-southeast1-eqsg3a\": 399,\n        \"ams\": 80,\n        \"iad\": 247,\n        \"koyeb_par\": 115,\n        \"koyeb_fra\": 63,\n        \"lax\": 355,\n        \"koyeb_tyo\": 314,\n        \"nrt\": 432,\n        \"sin\": 408,\n        \"railway_us-west2\": 240,\n        \"railway_europe-west4-drams3a\": 88,\n        \"koyeb_was\": 250,\n        \"koyeb_sin\": 388,\n        \"koyeb_sfo\": 328,\n        \"railway_us-east4-eqdc4a\": 252\n      },\n      {\n        \"timestamp\": \"2025-10-21T02:00:00.000Z\",\n        \"koyeb_fra\": 64,\n        \"sin\": 406,\n        \"koyeb_par\": 115,\n        \"nrt\": 314,\n        \"railway_us-west2\": 242,\n        \"railway_asia-southeast1-eqsg3a\": 398,\n        \"railway_us-east4-eqdc4a\": 250,\n        \"cdg\": 112,\n        \"railway_europe-west4-drams3a\": 91,\n        \"fra\": 62,\n        \"koyeb_sfo\": 396,\n        \"koyeb_sin\": 396,\n        \"koyeb_was\": 212,\n        \"lax\": 204,\n        \"koyeb_tyo\": 323,\n        \"iad\": 245,\n        \"ams\": 78\n      },\n      {\n        \"timestamp\": \"2025-10-21T03:00:00.000Z\",\n        \"sin\": 432,\n        \"koyeb_fra\": 62,\n        \"koyeb_par\": 91,\n        \"nrt\": 326,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"railway_europe-west4-drams3a\": 86,\n        \"railway_asia-southeast1-eqsg3a\": 408,\n        \"fra\": 63,\n        \"cdg\": 114,\n        \"koyeb_sin\": 406,\n        \"koyeb_sfo\": 398,\n        \"koyeb_was\": 256,\n        \"lax\": 362,\n        \"koyeb_tyo\": 556,\n        \"railway_us-west2\": 244,\n        \"ams\": 81,\n        \"iad\": 252\n      },\n      {\n        \"timestamp\": \"2025-10-21T04:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 448,\n        \"koyeb_fra\": 62,\n        \"railway_us-west2\": 243,\n        \"koyeb_par\": 114,\n        \"railway_us-east4-eqdc4a\": 264,\n        \"nrt\": 558,\n        \"sin\": 448,\n        \"ams\": 78,\n        \"iad\": 243,\n        \"koyeb_sin\": 418,\n        \"lax\": 284,\n        \"koyeb_sfo\": 396,\n        \"koyeb_was\": 260,\n        \"koyeb_tyo\": 452,\n        \"fra\": 63,\n        \"railway_europe-west4-drams3a\": 90,\n        \"cdg\": 111\n      },\n      {\n        \"timestamp\": \"2025-10-21T05:00:00.000Z\",\n        \"fra\": 62,\n        \"cdg\": 113,\n        \"railway_europe-west4-drams3a\": 93,\n        \"ams\": 80,\n        \"iad\": 254,\n        \"koyeb_fra\": 63,\n        \"koyeb_par\": 116,\n        \"lax\": 358,\n        \"railway_us-east4-eqdc4a\": 247,\n        \"railway_us-west2\": 386,\n        \"nrt\": 522,\n        \"koyeb_tyo\": 317,\n        \"railway_asia-southeast1-eqsg3a\": 538,\n        \"sin\": 513,\n        \"koyeb_sin\": 465,\n        \"koyeb_sfo\": 392,\n        \"koyeb_was\": 206\n      },\n      {\n        \"timestamp\": \"2025-10-21T06:00:00.000Z\",\n        \"railway_us-west2\": 406,\n        \"sin\": 451,\n        \"railway_us-east4-eqdc4a\": 211,\n        \"koyeb_par\": 115,\n        \"koyeb_fra\": 65,\n        \"railway_europe-west4-drams3a\": 92,\n        \"nrt\": 400,\n        \"cdg\": 103,\n        \"fra\": 64,\n        \"koyeb_was\": 252,\n        \"koyeb_sfo\": 390,\n        \"koyeb_sin\": 458,\n        \"lax\": 358,\n        \"koyeb_tyo\": 332,\n        \"railway_asia-southeast1-eqsg3a\": 460,\n        \"iad\": 252,\n        \"ams\": 82\n      },\n      {\n        \"timestamp\": \"2025-10-21T07:00:00.000Z\",\n        \"fra\": 65,\n        \"cdg\": 115,\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 64,\n        \"railway_europe-west4-drams3a\": 94,\n        \"lax\": 352,\n        \"ams\": 80,\n        \"iad\": 254,\n        \"sin\": 562,\n        \"railway_asia-southeast1-eqsg3a\": 569,\n        \"nrt\": 542,\n        \"koyeb_was\": 253,\n        \"koyeb_sfo\": 384,\n        \"koyeb_sin\": 570,\n        \"railway_us-east4-eqdc4a\": 214,\n        \"koyeb_tyo\": 444,\n        \"railway_us-west2\": 318\n      },\n      {\n        \"timestamp\": \"2025-10-21T08:00:00.000Z\",\n        \"fra\": 66,\n        \"cdg\": 114,\n        \"koyeb_fra\": 65,\n        \"koyeb_par\": 117,\n        \"railway_asia-southeast1-eqsg3a\": 436,\n        \"lax\": 355,\n        \"railway_us-east4-eqdc4a\": 261,\n        \"ams\": 81,\n        \"iad\": 254,\n        \"sin\": 452,\n        \"nrt\": 550,\n        \"railway_europe-west4-drams3a\": 92,\n        \"koyeb_tyo\": 336,\n        \"koyeb_sfo\": 386,\n        \"koyeb_sin\": 452,\n        \"railway_us-west2\": 263,\n        \"koyeb_was\": 262\n      },\n      {\n        \"timestamp\": \"2025-10-21T09:00:00.000Z\",\n        \"koyeb_tyo\": 318,\n        \"lax\": 205,\n        \"railway_europe-west4-drams3a\": 94,\n        \"ams\": 86,\n        \"iad\": 249,\n        \"fra\": 67,\n        \"cdg\": 114,\n        \"koyeb_was\": 255,\n        \"koyeb_sfo\": 362,\n        \"koyeb_sin\": 486,\n        \"railway_asia-southeast1-eqsg3a\": 482,\n        \"railway_us-east4-eqdc4a\": 211,\n        \"railway_us-west2\": 252,\n        \"sin\": 530,\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 66,\n        \"nrt\": 556\n      },\n      {\n        \"timestamp\": \"2025-10-21T10:00:00.000Z\",\n        \"koyeb_was\": 258,\n        \"koyeb_sfo\": 268,\n        \"koyeb_sin\": 523,\n        \"railway_asia-southeast1-eqsg3a\": 490,\n        \"sin\": 480,\n        \"nrt\": 550,\n        \"koyeb_tyo\": 328,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 66,\n        \"lax\": 208,\n        \"iad\": 160,\n        \"ams\": 84,\n        \"cdg\": 113,\n        \"fra\": 65,\n        \"railway_us-west2\": 256,\n        \"railway_europe-west4-drams3a\": 95\n      },\n      {\n        \"timestamp\": \"2025-10-21T11:00:00.000Z\",\n        \"koyeb_sfo\": 248,\n        \"koyeb_sin\": 474,\n        \"koyeb_was\": 263,\n        \"nrt\": 340,\n        \"koyeb_tyo\": 553,\n        \"sin\": 512,\n        \"railway_asia-southeast1-eqsg3a\": 454,\n        \"railway_us-east4-eqdc4a\": 164,\n        \"railway_us-west2\": 248,\n        \"ams\": 84,\n        \"railway_europe-west4-drams3a\": 95,\n        \"iad\": 251,\n        \"koyeb_fra\": 64,\n        \"koyeb_par\": 118,\n        \"lax\": 216,\n        \"fra\": 66,\n        \"cdg\": 115\n      },\n      {\n        \"timestamp\": \"2025-10-21T12:00:00.000Z\",\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 68,\n        \"nrt\": 343,\n        \"railway_europe-west4-drams3a\": 96,\n        \"sin\": 494,\n        \"iad\": 254,\n        \"ams\": 84,\n        \"railway_asia-southeast1-eqsg3a\": 465,\n        \"railway_us-west2\": 394,\n        \"lax\": 194,\n        \"koyeb_was\": 262,\n        \"koyeb_sfo\": 248,\n        \"koyeb_sin\": 466,\n        \"koyeb_tyo\": 344,\n        \"railway_us-east4-eqdc4a\": 271,\n        \"cdg\": 114,\n        \"fra\": 67\n      },\n      {\n        \"timestamp\": \"2025-10-21T13:00:00.000Z\",\n        \"cdg\": 106,\n        \"fra\": 67,\n        \"railway_us-east4-eqdc4a\": 256,\n        \"railway_europe-west4-drams3a\": 97,\n        \"koyeb_par\": 116,\n        \"lax\": 356,\n        \"koyeb_fra\": 66,\n        \"iad\": 248,\n        \"ams\": 86,\n        \"sin\": 534,\n        \"koyeb_tyo\": 406,\n        \"nrt\": 428,\n        \"koyeb_was\": 250,\n        \"koyeb_sin\": 532,\n        \"railway_asia-southeast1-eqsg3a\": 489,\n        \"koyeb_sfo\": 384,\n        \"railway_us-west2\": 257\n      },\n      {\n        \"timestamp\": \"2025-10-21T14:00:00.000Z\",\n        \"sin\": 662,\n        \"koyeb_par\": 120,\n        \"railway_asia-southeast1-eqsg3a\": 659,\n        \"koyeb_fra\": 66,\n        \"nrt\": 577,\n        \"railway_us-east4-eqdc4a\": 254,\n        \"fra\": 64,\n        \"cdg\": 114,\n        \"koyeb_was\": 252,\n        \"koyeb_sin\": 582,\n        \"koyeb_sfo\": 386,\n        \"koyeb_tyo\": 559,\n        \"railway_europe-west4-drams3a\": 96,\n        \"lax\": 360,\n        \"ams\": 85,\n        \"iad\": 252,\n        \"railway_us-west2\": 410\n      },\n      {\n        \"timestamp\": \"2025-10-21T15:00:00.000Z\",\n        \"koyeb_par\": 120,\n        \"railway_us-west2\": 246,\n        \"koyeb_fra\": 66,\n        \"cdg\": 106,\n        \"fra\": 67,\n        \"lax\": 354,\n        \"iad\": 249,\n        \"ams\": 85,\n        \"railway_asia-southeast1-eqsg3a\": 522,\n        \"koyeb_was\": 254,\n        \"sin\": 611,\n        \"koyeb_sin\": 665,\n        \"railway_europe-west4-drams3a\": 94,\n        \"koyeb_sfo\": 382,\n        \"nrt\": 594,\n        \"railway_us-east4-eqdc4a\": 260,\n        \"koyeb_tyo\": 642\n      },\n      {\n        \"timestamp\": \"2025-10-21T16:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 566,\n        \"koyeb_par\": 118,\n        \"koyeb_fra\": 68,\n        \"railway_us-west2\": 315,\n        \"fra\": 68,\n        \"cdg\": 114,\n        \"ams\": 86,\n        \"iad\": 160,\n        \"lax\": 354,\n        \"nrt\": 568,\n        \"koyeb_was\": 209,\n        \"koyeb_sfo\": 382,\n        \"sin\": 545,\n        \"koyeb_sin\": 540,\n        \"koyeb_tyo\": 611,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"railway_europe-west4-drams3a\": 100\n      },\n      {\n        \"timestamp\": \"2025-10-21T17:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 258,\n        \"railway_asia-southeast1-eqsg3a\": 450,\n        \"koyeb_sin\": 458,\n        \"koyeb_sfo\": 399,\n        \"koyeb_was\": 256,\n        \"nrt\": 564,\n        \"railway_us-west2\": 250,\n        \"koyeb_tyo\": 592,\n        \"sin\": 469,\n        \"ams\": 86,\n        \"iad\": 250,\n        \"koyeb_fra\": 66,\n        \"lax\": 342,\n        \"koyeb_par\": 120,\n        \"fra\": 68,\n        \"cdg\": 114,\n        \"railway_europe-west4-drams3a\": 96\n      },\n      {\n        \"timestamp\": \"2025-10-21T18:00:00.000Z\",\n        \"koyeb_tyo\": 573,\n        \"railway_us-east4-eqdc4a\": 260,\n        \"railway_us-west2\": 238,\n        \"nrt\": 570,\n        \"koyeb_sfo\": 390,\n        \"railway_asia-southeast1-eqsg3a\": 398,\n        \"koyeb_sin\": 430,\n        \"sin\": 462,\n        \"koyeb_was\": 250,\n        \"iad\": 256,\n        \"ams\": 86,\n        \"railway_europe-west4-drams3a\": 99,\n        \"lax\": 350,\n        \"koyeb_fra\": 67,\n        \"cdg\": 114,\n        \"koyeb_par\": 119,\n        \"fra\": 64\n      },\n      {\n        \"timestamp\": \"2025-10-21T19:00:00.000Z\",\n        \"cdg\": 114,\n        \"fra\": 67,\n        \"iad\": 249,\n        \"railway_us-west2\": 233,\n        \"ams\": 88,\n        \"koyeb_par\": 120,\n        \"railway_asia-southeast1-eqsg3a\": 404,\n        \"lax\": 347,\n        \"koyeb_fra\": 64,\n        \"nrt\": 576,\n        \"koyeb_tyo\": 566,\n        \"railway_europe-west4-drams3a\": 97,\n        \"sin\": 404,\n        \"koyeb_was\": 254,\n        \"railway_us-east4-eqdc4a\": 256,\n        \"koyeb_sin\": 362,\n        \"koyeb_sfo\": 379\n      },\n      {\n        \"timestamp\": \"2025-10-21T20:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 95,\n        \"koyeb_fra\": 66,\n        \"sin\": 385,\n        \"koyeb_par\": 117,\n        \"nrt\": 467,\n        \"railway_us-west2\": 248,\n        \"railway_us-east4-eqdc4a\": 232,\n        \"lax\": 211,\n        \"koyeb_tyo\": 544,\n        \"iad\": 247,\n        \"ams\": 84,\n        \"cdg\": 115,\n        \"fra\": 67,\n        \"koyeb_sfo\": 366,\n        \"koyeb_sin\": 402,\n        \"railway_asia-southeast1-eqsg3a\": 403,\n        \"koyeb_was\": 250\n      },\n      {\n        \"timestamp\": \"2025-10-21T21:00:00.000Z\",\n        \"railway_us-west2\": 241,\n        \"cdg\": 114,\n        \"fra\": 65,\n        \"koyeb_tyo\": 310,\n        \"railway_europe-west4-drams3a\": 86,\n        \"koyeb_was\": 246,\n        \"lax\": 340,\n        \"koyeb_sin\": 386,\n        \"koyeb_sfo\": 371,\n        \"iad\": 242,\n        \"ams\": 84,\n        \"sin\": 414,\n        \"railway_us-east4-eqdc4a\": 254,\n        \"nrt\": 547,\n        \"koyeb_par\": 119,\n        \"railway_asia-southeast1-eqsg3a\": 411,\n        \"koyeb_fra\": 65\n      },\n      {\n        \"timestamp\": \"2025-10-21T22:00:00.000Z\",\n        \"railway_us-west2\": 251,\n        \"koyeb_tyo\": 547,\n        \"railway_us-east4-eqdc4a\": 250,\n        \"koyeb_was\": 252,\n        \"sin\": 392,\n        \"koyeb_sfo\": 385,\n        \"koyeb_sin\": 380,\n        \"railway_europe-west4-drams3a\": 90,\n        \"nrt\": 317,\n        \"lax\": 356,\n        \"iad\": 247,\n        \"railway_asia-southeast1-eqsg3a\": 388,\n        \"ams\": 80,\n        \"cdg\": 114,\n        \"fra\": 62,\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 62\n      },\n      {\n        \"timestamp\": \"2025-10-21T23:00:00.000Z\",\n        \"cdg\": 112,\n        \"fra\": 62,\n        \"railway_europe-west4-drams3a\": 90,\n        \"koyeb_fra\": 64,\n        \"lax\": 352,\n        \"koyeb_par\": 116,\n        \"railway_us-west2\": 394,\n        \"iad\": 246,\n        \"ams\": 82,\n        \"sin\": 394,\n        \"railway_asia-southeast1-eqsg3a\": 381,\n        \"koyeb_tyo\": 317,\n        \"nrt\": 550,\n        \"koyeb_sin\": 396,\n        \"koyeb_sfo\": 323,\n        \"koyeb_was\": 248,\n        \"railway_us-east4-eqdc4a\": 256\n      },\n      {\n        \"timestamp\": \"2025-10-22T00:00:00.000Z\",\n        \"koyeb_sfo\": 383,\n        \"koyeb_sin\": 392,\n        \"railway_asia-southeast1-eqsg3a\": 392,\n        \"lax\": 358,\n        \"koyeb_was\": 247,\n        \"ams\": 67,\n        \"iad\": 248,\n        \"railway_us-east4-eqdc4a\": 255,\n        \"fra\": 62,\n        \"cdg\": 112,\n        \"koyeb_tyo\": 318,\n        \"koyeb_fra\": 62,\n        \"koyeb_par\": 114,\n        \"sin\": 394,\n        \"nrt\": 306,\n        \"railway_europe-west4-drams3a\": 92,\n        \"railway_us-west2\": 382\n      },\n      {\n        \"timestamp\": \"2025-10-22T01:00:00.000Z\",\n        \"koyeb_fra\": 64,\n        \"nrt\": 309,\n        \"railway_us-west2\": 389,\n        \"koyeb_par\": 106,\n        \"sin\": 395,\n        \"railway_europe-west4-drams3a\": 88,\n        \"railway_us-east4-eqdc4a\": 248,\n        \"koyeb_sfo\": 384,\n        \"koyeb_sin\": 398,\n        \"koyeb_was\": 246,\n        \"fra\": 64,\n        \"railway_asia-southeast1-eqsg3a\": 396,\n        \"cdg\": 112,\n        \"ams\": 58,\n        \"iad\": 246,\n        \"lax\": 354,\n        \"koyeb_tyo\": 312\n      },\n      {\n        \"timestamp\": \"2025-10-22T02:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 88,\n        \"railway_us-east4-eqdc4a\": 252,\n        \"koyeb_was\": 250,\n        \"koyeb_sin\": 396,\n        \"cdg\": 112,\n        \"koyeb_sfo\": 386,\n        \"fra\": 62,\n        \"iad\": 248,\n        \"koyeb_tyo\": 332,\n        \"ams\": 80,\n        \"lax\": 358,\n        \"nrt\": 324,\n        \"koyeb_par\": 115,\n        \"sin\": 434,\n        \"koyeb_fra\": 62,\n        \"railway_us-west2\": 269,\n        \"railway_asia-southeast1-eqsg3a\": 417\n      },\n      {\n        \"timestamp\": \"2025-10-22T03:00:00.000Z\",\n        \"railway_us-west2\": 327,\n        \"iad\": 257,\n        \"ams\": 68,\n        \"railway_europe-west4-drams3a\": 89,\n        \"koyeb_tyo\": 314,\n        \"lax\": 358,\n        \"koyeb_was\": 256,\n        \"koyeb_sfo\": 389,\n        \"koyeb_sin\": 438,\n        \"cdg\": 112,\n        \"fra\": 63,\n        \"railway_us-east4-eqdc4a\": 267,\n        \"railway_asia-southeast1-eqsg3a\": 424,\n        \"nrt\": 546,\n        \"koyeb_par\": 114,\n        \"koyeb_fra\": 62,\n        \"sin\": 457\n      },\n      {\n        \"timestamp\": \"2025-10-22T04:00:00.000Z\",\n        \"koyeb_tyo\": 316,\n        \"nrt\": 322,\n        \"railway_europe-west4-drams3a\": 94,\n        \"sin\": 500,\n        \"koyeb_was\": 260,\n        \"koyeb_sfo\": 390,\n        \"koyeb_sin\": 444,\n        \"railway_us-west2\": 256,\n        \"cdg\": 112,\n        \"fra\": 65,\n        \"iad\": 251,\n        \"railway_asia-southeast1-eqsg3a\": 446,\n        \"ams\": 80,\n        \"lax\": 220,\n        \"koyeb_par\": 114,\n        \"koyeb_fra\": 62,\n        \"railway_us-east4-eqdc4a\": 169\n      },\n      {\n        \"timestamp\": \"2025-10-22T05:00:00.000Z\",\n        \"lax\": 208,\n        \"iad\": 250,\n        \"ams\": 82,\n        \"railway_us-west2\": 398,\n        \"cdg\": 74,\n        \"fra\": 65,\n        \"koyeb_par\": 114,\n        \"koyeb_fra\": 62,\n        \"railway_asia-southeast1-eqsg3a\": 466,\n        \"koyeb_tyo\": 324,\n        \"railway_europe-west4-drams3a\": 90,\n        \"sin\": 574,\n        \"railway_us-east4-eqdc4a\": 214,\n        \"koyeb_was\": 254,\n        \"nrt\": 553,\n        \"koyeb_sfo\": 393,\n        \"koyeb_sin\": 519\n      },\n      {\n        \"timestamp\": \"2025-10-22T06:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 452,\n        \"koyeb_tyo\": 324,\n        \"koyeb_sin\": 599,\n        \"koyeb_sfo\": 398,\n        \"sin\": 568,\n        \"koyeb_was\": 252,\n        \"nrt\": 352,\n        \"railway_us-west2\": 409,\n        \"lax\": 363,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"ams\": 83,\n        \"iad\": 250,\n        \"fra\": 64,\n        \"cdg\": 112,\n        \"railway_europe-west4-drams3a\": 85,\n        \"koyeb_fra\": 66,\n        \"koyeb_par\": 115\n      },\n      {\n        \"timestamp\": \"2025-10-22T07:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 95,\n        \"fra\": 65,\n        \"cdg\": 112,\n        \"railway_us-west2\": 276,\n        \"ams\": 82,\n        \"iad\": 250,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"koyeb_par\": 116,\n        \"lax\": 362,\n        \"koyeb_fra\": 66,\n        \"koyeb_tyo\": 554,\n        \"nrt\": 550,\n        \"sin\": 540,\n        \"railway_asia-southeast1-eqsg3a\": 467,\n        \"koyeb_was\": 254,\n        \"koyeb_sin\": 546,\n        \"koyeb_sfo\": 400\n      },\n      {\n        \"timestamp\": \"2025-10-22T08:00:00.000Z\",\n        \"railway_us-west2\": 240,\n        \"sin\": 474,\n        \"nrt\": 556,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 66,\n        \"railway_asia-southeast1-eqsg3a\": 454,\n        \"koyeb_tyo\": 554,\n        \"fra\": 64,\n        \"cdg\": 115,\n        \"railway_europe-west4-drams3a\": 94,\n        \"lax\": 358,\n        \"ams\": 84,\n        \"railway_us-east4-eqdc4a\": 254,\n        \"koyeb_was\": 260,\n        \"iad\": 251,\n        \"koyeb_sfo\": 392,\n        \"koyeb_sin\": 456\n      },\n      {\n        \"timestamp\": \"2025-10-22T09:00:00.000Z\",\n        \"koyeb_tyo\": 440,\n        \"lax\": 354,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"ams\": 83,\n        \"iad\": 156,\n        \"fra\": 64,\n        \"cdg\": 112,\n        \"koyeb_sin\": 516,\n        \"railway_asia-southeast1-eqsg3a\": 452,\n        \"railway_us-west2\": 382,\n        \"koyeb_sfo\": 252,\n        \"koyeb_was\": 254,\n        \"railway_europe-west4-drams3a\": 96,\n        \"sin\": 535,\n        \"nrt\": 388,\n        \"koyeb_fra\": 65,\n        \"koyeb_par\": 116\n      },\n      {\n        \"timestamp\": \"2025-10-22T10:00:00.000Z\",\n        \"railway_us-west2\": 398,\n        \"nrt\": 318,\n        \"koyeb_fra\": 66,\n        \"koyeb_par\": 114,\n        \"sin\": 601,\n        \"railway_europe-west4-drams3a\": 97,\n        \"iad\": 253,\n        \"railway_asia-southeast1-eqsg3a\": 485,\n        \"ams\": 85,\n        \"railway_us-east4-eqdc4a\": 261,\n        \"koyeb_tyo\": 552,\n        \"lax\": 346,\n        \"koyeb_sin\": 532,\n        \"koyeb_sfo\": 377,\n        \"koyeb_was\": 256,\n        \"cdg\": 98,\n        \"fra\": 65\n      },\n      {\n        \"timestamp\": \"2025-10-22T11:00:00.000Z\",\n        \"koyeb_tyo\": 569,\n        \"cdg\": 113,\n        \"fra\": 64,\n        \"iad\": 250,\n        \"ams\": 83,\n        \"railway_us-west2\": 232,\n        \"koyeb_was\": 256,\n        \"koyeb_sfo\": 327,\n        \"koyeb_sin\": 497,\n        \"lax\": 212,\n        \"railway_europe-west4-drams3a\": 94,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"railway_asia-southeast1-eqsg3a\": 474,\n        \"nrt\": 572,\n        \"sin\": 578,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 67\n      },\n      {\n        \"timestamp\": \"2025-10-22T12:00:00.000Z\",\n        \"koyeb_fra\": 66,\n        \"iad\": 254,\n        \"ams\": 83,\n        \"koyeb_par\": 116,\n        \"lax\": 346,\n        \"railway_us-west2\": 398,\n        \"railway_europe-west4-drams3a\": 96,\n        \"railway_us-east4-eqdc4a\": 257,\n        \"cdg\": 114,\n        \"fra\": 65,\n        \"koyeb_sfo\": 386,\n        \"koyeb_sin\": 608,\n        \"koyeb_was\": 260,\n        \"railway_asia-southeast1-eqsg3a\": 599,\n        \"nrt\": 570,\n        \"sin\": 620,\n        \"koyeb_tyo\": 392\n      },\n      {\n        \"timestamp\": \"2025-10-22T13:00:00.000Z\",\n        \"koyeb_was\": 262,\n        \"nrt\": 352,\n        \"koyeb_sin\": 828,\n        \"koyeb_sfo\": 395,\n        \"sin\": 625,\n        \"railway_us-west2\": 410,\n        \"railway_europe-west4-drams3a\": 94,\n        \"koyeb_tyo\": 393,\n        \"koyeb_par\": 117,\n        \"railway_us-east4-eqdc4a\": 264,\n        \"koyeb_fra\": 66,\n        \"railway_asia-southeast1-eqsg3a\": 610,\n        \"cdg\": 114,\n        \"fra\": 66,\n        \"iad\": 260,\n        \"ams\": 85,\n        \"lax\": 356\n      },\n      {\n        \"timestamp\": \"2025-10-22T14:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 276,\n        \"fra\": 67,\n        \"cdg\": 114,\n        \"koyeb_par\": 118,\n        \"koyeb_fra\": 67,\n        \"railway_asia-southeast1-eqsg3a\": 612,\n        \"lax\": 207,\n        \"railway_us-west2\": 330,\n        \"ams\": 86,\n        \"iad\": 275,\n        \"sin\": 628,\n        \"koyeb_was\": 266,\n        \"railway_europe-west4-drams3a\": 97,\n        \"koyeb_sin\": 718,\n        \"koyeb_sfo\": 394,\n        \"nrt\": 570,\n        \"koyeb_tyo\": 579\n      },\n      {\n        \"timestamp\": \"2025-10-22T15:00:00.000Z\",\n        \"railway_us-west2\": 244,\n        \"koyeb_sfo\": 383,\n        \"koyeb_sin\": 604,\n        \"koyeb_was\": 264,\n        \"sin\": 626,\n        \"railway_asia-southeast1-eqsg3a\": 568,\n        \"koyeb_tyo\": 584,\n        \"nrt\": 548,\n        \"lax\": 346,\n        \"koyeb_fra\": 67,\n        \"ams\": 88,\n        \"iad\": 268,\n        \"railway_europe-west4-drams3a\": 92,\n        \"koyeb_par\": 116,\n        \"fra\": 66,\n        \"cdg\": 114,\n        \"railway_us-east4-eqdc4a\": 272\n      },\n      {\n        \"timestamp\": \"2025-10-22T16:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 97,\n        \"sin\": 654,\n        \"nrt\": 564,\n        \"koyeb_fra\": 67,\n        \"koyeb_par\": 116,\n        \"railway_us-east4-eqdc4a\": 280,\n        \"koyeb_tyo\": 567,\n        \"cdg\": 115,\n        \"fra\": 65,\n        \"railway_us-west2\": 264,\n        \"lax\": 360,\n        \"koyeb_sin\": 608,\n        \"iad\": 260,\n        \"koyeb_sfo\": 397,\n        \"ams\": 84,\n        \"railway_asia-southeast1-eqsg3a\": 652,\n        \"koyeb_was\": 282\n      },\n      {\n        \"timestamp\": \"2025-10-22T17:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 266,\n        \"railway_us-west2\": 255,\n        \"fra\": 66,\n        \"cdg\": 114,\n        \"ams\": 86,\n        \"iad\": 262,\n        \"koyeb_fra\": 67,\n        \"lax\": 352,\n        \"railway_europe-west4-drams3a\": 94,\n        \"koyeb_par\": 117,\n        \"railway_asia-southeast1-eqsg3a\": 532,\n        \"nrt\": 505,\n        \"koyeb_tyo\": 558,\n        \"sin\": 487,\n        \"koyeb_sfo\": 392,\n        \"koyeb_sin\": 534,\n        \"koyeb_was\": 266\n      },\n      {\n        \"timestamp\": \"2025-10-22T18:00:00.000Z\",\n        \"nrt\": 562,\n        \"koyeb_tyo\": 544,\n        \"sin\": 456,\n        \"railway_europe-west4-drams3a\": 88,\n        \"koyeb_was\": 268,\n        \"railway_us-east4-eqdc4a\": 265,\n        \"koyeb_sin\": 434,\n        \"koyeb_sfo\": 395,\n        \"cdg\": 114,\n        \"fra\": 68,\n        \"railway_us-west2\": 257,\n        \"koyeb_par\": 116,\n        \"railway_asia-southeast1-eqsg3a\": 440,\n        \"iad\": 258,\n        \"ams\": 87,\n        \"koyeb_fra\": 69,\n        \"lax\": 358\n      },\n      {\n        \"timestamp\": \"2025-10-22T19:00:00.000Z\",\n        \"lax\": 356,\n        \"railway_us-west2\": 244,\n        \"iad\": 260,\n        \"ams\": 89,\n        \"koyeb_par\": 116,\n        \"cdg\": 114,\n        \"fra\": 66,\n        \"railway_us-east4-eqdc4a\": 272,\n        \"koyeb_fra\": 69,\n        \"railway_asia-southeast1-eqsg3a\": 426,\n        \"railway_europe-west4-drams3a\": 96,\n        \"koyeb_tyo\": 304,\n        \"sin\": 439,\n        \"koyeb_was\": 266,\n        \"koyeb_sin\": 416,\n        \"koyeb_sfo\": 384,\n        \"nrt\": 547\n      },\n      {\n        \"timestamp\": \"2025-10-22T20:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 268,\n        \"sin\": 423,\n        \"nrt\": 306,\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 66,\n        \"railway_asia-southeast1-eqsg3a\": 392,\n        \"koyeb_tyo\": 536,\n        \"railway_us-west2\": 382,\n        \"cdg\": 115,\n        \"fra\": 66,\n        \"railway_europe-west4-drams3a\": 95,\n        \"lax\": 358,\n        \"koyeb_was\": 266,\n        \"iad\": 266,\n        \"ams\": 89,\n        \"koyeb_sin\": 408,\n        \"koyeb_sfo\": 392\n      },\n      {\n        \"timestamp\": \"2025-10-22T21:00:00.000Z\",\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 66,\n        \"sin\": 406,\n        \"railway_asia-southeast1-eqsg3a\": 400,\n        \"railway_us-west2\": 251,\n        \"nrt\": 424,\n        \"koyeb_was\": 254,\n        \"lax\": 358,\n        \"koyeb_sfo\": 394,\n        \"koyeb_sin\": 394,\n        \"railway_europe-west4-drams3a\": 94,\n        \"iad\": 250,\n        \"ams\": 84,\n        \"railway_us-east4-eqdc4a\": 256,\n        \"cdg\": 112,\n        \"fra\": 64,\n        \"koyeb_tyo\": 537\n      },\n      {\n        \"timestamp\": \"2025-10-22T22:00:00.000Z\",\n        \"lax\": 354,\n        \"railway_asia-southeast1-eqsg3a\": 384,\n        \"koyeb_sin\": 398,\n        \"koyeb_sfo\": 395,\n        \"ams\": 80,\n        \"iad\": 254,\n        \"koyeb_was\": 257,\n        \"fra\": 62,\n        \"cdg\": 114,\n        \"koyeb_tyo\": 534,\n        \"railway_us-west2\": 244,\n        \"koyeb_fra\": 63,\n        \"koyeb_par\": 116,\n        \"sin\": 400,\n        \"railway_us-east4-eqdc4a\": 232,\n        \"nrt\": 314,\n        \"railway_europe-west4-drams3a\": 92\n      },\n      {\n        \"timestamp\": \"2025-10-22T23:00:00.000Z\",\n        \"nrt\": 309,\n        \"koyeb_fra\": 64,\n        \"koyeb_par\": 114,\n        \"sin\": 404,\n        \"railway_europe-west4-drams3a\": 92,\n        \"railway_us-west2\": 250,\n        \"koyeb_sin\": 396,\n        \"railway_asia-southeast1-eqsg3a\": 242,\n        \"koyeb_sfo\": 315,\n        \"fra\": 62,\n        \"koyeb_was\": 245,\n        \"cdg\": 114,\n        \"ams\": 79,\n        \"iad\": 234,\n        \"koyeb_tyo\": 307,\n        \"railway_us-east4-eqdc4a\": 254,\n        \"lax\": 340\n      },\n      {\n        \"timestamp\": \"2025-10-23T00:00:00.000Z\",\n        \"koyeb_par\": 114,\n        \"nrt\": 430,\n        \"koyeb_fra\": 62,\n        \"sin\": 396,\n        \"railway_us-east4-eqdc4a\": 252,\n        \"railway_asia-southeast1-eqsg3a\": 403,\n        \"koyeb_was\": 250,\n        \"koyeb_sin\": 392,\n        \"railway_europe-west4-drams3a\": 93,\n        \"koyeb_sfo\": 242,\n        \"cdg\": 112,\n        \"fra\": 61,\n        \"iad\": 246,\n        \"ams\": 81,\n        \"railway_us-west2\": 262,\n        \"koyeb_tyo\": 314,\n        \"lax\": 336\n      },\n      {\n        \"timestamp\": \"2025-10-23T01:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 408,\n        \"koyeb_par\": 114,\n        \"railway_us-west2\": 250,\n        \"fra\": 62,\n        \"cdg\": 112,\n        \"koyeb_fra\": 62,\n        \"ams\": 81,\n        \"iad\": 250,\n        \"lax\": 210,\n        \"nrt\": 444,\n        \"railway_us-east4-eqdc4a\": 256,\n        \"koyeb_was\": 258,\n        \"sin\": 420,\n        \"koyeb_sin\": 401,\n        \"koyeb_sfo\": 242,\n        \"railway_europe-west4-drams3a\": 89,\n        \"koyeb_tyo\": 328\n      },\n      {\n        \"timestamp\": \"2025-10-23T02:00:00.000Z\",\n        \"sin\": 422,\n        \"nrt\": 335,\n        \"railway_asia-southeast1-eqsg3a\": 398,\n        \"koyeb_par\": 114,\n        \"koyeb_fra\": 62,\n        \"koyeb_tyo\": 320,\n        \"cdg\": 112,\n        \"fra\": 62,\n        \"lax\": 214,\n        \"railway_europe-west4-drams3a\": 76,\n        \"iad\": 248,\n        \"ams\": 80,\n        \"koyeb_was\": 258,\n        \"koyeb_sfo\": 249,\n        \"railway_us-west2\": 259,\n        \"railway_us-east4-eqdc4a\": 254,\n        \"koyeb_sin\": 411\n      },\n      {\n        \"timestamp\": \"2025-10-23T03:00:00.000Z\",\n        \"lax\": 295,\n        \"koyeb_par\": 114,\n        \"railway_us-east4-eqdc4a\": 248,\n        \"koyeb_fra\": 63,\n        \"iad\": 252,\n        \"ams\": 81,\n        \"cdg\": 111,\n        \"fra\": 62,\n        \"railway_us-west2\": 250,\n        \"railway_europe-west4-drams3a\": 84,\n        \"railway_asia-southeast1-eqsg3a\": 399,\n        \"koyeb_was\": 252,\n        \"koyeb_sfo\": 388,\n        \"koyeb_sin\": 430,\n        \"sin\": 434,\n        \"koyeb_tyo\": 337,\n        \"nrt\": 334\n      },\n      {\n        \"timestamp\": \"2025-10-23T04:00:00.000Z\",\n        \"ams\": 78,\n        \"iad\": 254,\n        \"koyeb_was\": 257,\n        \"lax\": 282,\n        \"koyeb_sin\": 531,\n        \"railway_europe-west4-drams3a\": 79,\n        \"koyeb_sfo\": 242,\n        \"koyeb_tyo\": 316,\n        \"railway_us-east4-eqdc4a\": 258,\n        \"fra\": 62,\n        \"cdg\": 105,\n        \"koyeb_par\": 110,\n        \"koyeb_fra\": 62,\n        \"nrt\": 444,\n        \"railway_us-west2\": 367,\n        \"railway_asia-southeast1-eqsg3a\": 514,\n        \"sin\": 524\n      },\n      {\n        \"timestamp\": \"2025-10-23T05:00:00.000Z\",\n        \"iad\": 256,\n        \"ams\": 80,\n        \"koyeb_was\": 256,\n        \"koyeb_sfo\": 262,\n        \"railway_us-west2\": 395,\n        \"koyeb_sin\": 466,\n        \"lax\": 211,\n        \"railway_asia-southeast1-eqsg3a\": 466,\n        \"railway_us-east4-eqdc4a\": 260,\n        \"koyeb_tyo\": 327,\n        \"cdg\": 112,\n        \"fra\": 62,\n        \"koyeb_par\": 116,\n        \"koyeb_fra\": 61,\n        \"railway_europe-west4-drams3a\": 90,\n        \"nrt\": 324,\n        \"sin\": 508\n      },\n      {\n        \"timestamp\": \"2025-10-23T06:00:00.000Z\",\n        \"railway_europe-west4-drams3a\": 94,\n        \"koyeb_fra\": 64,\n        \"koyeb_par\": 116,\n        \"ams\": 82,\n        \"iad\": 249,\n        \"lax\": 354,\n        \"railway_us-east4-eqdc4a\": 254,\n        \"fra\": 64,\n        \"cdg\": 114,\n        \"koyeb_sfo\": 278,\n        \"koyeb_sin\": 584,\n        \"koyeb_was\": 248,\n        \"railway_us-west2\": 401,\n        \"nrt\": 330,\n        \"railway_asia-southeast1-eqsg3a\": 480,\n        \"sin\": 558,\n        \"koyeb_tyo\": 336\n      },\n      {\n        \"timestamp\": \"2025-10-23T07:00:00.000Z\",\n        \"railway_us-west2\": 401,\n        \"nrt\": 331,\n        \"sin\": 500,\n        \"koyeb_fra\": 64,\n        \"railway_asia-southeast1-eqsg3a\": 470,\n        \"koyeb_par\": 115,\n        \"railway_us-east4-eqdc4a\": 253,\n        \"railway_europe-west4-drams3a\": 90,\n        \"fra\": 65,\n        \"koyeb_tyo\": 355,\n        \"cdg\": 112,\n        \"ams\": 68,\n        \"koyeb_sfo\": 394,\n        \"iad\": 248,\n        \"koyeb_sin\": 486,\n        \"koyeb_was\": 250,\n        \"lax\": 218\n      },\n      {\n        \"timestamp\": \"2025-10-23T08:00:00.000Z\",\n        \"railway_asia-southeast1-eqsg3a\": 450,\n        \"sin\": 544,\n        \"nrt\": 560,\n        \"koyeb_fra\": 64,\n        \"koyeb_par\": 116,\n        \"fra\": 65,\n        \"cdg\": 114,\n        \"koyeb_sin\": 451,\n        \"koyeb_sfo\": 254,\n        \"koyeb_was\": 254,\n        \"railway_us-west2\": 398,\n        \"koyeb_tyo\": 568,\n        \"lax\": 211,\n        \"railway_europe-west4-drams3a\": 87,\n        \"railway_us-east4-eqdc4a\": 256,\n        \"ams\": 83,\n        \"iad\": 253\n      },\n      {\n        \"timestamp\": \"2025-10-23T09:00:00.000Z\",\n        \"cdg\": 115,\n        \"koyeb_fra\": 65,\n        \"fra\": 67,\n        \"koyeb_par\": 117,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"railway_us-west2\": 261,\n        \"lax\": 210,\n        \"railway_asia-southeast1-eqsg3a\": 454,\n        \"iad\": 252,\n        \"ams\": 85,\n        \"sin\": 470,\n        \"koyeb_sin\": 561,\n        \"koyeb_sfo\": 247,\n        \"koyeb_was\": 252,\n        \"nrt\": 547,\n        \"railway_europe-west4-drams3a\": 92,\n        \"koyeb_tyo\": 507\n      },\n      {\n        \"timestamp\": \"2025-10-23T10:00:00.000Z\",\n        \"sin\": 570,\n        \"nrt\": 564,\n        \"railway_europe-west4-drams3a\": 91,\n        \"railway_us-west2\": 398,\n        \"railway_us-east4-eqdc4a\": 260,\n        \"koyeb_fra\": 65,\n        \"koyeb_par\": 116,\n        \"koyeb_tyo\": 556,\n        \"fra\": 65,\n        \"cdg\": 115,\n        \"railway_asia-southeast1-eqsg3a\": 476,\n        \"lax\": 210,\n        \"ams\": 86,\n        \"iad\": 253,\n        \"koyeb_sin\": 478,\n        \"koyeb_sfo\": 324,\n        \"koyeb_was\": 256\n      },\n      {\n        \"timestamp\": \"2025-10-23T11:00:00.000Z\",\n        \"railway_us-east4-eqdc4a\": 258,\n        \"sin\": 571,\n        \"railway_us-west2\": 403,\n        \"koyeb_tyo\": 341,\n        \"nrt\": 517,\n        \"koyeb_sin\": 472,\n        \"koyeb_sfo\": 386,\n        \"koyeb_was\": 258,\n        \"railway_asia-southeast1-eqsg3a\": 438,\n        \"cdg\": 116,\n        \"fra\": 65,\n        \"railway_europe-west4-drams3a\": 96,\n        \"lax\": 207,\n        \"koyeb_fra\": 66,\n        \"koyeb_par\": 114,\n        \"iad\": 254,\n        \"ams\": 77\n      },\n      {\n        \"timestamp\": \"2025-10-23T12:00:00.000Z\",\n        \"sin\": 578,\n        \"nrt\": 340,\n        \"railway_us-west2\": 392,\n        \"koyeb_par\": 117,\n        \"koyeb_fra\": 64,\n        \"railway_europe-west4-drams3a\": 98,\n        \"fra\": 65,\n        \"cdg\": 115,\n        \"railway_asia-southeast1-eqsg3a\": 480,\n        \"koyeb_tyo\": 342,\n        \"railway_us-east4-eqdc4a\": 262,\n        \"koyeb_was\": 252,\n        \"koyeb_sin\": 476,\n        \"koyeb_sfo\": 322,\n        \"lax\": 199,\n        \"ams\": 86,\n        \"iad\": 256\n      },\n      {\n        \"timestamp\": \"2025-10-23T13:00:00.000Z\",\n        \"nrt\": 343,\n        \"sin\": 571,\n        \"railway_europe-west4-drams3a\": 91,\n        \"railway_us-east4-eqdc4a\": 268,\n        \"koyeb_fra\": 67,\n        \"koyeb_par\": 117,\n        \"railway_us-west2\": 407,\n        \"koyeb_tyo\": 343,\n        \"cdg\": 116,\n        \"fra\": 66,\n        \"railway_asia-southeast1-eqsg3a\": 475,\n        \"iad\": 258,\n        \"ams\": 84,\n        \"koyeb_sin\": 474,\n        \"koyeb_sfo\": 371,\n        \"koyeb_was\": 261,\n        \"lax\": 336\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"sin\",\n      \"count\": 10134,\n      \"ok\": 10134,\n      \"p50Latency\": 409,\n      \"p75Latency\": 561,\n      \"p90Latency\": 754,\n      \"p95Latency\": 1006,\n      \"p99Latency\": 2088\n    },\n    {\n      \"region\": \"lhr\",\n      \"count\": 1,\n      \"ok\": 1,\n      \"p50Latency\": 113,\n      \"p75Latency\": 113,\n      \"p90Latency\": 113,\n      \"p95Latency\": 113,\n      \"p99Latency\": 113\n    },\n    {\n      \"region\": \"koyeb_fra\",\n      \"count\": 10140,\n      \"ok\": 10140,\n      \"p50Latency\": 65,\n      \"p75Latency\": 69,\n      \"p90Latency\": 75,\n      \"p95Latency\": 79,\n      \"p99Latency\": 91\n    },\n    {\n      \"region\": \"koyeb_was\",\n      \"count\": 10140,\n      \"ok\": 10140,\n      \"p50Latency\": 249,\n      \"p75Latency\": 267,\n      \"p90Latency\": 289,\n      \"p95Latency\": 297,\n      \"p99Latency\": 319\n    },\n    {\n      \"region\": \"koyeb_sin\",\n      \"count\": 10139,\n      \"ok\": 10139,\n      \"p50Latency\": 393,\n      \"p75Latency\": 467,\n      \"p90Latency\": 655,\n      \"p95Latency\": 1072,\n      \"p99Latency\": 2241\n    },\n    {\n      \"region\": \"railway_us-east4-eqdc4a\",\n      \"count\": 10138,\n      \"ok\": 10138,\n      \"p50Latency\": 251,\n      \"p75Latency\": 270,\n      \"p90Latency\": 292,\n      \"p95Latency\": 301,\n      \"p99Latency\": 336\n    },\n    {\n      \"region\": \"railway_us-west2\",\n      \"count\": 10138,\n      \"ok\": 10138,\n      \"p50Latency\": 309,\n      \"p75Latency\": 393,\n      \"p90Latency\": 436,\n      \"p95Latency\": 467,\n      \"p99Latency\": 540\n    },\n    {\n      \"region\": \"iad\",\n      \"count\": 10136,\n      \"ok\": 10136,\n      \"p50Latency\": 246,\n      \"p75Latency\": 264,\n      \"p90Latency\": 286,\n      \"p95Latency\": 294,\n      \"p99Latency\": 346\n    },\n    {\n      \"region\": \"ams\",\n      \"count\": 10141,\n      \"ok\": 10141,\n      \"p50Latency\": 82,\n      \"p75Latency\": 89,\n      \"p90Latency\": 96,\n      \"p95Latency\": 101,\n      \"p99Latency\": 112\n    },\n    {\n      \"region\": \"koyeb_sfo\",\n      \"count\": 10140,\n      \"ok\": 10140,\n      \"p50Latency\": 313,\n      \"p75Latency\": 405,\n      \"p90Latency\": 436,\n      \"p95Latency\": 476,\n      \"p99Latency\": 636\n    },\n    {\n      \"region\": \"railway_asia-southeast1-eqsg3a\",\n      \"count\": 10138,\n      \"ok\": 10138,\n      \"p50Latency\": 396,\n      \"p75Latency\": 459,\n      \"p90Latency\": 615,\n      \"p95Latency\": 1033,\n      \"p99Latency\": 2230\n    },\n    {\n      \"region\": \"koyeb_tyo\",\n      \"count\": 10138,\n      \"ok\": 10138,\n      \"p50Latency\": 338,\n      \"p75Latency\": 580,\n      \"p90Latency\": 608,\n      \"p95Latency\": 655,\n      \"p99Latency\": 1651\n    },\n    {\n      \"region\": \"nrt\",\n      \"count\": 10140,\n      \"ok\": 10140,\n      \"p50Latency\": 334,\n      \"p75Latency\": 579,\n      \"p90Latency\": 605,\n      \"p95Latency\": 646,\n      \"p99Latency\": 1703\n    },\n    {\n      \"region\": \"cdg\",\n      \"count\": 10182,\n      \"ok\": 10182,\n      \"p50Latency\": 113,\n      \"p75Latency\": 117,\n      \"p90Latency\": 121,\n      \"p95Latency\": 130,\n      \"p99Latency\": 140\n    },\n    {\n      \"region\": \"lax\",\n      \"count\": 10137,\n      \"ok\": 10137,\n      \"p50Latency\": 297,\n      \"p75Latency\": 367,\n      \"p90Latency\": 399,\n      \"p95Latency\": 418,\n      \"p99Latency\": 549\n    },\n    {\n      \"region\": \"koyeb_par\",\n      \"count\": 10140,\n      \"ok\": 10140,\n      \"p50Latency\": 116,\n      \"p75Latency\": 120,\n      \"p90Latency\": 139,\n      \"p95Latency\": 148,\n      \"p99Latency\": 162\n    },\n    {\n      \"region\": \"railway_europe-west4-drams3a\",\n      \"count\": 10137,\n      \"ok\": 10137,\n      \"p50Latency\": 92,\n      \"p75Latency\": 100,\n      \"p90Latency\": 110,\n      \"p95Latency\": 120,\n      \"p99Latency\": 130\n    },\n    {\n      \"region\": \"fra\",\n      \"count\": 10141,\n      \"ok\": 10141,\n      \"p50Latency\": 64,\n      \"p75Latency\": 69,\n      \"p90Latency\": 75,\n      \"p95Latency\": 79,\n      \"p99Latency\": 89\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/hono-vercel-fluid-compute/hono-cold.json",
    "content": "{\n  \"regions\": [\n    \"ams\",\n    \"arn\",\n    \"atl\",\n    \"bog\",\n    \"bom\",\n    \"bos\",\n    \"cdg\",\n    \"den\",\n    \"dfw\",\n    \"ewr\",\n    \"eze\",\n    \"fra\",\n    \"gdl\",\n    \"gig\",\n    \"gru\",\n    \"hkg\",\n    \"iad\",\n    \"jnb\",\n    \"lax\",\n    \"lhr\",\n    \"mad\",\n    \"mia\",\n    \"nrt\",\n    \"ord\",\n    \"otp\",\n    \"phx\",\n    \"scl\",\n    \"sjc\",\n    \"sea\",\n    \"sin\",\n    \"syd\",\n    \"yul\",\n    \"yyz\"\n  ],\n  \"data\": {\n    \"regions\": [\n      \"ams\",\n      \"arn\",\n      \"atl\",\n      \"bog\",\n      \"bom\",\n      \"bos\",\n      \"cdg\",\n      \"den\",\n      \"dfw\",\n      \"ewr\",\n      \"eze\",\n      \"fra\",\n      \"gdl\",\n      \"gig\",\n      \"gru\",\n      \"hkg\",\n      \"iad\",\n      \"jnb\",\n      \"lax\",\n      \"lhr\",\n      \"mad\",\n      \"mia\",\n      \"nrt\",\n      \"ord\",\n      \"otp\",\n      \"phx\",\n      \"scl\",\n      \"sea\",\n      \"sin\",\n      \"sjc\",\n      \"syd\",\n      \"yul\",\n      \"yyz\"\n    ],\n    \"data\": [\n      {\n        \"timestamp\": \"2025-08-18T16:00:00.000Z\",\n        \"yyz\": 132,\n        \"lhr\": 327,\n        \"bos\": 102,\n        \"cdg\": 167,\n        \"fra\": 142,\n        \"iad\": 113,\n        \"ams\": 207,\n        \"jnb\": 386,\n        \"gdl\": 479,\n        \"dfw\": 259,\n        \"sjc\": 151,\n        \"phx\": 174,\n        \"lax\": 193,\n        \"otp\": 218,\n        \"eze\": 283,\n        \"bog\": 434,\n        \"mad\": 345,\n        \"syd\": 263,\n        \"nrt\": 215,\n        \"mia\": 158,\n        \"bom\": 303,\n        \"ewr\": 131,\n        \"sin\": 502,\n        \"scl\": 347,\n        \"gru\": 207,\n        \"gig\": 286,\n        \"atl\": 346,\n        \"yul\": 114,\n        \"sea\": 209,\n        \"arn\": 433,\n        \"hkg\": 336,\n        \"den\": 236,\n        \"ord\": 183\n      },\n      {\n        \"timestamp\": \"2025-08-18T17:00:00.000Z\",\n        \"gru\": 192,\n        \"gig\": 376,\n        \"scl\": 296,\n        \"sea\": 216,\n        \"arn\": 233,\n        \"hkg\": 518,\n        \"den\": 300,\n        \"ord\": 124,\n        \"yul\": 116,\n        \"atl\": 155,\n        \"nrt\": 458,\n        \"mia\": 143,\n        \"mad\": 353,\n        \"syd\": 355,\n        \"sin\": 436,\n        \"ewr\": 100,\n        \"bom\": 307,\n        \"gdl\": 605,\n        \"dfw\": 238,\n        \"sjc\": 366,\n        \"phx\": 208,\n        \"iad\": 152,\n        \"ams\": 200,\n        \"jnb\": 382,\n        \"lax\": 135,\n        \"eze\": 266,\n        \"otp\": 249,\n        \"bog\": 357,\n        \"lhr\": 249,\n        \"yyz\": 158,\n        \"cdg\": 299,\n        \"fra\": 198,\n        \"bos\": 101\n      },\n      {\n        \"timestamp\": \"2025-08-18T18:00:00.000Z\",\n        \"bog\": 383,\n        \"eze\": 289,\n        \"otp\": 251,\n        \"lax\": 169,\n        \"phx\": 180,\n        \"sjc\": 132,\n        \"dfw\": 170,\n        \"gdl\": 565,\n        \"jnb\": 371,\n        \"ams\": 197,\n        \"iad\": 463,\n        \"fra\": 164,\n        \"cdg\": 160,\n        \"bos\": 305,\n        \"lhr\": 169,\n        \"yyz\": 150,\n        \"ord\": 106,\n        \"den\": 303,\n        \"hkg\": 346,\n        \"arn\": 201,\n        \"sea\": 194,\n        \"yul\": 111,\n        \"atl\": 170,\n        \"gig\": 383,\n        \"gru\": 401,\n        \"scl\": 344,\n        \"sin\": 425,\n        \"ewr\": 211,\n        \"bom\": 535,\n        \"mia\": 238,\n        \"nrt\": 265,\n        \"syd\": 319,\n        \"mad\": 339\n      },\n      {\n        \"timestamp\": \"2025-08-18T19:00:00.000Z\",\n        \"bom\": 289,\n        \"ewr\": 126,\n        \"sin\": 511,\n        \"syd\": 312,\n        \"mad\": 333,\n        \"mia\": 160,\n        \"nrt\": 224,\n        \"atl\": 157,\n        \"yul\": 109,\n        \"ord\": 214,\n        \"den\": 243,\n        \"hkg\": 336,\n        \"arn\": 202,\n        \"sea\": 218,\n        \"scl\": 362,\n        \"gig\": 220,\n        \"gru\": 209,\n        \"bos\": 119,\n        \"fra\": 391,\n        \"cdg\": 173,\n        \"yyz\": 125,\n        \"lhr\": 147,\n        \"bog\": 331,\n        \"otp\": 312,\n        \"eze\": 263,\n        \"lax\": 146,\n        \"jnb\": 381,\n        \"ams\": 206,\n        \"iad\": 99,\n        \"phx\": 184,\n        \"sjc\": 142,\n        \"dfw\": 266,\n        \"gdl\": 590\n      },\n      {\n        \"timestamp\": \"2025-08-18T20:00:00.000Z\",\n        \"dfw\": 251,\n        \"gdl\": 475,\n        \"phx\": 159,\n        \"sjc\": 114,\n        \"ams\": 185,\n        \"iad\": 157,\n        \"jnb\": 352,\n        \"otp\": 272,\n        \"eze\": 261,\n        \"lax\": 161,\n        \"bog\": 391,\n        \"lhr\": 155,\n        \"yyz\": 127,\n        \"fra\": 239,\n        \"cdg\": 152,\n        \"bos\": 124,\n        \"gru\": 278,\n        \"gig\": 361,\n        \"scl\": 496,\n        \"arn\": 248,\n        \"sea\": 175,\n        \"ord\": 142,\n        \"den\": 279,\n        \"hkg\": 275,\n        \"atl\": 190,\n        \"yul\": 227,\n        \"nrt\": 455,\n        \"mia\": 151,\n        \"mad\": 345,\n        \"syd\": 326,\n        \"sin\": 348,\n        \"bom\": 604,\n        \"ewr\": 189\n      },\n      {\n        \"timestamp\": \"2025-08-18T21:00:00.000Z\",\n        \"mad\": 332,\n        \"syd\": 301,\n        \"nrt\": 216,\n        \"mia\": 132,\n        \"ewr\": 183,\n        \"bom\": 321,\n        \"sin\": 449,\n        \"scl\": 338,\n        \"gru\": 193,\n        \"gig\": 353,\n        \"yul\": 114,\n        \"atl\": 169,\n        \"arn\": 212,\n        \"sea\": 169,\n        \"ord\": 128,\n        \"den\": 453,\n        \"hkg\": 334,\n        \"yyz\": 152,\n        \"lhr\": 160,\n        \"bos\": 108,\n        \"fra\": 319,\n        \"cdg\": 158,\n        \"ams\": 196,\n        \"iad\": 104,\n        \"jnb\": 366,\n        \"dfw\": 164,\n        \"gdl\": 704,\n        \"phx\": 188,\n        \"sjc\": 145,\n        \"eze\": 322,\n        \"otp\": 278,\n        \"lax\": 133,\n        \"bog\": 399\n      },\n      {\n        \"timestamp\": \"2025-08-18T22:00:00.000Z\",\n        \"bos\": 122,\n        \"cdg\": 496,\n        \"fra\": 154,\n        \"yyz\": 151,\n        \"lhr\": 158,\n        \"bog\": 425,\n        \"lax\": 138,\n        \"eze\": 494,\n        \"otp\": 298,\n        \"jnb\": 348,\n        \"iad\": 132,\n        \"ams\": 208,\n        \"sjc\": 119,\n        \"phx\": 170,\n        \"gdl\": 504,\n        \"dfw\": 163,\n        \"ewr\": 164,\n        \"bom\": 644,\n        \"sin\": 318,\n        \"syd\": 263,\n        \"mad\": 360,\n        \"mia\": 237,\n        \"nrt\": 323,\n        \"yul\": 109,\n        \"atl\": 156,\n        \"hkg\": 333,\n        \"den\": 246,\n        \"ord\": 169,\n        \"sea\": 190,\n        \"arn\": 217,\n        \"scl\": 312,\n        \"gig\": 355,\n        \"gru\": 194\n      },\n      {\n        \"timestamp\": \"2025-08-18T23:00:00.000Z\",\n        \"hkg\": 348,\n        \"den\": 229,\n        \"ord\": 139,\n        \"sea\": 165,\n        \"arn\": 223,\n        \"atl\": 125,\n        \"yul\": 123,\n        \"gig\": 215,\n        \"gru\": 229,\n        \"scl\": 537,\n        \"sin\": 322,\n        \"bom\": 252,\n        \"ewr\": 116,\n        \"mia\": 246,\n        \"nrt\": 217,\n        \"syd\": 380,\n        \"mad\": 339,\n        \"bog\": 351,\n        \"lax\": 153,\n        \"otp\": 260,\n        \"eze\": 311,\n        \"sjc\": 141,\n        \"phx\": 192,\n        \"gdl\": 595,\n        \"dfw\": 144,\n        \"jnb\": 349,\n        \"iad\": 73,\n        \"ams\": 182,\n        \"cdg\": 316,\n        \"fra\": 151,\n        \"bos\": 95,\n        \"lhr\": 154,\n        \"yyz\": 149\n      },\n      {\n        \"timestamp\": \"2025-08-19T00:00:00.000Z\",\n        \"bom\": 305,\n        \"ewr\": 162,\n        \"sin\": 592,\n        \"syd\": 314,\n        \"mad\": 329,\n        \"mia\": 135,\n        \"nrt\": 216,\n        \"atl\": 107,\n        \"yul\": 136,\n        \"den\": 226,\n        \"ord\": 130,\n        \"hkg\": 360,\n        \"arn\": 201,\n        \"sea\": 195,\n        \"scl\": 337,\n        \"gig\": 185,\n        \"gru\": 215,\n        \"bos\": 124,\n        \"fra\": 153,\n        \"cdg\": 144,\n        \"yyz\": 120,\n        \"lhr\": 170,\n        \"bog\": 349,\n        \"otp\": 240,\n        \"eze\": 484,\n        \"lax\": 141,\n        \"jnb\": 332,\n        \"ams\": 192,\n        \"iad\": 108,\n        \"phx\": 207,\n        \"sjc\": 133,\n        \"dfw\": 187,\n        \"gdl\": 452\n      },\n      {\n        \"timestamp\": \"2025-08-19T01:00:00.000Z\",\n        \"arn\": 210,\n        \"sea\": 155,\n        \"ord\": 128,\n        \"den\": 236,\n        \"hkg\": 339,\n        \"yul\": 136,\n        \"atl\": 173,\n        \"gru\": 193,\n        \"gig\": 266,\n        \"scl\": 336,\n        \"sin\": 448,\n        \"ewr\": 117,\n        \"bom\": 274,\n        \"nrt\": 381,\n        \"mia\": 192,\n        \"mad\": 338,\n        \"syd\": 303,\n        \"eze\": 295,\n        \"otp\": 272,\n        \"lax\": 138,\n        \"bog\": 384,\n        \"dfw\": 181,\n        \"gdl\": 593,\n        \"phx\": 179,\n        \"sjc\": 136,\n        \"ams\": 198,\n        \"iad\": 89,\n        \"jnb\": 362,\n        \"fra\": 144,\n        \"cdg\": 327,\n        \"bos\": 134,\n        \"lhr\": 142,\n        \"yyz\": 132\n      },\n      {\n        \"timestamp\": \"2025-08-19T02:00:00.000Z\",\n        \"bog\": 517,\n        \"lax\": 142,\n        \"eze\": 485,\n        \"otp\": 238,\n        \"jnb\": 351,\n        \"iad\": 243,\n        \"ams\": 216,\n        \"sjc\": 147,\n        \"phx\": 194,\n        \"gdl\": 584,\n        \"dfw\": 236,\n        \"bos\": 113,\n        \"cdg\": 169,\n        \"fra\": 158,\n        \"yyz\": 144,\n        \"lhr\": 144,\n        \"yul\": 156,\n        \"atl\": 177,\n        \"hkg\": 340,\n        \"ord\": 134,\n        \"den\": 257,\n        \"sea\": 189,\n        \"arn\": 238,\n        \"scl\": 541,\n        \"gig\": 357,\n        \"gru\": 192,\n        \"ewr\": 59,\n        \"bom\": 298,\n        \"sin\": 340,\n        \"syd\": 347,\n        \"mad\": 324,\n        \"mia\": 183,\n        \"nrt\": 270\n      },\n      {\n        \"timestamp\": \"2025-08-19T03:00:00.000Z\",\n        \"syd\": 251,\n        \"mad\": 318,\n        \"mia\": 242,\n        \"nrt\": 212,\n        \"bom\": 275,\n        \"ewr\": 137,\n        \"sin\": 574,\n        \"scl\": 501,\n        \"gig\": 344,\n        \"gru\": 223,\n        \"atl\": 307,\n        \"yul\": 113,\n        \"hkg\": 334,\n        \"den\": 271,\n        \"ord\": 145,\n        \"sea\": 202,\n        \"arn\": 260,\n        \"yyz\": 134,\n        \"lhr\": 150,\n        \"bos\": 136,\n        \"cdg\": 384,\n        \"fra\": 143,\n        \"jnb\": 328,\n        \"iad\": 104,\n        \"ams\": 192,\n        \"sjc\": 134,\n        \"phx\": 194,\n        \"gdl\": 740,\n        \"dfw\": 187,\n        \"bog\": 490,\n        \"lax\": 135,\n        \"otp\": 247,\n        \"eze\": 298\n      },\n      {\n        \"timestamp\": \"2025-08-19T04:00:00.000Z\",\n        \"gdl\": 596,\n        \"dfw\": 191,\n        \"sjc\": 143,\n        \"phx\": 184,\n        \"iad\": 198,\n        \"ams\": 200,\n        \"jnb\": 358,\n        \"lax\": 183,\n        \"eze\": 303,\n        \"otp\": 279,\n        \"bog\": 345,\n        \"lhr\": 154,\n        \"yyz\": 158,\n        \"cdg\": 155,\n        \"fra\": 149,\n        \"bos\": 139,\n        \"gru\": 354,\n        \"gig\": 513,\n        \"scl\": 501,\n        \"sea\": 186,\n        \"arn\": 216,\n        \"hkg\": 279,\n        \"ord\": 183,\n        \"den\": 308,\n        \"yul\": 114,\n        \"atl\": 139,\n        \"nrt\": 221,\n        \"mia\": 226,\n        \"mad\": 331,\n        \"syd\": 303,\n        \"sin\": 732,\n        \"ewr\": 768,\n        \"bom\": 293\n      },\n      {\n        \"timestamp\": \"2025-08-19T05:00:00.000Z\",\n        \"gru\": 200,\n        \"gig\": 453,\n        \"scl\": 342,\n        \"sea\": 172,\n        \"arn\": 244,\n        \"hkg\": 298,\n        \"den\": 271,\n        \"ord\": 79,\n        \"yul\": 103,\n        \"atl\": 199,\n        \"nrt\": 213,\n        \"mia\": 113,\n        \"mad\": 352,\n        \"syd\": 307,\n        \"sin\": 666,\n        \"ewr\": 185,\n        \"bom\": 577,\n        \"gdl\": 479,\n        \"dfw\": 176,\n        \"sjc\": 258,\n        \"phx\": 222,\n        \"iad\": 132,\n        \"ams\": 182,\n        \"jnb\": 359,\n        \"lax\": 140,\n        \"eze\": 294,\n        \"otp\": 268,\n        \"bog\": 240,\n        \"lhr\": 160,\n        \"yyz\": 143,\n        \"cdg\": 214,\n        \"fra\": 262,\n        \"bos\": 118\n      },\n      {\n        \"timestamp\": \"2025-08-19T06:00:00.000Z\",\n        \"jnb\": 381,\n        \"ams\": 187,\n        \"iad\": 50,\n        \"phx\": 190,\n        \"sjc\": 129,\n        \"dfw\": 198,\n        \"gdl\": 623,\n        \"bog\": 456,\n        \"eze\": 493,\n        \"otp\": 323,\n        \"lax\": 164,\n        \"yyz\": 177,\n        \"lhr\": 164,\n        \"bos\": 111,\n        \"fra\": 151,\n        \"cdg\": 146,\n        \"scl\": 382,\n        \"gig\": 223,\n        \"gru\": 149,\n        \"yul\": 106,\n        \"atl\": 461,\n        \"den\": 279,\n        \"ord\": 80,\n        \"hkg\": 338,\n        \"arn\": 260,\n        \"sea\": 173,\n        \"syd\": 302,\n        \"mad\": 335,\n        \"mia\": 123,\n        \"nrt\": 411,\n        \"ewr\": 51,\n        \"bom\": 284,\n        \"sin\": 426\n      },\n      {\n        \"timestamp\": \"2025-08-19T07:00:00.000Z\",\n        \"bom\": 569,\n        \"ewr\": 135,\n        \"sin\": 564,\n        \"syd\": 296,\n        \"mad\": 336,\n        \"mia\": 195,\n        \"nrt\": 456,\n        \"atl\": 178,\n        \"yul\": 99,\n        \"ord\": 182,\n        \"den\": 321,\n        \"hkg\": 331,\n        \"arn\": 253,\n        \"sea\": 228,\n        \"scl\": 330,\n        \"gig\": 211,\n        \"gru\": 160,\n        \"bos\": 99,\n        \"fra\": 148,\n        \"cdg\": 146,\n        \"yyz\": 133,\n        \"lhr\": 167,\n        \"bog\": 468,\n        \"otp\": 268,\n        \"eze\": 294,\n        \"lax\": 149,\n        \"jnb\": 357,\n        \"ams\": 200,\n        \"iad\": 129,\n        \"phx\": 180,\n        \"sjc\": 131,\n        \"dfw\": 162,\n        \"gdl\": 591\n      },\n      {\n        \"timestamp\": \"2025-08-19T08:00:00.000Z\",\n        \"yyz\": 140,\n        \"lhr\": 148,\n        \"bos\": 112,\n        \"fra\": 153,\n        \"cdg\": 314,\n        \"jnb\": 374,\n        \"ams\": 184,\n        \"iad\": 103,\n        \"phx\": 197,\n        \"sjc\": 126,\n        \"dfw\": 442,\n        \"gdl\": 814,\n        \"bog\": 676,\n        \"otp\": 527,\n        \"eze\": 290,\n        \"lax\": 143,\n        \"syd\": 264,\n        \"mad\": 335,\n        \"mia\": 200,\n        \"nrt\": 280,\n        \"bom\": 581,\n        \"ewr\": 126,\n        \"sin\": 368,\n        \"scl\": 343,\n        \"gig\": 365,\n        \"gru\": 162,\n        \"atl\": 164,\n        \"yul\": 96,\n        \"ord\": 135,\n        \"den\": 295,\n        \"hkg\": 334,\n        \"arn\": 209,\n        \"sea\": 194\n      },\n      {\n        \"timestamp\": \"2025-08-19T09:00:00.000Z\",\n        \"gig\": 205,\n        \"gru\": 398,\n        \"scl\": 343,\n        \"ord\": 170,\n        \"den\": 332,\n        \"hkg\": 275,\n        \"arn\": 397,\n        \"sea\": 184,\n        \"yul\": 112,\n        \"atl\": 123,\n        \"mia\": 150,\n        \"nrt\": 260,\n        \"syd\": 301,\n        \"mad\": 328,\n        \"sin\": 402,\n        \"ewr\": 119,\n        \"bom\": 542,\n        \"phx\": 190,\n        \"sjc\": 117,\n        \"dfw\": 209,\n        \"gdl\": 736,\n        \"jnb\": 383,\n        \"ams\": 222,\n        \"iad\": 118,\n        \"bog\": 462,\n        \"eze\": 265,\n        \"otp\": 299,\n        \"lax\": 141,\n        \"lhr\": 151,\n        \"yyz\": 143,\n        \"fra\": 358,\n        \"cdg\": 339,\n        \"bos\": 90\n      },\n      {\n        \"timestamp\": \"2025-08-19T10:00:00.000Z\",\n        \"scl\": 342,\n        \"gig\": 219,\n        \"gru\": 197,\n        \"yul\": 109,\n        \"atl\": 116,\n        \"hkg\": 328,\n        \"den\": 294,\n        \"ord\": 198,\n        \"sea\": 176,\n        \"arn\": 513,\n        \"syd\": 265,\n        \"mad\": 340,\n        \"mia\": 140,\n        \"nrt\": 215,\n        \"ewr\": 128,\n        \"bom\": 352,\n        \"sin\": 606,\n        \"jnb\": 369,\n        \"iad\": 124,\n        \"ams\": 185,\n        \"sjc\": 127,\n        \"phx\": 359,\n        \"gdl\": 500,\n        \"dfw\": 163,\n        \"bog\": 382,\n        \"lax\": 240,\n        \"eze\": 295,\n        \"otp\": 251,\n        \"yyz\": 123,\n        \"lhr\": 150,\n        \"bos\": 112,\n        \"cdg\": 257,\n        \"fra\": 148\n      },\n      {\n        \"timestamp\": \"2025-08-19T11:00:00.000Z\",\n        \"lhr\": 164,\n        \"yyz\": 138,\n        \"cdg\": 286,\n        \"fra\": 172,\n        \"bos\": 90,\n        \"sjc\": 121,\n        \"phx\": 189,\n        \"gdl\": 516,\n        \"dfw\": 186,\n        \"jnb\": 344,\n        \"iad\": 105,\n        \"ams\": 189,\n        \"bog\": 472,\n        \"lax\": 127,\n        \"otp\": 275,\n        \"eze\": 303,\n        \"mia\": 147,\n        \"nrt\": 322,\n        \"syd\": 298,\n        \"mad\": 328,\n        \"sin\": 690,\n        \"bom\": 327,\n        \"ewr\": 55,\n        \"gig\": 546,\n        \"gru\": 164,\n        \"scl\": 339,\n        \"hkg\": 343,\n        \"den\": 286,\n        \"ord\": 130,\n        \"sea\": 170,\n        \"arn\": 390,\n        \"atl\": 160,\n        \"yul\": 125\n      },\n      {\n        \"timestamp\": \"2025-08-19T12:00:00.000Z\",\n        \"sea\": 165,\n        \"arn\": 1156,\n        \"hkg\": 321,\n        \"den\": 244,\n        \"ord\": 142,\n        \"yul\": 141,\n        \"atl\": 120,\n        \"gru\": 213,\n        \"gig\": 400,\n        \"scl\": 566,\n        \"sin\": 426,\n        \"ewr\": 140,\n        \"bom\": 309,\n        \"nrt\": 259,\n        \"mia\": 188,\n        \"mad\": 721,\n        \"syd\": 299,\n        \"lax\": 160,\n        \"eze\": 612,\n        \"otp\": 242,\n        \"bog\": 389,\n        \"gdl\": 597,\n        \"dfw\": 183,\n        \"sjc\": 135,\n        \"phx\": 203,\n        \"iad\": 125,\n        \"ams\": 219,\n        \"jnb\": 347,\n        \"cdg\": 160,\n        \"fra\": 149,\n        \"bos\": 135,\n        \"lhr\": 161,\n        \"yyz\": 118\n      },\n      {\n        \"timestamp\": \"2025-08-19T13:00:00.000Z\",\n        \"bos\": 135,\n        \"cdg\": 159,\n        \"fra\": 150,\n        \"yyz\": 144,\n        \"lhr\": 202,\n        \"lax\": 140,\n        \"otp\": 274,\n        \"eze\": 691,\n        \"bog\": 344,\n        \"iad\": 111,\n        \"ams\": 185,\n        \"jnb\": 353,\n        \"gdl\": 698,\n        \"dfw\": 221,\n        \"sjc\": 128,\n        \"phx\": 191,\n        \"bom\": 318,\n        \"ewr\": 126,\n        \"sin\": 593,\n        \"mad\": 329,\n        \"syd\": 303,\n        \"nrt\": 570,\n        \"mia\": 152,\n        \"atl\": 170,\n        \"yul\": 119,\n        \"sea\": 192,\n        \"arn\": 280,\n        \"hkg\": 359,\n        \"den\": 235,\n        \"ord\": 165,\n        \"scl\": 367,\n        \"gru\": 333,\n        \"gig\": 356\n      },\n      {\n        \"timestamp\": \"2025-08-19T14:00:00.000Z\",\n        \"mia\": 146,\n        \"nrt\": 265,\n        \"syd\": 342,\n        \"mad\": 345,\n        \"sin\": 344,\n        \"ewr\": 70,\n        \"bom\": 335,\n        \"gig\": 187,\n        \"gru\": 219,\n        \"scl\": 490,\n        \"ord\": 202,\n        \"den\": 278,\n        \"hkg\": 345,\n        \"arn\": 252,\n        \"sea\": 183,\n        \"yul\": 110,\n        \"atl\": 321,\n        \"lhr\": 244,\n        \"yyz\": 143,\n        \"fra\": 167,\n        \"cdg\": 220,\n        \"bos\": 404,\n        \"phx\": 231,\n        \"sjc\": 134,\n        \"dfw\": 283,\n        \"gdl\": 487,\n        \"jnb\": 360,\n        \"ams\": 182,\n        \"iad\": 186,\n        \"bog\": 408,\n        \"eze\": 472,\n        \"otp\": 248,\n        \"lax\": 140\n      },\n      {\n        \"timestamp\": \"2025-08-19T15:00:00.000Z\",\n        \"jnb\": 368,\n        \"ams\": 208,\n        \"iad\": 124,\n        \"phx\": 222,\n        \"sjc\": 146,\n        \"dfw\": 172,\n        \"gdl\": 505,\n        \"bog\": 502,\n        \"otp\": 295,\n        \"eze\": 266,\n        \"lax\": 157,\n        \"yyz\": 139,\n        \"lhr\": 153,\n        \"bos\": 248,\n        \"fra\": 281,\n        \"cdg\": 686,\n        \"scl\": 299,\n        \"gig\": 387,\n        \"gru\": 210,\n        \"atl\": 112,\n        \"yul\": 109,\n        \"ord\": 160,\n        \"den\": 254,\n        \"hkg\": 329,\n        \"arn\": 226,\n        \"sea\": 182,\n        \"syd\": 271,\n        \"mad\": 332,\n        \"mia\": 184,\n        \"nrt\": 258,\n        \"bom\": 290,\n        \"ewr\": 129,\n        \"sin\": 445\n      },\n      {\n        \"timestamp\": \"2025-08-19T16:00:00.000Z\",\n        \"lhr\": 166,\n        \"yyz\": 115,\n        \"cdg\": 160,\n        \"fra\": 155,\n        \"bos\": 123,\n        \"sjc\": 184,\n        \"phx\": 298,\n        \"gdl\": 806,\n        \"dfw\": 415,\n        \"jnb\": 355,\n        \"iad\": 111,\n        \"ams\": 211,\n        \"bog\": 380,\n        \"lax\": 150,\n        \"otp\": 298,\n        \"eze\": 499,\n        \"mia\": 229,\n        \"nrt\": 554,\n        \"syd\": 304,\n        \"mad\": 342,\n        \"sin\": 455,\n        \"bom\": 275,\n        \"ewr\": 122,\n        \"gig\": 383,\n        \"gru\": 340,\n        \"scl\": 362,\n        \"hkg\": 264,\n        \"ord\": 236,\n        \"den\": 551,\n        \"sea\": 166,\n        \"arn\": 243,\n        \"atl\": 139,\n        \"yul\": 121\n      },\n      {\n        \"timestamp\": \"2025-08-19T17:00:00.000Z\",\n        \"hkg\": 342,\n        \"den\": 238,\n        \"ord\": 244,\n        \"sea\": 207,\n        \"arn\": 216,\n        \"yul\": 129,\n        \"atl\": 179,\n        \"gig\": 206,\n        \"gru\": 166,\n        \"scl\": 348,\n        \"sin\": 345,\n        \"ewr\": 133,\n        \"bom\": 270,\n        \"mia\": 160,\n        \"nrt\": 375,\n        \"syd\": 308,\n        \"mad\": 344,\n        \"bog\": 405,\n        \"lax\": 167,\n        \"eze\": 307,\n        \"otp\": 238,\n        \"sjc\": 127,\n        \"phx\": 180,\n        \"gdl\": 655,\n        \"dfw\": 247,\n        \"jnb\": 396,\n        \"iad\": 214,\n        \"ams\": 204,\n        \"cdg\": 152,\n        \"fra\": 154,\n        \"bos\": 90,\n        \"lhr\": 148,\n        \"yyz\": 137\n      },\n      {\n        \"timestamp\": \"2025-08-19T18:00:00.000Z\",\n        \"eze\": 308,\n        \"otp\": 279,\n        \"lax\": 179,\n        \"bog\": 349,\n        \"ams\": 220,\n        \"iad\": 109,\n        \"jnb\": 352,\n        \"dfw\": 251,\n        \"gdl\": 499,\n        \"phx\": 183,\n        \"sjc\": 124,\n        \"bos\": 108,\n        \"fra\": 152,\n        \"cdg\": 164,\n        \"yyz\": 239,\n        \"lhr\": 184,\n        \"yul\": 128,\n        \"atl\": 164,\n        \"arn\": 209,\n        \"sea\": 176,\n        \"den\": 227,\n        \"ord\": 134,\n        \"hkg\": 341,\n        \"scl\": 643,\n        \"gru\": 191,\n        \"gig\": 410,\n        \"ewr\": 135,\n        \"bom\": 282,\n        \"sin\": 429,\n        \"mad\": 343,\n        \"syd\": 300,\n        \"nrt\": 460,\n        \"mia\": 193\n      },\n      {\n        \"timestamp\": \"2025-08-19T19:00:00.000Z\",\n        \"fra\": 185,\n        \"cdg\": 174,\n        \"bos\": 164,\n        \"lhr\": 160,\n        \"yyz\": 138,\n        \"bog\": 233,\n        \"otp\": 316,\n        \"eze\": 566,\n        \"lax\": 156,\n        \"phx\": 211,\n        \"sjc\": 128,\n        \"dfw\": 174,\n        \"gdl\": 509,\n        \"jnb\": 364,\n        \"ams\": 187,\n        \"iad\": 226,\n        \"sin\": 725,\n        \"bom\": 289,\n        \"ewr\": 181,\n        \"mia\": 192,\n        \"nrt\": 257,\n        \"syd\": 280,\n        \"mad\": 445,\n        \"ord\": 143,\n        \"den\": 262,\n        \"hkg\": 285,\n        \"arn\": 210,\n        \"sea\": 192,\n        \"atl\": 151,\n        \"yul\": 103,\n        \"gig\": 204,\n        \"gru\": 216,\n        \"scl\": 339\n      },\n      {\n        \"timestamp\": \"2025-08-19T20:00:00.000Z\",\n        \"yul\": 115,\n        \"atl\": 175,\n        \"arn\": 221,\n        \"sea\": 170,\n        \"den\": 253,\n        \"ord\": 128,\n        \"hkg\": 276,\n        \"scl\": 299,\n        \"gru\": 207,\n        \"gig\": 390,\n        \"ewr\": 127,\n        \"bom\": 301,\n        \"sin\": 413,\n        \"mad\": 342,\n        \"syd\": 313,\n        \"nrt\": 455,\n        \"mia\": 243,\n        \"eze\": 303,\n        \"otp\": 278,\n        \"lax\": 131,\n        \"bog\": 371,\n        \"ams\": 229,\n        \"iad\": 109,\n        \"jnb\": 354,\n        \"dfw\": 246,\n        \"gdl\": 474,\n        \"phx\": 237,\n        \"sjc\": 125,\n        \"bos\": 404,\n        \"fra\": 152,\n        \"cdg\": 336,\n        \"yyz\": 126,\n        \"lhr\": 279\n      },\n      {\n        \"timestamp\": \"2025-08-19T21:00:00.000Z\",\n        \"yyz\": 205,\n        \"lhr\": 153,\n        \"bos\": 106,\n        \"fra\": 154,\n        \"cdg\": 155,\n        \"ams\": 189,\n        \"iad\": 134,\n        \"jnb\": 351,\n        \"dfw\": 256,\n        \"gdl\": 594,\n        \"phx\": 224,\n        \"sjc\": 123,\n        \"otp\": 227,\n        \"eze\": 388,\n        \"lax\": 130,\n        \"bog\": 476,\n        \"mad\": 373,\n        \"syd\": 300,\n        \"nrt\": 268,\n        \"mia\": 191,\n        \"bom\": 291,\n        \"ewr\": 114,\n        \"sin\": 389,\n        \"scl\": 287,\n        \"gru\": 224,\n        \"gig\": 206,\n        \"atl\": 123,\n        \"yul\": 110,\n        \"arn\": 220,\n        \"sea\": 192,\n        \"ord\": 136,\n        \"den\": 294,\n        \"hkg\": 450\n      },\n      {\n        \"timestamp\": \"2025-08-19T22:00:00.000Z\",\n        \"mia\": 187,\n        \"nrt\": 292,\n        \"syd\": 309,\n        \"mad\": 369,\n        \"sin\": 469,\n        \"bom\": 272,\n        \"ewr\": 134,\n        \"gig\": 297,\n        \"gru\": 425,\n        \"scl\": 344,\n        \"hkg\": 341,\n        \"ord\": 193,\n        \"den\": 302,\n        \"sea\": 200,\n        \"arn\": 250,\n        \"atl\": 122,\n        \"yul\": 128,\n        \"lhr\": 188,\n        \"yyz\": 163,\n        \"cdg\": 173,\n        \"fra\": 154,\n        \"bos\": 131,\n        \"sjc\": 131,\n        \"phx\": 237,\n        \"gdl\": 604,\n        \"dfw\": 215,\n        \"jnb\": 367,\n        \"iad\": 155,\n        \"ams\": 245,\n        \"bog\": 747,\n        \"lax\": 155,\n        \"otp\": 267,\n        \"eze\": 484\n      },\n      {\n        \"timestamp\": \"2025-08-19T23:00:00.000Z\",\n        \"fra\": 145,\n        \"cdg\": 157,\n        \"bos\": 97,\n        \"lhr\": 179,\n        \"yyz\": 127,\n        \"bog\": 598,\n        \"otp\": 289,\n        \"eze\": 260,\n        \"lax\": 133,\n        \"phx\": 200,\n        \"sjc\": 121,\n        \"dfw\": 222,\n        \"gdl\": 580,\n        \"jnb\": 397,\n        \"ams\": 200,\n        \"iad\": 106,\n        \"sin\": 588,\n        \"bom\": 282,\n        \"ewr\": 122,\n        \"mia\": 120,\n        \"nrt\": 262,\n        \"syd\": 318,\n        \"mad\": 364,\n        \"ord\": 154,\n        \"den\": 288,\n        \"hkg\": 331,\n        \"arn\": 197,\n        \"sea\": 191,\n        \"atl\": 104,\n        \"yul\": 127,\n        \"gig\": 389,\n        \"gru\": 186,\n        \"scl\": 395\n      },\n      {\n        \"timestamp\": \"2025-08-20T00:00:00.000Z\",\n        \"bos\": 85,\n        \"cdg\": 149,\n        \"fra\": 148,\n        \"yyz\": 141,\n        \"lhr\": 164,\n        \"bog\": 338,\n        \"lax\": 140,\n        \"eze\": 301,\n        \"otp\": 266,\n        \"jnb\": 346,\n        \"iad\": 197,\n        \"ams\": 205,\n        \"sjc\": 131,\n        \"phx\": 193,\n        \"gdl\": 591,\n        \"dfw\": 236,\n        \"ewr\": 114,\n        \"bom\": 290,\n        \"sin\": 286,\n        \"syd\": 307,\n        \"mad\": 664,\n        \"mia\": 186,\n        \"nrt\": 221,\n        \"yul\": 111,\n        \"atl\": 200,\n        \"hkg\": 336,\n        \"den\": 258,\n        \"ord\": 96,\n        \"sea\": 166,\n        \"arn\": 217,\n        \"scl\": 294,\n        \"gig\": 205,\n        \"gru\": 194\n      },\n      {\n        \"timestamp\": \"2025-08-20T01:00:00.000Z\",\n        \"ewr\": 182,\n        \"bom\": 291,\n        \"sin\": 649,\n        \"mad\": 400,\n        \"syd\": 301,\n        \"nrt\": 444,\n        \"mia\": 120,\n        \"yul\": 119,\n        \"atl\": 133,\n        \"arn\": 210,\n        \"sea\": 189,\n        \"ord\": 167,\n        \"den\": 258,\n        \"hkg\": 352,\n        \"scl\": 364,\n        \"gru\": 177,\n        \"gig\": 311,\n        \"bos\": 133,\n        \"fra\": 145,\n        \"cdg\": 148,\n        \"yyz\": 152,\n        \"lhr\": 175,\n        \"eze\": 306,\n        \"otp\": 260,\n        \"lax\": 171,\n        \"bog\": 522,\n        \"ams\": 209,\n        \"iad\": 97,\n        \"jnb\": 378,\n        \"dfw\": 161,\n        \"gdl\": 634,\n        \"phx\": 181,\n        \"sjc\": 143\n      },\n      {\n        \"timestamp\": \"2025-08-20T02:00:00.000Z\",\n        \"yyz\": 146,\n        \"lhr\": 149,\n        \"bos\": 107,\n        \"cdg\": 156,\n        \"fra\": 341,\n        \"iad\": 41,\n        \"ams\": 213,\n        \"jnb\": 358,\n        \"gdl\": 496,\n        \"dfw\": 227,\n        \"sjc\": 117,\n        \"phx\": 182,\n        \"lax\": 172,\n        \"otp\": 283,\n        \"eze\": 289,\n        \"bog\": 510,\n        \"mad\": 347,\n        \"syd\": 306,\n        \"nrt\": 495,\n        \"mia\": 117,\n        \"bom\": 304,\n        \"ewr\": 164,\n        \"sin\": 917,\n        \"scl\": 418,\n        \"gru\": 201,\n        \"gig\": 389,\n        \"atl\": 220,\n        \"yul\": 133,\n        \"sea\": 163,\n        \"arn\": 213,\n        \"hkg\": 452,\n        \"ord\": 142,\n        \"den\": 242\n      },\n      {\n        \"timestamp\": \"2025-08-20T03:00:00.000Z\",\n        \"gig\": 408,\n        \"gru\": 192,\n        \"scl\": 362,\n        \"hkg\": 481,\n        \"den\": 267,\n        \"ord\": 193,\n        \"sea\": 205,\n        \"arn\": 222,\n        \"atl\": 194,\n        \"yul\": 136,\n        \"mia\": 231,\n        \"nrt\": 286,\n        \"syd\": 303,\n        \"mad\": 356,\n        \"sin\": 1133,\n        \"bom\": 303,\n        \"ewr\": 194,\n        \"sjc\": 126,\n        \"phx\": 182,\n        \"gdl\": 585,\n        \"dfw\": 151,\n        \"jnb\": 374,\n        \"iad\": 106,\n        \"ams\": 207,\n        \"bog\": 353,\n        \"lax\": 148,\n        \"otp\": 271,\n        \"eze\": 297,\n        \"lhr\": 135,\n        \"yyz\": 187,\n        \"cdg\": 164,\n        \"fra\": 156,\n        \"bos\": 321\n      },\n      {\n        \"timestamp\": \"2025-08-20T04:00:00.000Z\",\n        \"jnb\": 369,\n        \"ams\": 201,\n        \"iad\": 142,\n        \"phx\": 225,\n        \"sjc\": 118,\n        \"dfw\": 164,\n        \"gdl\": 606,\n        \"bog\": 390,\n        \"otp\": 255,\n        \"eze\": 348,\n        \"lax\": 122,\n        \"yyz\": 136,\n        \"lhr\": 142,\n        \"bos\": 122,\n        \"fra\": 197,\n        \"cdg\": 168,\n        \"scl\": 529,\n        \"gig\": 205,\n        \"gru\": 226,\n        \"atl\": 233,\n        \"yul\": 139,\n        \"ord\": 167,\n        \"den\": 245,\n        \"hkg\": 282,\n        \"arn\": 205,\n        \"sea\": 184,\n        \"syd\": 264,\n        \"mad\": 317,\n        \"mia\": 167,\n        \"nrt\": 498,\n        \"bom\": 322,\n        \"ewr\": 135,\n        \"sin\": 471\n      },\n      {\n        \"timestamp\": \"2025-08-20T05:00:00.000Z\",\n        \"sjc\": 146,\n        \"phx\": 187,\n        \"gdl\": 611,\n        \"dfw\": 160,\n        \"jnb\": 385,\n        \"iad\": 258,\n        \"ams\": 230,\n        \"bog\": 495,\n        \"lax\": 140,\n        \"otp\": 242,\n        \"eze\": 505,\n        \"lhr\": 177,\n        \"yyz\": 128,\n        \"cdg\": 173,\n        \"fra\": 152,\n        \"bos\": 106,\n        \"gig\": 363,\n        \"gru\": 170,\n        \"scl\": 296,\n        \"hkg\": 401,\n        \"den\": 263,\n        \"ord\": 98,\n        \"sea\": 213,\n        \"arn\": 253,\n        \"atl\": 159,\n        \"yul\": 129,\n        \"mia\": 177,\n        \"nrt\": 270,\n        \"syd\": 351,\n        \"mad\": 334,\n        \"sin\": 804,\n        \"bom\": 508,\n        \"ewr\": 122\n      },\n      {\n        \"timestamp\": \"2025-08-20T06:00:00.000Z\",\n        \"den\": 248,\n        \"ord\": 131,\n        \"hkg\": 282,\n        \"arn\": 229,\n        \"sea\": 176,\n        \"yul\": 130,\n        \"atl\": 141,\n        \"gig\": 216,\n        \"gru\": 184,\n        \"scl\": 336,\n        \"sin\": 627,\n        \"ewr\": 193,\n        \"bom\": 295,\n        \"mia\": 239,\n        \"nrt\": 275,\n        \"syd\": 347,\n        \"mad\": 358,\n        \"bog\": 782,\n        \"eze\": 299,\n        \"otp\": 282,\n        \"lax\": 138,\n        \"phx\": 182,\n        \"sjc\": 121,\n        \"dfw\": 145,\n        \"gdl\": 657,\n        \"jnb\": 388,\n        \"ams\": 206,\n        \"iad\": 138,\n        \"fra\": 145,\n        \"cdg\": 156,\n        \"bos\": 113,\n        \"lhr\": 152,\n        \"yyz\": 131\n      },\n      {\n        \"timestamp\": \"2025-08-20T07:00:00.000Z\",\n        \"bos\": 98,\n        \"fra\": 204,\n        \"cdg\": 332,\n        \"yyz\": 141,\n        \"lhr\": 161,\n        \"eze\": 303,\n        \"otp\": 280,\n        \"lax\": 155,\n        \"bog\": 500,\n        \"ams\": 189,\n        \"iad\": 172,\n        \"jnb\": 351,\n        \"dfw\": 222,\n        \"gdl\": 662,\n        \"phx\": 185,\n        \"sjc\": 157,\n        \"ewr\": 128,\n        \"bom\": 298,\n        \"sin\": 283,\n        \"mad\": 320,\n        \"syd\": 261,\n        \"nrt\": 228,\n        \"mia\": 202,\n        \"yul\": 95,\n        \"atl\": 118,\n        \"arn\": 458,\n        \"sea\": 189,\n        \"ord\": 135,\n        \"den\": 232,\n        \"hkg\": 397,\n        \"scl\": 352,\n        \"gru\": 480,\n        \"gig\": 363\n      },\n      {\n        \"timestamp\": \"2025-08-20T08:00:00.000Z\",\n        \"yyz\": 133,\n        \"lhr\": 429,\n        \"bos\": 140,\n        \"fra\": 153,\n        \"cdg\": 308,\n        \"jnb\": 350,\n        \"ams\": 198,\n        \"iad\": 217,\n        \"phx\": 184,\n        \"sjc\": 158,\n        \"dfw\": 199,\n        \"gdl\": 363,\n        \"bog\": 378,\n        \"eze\": 491,\n        \"otp\": 301,\n        \"lax\": 158,\n        \"syd\": 298,\n        \"mad\": 357,\n        \"mia\": 237,\n        \"nrt\": 273,\n        \"ewr\": 125,\n        \"bom\": 540,\n        \"sin\": 452,\n        \"scl\": 350,\n        \"gig\": 376,\n        \"gru\": 168,\n        \"yul\": 100,\n        \"atl\": 192,\n        \"ord\": 184,\n        \"den\": 239,\n        \"hkg\": 330,\n        \"arn\": 351,\n        \"sea\": 165\n      },\n      {\n        \"timestamp\": \"2025-08-20T09:00:00.000Z\",\n        \"mad\": 379,\n        \"syd\": 314,\n        \"nrt\": 238,\n        \"mia\": 159,\n        \"ewr\": 134,\n        \"bom\": 343,\n        \"sin\": 380,\n        \"scl\": 341,\n        \"gru\": 174,\n        \"gig\": 192,\n        \"yul\": 160,\n        \"atl\": 134,\n        \"sea\": 197,\n        \"arn\": 421,\n        \"hkg\": 338,\n        \"den\": 266,\n        \"ord\": 147,\n        \"yyz\": 136,\n        \"lhr\": 165,\n        \"bos\": 167,\n        \"cdg\": 526,\n        \"fra\": 155,\n        \"iad\": 54,\n        \"ams\": 201,\n        \"jnb\": 399,\n        \"gdl\": 848,\n        \"dfw\": 211,\n        \"sjc\": 124,\n        \"phx\": 203,\n        \"lax\": 132,\n        \"eze\": 245,\n        \"otp\": 284,\n        \"bog\": 393\n      },\n      {\n        \"timestamp\": \"2025-08-20T10:00:00.000Z\",\n        \"lhr\": 150,\n        \"yyz\": 132,\n        \"fra\": 158,\n        \"cdg\": 296,\n        \"bos\": 95,\n        \"dfw\": 199,\n        \"gdl\": 586,\n        \"phx\": 207,\n        \"sjc\": 118,\n        \"ams\": 207,\n        \"iad\": 162,\n        \"jnb\": 362,\n        \"otp\": 245,\n        \"eze\": 490,\n        \"lax\": 141,\n        \"bog\": 496,\n        \"nrt\": 210,\n        \"mia\": 181,\n        \"mad\": 352,\n        \"syd\": 302,\n        \"sin\": 422,\n        \"bom\": 312,\n        \"ewr\": 176,\n        \"gru\": 227,\n        \"gig\": 393,\n        \"scl\": 398,\n        \"arn\": 393,\n        \"sea\": 196,\n        \"ord\": 160,\n        \"den\": 251,\n        \"hkg\": 267,\n        \"atl\": 297,\n        \"yul\": 95\n      },\n      {\n        \"timestamp\": \"2025-08-20T11:00:00.000Z\",\n        \"ord\": 87,\n        \"den\": 258,\n        \"hkg\": 338,\n        \"arn\": 352,\n        \"sea\": 171,\n        \"atl\": 172,\n        \"yul\": 128,\n        \"gig\": 204,\n        \"gru\": 182,\n        \"scl\": 307,\n        \"sin\": 468,\n        \"bom\": 510,\n        \"ewr\": 190,\n        \"mia\": 179,\n        \"nrt\": 220,\n        \"syd\": 362,\n        \"mad\": 335,\n        \"bog\": 520,\n        \"otp\": 287,\n        \"eze\": 307,\n        \"lax\": 153,\n        \"phx\": 182,\n        \"sjc\": 113,\n        \"dfw\": 202,\n        \"gdl\": 452,\n        \"jnb\": 390,\n        \"ams\": 212,\n        \"iad\": 174,\n        \"fra\": 157,\n        \"cdg\": 160,\n        \"bos\": 181,\n        \"lhr\": 318,\n        \"yyz\": 156\n      },\n      {\n        \"timestamp\": \"2025-08-20T12:00:00.000Z\",\n        \"atl\": 120,\n        \"yul\": 100,\n        \"sea\": 187,\n        \"arn\": 244,\n        \"hkg\": 337,\n        \"ord\": 148,\n        \"den\": 234,\n        \"scl\": 334,\n        \"gru\": 167,\n        \"gig\": 248,\n        \"bom\": 323,\n        \"ewr\": 200,\n        \"sin\": 356,\n        \"mad\": 343,\n        \"syd\": 344,\n        \"nrt\": 304,\n        \"mia\": 229,\n        \"lax\": 176,\n        \"otp\": 308,\n        \"eze\": 505,\n        \"bog\": 460,\n        \"iad\": 98,\n        \"ams\": 176,\n        \"jnb\": 379,\n        \"gdl\": 496,\n        \"dfw\": 147,\n        \"sjc\": 136,\n        \"phx\": 188,\n        \"bos\": 134,\n        \"cdg\": 165,\n        \"fra\": 139,\n        \"yyz\": 124,\n        \"lhr\": 159\n      },\n      {\n        \"timestamp\": \"2025-08-20T13:00:00.000Z\",\n        \"bog\": 480,\n        \"otp\": 270,\n        \"eze\": 399,\n        \"lax\": 165,\n        \"phx\": 186,\n        \"sjc\": 119,\n        \"dfw\": 281,\n        \"gdl\": 498,\n        \"jnb\": 354,\n        \"ams\": 194,\n        \"iad\": 97,\n        \"fra\": 148,\n        \"cdg\": 155,\n        \"bos\": 187,\n        \"lhr\": 151,\n        \"yyz\": 150,\n        \"ord\": 104,\n        \"den\": 238,\n        \"hkg\": 354,\n        \"arn\": 237,\n        \"sea\": 193,\n        \"atl\": 213,\n        \"yul\": 150,\n        \"gig\": 457,\n        \"gru\": 185,\n        \"scl\": 525,\n        \"sin\": 438,\n        \"bom\": 322,\n        \"ewr\": 207,\n        \"mia\": 170,\n        \"nrt\": 266,\n        \"syd\": 304,\n        \"mad\": 336\n      },\n      {\n        \"timestamp\": \"2025-08-20T14:00:00.000Z\",\n        \"yul\": 132,\n        \"atl\": 196,\n        \"hkg\": 282,\n        \"den\": 261,\n        \"ord\": 173,\n        \"sea\": 208,\n        \"arn\": 391,\n        \"scl\": 542,\n        \"gig\": 520,\n        \"gru\": 288,\n        \"ewr\": 197,\n        \"bom\": 317,\n        \"sin\": 501,\n        \"syd\": 349,\n        \"mad\": 341,\n        \"mia\": 239,\n        \"nrt\": 267,\n        \"bog\": 488,\n        \"lax\": 147,\n        \"eze\": 523,\n        \"otp\": 316,\n        \"jnb\": 363,\n        \"iad\": 95,\n        \"ams\": 309,\n        \"sjc\": 129,\n        \"phx\": 183,\n        \"gdl\": 645,\n        \"dfw\": 281,\n        \"bos\": 110,\n        \"cdg\": 390,\n        \"fra\": 160,\n        \"yyz\": 145,\n        \"lhr\": 162\n      },\n      {\n        \"timestamp\": \"2025-08-20T15:00:00.000Z\",\n        \"yyz\": 145,\n        \"lhr\": 255,\n        \"bos\": 114,\n        \"cdg\": 185,\n        \"fra\": 163,\n        \"iad\": 48,\n        \"ams\": 205,\n        \"jnb\": 414,\n        \"gdl\": 626,\n        \"dfw\": 206,\n        \"sjc\": 118,\n        \"phx\": 646,\n        \"lax\": 170,\n        \"eze\": 452,\n        \"otp\": 270,\n        \"bog\": 394,\n        \"mad\": 346,\n        \"syd\": 306,\n        \"nrt\": 268,\n        \"mia\": 122,\n        \"ewr\": 188,\n        \"bom\": 666,\n        \"sin\": 271,\n        \"scl\": 383,\n        \"gru\": 205,\n        \"gig\": 204,\n        \"yul\": 121,\n        \"atl\": 236,\n        \"sea\": 191,\n        \"arn\": 193,\n        \"hkg\": 274,\n        \"den\": 235,\n        \"ord\": 138\n      },\n      {\n        \"timestamp\": \"2025-08-20T16:00:00.000Z\",\n        \"ord\": 130,\n        \"den\": 226,\n        \"hkg\": 786,\n        \"arn\": 207,\n        \"sea\": 168,\n        \"atl\": 250,\n        \"yul\": 113,\n        \"gig\": 208,\n        \"gru\": 164,\n        \"scl\": 308,\n        \"sin\": 447,\n        \"bom\": 295,\n        \"ewr\": 114,\n        \"mia\": 226,\n        \"nrt\": 231,\n        \"syd\": 257,\n        \"mad\": 341,\n        \"bog\": 520,\n        \"otp\": 260,\n        \"eze\": 294,\n        \"lax\": 155,\n        \"phx\": 209,\n        \"sjc\": 121,\n        \"dfw\": 277,\n        \"gdl\": 619,\n        \"jnb\": 358,\n        \"ams\": 195,\n        \"iad\": 103,\n        \"fra\": 144,\n        \"cdg\": 295,\n        \"bos\": 103,\n        \"lhr\": 152,\n        \"yyz\": 145\n      },\n      {\n        \"timestamp\": \"2025-08-20T17:00:00.000Z\",\n        \"lax\": 523,\n        \"otp\": 352,\n        \"eze\": 387,\n        \"bog\": 465,\n        \"gdl\": 634,\n        \"dfw\": 151,\n        \"sjc\": 125,\n        \"phx\": 189,\n        \"iad\": 98,\n        \"ams\": 183,\n        \"jnb\": 373,\n        \"cdg\": 292,\n        \"fra\": 158,\n        \"bos\": 122,\n        \"lhr\": 430,\n        \"yyz\": 136,\n        \"sea\": 158,\n        \"arn\": 205,\n        \"hkg\": 354,\n        \"den\": 249,\n        \"ord\": 180,\n        \"atl\": 182,\n        \"yul\": 134,\n        \"gru\": 218,\n        \"gig\": 190,\n        \"scl\": 394,\n        \"sin\": 503,\n        \"bom\": 331,\n        \"ewr\": 190,\n        \"nrt\": 676,\n        \"mia\": 172,\n        \"mad\": 333,\n        \"syd\": 258\n      },\n      {\n        \"timestamp\": \"2025-08-20T18:00:00.000Z\",\n        \"cdg\": 160,\n        \"fra\": 156,\n        \"bos\": 152,\n        \"lhr\": 162,\n        \"yyz\": 117,\n        \"bog\": 408,\n        \"lax\": 136,\n        \"eze\": 268,\n        \"otp\": 387,\n        \"sjc\": 119,\n        \"phx\": 187,\n        \"gdl\": 618,\n        \"dfw\": 167,\n        \"jnb\": 396,\n        \"iad\": 102,\n        \"ams\": 181,\n        \"sin\": 347,\n        \"ewr\": 69,\n        \"bom\": 319,\n        \"mia\": 176,\n        \"nrt\": 243,\n        \"syd\": 300,\n        \"mad\": 320,\n        \"hkg\": 335,\n        \"ord\": 210,\n        \"den\": 210,\n        \"sea\": 146,\n        \"arn\": 224,\n        \"yul\": 121,\n        \"atl\": 137,\n        \"gig\": 235,\n        \"gru\": 164,\n        \"scl\": 498\n      },\n      {\n        \"timestamp\": \"2025-08-20T19:00:00.000Z\",\n        \"yul\": 117,\n        \"atl\": 199,\n        \"sea\": 208,\n        \"arn\": 210,\n        \"hkg\": 277,\n        \"den\": 229,\n        \"ord\": 202,\n        \"scl\": 349,\n        \"gru\": 183,\n        \"gig\": 198,\n        \"ewr\": 297,\n        \"bom\": 300,\n        \"sin\": 416,\n        \"mad\": 345,\n        \"syd\": 307,\n        \"nrt\": 270,\n        \"mia\": 178,\n        \"lax\": 149,\n        \"eze\": 351,\n        \"otp\": 317,\n        \"bog\": 417,\n        \"iad\": 162,\n        \"ams\": 211,\n        \"jnb\": 384,\n        \"gdl\": 591,\n        \"dfw\": 168,\n        \"sjc\": 152,\n        \"phx\": 200,\n        \"bos\": 113,\n        \"cdg\": 160,\n        \"fra\": 161,\n        \"yyz\": 137,\n        \"lhr\": 152\n      },\n      {\n        \"timestamp\": \"2025-08-20T20:00:00.000Z\",\n        \"scl\": 344,\n        \"gig\": 353,\n        \"gru\": 204,\n        \"yul\": 141,\n        \"atl\": 222,\n        \"ord\": 117,\n        \"den\": 266,\n        \"hkg\": 269,\n        \"arn\": 221,\n        \"sea\": 171,\n        \"syd\": 307,\n        \"mad\": 345,\n        \"mia\": 175,\n        \"nrt\": 271,\n        \"ewr\": 182,\n        \"bom\": 296,\n        \"sin\": 314,\n        \"jnb\": 357,\n        \"ams\": 194,\n        \"iad\": 194,\n        \"phx\": 160,\n        \"sjc\": 126,\n        \"dfw\": 157,\n        \"gdl\": 644,\n        \"bog\": 382,\n        \"eze\": 313,\n        \"otp\": 272,\n        \"lax\": 163,\n        \"yyz\": 195,\n        \"lhr\": 201,\n        \"bos\": 88,\n        \"fra\": 150,\n        \"cdg\": 146\n      },\n      {\n        \"timestamp\": \"2025-08-20T21:00:00.000Z\",\n        \"gig\": 377,\n        \"gru\": 214,\n        \"scl\": 343,\n        \"hkg\": 274,\n        \"den\": 222,\n        \"ord\": 132,\n        \"sea\": 158,\n        \"arn\": 232,\n        \"yul\": 97,\n        \"atl\": 117,\n        \"mia\": 172,\n        \"nrt\": 216,\n        \"syd\": 254,\n        \"mad\": 323,\n        \"sin\": 376,\n        \"ewr\": 202,\n        \"bom\": 288,\n        \"sjc\": 144,\n        \"phx\": 204,\n        \"gdl\": 488,\n        \"dfw\": 203,\n        \"jnb\": 346,\n        \"iad\": 164,\n        \"ams\": 194,\n        \"bog\": 384,\n        \"lax\": 149,\n        \"eze\": 273,\n        \"otp\": 334,\n        \"lhr\": 162,\n        \"yyz\": 140,\n        \"cdg\": 332,\n        \"fra\": 138,\n        \"bos\": 107\n      },\n      {\n        \"timestamp\": \"2025-08-20T22:00:00.000Z\",\n        \"scl\": 363,\n        \"gru\": 180,\n        \"gig\": 239,\n        \"atl\": 173,\n        \"yul\": 122,\n        \"arn\": 241,\n        \"sea\": 233,\n        \"den\": 291,\n        \"ord\": 154,\n        \"hkg\": 270,\n        \"mad\": 494,\n        \"syd\": 380,\n        \"nrt\": 274,\n        \"mia\": 177,\n        \"bom\": 297,\n        \"ewr\": 126,\n        \"sin\": 353,\n        \"ams\": 188,\n        \"iad\": 140,\n        \"jnb\": 366,\n        \"dfw\": 204,\n        \"gdl\": 500,\n        \"phx\": 226,\n        \"sjc\": 120,\n        \"otp\": 239,\n        \"eze\": 324,\n        \"lax\": 147,\n        \"bog\": 394,\n        \"yyz\": 148,\n        \"lhr\": 162,\n        \"bos\": 144,\n        \"fra\": 151,\n        \"cdg\": 163\n      },\n      {\n        \"timestamp\": \"2025-08-20T23:00:00.000Z\",\n        \"lhr\": 163,\n        \"yyz\": 124,\n        \"fra\": 151,\n        \"cdg\": 163,\n        \"bos\": 104,\n        \"phx\": 185,\n        \"sjc\": 121,\n        \"dfw\": 190,\n        \"gdl\": 455,\n        \"jnb\": 380,\n        \"ams\": 199,\n        \"iad\": 154,\n        \"bog\": 373,\n        \"otp\": 246,\n        \"eze\": 379,\n        \"lax\": 163,\n        \"mia\": 177,\n        \"nrt\": 313,\n        \"syd\": 302,\n        \"mad\": 346,\n        \"sin\": 336,\n        \"bom\": 296,\n        \"ewr\": 434,\n        \"gig\": 378,\n        \"gru\": 154,\n        \"scl\": 321,\n        \"ord\": 161,\n        \"den\": 233,\n        \"hkg\": 331,\n        \"arn\": 243,\n        \"sea\": 201,\n        \"atl\": 152,\n        \"yul\": 108\n      },\n      {\n        \"timestamp\": \"2025-08-21T00:00:00.000Z\",\n        \"bos\": 149,\n        \"fra\": 154,\n        \"cdg\": 165,\n        \"yyz\": 144,\n        \"lhr\": 155,\n        \"eze\": 262,\n        \"otp\": 257,\n        \"lax\": 150,\n        \"bog\": 423,\n        \"ams\": 176,\n        \"iad\": 384,\n        \"jnb\": 397,\n        \"dfw\": 181,\n        \"gdl\": 496,\n        \"phx\": 179,\n        \"sjc\": 116,\n        \"ewr\": 121,\n        \"bom\": 305,\n        \"sin\": 341,\n        \"mad\": 332,\n        \"syd\": 299,\n        \"nrt\": 237,\n        \"mia\": 184,\n        \"yul\": 127,\n        \"atl\": 139,\n        \"arn\": 237,\n        \"sea\": 166,\n        \"ord\": 133,\n        \"den\": 224,\n        \"hkg\": 263,\n        \"scl\": 399,\n        \"gru\": 330,\n        \"gig\": 165\n      },\n      {\n        \"timestamp\": \"2025-08-21T01:00:00.000Z\",\n        \"ewr\": 55,\n        \"bom\": 291,\n        \"sin\": 437,\n        \"syd\": 313,\n        \"mad\": 340,\n        \"mia\": 224,\n        \"nrt\": 235,\n        \"yul\": 116,\n        \"atl\": 159,\n        \"hkg\": 544,\n        \"den\": 232,\n        \"ord\": 138,\n        \"sea\": 175,\n        \"arn\": 213,\n        \"scl\": 392,\n        \"gig\": 200,\n        \"gru\": 186,\n        \"bos\": 113,\n        \"cdg\": 293,\n        \"fra\": 162,\n        \"yyz\": 139,\n        \"lhr\": 150,\n        \"bog\": 414,\n        \"lax\": 140,\n        \"eze\": 402,\n        \"otp\": 277,\n        \"jnb\": 385,\n        \"iad\": 300,\n        \"ams\": 218,\n        \"sjc\": 137,\n        \"phx\": 179,\n        \"gdl\": 628,\n        \"dfw\": 207\n      },\n      {\n        \"timestamp\": \"2025-08-21T02:00:00.000Z\",\n        \"fra\": 153,\n        \"cdg\": 150,\n        \"bos\": 106,\n        \"lhr\": 164,\n        \"yyz\": 146,\n        \"bog\": 488,\n        \"otp\": 271,\n        \"eze\": 515,\n        \"lax\": 135,\n        \"phx\": 190,\n        \"sjc\": 116,\n        \"dfw\": 502,\n        \"gdl\": 505,\n        \"jnb\": 407,\n        \"ams\": 189,\n        \"iad\": 64,\n        \"sin\": 844,\n        \"bom\": 292,\n        \"ewr\": 123,\n        \"mia\": 174,\n        \"nrt\": 224,\n        \"syd\": 269,\n        \"mad\": 332,\n        \"ord\": 139,\n        \"den\": 463,\n        \"hkg\": 346,\n        \"arn\": 233,\n        \"sea\": 223,\n        \"atl\": 179,\n        \"yul\": 105,\n        \"gig\": 381,\n        \"gru\": 184,\n        \"scl\": 297\n      },\n      {\n        \"timestamp\": \"2025-08-21T03:00:00.000Z\",\n        \"scl\": 291,\n        \"gru\": 179,\n        \"gig\": 194,\n        \"yul\": 99,\n        \"atl\": 199,\n        \"arn\": 201,\n        \"sea\": 197,\n        \"ord\": 165,\n        \"den\": 257,\n        \"hkg\": 327,\n        \"mad\": 323,\n        \"syd\": 346,\n        \"nrt\": 457,\n        \"mia\": 143,\n        \"ewr\": 101,\n        \"bom\": 619,\n        \"sin\": 710,\n        \"ams\": 202,\n        \"iad\": 43,\n        \"jnb\": 356,\n        \"dfw\": 202,\n        \"gdl\": 647,\n        \"phx\": 183,\n        \"sjc\": 124,\n        \"eze\": 530,\n        \"otp\": 271,\n        \"lax\": 137,\n        \"bog\": 304,\n        \"yyz\": 659,\n        \"lhr\": 186,\n        \"bos\": 122,\n        \"fra\": 171,\n        \"cdg\": 161\n      },\n      {\n        \"timestamp\": \"2025-08-21T04:00:00.000Z\",\n        \"gig\": 268,\n        \"gru\": 183,\n        \"scl\": 316,\n        \"hkg\": 344,\n        \"ord\": 135,\n        \"den\": 234,\n        \"sea\": 177,\n        \"arn\": 216,\n        \"yul\": 111,\n        \"atl\": 142,\n        \"mia\": 198,\n        \"nrt\": 239,\n        \"syd\": 446,\n        \"mad\": 351,\n        \"sin\": 494,\n        \"ewr\": 180,\n        \"bom\": 321,\n        \"sjc\": 136,\n        \"phx\": 229,\n        \"gdl\": 606,\n        \"dfw\": 187,\n        \"jnb\": 374,\n        \"iad\": 488,\n        \"ams\": 227,\n        \"bog\": 304,\n        \"lax\": 151,\n        \"eze\": 297,\n        \"otp\": 283,\n        \"lhr\": 161,\n        \"yyz\": 143,\n        \"cdg\": 157,\n        \"fra\": 161,\n        \"bos\": 115\n      },\n      {\n        \"timestamp\": \"2025-08-21T05:00:00.000Z\",\n        \"ord\": 160,\n        \"den\": 215,\n        \"hkg\": 275,\n        \"arn\": 267,\n        \"sea\": 185,\n        \"yul\": 118,\n        \"atl\": 235,\n        \"gig\": 167,\n        \"gru\": 170,\n        \"scl\": 509,\n        \"sin\": 948,\n        \"ewr\": 66,\n        \"bom\": 518,\n        \"mia\": 151,\n        \"nrt\": 267,\n        \"syd\": 302,\n        \"mad\": 318,\n        \"bog\": 508,\n        \"eze\": 261,\n        \"otp\": 288,\n        \"lax\": 146,\n        \"phx\": 222,\n        \"sjc\": 128,\n        \"dfw\": 167,\n        \"gdl\": 491,\n        \"jnb\": 365,\n        \"ams\": 197,\n        \"iad\": 47,\n        \"fra\": 151,\n        \"cdg\": 155,\n        \"bos\": 142,\n        \"lhr\": 188,\n        \"yyz\": 116\n      },\n      {\n        \"timestamp\": \"2025-08-21T06:00:00.000Z\",\n        \"bog\": 516,\n        \"lax\": 147,\n        \"otp\": 274,\n        \"eze\": 269,\n        \"jnb\": 372,\n        \"iad\": 95,\n        \"ams\": 198,\n        \"sjc\": 127,\n        \"phx\": 198,\n        \"gdl\": 621,\n        \"dfw\": 299,\n        \"bos\": 126,\n        \"cdg\": 179,\n        \"fra\": 154,\n        \"yyz\": 174,\n        \"lhr\": 156,\n        \"atl\": 144,\n        \"yul\": 104,\n        \"hkg\": 383,\n        \"den\": 280,\n        \"ord\": 149,\n        \"sea\": 203,\n        \"arn\": 211,\n        \"scl\": 326,\n        \"gig\": 194,\n        \"gru\": 174,\n        \"bom\": 580,\n        \"ewr\": 236,\n        \"sin\": 644,\n        \"syd\": 345,\n        \"mad\": 321,\n        \"mia\": 121,\n        \"nrt\": 498\n      },\n      {\n        \"timestamp\": \"2025-08-21T07:00:00.000Z\",\n        \"cdg\": 180,\n        \"fra\": 145,\n        \"bos\": 104,\n        \"lhr\": 156,\n        \"yyz\": 123,\n        \"bog\": 303,\n        \"lax\": 154,\n        \"otp\": 252,\n        \"eze\": 593,\n        \"sjc\": 152,\n        \"phx\": 240,\n        \"gdl\": 636,\n        \"dfw\": 409,\n        \"jnb\": 391,\n        \"iad\": 128,\n        \"ams\": 239,\n        \"sin\": 551,\n        \"bom\": 629,\n        \"ewr\": 116,\n        \"mia\": 135,\n        \"nrt\": 496,\n        \"syd\": 261,\n        \"mad\": 332,\n        \"hkg\": 351,\n        \"den\": 207,\n        \"ord\": 186,\n        \"sea\": 144,\n        \"arn\": 350,\n        \"atl\": 202,\n        \"yul\": 126,\n        \"gig\": 348,\n        \"gru\": 188,\n        \"scl\": 288\n      },\n      {\n        \"timestamp\": \"2025-08-21T08:00:00.000Z\",\n        \"bos\": 98,\n        \"fra\": 151,\n        \"cdg\": 294,\n        \"yyz\": 133,\n        \"lhr\": 197,\n        \"bog\": 353,\n        \"eze\": 292,\n        \"otp\": 283,\n        \"lax\": 163,\n        \"jnb\": 355,\n        \"ams\": 153,\n        \"iad\": 140,\n        \"phx\": 212,\n        \"sjc\": 157,\n        \"dfw\": 237,\n        \"gdl\": 752,\n        \"ewr\": 129,\n        \"bom\": 299,\n        \"sin\": 605,\n        \"syd\": 261,\n        \"mad\": 331,\n        \"mia\": 136,\n        \"nrt\": 309,\n        \"yul\": 125,\n        \"atl\": 157,\n        \"ord\": 78,\n        \"den\": 255,\n        \"hkg\": 340,\n        \"arn\": 417,\n        \"sea\": 202,\n        \"scl\": 356,\n        \"gig\": 178,\n        \"gru\": 234\n      },\n      {\n        \"timestamp\": \"2025-08-21T09:00:00.000Z\",\n        \"scl\": 299,\n        \"gig\": 277,\n        \"gru\": 185,\n        \"atl\": 110,\n        \"yul\": 95,\n        \"den\": 461,\n        \"ord\": 139,\n        \"hkg\": 271,\n        \"arn\": 349,\n        \"sea\": 176,\n        \"syd\": 313,\n        \"mad\": 333,\n        \"mia\": 165,\n        \"nrt\": 270,\n        \"bom\": 514,\n        \"ewr\": 120,\n        \"sin\": 385,\n        \"jnb\": 343,\n        \"ams\": 218,\n        \"iad\": 129,\n        \"phx\": 183,\n        \"sjc\": 124,\n        \"dfw\": 232,\n        \"gdl\": 523,\n        \"bog\": 471,\n        \"otp\": 247,\n        \"eze\": 295,\n        \"lax\": 127,\n        \"yyz\": 127,\n        \"lhr\": 177,\n        \"bos\": 110,\n        \"fra\": 159,\n        \"cdg\": 419\n      },\n      {\n        \"timestamp\": \"2025-08-21T10:00:00.000Z\",\n        \"yyz\": 133,\n        \"lhr\": 309,\n        \"bos\": 110,\n        \"fra\": 165,\n        \"cdg\": 159,\n        \"ams\": 192,\n        \"iad\": 135,\n        \"jnb\": 378,\n        \"dfw\": 144,\n        \"gdl\": 499,\n        \"phx\": 190,\n        \"sjc\": 132,\n        \"otp\": 284,\n        \"eze\": 415,\n        \"lax\": 148,\n        \"bog\": 357,\n        \"mad\": 339,\n        \"syd\": 307,\n        \"nrt\": 275,\n        \"mia\": 123,\n        \"bom\": 302,\n        \"ewr\": 71,\n        \"sin\": 594,\n        \"scl\": 398,\n        \"gru\": 259,\n        \"gig\": 212,\n        \"atl\": 152,\n        \"yul\": 103,\n        \"arn\": 256,\n        \"sea\": 185,\n        \"den\": 232,\n        \"ord\": 133,\n        \"hkg\": 348\n      },\n      {\n        \"timestamp\": \"2025-08-21T11:00:00.000Z\",\n        \"gru\": 189,\n        \"gig\": 184,\n        \"scl\": 292,\n        \"arn\": 353,\n        \"sea\": 231,\n        \"den\": 232,\n        \"ord\": 181,\n        \"hkg\": 372,\n        \"yul\": 118,\n        \"atl\": 129,\n        \"nrt\": 306,\n        \"mia\": 190,\n        \"mad\": 338,\n        \"syd\": 273,\n        \"sin\": 282,\n        \"ewr\": 190,\n        \"bom\": 325,\n        \"dfw\": 204,\n        \"gdl\": 501,\n        \"phx\": 206,\n        \"sjc\": 122,\n        \"ams\": 194,\n        \"iad\": 101,\n        \"jnb\": 365,\n        \"eze\": 327,\n        \"otp\": 276,\n        \"lax\": 240,\n        \"bog\": 318,\n        \"lhr\": 298,\n        \"yyz\": 135,\n        \"fra\": 347,\n        \"cdg\": 378,\n        \"bos\": 102\n      },\n      {\n        \"timestamp\": \"2025-08-21T12:00:00.000Z\",\n        \"sin\": 384,\n        \"ewr\": 59,\n        \"bom\": 330,\n        \"nrt\": 260,\n        \"mia\": 151,\n        \"mad\": 358,\n        \"syd\": 307,\n        \"arn\": 208,\n        \"sea\": 209,\n        \"ord\": 187,\n        \"den\": 278,\n        \"hkg\": 350,\n        \"yul\": 119,\n        \"atl\": 128,\n        \"gru\": 186,\n        \"gig\": 532,\n        \"scl\": 554,\n        \"fra\": 230,\n        \"cdg\": 364,\n        \"bos\": 127,\n        \"lhr\": 230,\n        \"yyz\": 136,\n        \"eze\": 324,\n        \"otp\": 276,\n        \"lax\": 152,\n        \"bog\": 489,\n        \"dfw\": 387,\n        \"gdl\": 486,\n        \"phx\": 202,\n        \"sjc\": 132,\n        \"ams\": 202,\n        \"iad\": 171,\n        \"jnb\": 354\n      },\n      {\n        \"timestamp\": \"2025-08-21T13:00:00.000Z\",\n        \"otp\": 254,\n        \"eze\": 319,\n        \"lax\": 147,\n        \"bog\": 457,\n        \"ams\": 192,\n        \"iad\": 167,\n        \"jnb\": 523,\n        \"dfw\": 189,\n        \"gdl\": 543,\n        \"phx\": 215,\n        \"sjc\": 118,\n        \"bos\": 109,\n        \"fra\": 185,\n        \"cdg\": 169,\n        \"yyz\": 156,\n        \"lhr\": 140,\n        \"atl\": 161,\n        \"yul\": 134,\n        \"arn\": 216,\n        \"sea\": 191,\n        \"ord\": 141,\n        \"den\": 210,\n        \"hkg\": 358,\n        \"scl\": 364,\n        \"gru\": 410,\n        \"gig\": 394,\n        \"bom\": 316,\n        \"ewr\": 246,\n        \"sin\": 358,\n        \"mad\": 486,\n        \"syd\": 306,\n        \"nrt\": 217,\n        \"mia\": 170\n      },\n      {\n        \"timestamp\": \"2025-08-21T14:00:00.000Z\",\n        \"yyz\": 287,\n        \"lhr\": 152,\n        \"bos\": 192,\n        \"fra\": 137,\n        \"cdg\": 171,\n        \"ams\": 235,\n        \"iad\": 202,\n        \"jnb\": 397,\n        \"dfw\": 161,\n        \"gdl\": 471,\n        \"phx\": 179,\n        \"sjc\": 160,\n        \"otp\": 326,\n        \"eze\": 265,\n        \"lax\": 159,\n        \"bog\": 314,\n        \"mad\": 329,\n        \"syd\": 303,\n        \"nrt\": 317,\n        \"mia\": 148,\n        \"bom\": 358,\n        \"ewr\": 272,\n        \"sin\": 438,\n        \"scl\": 339,\n        \"gru\": 179,\n        \"gig\": 587,\n        \"atl\": 210,\n        \"yul\": 96,\n        \"arn\": 231,\n        \"sea\": 170,\n        \"den\": 239,\n        \"ord\": 135,\n        \"hkg\": 270\n      },\n      {\n        \"timestamp\": \"2025-08-21T15:00:00.000Z\",\n        \"gru\": 187,\n        \"gig\": 247,\n        \"scl\": 348,\n        \"arn\": 202,\n        \"sea\": 186,\n        \"den\": 239,\n        \"ord\": 184,\n        \"hkg\": 370,\n        \"yul\": 106,\n        \"atl\": 111,\n        \"nrt\": 480,\n        \"mia\": 160,\n        \"mad\": 339,\n        \"syd\": 339,\n        \"sin\": 454,\n        \"ewr\": 127,\n        \"bom\": 306,\n        \"dfw\": 565,\n        \"gdl\": 585,\n        \"phx\": 190,\n        \"sjc\": 121,\n        \"ams\": 210,\n        \"iad\": 145,\n        \"jnb\": 397,\n        \"eze\": 258,\n        \"otp\": 312,\n        \"lax\": 292,\n        \"bog\": 452,\n        \"lhr\": 165,\n        \"yyz\": 240,\n        \"fra\": 153,\n        \"cdg\": 287,\n        \"bos\": 139\n      },\n      {\n        \"timestamp\": \"2025-08-21T16:00:00.000Z\",\n        \"phx\": 205,\n        \"sjc\": 131,\n        \"dfw\": 210,\n        \"gdl\": 506,\n        \"jnb\": 373,\n        \"ams\": 233,\n        \"iad\": 130,\n        \"bog\": 308,\n        \"eze\": 415,\n        \"otp\": 255,\n        \"lax\": 277,\n        \"lhr\": 243,\n        \"yyz\": 138,\n        \"fra\": 168,\n        \"cdg\": 171,\n        \"bos\": 107,\n        \"gig\": 207,\n        \"gru\": 248,\n        \"scl\": 354,\n        \"ord\": 134,\n        \"den\": 223,\n        \"hkg\": 335,\n        \"arn\": 234,\n        \"sea\": 177,\n        \"yul\": 328,\n        \"atl\": 155,\n        \"mia\": 136,\n        \"nrt\": 463,\n        \"syd\": 330,\n        \"mad\": 333,\n        \"sin\": 495,\n        \"ewr\": 177,\n        \"bom\": 328\n      },\n      {\n        \"timestamp\": \"2025-08-21T17:00:00.000Z\",\n        \"syd\": 311,\n        \"mad\": 355,\n        \"mia\": 201,\n        \"nrt\": 276,\n        \"bom\": 309,\n        \"ewr\": 339,\n        \"sin\": 457,\n        \"scl\": 336,\n        \"gig\": 391,\n        \"gru\": 213,\n        \"atl\": 128,\n        \"yul\": 129,\n        \"ord\": 221,\n        \"den\": 247,\n        \"hkg\": 283,\n        \"arn\": 240,\n        \"sea\": 198,\n        \"yyz\": 125,\n        \"lhr\": 149,\n        \"bos\": 103,\n        \"fra\": 197,\n        \"cdg\": 277,\n        \"jnb\": 344,\n        \"ams\": 213,\n        \"iad\": 359,\n        \"phx\": 206,\n        \"sjc\": 232,\n        \"dfw\": 156,\n        \"gdl\": 631,\n        \"bog\": 448,\n        \"otp\": 320,\n        \"eze\": 310,\n        \"lax\": 163\n      },\n      {\n        \"timestamp\": \"2025-08-21T18:00:00.000Z\",\n        \"ams\": 217,\n        \"iad\": 136,\n        \"jnb\": 346,\n        \"dfw\": 158,\n        \"gdl\": 642,\n        \"phx\": 180,\n        \"sjc\": 147,\n        \"otp\": 274,\n        \"eze\": 257,\n        \"lax\": 151,\n        \"bog\": 467,\n        \"yyz\": 139,\n        \"lhr\": 173,\n        \"bos\": 112,\n        \"fra\": 142,\n        \"cdg\": 158,\n        \"scl\": 290,\n        \"gru\": 244,\n        \"gig\": 362,\n        \"atl\": 166,\n        \"yul\": 108,\n        \"arn\": 185,\n        \"sea\": 184,\n        \"ord\": 176,\n        \"den\": 417,\n        \"hkg\": 332,\n        \"mad\": 329,\n        \"syd\": 341,\n        \"nrt\": 219,\n        \"mia\": 132,\n        \"bom\": 311,\n        \"ewr\": 149,\n        \"sin\": 344\n      },\n      {\n        \"timestamp\": \"2025-08-21T19:00:00.000Z\",\n        \"ewr\": 129,\n        \"bom\": 316,\n        \"sin\": 407,\n        \"mad\": 763,\n        \"syd\": 322,\n        \"nrt\": 221,\n        \"mia\": 208,\n        \"yul\": 121,\n        \"atl\": 139,\n        \"arn\": 228,\n        \"sea\": 208,\n        \"den\": 213,\n        \"ord\": 106,\n        \"hkg\": 357,\n        \"scl\": 292,\n        \"gru\": 317,\n        \"gig\": 367,\n        \"bos\": 140,\n        \"fra\": 140,\n        \"cdg\": 145,\n        \"yyz\": 116,\n        \"lhr\": 155,\n        \"eze\": 323,\n        \"otp\": 283,\n        \"lax\": 180,\n        \"bog\": 479,\n        \"ams\": 202,\n        \"iad\": 419,\n        \"jnb\": 375,\n        \"dfw\": 199,\n        \"gdl\": 527,\n        \"phx\": 199,\n        \"sjc\": 129\n      },\n      {\n        \"timestamp\": \"2025-08-21T20:00:00.000Z\",\n        \"sin\": 836,\n        \"bom\": 330,\n        \"ewr\": 121,\n        \"nrt\": 264,\n        \"mia\": 887,\n        \"mad\": 341,\n        \"syd\": 301,\n        \"sea\": 173,\n        \"arn\": 211,\n        \"hkg\": 329,\n        \"ord\": 187,\n        \"den\": 269,\n        \"atl\": 118,\n        \"yul\": 95,\n        \"gru\": 195,\n        \"gig\": 392,\n        \"scl\": 303,\n        \"cdg\": 161,\n        \"fra\": 158,\n        \"bos\": 128,\n        \"lhr\": 155,\n        \"yyz\": 132,\n        \"lax\": 136,\n        \"otp\": 289,\n        \"eze\": 301,\n        \"bog\": 573,\n        \"gdl\": 541,\n        \"dfw\": 198,\n        \"sjc\": 140,\n        \"phx\": 189,\n        \"iad\": 181,\n        \"ams\": 193,\n        \"jnb\": 663\n      },\n      {\n        \"timestamp\": \"2025-08-21T21:00:00.000Z\",\n        \"iad\": 129,\n        \"ams\": 213,\n        \"jnb\": 365,\n        \"gdl\": 504,\n        \"dfw\": 458,\n        \"sjc\": 128,\n        \"phx\": 185,\n        \"lax\": 164,\n        \"otp\": 236,\n        \"eze\": 263,\n        \"bog\": 387,\n        \"yyz\": 137,\n        \"lhr\": 142,\n        \"bos\": 140,\n        \"cdg\": 158,\n        \"fra\": 151,\n        \"scl\": 300,\n        \"gru\": 192,\n        \"gig\": 346,\n        \"atl\": 138,\n        \"yul\": 105,\n        \"sea\": 194,\n        \"arn\": 239,\n        \"hkg\": 326,\n        \"den\": 278,\n        \"ord\": 202,\n        \"mad\": 357,\n        \"syd\": 299,\n        \"nrt\": 254,\n        \"mia\": 187,\n        \"bom\": 273,\n        \"ewr\": 123,\n        \"sin\": 427\n      },\n      {\n        \"timestamp\": \"2025-08-21T22:00:00.000Z\",\n        \"lhr\": 212,\n        \"yyz\": 131,\n        \"cdg\": 147,\n        \"fra\": 157,\n        \"bos\": 103,\n        \"gdl\": 502,\n        \"dfw\": 159,\n        \"sjc\": 117,\n        \"phx\": 188,\n        \"iad\": 76,\n        \"ams\": 197,\n        \"jnb\": 359,\n        \"lax\": 153,\n        \"otp\": 270,\n        \"eze\": 275,\n        \"bog\": 439,\n        \"nrt\": 266,\n        \"mia\": 120,\n        \"mad\": 319,\n        \"syd\": 336,\n        \"sin\": 319,\n        \"bom\": 306,\n        \"ewr\": 116,\n        \"gru\": 196,\n        \"gig\": 216,\n        \"scl\": 342,\n        \"sea\": 168,\n        \"arn\": 209,\n        \"hkg\": 329,\n        \"den\": 283,\n        \"ord\": 190,\n        \"atl\": 139,\n        \"yul\": 119\n      },\n      {\n        \"timestamp\": \"2025-08-21T23:00:00.000Z\",\n        \"sea\": 199,\n        \"arn\": 205,\n        \"hkg\": 341,\n        \"ord\": 148,\n        \"den\": 253,\n        \"yul\": 109,\n        \"atl\": 155,\n        \"gru\": 180,\n        \"gig\": 474,\n        \"scl\": 381,\n        \"sin\": 443,\n        \"ewr\": 162,\n        \"bom\": 299,\n        \"nrt\": 324,\n        \"mia\": 155,\n        \"mad\": 333,\n        \"syd\": 477,\n        \"lax\": 141,\n        \"eze\": 323,\n        \"otp\": 254,\n        \"bog\": 506,\n        \"gdl\": 529,\n        \"dfw\": 220,\n        \"sjc\": 140,\n        \"phx\": 201,\n        \"iad\": 103,\n        \"ams\": 186,\n        \"jnb\": 405,\n        \"cdg\": 157,\n        \"fra\": 376,\n        \"bos\": 168,\n        \"lhr\": 154,\n        \"yyz\": 120\n      },\n      {\n        \"timestamp\": \"2025-08-22T00:00:00.000Z\",\n        \"yyz\": 133,\n        \"lhr\": 150,\n        \"bos\": 113,\n        \"fra\": 190,\n        \"cdg\": 149,\n        \"ams\": 180,\n        \"iad\": 134,\n        \"jnb\": 392,\n        \"dfw\": 182,\n        \"gdl\": 502,\n        \"phx\": 291,\n        \"sjc\": 121,\n        \"eze\": 269,\n        \"otp\": 282,\n        \"lax\": 144,\n        \"bog\": 394,\n        \"mad\": 346,\n        \"syd\": 259,\n        \"nrt\": 227,\n        \"mia\": 181,\n        \"ewr\": 134,\n        \"bom\": 293,\n        \"sin\": 585,\n        \"scl\": 288,\n        \"gru\": 210,\n        \"gig\": 242,\n        \"yul\": 100,\n        \"atl\": 154,\n        \"arn\": 206,\n        \"sea\": 195,\n        \"ord\": 155,\n        \"den\": 298,\n        \"hkg\": 329\n      },\n      {\n        \"timestamp\": \"2025-08-22T01:00:00.000Z\",\n        \"phx\": 182,\n        \"sjc\": 133,\n        \"dfw\": 249,\n        \"gdl\": 485,\n        \"jnb\": 374,\n        \"ams\": 233,\n        \"iad\": 198,\n        \"bog\": 478,\n        \"otp\": 256,\n        \"eze\": 289,\n        \"lax\": 143,\n        \"lhr\": 141,\n        \"yyz\": 133,\n        \"fra\": 150,\n        \"cdg\": 168,\n        \"bos\": 106,\n        \"gig\": 349,\n        \"gru\": 175,\n        \"scl\": 496,\n        \"den\": 272,\n        \"ord\": 396,\n        \"hkg\": 386,\n        \"arn\": 204,\n        \"sea\": 178,\n        \"atl\": 143,\n        \"yul\": 118,\n        \"mia\": 161,\n        \"nrt\": 221,\n        \"syd\": 300,\n        \"mad\": 331,\n        \"sin\": 978,\n        \"bom\": 302,\n        \"ewr\": 183\n      },\n      {\n        \"timestamp\": \"2025-08-22T02:00:00.000Z\",\n        \"nrt\": 458,\n        \"mia\": 154,\n        \"mad\": 318,\n        \"syd\": 306,\n        \"sin\": 886,\n        \"bom\": 290,\n        \"ewr\": 316,\n        \"gru\": 370,\n        \"gig\": 410,\n        \"scl\": 340,\n        \"arn\": 223,\n        \"sea\": 217,\n        \"den\": 233,\n        \"ord\": 182,\n        \"hkg\": 352,\n        \"atl\": 119,\n        \"yul\": 83,\n        \"lhr\": 153,\n        \"yyz\": 153,\n        \"fra\": 145,\n        \"cdg\": 172,\n        \"bos\": 128,\n        \"dfw\": 229,\n        \"gdl\": 587,\n        \"phx\": 192,\n        \"sjc\": 113,\n        \"ams\": 311,\n        \"iad\": 151,\n        \"jnb\": 388,\n        \"otp\": 358,\n        \"eze\": 506,\n        \"lax\": 145,\n        \"bog\": 356\n      },\n      {\n        \"timestamp\": \"2025-08-22T03:00:00.000Z\",\n        \"ams\": 194,\n        \"iad\": 113,\n        \"jnb\": 344,\n        \"dfw\": 157,\n        \"gdl\": 475,\n        \"phx\": 203,\n        \"sjc\": 115,\n        \"eze\": 460,\n        \"otp\": 230,\n        \"lax\": 166,\n        \"bog\": 345,\n        \"yyz\": 138,\n        \"lhr\": 161,\n        \"bos\": 101,\n        \"fra\": 154,\n        \"cdg\": 168,\n        \"scl\": 496,\n        \"gru\": 186,\n        \"gig\": 355,\n        \"yul\": 81,\n        \"atl\": 146,\n        \"arn\": 244,\n        \"sea\": 199,\n        \"den\": 232,\n        \"ord\": 182,\n        \"hkg\": 638,\n        \"mad\": 328,\n        \"syd\": 274,\n        \"nrt\": 215,\n        \"mia\": 107,\n        \"ewr\": 186,\n        \"bom\": 317,\n        \"sin\": 621\n      },\n      {\n        \"timestamp\": \"2025-08-22T04:00:00.000Z\",\n        \"lax\": 144,\n        \"otp\": 287,\n        \"eze\": 493,\n        \"bog\": 461,\n        \"iad\": 380,\n        \"ams\": 191,\n        \"jnb\": 351,\n        \"gdl\": 726,\n        \"dfw\": 207,\n        \"sjc\": 155,\n        \"phx\": 185,\n        \"bos\": 96,\n        \"cdg\": 172,\n        \"fra\": 148,\n        \"yyz\": 120,\n        \"lhr\": 153,\n        \"atl\": 186,\n        \"yul\": 93,\n        \"sea\": 199,\n        \"arn\": 216,\n        \"hkg\": 411,\n        \"den\": 221,\n        \"ord\": 134,\n        \"scl\": 287,\n        \"gru\": 239,\n        \"gig\": 236,\n        \"bom\": 393,\n        \"ewr\": 184,\n        \"sin\": 449,\n        \"mad\": 319,\n        \"syd\": 263,\n        \"nrt\": 421,\n        \"mia\": 160\n      },\n      {\n        \"timestamp\": \"2025-08-22T05:00:00.000Z\",\n        \"sin\": 453,\n        \"ewr\": 61,\n        \"bom\": 300,\n        \"nrt\": 504,\n        \"mia\": 177,\n        \"mad\": 367,\n        \"syd\": 315,\n        \"sea\": 189,\n        \"arn\": 226,\n        \"hkg\": 427,\n        \"den\": 228,\n        \"ord\": 129,\n        \"yul\": 106,\n        \"atl\": 142,\n        \"gru\": 206,\n        \"gig\": 198,\n        \"scl\": 342,\n        \"cdg\": 184,\n        \"fra\": 143,\n        \"bos\": 136,\n        \"lhr\": 149,\n        \"yyz\": 162,\n        \"lax\": 132,\n        \"eze\": 296,\n        \"otp\": 240,\n        \"bog\": 300,\n        \"gdl\": 473,\n        \"dfw\": 190,\n        \"sjc\": 125,\n        \"phx\": 176,\n        \"iad\": 125,\n        \"ams\": 195,\n        \"jnb\": 378\n      },\n      {\n        \"timestamp\": \"2025-08-22T06:00:00.000Z\",\n        \"ewr\": 115,\n        \"bom\": 661,\n        \"sin\": 395,\n        \"syd\": 307,\n        \"mad\": 315,\n        \"mia\": 194,\n        \"nrt\": 281,\n        \"yul\": 92,\n        \"atl\": 139,\n        \"den\": 234,\n        \"ord\": 128,\n        \"hkg\": 326,\n        \"arn\": 240,\n        \"sea\": 170,\n        \"scl\": 393,\n        \"gig\": 229,\n        \"gru\": 160,\n        \"bos\": 118,\n        \"fra\": 385,\n        \"cdg\": 153,\n        \"yyz\": 119,\n        \"lhr\": 147,\n        \"bog\": 505,\n        \"eze\": 294,\n        \"otp\": 299,\n        \"lax\": 159,\n        \"jnb\": 398,\n        \"ams\": 212,\n        \"iad\": 36,\n        \"phx\": 201,\n        \"sjc\": 115,\n        \"dfw\": 238,\n        \"gdl\": 480\n      },\n      {\n        \"timestamp\": \"2025-08-22T07:00:00.000Z\",\n        \"bog\": 473,\n        \"otp\": 283,\n        \"eze\": 404,\n        \"lax\": 145,\n        \"phx\": 185,\n        \"sjc\": 126,\n        \"dfw\": 236,\n        \"gdl\": 651,\n        \"jnb\": 364,\n        \"ams\": 204,\n        \"iad\": 116,\n        \"fra\": 144,\n        \"cdg\": 158,\n        \"bos\": 140,\n        \"lhr\": 167,\n        \"yyz\": 119,\n        \"den\": 247,\n        \"ord\": 138,\n        \"hkg\": 272,\n        \"arn\": 590,\n        \"sea\": 161,\n        \"atl\": 153,\n        \"yul\": 107,\n        \"gig\": 347,\n        \"gru\": 200,\n        \"scl\": 305,\n        \"sin\": 718,\n        \"bom\": 300,\n        \"ewr\": 117,\n        \"mia\": 133,\n        \"nrt\": 522,\n        \"syd\": 271,\n        \"mad\": 635\n      },\n      {\n        \"timestamp\": \"2025-08-22T08:00:00.000Z\",\n        \"jnb\": 360,\n        \"ams\": 212,\n        \"iad\": 211,\n        \"phx\": 180,\n        \"sjc\": 131,\n        \"dfw\": 183,\n        \"gdl\": 500,\n        \"bog\": 457,\n        \"otp\": 303,\n        \"eze\": 308,\n        \"lax\": 161,\n        \"yyz\": 104,\n        \"lhr\": 190,\n        \"bos\": 111,\n        \"fra\": 149,\n        \"cdg\": 278,\n        \"scl\": 341,\n        \"gig\": 376,\n        \"gru\": 216,\n        \"atl\": 194,\n        \"yul\": 89,\n        \"ord\": 135,\n        \"den\": 262,\n        \"hkg\": 262,\n        \"arn\": 363,\n        \"sea\": 175,\n        \"syd\": 294,\n        \"mad\": 341,\n        \"mia\": 110,\n        \"nrt\": 220,\n        \"bom\": 304,\n        \"ewr\": 197,\n        \"sin\": 310\n      },\n      {\n        \"timestamp\": \"2025-08-22T09:00:00.000Z\",\n        \"mia\": 152,\n        \"nrt\": 287,\n        \"syd\": 421,\n        \"mad\": 330,\n        \"sin\": 433,\n        \"ewr\": 126,\n        \"bom\": 300,\n        \"gig\": 249,\n        \"gru\": 220,\n        \"scl\": 340,\n        \"ord\": 130,\n        \"den\": 276,\n        \"hkg\": 345,\n        \"arn\": 187,\n        \"sea\": 201,\n        \"yul\": 114,\n        \"atl\": 174,\n        \"lhr\": 156,\n        \"yyz\": 116,\n        \"fra\": 160,\n        \"cdg\": 181,\n        \"bos\": 89,\n        \"phx\": 195,\n        \"sjc\": 118,\n        \"dfw\": 198,\n        \"gdl\": 630,\n        \"jnb\": 352,\n        \"ams\": 190,\n        \"iad\": 506,\n        \"bog\": 347,\n        \"eze\": 304,\n        \"otp\": 291,\n        \"lax\": 142\n      },\n      {\n        \"timestamp\": \"2025-08-22T10:00:00.000Z\",\n        \"dfw\": 169,\n        \"gdl\": 860,\n        \"phx\": 192,\n        \"sjc\": 127,\n        \"ams\": 185,\n        \"iad\": 114,\n        \"jnb\": 360,\n        \"eze\": 299,\n        \"otp\": 238,\n        \"lax\": 142,\n        \"bog\": 379,\n        \"lhr\": 162,\n        \"yyz\": 263,\n        \"fra\": 170,\n        \"cdg\": 552,\n        \"bos\": 113,\n        \"gru\": 180,\n        \"gig\": 392,\n        \"scl\": 334,\n        \"arn\": 224,\n        \"sea\": 175,\n        \"ord\": 132,\n        \"den\": 241,\n        \"hkg\": 275,\n        \"yul\": 140,\n        \"atl\": 149,\n        \"nrt\": 723,\n        \"mia\": 138,\n        \"mad\": 323,\n        \"syd\": 308,\n        \"sin\": 472,\n        \"ewr\": 114,\n        \"bom\": 327\n      },\n      {\n        \"timestamp\": \"2025-08-22T11:00:00.000Z\",\n        \"yyz\": 133,\n        \"lhr\": 172,\n        \"bos\": 134,\n        \"fra\": 162,\n        \"cdg\": 150,\n        \"jnb\": 381,\n        \"ams\": 186,\n        \"iad\": 132,\n        \"phx\": 180,\n        \"sjc\": 143,\n        \"dfw\": 190,\n        \"gdl\": 666,\n        \"bog\": 660,\n        \"otp\": 276,\n        \"eze\": 489,\n        \"lax\": 143,\n        \"syd\": 296,\n        \"mad\": 366,\n        \"mia\": 186,\n        \"nrt\": 270,\n        \"bom\": 301,\n        \"ewr\": 130,\n        \"sin\": 433,\n        \"scl\": 343,\n        \"gig\": 220,\n        \"gru\": 189,\n        \"atl\": 133,\n        \"yul\": 125,\n        \"den\": 248,\n        \"ord\": 141,\n        \"hkg\": 429,\n        \"arn\": 220,\n        \"sea\": 188\n      },\n      {\n        \"timestamp\": \"2025-08-22T12:00:00.000Z\",\n        \"lhr\": 161,\n        \"yyz\": 111,\n        \"cdg\": 165,\n        \"fra\": 443,\n        \"bos\": 115,\n        \"sjc\": 130,\n        \"phx\": 188,\n        \"gdl\": 669,\n        \"dfw\": 218,\n        \"jnb\": 376,\n        \"iad\": 508,\n        \"ams\": 208,\n        \"bog\": 508,\n        \"lax\": 165,\n        \"eze\": 307,\n        \"otp\": 236,\n        \"mia\": 154,\n        \"nrt\": 279,\n        \"syd\": 305,\n        \"mad\": 344,\n        \"sin\": 776,\n        \"ewr\": 123,\n        \"bom\": 325,\n        \"gig\": 230,\n        \"gru\": 212,\n        \"scl\": 450,\n        \"hkg\": 348,\n        \"ord\": 181,\n        \"den\": 225,\n        \"sea\": 170,\n        \"arn\": 215,\n        \"yul\": 107,\n        \"atl\": 166\n      },\n      {\n        \"timestamp\": \"2025-08-22T13:00:00.000Z\",\n        \"hkg\": 362,\n        \"den\": 232,\n        \"ord\": 148,\n        \"sea\": 178,\n        \"arn\": 315,\n        \"atl\": 438,\n        \"yul\": 107,\n        \"gig\": 392,\n        \"gru\": 373,\n        \"scl\": 535,\n        \"sin\": 463,\n        \"bom\": 315,\n        \"ewr\": 193,\n        \"mia\": 186,\n        \"nrt\": 276,\n        \"syd\": 311,\n        \"mad\": 336,\n        \"bog\": 363,\n        \"lax\": 151,\n        \"otp\": 272,\n        \"eze\": 405,\n        \"sjc\": 145,\n        \"phx\": 183,\n        \"gdl\": 482,\n        \"dfw\": 191,\n        \"jnb\": 545,\n        \"iad\": 244,\n        \"ams\": 198,\n        \"cdg\": 296,\n        \"fra\": 134,\n        \"bos\": 174,\n        \"lhr\": 135,\n        \"yyz\": 132\n      },\n      {\n        \"timestamp\": \"2025-08-22T14:00:00.000Z\",\n        \"bom\": 548,\n        \"ewr\": 166,\n        \"sin\": 428,\n        \"syd\": 339,\n        \"mad\": 340,\n        \"mia\": 144,\n        \"nrt\": 269,\n        \"atl\": 151,\n        \"yul\": 95,\n        \"hkg\": 264,\n        \"den\": 237,\n        \"ord\": 177,\n        \"sea\": 207,\n        \"arn\": 203,\n        \"scl\": 352,\n        \"gig\": 176,\n        \"gru\": 182,\n        \"bos\": 95,\n        \"cdg\": 169,\n        \"fra\": 153,\n        \"yyz\": 124,\n        \"lhr\": 155,\n        \"bog\": 351,\n        \"lax\": 155,\n        \"otp\": 308,\n        \"eze\": 297,\n        \"jnb\": 381,\n        \"iad\": 167,\n        \"ams\": 218,\n        \"sjc\": 144,\n        \"phx\": 157,\n        \"gdl\": 596,\n        \"dfw\": 454\n      },\n      {\n        \"timestamp\": \"2025-08-22T15:00:00.000Z\",\n        \"sjc\": 139,\n        \"phx\": 164,\n        \"gdl\": 484,\n        \"dfw\": 205,\n        \"jnb\": 364,\n        \"iad\": 140,\n        \"ams\": 227,\n        \"bog\": 330,\n        \"lax\": 265,\n        \"otp\": 251,\n        \"eze\": 286,\n        \"lhr\": 304,\n        \"yyz\": 172,\n        \"cdg\": 168,\n        \"fra\": 151,\n        \"bos\": 100,\n        \"gig\": 194,\n        \"gru\": 198,\n        \"scl\": 389,\n        \"hkg\": 329,\n        \"ord\": 182,\n        \"den\": 219,\n        \"sea\": 199,\n        \"arn\": 204,\n        \"atl\": 157,\n        \"yul\": 92,\n        \"mia\": 111,\n        \"nrt\": 402,\n        \"syd\": 320,\n        \"mad\": 339,\n        \"sin\": 552,\n        \"bom\": 311,\n        \"ewr\": 66\n      },\n      {\n        \"timestamp\": \"2025-08-22T16:00:00.000Z\",\n        \"jnb\": 488,\n        \"ams\": 194,\n        \"iad\": 159,\n        \"phx\": 198,\n        \"sjc\": 267,\n        \"dfw\": 121,\n        \"gdl\": 485,\n        \"bog\": 567,\n        \"eze\": 326,\n        \"otp\": 275,\n        \"lax\": 151,\n        \"yyz\": 186,\n        \"lhr\": 167,\n        \"bos\": 86,\n        \"fra\": 140,\n        \"cdg\": 276,\n        \"scl\": 330,\n        \"gig\": 237,\n        \"gru\": 183,\n        \"yul\": 91,\n        \"atl\": 117,\n        \"den\": 317,\n        \"ord\": 140,\n        \"hkg\": 352,\n        \"arn\": 186,\n        \"sea\": 223,\n        \"syd\": 321,\n        \"mad\": 371,\n        \"mia\": 133,\n        \"nrt\": 274,\n        \"ewr\": 150,\n        \"bom\": 293,\n        \"sin\": 347\n      },\n      {\n        \"timestamp\": \"2025-08-22T17:00:00.000Z\",\n        \"ewr\": 137,\n        \"bom\": 400,\n        \"sin\": 351,\n        \"mad\": 325,\n        \"syd\": 288,\n        \"nrt\": 436,\n        \"mia\": 220,\n        \"yul\": 121,\n        \"atl\": 135,\n        \"arn\": 195,\n        \"sea\": 421,\n        \"den\": 500,\n        \"ord\": 179,\n        \"hkg\": 278,\n        \"scl\": 288,\n        \"gru\": 261,\n        \"gig\": 351,\n        \"bos\": 121,\n        \"fra\": 170,\n        \"cdg\": 333,\n        \"yyz\": 124,\n        \"lhr\": 174,\n        \"eze\": 325,\n        \"otp\": 281,\n        \"lax\": 152,\n        \"bog\": 454,\n        \"ams\": 197,\n        \"iad\": 207,\n        \"jnb\": 402,\n        \"dfw\": 195,\n        \"gdl\": 487,\n        \"phx\": 189,\n        \"sjc\": 291\n      },\n      {\n        \"timestamp\": \"2025-08-22T18:00:00.000Z\",\n        \"cdg\": 289,\n        \"fra\": 149,\n        \"bos\": 104,\n        \"lhr\": 151,\n        \"yyz\": 148,\n        \"lax\": 172,\n        \"otp\": 331,\n        \"eze\": 351,\n        \"bog\": 418,\n        \"gdl\": 487,\n        \"dfw\": 219,\n        \"sjc\": 116,\n        \"phx\": 184,\n        \"iad\": 136,\n        \"ams\": 200,\n        \"jnb\": 784,\n        \"sin\": 496,\n        \"bom\": 308,\n        \"ewr\": 187,\n        \"nrt\": 394,\n        \"mia\": 151,\n        \"mad\": 337,\n        \"syd\": 314,\n        \"sea\": 202,\n        \"arn\": 222,\n        \"hkg\": 632,\n        \"ord\": 187,\n        \"den\": 471,\n        \"atl\": 162,\n        \"yul\": 100,\n        \"gru\": 186,\n        \"gig\": 178,\n        \"scl\": 319\n      },\n      {\n        \"timestamp\": \"2025-08-22T19:00:00.000Z\",\n        \"sin\": 380,\n        \"bom\": 309,\n        \"ewr\": 161,\n        \"mia\": 120,\n        \"nrt\": 440,\n        \"syd\": 318,\n        \"mad\": 492,\n        \"den\": 221,\n        \"ord\": 168,\n        \"hkg\": 354,\n        \"arn\": 260,\n        \"sea\": 336,\n        \"atl\": 158,\n        \"yul\": 117,\n        \"gig\": 551,\n        \"gru\": 192,\n        \"scl\": 316,\n        \"fra\": 190,\n        \"cdg\": 167,\n        \"bos\": 105,\n        \"lhr\": 161,\n        \"yyz\": 128,\n        \"bog\": 418,\n        \"otp\": 315,\n        \"eze\": 490,\n        \"lax\": 172,\n        \"phx\": 189,\n        \"sjc\": 158,\n        \"dfw\": 154,\n        \"gdl\": 664,\n        \"jnb\": 548,\n        \"ams\": 213,\n        \"iad\": 121\n      },\n      {\n        \"timestamp\": \"2025-08-22T20:00:00.000Z\",\n        \"bos\": 82,\n        \"cdg\": 171,\n        \"fra\": 152,\n        \"yyz\": 136,\n        \"lhr\": 153,\n        \"bog\": 373,\n        \"lax\": 141,\n        \"otp\": 288,\n        \"eze\": 290,\n        \"jnb\": 403,\n        \"iad\": 148,\n        \"ams\": 248,\n        \"sjc\": 115,\n        \"phx\": 195,\n        \"gdl\": 590,\n        \"dfw\": 178,\n        \"bom\": 306,\n        \"ewr\": 146,\n        \"sin\": 380,\n        \"syd\": 270,\n        \"mad\": 363,\n        \"mia\": 199,\n        \"nrt\": 274,\n        \"atl\": 147,\n        \"yul\": 84,\n        \"hkg\": 371,\n        \"ord\": 185,\n        \"den\": 239,\n        \"sea\": 212,\n        \"arn\": 205,\n        \"scl\": 327,\n        \"gig\": 180,\n        \"gru\": 430\n      },\n      {\n        \"timestamp\": \"2025-08-22T21:00:00.000Z\",\n        \"sea\": 201,\n        \"arn\": 201,\n        \"hkg\": 733,\n        \"den\": 237,\n        \"ord\": 191,\n        \"atl\": 155,\n        \"yul\": 87,\n        \"gru\": 200,\n        \"gig\": 171,\n        \"scl\": 398,\n        \"sin\": 389,\n        \"bom\": 301,\n        \"ewr\": 190,\n        \"nrt\": 266,\n        \"mia\": 179,\n        \"mad\": 330,\n        \"syd\": 481,\n        \"lax\": 159,\n        \"otp\": 248,\n        \"eze\": 512,\n        \"bog\": 414,\n        \"gdl\": 495,\n        \"dfw\": 131,\n        \"sjc\": 139,\n        \"phx\": 193,\n        \"iad\": 128,\n        \"ams\": 200,\n        \"jnb\": 393,\n        \"cdg\": 195,\n        \"fra\": 154,\n        \"bos\": 141,\n        \"lhr\": 149,\n        \"yyz\": 127\n      },\n      {\n        \"timestamp\": \"2025-08-22T22:00:00.000Z\",\n        \"dfw\": 169,\n        \"gdl\": 594,\n        \"phx\": 175,\n        \"sjc\": 174,\n        \"ams\": 204,\n        \"iad\": 119,\n        \"jnb\": 360,\n        \"eze\": 462,\n        \"otp\": 298,\n        \"lax\": 152,\n        \"bog\": 387,\n        \"lhr\": 559,\n        \"yyz\": 159,\n        \"fra\": 149,\n        \"cdg\": 154,\n        \"bos\": 77,\n        \"gru\": 233,\n        \"gig\": 204,\n        \"scl\": 302,\n        \"arn\": 205,\n        \"sea\": 192,\n        \"den\": 246,\n        \"ord\": 134,\n        \"hkg\": 653,\n        \"yul\": 109,\n        \"atl\": 207,\n        \"nrt\": 451,\n        \"mia\": 138,\n        \"mad\": 339,\n        \"syd\": 263,\n        \"sin\": 502,\n        \"ewr\": 126,\n        \"bom\": 300\n      },\n      {\n        \"timestamp\": \"2025-08-22T23:00:00.000Z\",\n        \"iad\": 115,\n        \"ams\": 191,\n        \"jnb\": 622,\n        \"gdl\": 648,\n        \"dfw\": 222,\n        \"sjc\": 196,\n        \"phx\": 182,\n        \"lax\": 160,\n        \"eze\": 286,\n        \"otp\": 242,\n        \"bog\": 466,\n        \"yyz\": 150,\n        \"lhr\": 168,\n        \"bos\": 97,\n        \"cdg\": 177,\n        \"fra\": 146,\n        \"scl\": 368,\n        \"gru\": 224,\n        \"gig\": 389,\n        \"yul\": 98,\n        \"atl\": 153,\n        \"sea\": 171,\n        \"arn\": 288,\n        \"hkg\": 270,\n        \"ord\": 127,\n        \"den\": 211,\n        \"mad\": 354,\n        \"syd\": 309,\n        \"nrt\": 267,\n        \"mia\": 523,\n        \"ewr\": 115,\n        \"bom\": 292,\n        \"sin\": 442\n      },\n      {\n        \"timestamp\": \"2025-08-23T00:00:00.000Z\",\n        \"fra\": 140,\n        \"cdg\": 145,\n        \"bos\": 115,\n        \"lhr\": 153,\n        \"yyz\": 124,\n        \"bog\": 306,\n        \"eze\": 268,\n        \"otp\": 272,\n        \"lax\": 175,\n        \"phx\": 228,\n        \"sjc\": 127,\n        \"dfw\": 209,\n        \"gdl\": 835,\n        \"jnb\": 377,\n        \"ams\": 188,\n        \"iad\": 112,\n        \"sin\": 306,\n        \"ewr\": 125,\n        \"bom\": 299,\n        \"mia\": 165,\n        \"nrt\": 277,\n        \"syd\": 312,\n        \"mad\": 315,\n        \"den\": 207,\n        \"ord\": 203,\n        \"hkg\": 280,\n        \"arn\": 204,\n        \"sea\": 448,\n        \"yul\": 93,\n        \"atl\": 146,\n        \"gig\": 203,\n        \"gru\": 192,\n        \"scl\": 347\n      },\n      {\n        \"timestamp\": \"2025-08-23T01:00:00.000Z\",\n        \"yul\": 109,\n        \"atl\": 137,\n        \"arn\": 183,\n        \"sea\": 203,\n        \"ord\": 208,\n        \"den\": 245,\n        \"hkg\": 285,\n        \"scl\": 337,\n        \"gru\": 165,\n        \"gig\": 367,\n        \"ewr\": 245,\n        \"bom\": 286,\n        \"sin\": 691,\n        \"mad\": 388,\n        \"syd\": 297,\n        \"nrt\": 445,\n        \"mia\": 147,\n        \"eze\": 325,\n        \"otp\": 249,\n        \"lax\": 294,\n        \"bog\": 507,\n        \"ams\": 231,\n        \"iad\": 101,\n        \"jnb\": 363,\n        \"dfw\": 157,\n        \"gdl\": 658,\n        \"phx\": 187,\n        \"sjc\": 133,\n        \"bos\": 140,\n        \"fra\": 155,\n        \"cdg\": 156,\n        \"yyz\": 126,\n        \"lhr\": 147\n      },\n      {\n        \"timestamp\": \"2025-08-23T02:00:00.000Z\",\n        \"iad\": 187,\n        \"ams\": 200,\n        \"jnb\": 359,\n        \"gdl\": 484,\n        \"dfw\": 199,\n        \"sjc\": 130,\n        \"phx\": 191,\n        \"lax\": 154,\n        \"otp\": 239,\n        \"eze\": 341,\n        \"bog\": 557,\n        \"yyz\": 127,\n        \"lhr\": 160,\n        \"bos\": 116,\n        \"cdg\": 173,\n        \"fra\": 158,\n        \"scl\": 364,\n        \"gru\": 182,\n        \"gig\": 176,\n        \"atl\": 154,\n        \"yul\": 102,\n        \"sea\": 214,\n        \"arn\": 202,\n        \"hkg\": 329,\n        \"ord\": 95,\n        \"den\": 290,\n        \"mad\": 410,\n        \"syd\": 273,\n        \"nrt\": 333,\n        \"mia\": 165,\n        \"bom\": 328,\n        \"ewr\": 132,\n        \"sin\": 412\n      },\n      {\n        \"timestamp\": \"2025-08-23T03:00:00.000Z\",\n        \"scl\": 333,\n        \"gig\": 177,\n        \"gru\": 196,\n        \"atl\": 176,\n        \"yul\": 95,\n        \"den\": 239,\n        \"ord\": 132,\n        \"hkg\": 343,\n        \"arn\": 227,\n        \"sea\": 157,\n        \"syd\": 261,\n        \"mad\": 358,\n        \"mia\": 161,\n        \"nrt\": 439,\n        \"bom\": 295,\n        \"ewr\": 219,\n        \"sin\": 346,\n        \"jnb\": 401,\n        \"ams\": 198,\n        \"iad\": 77,\n        \"phx\": 174,\n        \"sjc\": 128,\n        \"dfw\": 192,\n        \"gdl\": 579,\n        \"bog\": 422,\n        \"otp\": 243,\n        \"eze\": 264,\n        \"lax\": 179,\n        \"yyz\": 120,\n        \"lhr\": 151,\n        \"bos\": 91,\n        \"fra\": 146,\n        \"cdg\": 154\n      },\n      {\n        \"timestamp\": \"2025-08-23T04:00:00.000Z\",\n        \"gru\": 203,\n        \"gig\": 387,\n        \"scl\": 380,\n        \"sea\": 168,\n        \"arn\": 280,\n        \"hkg\": 336,\n        \"den\": 227,\n        \"ord\": 187,\n        \"atl\": 140,\n        \"yul\": 99,\n        \"nrt\": 273,\n        \"mia\": 192,\n        \"mad\": 367,\n        \"syd\": 366,\n        \"sin\": 582,\n        \"bom\": 316,\n        \"ewr\": 123,\n        \"gdl\": 642,\n        \"dfw\": 198,\n        \"sjc\": 115,\n        \"phx\": 194,\n        \"iad\": 126,\n        \"ams\": 252,\n        \"jnb\": 390,\n        \"lax\": 176,\n        \"otp\": 266,\n        \"eze\": 496,\n        \"bog\": 468,\n        \"lhr\": 151,\n        \"yyz\": 156,\n        \"cdg\": 165,\n        \"fra\": 154,\n        \"bos\": 105\n      },\n      {\n        \"timestamp\": \"2025-08-23T05:00:00.000Z\",\n        \"mad\": 325,\n        \"syd\": 318,\n        \"nrt\": 268,\n        \"mia\": 179,\n        \"bom\": 323,\n        \"ewr\": 130,\n        \"sin\": 688,\n        \"scl\": 341,\n        \"gru\": 186,\n        \"gig\": 239,\n        \"atl\": 182,\n        \"yul\": 91,\n        \"sea\": 195,\n        \"arn\": 196,\n        \"hkg\": 357,\n        \"den\": 245,\n        \"ord\": 137,\n        \"yyz\": 233,\n        \"lhr\": 173,\n        \"bos\": 92,\n        \"cdg\": 149,\n        \"fra\": 169,\n        \"iad\": 169,\n        \"ams\": 208,\n        \"jnb\": 433,\n        \"gdl\": 607,\n        \"dfw\": 198,\n        \"sjc\": 117,\n        \"phx\": 345,\n        \"lax\": 161,\n        \"otp\": 308,\n        \"eze\": 603,\n        \"bog\": 380\n      },\n      {\n        \"timestamp\": \"2025-08-23T06:00:00.000Z\",\n        \"lhr\": 165,\n        \"yyz\": 156,\n        \"fra\": 152,\n        \"cdg\": 172,\n        \"bos\": 100,\n        \"dfw\": 166,\n        \"gdl\": 581,\n        \"phx\": 173,\n        \"sjc\": 112,\n        \"ams\": 261,\n        \"iad\": 125,\n        \"jnb\": 385,\n        \"eze\": 328,\n        \"otp\": 233,\n        \"lax\": 154,\n        \"bog\": 547,\n        \"nrt\": 261,\n        \"mia\": 148,\n        \"mad\": 337,\n        \"syd\": 257,\n        \"sin\": 384,\n        \"ewr\": 126,\n        \"bom\": 302,\n        \"gru\": 163,\n        \"gig\": 159,\n        \"scl\": 347,\n        \"arn\": 206,\n        \"sea\": 177,\n        \"den\": 327,\n        \"hkg\": 349,\n        \"yul\": 105,\n        \"atl\": 157\n      },\n      {\n        \"timestamp\": \"2025-08-23T07:00:00.000Z\",\n        \"cdg\": 150,\n        \"fra\": 159,\n        \"bos\": 118,\n        \"lhr\": 166,\n        \"yyz\": 114,\n        \"lax\": 147,\n        \"eze\": 261,\n        \"otp\": 277,\n        \"bog\": 234,\n        \"gdl\": 616,\n        \"dfw\": 201,\n        \"sjc\": 149,\n        \"phx\": 217,\n        \"iad\": 69,\n        \"ams\": 209,\n        \"jnb\": 418,\n        \"sin\": 564,\n        \"ewr\": 113,\n        \"bom\": 565,\n        \"nrt\": 346,\n        \"mia\": 149,\n        \"mad\": 532,\n        \"syd\": 302,\n        \"sea\": 196,\n        \"arn\": 199,\n        \"hkg\": 269,\n        \"ord\": 179,\n        \"den\": 204,\n        \"yul\": 110,\n        \"atl\": 181,\n        \"gru\": 155,\n        \"gig\": 245,\n        \"scl\": 387\n      },\n      {\n        \"timestamp\": \"2025-08-23T08:00:00.000Z\",\n        \"scl\": 310,\n        \"gig\": 355,\n        \"gru\": 203,\n        \"atl\": 138,\n        \"yul\": 120,\n        \"den\": 228,\n        \"ord\": 184,\n        \"hkg\": 367,\n        \"arn\": 178,\n        \"sea\": 182,\n        \"syd\": 307,\n        \"mad\": 322,\n        \"mia\": 182,\n        \"nrt\": 266,\n        \"bom\": 325,\n        \"ewr\": 139,\n        \"sin\": 351,\n        \"jnb\": 384,\n        \"ams\": 258,\n        \"iad\": 182,\n        \"phx\": 202,\n        \"sjc\": 117,\n        \"dfw\": 221,\n        \"gdl\": 407,\n        \"bog\": 399,\n        \"otp\": 272,\n        \"eze\": 289,\n        \"lax\": 171,\n        \"yyz\": 137,\n        \"lhr\": 135,\n        \"bos\": 75,\n        \"fra\": 147,\n        \"cdg\": 145\n      },\n      {\n        \"timestamp\": \"2025-08-23T09:00:00.000Z\",\n        \"fra\": 332,\n        \"cdg\": 203,\n        \"bos\": 101,\n        \"lhr\": 150,\n        \"yyz\": 123,\n        \"eze\": 334,\n        \"otp\": 247,\n        \"lax\": 141,\n        \"bog\": 358,\n        \"dfw\": 206,\n        \"gdl\": 580,\n        \"phx\": 186,\n        \"sjc\": 215,\n        \"ams\": 202,\n        \"iad\": 187,\n        \"jnb\": 361,\n        \"sin\": 350,\n        \"ewr\": 201,\n        \"bom\": 565,\n        \"nrt\": 272,\n        \"mia\": 194,\n        \"mad\": 342,\n        \"syd\": 259,\n        \"arn\": 222,\n        \"sea\": 234,\n        \"den\": 249,\n        \"ord\": 186,\n        \"hkg\": 394,\n        \"yul\": 111,\n        \"atl\": 112,\n        \"gru\": 199,\n        \"gig\": 207,\n        \"scl\": 334\n      },\n      {\n        \"timestamp\": \"2025-08-23T10:00:00.000Z\",\n        \"bom\": 283,\n        \"ewr\": 268,\n        \"sin\": 320,\n        \"mad\": 346,\n        \"syd\": 306,\n        \"nrt\": 266,\n        \"mia\": 158,\n        \"atl\": 175,\n        \"yul\": 89,\n        \"sea\": 164,\n        \"arn\": 215,\n        \"hkg\": 354,\n        \"ord\": 131,\n        \"den\": 219,\n        \"scl\": 310,\n        \"gru\": 207,\n        \"gig\": 200,\n        \"bos\": 114,\n        \"cdg\": 155,\n        \"fra\": 149,\n        \"yyz\": 123,\n        \"lhr\": 361,\n        \"lax\": 167,\n        \"otp\": 254,\n        \"eze\": 294,\n        \"bog\": 507,\n        \"iad\": 187,\n        \"ams\": 259,\n        \"jnb\": 385,\n        \"gdl\": 511,\n        \"dfw\": 195,\n        \"sjc\": 118,\n        \"phx\": 192\n      },\n      {\n        \"timestamp\": \"2025-08-23T11:00:00.000Z\",\n        \"bos\": 131,\n        \"fra\": 144,\n        \"cdg\": 161,\n        \"yyz\": 154,\n        \"lhr\": 155,\n        \"bog\": 457,\n        \"otp\": 266,\n        \"eze\": 484,\n        \"lax\": 168,\n        \"jnb\": 384,\n        \"ams\": 197,\n        \"iad\": 363,\n        \"phx\": 164,\n        \"sjc\": 126,\n        \"dfw\": 153,\n        \"gdl\": 529,\n        \"bom\": 346,\n        \"ewr\": 129,\n        \"sin\": 422,\n        \"syd\": 262,\n        \"mad\": 358,\n        \"mia\": 145,\n        \"nrt\": 222,\n        \"atl\": 110,\n        \"yul\": 82,\n        \"den\": 217,\n        \"ord\": 182,\n        \"hkg\": 270,\n        \"arn\": 212,\n        \"sea\": 192,\n        \"scl\": 539,\n        \"gig\": 250,\n        \"gru\": 171\n      },\n      {\n        \"timestamp\": \"2025-08-23T12:00:00.000Z\",\n        \"yyz\": 389,\n        \"lhr\": 434,\n        \"bos\": 103,\n        \"cdg\": 169,\n        \"fra\": 180,\n        \"iad\": 135,\n        \"ams\": 186,\n        \"jnb\": 454,\n        \"gdl\": 618,\n        \"dfw\": 188,\n        \"sjc\": 131,\n        \"phx\": 199,\n        \"lax\": 144,\n        \"otp\": 282,\n        \"eze\": 308,\n        \"bog\": 552,\n        \"mad\": 344,\n        \"syd\": 310,\n        \"nrt\": 261,\n        \"mia\": 174,\n        \"bom\": 317,\n        \"ewr\": 130,\n        \"sin\": 501,\n        \"scl\": 335,\n        \"gru\": 162,\n        \"gig\": 201,\n        \"atl\": 164,\n        \"yul\": 105,\n        \"sea\": 208,\n        \"arn\": 213,\n        \"hkg\": 360,\n        \"ord\": 139,\n        \"den\": 255\n      },\n      {\n        \"timestamp\": \"2025-08-23T13:00:00.000Z\",\n        \"gig\": 227,\n        \"gru\": 236,\n        \"scl\": 504,\n        \"hkg\": 272,\n        \"den\": 494,\n        \"ord\": 129,\n        \"sea\": 197,\n        \"arn\": 382,\n        \"atl\": 175,\n        \"yul\": 96,\n        \"mia\": 160,\n        \"nrt\": 264,\n        \"syd\": 308,\n        \"mad\": 326,\n        \"sin\": 437,\n        \"bom\": 590,\n        \"ewr\": 181,\n        \"sjc\": 119,\n        \"phx\": 182,\n        \"gdl\": 583,\n        \"dfw\": 190,\n        \"jnb\": 396,\n        \"iad\": 127,\n        \"ams\": 249,\n        \"bog\": 360,\n        \"lax\": 165,\n        \"otp\": 297,\n        \"eze\": 318,\n        \"lhr\": 146,\n        \"yyz\": 133,\n        \"cdg\": 507,\n        \"fra\": 447,\n        \"bos\": 115\n      },\n      {\n        \"timestamp\": \"2025-08-23T14:00:00.000Z\",\n        \"scl\": 345,\n        \"gru\": 194,\n        \"gig\": 214,\n        \"yul\": 93,\n        \"atl\": 157,\n        \"arn\": 204,\n        \"sea\": 178,\n        \"den\": 245,\n        \"ord\": 80,\n        \"hkg\": 344,\n        \"mad\": 343,\n        \"syd\": 329,\n        \"nrt\": 227,\n        \"mia\": 136,\n        \"ewr\": 123,\n        \"bom\": 314,\n        \"sin\": 396,\n        \"ams\": 188,\n        \"iad\": 147,\n        \"jnb\": 406,\n        \"dfw\": 156,\n        \"gdl\": 585,\n        \"phx\": 179,\n        \"sjc\": 130,\n        \"eze\": 295,\n        \"otp\": 247,\n        \"lax\": 156,\n        \"bog\": 400,\n        \"yyz\": 189,\n        \"lhr\": 152,\n        \"bos\": 107,\n        \"fra\": 222,\n        \"cdg\": 276\n      },\n      {\n        \"timestamp\": \"2025-08-23T15:00:00.000Z\",\n        \"gru\": 165,\n        \"gig\": 367,\n        \"scl\": 332,\n        \"sea\": 203,\n        \"arn\": 194,\n        \"hkg\": 631,\n        \"ord\": 187,\n        \"den\": 219,\n        \"yul\": 93,\n        \"atl\": 103,\n        \"nrt\": 266,\n        \"mia\": 141,\n        \"mad\": 330,\n        \"syd\": 298,\n        \"sin\": 435,\n        \"ewr\": 118,\n        \"bom\": 293,\n        \"gdl\": 448,\n        \"dfw\": 229,\n        \"sjc\": 125,\n        \"phx\": 210,\n        \"iad\": 140,\n        \"ams\": 202,\n        \"jnb\": 363,\n        \"lax\": 168,\n        \"eze\": 331,\n        \"otp\": 254,\n        \"bog\": 469,\n        \"lhr\": 175,\n        \"yyz\": 127,\n        \"cdg\": 157,\n        \"fra\": 141,\n        \"bos\": 122\n      },\n      {\n        \"timestamp\": \"2025-08-23T16:00:00.000Z\",\n        \"atl\": 168,\n        \"yul\": 101,\n        \"hkg\": 360,\n        \"ord\": 153,\n        \"den\": 248,\n        \"sea\": 185,\n        \"arn\": 273,\n        \"scl\": 363,\n        \"gig\": 179,\n        \"gru\": 196,\n        \"bom\": 293,\n        \"ewr\": 148,\n        \"sin\": 584,\n        \"syd\": 304,\n        \"mad\": 332,\n        \"mia\": 152,\n        \"nrt\": 298,\n        \"bog\": 302,\n        \"lax\": 151,\n        \"otp\": 286,\n        \"eze\": 281,\n        \"jnb\": 405,\n        \"iad\": 97,\n        \"ams\": 216,\n        \"sjc\": 125,\n        \"phx\": 202,\n        \"gdl\": 496,\n        \"dfw\": 203,\n        \"bos\": 116,\n        \"cdg\": 353,\n        \"fra\": 137,\n        \"yyz\": 121,\n        \"lhr\": 150\n      },\n      {\n        \"timestamp\": \"2025-08-23T17:00:00.000Z\",\n        \"cdg\": 158,\n        \"fra\": 151,\n        \"bos\": 82,\n        \"lhr\": 161,\n        \"yyz\": 123,\n        \"lax\": 142,\n        \"otp\": 285,\n        \"eze\": 241,\n        \"bog\": 440,\n        \"gdl\": 586,\n        \"dfw\": 204,\n        \"sjc\": 142,\n        \"phx\": 224,\n        \"iad\": 127,\n        \"ams\": 205,\n        \"jnb\": 362,\n        \"sin\": 370,\n        \"bom\": 318,\n        \"ewr\": 119,\n        \"nrt\": 311,\n        \"mia\": 156,\n        \"mad\": 379,\n        \"syd\": 299,\n        \"sea\": 190,\n        \"arn\": 240,\n        \"hkg\": 334,\n        \"den\": 214,\n        \"ord\": 128,\n        \"atl\": 144,\n        \"yul\": 129,\n        \"gru\": 194,\n        \"gig\": 235,\n        \"scl\": 334\n      },\n      {\n        \"timestamp\": \"2025-08-23T18:00:00.000Z\",\n        \"bog\": 387,\n        \"lax\": 157,\n        \"eze\": 296,\n        \"otp\": 304,\n        \"sjc\": 133,\n        \"phx\": 195,\n        \"gdl\": 487,\n        \"dfw\": 141,\n        \"jnb\": 386,\n        \"iad\": 150,\n        \"ams\": 205,\n        \"cdg\": 157,\n        \"fra\": 152,\n        \"bos\": 124,\n        \"lhr\": 156,\n        \"yyz\": 129,\n        \"hkg\": 570,\n        \"ord\": 433,\n        \"den\": 249,\n        \"sea\": 220,\n        \"arn\": 213,\n        \"yul\": 96,\n        \"atl\": 140,\n        \"gig\": 286,\n        \"gru\": 172,\n        \"scl\": 343,\n        \"sin\": 356,\n        \"ewr\": 118,\n        \"bom\": 296,\n        \"mia\": 166,\n        \"nrt\": 258,\n        \"syd\": 361,\n        \"mad\": 326\n      },\n      {\n        \"timestamp\": \"2025-08-23T19:00:00.000Z\",\n        \"arn\": 530,\n        \"sea\": 323,\n        \"den\": 212,\n        \"ord\": 80,\n        \"hkg\": 332,\n        \"yul\": 110,\n        \"atl\": 134,\n        \"gru\": 178,\n        \"gig\": 244,\n        \"scl\": 392,\n        \"sin\": 377,\n        \"ewr\": 119,\n        \"bom\": 542,\n        \"nrt\": 264,\n        \"mia\": 145,\n        \"mad\": 342,\n        \"syd\": 303,\n        \"eze\": 465,\n        \"otp\": 264,\n        \"lax\": 167,\n        \"bog\": 438,\n        \"dfw\": 146,\n        \"gdl\": 530,\n        \"phx\": 196,\n        \"sjc\": 134,\n        \"ams\": 211,\n        \"iad\": 111,\n        \"jnb\": 420,\n        \"fra\": 155,\n        \"cdg\": 296,\n        \"bos\": 81,\n        \"lhr\": 152,\n        \"yyz\": 141\n      },\n      {\n        \"timestamp\": \"2025-08-23T20:00:00.000Z\",\n        \"yul\": 102,\n        \"atl\": 172,\n        \"hkg\": 332,\n        \"den\": 245,\n        \"ord\": 167,\n        \"sea\": 192,\n        \"arn\": 200,\n        \"scl\": 513,\n        \"gig\": 231,\n        \"gru\": 207,\n        \"ewr\": 125,\n        \"bom\": 324,\n        \"sin\": 465,\n        \"syd\": 299,\n        \"mad\": 338,\n        \"mia\": 170,\n        \"nrt\": 261,\n        \"bog\": 478,\n        \"lax\": 204,\n        \"eze\": 269,\n        \"otp\": 284,\n        \"jnb\": 359,\n        \"iad\": 540,\n        \"ams\": 193,\n        \"sjc\": 121,\n        \"phx\": 197,\n        \"gdl\": 593,\n        \"dfw\": 206,\n        \"bos\": 102,\n        \"cdg\": 172,\n        \"fra\": 210,\n        \"yyz\": 130,\n        \"lhr\": 245\n      },\n      {\n        \"timestamp\": \"2025-08-23T21:00:00.000Z\",\n        \"yyz\": 137,\n        \"lhr\": 154,\n        \"bos\": 92,\n        \"cdg\": 157,\n        \"fra\": 151,\n        \"iad\": 41,\n        \"ams\": 198,\n        \"jnb\": 407,\n        \"gdl\": 470,\n        \"dfw\": 244,\n        \"sjc\": 126,\n        \"phx\": 207,\n        \"lax\": 156,\n        \"eze\": 491,\n        \"otp\": 316,\n        \"bog\": 538,\n        \"mad\": 312,\n        \"syd\": 314,\n        \"nrt\": 275,\n        \"mia\": 141,\n        \"ewr\": 117,\n        \"bom\": 292,\n        \"sin\": 389,\n        \"scl\": 339,\n        \"gru\": 218,\n        \"gig\": 359,\n        \"yul\": 104,\n        \"atl\": 171,\n        \"sea\": 186,\n        \"arn\": 229,\n        \"hkg\": 291,\n        \"den\": 232,\n        \"ord\": 213\n      },\n      {\n        \"timestamp\": \"2025-08-23T22:00:00.000Z\",\n        \"nrt\": 264,\n        \"mia\": 146,\n        \"mad\": 376,\n        \"syd\": 324,\n        \"sin\": 361,\n        \"bom\": 331,\n        \"ewr\": 130,\n        \"gru\": 151,\n        \"gig\": 239,\n        \"scl\": 286,\n        \"arn\": 219,\n        \"sea\": 197,\n        \"ord\": 145,\n        \"den\": 236,\n        \"hkg\": 558,\n        \"atl\": 149,\n        \"yul\": 82,\n        \"lhr\": 152,\n        \"yyz\": 136,\n        \"fra\": 150,\n        \"cdg\": 174,\n        \"bos\": 85,\n        \"dfw\": 204,\n        \"gdl\": 535,\n        \"phx\": 195,\n        \"sjc\": 126,\n        \"ams\": 245,\n        \"iad\": 145,\n        \"jnb\": 347,\n        \"otp\": 241,\n        \"eze\": 538,\n        \"lax\": 149,\n        \"bog\": 236\n      },\n      {\n        \"timestamp\": \"2025-08-23T23:00:00.000Z\",\n        \"yyz\": 134,\n        \"lhr\": 160,\n        \"bos\": 103,\n        \"cdg\": 166,\n        \"fra\": 165,\n        \"jnb\": 381,\n        \"iad\": 104,\n        \"ams\": 198,\n        \"sjc\": 126,\n        \"phx\": 198,\n        \"gdl\": 479,\n        \"dfw\": 143,\n        \"bog\": 359,\n        \"lax\": 150,\n        \"otp\": 251,\n        \"eze\": 269,\n        \"syd\": 298,\n        \"mad\": 345,\n        \"mia\": 192,\n        \"nrt\": 258,\n        \"bom\": 301,\n        \"ewr\": 203,\n        \"sin\": 433,\n        \"scl\": 507,\n        \"gig\": 175,\n        \"gru\": 196,\n        \"atl\": 147,\n        \"yul\": 119,\n        \"hkg\": 278,\n        \"ord\": 131,\n        \"den\": 252,\n        \"sea\": 186,\n        \"arn\": 221\n      },\n      {\n        \"timestamp\": \"2025-08-24T00:00:00.000Z\",\n        \"lhr\": 229,\n        \"yyz\": 128,\n        \"fra\": 151,\n        \"cdg\": 147,\n        \"bos\": 107,\n        \"phx\": 185,\n        \"sjc\": 133,\n        \"dfw\": 200,\n        \"gdl\": 615,\n        \"jnb\": 475,\n        \"ams\": 230,\n        \"iad\": 191,\n        \"bog\": 708,\n        \"eze\": 251,\n        \"otp\": 298,\n        \"lax\": 148,\n        \"mia\": 177,\n        \"nrt\": 267,\n        \"syd\": 321,\n        \"mad\": 346,\n        \"sin\": 365,\n        \"ewr\": 126,\n        \"bom\": 304,\n        \"gig\": 397,\n        \"gru\": 175,\n        \"scl\": 338,\n        \"den\": 242,\n        \"ord\": 128,\n        \"hkg\": 274,\n        \"arn\": 248,\n        \"sea\": 177,\n        \"yul\": 114,\n        \"atl\": 149\n      },\n      {\n        \"timestamp\": \"2025-08-24T01:00:00.000Z\",\n        \"ord\": 165,\n        \"den\": 257,\n        \"hkg\": 343,\n        \"arn\": 194,\n        \"sea\": 187,\n        \"atl\": 148,\n        \"yul\": 112,\n        \"gig\": 230,\n        \"gru\": 220,\n        \"scl\": 333,\n        \"sin\": 441,\n        \"bom\": 296,\n        \"ewr\": 60,\n        \"mia\": 185,\n        \"nrt\": 457,\n        \"syd\": 299,\n        \"mad\": 330,\n        \"bog\": 456,\n        \"otp\": 250,\n        \"eze\": 230,\n        \"lax\": 172,\n        \"phx\": 209,\n        \"sjc\": 121,\n        \"dfw\": 123,\n        \"gdl\": 468,\n        \"jnb\": 359,\n        \"ams\": 190,\n        \"iad\": 206,\n        \"fra\": 152,\n        \"cdg\": 174,\n        \"bos\": 110,\n        \"lhr\": 161,\n        \"yyz\": 136\n      },\n      {\n        \"timestamp\": \"2025-08-24T02:00:00.000Z\",\n        \"lax\": 163,\n        \"otp\": 256,\n        \"eze\": 656,\n        \"bog\": 577,\n        \"iad\": 494,\n        \"ams\": 184,\n        \"jnb\": 390,\n        \"gdl\": 491,\n        \"dfw\": 217,\n        \"sjc\": 121,\n        \"phx\": 248,\n        \"bos\": 97,\n        \"cdg\": 164,\n        \"fra\": 159,\n        \"yyz\": 128,\n        \"lhr\": 141,\n        \"atl\": 151,\n        \"yul\": 101,\n        \"sea\": 171,\n        \"arn\": 211,\n        \"hkg\": 297,\n        \"ord\": 152,\n        \"den\": 232,\n        \"scl\": 306,\n        \"gru\": 198,\n        \"gig\": 224,\n        \"bom\": 335,\n        \"ewr\": 120,\n        \"sin\": 544,\n        \"mad\": 347,\n        \"syd\": 315,\n        \"nrt\": 261,\n        \"mia\": 149\n      },\n      {\n        \"timestamp\": \"2025-08-24T03:00:00.000Z\",\n        \"nrt\": 265,\n        \"mia\": 121,\n        \"mad\": 347,\n        \"syd\": 305,\n        \"sin\": 599,\n        \"bom\": 322,\n        \"ewr\": 125,\n        \"gru\": 186,\n        \"gig\": 182,\n        \"scl\": 347,\n        \"sea\": 195,\n        \"arn\": 237,\n        \"hkg\": 330,\n        \"den\": 468,\n        \"ord\": 191,\n        \"atl\": 138,\n        \"yul\": 81,\n        \"lhr\": 140,\n        \"yyz\": 255,\n        \"cdg\": 154,\n        \"fra\": 136,\n        \"bos\": 107,\n        \"gdl\": 500,\n        \"dfw\": 213,\n        \"sjc\": 120,\n        \"phx\": 196,\n        \"iad\": 137,\n        \"ams\": 180,\n        \"jnb\": 382,\n        \"lax\": 255,\n        \"otp\": 277,\n        \"eze\": 296,\n        \"bog\": 484\n      },\n      {\n        \"timestamp\": \"2025-08-24T04:00:00.000Z\",\n        \"gru\": 488,\n        \"gig\": 204,\n        \"scl\": 300,\n        \"sea\": 216,\n        \"arn\": 223,\n        \"hkg\": 341,\n        \"ord\": 123,\n        \"den\": 229,\n        \"yul\": 98,\n        \"atl\": 192,\n        \"nrt\": 290,\n        \"mia\": 123,\n        \"mad\": 432,\n        \"syd\": 308,\n        \"sin\": 745,\n        \"ewr\": 155,\n        \"bom\": 298,\n        \"gdl\": 1142,\n        \"dfw\": 194,\n        \"sjc\": 127,\n        \"phx\": 190,\n        \"iad\": 174,\n        \"ams\": 160,\n        \"jnb\": 363,\n        \"lax\": 156,\n        \"eze\": 314,\n        \"otp\": 263,\n        \"bog\": 369,\n        \"lhr\": 166,\n        \"yyz\": 137,\n        \"cdg\": 169,\n        \"fra\": 188,\n        \"bos\": 102\n      },\n      {\n        \"timestamp\": \"2025-08-24T05:00:00.000Z\",\n        \"yyz\": 127,\n        \"lhr\": 154,\n        \"bos\": 109,\n        \"cdg\": 163,\n        \"fra\": 156,\n        \"iad\": 106,\n        \"ams\": 249,\n        \"jnb\": 377,\n        \"gdl\": 487,\n        \"dfw\": 190,\n        \"sjc\": 133,\n        \"phx\": 207,\n        \"lax\": 137,\n        \"otp\": 337,\n        \"eze\": 326,\n        \"bog\": 370,\n        \"mad\": 336,\n        \"syd\": 304,\n        \"nrt\": 225,\n        \"mia\": 183,\n        \"bom\": 307,\n        \"ewr\": 127,\n        \"sin\": 575,\n        \"scl\": 478,\n        \"gru\": 168,\n        \"gig\": 379,\n        \"atl\": 148,\n        \"yul\": 118,\n        \"sea\": 235,\n        \"arn\": 232,\n        \"hkg\": 362,\n        \"ord\": 132,\n        \"den\": 206\n      },\n      {\n        \"timestamp\": \"2025-08-24T06:00:00.000Z\",\n        \"bom\": 387,\n        \"ewr\": 120,\n        \"sin\": 396,\n        \"syd\": 284,\n        \"mad\": 329,\n        \"mia\": 151,\n        \"nrt\": 275,\n        \"atl\": 117,\n        \"yul\": 98,\n        \"den\": 269,\n        \"ord\": 112,\n        \"hkg\": 334,\n        \"arn\": 225,\n        \"sea\": 173,\n        \"scl\": 285,\n        \"gig\": 234,\n        \"gru\": 196,\n        \"bos\": 156,\n        \"fra\": 138,\n        \"cdg\": 160,\n        \"yyz\": 135,\n        \"lhr\": 170,\n        \"bog\": 439,\n        \"otp\": 281,\n        \"eze\": 254,\n        \"lax\": 134,\n        \"jnb\": 355,\n        \"ams\": 207,\n        \"iad\": 166,\n        \"phx\": 188,\n        \"sjc\": 116,\n        \"dfw\": 206,\n        \"gdl\": 665\n      },\n      {\n        \"timestamp\": \"2025-08-24T07:00:00.000Z\",\n        \"bog\": 384,\n        \"eze\": 348,\n        \"otp\": 265,\n        \"lax\": 150,\n        \"phx\": 200,\n        \"sjc\": 118,\n        \"dfw\": 348,\n        \"gdl\": 629,\n        \"jnb\": 344,\n        \"ams\": 242,\n        \"iad\": 197,\n        \"fra\": 141,\n        \"cdg\": 170,\n        \"bos\": 99,\n        \"lhr\": 146,\n        \"yyz\": 136,\n        \"den\": 255,\n        \"ord\": 144,\n        \"hkg\": 264,\n        \"arn\": 239,\n        \"sea\": 232,\n        \"yul\": 112,\n        \"atl\": 135,\n        \"gig\": 340,\n        \"gru\": 339,\n        \"scl\": 373,\n        \"sin\": 496,\n        \"ewr\": 116,\n        \"bom\": 287,\n        \"mia\": 207,\n        \"nrt\": 219,\n        \"syd\": 301,\n        \"mad\": 344\n      },\n      {\n        \"timestamp\": \"2025-08-24T08:00:00.000Z\",\n        \"cdg\": 159,\n        \"fra\": 144,\n        \"bos\": 98,\n        \"lhr\": 149,\n        \"yyz\": 138,\n        \"bog\": 417,\n        \"lax\": 141,\n        \"eze\": 475,\n        \"otp\": 248,\n        \"sjc\": 120,\n        \"phx\": 184,\n        \"gdl\": 592,\n        \"dfw\": 161,\n        \"jnb\": 388,\n        \"iad\": 131,\n        \"ams\": 190,\n        \"sin\": 496,\n        \"ewr\": 151,\n        \"bom\": 558,\n        \"mia\": 207,\n        \"nrt\": 457,\n        \"syd\": 305,\n        \"mad\": 326,\n        \"hkg\": 350,\n        \"ord\": 125,\n        \"den\": 249,\n        \"sea\": 160,\n        \"arn\": 218,\n        \"yul\": 88,\n        \"atl\": 143,\n        \"gig\": 211,\n        \"gru\": 188,\n        \"scl\": 406\n      },\n      {\n        \"timestamp\": \"2025-08-24T09:00:00.000Z\",\n        \"atl\": 122,\n        \"yul\": 106,\n        \"hkg\": 334,\n        \"ord\": 123,\n        \"den\": 238,\n        \"sea\": 183,\n        \"arn\": 196,\n        \"scl\": 334,\n        \"gig\": 202,\n        \"gru\": 204,\n        \"bom\": 279,\n        \"ewr\": 185,\n        \"sin\": 695,\n        \"syd\": 345,\n        \"mad\": 333,\n        \"mia\": 175,\n        \"nrt\": 225,\n        \"bog\": 384,\n        \"lax\": 168,\n        \"otp\": 286,\n        \"eze\": 302,\n        \"jnb\": 363,\n        \"iad\": 168,\n        \"ams\": 212,\n        \"sjc\": 118,\n        \"phx\": 178,\n        \"gdl\": 576,\n        \"dfw\": 225,\n        \"bos\": 104,\n        \"cdg\": 315,\n        \"fra\": 159,\n        \"yyz\": 124,\n        \"lhr\": 274\n      },\n      {\n        \"timestamp\": \"2025-08-24T10:00:00.000Z\",\n        \"ams\": 187,\n        \"iad\": 146,\n        \"jnb\": 410,\n        \"dfw\": 181,\n        \"gdl\": 459,\n        \"phx\": 179,\n        \"sjc\": 115,\n        \"otp\": 237,\n        \"eze\": 293,\n        \"lax\": 147,\n        \"bog\": 724,\n        \"yyz\": 124,\n        \"lhr\": 150,\n        \"bos\": 114,\n        \"fra\": 142,\n        \"cdg\": 177,\n        \"scl\": 393,\n        \"gru\": 233,\n        \"gig\": 373,\n        \"atl\": 178,\n        \"yul\": 101,\n        \"arn\": 198,\n        \"sea\": 194,\n        \"den\": 239,\n        \"ord\": 94,\n        \"hkg\": 461,\n        \"mad\": 350,\n        \"syd\": 305,\n        \"nrt\": 273,\n        \"mia\": 148,\n        \"bom\": 297,\n        \"ewr\": 295,\n        \"sin\": 503\n      },\n      {\n        \"timestamp\": \"2025-08-24T11:00:00.000Z\",\n        \"nrt\": 258,\n        \"mia\": 132,\n        \"mad\": 334,\n        \"syd\": 260,\n        \"sin\": 528,\n        \"ewr\": 176,\n        \"bom\": 308,\n        \"gru\": 190,\n        \"gig\": 198,\n        \"scl\": 533,\n        \"arn\": 232,\n        \"sea\": 166,\n        \"den\": 186,\n        \"ord\": 170,\n        \"hkg\": 362,\n        \"yul\": 86,\n        \"atl\": 158,\n        \"lhr\": 154,\n        \"yyz\": 141,\n        \"fra\": 142,\n        \"cdg\": 444,\n        \"bos\": 93,\n        \"dfw\": 159,\n        \"gdl\": 585,\n        \"phx\": 194,\n        \"sjc\": 125,\n        \"ams\": 203,\n        \"iad\": 203,\n        \"jnb\": 363,\n        \"eze\": 316,\n        \"otp\": 313,\n        \"lax\": 148,\n        \"bog\": 363\n      },\n      {\n        \"timestamp\": \"2025-08-24T12:00:00.000Z\",\n        \"gru\": 186,\n        \"gig\": 348,\n        \"scl\": 331,\n        \"arn\": 199,\n        \"sea\": 179,\n        \"ord\": 131,\n        \"den\": 265,\n        \"hkg\": 333,\n        \"atl\": 196,\n        \"yul\": 98,\n        \"nrt\": 278,\n        \"mia\": 151,\n        \"mad\": 355,\n        \"syd\": 348,\n        \"sin\": 364,\n        \"bom\": 592,\n        \"ewr\": 136,\n        \"dfw\": 200,\n        \"gdl\": 517,\n        \"phx\": 197,\n        \"sjc\": 129,\n        \"ams\": 207,\n        \"iad\": 192,\n        \"jnb\": 410,\n        \"otp\": 241,\n        \"eze\": 474,\n        \"lax\": 160,\n        \"bog\": 391,\n        \"lhr\": 153,\n        \"yyz\": 130,\n        \"fra\": 141,\n        \"cdg\": 289,\n        \"bos\": 206\n      },\n      {\n        \"timestamp\": \"2025-08-24T13:00:00.000Z\",\n        \"lax\": 143,\n        \"otp\": 287,\n        \"eze\": 478,\n        \"bog\": 351,\n        \"gdl\": 749,\n        \"dfw\": 187,\n        \"sjc\": 140,\n        \"phx\": 209,\n        \"iad\": 130,\n        \"ams\": 191,\n        \"jnb\": 342,\n        \"cdg\": 420,\n        \"fra\": 183,\n        \"bos\": 109,\n        \"lhr\": 148,\n        \"yyz\": 136,\n        \"sea\": 234,\n        \"arn\": 352,\n        \"hkg\": 333,\n        \"ord\": 131,\n        \"den\": 248,\n        \"atl\": 114,\n        \"yul\": 107,\n        \"gru\": 185,\n        \"gig\": 207,\n        \"scl\": 548,\n        \"sin\": 610,\n        \"bom\": 319,\n        \"ewr\": 176,\n        \"nrt\": 276,\n        \"mia\": 206,\n        \"mad\": 341,\n        \"syd\": 302\n      },\n      {\n        \"timestamp\": \"2025-08-24T14:00:00.000Z\",\n        \"atl\": 172,\n        \"yul\": 105,\n        \"ord\": 153,\n        \"den\": 264,\n        \"hkg\": 300,\n        \"arn\": 217,\n        \"sea\": 188,\n        \"scl\": 383,\n        \"gig\": 245,\n        \"gru\": 191,\n        \"bom\": 297,\n        \"ewr\": 126,\n        \"sin\": 499,\n        \"syd\": 462,\n        \"mad\": 355,\n        \"mia\": 149,\n        \"nrt\": 223,\n        \"bog\": 521,\n        \"otp\": 243,\n        \"eze\": 313,\n        \"lax\": 138,\n        \"jnb\": 384,\n        \"ams\": 190,\n        \"iad\": 151,\n        \"phx\": 198,\n        \"sjc\": 132,\n        \"dfw\": 178,\n        \"gdl\": 512,\n        \"bos\": 90,\n        \"fra\": 149,\n        \"cdg\": 274,\n        \"yyz\": 117,\n        \"lhr\": 154\n      },\n      {\n        \"timestamp\": \"2025-08-24T15:00:00.000Z\",\n        \"yyz\": 162,\n        \"lhr\": 351,\n        \"bos\": 88,\n        \"fra\": 142,\n        \"cdg\": 199,\n        \"jnb\": 363,\n        \"ams\": 191,\n        \"iad\": 229,\n        \"phx\": 197,\n        \"sjc\": 128,\n        \"dfw\": 174,\n        \"gdl\": 488,\n        \"bog\": 383,\n        \"eze\": 269,\n        \"otp\": 250,\n        \"lax\": 144,\n        \"syd\": 345,\n        \"mad\": 349,\n        \"mia\": 182,\n        \"nrt\": 229,\n        \"ewr\": 122,\n        \"bom\": 301,\n        \"sin\": 447,\n        \"scl\": 354,\n        \"gig\": 339,\n        \"gru\": 363,\n        \"yul\": 107,\n        \"atl\": 211,\n        \"den\": 241,\n        \"ord\": 127,\n        \"hkg\": 349,\n        \"arn\": 206,\n        \"sea\": 172\n      },\n      {\n        \"timestamp\": \"2025-08-24T16:00:00.000Z\",\n        \"fra\": 148,\n        \"cdg\": 485,\n        \"bos\": 82,\n        \"lhr\": 154,\n        \"yyz\": 128,\n        \"eze\": 302,\n        \"otp\": 302,\n        \"lax\": 140,\n        \"bog\": 325,\n        \"dfw\": 132,\n        \"gdl\": 594,\n        \"phx\": 200,\n        \"sjc\": 122,\n        \"ams\": 184,\n        \"iad\": 148,\n        \"jnb\": 390,\n        \"sin\": 413,\n        \"ewr\": 129,\n        \"bom\": 316,\n        \"nrt\": 257,\n        \"mia\": 127,\n        \"mad\": 354,\n        \"syd\": 331,\n        \"arn\": 218,\n        \"sea\": 209,\n        \"den\": 284,\n        \"ord\": 152,\n        \"hkg\": 498,\n        \"yul\": 109,\n        \"atl\": 144,\n        \"gru\": 170,\n        \"gig\": 428,\n        \"scl\": 331\n      },\n      {\n        \"timestamp\": \"2025-08-24T17:00:00.000Z\",\n        \"bog\": 441,\n        \"otp\": 287,\n        \"eze\": 326,\n        \"lax\": 153,\n        \"jnb\": 393,\n        \"ams\": 177,\n        \"iad\": 47,\n        \"phx\": 178,\n        \"sjc\": 452,\n        \"dfw\": 228,\n        \"gdl\": 583,\n        \"bos\": 96,\n        \"fra\": 161,\n        \"cdg\": 150,\n        \"yyz\": 163,\n        \"lhr\": 197,\n        \"atl\": 180,\n        \"yul\": 88,\n        \"ord\": 183,\n        \"den\": 284,\n        \"hkg\": 461,\n        \"arn\": 214,\n        \"sea\": 225,\n        \"scl\": 334,\n        \"gig\": 234,\n        \"gru\": 218,\n        \"bom\": 297,\n        \"ewr\": 185,\n        \"sin\": 514,\n        \"syd\": 312,\n        \"mad\": 357,\n        \"mia\": 168,\n        \"nrt\": 455\n      },\n      {\n        \"timestamp\": \"2025-08-24T18:00:00.000Z\",\n        \"sea\": 193,\n        \"arn\": 345,\n        \"hkg\": 269,\n        \"ord\": 156,\n        \"den\": 224,\n        \"atl\": 182,\n        \"yul\": 102,\n        \"gru\": 324,\n        \"gig\": 393,\n        \"scl\": 314,\n        \"sin\": 354,\n        \"bom\": 308,\n        \"ewr\": 134,\n        \"nrt\": 740,\n        \"mia\": 157,\n        \"mad\": 345,\n        \"syd\": 302,\n        \"lax\": 143,\n        \"otp\": 286,\n        \"eze\": 323,\n        \"bog\": 520,\n        \"gdl\": 469,\n        \"dfw\": 123,\n        \"sjc\": 116,\n        \"phx\": 180,\n        \"iad\": 135,\n        \"ams\": 215,\n        \"jnb\": 337,\n        \"cdg\": 149,\n        \"fra\": 148,\n        \"bos\": 92,\n        \"lhr\": 174,\n        \"yyz\": 125\n      },\n      {\n        \"timestamp\": \"2025-08-24T19:00:00.000Z\",\n        \"lhr\": 144,\n        \"yyz\": 119,\n        \"cdg\": 179,\n        \"fra\": 154,\n        \"bos\": 94,\n        \"gdl\": 497,\n        \"dfw\": 161,\n        \"sjc\": 128,\n        \"phx\": 190,\n        \"iad\": 129,\n        \"ams\": 193,\n        \"jnb\": 370,\n        \"lax\": 166,\n        \"eze\": 340,\n        \"otp\": 257,\n        \"bog\": 393,\n        \"nrt\": 543,\n        \"mia\": 154,\n        \"mad\": 334,\n        \"syd\": 470,\n        \"sin\": 470,\n        \"ewr\": 58,\n        \"bom\": 312,\n        \"gru\": 162,\n        \"gig\": 216,\n        \"scl\": 343,\n        \"sea\": 191,\n        \"arn\": 199,\n        \"hkg\": 348,\n        \"den\": 268,\n        \"ord\": 213,\n        \"yul\": 101,\n        \"atl\": 175\n      },\n      {\n        \"timestamp\": \"2025-08-24T20:00:00.000Z\",\n        \"gdl\": 595,\n        \"dfw\": 200,\n        \"sjc\": 132,\n        \"phx\": 306,\n        \"iad\": 296,\n        \"ams\": 216,\n        \"jnb\": 379,\n        \"lax\": 181,\n        \"otp\": 303,\n        \"eze\": 461,\n        \"bog\": 505,\n        \"lhr\": 155,\n        \"yyz\": 150,\n        \"cdg\": 159,\n        \"fra\": 149,\n        \"bos\": 125,\n        \"gru\": 165,\n        \"gig\": 250,\n        \"scl\": 337,\n        \"sea\": 202,\n        \"arn\": 204,\n        \"hkg\": 279,\n        \"ord\": 198,\n        \"den\": 279,\n        \"atl\": 151,\n        \"yul\": 118,\n        \"nrt\": 266,\n        \"mia\": 151,\n        \"mad\": 362,\n        \"syd\": 298,\n        \"sin\": 324,\n        \"bom\": 320,\n        \"ewr\": 125\n      },\n      {\n        \"timestamp\": \"2025-08-24T21:00:00.000Z\",\n        \"mad\": 329,\n        \"syd\": 309,\n        \"nrt\": 254,\n        \"mia\": 188,\n        \"ewr\": 132,\n        \"bom\": 301,\n        \"sin\": 358,\n        \"scl\": 288,\n        \"gru\": 181,\n        \"gig\": 226,\n        \"yul\": 92,\n        \"atl\": 156,\n        \"sea\": 210,\n        \"arn\": 185,\n        \"hkg\": 285,\n        \"ord\": 138,\n        \"den\": 555,\n        \"yyz\": 161,\n        \"lhr\": 169,\n        \"bos\": 104,\n        \"cdg\": 166,\n        \"fra\": 142,\n        \"iad\": 117,\n        \"ams\": 200,\n        \"jnb\": 373,\n        \"gdl\": 470,\n        \"dfw\": 532,\n        \"sjc\": 123,\n        \"phx\": 177,\n        \"lax\": 138,\n        \"eze\": 444,\n        \"otp\": 271,\n        \"bog\": 503\n      },\n      {\n        \"timestamp\": \"2025-08-24T22:00:00.000Z\",\n        \"fra\": 154,\n        \"cdg\": 166,\n        \"bos\": 123,\n        \"lhr\": 162,\n        \"yyz\": 193,\n        \"bog\": 382,\n        \"otp\": 277,\n        \"eze\": 269,\n        \"lax\": 150,\n        \"phx\": 198,\n        \"sjc\": 142,\n        \"dfw\": 185,\n        \"gdl\": 593,\n        \"jnb\": 571,\n        \"ams\": 199,\n        \"iad\": 133,\n        \"sin\": 425,\n        \"bom\": 321,\n        \"ewr\": 125,\n        \"mia\": 174,\n        \"nrt\": 232,\n        \"syd\": 310,\n        \"mad\": 348,\n        \"den\": 267,\n        \"ord\": 142,\n        \"hkg\": 410,\n        \"arn\": 213,\n        \"sea\": 200,\n        \"atl\": 137,\n        \"yul\": 97,\n        \"gig\": 358,\n        \"gru\": 377,\n        \"scl\": 572\n      },\n      {\n        \"timestamp\": \"2025-08-24T23:00:00.000Z\",\n        \"yul\": 97,\n        \"atl\": 127,\n        \"den\": 214,\n        \"ord\": 135,\n        \"hkg\": 334,\n        \"arn\": 245,\n        \"sea\": 175,\n        \"scl\": 362,\n        \"gig\": 389,\n        \"gru\": 210,\n        \"ewr\": 148,\n        \"bom\": 301,\n        \"sin\": 432,\n        \"syd\": 312,\n        \"mad\": 351,\n        \"mia\": 133,\n        \"nrt\": 284,\n        \"bog\": 424,\n        \"eze\": 275,\n        \"otp\": 282,\n        \"lax\": 200,\n        \"jnb\": 348,\n        \"ams\": 203,\n        \"iad\": 98,\n        \"phx\": 195,\n        \"sjc\": 130,\n        \"dfw\": 200,\n        \"gdl\": 620,\n        \"bos\": 104,\n        \"fra\": 159,\n        \"cdg\": 404,\n        \"yyz\": 145,\n        \"lhr\": 166\n      },\n      {\n        \"timestamp\": \"2025-08-25T00:00:00.000Z\",\n        \"lhr\": 146,\n        \"yyz\": 129,\n        \"cdg\": 156,\n        \"fra\": 213,\n        \"bos\": 114,\n        \"gdl\": 498,\n        \"dfw\": 232,\n        \"sjc\": 120,\n        \"phx\": 226,\n        \"iad\": 139,\n        \"ams\": 193,\n        \"jnb\": 373,\n        \"lax\": 155,\n        \"eze\": 312,\n        \"otp\": 221,\n        \"bog\": 511,\n        \"nrt\": 319,\n        \"mia\": 124,\n        \"mad\": 331,\n        \"syd\": 272,\n        \"sin\": 432,\n        \"ewr\": 143,\n        \"bom\": 266,\n        \"gru\": 187,\n        \"gig\": 395,\n        \"scl\": 317,\n        \"sea\": 226,\n        \"arn\": 193,\n        \"hkg\": 403,\n        \"ord\": 127,\n        \"den\": 232,\n        \"yul\": 98,\n        \"atl\": 165\n      },\n      {\n        \"timestamp\": \"2025-08-25T01:00:00.000Z\",\n        \"scl\": 540,\n        \"gru\": 189,\n        \"gig\": 203,\n        \"atl\": 126,\n        \"yul\": 104,\n        \"sea\": 169,\n        \"arn\": 208,\n        \"hkg\": 343,\n        \"ord\": 128,\n        \"den\": 216,\n        \"mad\": 330,\n        \"syd\": 303,\n        \"nrt\": 271,\n        \"mia\": 193,\n        \"bom\": 279,\n        \"ewr\": 190,\n        \"sin\": 767,\n        \"iad\": 338,\n        \"ams\": 207,\n        \"jnb\": 383,\n        \"gdl\": 580,\n        \"dfw\": 274,\n        \"sjc\": 108,\n        \"phx\": 199,\n        \"lax\": 173,\n        \"otp\": 320,\n        \"eze\": 304,\n        \"bog\": 375,\n        \"yyz\": 224,\n        \"lhr\": 156,\n        \"bos\": 400,\n        \"cdg\": 172,\n        \"fra\": 145\n      },\n      {\n        \"timestamp\": \"2025-08-25T02:00:00.000Z\",\n        \"gru\": 231,\n        \"gig\": 383,\n        \"scl\": 353,\n        \"arn\": 208,\n        \"sea\": 184,\n        \"den\": 566,\n        \"ord\": 90,\n        \"hkg\": 377,\n        \"atl\": 205,\n        \"yul\": 96,\n        \"nrt\": 279,\n        \"mia\": 156,\n        \"mad\": 352,\n        \"syd\": 312,\n        \"sin\": 684,\n        \"bom\": 317,\n        \"ewr\": 78,\n        \"dfw\": 232,\n        \"gdl\": 461,\n        \"phx\": 220,\n        \"sjc\": 125,\n        \"ams\": 222,\n        \"iad\": 135,\n        \"jnb\": 354,\n        \"otp\": 240,\n        \"eze\": 275,\n        \"lax\": 165,\n        \"bog\": 494,\n        \"lhr\": 166,\n        \"yyz\": 137,\n        \"fra\": 143,\n        \"cdg\": 156,\n        \"bos\": 114\n      },\n      {\n        \"timestamp\": \"2025-08-25T03:00:00.000Z\",\n        \"yyz\": 124,\n        \"lhr\": 166,\n        \"bos\": 100,\n        \"fra\": 187,\n        \"cdg\": 202,\n        \"ams\": 204,\n        \"iad\": 199,\n        \"jnb\": 415,\n        \"dfw\": 192,\n        \"gdl\": 634,\n        \"phx\": 182,\n        \"sjc\": 134,\n        \"eze\": 321,\n        \"otp\": 275,\n        \"lax\": 169,\n        \"bog\": 378,\n        \"mad\": 353,\n        \"syd\": 332,\n        \"nrt\": 277,\n        \"mia\": 128,\n        \"ewr\": 79,\n        \"bom\": 305,\n        \"sin\": 354,\n        \"scl\": 368,\n        \"gru\": 296,\n        \"gig\": 243,\n        \"yul\": 96,\n        \"atl\": 278,\n        \"arn\": 226,\n        \"sea\": 192,\n        \"den\": 221,\n        \"ord\": 158,\n        \"hkg\": 263\n      },\n      {\n        \"timestamp\": \"2025-08-25T04:00:00.000Z\",\n        \"ams\": 220,\n        \"iad\": 180,\n        \"jnb\": 573,\n        \"dfw\": 214,\n        \"gdl\": 592,\n        \"phx\": 198,\n        \"sjc\": 157,\n        \"otp\": 317,\n        \"eze\": 267,\n        \"lax\": 134,\n        \"bog\": 307,\n        \"yyz\": 120,\n        \"lhr\": 157,\n        \"bos\": 107,\n        \"fra\": 161,\n        \"cdg\": 148,\n        \"scl\": 345,\n        \"gru\": 178,\n        \"gig\": 325,\n        \"atl\": 148,\n        \"yul\": 96,\n        \"arn\": 244,\n        \"sea\": 173,\n        \"ord\": 150,\n        \"den\": 235,\n        \"hkg\": 285,\n        \"mad\": 333,\n        \"syd\": 266,\n        \"nrt\": 311,\n        \"mia\": 198,\n        \"bom\": 294,\n        \"ewr\": 122,\n        \"sin\": 839\n      },\n      {\n        \"timestamp\": \"2025-08-25T05:00:00.000Z\",\n        \"ewr\": 116,\n        \"bom\": 602,\n        \"sin\": 702,\n        \"mad\": 330,\n        \"syd\": 344,\n        \"nrt\": 215,\n        \"mia\": 109,\n        \"yul\": 97,\n        \"atl\": 139,\n        \"arn\": 194,\n        \"sea\": 187,\n        \"den\": 269,\n        \"ord\": 127,\n        \"hkg\": 332,\n        \"scl\": 326,\n        \"gru\": 179,\n        \"gig\": 228,\n        \"bos\": 93,\n        \"fra\": 153,\n        \"cdg\": 148,\n        \"yyz\": 150,\n        \"lhr\": 147,\n        \"eze\": 309,\n        \"otp\": 252,\n        \"lax\": 185,\n        \"bog\": 355,\n        \"ams\": 368,\n        \"iad\": 173,\n        \"jnb\": 366,\n        \"dfw\": 204,\n        \"gdl\": 472,\n        \"phx\": 181,\n        \"sjc\": 121\n      },\n      {\n        \"timestamp\": \"2025-08-25T06:00:00.000Z\",\n        \"cdg\": 179,\n        \"fra\": 155,\n        \"bos\": 145,\n        \"lhr\": 138,\n        \"yyz\": 154,\n        \"bog\": 500,\n        \"lax\": 171,\n        \"eze\": 336,\n        \"otp\": 249,\n        \"sjc\": 123,\n        \"phx\": 181,\n        \"gdl\": 615,\n        \"dfw\": 208,\n        \"jnb\": 368,\n        \"iad\": 172,\n        \"ams\": 198,\n        \"sin\": 425,\n        \"ewr\": 112,\n        \"bom\": 556,\n        \"mia\": 149,\n        \"nrt\": 213,\n        \"syd\": 309,\n        \"mad\": 334,\n        \"hkg\": 346,\n        \"den\": 205,\n        \"ord\": 129,\n        \"sea\": 175,\n        \"arn\": 209,\n        \"yul\": 95,\n        \"atl\": 113,\n        \"gig\": 440,\n        \"gru\": 207,\n        \"scl\": 549\n      },\n      {\n        \"timestamp\": \"2025-08-25T07:00:00.000Z\",\n        \"sin\": 429,\n        \"ewr\": 116,\n        \"bom\": 565,\n        \"mia\": 240,\n        \"nrt\": 267,\n        \"syd\": 259,\n        \"mad\": 339,\n        \"hkg\": 280,\n        \"ord\": 135,\n        \"den\": 243,\n        \"sea\": 192,\n        \"arn\": 328,\n        \"yul\": 111,\n        \"atl\": 128,\n        \"gig\": 243,\n        \"gru\": 320,\n        \"scl\": 479,\n        \"cdg\": 162,\n        \"fra\": 278,\n        \"bos\": 102,\n        \"lhr\": 163,\n        \"yyz\": 127,\n        \"bog\": 443,\n        \"lax\": 149,\n        \"eze\": 278,\n        \"otp\": 262,\n        \"sjc\": 117,\n        \"phx\": 196,\n        \"gdl\": 630,\n        \"dfw\": 181,\n        \"jnb\": 378,\n        \"iad\": 105,\n        \"ams\": 195\n      },\n      {\n        \"timestamp\": \"2025-08-25T08:00:00.000Z\",\n        \"bom\": 310,\n        \"ewr\": 124,\n        \"sin\": 727,\n        \"syd\": 300,\n        \"mad\": 324,\n        \"mia\": 149,\n        \"nrt\": 456,\n        \"atl\": 201,\n        \"yul\": 103,\n        \"den\": 228,\n        \"ord\": 123,\n        \"hkg\": 276,\n        \"arn\": 262,\n        \"sea\": 203,\n        \"scl\": 349,\n        \"gig\": 384,\n        \"gru\": 239,\n        \"bos\": 95,\n        \"fra\": 145,\n        \"cdg\": 469,\n        \"yyz\": 142,\n        \"lhr\": 189,\n        \"bog\": 361,\n        \"otp\": 238,\n        \"eze\": 227,\n        \"lax\": 143,\n        \"jnb\": 408,\n        \"ams\": 190,\n        \"iad\": 105,\n        \"phx\": 212,\n        \"sjc\": 123,\n        \"dfw\": 242,\n        \"gdl\": 339\n      },\n      {\n        \"timestamp\": \"2025-08-25T09:00:00.000Z\",\n        \"syd\": 355,\n        \"mad\": 346,\n        \"mia\": 135,\n        \"nrt\": 268,\n        \"bom\": 586,\n        \"ewr\": 178,\n        \"sin\": 756,\n        \"scl\": 354,\n        \"gig\": 228,\n        \"gru\": 210,\n        \"atl\": 129,\n        \"yul\": 98,\n        \"hkg\": 429,\n        \"den\": 261,\n        \"ord\": 298,\n        \"sea\": 172,\n        \"arn\": 371,\n        \"yyz\": 111,\n        \"lhr\": 158,\n        \"bos\": 136,\n        \"cdg\": 632,\n        \"fra\": 149,\n        \"jnb\": 362,\n        \"iad\": 195,\n        \"ams\": 191,\n        \"sjc\": 126,\n        \"phx\": 487,\n        \"gdl\": 611,\n        \"dfw\": 249,\n        \"bog\": 376,\n        \"lax\": 137,\n        \"otp\": 258,\n        \"eze\": 276\n      },\n      {\n        \"timestamp\": \"2025-08-25T10:00:00.000Z\",\n        \"lhr\": 150,\n        \"yyz\": 120,\n        \"fra\": 146,\n        \"cdg\": 307,\n        \"bos\": 185,\n        \"phx\": 182,\n        \"sjc\": 144,\n        \"dfw\": 234,\n        \"gdl\": 587,\n        \"jnb\": 383,\n        \"ams\": 235,\n        \"iad\": 199,\n        \"bog\": 355,\n        \"eze\": 289,\n        \"otp\": 272,\n        \"lax\": 161,\n        \"mia\": 139,\n        \"nrt\": 302,\n        \"syd\": 352,\n        \"mad\": 354,\n        \"sin\": 412,\n        \"ewr\": 56,\n        \"bom\": 296,\n        \"gig\": 346,\n        \"gru\": 181,\n        \"scl\": 530,\n        \"ord\": 169,\n        \"den\": 197,\n        \"hkg\": 392,\n        \"arn\": 352,\n        \"sea\": 196,\n        \"yul\": 104,\n        \"atl\": 131\n      },\n      {\n        \"timestamp\": \"2025-08-25T11:00:00.000Z\",\n        \"jnb\": 443,\n        \"ams\": 179,\n        \"iad\": 177,\n        \"phx\": 182,\n        \"sjc\": 117,\n        \"dfw\": 167,\n        \"gdl\": 511,\n        \"bog\": 414,\n        \"eze\": 277,\n        \"otp\": 286,\n        \"lax\": 135,\n        \"yyz\": 154,\n        \"lhr\": 149,\n        \"bos\": 140,\n        \"fra\": 186,\n        \"cdg\": 164,\n        \"scl\": 401,\n        \"gig\": 355,\n        \"gru\": 192,\n        \"yul\": 88,\n        \"atl\": 118,\n        \"ord\": 123,\n        \"den\": 307,\n        \"hkg\": 337,\n        \"arn\": 224,\n        \"sea\": 161,\n        \"syd\": 301,\n        \"mad\": 319,\n        \"mia\": 161,\n        \"nrt\": 259,\n        \"ewr\": 62,\n        \"bom\": 293,\n        \"sin\": 560\n      },\n      {\n        \"timestamp\": \"2025-08-25T12:00:00.000Z\",\n        \"gdl\": 679,\n        \"dfw\": 122,\n        \"sjc\": 124,\n        \"phx\": 204,\n        \"iad\": 206,\n        \"ams\": 187,\n        \"jnb\": 376,\n        \"lax\": 132,\n        \"eze\": 645,\n        \"otp\": 246,\n        \"bog\": 511,\n        \"lhr\": 156,\n        \"yyz\": 517,\n        \"cdg\": 171,\n        \"fra\": 163,\n        \"bos\": 134,\n        \"gru\": 522,\n        \"gig\": 493,\n        \"scl\": 763,\n        \"sea\": 166,\n        \"arn\": 201,\n        \"hkg\": 273,\n        \"ord\": 139,\n        \"den\": 242,\n        \"yul\": 113,\n        \"atl\": 135,\n        \"nrt\": 301,\n        \"mia\": 141,\n        \"mad\": 337,\n        \"syd\": 304,\n        \"sin\": 411,\n        \"ewr\": 185,\n        \"bom\": 319\n      },\n      {\n        \"timestamp\": \"2025-08-25T13:00:00.000Z\",\n        \"gig\": 492,\n        \"gru\": 208,\n        \"scl\": 390,\n        \"den\": 244,\n        \"ord\": 127,\n        \"hkg\": 278,\n        \"arn\": 383,\n        \"sea\": 182,\n        \"yul\": 109,\n        \"atl\": 93,\n        \"mia\": 163,\n        \"nrt\": 265,\n        \"syd\": 343,\n        \"mad\": 345,\n        \"sin\": 584,\n        \"ewr\": 121,\n        \"bom\": 302,\n        \"phx\": 187,\n        \"sjc\": 123,\n        \"dfw\": 227,\n        \"gdl\": 496,\n        \"jnb\": 358,\n        \"ams\": 205,\n        \"iad\": 218,\n        \"bog\": 323,\n        \"eze\": 453,\n        \"otp\": 359,\n        \"lax\": 215,\n        \"lhr\": 159,\n        \"yyz\": 138,\n        \"fra\": 342,\n        \"cdg\": 174,\n        \"bos\": 132\n      },\n      {\n        \"timestamp\": \"2025-08-25T14:00:00.000Z\",\n        \"bog\": 509,\n        \"lax\": 158,\n        \"otp\": 310,\n        \"eze\": 615,\n        \"sjc\": 162,\n        \"phx\": 198,\n        \"gdl\": 478,\n        \"dfw\": 237,\n        \"jnb\": 419,\n        \"iad\": 135,\n        \"ams\": 201,\n        \"cdg\": 313,\n        \"fra\": 153,\n        \"bos\": 111,\n        \"lhr\": 150,\n        \"yyz\": 135,\n        \"hkg\": 338,\n        \"den\": 267,\n        \"ord\": 150,\n        \"sea\": 207,\n        \"arn\": 219,\n        \"atl\": 235,\n        \"yul\": 146,\n        \"gig\": 387,\n        \"gru\": 209,\n        \"scl\": 342,\n        \"sin\": 502,\n        \"bom\": 284,\n        \"ewr\": 125,\n        \"mia\": 191,\n        \"nrt\": 261,\n        \"syd\": 298,\n        \"mad\": 357\n      },\n      {\n        \"timestamp\": \"2025-08-25T15:00:00.000Z\",\n        \"bom\": 309,\n        \"ewr\": 113,\n        \"sin\": 467,\n        \"mad\": 327,\n        \"syd\": 309,\n        \"nrt\": 214,\n        \"mia\": 157,\n        \"atl\": 217,\n        \"yul\": 118,\n        \"sea\": 178,\n        \"arn\": 222,\n        \"hkg\": 330,\n        \"ord\": 149,\n        \"den\": 229,\n        \"scl\": 285,\n        \"gru\": 255,\n        \"gig\": 375,\n        \"bos\": 109,\n        \"cdg\": 157,\n        \"fra\": 148,\n        \"yyz\": 157,\n        \"lhr\": 145,\n        \"lax\": 158,\n        \"otp\": 254,\n        \"eze\": 229,\n        \"bog\": 310,\n        \"iad\": 146,\n        \"ams\": 216,\n        \"jnb\": 410,\n        \"gdl\": 577,\n        \"dfw\": 461,\n        \"sjc\": 125,\n        \"phx\": 208\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"mad\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 340,\n      \"p75Latency\": 353,\n      \"p90Latency\": 372,\n      \"p95Latency\": 440,\n      \"p99Latency\": 683\n    },\n    {\n      \"region\": \"bos\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 111,\n      \"p75Latency\": 131,\n      \"p90Latency\": 150,\n      \"p95Latency\": 186,\n      \"p99Latency\": 401\n    },\n    {\n      \"region\": \"sin\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 444,\n      \"p75Latency\": 574,\n      \"p90Latency\": 712,\n      \"p95Latency\": 794,\n      \"p99Latency\": 958\n    },\n    {\n      \"region\": \"lhr\",\n      \"count\": 169,\n      \"ok\": 169,\n      \"p50Latency\": 159,\n      \"p75Latency\": 170,\n      \"p90Latency\": 243,\n      \"p95Latency\": 307,\n      \"p99Latency\": 431\n    },\n    {\n      \"region\": \"yul\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 108,\n      \"p75Latency\": 119,\n      \"p90Latency\": 131,\n      \"p95Latency\": 140,\n      \"p99Latency\": 182\n    },\n    {\n      \"region\": \"dfw\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 199,\n      \"p75Latency\": 227,\n      \"p90Latency\": 268,\n      \"p95Latency\": 401,\n      \"p99Latency\": 512\n    },\n    {\n      \"region\": \"phx\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 192,\n      \"p75Latency\": 204,\n      \"p90Latency\": 224,\n      \"p95Latency\": 239,\n      \"p99Latency\": 401\n    },\n    {\n      \"region\": \"gig\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 250,\n      \"p75Latency\": 376,\n      \"p90Latency\": 398,\n      \"p95Latency\": 468,\n      \"p99Latency\": 548\n    },\n    {\n      \"region\": \"bog\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 416,\n      \"p75Latency\": 488,\n      \"p90Latency\": 520,\n      \"p95Latency\": 571,\n      \"p99Latency\": 732\n    },\n    {\n      \"region\": \"sea\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 190,\n      \"p75Latency\": 202,\n      \"p90Latency\": 217,\n      \"p95Latency\": 232,\n      \"p99Latency\": 364\n    },\n    {\n      \"region\": \"syd\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 306,\n      \"p75Latency\": 318,\n      \"p90Latency\": 347,\n      \"p95Latency\": 365,\n      \"p99Latency\": 472\n    },\n    {\n      \"region\": \"gru\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 196,\n      \"p75Latency\": 220,\n      \"p90Latency\": 326,\n      \"p95Latency\": 376,\n      \"p99Latency\": 483\n    },\n    {\n      \"region\": \"scl\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 344,\n      \"p75Latency\": 392,\n      \"p90Latency\": 517,\n      \"p95Latency\": 541,\n      \"p99Latency\": 595\n    },\n    {\n      \"region\": \"ewr\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 129,\n      \"p75Latency\": 181,\n      \"p90Latency\": 200,\n      \"p95Latency\": 246,\n      \"p99Latency\": 370\n    },\n    {\n      \"region\": \"mia\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 161,\n      \"p75Latency\": 187,\n      \"p90Latency\": 221,\n      \"p95Latency\": 238,\n      \"p99Latency\": 337\n    },\n    {\n      \"region\": \"iad\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 136,\n      \"p75Latency\": 183,\n      \"p90Latency\": 233,\n      \"p95Latency\": 374,\n      \"p99Latency\": 507\n    },\n    {\n      \"region\": \"ams\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 200,\n      \"p75Latency\": 212,\n      \"p90Latency\": 233,\n      \"p95Latency\": 249,\n      \"p99Latency\": 310\n    },\n    {\n      \"region\": \"arn\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 222,\n      \"p75Latency\": 246,\n      \"p90Latency\": 352,\n      \"p95Latency\": 396,\n      \"p99Latency\": 550\n    },\n    {\n      \"region\": \"atl\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 154,\n      \"p75Latency\": 178,\n      \"p90Latency\": 210,\n      \"p95Latency\": 236,\n      \"p99Latency\": 376\n    },\n    {\n      \"region\": \"yyz\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 136,\n      \"p75Latency\": 150,\n      \"p90Latency\": 175,\n      \"p95Latency\": 230,\n      \"p99Latency\": 431\n    },\n    {\n      \"region\": \"sjc\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 128,\n      \"p75Latency\": 139,\n      \"p90Latency\": 156,\n      \"p95Latency\": 181,\n      \"p99Latency\": 316\n    },\n    {\n      \"region\": \"den\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 246,\n      \"p75Latency\": 270,\n      \"p90Latency\": 304,\n      \"p95Latency\": 458,\n      \"p99Latency\": 552\n    },\n    {\n      \"region\": \"nrt\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 270,\n      \"p75Latency\": 320,\n      \"p90Latency\": 457,\n      \"p95Latency\": 498,\n      \"p99Latency\": 692\n    },\n    {\n      \"region\": \"cdg\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 169,\n      \"p75Latency\": 277,\n      \"p90Latency\": 343,\n      \"p95Latency\": 436,\n      \"p99Latency\": 578\n    },\n    {\n      \"region\": \"lax\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 152,\n      \"p75Latency\": 165,\n      \"p90Latency\": 179,\n      \"p95Latency\": 211,\n      \"p99Latency\": 293\n    },\n    {\n      \"region\": \"ord\",\n      \"count\": 167,\n      \"ok\": 167,\n      \"p50Latency\": 144,\n      \"p75Latency\": 181,\n      \"p90Latency\": 195,\n      \"p95Latency\": 212,\n      \"p99Latency\": 331\n    },\n    {\n      \"region\": \"gdl\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 582,\n      \"p75Latency\": 619,\n      \"p90Latency\": 664,\n      \"p95Latency\": 739,\n      \"p99Latency\": 852\n    },\n    {\n      \"region\": \"bom\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 308,\n      \"p75Latency\": 328,\n      \"p90Latency\": 560,\n      \"p95Latency\": 589,\n      \"p99Latency\": 650\n    },\n    {\n      \"region\": \"eze\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 308,\n      \"p75Latency\": 415,\n      \"p90Latency\": 497,\n      \"p95Latency\": 535,\n      \"p99Latency\": 649\n    },\n    {\n      \"region\": \"hkg\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 336,\n      \"p75Latency\": 357,\n      \"p90Latency\": 429,\n      \"p95Latency\": 535,\n      \"p99Latency\": 679\n    },\n    {\n      \"region\": \"fra\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 153,\n      \"p75Latency\": 163,\n      \"p90Latency\": 216,\n      \"p95Latency\": 338,\n      \"p99Latency\": 408\n    },\n    {\n      \"region\": \"otp\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 273,\n      \"p75Latency\": 288,\n      \"p90Latency\": 315,\n      \"p95Latency\": 325,\n      \"p99Latency\": 368\n    },\n    {\n      \"region\": \"jnb\",\n      \"count\": 168,\n      \"ok\": 168,\n      \"p50Latency\": 374,\n      \"p75Latency\": 391,\n      \"p90Latency\": 414,\n      \"p95Latency\": 483,\n      \"p99Latency\": 636\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/hono-vercel-fluid-compute/hono-warm.json",
    "content": "{\n  \"regions\": [\n    \"ams\",\n    \"arn\",\n    \"atl\",\n    \"bog\",\n    \"bom\",\n    \"bos\",\n    \"cdg\",\n    \"den\",\n    \"dfw\",\n    \"ewr\",\n    \"eze\",\n    \"fra\",\n    \"gdl\",\n    \"gig\",\n    \"gru\",\n    \"hkg\",\n    \"iad\",\n    \"jnb\",\n    \"lax\",\n    \"lhr\",\n    \"mad\",\n    \"mia\",\n    \"nrt\",\n    \"ord\",\n    \"otp\",\n    \"phx\",\n    \"scl\",\n    \"sjc\",\n    \"sea\",\n    \"sin\",\n    \"syd\",\n    \"yul\",\n    \"yyz\"\n  ],\n  \"data\": {\n    \"regions\": [\n      \"ams\",\n      \"arn\",\n      \"atl\",\n      \"bog\",\n      \"bom\",\n      \"bos\",\n      \"cdg\",\n      \"den\",\n      \"dfw\",\n      \"ewr\",\n      \"eze\",\n      \"fra\",\n      \"gdl\",\n      \"gig\",\n      \"gru\",\n      \"hkg\",\n      \"iad\",\n      \"jnb\",\n      \"lax\",\n      \"lhr\",\n      \"mad\",\n      \"mia\",\n      \"nrt\",\n      \"ord\",\n      \"otp\",\n      \"phx\",\n      \"scl\",\n      \"sea\",\n      \"sin\",\n      \"sjc\",\n      \"syd\",\n      \"yul\",\n      \"yyz\"\n    ],\n    \"data\": [\n      {\n        \"timestamp\": \"2025-08-18T15:00:00.000Z\",\n        \"ewr\": 134,\n        \"bom\": 340,\n        \"scl\": 508,\n        \"hkg\": 386,\n        \"eze\": 450,\n        \"mad\": 328,\n        \"sin\": 503,\n        \"arn\": 197,\n        \"bos\": 136,\n        \"iad\": 132,\n        \"mia\": 234,\n        \"nrt\": 288,\n        \"gig\": 347,\n        \"atl\": 144,\n        \"cdg\": 255,\n        \"sjc\": 136,\n        \"dfw\": 176,\n        \"ams\": 197,\n        \"otp\": 276,\n        \"lax\": 320,\n        \"syd\": 428,\n        \"gru\": 263,\n        \"den\": 293,\n        \"ord\": 175,\n        \"sea\": 155,\n        \"yul\": 131,\n        \"lhr\": 145,\n        \"yyz\": 228,\n        \"fra\": 218,\n        \"phx\": 190,\n        \"gdl\": 502,\n        \"jnb\": 610,\n        \"bog\": 417\n      },\n      {\n        \"timestamp\": \"2025-08-18T15:30:00.000Z\",\n        \"bos\": 113,\n        \"iad\": 195,\n        \"mad\": 353,\n        \"sin\": 415,\n        \"arn\": 202,\n        \"eze\": 456,\n        \"ewr\": 140,\n        \"bom\": 296,\n        \"scl\": 491,\n        \"hkg\": 385,\n        \"yyz\": 167,\n        \"lhr\": 206,\n        \"fra\": 170,\n        \"jnb\": 494,\n        \"phx\": 213,\n        \"gdl\": 587,\n        \"bog\": 423,\n        \"syd\": 340,\n        \"gru\": 278,\n        \"yul\": 116,\n        \"ord\": 140,\n        \"den\": 233,\n        \"sea\": 165,\n        \"cdg\": 160,\n        \"ams\": 183,\n        \"sjc\": 205,\n        \"dfw\": 259,\n        \"lax\": 159,\n        \"otp\": 283,\n        \"mia\": 265,\n        \"nrt\": 430,\n        \"gig\": 348,\n        \"atl\": 146\n      },\n      {\n        \"timestamp\": \"2025-08-18T16:00:00.000Z\",\n        \"cdg\": 169,\n        \"ams\": 193,\n        \"dfw\": 174,\n        \"sjc\": 149,\n        \"lax\": 166,\n        \"otp\": 302,\n        \"nrt\": 357,\n        \"mia\": 168,\n        \"gig\": 351,\n        \"atl\": 160,\n        \"yyz\": 147,\n        \"lhr\": 335,\n        \"fra\": 172,\n        \"jnb\": 514,\n        \"gdl\": 644,\n        \"phx\": 196,\n        \"bog\": 395,\n        \"syd\": 420,\n        \"gru\": 231,\n        \"yul\": 111,\n        \"sea\": 180,\n        \"den\": 232,\n        \"ord\": 170,\n        \"eze\": 423,\n        \"bom\": 302,\n        \"ewr\": 144,\n        \"scl\": 498,\n        \"hkg\": 455,\n        \"bos\": 124,\n        \"iad\": 140,\n        \"mad\": 358,\n        \"sin\": 413,\n        \"arn\": 199\n      },\n      {\n        \"timestamp\": \"2025-08-18T16:30:00.000Z\",\n        \"hkg\": 337,\n        \"scl\": 382,\n        \"bom\": 292,\n        \"ewr\": 173,\n        \"eze\": 458,\n        \"arn\": 209,\n        \"sin\": 488,\n        \"mad\": 414,\n        \"iad\": 135,\n        \"bos\": 118,\n        \"atl\": 120,\n        \"gig\": 350,\n        \"nrt\": 461,\n        \"mia\": 211,\n        \"otp\": 350,\n        \"lax\": 183,\n        \"dfw\": 180,\n        \"sjc\": 175,\n        \"ams\": 212,\n        \"cdg\": 149,\n        \"sea\": 188,\n        \"den\": 307,\n        \"ord\": 186,\n        \"yul\": 122,\n        \"gru\": 261,\n        \"syd\": 428,\n        \"bog\": 445,\n        \"gdl\": 601,\n        \"phx\": 197,\n        \"jnb\": 384,\n        \"fra\": 162,\n        \"lhr\": 156,\n        \"yyz\": 327\n      },\n      {\n        \"timestamp\": \"2025-08-18T17:00:00.000Z\",\n        \"gig\": 300,\n        \"atl\": 149,\n        \"nrt\": 349,\n        \"mia\": 186,\n        \"dfw\": 194,\n        \"sjc\": 139,\n        \"ams\": 181,\n        \"lax\": 162,\n        \"otp\": 279,\n        \"cdg\": 147,\n        \"gru\": 328,\n        \"sea\": 172,\n        \"den\": 233,\n        \"ord\": 178,\n        \"yul\": 188,\n        \"syd\": 297,\n        \"gdl\": 538,\n        \"phx\": 195,\n        \"jnb\": 501,\n        \"bog\": 396,\n        \"lhr\": 142,\n        \"yyz\": 153,\n        \"fra\": 183,\n        \"scl\": 522,\n        \"hkg\": 330,\n        \"ewr\": 158,\n        \"bom\": 285,\n        \"eze\": 386,\n        \"arn\": 217,\n        \"mad\": 366,\n        \"sin\": 470,\n        \"iad\": 138,\n        \"bos\": 120\n      },\n      {\n        \"timestamp\": \"2025-08-18T17:30:00.000Z\",\n        \"bos\": 212,\n        \"iad\": 128,\n        \"mad\": 329,\n        \"sin\": 505,\n        \"arn\": 212,\n        \"eze\": 454,\n        \"bom\": 321,\n        \"ewr\": 166,\n        \"scl\": 493,\n        \"hkg\": 343,\n        \"lhr\": 156,\n        \"yyz\": 159,\n        \"fra\": 155,\n        \"phx\": 204,\n        \"gdl\": 521,\n        \"jnb\": 497,\n        \"bog\": 423,\n        \"syd\": 373,\n        \"gru\": 208,\n        \"den\": 256,\n        \"ord\": 172,\n        \"sea\": 182,\n        \"yul\": 124,\n        \"cdg\": 154,\n        \"sjc\": 151,\n        \"dfw\": 189,\n        \"ams\": 180,\n        \"otp\": 309,\n        \"lax\": 175,\n        \"mia\": 173,\n        \"nrt\": 368,\n        \"gig\": 360,\n        \"atl\": 141\n      },\n      {\n        \"timestamp\": \"2025-08-18T18:00:00.000Z\",\n        \"otp\": 282,\n        \"lax\": 319,\n        \"sjc\": 129,\n        \"dfw\": 211,\n        \"ams\": 213,\n        \"cdg\": 156,\n        \"atl\": 145,\n        \"gig\": 361,\n        \"mia\": 184,\n        \"nrt\": 486,\n        \"bog\": 501,\n        \"phx\": 224,\n        \"gdl\": 528,\n        \"jnb\": 519,\n        \"fra\": 257,\n        \"lhr\": 225,\n        \"yyz\": 177,\n        \"ord\": 146,\n        \"den\": 232,\n        \"sea\": 187,\n        \"yul\": 110,\n        \"gru\": 302,\n        \"syd\": 333,\n        \"eze\": 456,\n        \"hkg\": 345,\n        \"scl\": 522,\n        \"ewr\": 172,\n        \"bom\": 282,\n        \"iad\": 175,\n        \"bos\": 127,\n        \"arn\": 212,\n        \"sin\": 485,\n        \"mad\": 411\n      },\n      {\n        \"timestamp\": \"2025-08-18T18:30:00.000Z\",\n        \"ewr\": 163,\n        \"bom\": 281,\n        \"scl\": 500,\n        \"hkg\": 343,\n        \"eze\": 479,\n        \"mad\": 346,\n        \"sin\": 487,\n        \"arn\": 258,\n        \"bos\": 139,\n        \"iad\": 131,\n        \"mia\": 162,\n        \"nrt\": 488,\n        \"gig\": 378,\n        \"atl\": 159,\n        \"cdg\": 156,\n        \"ams\": 178,\n        \"sjc\": 151,\n        \"dfw\": 242,\n        \"lax\": 170,\n        \"otp\": 306,\n        \"syd\": 374,\n        \"gru\": 345,\n        \"yul\": 138,\n        \"ord\": 194,\n        \"den\": 232,\n        \"sea\": 172,\n        \"yyz\": 139,\n        \"lhr\": 152,\n        \"fra\": 178,\n        \"jnb\": 478,\n        \"phx\": 193,\n        \"gdl\": 611,\n        \"bog\": 435\n      },\n      {\n        \"timestamp\": \"2025-08-18T19:00:00.000Z\",\n        \"mia\": 189,\n        \"nrt\": 360,\n        \"atl\": 132,\n        \"gig\": 357,\n        \"cdg\": 244,\n        \"otp\": 287,\n        \"lax\": 152,\n        \"ams\": 180,\n        \"sjc\": 166,\n        \"dfw\": 198,\n        \"syd\": 371,\n        \"yul\": 140,\n        \"ord\": 154,\n        \"den\": 243,\n        \"sea\": 178,\n        \"gru\": 213,\n        \"fra\": 180,\n        \"yyz\": 158,\n        \"lhr\": 169,\n        \"bog\": 471,\n        \"jnb\": 444,\n        \"phx\": 333,\n        \"gdl\": 590,\n        \"bom\": 283,\n        \"ewr\": 162,\n        \"hkg\": 350,\n        \"scl\": 616,\n        \"eze\": 485,\n        \"sin\": 416,\n        \"mad\": 341,\n        \"arn\": 221,\n        \"bos\": 110,\n        \"iad\": 155\n      },\n      {\n        \"timestamp\": \"2025-08-18T19:30:00.000Z\",\n        \"eze\": 490,\n        \"ewr\": 174,\n        \"bom\": 282,\n        \"scl\": 492,\n        \"hkg\": 356,\n        \"bos\": 137,\n        \"iad\": 160,\n        \"mad\": 384,\n        \"sin\": 458,\n        \"arn\": 203,\n        \"cdg\": 158,\n        \"ams\": 237,\n        \"dfw\": 187,\n        \"sjc\": 139,\n        \"otp\": 328,\n        \"lax\": 157,\n        \"nrt\": 451,\n        \"mia\": 272,\n        \"gig\": 373,\n        \"atl\": 171,\n        \"yyz\": 144,\n        \"lhr\": 167,\n        \"fra\": 173,\n        \"jnb\": 501,\n        \"gdl\": 535,\n        \"phx\": 195,\n        \"bog\": 405,\n        \"syd\": 424,\n        \"gru\": 335,\n        \"yul\": 178,\n        \"sea\": 184,\n        \"den\": 235,\n        \"ord\": 170\n      },\n      {\n        \"timestamp\": \"2025-08-18T20:00:00.000Z\",\n        \"eze\": 400,\n        \"scl\": 528,\n        \"hkg\": 337,\n        \"bom\": 282,\n        \"ewr\": 132,\n        \"iad\": 178,\n        \"bos\": 122,\n        \"arn\": 216,\n        \"mad\": 362,\n        \"sin\": 451,\n        \"dfw\": 174,\n        \"sjc\": 132,\n        \"ams\": 220,\n        \"otp\": 275,\n        \"lax\": 157,\n        \"cdg\": 178,\n        \"gig\": 354,\n        \"atl\": 158,\n        \"nrt\": 403,\n        \"mia\": 166,\n        \"gdl\": 571,\n        \"phx\": 193,\n        \"jnb\": 504,\n        \"bog\": 458,\n        \"lhr\": 172,\n        \"yyz\": 215,\n        \"fra\": 165,\n        \"gru\": 189,\n        \"sea\": 166,\n        \"ord\": 206,\n        \"den\": 218,\n        \"yul\": 126,\n        \"syd\": 439\n      },\n      {\n        \"timestamp\": \"2025-08-18T20:30:00.000Z\",\n        \"bos\": 161,\n        \"iad\": 140,\n        \"sin\": 442,\n        \"mad\": 346,\n        \"arn\": 193,\n        \"eze\": 456,\n        \"bom\": 316,\n        \"ewr\": 120,\n        \"hkg\": 334,\n        \"scl\": 495,\n        \"fra\": 248,\n        \"lhr\": 150,\n        \"yyz\": 141,\n        \"bog\": 453,\n        \"phx\": 196,\n        \"gdl\": 595,\n        \"jnb\": 432,\n        \"syd\": 378,\n        \"ord\": 158,\n        \"den\": 236,\n        \"sea\": 156,\n        \"yul\": 114,\n        \"gru\": 340,\n        \"cdg\": 149,\n        \"lax\": 176,\n        \"otp\": 272,\n        \"sjc\": 142,\n        \"dfw\": 184,\n        \"ams\": 217,\n        \"mia\": 221,\n        \"nrt\": 357,\n        \"atl\": 153,\n        \"gig\": 371\n      },\n      {\n        \"timestamp\": \"2025-08-18T21:00:00.000Z\",\n        \"ewr\": 193,\n        \"bom\": 265,\n        \"scl\": 503,\n        \"hkg\": 441,\n        \"eze\": 449,\n        \"mad\": 346,\n        \"sin\": 456,\n        \"arn\": 203,\n        \"bos\": 121,\n        \"iad\": 142,\n        \"nrt\": 415,\n        \"mia\": 179,\n        \"gig\": 349,\n        \"atl\": 155,\n        \"cdg\": 154,\n        \"ams\": 173,\n        \"dfw\": 206,\n        \"sjc\": 128,\n        \"otp\": 266,\n        \"lax\": 173,\n        \"syd\": 368,\n        \"gru\": 204,\n        \"yul\": 123,\n        \"sea\": 177,\n        \"ord\": 169,\n        \"den\": 335,\n        \"yyz\": 151,\n        \"lhr\": 168,\n        \"fra\": 180,\n        \"jnb\": 370,\n        \"gdl\": 587,\n        \"phx\": 187,\n        \"bog\": 459\n      },\n      {\n        \"timestamp\": \"2025-08-18T21:30:00.000Z\",\n        \"yul\": 112,\n        \"sea\": 170,\n        \"den\": 221,\n        \"ord\": 161,\n        \"gru\": 193,\n        \"syd\": 421,\n        \"bog\": 405,\n        \"jnb\": 426,\n        \"gdl\": 563,\n        \"phx\": 188,\n        \"fra\": 150,\n        \"yyz\": 136,\n        \"lhr\": 140,\n        \"atl\": 142,\n        \"gig\": 365,\n        \"nrt\": 401,\n        \"mia\": 183,\n        \"lax\": 235,\n        \"otp\": 279,\n        \"ams\": 178,\n        \"dfw\": 170,\n        \"sjc\": 132,\n        \"cdg\": 147,\n        \"arn\": 207,\n        \"sin\": 426,\n        \"mad\": 355,\n        \"iad\": 140,\n        \"bos\": 123,\n        \"hkg\": 340,\n        \"scl\": 499,\n        \"bom\": 305,\n        \"ewr\": 187,\n        \"eze\": 475\n      },\n      {\n        \"timestamp\": \"2025-08-18T22:00:00.000Z\",\n        \"eze\": 474,\n        \"ewr\": 138,\n        \"bom\": 271,\n        \"hkg\": 452,\n        \"scl\": 494,\n        \"bos\": 119,\n        \"iad\": 153,\n        \"sin\": 430,\n        \"mad\": 332,\n        \"arn\": 195,\n        \"cdg\": 194,\n        \"lax\": 209,\n        \"otp\": 278,\n        \"ams\": 227,\n        \"sjc\": 124,\n        \"dfw\": 169,\n        \"mia\": 150,\n        \"nrt\": 428,\n        \"atl\": 151,\n        \"gig\": 358,\n        \"fra\": 226,\n        \"yyz\": 145,\n        \"lhr\": 146,\n        \"bog\": 433,\n        \"jnb\": 456,\n        \"phx\": 222,\n        \"gdl\": 578,\n        \"syd\": 466,\n        \"yul\": 105,\n        \"den\": 255,\n        \"ord\": 166,\n        \"sea\": 162,\n        \"gru\": 192\n      },\n      {\n        \"timestamp\": \"2025-08-18T22:30:00.000Z\",\n        \"iad\": 158,\n        \"bos\": 129,\n        \"arn\": 192,\n        \"mad\": 346,\n        \"sin\": 414,\n        \"eze\": 459,\n        \"scl\": 492,\n        \"hkg\": 351,\n        \"ewr\": 136,\n        \"bom\": 280,\n        \"jnb\": 461,\n        \"gdl\": 566,\n        \"phx\": 193,\n        \"bog\": 405,\n        \"yyz\": 136,\n        \"lhr\": 179,\n        \"fra\": 149,\n        \"gru\": 205,\n        \"yul\": 130,\n        \"sea\": 174,\n        \"den\": 221,\n        \"ord\": 160,\n        \"syd\": 396,\n        \"ams\": 181,\n        \"dfw\": 179,\n        \"sjc\": 136,\n        \"otp\": 275,\n        \"lax\": 153,\n        \"cdg\": 145,\n        \"gig\": 295,\n        \"atl\": 179,\n        \"nrt\": 417,\n        \"mia\": 187\n      },\n      {\n        \"timestamp\": \"2025-08-18T23:00:00.000Z\",\n        \"hkg\": 359,\n        \"scl\": 352,\n        \"bom\": 283,\n        \"ewr\": 137,\n        \"eze\": 451,\n        \"arn\": 216,\n        \"sin\": 407,\n        \"mad\": 352,\n        \"iad\": 136,\n        \"bos\": 110,\n        \"atl\": 226,\n        \"gig\": 358,\n        \"mia\": 169,\n        \"nrt\": 414,\n        \"lax\": 163,\n        \"otp\": 294,\n        \"sjc\": 134,\n        \"dfw\": 229,\n        \"ams\": 178,\n        \"cdg\": 146,\n        \"den\": 225,\n        \"ord\": 197,\n        \"sea\": 173,\n        \"yul\": 128,\n        \"gru\": 214,\n        \"syd\": 458,\n        \"bog\": 415,\n        \"phx\": 240,\n        \"gdl\": 574,\n        \"jnb\": 456,\n        \"fra\": 160,\n        \"lhr\": 290,\n        \"yyz\": 160\n      },\n      {\n        \"timestamp\": \"2025-08-18T23:30:00.000Z\",\n        \"dfw\": 210,\n        \"sjc\": 128,\n        \"ams\": 172,\n        \"lax\": 194,\n        \"otp\": 302,\n        \"cdg\": 164,\n        \"gig\": 356,\n        \"atl\": 153,\n        \"nrt\": 432,\n        \"mia\": 170,\n        \"gdl\": 550,\n        \"phx\": 195,\n        \"jnb\": 453,\n        \"bog\": 474,\n        \"lhr\": 137,\n        \"yyz\": 179,\n        \"fra\": 158,\n        \"gru\": 283,\n        \"sea\": 166,\n        \"ord\": 175,\n        \"den\": 231,\n        \"yul\": 112,\n        \"syd\": 342,\n        \"eze\": 423,\n        \"scl\": 489,\n        \"hkg\": 312,\n        \"ewr\": 154,\n        \"bom\": 275,\n        \"iad\": 166,\n        \"bos\": 129,\n        \"arn\": 203,\n        \"mad\": 325,\n        \"sin\": 413\n      },\n      {\n        \"timestamp\": \"2025-08-19T00:00:00.000Z\",\n        \"mia\": 187,\n        \"nrt\": 425,\n        \"atl\": 206,\n        \"gig\": 358,\n        \"cdg\": 173,\n        \"otp\": 278,\n        \"lax\": 165,\n        \"ams\": 185,\n        \"sjc\": 131,\n        \"dfw\": 199,\n        \"syd\": 375,\n        \"yul\": 114,\n        \"den\": 255,\n        \"ord\": 142,\n        \"sea\": 167,\n        \"gru\": 219,\n        \"fra\": 160,\n        \"yyz\": 142,\n        \"lhr\": 140,\n        \"bog\": 491,\n        \"jnb\": 439,\n        \"phx\": 195,\n        \"gdl\": 581,\n        \"bom\": 277,\n        \"ewr\": 172,\n        \"hkg\": 317,\n        \"scl\": 505,\n        \"eze\": 298,\n        \"sin\": 439,\n        \"mad\": 336,\n        \"arn\": 205,\n        \"bos\": 131,\n        \"iad\": 149\n      },\n      {\n        \"timestamp\": \"2025-08-19T00:30:00.000Z\",\n        \"scl\": 437,\n        \"hkg\": 359,\n        \"bom\": 264,\n        \"ewr\": 128,\n        \"eze\": 383,\n        \"arn\": 188,\n        \"mad\": 397,\n        \"sin\": 562,\n        \"iad\": 132,\n        \"bos\": 198,\n        \"gig\": 353,\n        \"atl\": 240,\n        \"nrt\": 418,\n        \"mia\": 179,\n        \"dfw\": 202,\n        \"sjc\": 129,\n        \"ams\": 206,\n        \"lax\": 174,\n        \"otp\": 317,\n        \"cdg\": 147,\n        \"gru\": 327,\n        \"sea\": 166,\n        \"ord\": 146,\n        \"den\": 223,\n        \"yul\": 120,\n        \"syd\": 442,\n        \"gdl\": 554,\n        \"phx\": 190,\n        \"jnb\": 354,\n        \"bog\": 467,\n        \"lhr\": 161,\n        \"yyz\": 150,\n        \"fra\": 161\n      },\n      {\n        \"timestamp\": \"2025-08-19T01:00:00.000Z\",\n        \"atl\": 205,\n        \"gig\": 367,\n        \"nrt\": 398,\n        \"mia\": 181,\n        \"otp\": 277,\n        \"lax\": 177,\n        \"dfw\": 221,\n        \"sjc\": 131,\n        \"ams\": 186,\n        \"cdg\": 177,\n        \"sea\": 164,\n        \"ord\": 144,\n        \"den\": 244,\n        \"yul\": 147,\n        \"gru\": 202,\n        \"syd\": 437,\n        \"bog\": 491,\n        \"gdl\": 564,\n        \"phx\": 202,\n        \"jnb\": 510,\n        \"fra\": 153,\n        \"lhr\": 162,\n        \"yyz\": 145,\n        \"hkg\": 511,\n        \"scl\": 497,\n        \"ewr\": 180,\n        \"bom\": 287,\n        \"eze\": 458,\n        \"arn\": 188,\n        \"sin\": 530,\n        \"mad\": 365,\n        \"iad\": 271,\n        \"bos\": 118\n      },\n      {\n        \"timestamp\": \"2025-08-19T01:30:00.000Z\",\n        \"cdg\": 149,\n        \"ams\": 180,\n        \"sjc\": 126,\n        \"dfw\": 192,\n        \"lax\": 173,\n        \"otp\": 291,\n        \"mia\": 177,\n        \"nrt\": 351,\n        \"gig\": 355,\n        \"atl\": 196,\n        \"yyz\": 132,\n        \"lhr\": 144,\n        \"fra\": 155,\n        \"jnb\": 436,\n        \"phx\": 210,\n        \"gdl\": 483,\n        \"bog\": 447,\n        \"syd\": 417,\n        \"gru\": 198,\n        \"yul\": 118,\n        \"den\": 239,\n        \"ord\": 155,\n        \"sea\": 189,\n        \"eze\": 287,\n        \"bom\": 273,\n        \"ewr\": 123,\n        \"scl\": 528,\n        \"hkg\": 319,\n        \"bos\": 119,\n        \"iad\": 133,\n        \"mad\": 342,\n        \"sin\": 505,\n        \"arn\": 194\n      },\n      {\n        \"timestamp\": \"2025-08-19T02:00:00.000Z\",\n        \"iad\": 136,\n        \"bos\": 124,\n        \"arn\": 192,\n        \"sin\": 511,\n        \"mad\": 342,\n        \"eze\": 453,\n        \"hkg\": 323,\n        \"scl\": 525,\n        \"ewr\": 126,\n        \"bom\": 289,\n        \"bog\": 399,\n        \"jnb\": 357,\n        \"phx\": 205,\n        \"gdl\": 522,\n        \"fra\": 180,\n        \"yyz\": 136,\n        \"lhr\": 156,\n        \"yul\": 109,\n        \"ord\": 178,\n        \"den\": 244,\n        \"sea\": 229,\n        \"gru\": 289,\n        \"syd\": 416,\n        \"lax\": 194,\n        \"otp\": 270,\n        \"ams\": 217,\n        \"sjc\": 174,\n        \"dfw\": 201,\n        \"cdg\": 151,\n        \"atl\": 219,\n        \"gig\": 350,\n        \"mia\": 176,\n        \"nrt\": 426\n      },\n      {\n        \"timestamp\": \"2025-08-19T02:30:00.000Z\",\n        \"ewr\": 122,\n        \"bom\": 270,\n        \"hkg\": 356,\n        \"scl\": 445,\n        \"eze\": 451,\n        \"sin\": 411,\n        \"mad\": 336,\n        \"arn\": 187,\n        \"bos\": 113,\n        \"iad\": 118,\n        \"mia\": 223,\n        \"nrt\": 422,\n        \"atl\": 337,\n        \"gig\": 354,\n        \"cdg\": 150,\n        \"otp\": 274,\n        \"lax\": 152,\n        \"ams\": 200,\n        \"sjc\": 205,\n        \"dfw\": 182,\n        \"syd\": 353,\n        \"yul\": 124,\n        \"den\": 231,\n        \"ord\": 146,\n        \"sea\": 175,\n        \"gru\": 207,\n        \"fra\": 248,\n        \"yyz\": 178,\n        \"lhr\": 147,\n        \"bog\": 410,\n        \"jnb\": 352,\n        \"phx\": 199,\n        \"gdl\": 651\n      },\n      {\n        \"timestamp\": \"2025-08-19T03:00:00.000Z\",\n        \"mia\": 195,\n        \"nrt\": 421,\n        \"gig\": 361,\n        \"atl\": 216,\n        \"cdg\": 148,\n        \"ams\": 183,\n        \"sjc\": 144,\n        \"dfw\": 196,\n        \"lax\": 181,\n        \"otp\": 302,\n        \"syd\": 382,\n        \"gru\": 285,\n        \"yul\": 121,\n        \"den\": 296,\n        \"ord\": 146,\n        \"sea\": 169,\n        \"yyz\": 149,\n        \"lhr\": 137,\n        \"fra\": 163,\n        \"jnb\": 430,\n        \"phx\": 216,\n        \"gdl\": 650,\n        \"bog\": 392,\n        \"bom\": 288,\n        \"ewr\": 142,\n        \"scl\": 446,\n        \"hkg\": 390,\n        \"eze\": 445,\n        \"mad\": 348,\n        \"sin\": 454,\n        \"arn\": 201,\n        \"bos\": 116,\n        \"iad\": 119\n      },\n      {\n        \"timestamp\": \"2025-08-19T03:30:00.000Z\",\n        \"eze\": 450,\n        \"ewr\": 177,\n        \"bom\": 279,\n        \"hkg\": 481,\n        \"scl\": 457,\n        \"bos\": 124,\n        \"iad\": 128,\n        \"sin\": 486,\n        \"mad\": 390,\n        \"arn\": 240,\n        \"cdg\": 139,\n        \"lax\": 162,\n        \"otp\": 290,\n        \"ams\": 182,\n        \"dfw\": 179,\n        \"sjc\": 131,\n        \"nrt\": 326,\n        \"mia\": 182,\n        \"atl\": 323,\n        \"gig\": 344,\n        \"fra\": 160,\n        \"yyz\": 149,\n        \"lhr\": 148,\n        \"bog\": 407,\n        \"jnb\": 350,\n        \"gdl\": 616,\n        \"phx\": 249,\n        \"syd\": 333,\n        \"yul\": 118,\n        \"sea\": 156,\n        \"ord\": 203,\n        \"den\": 277,\n        \"gru\": 298\n      },\n      {\n        \"timestamp\": \"2025-08-19T04:00:00.000Z\",\n        \"dfw\": 163,\n        \"sjc\": 136,\n        \"ams\": 170,\n        \"lax\": 155,\n        \"otp\": 300,\n        \"cdg\": 144,\n        \"gig\": 374,\n        \"atl\": 662,\n        \"nrt\": 430,\n        \"mia\": 203,\n        \"gdl\": 509,\n        \"phx\": 218,\n        \"jnb\": 433,\n        \"bog\": 462,\n        \"lhr\": 147,\n        \"yyz\": 140,\n        \"fra\": 166,\n        \"gru\": 280,\n        \"sea\": 167,\n        \"ord\": 230,\n        \"den\": 279,\n        \"yul\": 129,\n        \"syd\": 378,\n        \"eze\": 450,\n        \"scl\": 435,\n        \"hkg\": 333,\n        \"ewr\": 128,\n        \"bom\": 344,\n        \"iad\": 184,\n        \"bos\": 116,\n        \"arn\": 187,\n        \"mad\": 341,\n        \"sin\": 419\n      },\n      {\n        \"timestamp\": \"2025-08-19T04:30:00.000Z\",\n        \"fra\": 167,\n        \"lhr\": 154,\n        \"yyz\": 147,\n        \"bog\": 421,\n        \"phx\": 197,\n        \"gdl\": 574,\n        \"jnb\": 495,\n        \"syd\": 417,\n        \"ord\": 169,\n        \"den\": 275,\n        \"sea\": 195,\n        \"yul\": 144,\n        \"gru\": 279,\n        \"cdg\": 141,\n        \"otp\": 358,\n        \"lax\": 164,\n        \"sjc\": 152,\n        \"dfw\": 175,\n        \"ams\": 178,\n        \"mia\": 194,\n        \"nrt\": 427,\n        \"atl\": 518,\n        \"gig\": 352,\n        \"bos\": 114,\n        \"iad\": 129,\n        \"sin\": 439,\n        \"mad\": 330,\n        \"arn\": 277,\n        \"eze\": 449,\n        \"ewr\": 176,\n        \"bom\": 322,\n        \"hkg\": 318,\n        \"scl\": 540\n      },\n      {\n        \"timestamp\": \"2025-08-19T05:00:00.000Z\",\n        \"gru\": 255,\n        \"sea\": 176,\n        \"den\": 314,\n        \"ord\": 169,\n        \"yul\": 136,\n        \"syd\": 365,\n        \"gdl\": 620,\n        \"phx\": 205,\n        \"jnb\": 428,\n        \"bog\": 384,\n        \"lhr\": 155,\n        \"yyz\": 163,\n        \"fra\": 179,\n        \"gig\": 354,\n        \"atl\": 613,\n        \"nrt\": 311,\n        \"mia\": 228,\n        \"dfw\": 187,\n        \"sjc\": 171,\n        \"ams\": 237,\n        \"lax\": 175,\n        \"otp\": 290,\n        \"cdg\": 160,\n        \"arn\": 240,\n        \"mad\": 351,\n        \"sin\": 423,\n        \"iad\": 185,\n        \"bos\": 126,\n        \"scl\": 502,\n        \"hkg\": 448,\n        \"ewr\": 136,\n        \"bom\": 319,\n        \"eze\": 381\n      },\n      {\n        \"timestamp\": \"2025-08-19T05:30:00.000Z\",\n        \"bom\": 293,\n        \"ewr\": 176,\n        \"hkg\": 318,\n        \"scl\": 490,\n        \"eze\": 448,\n        \"sin\": 448,\n        \"mad\": 348,\n        \"arn\": 202,\n        \"bos\": 129,\n        \"iad\": 151,\n        \"nrt\": 436,\n        \"mia\": 183,\n        \"atl\": 645,\n        \"gig\": 373,\n        \"cdg\": 154,\n        \"otp\": 303,\n        \"lax\": 187,\n        \"dfw\": 201,\n        \"sjc\": 130,\n        \"ams\": 178,\n        \"syd\": 446,\n        \"sea\": 187,\n        \"ord\": 139,\n        \"den\": 293,\n        \"yul\": 140,\n        \"gru\": 338,\n        \"fra\": 172,\n        \"lhr\": 148,\n        \"yyz\": 138,\n        \"bog\": 387,\n        \"gdl\": 576,\n        \"phx\": 206,\n        \"jnb\": 450\n      },\n      {\n        \"timestamp\": \"2025-08-19T06:00:00.000Z\",\n        \"eze\": 469,\n        \"scl\": 494,\n        \"hkg\": 322,\n        \"ewr\": 169,\n        \"bom\": 284,\n        \"iad\": 146,\n        \"bos\": 113,\n        \"arn\": 230,\n        \"mad\": 376,\n        \"sin\": 444,\n        \"ams\": 194,\n        \"sjc\": 147,\n        \"dfw\": 164,\n        \"otp\": 297,\n        \"lax\": 167,\n        \"cdg\": 171,\n        \"gig\": 357,\n        \"atl\": 372,\n        \"mia\": 207,\n        \"nrt\": 267,\n        \"jnb\": 503,\n        \"phx\": 207,\n        \"gdl\": 618,\n        \"bog\": 396,\n        \"yyz\": 152,\n        \"lhr\": 136,\n        \"fra\": 175,\n        \"gru\": 277,\n        \"yul\": 131,\n        \"den\": 300,\n        \"ord\": 174,\n        \"sea\": 176,\n        \"syd\": 294\n      },\n      {\n        \"timestamp\": \"2025-08-19T06:30:00.000Z\",\n        \"bos\": 115,\n        \"iad\": 140,\n        \"sin\": 437,\n        \"mad\": 346,\n        \"arn\": 205,\n        \"eze\": 457,\n        \"ewr\": 134,\n        \"bom\": 289,\n        \"hkg\": 489,\n        \"scl\": 490,\n        \"fra\": 172,\n        \"yyz\": 157,\n        \"lhr\": 187,\n        \"bog\": 390,\n        \"jnb\": 497,\n        \"gdl\": 581,\n        \"phx\": 350,\n        \"syd\": 443,\n        \"yul\": 128,\n        \"sea\": 163,\n        \"den\": 298,\n        \"ord\": 155,\n        \"gru\": 264,\n        \"cdg\": 147,\n        \"lax\": 163,\n        \"otp\": 275,\n        \"ams\": 247,\n        \"dfw\": 190,\n        \"sjc\": 146,\n        \"nrt\": 344,\n        \"mia\": 183,\n        \"atl\": 273,\n        \"gig\": 349\n      },\n      {\n        \"timestamp\": \"2025-08-19T07:00:00.000Z\",\n        \"syd\": 377,\n        \"yul\": 125,\n        \"ord\": 141,\n        \"den\": 297,\n        \"sea\": 169,\n        \"gru\": 302,\n        \"fra\": 254,\n        \"yyz\": 144,\n        \"lhr\": 200,\n        \"bog\": 698,\n        \"jnb\": 488,\n        \"phx\": 212,\n        \"gdl\": 618,\n        \"mia\": 203,\n        \"nrt\": 417,\n        \"atl\": 229,\n        \"gig\": 353,\n        \"cdg\": 163,\n        \"otp\": 312,\n        \"lax\": 160,\n        \"ams\": 179,\n        \"sjc\": 179,\n        \"dfw\": 196,\n        \"sin\": 659,\n        \"mad\": 365,\n        \"arn\": 240,\n        \"bos\": 125,\n        \"iad\": 140,\n        \"bom\": 298,\n        \"ewr\": 159,\n        \"hkg\": 358,\n        \"scl\": 493,\n        \"eze\": 383\n      },\n      {\n        \"timestamp\": \"2025-08-19T07:30:00.000Z\",\n        \"bos\": 117,\n        \"iad\": 140,\n        \"mad\": 357,\n        \"sin\": 441,\n        \"arn\": 268,\n        \"eze\": 388,\n        \"ewr\": 130,\n        \"bom\": 291,\n        \"scl\": 493,\n        \"hkg\": 324,\n        \"yyz\": 150,\n        \"lhr\": 155,\n        \"fra\": 154,\n        \"jnb\": 489,\n        \"gdl\": 618,\n        \"phx\": 282,\n        \"bog\": 674,\n        \"syd\": 416,\n        \"gru\": 328,\n        \"yul\": 125,\n        \"sea\": 170,\n        \"den\": 288,\n        \"ord\": 159,\n        \"cdg\": 272,\n        \"ams\": 182,\n        \"dfw\": 186,\n        \"sjc\": 151,\n        \"otp\": 280,\n        \"lax\": 150,\n        \"nrt\": 423,\n        \"mia\": 179,\n        \"gig\": 349,\n        \"atl\": 222\n      },\n      {\n        \"timestamp\": \"2025-08-19T08:00:00.000Z\",\n        \"cdg\": 221,\n        \"ams\": 180,\n        \"sjc\": 152,\n        \"dfw\": 209,\n        \"otp\": 313,\n        \"lax\": 204,\n        \"mia\": 168,\n        \"nrt\": 345,\n        \"gig\": 208,\n        \"atl\": 143,\n        \"yyz\": 192,\n        \"lhr\": 151,\n        \"fra\": 158,\n        \"jnb\": 524,\n        \"phx\": 260,\n        \"gdl\": 556,\n        \"bog\": 658,\n        \"syd\": 291,\n        \"gru\": 211,\n        \"yul\": 111,\n        \"ord\": 168,\n        \"den\": 285,\n        \"sea\": 165,\n        \"eze\": 446,\n        \"bom\": 281,\n        \"ewr\": 145,\n        \"scl\": 488,\n        \"hkg\": 363,\n        \"bos\": 109,\n        \"iad\": 152,\n        \"mad\": 403,\n        \"sin\": 430,\n        \"arn\": 201\n      },\n      {\n        \"timestamp\": \"2025-08-19T08:30:00.000Z\",\n        \"lhr\": 151,\n        \"yyz\": 153,\n        \"fra\": 263,\n        \"gdl\": 566,\n        \"phx\": 218,\n        \"jnb\": 521,\n        \"bog\": 451,\n        \"syd\": 372,\n        \"gru\": 267,\n        \"sea\": 165,\n        \"ord\": 147,\n        \"den\": 330,\n        \"yul\": 138,\n        \"cdg\": 271,\n        \"dfw\": 202,\n        \"sjc\": 150,\n        \"ams\": 516,\n        \"lax\": 153,\n        \"otp\": 276,\n        \"nrt\": 433,\n        \"mia\": 156,\n        \"gig\": 345,\n        \"atl\": 134,\n        \"bos\": 114,\n        \"iad\": 138,\n        \"mad\": 345,\n        \"sin\": 433,\n        \"arn\": 195,\n        \"eze\": 536,\n        \"bom\": 297,\n        \"ewr\": 155,\n        \"scl\": 534,\n        \"hkg\": 349\n      },\n      {\n        \"timestamp\": \"2025-08-19T09:00:00.000Z\",\n        \"gig\": 358,\n        \"atl\": 149,\n        \"mia\": 177,\n        \"nrt\": 422,\n        \"sjc\": 139,\n        \"dfw\": 195,\n        \"ams\": 180,\n        \"otp\": 290,\n        \"lax\": 155,\n        \"cdg\": 145,\n        \"gru\": 224,\n        \"ord\": 169,\n        \"den\": 298,\n        \"sea\": 186,\n        \"yul\": 121,\n        \"syd\": 389,\n        \"phx\": 206,\n        \"gdl\": 547,\n        \"jnb\": 499,\n        \"bog\": 455,\n        \"lhr\": 139,\n        \"yyz\": 141,\n        \"fra\": 174,\n        \"scl\": 486,\n        \"hkg\": 374,\n        \"ewr\": 136,\n        \"bom\": 284,\n        \"eze\": 459,\n        \"arn\": 204,\n        \"mad\": 350,\n        \"sin\": 523,\n        \"iad\": 149,\n        \"bos\": 111\n      },\n      {\n        \"timestamp\": \"2025-08-19T09:30:00.000Z\",\n        \"jnb\": 414,\n        \"phx\": 199,\n        \"gdl\": 511,\n        \"bog\": 455,\n        \"yyz\": 141,\n        \"lhr\": 210,\n        \"fra\": 150,\n        \"gru\": 277,\n        \"yul\": 105,\n        \"den\": 333,\n        \"ord\": 156,\n        \"sea\": 180,\n        \"syd\": 385,\n        \"ams\": 182,\n        \"sjc\": 163,\n        \"dfw\": 192,\n        \"lax\": 165,\n        \"otp\": 292,\n        \"cdg\": 163,\n        \"gig\": 358,\n        \"atl\": 136,\n        \"mia\": 160,\n        \"nrt\": 426,\n        \"iad\": 142,\n        \"bos\": 112,\n        \"arn\": 196,\n        \"mad\": 430,\n        \"sin\": 458,\n        \"eze\": 453,\n        \"scl\": 492,\n        \"hkg\": 476,\n        \"ewr\": 161,\n        \"bom\": 295\n      },\n      {\n        \"timestamp\": \"2025-08-19T10:00:00.000Z\",\n        \"arn\": 202,\n        \"mad\": 346,\n        \"sin\": 435,\n        \"iad\": 167,\n        \"bos\": 113,\n        \"scl\": 495,\n        \"hkg\": 453,\n        \"ewr\": 140,\n        \"bom\": 298,\n        \"eze\": 449,\n        \"gru\": 194,\n        \"yul\": 121,\n        \"den\": 290,\n        \"ord\": 173,\n        \"sea\": 194,\n        \"syd\": 352,\n        \"jnb\": 435,\n        \"phx\": 202,\n        \"gdl\": 610,\n        \"bog\": 491,\n        \"yyz\": 247,\n        \"lhr\": 166,\n        \"fra\": 174,\n        \"gig\": 385,\n        \"atl\": 125,\n        \"mia\": 173,\n        \"nrt\": 427,\n        \"ams\": 178,\n        \"sjc\": 134,\n        \"dfw\": 191,\n        \"lax\": 165,\n        \"otp\": 275,\n        \"cdg\": 163\n      },\n      {\n        \"timestamp\": \"2025-08-19T10:30:00.000Z\",\n        \"scl\": 493,\n        \"hkg\": 345,\n        \"ewr\": 135,\n        \"bom\": 291,\n        \"eze\": 456,\n        \"arn\": 209,\n        \"mad\": 335,\n        \"sin\": 454,\n        \"iad\": 141,\n        \"bos\": 123,\n        \"gig\": 353,\n        \"atl\": 130,\n        \"nrt\": 448,\n        \"mia\": 205,\n        \"dfw\": 181,\n        \"sjc\": 149,\n        \"ams\": 178,\n        \"otp\": 277,\n        \"lax\": 170,\n        \"cdg\": 141,\n        \"gru\": 225,\n        \"sea\": 177,\n        \"den\": 297,\n        \"ord\": 173,\n        \"yul\": 124,\n        \"syd\": 380,\n        \"gdl\": 582,\n        \"phx\": 204,\n        \"jnb\": 503,\n        \"bog\": 452,\n        \"lhr\": 142,\n        \"yyz\": 152,\n        \"fra\": 167\n      },\n      {\n        \"timestamp\": \"2025-08-19T11:00:00.000Z\",\n        \"bos\": 121,\n        \"iad\": 136,\n        \"mad\": 438,\n        \"sin\": 433,\n        \"arn\": 212,\n        \"eze\": 411,\n        \"bom\": 359,\n        \"ewr\": 146,\n        \"scl\": 375,\n        \"hkg\": 322,\n        \"lhr\": 169,\n        \"yyz\": 172,\n        \"fra\": 170,\n        \"phx\": 213,\n        \"gdl\": 635,\n        \"jnb\": 574,\n        \"bog\": 399,\n        \"syd\": 392,\n        \"gru\": 216,\n        \"den\": 289,\n        \"ord\": 139,\n        \"sea\": 169,\n        \"yul\": 117,\n        \"cdg\": 154,\n        \"sjc\": 139,\n        \"dfw\": 199,\n        \"ams\": 201,\n        \"lax\": 186,\n        \"otp\": 295,\n        \"mia\": 180,\n        \"nrt\": 358,\n        \"gig\": 348,\n        \"atl\": 124\n      },\n      {\n        \"timestamp\": \"2025-08-19T11:30:00.000Z\",\n        \"fra\": 158,\n        \"lhr\": 149,\n        \"yyz\": 211,\n        \"bog\": 474,\n        \"gdl\": 612,\n        \"phx\": 238,\n        \"jnb\": 511,\n        \"syd\": 325,\n        \"sea\": 167,\n        \"ord\": 168,\n        \"den\": 286,\n        \"yul\": 134,\n        \"gru\": 340,\n        \"cdg\": 182,\n        \"otp\": 370,\n        \"lax\": 155,\n        \"dfw\": 174,\n        \"sjc\": 213,\n        \"ams\": 197,\n        \"nrt\": 455,\n        \"mia\": 181,\n        \"atl\": 136,\n        \"gig\": 359,\n        \"bos\": 120,\n        \"iad\": 142,\n        \"sin\": 503,\n        \"mad\": 345,\n        \"arn\": 202,\n        \"eze\": 452,\n        \"bom\": 291,\n        \"ewr\": 126,\n        \"hkg\": 487,\n        \"scl\": 517\n      },\n      {\n        \"timestamp\": \"2025-08-19T12:00:00.000Z\",\n        \"atl\": 137,\n        \"gig\": 372,\n        \"nrt\": 415,\n        \"mia\": 156,\n        \"lax\": 216,\n        \"otp\": 281,\n        \"dfw\": 166,\n        \"sjc\": 155,\n        \"ams\": 196,\n        \"cdg\": 144,\n        \"sea\": 169,\n        \"den\": 286,\n        \"ord\": 147,\n        \"yul\": 129,\n        \"gru\": 279,\n        \"syd\": 418,\n        \"bog\": 404,\n        \"gdl\": 635,\n        \"phx\": 196,\n        \"jnb\": 512,\n        \"fra\": 286,\n        \"lhr\": 202,\n        \"yyz\": 141,\n        \"hkg\": 321,\n        \"scl\": 509,\n        \"ewr\": 168,\n        \"bom\": 306,\n        \"eze\": 482,\n        \"arn\": 197,\n        \"sin\": 412,\n        \"mad\": 391,\n        \"iad\": 148,\n        \"bos\": 116\n      },\n      {\n        \"timestamp\": \"2025-08-19T12:30:00.000Z\",\n        \"eze\": 397,\n        \"hkg\": 452,\n        \"scl\": 501,\n        \"ewr\": 166,\n        \"bom\": 295,\n        \"iad\": 141,\n        \"bos\": 115,\n        \"arn\": 214,\n        \"sin\": 430,\n        \"mad\": 345,\n        \"otp\": 284,\n        \"lax\": 159,\n        \"dfw\": 182,\n        \"sjc\": 155,\n        \"ams\": 173,\n        \"cdg\": 283,\n        \"atl\": 143,\n        \"gig\": 373,\n        \"nrt\": 370,\n        \"mia\": 166,\n        \"bog\": 442,\n        \"gdl\": 548,\n        \"phx\": 197,\n        \"jnb\": 441,\n        \"fra\": 165,\n        \"lhr\": 138,\n        \"yyz\": 137,\n        \"sea\": 207,\n        \"den\": 338,\n        \"ord\": 181,\n        \"yul\": 144,\n        \"gru\": 233,\n        \"syd\": 371\n      },\n      {\n        \"timestamp\": \"2025-08-19T13:00:00.000Z\",\n        \"cdg\": 247,\n        \"lax\": 156,\n        \"otp\": 268,\n        \"ams\": 281,\n        \"dfw\": 175,\n        \"sjc\": 134,\n        \"nrt\": 373,\n        \"mia\": 176,\n        \"atl\": 150,\n        \"gig\": 298,\n        \"fra\": 193,\n        \"yyz\": 241,\n        \"lhr\": 147,\n        \"bog\": 450,\n        \"jnb\": 444,\n        \"gdl\": 619,\n        \"phx\": 233,\n        \"syd\": 445,\n        \"yul\": 116,\n        \"sea\": 171,\n        \"den\": 236,\n        \"ord\": 178,\n        \"gru\": 303,\n        \"eze\": 457,\n        \"bom\": 295,\n        \"ewr\": 124,\n        \"hkg\": 410,\n        \"scl\": 501,\n        \"bos\": 116,\n        \"iad\": 151,\n        \"sin\": 414,\n        \"mad\": 354,\n        \"arn\": 204\n      },\n      {\n        \"timestamp\": \"2025-08-19T13:30:00.000Z\",\n        \"bom\": 291,\n        \"ewr\": 167,\n        \"hkg\": 396,\n        \"scl\": 518,\n        \"eze\": 469,\n        \"sin\": 562,\n        \"mad\": 351,\n        \"arn\": 196,\n        \"bos\": 122,\n        \"iad\": 142,\n        \"nrt\": 283,\n        \"mia\": 201,\n        \"atl\": 141,\n        \"gig\": 353,\n        \"cdg\": 186,\n        \"otp\": 313,\n        \"lax\": 171,\n        \"ams\": 189,\n        \"dfw\": 201,\n        \"sjc\": 127,\n        \"syd\": 292,\n        \"yul\": 119,\n        \"sea\": 213,\n        \"den\": 246,\n        \"ord\": 147,\n        \"gru\": 225,\n        \"fra\": 202,\n        \"yyz\": 146,\n        \"lhr\": 163,\n        \"bog\": 392,\n        \"jnb\": 363,\n        \"gdl\": 593,\n        \"phx\": 204\n      },\n      {\n        \"timestamp\": \"2025-08-19T14:00:00.000Z\",\n        \"syd\": 441,\n        \"gru\": 189,\n        \"ord\": 193,\n        \"den\": 266,\n        \"sea\": 183,\n        \"yul\": 173,\n        \"lhr\": 226,\n        \"yyz\": 146,\n        \"fra\": 156,\n        \"phx\": 207,\n        \"gdl\": 595,\n        \"jnb\": 463,\n        \"bog\": 408,\n        \"mia\": 171,\n        \"nrt\": 390,\n        \"gig\": 361,\n        \"atl\": 145,\n        \"cdg\": 149,\n        \"sjc\": 138,\n        \"dfw\": 182,\n        \"ams\": 205,\n        \"otp\": 278,\n        \"lax\": 294,\n        \"mad\": 336,\n        \"sin\": 440,\n        \"arn\": 198,\n        \"bos\": 152,\n        \"iad\": 194,\n        \"ewr\": 163,\n        \"bom\": 299,\n        \"scl\": 494,\n        \"hkg\": 365,\n        \"eze\": 456\n      },\n      {\n        \"timestamp\": \"2025-08-19T14:30:00.000Z\",\n        \"bos\": 130,\n        \"iad\": 187,\n        \"mad\": 348,\n        \"sin\": 459,\n        \"arn\": 231,\n        \"eze\": 472,\n        \"ewr\": 195,\n        \"bom\": 317,\n        \"scl\": 496,\n        \"hkg\": 420,\n        \"lhr\": 147,\n        \"yyz\": 142,\n        \"fra\": 227,\n        \"phx\": 218,\n        \"gdl\": 595,\n        \"jnb\": 532,\n        \"bog\": 432,\n        \"syd\": 316,\n        \"gru\": 250,\n        \"ord\": 170,\n        \"den\": 253,\n        \"sea\": 194,\n        \"yul\": 122,\n        \"cdg\": 205,\n        \"sjc\": 133,\n        \"dfw\": 196,\n        \"ams\": 186,\n        \"lax\": 168,\n        \"otp\": 303,\n        \"mia\": 169,\n        \"nrt\": 424,\n        \"gig\": 352,\n        \"atl\": 130\n      },\n      {\n        \"timestamp\": \"2025-08-19T15:00:00.000Z\",\n        \"jnb\": 443,\n        \"phx\": 200,\n        \"gdl\": 646,\n        \"bog\": 418,\n        \"yyz\": 150,\n        \"lhr\": 161,\n        \"fra\": 154,\n        \"gru\": 191,\n        \"yul\": 146,\n        \"ord\": 169,\n        \"den\": 252,\n        \"sea\": 167,\n        \"syd\": 294,\n        \"ams\": 177,\n        \"sjc\": 143,\n        \"dfw\": 188,\n        \"otp\": 291,\n        \"lax\": 150,\n        \"cdg\": 244,\n        \"gig\": 226,\n        \"atl\": 132,\n        \"mia\": 167,\n        \"nrt\": 257,\n        \"iad\": 162,\n        \"bos\": 230,\n        \"arn\": 236,\n        \"mad\": 403,\n        \"sin\": 428,\n        \"eze\": 476,\n        \"scl\": 497,\n        \"hkg\": 320,\n        \"bom\": 310,\n        \"ewr\": 135\n      },\n      {\n        \"timestamp\": \"2025-08-19T15:30:00.000Z\",\n        \"arn\": 213,\n        \"mad\": 347,\n        \"sin\": 445,\n        \"iad\": 206,\n        \"bos\": 121,\n        \"scl\": 446,\n        \"hkg\": 364,\n        \"bom\": 290,\n        \"ewr\": 165,\n        \"eze\": 448,\n        \"gru\": 347,\n        \"yul\": 121,\n        \"ord\": 195,\n        \"den\": 251,\n        \"sea\": 162,\n        \"syd\": 424,\n        \"jnb\": 380,\n        \"phx\": 194,\n        \"gdl\": 603,\n        \"bog\": 433,\n        \"yyz\": 158,\n        \"lhr\": 194,\n        \"fra\": 168,\n        \"gig\": 363,\n        \"atl\": 143,\n        \"mia\": 194,\n        \"nrt\": 431,\n        \"ams\": 212,\n        \"sjc\": 130,\n        \"dfw\": 191,\n        \"lax\": 147,\n        \"otp\": 279,\n        \"cdg\": 157\n      },\n      {\n        \"timestamp\": \"2025-08-19T16:00:00.000Z\",\n        \"bos\": 119,\n        \"iad\": 161,\n        \"mad\": 333,\n        \"sin\": 416,\n        \"arn\": 196,\n        \"eze\": 450,\n        \"bom\": 307,\n        \"ewr\": 144,\n        \"scl\": 499,\n        \"hkg\": 534,\n        \"lhr\": 258,\n        \"yyz\": 151,\n        \"fra\": 185,\n        \"phx\": 229,\n        \"gdl\": 597,\n        \"jnb\": 497,\n        \"bog\": 419,\n        \"syd\": 417,\n        \"gru\": 342,\n        \"ord\": 146,\n        \"den\": 245,\n        \"sea\": 205,\n        \"yul\": 121,\n        \"cdg\": 157,\n        \"sjc\": 195,\n        \"dfw\": 205,\n        \"ams\": 185,\n        \"lax\": 147,\n        \"otp\": 274,\n        \"mia\": 176,\n        \"nrt\": 419,\n        \"gig\": 398,\n        \"atl\": 128\n      },\n      {\n        \"timestamp\": \"2025-08-19T16:30:00.000Z\",\n        \"fra\": 150,\n        \"lhr\": 158,\n        \"yyz\": 145,\n        \"bog\": 479,\n        \"gdl\": 612,\n        \"phx\": 197,\n        \"jnb\": 429,\n        \"syd\": 375,\n        \"sea\": 178,\n        \"den\": 260,\n        \"ord\": 173,\n        \"yul\": 106,\n        \"gru\": 205,\n        \"cdg\": 146,\n        \"otp\": 292,\n        \"lax\": 149,\n        \"dfw\": 179,\n        \"sjc\": 142,\n        \"ams\": 215,\n        \"nrt\": 445,\n        \"mia\": 171,\n        \"atl\": 144,\n        \"gig\": 374,\n        \"bos\": 134,\n        \"iad\": 144,\n        \"sin\": 432,\n        \"mad\": 334,\n        \"arn\": 315,\n        \"eze\": 474,\n        \"bom\": 297,\n        \"ewr\": 189,\n        \"hkg\": 324,\n        \"scl\": 496\n      },\n      {\n        \"timestamp\": \"2025-08-19T17:00:00.000Z\",\n        \"atl\": 130,\n        \"gig\": 347,\n        \"mia\": 173,\n        \"nrt\": 433,\n        \"lax\": 152,\n        \"otp\": 284,\n        \"sjc\": 129,\n        \"dfw\": 227,\n        \"ams\": 183,\n        \"cdg\": 145,\n        \"den\": 239,\n        \"ord\": 180,\n        \"sea\": 161,\n        \"yul\": 124,\n        \"gru\": 287,\n        \"syd\": 422,\n        \"bog\": 388,\n        \"phx\": 198,\n        \"gdl\": 642,\n        \"jnb\": 464,\n        \"fra\": 159,\n        \"lhr\": 184,\n        \"yyz\": 136,\n        \"hkg\": 526,\n        \"scl\": 510,\n        \"ewr\": 130,\n        \"bom\": 281,\n        \"eze\": 375,\n        \"arn\": 202,\n        \"sin\": 451,\n        \"mad\": 410,\n        \"iad\": 159,\n        \"bos\": 126\n      },\n      {\n        \"timestamp\": \"2025-08-19T17:30:00.000Z\",\n        \"yul\": 134,\n        \"sea\": 277,\n        \"den\": 279,\n        \"ord\": 143,\n        \"gru\": 208,\n        \"syd\": 296,\n        \"bog\": 459,\n        \"jnb\": 354,\n        \"gdl\": 564,\n        \"phx\": 237,\n        \"fra\": 176,\n        \"yyz\": 208,\n        \"lhr\": 152,\n        \"atl\": 162,\n        \"gig\": 375,\n        \"nrt\": 389,\n        \"mia\": 166,\n        \"otp\": 279,\n        \"lax\": 148,\n        \"ams\": 177,\n        \"dfw\": 312,\n        \"sjc\": 146,\n        \"cdg\": 144,\n        \"arn\": 196,\n        \"sin\": 444,\n        \"mad\": 375,\n        \"iad\": 186,\n        \"bos\": 131,\n        \"hkg\": 468,\n        \"scl\": 507,\n        \"ewr\": 172,\n        \"bom\": 285,\n        \"eze\": 460\n      },\n      {\n        \"timestamp\": \"2025-08-19T18:00:00.000Z\",\n        \"iad\": 132,\n        \"bos\": 116,\n        \"arn\": 199,\n        \"sin\": 477,\n        \"mad\": 361,\n        \"eze\": 448,\n        \"hkg\": 370,\n        \"scl\": 496,\n        \"ewr\": 140,\n        \"bom\": 300,\n        \"bog\": 497,\n        \"jnb\": 357,\n        \"gdl\": 625,\n        \"phx\": 203,\n        \"fra\": 163,\n        \"yyz\": 141,\n        \"lhr\": 208,\n        \"yul\": 125,\n        \"sea\": 186,\n        \"den\": 263,\n        \"ord\": 187,\n        \"gru\": 201,\n        \"syd\": 415,\n        \"otp\": 271,\n        \"lax\": 194,\n        \"ams\": 188,\n        \"dfw\": 207,\n        \"sjc\": 137,\n        \"cdg\": 158,\n        \"atl\": 143,\n        \"gig\": 358,\n        \"nrt\": 344,\n        \"mia\": 178\n      },\n      {\n        \"timestamp\": \"2025-08-19T18:30:00.000Z\",\n        \"hkg\": 495,\n        \"scl\": 498,\n        \"ewr\": 175,\n        \"bom\": 279,\n        \"eze\": 485,\n        \"arn\": 214,\n        \"sin\": 467,\n        \"mad\": 357,\n        \"iad\": 128,\n        \"bos\": 187,\n        \"atl\": 152,\n        \"gig\": 378,\n        \"nrt\": 430,\n        \"mia\": 196,\n        \"lax\": 171,\n        \"otp\": 306,\n        \"dfw\": 187,\n        \"sjc\": 148,\n        \"ams\": 186,\n        \"cdg\": 149,\n        \"sea\": 169,\n        \"ord\": 154,\n        \"den\": 263,\n        \"yul\": 123,\n        \"gru\": 300,\n        \"syd\": 413,\n        \"bog\": 397,\n        \"gdl\": 584,\n        \"phx\": 207,\n        \"jnb\": 498,\n        \"fra\": 246,\n        \"lhr\": 139,\n        \"yyz\": 138\n      },\n      {\n        \"timestamp\": \"2025-08-19T19:00:00.000Z\",\n        \"bos\": 119,\n        \"iad\": 166,\n        \"sin\": 489,\n        \"mad\": 375,\n        \"arn\": 198,\n        \"eze\": 458,\n        \"bom\": 271,\n        \"ewr\": 248,\n        \"hkg\": 485,\n        \"scl\": 499,\n        \"fra\": 154,\n        \"lhr\": 155,\n        \"yyz\": 144,\n        \"bog\": 445,\n        \"phx\": 194,\n        \"gdl\": 541,\n        \"jnb\": 468,\n        \"syd\": 452,\n        \"ord\": 209,\n        \"den\": 248,\n        \"sea\": 173,\n        \"yul\": 139,\n        \"gru\": 293,\n        \"cdg\": 152,\n        \"otp\": 295,\n        \"lax\": 150,\n        \"sjc\": 128,\n        \"dfw\": 169,\n        \"ams\": 192,\n        \"mia\": 183,\n        \"nrt\": 419,\n        \"atl\": 138,\n        \"gig\": 346\n      },\n      {\n        \"timestamp\": \"2025-08-19T19:30:00.000Z\",\n        \"eze\": 453,\n        \"bom\": 371,\n        \"ewr\": 128,\n        \"hkg\": 375,\n        \"scl\": 523,\n        \"bos\": 124,\n        \"iad\": 159,\n        \"sin\": 460,\n        \"mad\": 335,\n        \"arn\": 196,\n        \"cdg\": 173,\n        \"lax\": 165,\n        \"otp\": 306,\n        \"ams\": 171,\n        \"dfw\": 170,\n        \"sjc\": 145,\n        \"nrt\": 347,\n        \"mia\": 169,\n        \"atl\": 196,\n        \"gig\": 350,\n        \"fra\": 155,\n        \"yyz\": 151,\n        \"lhr\": 158,\n        \"bog\": 403,\n        \"jnb\": 517,\n        \"gdl\": 626,\n        \"phx\": 220,\n        \"syd\": 432,\n        \"yul\": 119,\n        \"sea\": 166,\n        \"ord\": 303,\n        \"den\": 286,\n        \"gru\": 230\n      },\n      {\n        \"timestamp\": \"2025-08-19T20:00:00.000Z\",\n        \"arn\": 235,\n        \"sin\": 493,\n        \"mad\": 348,\n        \"iad\": 251,\n        \"bos\": 120,\n        \"hkg\": 331,\n        \"scl\": 526,\n        \"ewr\": 157,\n        \"bom\": 278,\n        \"eze\": 384,\n        \"yul\": 125,\n        \"sea\": 173,\n        \"den\": 244,\n        \"ord\": 152,\n        \"gru\": 198,\n        \"syd\": 373,\n        \"bog\": 398,\n        \"jnb\": 478,\n        \"gdl\": 579,\n        \"phx\": 199,\n        \"fra\": 152,\n        \"yyz\": 143,\n        \"lhr\": 223,\n        \"atl\": 173,\n        \"gig\": 389,\n        \"nrt\": 357,\n        \"mia\": 196,\n        \"otp\": 281,\n        \"lax\": 154,\n        \"ams\": 222,\n        \"dfw\": 202,\n        \"sjc\": 133,\n        \"cdg\": 195\n      },\n      {\n        \"timestamp\": \"2025-08-19T20:30:00.000Z\",\n        \"bog\": 481,\n        \"jnb\": 370,\n        \"gdl\": 567,\n        \"phx\": 292,\n        \"fra\": 157,\n        \"yyz\": 150,\n        \"lhr\": 157,\n        \"yul\": 126,\n        \"sea\": 187,\n        \"den\": 280,\n        \"ord\": 144,\n        \"gru\": 201,\n        \"syd\": 335,\n        \"lax\": 158,\n        \"otp\": 288,\n        \"ams\": 171,\n        \"dfw\": 253,\n        \"sjc\": 124,\n        \"cdg\": 150,\n        \"atl\": 182,\n        \"gig\": 368,\n        \"nrt\": 422,\n        \"mia\": 202,\n        \"iad\": 180,\n        \"bos\": 114,\n        \"arn\": 206,\n        \"sin\": 532,\n        \"mad\": 343,\n        \"eze\": 480,\n        \"hkg\": 331,\n        \"scl\": 490,\n        \"ewr\": 302,\n        \"bom\": 336\n      },\n      {\n        \"timestamp\": \"2025-08-19T21:00:00.000Z\",\n        \"cdg\": 148,\n        \"ams\": 180,\n        \"dfw\": 191,\n        \"sjc\": 168,\n        \"otp\": 279,\n        \"lax\": 148,\n        \"nrt\": 433,\n        \"mia\": 205,\n        \"gig\": 365,\n        \"atl\": 173,\n        \"yyz\": 157,\n        \"lhr\": 151,\n        \"fra\": 174,\n        \"jnb\": 469,\n        \"gdl\": 644,\n        \"phx\": 199,\n        \"bog\": 393,\n        \"syd\": 385,\n        \"gru\": 203,\n        \"yul\": 132,\n        \"sea\": 172,\n        \"ord\": 170,\n        \"den\": 247,\n        \"eze\": 462,\n        \"bom\": 271,\n        \"ewr\": 171,\n        \"scl\": 493,\n        \"hkg\": 389,\n        \"bos\": 114,\n        \"iad\": 159,\n        \"mad\": 353,\n        \"sin\": 424,\n        \"arn\": 200\n      },\n      {\n        \"timestamp\": \"2025-08-19T21:30:00.000Z\",\n        \"bom\": 265,\n        \"ewr\": 154,\n        \"scl\": 447,\n        \"hkg\": 334,\n        \"eze\": 446,\n        \"mad\": 345,\n        \"sin\": 428,\n        \"arn\": 197,\n        \"bos\": 133,\n        \"iad\": 224,\n        \"nrt\": 431,\n        \"mia\": 221,\n        \"gig\": 357,\n        \"atl\": 164,\n        \"cdg\": 211,\n        \"ams\": 174,\n        \"dfw\": 187,\n        \"sjc\": 142,\n        \"lax\": 161,\n        \"otp\": 285,\n        \"syd\": 423,\n        \"gru\": 281,\n        \"yul\": 111,\n        \"sea\": 165,\n        \"ord\": 176,\n        \"den\": 267,\n        \"yyz\": 196,\n        \"lhr\": 145,\n        \"fra\": 162,\n        \"jnb\": 481,\n        \"gdl\": 609,\n        \"phx\": 280,\n        \"bog\": 425\n      },\n      {\n        \"timestamp\": \"2025-08-19T22:00:00.000Z\",\n        \"mad\": 341,\n        \"sin\": 421,\n        \"arn\": 209,\n        \"bos\": 126,\n        \"iad\": 179,\n        \"bom\": 311,\n        \"ewr\": 159,\n        \"scl\": 494,\n        \"hkg\": 328,\n        \"eze\": 451,\n        \"syd\": 420,\n        \"gru\": 334,\n        \"ord\": 149,\n        \"den\": 273,\n        \"sea\": 162,\n        \"yul\": 114,\n        \"lhr\": 221,\n        \"yyz\": 142,\n        \"fra\": 165,\n        \"phx\": 212,\n        \"gdl\": 643,\n        \"jnb\": 510,\n        \"bog\": 441,\n        \"mia\": 237,\n        \"nrt\": 433,\n        \"gig\": 371,\n        \"atl\": 195,\n        \"cdg\": 244,\n        \"sjc\": 126,\n        \"dfw\": 177,\n        \"ams\": 179,\n        \"lax\": 152,\n        \"otp\": 300\n      },\n      {\n        \"timestamp\": \"2025-08-19T22:30:00.000Z\",\n        \"lhr\": 162,\n        \"yyz\": 155,\n        \"fra\": 156,\n        \"phx\": 208,\n        \"gdl\": 575,\n        \"jnb\": 460,\n        \"bog\": 465,\n        \"syd\": 313,\n        \"gru\": 206,\n        \"ord\": 235,\n        \"den\": 245,\n        \"sea\": 167,\n        \"yul\": 129,\n        \"cdg\": 141,\n        \"sjc\": 134,\n        \"dfw\": 175,\n        \"ams\": 243,\n        \"otp\": 275,\n        \"lax\": 172,\n        \"mia\": 188,\n        \"nrt\": 426,\n        \"gig\": 352,\n        \"atl\": 177,\n        \"bos\": 131,\n        \"iad\": 137,\n        \"mad\": 355,\n        \"sin\": 475,\n        \"arn\": 240,\n        \"eze\": 455,\n        \"bom\": 285,\n        \"ewr\": 186,\n        \"scl\": 485,\n        \"hkg\": 468\n      },\n      {\n        \"timestamp\": \"2025-08-19T23:00:00.000Z\",\n        \"cdg\": 184,\n        \"otp\": 277,\n        \"lax\": 147,\n        \"sjc\": 131,\n        \"dfw\": 187,\n        \"ams\": 172,\n        \"mia\": 243,\n        \"nrt\": 392,\n        \"atl\": 195,\n        \"gig\": 305,\n        \"fra\": 183,\n        \"lhr\": 138,\n        \"yyz\": 160,\n        \"bog\": 476,\n        \"phx\": 207,\n        \"gdl\": 584,\n        \"jnb\": 380,\n        \"syd\": 422,\n        \"ord\": 175,\n        \"den\": 248,\n        \"sea\": 165,\n        \"yul\": 123,\n        \"gru\": 227,\n        \"eze\": 451,\n        \"bom\": 264,\n        \"ewr\": 175,\n        \"hkg\": 361,\n        \"scl\": 496,\n        \"bos\": 113,\n        \"iad\": 166,\n        \"sin\": 428,\n        \"mad\": 328,\n        \"arn\": 208\n      },\n      {\n        \"timestamp\": \"2025-08-19T23:30:00.000Z\",\n        \"bom\": 268,\n        \"ewr\": 138,\n        \"hkg\": 339,\n        \"scl\": 499,\n        \"eze\": 467,\n        \"sin\": 450,\n        \"mad\": 328,\n        \"arn\": 195,\n        \"bos\": 117,\n        \"iad\": 180,\n        \"mia\": 218,\n        \"nrt\": 404,\n        \"atl\": 219,\n        \"gig\": 371,\n        \"cdg\": 142,\n        \"lax\": 176,\n        \"otp\": 290,\n        \"sjc\": 126,\n        \"dfw\": 200,\n        \"ams\": 173,\n        \"syd\": 365,\n        \"ord\": 219,\n        \"den\": 241,\n        \"sea\": 283,\n        \"yul\": 137,\n        \"gru\": 244,\n        \"fra\": 152,\n        \"lhr\": 141,\n        \"yyz\": 156,\n        \"bog\": 419,\n        \"phx\": 206,\n        \"gdl\": 630,\n        \"jnb\": 490\n      },\n      {\n        \"timestamp\": \"2025-08-20T00:00:00.000Z\",\n        \"eze\": 381,\n        \"ewr\": 177,\n        \"bom\": 283,\n        \"hkg\": 317,\n        \"scl\": 500,\n        \"bos\": 144,\n        \"iad\": 170,\n        \"sin\": 414,\n        \"mad\": 459,\n        \"arn\": 193,\n        \"cdg\": 142,\n        \"lax\": 159,\n        \"otp\": 283,\n        \"ams\": 193,\n        \"sjc\": 124,\n        \"dfw\": 226,\n        \"mia\": 218,\n        \"nrt\": 295,\n        \"atl\": 180,\n        \"gig\": 370,\n        \"fra\": 162,\n        \"yyz\": 146,\n        \"lhr\": 142,\n        \"bog\": 400,\n        \"jnb\": 495,\n        \"phx\": 268,\n        \"gdl\": 571,\n        \"syd\": 341,\n        \"yul\": 139,\n        \"den\": 236,\n        \"ord\": 185,\n        \"sea\": 166,\n        \"gru\": 281\n      },\n      {\n        \"timestamp\": \"2025-08-20T00:30:00.000Z\",\n        \"nrt\": 361,\n        \"mia\": 179,\n        \"gig\": 351,\n        \"atl\": 237,\n        \"cdg\": 156,\n        \"ams\": 187,\n        \"dfw\": 210,\n        \"sjc\": 135,\n        \"lax\": 155,\n        \"otp\": 304,\n        \"syd\": 416,\n        \"gru\": 205,\n        \"yul\": 123,\n        \"sea\": 192,\n        \"ord\": 141,\n        \"den\": 261,\n        \"yyz\": 152,\n        \"lhr\": 152,\n        \"fra\": 156,\n        \"jnb\": 362,\n        \"gdl\": 504,\n        \"phx\": 196,\n        \"bog\": 393,\n        \"bom\": 262,\n        \"ewr\": 153,\n        \"scl\": 493,\n        \"hkg\": 355,\n        \"eze\": 456,\n        \"mad\": 458,\n        \"sin\": 484,\n        \"arn\": 201,\n        \"bos\": 122,\n        \"iad\": 159\n      },\n      {\n        \"timestamp\": \"2025-08-20T01:00:00.000Z\",\n        \"ewr\": 190,\n        \"bom\": 265,\n        \"hkg\": 325,\n        \"scl\": 486,\n        \"eze\": 474,\n        \"sin\": 641,\n        \"mad\": 478,\n        \"arn\": 210,\n        \"bos\": 122,\n        \"iad\": 167,\n        \"nrt\": 433,\n        \"mia\": 185,\n        \"atl\": 241,\n        \"gig\": 354,\n        \"cdg\": 187,\n        \"otp\": 285,\n        \"lax\": 173,\n        \"ams\": 182,\n        \"dfw\": 189,\n        \"sjc\": 137,\n        \"syd\": 290,\n        \"yul\": 132,\n        \"sea\": 174,\n        \"ord\": 171,\n        \"den\": 247,\n        \"gru\": 227,\n        \"fra\": 160,\n        \"yyz\": 195,\n        \"lhr\": 134,\n        \"bog\": 474,\n        \"jnb\": 505,\n        \"gdl\": 631,\n        \"phx\": 218\n      },\n      {\n        \"timestamp\": \"2025-08-20T01:30:00.000Z\",\n        \"iad\": 134,\n        \"bos\": 136,\n        \"arn\": 203,\n        \"sin\": 520,\n        \"mad\": 417,\n        \"eze\": 415,\n        \"hkg\": 356,\n        \"scl\": 723,\n        \"ewr\": 169,\n        \"bom\": 274,\n        \"bog\": 418,\n        \"jnb\": 502,\n        \"gdl\": 500,\n        \"phx\": 296,\n        \"fra\": 164,\n        \"yyz\": 152,\n        \"lhr\": 148,\n        \"yul\": 125,\n        \"sea\": 176,\n        \"den\": 227,\n        \"ord\": 257,\n        \"gru\": 204,\n        \"syd\": 392,\n        \"lax\": 152,\n        \"otp\": 271,\n        \"ams\": 182,\n        \"dfw\": 194,\n        \"sjc\": 213,\n        \"cdg\": 143,\n        \"atl\": 234,\n        \"gig\": 357,\n        \"nrt\": 422,\n        \"mia\": 235\n      },\n      {\n        \"timestamp\": \"2025-08-20T02:00:00.000Z\",\n        \"cdg\": 142,\n        \"ams\": 188,\n        \"dfw\": 202,\n        \"sjc\": 143,\n        \"lax\": 161,\n        \"otp\": 266,\n        \"nrt\": 437,\n        \"mia\": 179,\n        \"gig\": 362,\n        \"atl\": 222,\n        \"yyz\": 227,\n        \"lhr\": 141,\n        \"fra\": 275,\n        \"jnb\": 442,\n        \"gdl\": 564,\n        \"phx\": 193,\n        \"bog\": 487,\n        \"syd\": 416,\n        \"gru\": 222,\n        \"yul\": 124,\n        \"sea\": 170,\n        \"ord\": 147,\n        \"den\": 234,\n        \"eze\": 490,\n        \"bom\": 278,\n        \"ewr\": 133,\n        \"scl\": 498,\n        \"hkg\": 349,\n        \"bos\": 123,\n        \"iad\": 191,\n        \"mad\": 334,\n        \"sin\": 573,\n        \"arn\": 198\n      },\n      {\n        \"timestamp\": \"2025-08-20T02:30:00.000Z\",\n        \"atl\": 234,\n        \"gig\": 366,\n        \"mia\": 188,\n        \"nrt\": 366,\n        \"otp\": 311,\n        \"lax\": 165,\n        \"sjc\": 136,\n        \"dfw\": 185,\n        \"ams\": 194,\n        \"cdg\": 145,\n        \"den\": 224,\n        \"ord\": 154,\n        \"sea\": 158,\n        \"yul\": 129,\n        \"gru\": 215,\n        \"syd\": 385,\n        \"bog\": 416,\n        \"phx\": 270,\n        \"gdl\": 603,\n        \"jnb\": 466,\n        \"fra\": 174,\n        \"lhr\": 161,\n        \"yyz\": 144,\n        \"hkg\": 356,\n        \"scl\": 504,\n        \"ewr\": 133,\n        \"bom\": 297,\n        \"eze\": 456,\n        \"arn\": 216,\n        \"sin\": 480,\n        \"mad\": 335,\n        \"iad\": 165,\n        \"bos\": 118\n      },\n      {\n        \"timestamp\": \"2025-08-20T03:00:00.000Z\",\n        \"scl\": 491,\n        \"hkg\": 316,\n        \"bom\": 272,\n        \"ewr\": 159,\n        \"eze\": 449,\n        \"arn\": 189,\n        \"mad\": 341,\n        \"sin\": 417,\n        \"iad\": 134,\n        \"bos\": 125,\n        \"gig\": 296,\n        \"atl\": 290,\n        \"mia\": 177,\n        \"nrt\": 423,\n        \"sjc\": 130,\n        \"dfw\": 210,\n        \"ams\": 225,\n        \"lax\": 152,\n        \"otp\": 273,\n        \"cdg\": 143,\n        \"gru\": 198,\n        \"den\": 234,\n        \"ord\": 169,\n        \"sea\": 180,\n        \"yul\": 115,\n        \"syd\": 309,\n        \"phx\": 202,\n        \"gdl\": 632,\n        \"jnb\": 447,\n        \"bog\": 485,\n        \"lhr\": 154,\n        \"yyz\": 145,\n        \"fra\": 153\n      },\n      {\n        \"timestamp\": \"2025-08-20T03:30:00.000Z\",\n        \"nrt\": 284,\n        \"mia\": 192,\n        \"atl\": 330,\n        \"gig\": 354,\n        \"cdg\": 143,\n        \"otp\": 310,\n        \"lax\": 153,\n        \"ams\": 203,\n        \"dfw\": 205,\n        \"sjc\": 126,\n        \"syd\": 417,\n        \"yul\": 138,\n        \"sea\": 168,\n        \"ord\": 140,\n        \"den\": 228,\n        \"gru\": 188,\n        \"fra\": 171,\n        \"yyz\": 145,\n        \"lhr\": 174,\n        \"bog\": 464,\n        \"jnb\": 375,\n        \"gdl\": 612,\n        \"phx\": 203,\n        \"bom\": 391,\n        \"ewr\": 177,\n        \"hkg\": 382,\n        \"scl\": 494,\n        \"eze\": 451,\n        \"sin\": 417,\n        \"mad\": 337,\n        \"arn\": 201,\n        \"bos\": 119,\n        \"iad\": 183\n      },\n      {\n        \"timestamp\": \"2025-08-20T04:00:00.000Z\",\n        \"jnb\": 459,\n        \"phx\": 206,\n        \"gdl\": 633,\n        \"bog\": 389,\n        \"yyz\": 239,\n        \"lhr\": 142,\n        \"fra\": 188,\n        \"gru\": 184,\n        \"yul\": 136,\n        \"ord\": 414,\n        \"den\": 240,\n        \"sea\": 169,\n        \"syd\": 345,\n        \"ams\": 212,\n        \"sjc\": 136,\n        \"dfw\": 164,\n        \"otp\": 313,\n        \"lax\": 154,\n        \"cdg\": 144,\n        \"gig\": 354,\n        \"atl\": 295,\n        \"mia\": 211,\n        \"nrt\": 430,\n        \"iad\": 219,\n        \"bos\": 121,\n        \"arn\": 208,\n        \"mad\": 324,\n        \"sin\": 436,\n        \"eze\": 454,\n        \"scl\": 431,\n        \"hkg\": 394,\n        \"bom\": 288,\n        \"ewr\": 147\n      },\n      {\n        \"timestamp\": \"2025-08-20T04:30:00.000Z\",\n        \"arn\": 217,\n        \"sin\": 413,\n        \"mad\": 332,\n        \"iad\": 142,\n        \"bos\": 109,\n        \"hkg\": 511,\n        \"scl\": 498,\n        \"ewr\": 188,\n        \"bom\": 281,\n        \"eze\": 452,\n        \"yul\": 146,\n        \"sea\": 186,\n        \"den\": 233,\n        \"ord\": 164,\n        \"gru\": 263,\n        \"syd\": 375,\n        \"bog\": 418,\n        \"jnb\": 477,\n        \"gdl\": 498,\n        \"phx\": 201,\n        \"fra\": 150,\n        \"yyz\": 171,\n        \"lhr\": 193,\n        \"atl\": 242,\n        \"gig\": 344,\n        \"nrt\": 426,\n        \"mia\": 175,\n        \"otp\": 439,\n        \"lax\": 146,\n        \"ams\": 233,\n        \"dfw\": 206,\n        \"sjc\": 136,\n        \"cdg\": 154\n      },\n      {\n        \"timestamp\": \"2025-08-20T05:00:00.000Z\",\n        \"eze\": 448,\n        \"scl\": 498,\n        \"hkg\": 348,\n        \"bom\": 445,\n        \"ewr\": 142,\n        \"iad\": 343,\n        \"bos\": 116,\n        \"arn\": 198,\n        \"mad\": 333,\n        \"sin\": 401,\n        \"sjc\": 127,\n        \"dfw\": 188,\n        \"ams\": 176,\n        \"lax\": 142,\n        \"otp\": 315,\n        \"cdg\": 145,\n        \"gig\": 350,\n        \"atl\": 300,\n        \"mia\": 197,\n        \"nrt\": 421,\n        \"phx\": 265,\n        \"gdl\": 566,\n        \"jnb\": 465,\n        \"bog\": 397,\n        \"lhr\": 144,\n        \"yyz\": 170,\n        \"fra\": 165,\n        \"gru\": 212,\n        \"den\": 234,\n        \"ord\": 158,\n        \"sea\": 162,\n        \"yul\": 118,\n        \"syd\": 379\n      },\n      {\n        \"timestamp\": \"2025-08-20T05:30:00.000Z\",\n        \"bos\": 155,\n        \"iad\": 154,\n        \"sin\": 421,\n        \"mad\": 334,\n        \"arn\": 203,\n        \"eze\": 452,\n        \"bom\": 290,\n        \"ewr\": 185,\n        \"hkg\": 361,\n        \"scl\": 501,\n        \"fra\": 151,\n        \"lhr\": 162,\n        \"yyz\": 155,\n        \"bog\": 411,\n        \"gdl\": 567,\n        \"phx\": 202,\n        \"jnb\": 457,\n        \"syd\": 319,\n        \"sea\": 165,\n        \"den\": 241,\n        \"ord\": 188,\n        \"yul\": 125,\n        \"gru\": 229,\n        \"cdg\": 176,\n        \"otp\": 273,\n        \"lax\": 156,\n        \"dfw\": 192,\n        \"sjc\": 133,\n        \"ams\": 177,\n        \"nrt\": 420,\n        \"mia\": 181,\n        \"atl\": 260,\n        \"gig\": 353\n      },\n      {\n        \"timestamp\": \"2025-08-20T06:00:00.000Z\",\n        \"atl\": 192,\n        \"gig\": 374,\n        \"mia\": 173,\n        \"nrt\": 364,\n        \"otp\": 283,\n        \"lax\": 170,\n        \"sjc\": 130,\n        \"dfw\": 170,\n        \"ams\": 187,\n        \"cdg\": 181,\n        \"den\": 230,\n        \"ord\": 150,\n        \"sea\": 167,\n        \"yul\": 114,\n        \"gru\": 187,\n        \"syd\": 419,\n        \"bog\": 483,\n        \"phx\": 223,\n        \"gdl\": 627,\n        \"jnb\": 391,\n        \"fra\": 156,\n        \"lhr\": 144,\n        \"yyz\": 164,\n        \"hkg\": 389,\n        \"scl\": 496,\n        \"ewr\": 152,\n        \"bom\": 286,\n        \"eze\": 453,\n        \"arn\": 327,\n        \"sin\": 457,\n        \"mad\": 351,\n        \"iad\": 165,\n        \"bos\": 150\n      },\n      {\n        \"timestamp\": \"2025-08-20T06:30:00.000Z\",\n        \"mad\": 329,\n        \"sin\": 525,\n        \"arn\": 361,\n        \"bos\": 149,\n        \"iad\": 135,\n        \"bom\": 337,\n        \"ewr\": 128,\n        \"scl\": 520,\n        \"hkg\": 329,\n        \"eze\": 474,\n        \"syd\": 414,\n        \"gru\": 283,\n        \"ord\": 152,\n        \"den\": 226,\n        \"sea\": 191,\n        \"yul\": 115,\n        \"lhr\": 159,\n        \"yyz\": 213,\n        \"fra\": 163,\n        \"phx\": 227,\n        \"gdl\": 624,\n        \"jnb\": 505,\n        \"bog\": 405,\n        \"mia\": 208,\n        \"nrt\": 436,\n        \"gig\": 346,\n        \"atl\": 179,\n        \"cdg\": 189,\n        \"sjc\": 140,\n        \"dfw\": 188,\n        \"ams\": 197,\n        \"lax\": 174,\n        \"otp\": 285\n      },\n      {\n        \"timestamp\": \"2025-08-20T07:00:00.000Z\",\n        \"eze\": 450,\n        \"ewr\": 130,\n        \"bom\": 357,\n        \"hkg\": 365,\n        \"scl\": 504,\n        \"bos\": 125,\n        \"iad\": 126,\n        \"sin\": 469,\n        \"mad\": 374,\n        \"arn\": 254,\n        \"cdg\": 210,\n        \"otp\": 317,\n        \"lax\": 149,\n        \"ams\": 172,\n        \"dfw\": 216,\n        \"sjc\": 162,\n        \"nrt\": 412,\n        \"mia\": 210,\n        \"atl\": 157,\n        \"gig\": 346,\n        \"fra\": 269,\n        \"yyz\": 174,\n        \"lhr\": 177,\n        \"bog\": 483,\n        \"jnb\": 473,\n        \"gdl\": 599,\n        \"phx\": 225,\n        \"syd\": 364,\n        \"yul\": 119,\n        \"sea\": 159,\n        \"ord\": 181,\n        \"den\": 242,\n        \"gru\": 272\n      },\n      {\n        \"timestamp\": \"2025-08-20T07:30:00.000Z\",\n        \"iad\": 157,\n        \"bos\": 137,\n        \"arn\": 255,\n        \"mad\": 374,\n        \"sin\": 408,\n        \"eze\": 439,\n        \"scl\": 496,\n        \"hkg\": 398,\n        \"ewr\": 170,\n        \"bom\": 304,\n        \"jnb\": 501,\n        \"phx\": 198,\n        \"gdl\": 628,\n        \"bog\": 411,\n        \"yyz\": 179,\n        \"lhr\": 140,\n        \"fra\": 152,\n        \"gru\": 221,\n        \"yul\": 134,\n        \"ord\": 174,\n        \"den\": 232,\n        \"sea\": 171,\n        \"syd\": 346,\n        \"ams\": 183,\n        \"sjc\": 139,\n        \"dfw\": 227,\n        \"lax\": 174,\n        \"otp\": 289,\n        \"cdg\": 225,\n        \"gig\": 344,\n        \"atl\": 189,\n        \"mia\": 203,\n        \"nrt\": 444\n      },\n      {\n        \"timestamp\": \"2025-08-20T08:00:00.000Z\",\n        \"eze\": 456,\n        \"ewr\": 149,\n        \"bom\": 306,\n        \"scl\": 501,\n        \"hkg\": 537,\n        \"bos\": 128,\n        \"iad\": 167,\n        \"mad\": 346,\n        \"sin\": 403,\n        \"arn\": 233,\n        \"cdg\": 184,\n        \"ams\": 211,\n        \"sjc\": 124,\n        \"dfw\": 205,\n        \"otp\": 274,\n        \"lax\": 232,\n        \"mia\": 209,\n        \"nrt\": 368,\n        \"gig\": 349,\n        \"atl\": 185,\n        \"yyz\": 159,\n        \"lhr\": 148,\n        \"fra\": 166,\n        \"jnb\": 452,\n        \"phx\": 193,\n        \"gdl\": 500,\n        \"bog\": 392,\n        \"syd\": 376,\n        \"gru\": 238,\n        \"yul\": 122,\n        \"ord\": 153,\n        \"den\": 251,\n        \"sea\": 171\n      },\n      {\n        \"timestamp\": \"2025-08-20T08:30:00.000Z\",\n        \"nrt\": 427,\n        \"mia\": 212,\n        \"atl\": 149,\n        \"gig\": 353,\n        \"cdg\": 266,\n        \"otp\": 303,\n        \"lax\": 148,\n        \"ams\": 264,\n        \"dfw\": 200,\n        \"sjc\": 131,\n        \"syd\": 376,\n        \"yul\": 120,\n        \"sea\": 175,\n        \"den\": 233,\n        \"ord\": 177,\n        \"gru\": 268,\n        \"fra\": 162,\n        \"yyz\": 151,\n        \"lhr\": 357,\n        \"bog\": 497,\n        \"jnb\": 520,\n        \"gdl\": 586,\n        \"phx\": 201,\n        \"bom\": 292,\n        \"ewr\": 130,\n        \"hkg\": 395,\n        \"scl\": 496,\n        \"eze\": 455,\n        \"sin\": 486,\n        \"mad\": 359,\n        \"arn\": 205,\n        \"bos\": 124,\n        \"iad\": 149\n      },\n      {\n        \"timestamp\": \"2025-08-20T09:00:00.000Z\",\n        \"ewr\": 140,\n        \"bom\": 318,\n        \"scl\": 492,\n        \"hkg\": 422,\n        \"eze\": 452,\n        \"mad\": 386,\n        \"sin\": 456,\n        \"arn\": 200,\n        \"bos\": 110,\n        \"iad\": 132,\n        \"nrt\": 353,\n        \"mia\": 180,\n        \"gig\": 356,\n        \"atl\": 164,\n        \"cdg\": 157,\n        \"ams\": 208,\n        \"dfw\": 233,\n        \"sjc\": 132,\n        \"lax\": 153,\n        \"otp\": 303,\n        \"syd\": 413,\n        \"gru\": 209,\n        \"yul\": 118,\n        \"sea\": 175,\n        \"den\": 225,\n        \"ord\": 155,\n        \"yyz\": 160,\n        \"lhr\": 152,\n        \"fra\": 187,\n        \"jnb\": 501,\n        \"gdl\": 596,\n        \"phx\": 199,\n        \"bog\": 400\n      },\n      {\n        \"timestamp\": \"2025-08-20T09:30:00.000Z\",\n        \"otp\": 296,\n        \"lax\": 157,\n        \"dfw\": 199,\n        \"sjc\": 146,\n        \"ams\": 176,\n        \"cdg\": 143,\n        \"atl\": 153,\n        \"gig\": 349,\n        \"nrt\": 438,\n        \"mia\": 212,\n        \"bog\": 417,\n        \"gdl\": 568,\n        \"phx\": 214,\n        \"jnb\": 394,\n        \"fra\": 153,\n        \"lhr\": 171,\n        \"yyz\": 166,\n        \"sea\": 173,\n        \"den\": 234,\n        \"ord\": 166,\n        \"yul\": 146,\n        \"gru\": 190,\n        \"syd\": 414,\n        \"eze\": 475,\n        \"hkg\": 476,\n        \"scl\": 498,\n        \"ewr\": 125,\n        \"bom\": 356,\n        \"iad\": 174,\n        \"bos\": 115,\n        \"arn\": 202,\n        \"sin\": 427,\n        \"mad\": 346\n      },\n      {\n        \"timestamp\": \"2025-08-20T10:00:00.000Z\",\n        \"bos\": 130,\n        \"iad\": 134,\n        \"mad\": 388,\n        \"sin\": 433,\n        \"arn\": 195,\n        \"eze\": 450,\n        \"bom\": 330,\n        \"ewr\": 161,\n        \"scl\": 511,\n        \"hkg\": 339,\n        \"lhr\": 148,\n        \"yyz\": 142,\n        \"fra\": 165,\n        \"gdl\": 614,\n        \"phx\": 202,\n        \"jnb\": 529,\n        \"bog\": 436,\n        \"syd\": 312,\n        \"gru\": 207,\n        \"sea\": 197,\n        \"ord\": 200,\n        \"den\": 252,\n        \"yul\": 128,\n        \"cdg\": 196,\n        \"dfw\": 170,\n        \"sjc\": 144,\n        \"ams\": 178,\n        \"otp\": 281,\n        \"lax\": 159,\n        \"nrt\": 432,\n        \"mia\": 205,\n        \"gig\": 343,\n        \"atl\": 166\n      },\n      {\n        \"timestamp\": \"2025-08-20T10:30:00.000Z\",\n        \"gig\": 348,\n        \"atl\": 199,\n        \"mia\": 231,\n        \"nrt\": 297,\n        \"sjc\": 139,\n        \"dfw\": 176,\n        \"ams\": 210,\n        \"lax\": 186,\n        \"otp\": 293,\n        \"cdg\": 200,\n        \"gru\": 303,\n        \"ord\": 173,\n        \"den\": 233,\n        \"sea\": 189,\n        \"yul\": 127,\n        \"syd\": 332,\n        \"phx\": 196,\n        \"gdl\": 645,\n        \"jnb\": 537,\n        \"bog\": 541,\n        \"lhr\": 179,\n        \"yyz\": 134,\n        \"fra\": 165,\n        \"scl\": 530,\n        \"hkg\": 419,\n        \"ewr\": 131,\n        \"bom\": 331,\n        \"eze\": 456,\n        \"arn\": 186,\n        \"mad\": 634,\n        \"sin\": 456,\n        \"iad\": 294,\n        \"bos\": 121\n      },\n      {\n        \"timestamp\": \"2025-08-20T11:00:00.000Z\",\n        \"hkg\": 474,\n        \"scl\": 432,\n        \"bom\": 335,\n        \"ewr\": 182,\n        \"eze\": 456,\n        \"arn\": 199,\n        \"sin\": 437,\n        \"mad\": 354,\n        \"iad\": 203,\n        \"bos\": 127,\n        \"atl\": 226,\n        \"gig\": 299,\n        \"mia\": 234,\n        \"nrt\": 436,\n        \"otp\": 285,\n        \"lax\": 177,\n        \"sjc\": 141,\n        \"dfw\": 166,\n        \"ams\": 195,\n        \"cdg\": 148,\n        \"ord\": 199,\n        \"den\": 231,\n        \"sea\": 179,\n        \"yul\": 132,\n        \"gru\": 229,\n        \"syd\": 316,\n        \"bog\": 495,\n        \"phx\": 199,\n        \"gdl\": 543,\n        \"jnb\": 461,\n        \"fra\": 187,\n        \"lhr\": 175,\n        \"yyz\": 158\n      },\n      {\n        \"timestamp\": \"2025-08-20T11:30:00.000Z\",\n        \"cdg\": 339,\n        \"ams\": 176,\n        \"sjc\": 199,\n        \"dfw\": 191,\n        \"lax\": 168,\n        \"otp\": 296,\n        \"mia\": 188,\n        \"nrt\": 460,\n        \"gig\": 441,\n        \"atl\": 152,\n        \"yyz\": 157,\n        \"lhr\": 191,\n        \"fra\": 209,\n        \"jnb\": 385,\n        \"phx\": 282,\n        \"gdl\": 598,\n        \"bog\": 392,\n        \"syd\": 430,\n        \"gru\": 279,\n        \"yul\": 124,\n        \"ord\": 183,\n        \"den\": 238,\n        \"sea\": 218,\n        \"eze\": 462,\n        \"bom\": 325,\n        \"ewr\": 127,\n        \"scl\": 500,\n        \"hkg\": 412,\n        \"bos\": 160,\n        \"iad\": 170,\n        \"mad\": 342,\n        \"sin\": 409,\n        \"arn\": 220\n      },\n      {\n        \"timestamp\": \"2025-08-20T12:00:00.000Z\",\n        \"yul\": 122,\n        \"sea\": 170,\n        \"ord\": 170,\n        \"den\": 252,\n        \"gru\": 239,\n        \"syd\": 439,\n        \"bog\": 395,\n        \"jnb\": 487,\n        \"gdl\": 614,\n        \"phx\": 203,\n        \"fra\": 161,\n        \"yyz\": 164,\n        \"lhr\": 146,\n        \"atl\": 149,\n        \"gig\": 367,\n        \"nrt\": 463,\n        \"mia\": 234,\n        \"lax\": 159,\n        \"otp\": 298,\n        \"ams\": 178,\n        \"dfw\": 182,\n        \"sjc\": 141,\n        \"cdg\": 143,\n        \"arn\": 192,\n        \"sin\": 412,\n        \"mad\": 343,\n        \"iad\": 187,\n        \"bos\": 117,\n        \"hkg\": 422,\n        \"scl\": 503,\n        \"bom\": 322,\n        \"ewr\": 156,\n        \"eze\": 482\n      },\n      {\n        \"timestamp\": \"2025-08-20T12:30:00.000Z\",\n        \"iad\": 186,\n        \"bos\": 126,\n        \"arn\": 198,\n        \"mad\": 339,\n        \"sin\": 432,\n        \"eze\": 448,\n        \"scl\": 510,\n        \"hkg\": 331,\n        \"ewr\": 168,\n        \"bom\": 317,\n        \"jnb\": 465,\n        \"phx\": 198,\n        \"gdl\": 543,\n        \"bog\": 475,\n        \"yyz\": 166,\n        \"lhr\": 146,\n        \"fra\": 184,\n        \"gru\": 300,\n        \"yul\": 126,\n        \"den\": 227,\n        \"ord\": 174,\n        \"sea\": 196,\n        \"syd\": 330,\n        \"ams\": 192,\n        \"sjc\": 132,\n        \"dfw\": 174,\n        \"lax\": 159,\n        \"otp\": 284,\n        \"cdg\": 163,\n        \"gig\": 362,\n        \"atl\": 202,\n        \"mia\": 196,\n        \"nrt\": 444\n      },\n      {\n        \"timestamp\": \"2025-08-20T13:00:00.000Z\",\n        \"eze\": 411,\n        \"hkg\": 527,\n        \"scl\": 498,\n        \"bom\": 335,\n        \"ewr\": 266,\n        \"iad\": 186,\n        \"bos\": 112,\n        \"arn\": 201,\n        \"sin\": 496,\n        \"mad\": 338,\n        \"otp\": 326,\n        \"lax\": 168,\n        \"sjc\": 145,\n        \"dfw\": 177,\n        \"ams\": 189,\n        \"cdg\": 158,\n        \"atl\": 184,\n        \"gig\": 365,\n        \"mia\": 223,\n        \"nrt\": 365,\n        \"bog\": 408,\n        \"phx\": 198,\n        \"gdl\": 606,\n        \"jnb\": 426,\n        \"fra\": 196,\n        \"lhr\": 145,\n        \"yyz\": 150,\n        \"ord\": 186,\n        \"den\": 222,\n        \"sea\": 171,\n        \"yul\": 125,\n        \"gru\": 335,\n        \"syd\": 292\n      },\n      {\n        \"timestamp\": \"2025-08-20T13:30:00.000Z\",\n        \"bos\": 118,\n        \"iad\": 131,\n        \"mad\": 406,\n        \"sin\": 436,\n        \"arn\": 192,\n        \"eze\": 458,\n        \"bom\": 355,\n        \"ewr\": 165,\n        \"scl\": 495,\n        \"hkg\": 331,\n        \"lhr\": 150,\n        \"yyz\": 220,\n        \"fra\": 163,\n        \"gdl\": 632,\n        \"phx\": 188,\n        \"jnb\": 405,\n        \"bog\": 464,\n        \"syd\": 364,\n        \"gru\": 258,\n        \"sea\": 168,\n        \"ord\": 164,\n        \"den\": 234,\n        \"yul\": 119,\n        \"cdg\": 154,\n        \"dfw\": 207,\n        \"sjc\": 129,\n        \"ams\": 181,\n        \"lax\": 194,\n        \"otp\": 272,\n        \"nrt\": 453,\n        \"mia\": 301,\n        \"gig\": 353,\n        \"atl\": 162\n      },\n      {\n        \"timestamp\": \"2025-08-20T14:00:00.000Z\",\n        \"arn\": 211,\n        \"sin\": 424,\n        \"mad\": 360,\n        \"iad\": 161,\n        \"bos\": 119,\n        \"hkg\": 348,\n        \"scl\": 368,\n        \"ewr\": 158,\n        \"bom\": 314,\n        \"eze\": 452,\n        \"yul\": 138,\n        \"den\": 221,\n        \"ord\": 145,\n        \"sea\": 188,\n        \"gru\": 202,\n        \"syd\": 364,\n        \"bog\": 458,\n        \"jnb\": 445,\n        \"phx\": 283,\n        \"gdl\": 613,\n        \"fra\": 176,\n        \"yyz\": 149,\n        \"lhr\": 206,\n        \"atl\": 227,\n        \"gig\": 384,\n        \"mia\": 202,\n        \"nrt\": 355,\n        \"lax\": 180,\n        \"otp\": 282,\n        \"ams\": 176,\n        \"sjc\": 135,\n        \"dfw\": 209,\n        \"cdg\": 159\n      },\n      {\n        \"timestamp\": \"2025-08-20T14:30:00.000Z\",\n        \"mia\": 184,\n        \"nrt\": 424,\n        \"gig\": 364,\n        \"atl\": 206,\n        \"cdg\": 153,\n        \"ams\": 221,\n        \"sjc\": 133,\n        \"dfw\": 178,\n        \"otp\": 276,\n        \"lax\": 150,\n        \"syd\": 286,\n        \"gru\": 224,\n        \"yul\": 114,\n        \"ord\": 132,\n        \"den\": 238,\n        \"sea\": 156,\n        \"yyz\": 140,\n        \"lhr\": 136,\n        \"fra\": 233,\n        \"jnb\": 414,\n        \"phx\": 194,\n        \"gdl\": 619,\n        \"bog\": 433,\n        \"bom\": 488,\n        \"ewr\": 155,\n        \"scl\": 542,\n        \"hkg\": 313,\n        \"eze\": 381,\n        \"mad\": 322,\n        \"sin\": 424,\n        \"arn\": 201,\n        \"bos\": 162,\n        \"iad\": 146\n      },\n      {\n        \"timestamp\": \"2025-08-20T15:00:00.000Z\",\n        \"eze\": 452,\n        \"ewr\": 151,\n        \"bom\": 303,\n        \"scl\": 442,\n        \"hkg\": 335,\n        \"bos\": 130,\n        \"iad\": 186,\n        \"mad\": 368,\n        \"sin\": 462,\n        \"arn\": 214,\n        \"cdg\": 153,\n        \"ams\": 182,\n        \"dfw\": 223,\n        \"sjc\": 134,\n        \"lax\": 207,\n        \"otp\": 282,\n        \"nrt\": 360,\n        \"mia\": 181,\n        \"gig\": 367,\n        \"atl\": 149,\n        \"yyz\": 147,\n        \"lhr\": 170,\n        \"fra\": 211,\n        \"jnb\": 463,\n        \"gdl\": 606,\n        \"phx\": 195,\n        \"bog\": 395,\n        \"syd\": 418,\n        \"gru\": 195,\n        \"yul\": 110,\n        \"sea\": 163,\n        \"den\": 228,\n        \"ord\": 147\n      },\n      {\n        \"timestamp\": \"2025-08-20T15:30:00.000Z\",\n        \"iad\": 198,\n        \"bos\": 116,\n        \"arn\": 206,\n        \"sin\": 498,\n        \"mad\": 422,\n        \"eze\": 452,\n        \"hkg\": 324,\n        \"scl\": 514,\n        \"ewr\": 157,\n        \"bom\": 314,\n        \"bog\": 408,\n        \"jnb\": 499,\n        \"phx\": 238,\n        \"gdl\": 608,\n        \"fra\": 151,\n        \"yyz\": 157,\n        \"lhr\": 359,\n        \"yul\": 119,\n        \"den\": 276,\n        \"ord\": 168,\n        \"sea\": 199,\n        \"gru\": 224,\n        \"syd\": 386,\n        \"otp\": 271,\n        \"lax\": 176,\n        \"ams\": 185,\n        \"sjc\": 131,\n        \"dfw\": 192,\n        \"cdg\": 147,\n        \"atl\": 218,\n        \"gig\": 376,\n        \"mia\": 191,\n        \"nrt\": 388\n      },\n      {\n        \"timestamp\": \"2025-08-20T16:00:00.000Z\",\n        \"arn\": 185,\n        \"sin\": 430,\n        \"mad\": 378,\n        \"iad\": 325,\n        \"bos\": 129,\n        \"hkg\": 387,\n        \"scl\": 380,\n        \"bom\": 321,\n        \"ewr\": 130,\n        \"eze\": 449,\n        \"ord\": 149,\n        \"den\": 234,\n        \"sea\": 158,\n        \"yul\": 144,\n        \"gru\": 294,\n        \"syd\": 309,\n        \"bog\": 429,\n        \"phx\": 207,\n        \"gdl\": 599,\n        \"jnb\": 404,\n        \"fra\": 157,\n        \"lhr\": 141,\n        \"yyz\": 137,\n        \"atl\": 147,\n        \"gig\": 372,\n        \"mia\": 194,\n        \"nrt\": 391,\n        \"otp\": 307,\n        \"lax\": 180,\n        \"sjc\": 162,\n        \"dfw\": 193,\n        \"ams\": 206,\n        \"cdg\": 188\n      },\n      {\n        \"timestamp\": \"2025-08-20T16:30:00.000Z\",\n        \"bog\": 447,\n        \"phx\": 204,\n        \"gdl\": 622,\n        \"jnb\": 502,\n        \"fra\": 162,\n        \"lhr\": 183,\n        \"yyz\": 162,\n        \"ord\": 151,\n        \"den\": 216,\n        \"sea\": 175,\n        \"yul\": 108,\n        \"gru\": 223,\n        \"syd\": 413,\n        \"lax\": 175,\n        \"otp\": 276,\n        \"sjc\": 139,\n        \"dfw\": 188,\n        \"ams\": 203,\n        \"cdg\": 151,\n        \"atl\": 163,\n        \"gig\": 356,\n        \"mia\": 182,\n        \"nrt\": 591,\n        \"iad\": 150,\n        \"bos\": 135,\n        \"arn\": 195,\n        \"sin\": 420,\n        \"mad\": 345,\n        \"eze\": 437,\n        \"hkg\": 464,\n        \"scl\": 498,\n        \"bom\": 425,\n        \"ewr\": 124\n      },\n      {\n        \"timestamp\": \"2025-08-20T17:00:00.000Z\",\n        \"iad\": 361,\n        \"bos\": 126,\n        \"arn\": 223,\n        \"sin\": 458,\n        \"mad\": 347,\n        \"eze\": 469,\n        \"hkg\": 371,\n        \"scl\": 496,\n        \"bom\": 298,\n        \"ewr\": 140,\n        \"bog\": 394,\n        \"gdl\": 636,\n        \"phx\": 194,\n        \"jnb\": 438,\n        \"fra\": 220,\n        \"lhr\": 155,\n        \"yyz\": 147,\n        \"sea\": 167,\n        \"den\": 228,\n        \"ord\": 161,\n        \"yul\": 125,\n        \"gru\": 282,\n        \"syd\": 442,\n        \"lax\": 173,\n        \"otp\": 329,\n        \"dfw\": 174,\n        \"sjc\": 141,\n        \"ams\": 225,\n        \"cdg\": 177,\n        \"atl\": 172,\n        \"gig\": 363,\n        \"nrt\": 858,\n        \"mia\": 181\n      },\n      {\n        \"timestamp\": \"2025-08-20T17:30:00.000Z\",\n        \"sin\": 421,\n        \"mad\": 345,\n        \"arn\": 243,\n        \"bos\": 114,\n        \"iad\": 183,\n        \"bom\": 313,\n        \"ewr\": 133,\n        \"hkg\": 415,\n        \"scl\": 502,\n        \"eze\": 455,\n        \"syd\": 412,\n        \"sea\": 160,\n        \"den\": 230,\n        \"ord\": 206,\n        \"yul\": 112,\n        \"gru\": 195,\n        \"fra\": 174,\n        \"lhr\": 141,\n        \"yyz\": 138,\n        \"bog\": 493,\n        \"gdl\": 552,\n        \"phx\": 206,\n        \"jnb\": 454,\n        \"nrt\": 457,\n        \"mia\": 216,\n        \"atl\": 149,\n        \"gig\": 354,\n        \"cdg\": 173,\n        \"lax\": 169,\n        \"otp\": 277,\n        \"dfw\": 197,\n        \"sjc\": 133,\n        \"ams\": 187\n      },\n      {\n        \"timestamp\": \"2025-08-20T18:00:00.000Z\",\n        \"bos\": 111,\n        \"iad\": 419,\n        \"sin\": 456,\n        \"mad\": 336,\n        \"arn\": 185,\n        \"eze\": 452,\n        \"ewr\": 131,\n        \"bom\": 298,\n        \"hkg\": 327,\n        \"scl\": 515,\n        \"fra\": 196,\n        \"lhr\": 139,\n        \"yyz\": 159,\n        \"bog\": 388,\n        \"phx\": 207,\n        \"gdl\": 586,\n        \"jnb\": 439,\n        \"syd\": 295,\n        \"ord\": 148,\n        \"den\": 219,\n        \"sea\": 190,\n        \"yul\": 130,\n        \"gru\": 196,\n        \"cdg\": 194,\n        \"lax\": 194,\n        \"otp\": 287,\n        \"sjc\": 140,\n        \"dfw\": 228,\n        \"ams\": 181,\n        \"mia\": 179,\n        \"nrt\": 474,\n        \"atl\": 154,\n        \"gig\": 374\n      },\n      {\n        \"timestamp\": \"2025-08-20T18:30:00.000Z\",\n        \"eze\": 466,\n        \"ewr\": 131,\n        \"bom\": 308,\n        \"hkg\": 326,\n        \"scl\": 497,\n        \"bos\": 120,\n        \"iad\": 295,\n        \"sin\": 438,\n        \"mad\": 332,\n        \"arn\": 200,\n        \"cdg\": 148,\n        \"otp\": 302,\n        \"lax\": 157,\n        \"ams\": 186,\n        \"dfw\": 209,\n        \"sjc\": 146,\n        \"nrt\": 427,\n        \"mia\": 254,\n        \"atl\": 164,\n        \"gig\": 361,\n        \"fra\": 155,\n        \"yyz\": 162,\n        \"lhr\": 139,\n        \"bog\": 462,\n        \"jnb\": 445,\n        \"gdl\": 581,\n        \"phx\": 188,\n        \"syd\": 381,\n        \"yul\": 125,\n        \"sea\": 175,\n        \"ord\": 152,\n        \"den\": 235,\n        \"gru\": 219\n      },\n      {\n        \"timestamp\": \"2025-08-20T19:00:00.000Z\",\n        \"yul\": 110,\n        \"sea\": 172,\n        \"den\": 235,\n        \"ord\": 167,\n        \"gru\": 190,\n        \"syd\": 375,\n        \"bog\": 390,\n        \"jnb\": 365,\n        \"gdl\": 595,\n        \"phx\": 266,\n        \"fra\": 198,\n        \"yyz\": 198,\n        \"lhr\": 152,\n        \"atl\": 152,\n        \"gig\": 360,\n        \"nrt\": 428,\n        \"mia\": 211,\n        \"lax\": 174,\n        \"otp\": 396,\n        \"ams\": 207,\n        \"dfw\": 269,\n        \"sjc\": 158,\n        \"cdg\": 174,\n        \"arn\": 204,\n        \"sin\": 410,\n        \"mad\": 410,\n        \"iad\": 325,\n        \"bos\": 147,\n        \"hkg\": 339,\n        \"scl\": 516,\n        \"ewr\": 195,\n        \"bom\": 309,\n        \"eze\": 447\n      },\n      {\n        \"timestamp\": \"2025-08-20T19:30:00.000Z\",\n        \"arn\": 186,\n        \"mad\": 343,\n        \"sin\": 439,\n        \"iad\": 150,\n        \"bos\": 119,\n        \"scl\": 510,\n        \"hkg\": 366,\n        \"ewr\": 134,\n        \"bom\": 360,\n        \"eze\": 448,\n        \"gru\": 309,\n        \"yul\": 123,\n        \"ord\": 161,\n        \"den\": 230,\n        \"sea\": 164,\n        \"syd\": 291,\n        \"jnb\": 447,\n        \"phx\": 193,\n        \"gdl\": 550,\n        \"bog\": 393,\n        \"yyz\": 145,\n        \"lhr\": 141,\n        \"fra\": 158,\n        \"gig\": 379,\n        \"atl\": 182,\n        \"mia\": 177,\n        \"nrt\": 423,\n        \"ams\": 171,\n        \"sjc\": 126,\n        \"dfw\": 213,\n        \"otp\": 285,\n        \"lax\": 152,\n        \"cdg\": 139\n      },\n      {\n        \"timestamp\": \"2025-08-20T20:00:00.000Z\",\n        \"gru\": 269,\n        \"yul\": 143,\n        \"ord\": 244,\n        \"den\": 265,\n        \"sea\": 186,\n        \"syd\": 392,\n        \"jnb\": 455,\n        \"phx\": 189,\n        \"gdl\": 691,\n        \"bog\": 437,\n        \"yyz\": 164,\n        \"lhr\": 141,\n        \"fra\": 159,\n        \"gig\": 364,\n        \"atl\": 158,\n        \"mia\": 242,\n        \"nrt\": 343,\n        \"ams\": 211,\n        \"sjc\": 127,\n        \"dfw\": 174,\n        \"otp\": 320,\n        \"lax\": 407,\n        \"cdg\": 185,\n        \"arn\": 193,\n        \"mad\": 344,\n        \"sin\": 433,\n        \"iad\": 190,\n        \"bos\": 108,\n        \"scl\": 461,\n        \"hkg\": 334,\n        \"ewr\": 167,\n        \"bom\": 294,\n        \"eze\": 470\n      },\n      {\n        \"timestamp\": \"2025-08-20T20:30:00.000Z\",\n        \"gig\": 354,\n        \"atl\": 167,\n        \"nrt\": 356,\n        \"mia\": 181,\n        \"dfw\": 194,\n        \"sjc\": 132,\n        \"ams\": 170,\n        \"lax\": 153,\n        \"otp\": 269,\n        \"cdg\": 171,\n        \"gru\": 215,\n        \"sea\": 174,\n        \"ord\": 172,\n        \"den\": 222,\n        \"yul\": 123,\n        \"syd\": 426,\n        \"gdl\": 603,\n        \"phx\": 199,\n        \"jnb\": 455,\n        \"bog\": 422,\n        \"lhr\": 179,\n        \"yyz\": 143,\n        \"fra\": 158,\n        \"scl\": 504,\n        \"hkg\": 334,\n        \"ewr\": 162,\n        \"bom\": 303,\n        \"eze\": 456,\n        \"arn\": 194,\n        \"mad\": 355,\n        \"sin\": 419,\n        \"iad\": 279,\n        \"bos\": 117\n      },\n      {\n        \"timestamp\": \"2025-08-20T21:00:00.000Z\",\n        \"scl\": 505,\n        \"hkg\": 460,\n        \"ewr\": 180,\n        \"bom\": 289,\n        \"eze\": 459,\n        \"arn\": 207,\n        \"mad\": 330,\n        \"sin\": 452,\n        \"iad\": 437,\n        \"bos\": 116,\n        \"gig\": 350,\n        \"atl\": 158,\n        \"mia\": 219,\n        \"nrt\": 415,\n        \"sjc\": 134,\n        \"dfw\": 199,\n        \"ams\": 177,\n        \"lax\": 150,\n        \"otp\": 270,\n        \"cdg\": 169,\n        \"gru\": 194,\n        \"den\": 248,\n        \"ord\": 162,\n        \"sea\": 169,\n        \"yul\": 113,\n        \"syd\": 370,\n        \"phx\": 202,\n        \"gdl\": 564,\n        \"jnb\": 449,\n        \"bog\": 392,\n        \"lhr\": 153,\n        \"yyz\": 146,\n        \"fra\": 154\n      },\n      {\n        \"timestamp\": \"2025-08-20T21:30:00.000Z\",\n        \"sjc\": 169,\n        \"dfw\": 166,\n        \"ams\": 187,\n        \"otp\": 276,\n        \"lax\": 152,\n        \"cdg\": 167,\n        \"gig\": 355,\n        \"atl\": 147,\n        \"mia\": 214,\n        \"nrt\": 434,\n        \"phx\": 218,\n        \"gdl\": 632,\n        \"jnb\": 453,\n        \"bog\": 421,\n        \"lhr\": 188,\n        \"yyz\": 151,\n        \"fra\": 148,\n        \"gru\": 195,\n        \"den\": 241,\n        \"ord\": 178,\n        \"sea\": 163,\n        \"yul\": 119,\n        \"syd\": 330,\n        \"eze\": 390,\n        \"scl\": 497,\n        \"hkg\": 560,\n        \"ewr\": 147,\n        \"bom\": 282,\n        \"iad\": 206,\n        \"bos\": 129,\n        \"arn\": 200,\n        \"mad\": 340,\n        \"sin\": 434\n      },\n      {\n        \"timestamp\": \"2025-08-20T22:00:00.000Z\",\n        \"arn\": 198,\n        \"mad\": 368,\n        \"sin\": 473,\n        \"iad\": 282,\n        \"bos\": 106,\n        \"scl\": 507,\n        \"hkg\": 340,\n        \"bom\": 292,\n        \"ewr\": 134,\n        \"eze\": 446,\n        \"gru\": 190,\n        \"yul\": 110,\n        \"sea\": 175,\n        \"den\": 237,\n        \"ord\": 150,\n        \"syd\": 340,\n        \"jnb\": 465,\n        \"gdl\": 638,\n        \"phx\": 210,\n        \"bog\": 388,\n        \"yyz\": 138,\n        \"lhr\": 139,\n        \"fra\": 154,\n        \"gig\": 385,\n        \"atl\": 142,\n        \"nrt\": 373,\n        \"mia\": 173,\n        \"ams\": 177,\n        \"dfw\": 169,\n        \"sjc\": 143,\n        \"otp\": 282,\n        \"lax\": 176,\n        \"cdg\": 149\n      },\n      {\n        \"timestamp\": \"2025-08-20T22:30:00.000Z\",\n        \"jnb\": 520,\n        \"gdl\": 638,\n        \"phx\": 194,\n        \"bog\": 460,\n        \"yyz\": 195,\n        \"lhr\": 166,\n        \"fra\": 157,\n        \"gru\": 213,\n        \"yul\": 127,\n        \"sea\": 171,\n        \"den\": 239,\n        \"ord\": 197,\n        \"syd\": 330,\n        \"ams\": 177,\n        \"dfw\": 208,\n        \"sjc\": 143,\n        \"lax\": 208,\n        \"otp\": 309,\n        \"cdg\": 155,\n        \"gig\": 355,\n        \"atl\": 154,\n        \"nrt\": 420,\n        \"mia\": 211,\n        \"iad\": 140,\n        \"bos\": 117,\n        \"arn\": 202,\n        \"mad\": 337,\n        \"sin\": 443,\n        \"eze\": 450,\n        \"scl\": 509,\n        \"hkg\": 431,\n        \"bom\": 304,\n        \"ewr\": 144\n      },\n      {\n        \"timestamp\": \"2025-08-20T23:00:00.000Z\",\n        \"lhr\": 142,\n        \"yyz\": 136,\n        \"fra\": 148,\n        \"phx\": 205,\n        \"gdl\": 515,\n        \"jnb\": 396,\n        \"bog\": 474,\n        \"syd\": 426,\n        \"gru\": 205,\n        \"ord\": 149,\n        \"den\": 226,\n        \"sea\": 169,\n        \"yul\": 124,\n        \"cdg\": 157,\n        \"sjc\": 144,\n        \"dfw\": 174,\n        \"ams\": 193,\n        \"otp\": 293,\n        \"lax\": 159,\n        \"mia\": 180,\n        \"nrt\": 353,\n        \"gig\": 373,\n        \"atl\": 176,\n        \"bos\": 146,\n        \"iad\": 370,\n        \"mad\": 344,\n        \"sin\": 424,\n        \"arn\": 187,\n        \"eze\": 387,\n        \"bom\": 312,\n        \"ewr\": 144,\n        \"scl\": 464,\n        \"hkg\": 394\n      },\n      {\n        \"timestamp\": \"2025-08-20T23:30:00.000Z\",\n        \"mad\": 333,\n        \"sin\": 448,\n        \"arn\": 213,\n        \"bos\": 118,\n        \"iad\": 282,\n        \"bom\": 396,\n        \"ewr\": 138,\n        \"scl\": 506,\n        \"hkg\": 460,\n        \"eze\": 451,\n        \"syd\": 369,\n        \"gru\": 199,\n        \"ord\": 137,\n        \"den\": 231,\n        \"sea\": 161,\n        \"yul\": 120,\n        \"lhr\": 147,\n        \"yyz\": 148,\n        \"fra\": 150,\n        \"phx\": 194,\n        \"gdl\": 560,\n        \"jnb\": 365,\n        \"bog\": 513,\n        \"mia\": 174,\n        \"nrt\": 425,\n        \"gig\": 374,\n        \"atl\": 188,\n        \"cdg\": 155,\n        \"sjc\": 172,\n        \"dfw\": 208,\n        \"ams\": 174,\n        \"lax\": 155,\n        \"otp\": 316\n      },\n      {\n        \"timestamp\": \"2025-08-21T00:00:00.000Z\",\n        \"cdg\": 144,\n        \"otp\": 275,\n        \"lax\": 162,\n        \"ams\": 173,\n        \"dfw\": 183,\n        \"sjc\": 138,\n        \"nrt\": 425,\n        \"mia\": 181,\n        \"atl\": 172,\n        \"gig\": 352,\n        \"fra\": 152,\n        \"yyz\": 177,\n        \"lhr\": 157,\n        \"bog\": 481,\n        \"jnb\": 366,\n        \"gdl\": 589,\n        \"phx\": 195,\n        \"syd\": 319,\n        \"yul\": 113,\n        \"sea\": 168,\n        \"ord\": 142,\n        \"den\": 229,\n        \"gru\": 193,\n        \"eze\": 447,\n        \"ewr\": 134,\n        \"bom\": 294,\n        \"hkg\": 341,\n        \"scl\": 453,\n        \"bos\": 129,\n        \"iad\": 423,\n        \"sin\": 414,\n        \"mad\": 322,\n        \"arn\": 190\n      },\n      {\n        \"timestamp\": \"2025-08-21T00:30:00.000Z\",\n        \"ewr\": 145,\n        \"bom\": 329,\n        \"hkg\": 356,\n        \"scl\": 500,\n        \"eze\": 450,\n        \"sin\": 534,\n        \"mad\": 343,\n        \"arn\": 197,\n        \"bos\": 101,\n        \"iad\": 227,\n        \"nrt\": 362,\n        \"mia\": 172,\n        \"atl\": 156,\n        \"gig\": 351,\n        \"cdg\": 161,\n        \"lax\": 147,\n        \"otp\": 263,\n        \"ams\": 173,\n        \"dfw\": 186,\n        \"sjc\": 130,\n        \"syd\": 420,\n        \"yul\": 110,\n        \"sea\": 194,\n        \"ord\": 164,\n        \"den\": 223,\n        \"gru\": 206,\n        \"fra\": 189,\n        \"yyz\": 131,\n        \"lhr\": 144,\n        \"bog\": 386,\n        \"jnb\": 365,\n        \"gdl\": 622,\n        \"phx\": 206\n      },\n      {\n        \"timestamp\": \"2025-08-21T01:00:00.000Z\",\n        \"mia\": 209,\n        \"nrt\": 359,\n        \"atl\": 200,\n        \"gig\": 302,\n        \"cdg\": 147,\n        \"lax\": 152,\n        \"otp\": 278,\n        \"ams\": 203,\n        \"sjc\": 136,\n        \"dfw\": 203,\n        \"syd\": 333,\n        \"yul\": 113,\n        \"den\": 226,\n        \"ord\": 166,\n        \"sea\": 288,\n        \"gru\": 209,\n        \"fra\": 165,\n        \"yyz\": 138,\n        \"lhr\": 141,\n        \"bog\": 506,\n        \"jnb\": 447,\n        \"phx\": 208,\n        \"gdl\": 620,\n        \"ewr\": 183,\n        \"bom\": 346,\n        \"hkg\": 471,\n        \"scl\": 432,\n        \"eze\": 458,\n        \"sin\": 454,\n        \"mad\": 376,\n        \"arn\": 203,\n        \"bos\": 125,\n        \"iad\": 250\n      },\n      {\n        \"timestamp\": \"2025-08-21T01:30:00.000Z\",\n        \"syd\": 411,\n        \"sea\": 173,\n        \"den\": 234,\n        \"ord\": 136,\n        \"yul\": 105,\n        \"gru\": 280,\n        \"fra\": 168,\n        \"lhr\": 182,\n        \"yyz\": 159,\n        \"bog\": 478,\n        \"gdl\": 607,\n        \"phx\": 195,\n        \"jnb\": 511,\n        \"nrt\": 434,\n        \"mia\": 191,\n        \"atl\": 227,\n        \"gig\": 389,\n        \"cdg\": 149,\n        \"otp\": 283,\n        \"lax\": 213,\n        \"dfw\": 244,\n        \"sjc\": 130,\n        \"ams\": 202,\n        \"sin\": 558,\n        \"mad\": 346,\n        \"arn\": 205,\n        \"bos\": 115,\n        \"iad\": 174,\n        \"ewr\": 157,\n        \"bom\": 291,\n        \"hkg\": 357,\n        \"scl\": 513,\n        \"eze\": 385\n      },\n      {\n        \"timestamp\": \"2025-08-21T02:00:00.000Z\",\n        \"fra\": 151,\n        \"lhr\": 149,\n        \"yyz\": 131,\n        \"bog\": 404,\n        \"phx\": 218,\n        \"gdl\": 576,\n        \"jnb\": 458,\n        \"syd\": 314,\n        \"ord\": 189,\n        \"den\": 226,\n        \"sea\": 161,\n        \"yul\": 118,\n        \"gru\": 191,\n        \"cdg\": 142,\n        \"otp\": 269,\n        \"lax\": 168,\n        \"sjc\": 162,\n        \"dfw\": 202,\n        \"ams\": 169,\n        \"mia\": 150,\n        \"nrt\": 418,\n        \"atl\": 249,\n        \"gig\": 348,\n        \"bos\": 104,\n        \"iad\": 213,\n        \"sin\": 564,\n        \"mad\": 361,\n        \"arn\": 203,\n        \"eze\": 566,\n        \"bom\": 303,\n        \"ewr\": 123,\n        \"hkg\": 356,\n        \"scl\": 497\n      },\n      {\n        \"timestamp\": \"2025-08-21T02:30:00.000Z\",\n        \"eze\": 473,\n        \"ewr\": 138,\n        \"bom\": 282,\n        \"scl\": 505,\n        \"hkg\": 324,\n        \"bos\": 123,\n        \"iad\": 188,\n        \"mad\": 338,\n        \"sin\": 419,\n        \"arn\": 206,\n        \"cdg\": 152,\n        \"ams\": 177,\n        \"dfw\": 175,\n        \"sjc\": 133,\n        \"lax\": 157,\n        \"otp\": 273,\n        \"nrt\": 423,\n        \"mia\": 193,\n        \"gig\": 350,\n        \"atl\": 247,\n        \"yyz\": 157,\n        \"lhr\": 236,\n        \"fra\": 152,\n        \"jnb\": 531,\n        \"gdl\": 612,\n        \"phx\": 195,\n        \"bog\": 413,\n        \"syd\": 416,\n        \"gru\": 193,\n        \"yul\": 119,\n        \"sea\": 163,\n        \"den\": 224,\n        \"ord\": 136\n      },\n      {\n        \"timestamp\": \"2025-08-21T03:00:00.000Z\",\n        \"gru\": 204,\n        \"yul\": 188,\n        \"sea\": 172,\n        \"ord\": 152,\n        \"den\": 229,\n        \"syd\": 414,\n        \"jnb\": 521,\n        \"gdl\": 630,\n        \"phx\": 190,\n        \"bog\": 413,\n        \"yyz\": 140,\n        \"lhr\": 144,\n        \"fra\": 201,\n        \"gig\": 347,\n        \"atl\": 386,\n        \"nrt\": 352,\n        \"mia\": 160,\n        \"ams\": 182,\n        \"dfw\": 168,\n        \"sjc\": 138,\n        \"otp\": 264,\n        \"lax\": 150,\n        \"cdg\": 137,\n        \"arn\": 212,\n        \"mad\": 344,\n        \"sin\": 447,\n        \"iad\": 134,\n        \"bos\": 108,\n        \"scl\": 506,\n        \"hkg\": 317,\n        \"ewr\": 127,\n        \"bom\": 321,\n        \"eze\": 431\n      },\n      {\n        \"timestamp\": \"2025-08-21T03:30:00.000Z\",\n        \"gig\": 347,\n        \"atl\": 278,\n        \"mia\": 170,\n        \"nrt\": 375,\n        \"sjc\": 131,\n        \"dfw\": 195,\n        \"ams\": 177,\n        \"lax\": 152,\n        \"otp\": 260,\n        \"cdg\": 175,\n        \"gru\": 270,\n        \"ord\": 155,\n        \"den\": 217,\n        \"sea\": 192,\n        \"yul\": 142,\n        \"syd\": 379,\n        \"phx\": 188,\n        \"gdl\": 594,\n        \"jnb\": 387,\n        \"bog\": 390,\n        \"lhr\": 144,\n        \"yyz\": 160,\n        \"fra\": 198,\n        \"scl\": 504,\n        \"hkg\": 339,\n        \"ewr\": 139,\n        \"bom\": 336,\n        \"eze\": 444,\n        \"arn\": 198,\n        \"mad\": 372,\n        \"sin\": 465,\n        \"iad\": 176,\n        \"bos\": 105\n      },\n      {\n        \"timestamp\": \"2025-08-21T04:00:00.000Z\",\n        \"scl\": 503,\n        \"hkg\": 330,\n        \"ewr\": 186,\n        \"bom\": 286,\n        \"eze\": 446,\n        \"arn\": 197,\n        \"mad\": 325,\n        \"sin\": 450,\n        \"iad\": 168,\n        \"bos\": 113,\n        \"gig\": 351,\n        \"atl\": 323,\n        \"mia\": 162,\n        \"nrt\": 247,\n        \"sjc\": 138,\n        \"dfw\": 172,\n        \"ams\": 232,\n        \"lax\": 153,\n        \"otp\": 281,\n        \"cdg\": 148,\n        \"gru\": 211,\n        \"ord\": 140,\n        \"den\": 235,\n        \"sea\": 155,\n        \"yul\": 112,\n        \"syd\": 403,\n        \"phx\": 194,\n        \"gdl\": 547,\n        \"jnb\": 380,\n        \"bog\": 402,\n        \"lhr\": 215,\n        \"yyz\": 144,\n        \"fra\": 159\n      },\n      {\n        \"timestamp\": \"2025-08-21T04:30:00.000Z\",\n        \"atl\": 240,\n        \"gig\": 376,\n        \"nrt\": 445,\n        \"mia\": 216,\n        \"otp\": 275,\n        \"lax\": 179,\n        \"dfw\": 173,\n        \"sjc\": 157,\n        \"ams\": 235,\n        \"cdg\": 165,\n        \"sea\": 168,\n        \"den\": 220,\n        \"ord\": 174,\n        \"yul\": 110,\n        \"gru\": 202,\n        \"syd\": 440,\n        \"bog\": 437,\n        \"gdl\": 543,\n        \"phx\": 194,\n        \"jnb\": 504,\n        \"fra\": 172,\n        \"lhr\": 175,\n        \"yyz\": 136,\n        \"hkg\": 323,\n        \"scl\": 454,\n        \"ewr\": 140,\n        \"bom\": 304,\n        \"eze\": 460,\n        \"arn\": 197,\n        \"sin\": 400,\n        \"mad\": 466,\n        \"iad\": 156,\n        \"bos\": 137\n      },\n      {\n        \"timestamp\": \"2025-08-21T05:00:00.000Z\",\n        \"hkg\": 388,\n        \"scl\": 506,\n        \"ewr\": 121,\n        \"bom\": 288,\n        \"eze\": 417,\n        \"arn\": 210,\n        \"sin\": 437,\n        \"mad\": 354,\n        \"iad\": 126,\n        \"bos\": 99,\n        \"atl\": 237,\n        \"gig\": 319,\n        \"mia\": 161,\n        \"nrt\": 432,\n        \"otp\": 330,\n        \"lax\": 162,\n        \"sjc\": 134,\n        \"dfw\": 169,\n        \"ams\": 179,\n        \"cdg\": 149,\n        \"ord\": 144,\n        \"den\": 235,\n        \"sea\": 198,\n        \"yul\": 106,\n        \"gru\": 336,\n        \"syd\": 338,\n        \"bog\": 433,\n        \"phx\": 213,\n        \"gdl\": 580,\n        \"jnb\": 519,\n        \"fra\": 156,\n        \"lhr\": 170,\n        \"yyz\": 230\n      },\n      {\n        \"timestamp\": \"2025-08-21T05:30:00.000Z\",\n        \"lax\": 187,\n        \"otp\": 261,\n        \"sjc\": 163,\n        \"dfw\": 178,\n        \"ams\": 229,\n        \"cdg\": 176,\n        \"atl\": 130,\n        \"gig\": 348,\n        \"mia\": 156,\n        \"nrt\": 367,\n        \"bog\": 396,\n        \"phx\": 189,\n        \"gdl\": 648,\n        \"jnb\": 457,\n        \"fra\": 208,\n        \"lhr\": 240,\n        \"yyz\": 145,\n        \"ord\": 140,\n        \"den\": 231,\n        \"sea\": 172,\n        \"yul\": 109,\n        \"gru\": 184,\n        \"syd\": 380,\n        \"eze\": 452,\n        \"hkg\": 334,\n        \"scl\": 500,\n        \"ewr\": 151,\n        \"bom\": 307,\n        \"iad\": 146,\n        \"bos\": 96,\n        \"arn\": 192,\n        \"sin\": 403,\n        \"mad\": 350\n      },\n      {\n        \"timestamp\": \"2025-08-21T06:00:00.000Z\",\n        \"iad\": 138,\n        \"bos\": 117,\n        \"arn\": 302,\n        \"sin\": 415,\n        \"mad\": 331,\n        \"eze\": 454,\n        \"hkg\": 356,\n        \"scl\": 500,\n        \"bom\": 297,\n        \"ewr\": 130,\n        \"bog\": 400,\n        \"jnb\": 476,\n        \"phx\": 193,\n        \"gdl\": 598,\n        \"fra\": 179,\n        \"yyz\": 190,\n        \"lhr\": 153,\n        \"yul\": 113,\n        \"den\": 233,\n        \"ord\": 148,\n        \"sea\": 169,\n        \"gru\": 205,\n        \"syd\": 428,\n        \"lax\": 154,\n        \"otp\": 282,\n        \"ams\": 187,\n        \"sjc\": 130,\n        \"dfw\": 177,\n        \"cdg\": 157,\n        \"atl\": 122,\n        \"gig\": 346,\n        \"mia\": 155,\n        \"nrt\": 246\n      },\n      {\n        \"timestamp\": \"2025-08-21T06:30:00.000Z\",\n        \"yul\": 112,\n        \"den\": 236,\n        \"ord\": 141,\n        \"sea\": 174,\n        \"gru\": 285,\n        \"syd\": 323,\n        \"bog\": 433,\n        \"jnb\": 456,\n        \"phx\": 192,\n        \"gdl\": 612,\n        \"fra\": 221,\n        \"yyz\": 145,\n        \"lhr\": 151,\n        \"atl\": 129,\n        \"gig\": 426,\n        \"mia\": 194,\n        \"nrt\": 436,\n        \"otp\": 277,\n        \"lax\": 165,\n        \"ams\": 240,\n        \"sjc\": 128,\n        \"dfw\": 168,\n        \"cdg\": 248,\n        \"arn\": 197,\n        \"sin\": 432,\n        \"mad\": 382,\n        \"iad\": 145,\n        \"bos\": 161,\n        \"hkg\": 318,\n        \"scl\": 509,\n        \"bom\": 324,\n        \"ewr\": 183,\n        \"eze\": 446\n      },\n      {\n        \"timestamp\": \"2025-08-21T07:00:00.000Z\",\n        \"fra\": 154,\n        \"lhr\": 168,\n        \"yyz\": 157,\n        \"bog\": 399,\n        \"phx\": 273,\n        \"gdl\": 585,\n        \"jnb\": 358,\n        \"syd\": 366,\n        \"den\": 223,\n        \"ord\": 135,\n        \"sea\": 165,\n        \"yul\": 112,\n        \"gru\": 224,\n        \"cdg\": 268,\n        \"lax\": 161,\n        \"otp\": 314,\n        \"sjc\": 131,\n        \"dfw\": 187,\n        \"ams\": 197,\n        \"mia\": 177,\n        \"nrt\": 428,\n        \"atl\": 135,\n        \"gig\": 248,\n        \"bos\": 117,\n        \"iad\": 136,\n        \"sin\": 425,\n        \"mad\": 334,\n        \"arn\": 246,\n        \"eze\": 455,\n        \"bom\": 346,\n        \"ewr\": 155,\n        \"hkg\": 332,\n        \"scl\": 496\n      },\n      {\n        \"timestamp\": \"2025-08-21T07:30:00.000Z\",\n        \"sin\": 452,\n        \"mad\": 341,\n        \"arn\": 183,\n        \"bos\": 181,\n        \"iad\": 141,\n        \"bom\": 318,\n        \"ewr\": 153,\n        \"hkg\": 368,\n        \"scl\": 498,\n        \"eze\": 438,\n        \"syd\": 357,\n        \"den\": 235,\n        \"ord\": 158,\n        \"sea\": 165,\n        \"yul\": 112,\n        \"gru\": 203,\n        \"fra\": 170,\n        \"lhr\": 206,\n        \"yyz\": 222,\n        \"bog\": 480,\n        \"phx\": 204,\n        \"gdl\": 593,\n        \"jnb\": 444,\n        \"mia\": 192,\n        \"nrt\": 426,\n        \"atl\": 140,\n        \"gig\": 287,\n        \"cdg\": 155,\n        \"otp\": 270,\n        \"lax\": 159,\n        \"sjc\": 142,\n        \"dfw\": 180,\n        \"ams\": 174\n      },\n      {\n        \"timestamp\": \"2025-08-21T08:00:00.000Z\",\n        \"bos\": 117,\n        \"iad\": 135,\n        \"sin\": 417,\n        \"mad\": 335,\n        \"arn\": 189,\n        \"eze\": 452,\n        \"ewr\": 135,\n        \"bom\": 295,\n        \"hkg\": 356,\n        \"scl\": 442,\n        \"fra\": 211,\n        \"yyz\": 164,\n        \"lhr\": 147,\n        \"bog\": 435,\n        \"jnb\": 373,\n        \"phx\": 238,\n        \"gdl\": 562,\n        \"syd\": 416,\n        \"yul\": 119,\n        \"ord\": 138,\n        \"den\": 246,\n        \"sea\": 171,\n        \"gru\": 228,\n        \"cdg\": 260,\n        \"otp\": 310,\n        \"lax\": 158,\n        \"ams\": 181,\n        \"sjc\": 187,\n        \"dfw\": 178,\n        \"mia\": 164,\n        \"nrt\": 435,\n        \"atl\": 120,\n        \"gig\": 351\n      },\n      {\n        \"timestamp\": \"2025-08-21T08:30:00.000Z\",\n        \"eze\": 379,\n        \"scl\": 531,\n        \"hkg\": 477,\n        \"ewr\": 170,\n        \"bom\": 291,\n        \"iad\": 135,\n        \"bos\": 104,\n        \"arn\": 258,\n        \"mad\": 334,\n        \"sin\": 412,\n        \"ams\": 191,\n        \"dfw\": 176,\n        \"sjc\": 142,\n        \"lax\": 156,\n        \"otp\": 280,\n        \"cdg\": 194,\n        \"gig\": 348,\n        \"atl\": 177,\n        \"nrt\": 428,\n        \"mia\": 179,\n        \"jnb\": 495,\n        \"gdl\": 587,\n        \"phx\": 217,\n        \"bog\": 467,\n        \"yyz\": 165,\n        \"lhr\": 147,\n        \"fra\": 168,\n        \"gru\": 211,\n        \"yul\": 120,\n        \"sea\": 171,\n        \"ord\": 136,\n        \"den\": 240,\n        \"syd\": 288\n      },\n      {\n        \"timestamp\": \"2025-08-21T09:00:00.000Z\",\n        \"gig\": 369,\n        \"atl\": 142,\n        \"mia\": 150,\n        \"nrt\": 439,\n        \"ams\": 196,\n        \"sjc\": 158,\n        \"dfw\": 200,\n        \"otp\": 274,\n        \"lax\": 177,\n        \"cdg\": 159,\n        \"gru\": 223,\n        \"yul\": 105,\n        \"den\": 338,\n        \"ord\": 180,\n        \"sea\": 159,\n        \"syd\": 392,\n        \"jnb\": 464,\n        \"phx\": 184,\n        \"gdl\": 554,\n        \"bog\": 398,\n        \"yyz\": 151,\n        \"lhr\": 179,\n        \"fra\": 193,\n        \"scl\": 494,\n        \"hkg\": 326,\n        \"bom\": 400,\n        \"ewr\": 142,\n        \"eze\": 454,\n        \"arn\": 219,\n        \"mad\": 377,\n        \"sin\": 425,\n        \"iad\": 152,\n        \"bos\": 124\n      },\n      {\n        \"timestamp\": \"2025-08-21T09:30:00.000Z\",\n        \"eze\": 448,\n        \"hkg\": 458,\n        \"scl\": 504,\n        \"ewr\": 146,\n        \"bom\": 328,\n        \"iad\": 144,\n        \"bos\": 123,\n        \"arn\": 188,\n        \"sin\": 517,\n        \"mad\": 354,\n        \"otp\": 275,\n        \"lax\": 211,\n        \"ams\": 183,\n        \"dfw\": 193,\n        \"sjc\": 176,\n        \"cdg\": 146,\n        \"atl\": 162,\n        \"gig\": 358,\n        \"nrt\": 454,\n        \"mia\": 173,\n        \"bog\": 499,\n        \"jnb\": 371,\n        \"gdl\": 606,\n        \"phx\": 188,\n        \"fra\": 180,\n        \"yyz\": 150,\n        \"lhr\": 312,\n        \"yul\": 116,\n        \"sea\": 175,\n        \"ord\": 151,\n        \"den\": 261,\n        \"gru\": 194,\n        \"syd\": 298\n      },\n      {\n        \"timestamp\": \"2025-08-21T10:00:00.000Z\",\n        \"yyz\": 138,\n        \"lhr\": 141,\n        \"fra\": 169,\n        \"jnb\": 494,\n        \"gdl\": 574,\n        \"phx\": 189,\n        \"bog\": 497,\n        \"syd\": 412,\n        \"gru\": 337,\n        \"yul\": 122,\n        \"sea\": 178,\n        \"den\": 232,\n        \"ord\": 203,\n        \"cdg\": 146,\n        \"ams\": 195,\n        \"dfw\": 180,\n        \"sjc\": 132,\n        \"otp\": 269,\n        \"lax\": 150,\n        \"nrt\": 454,\n        \"mia\": 173,\n        \"gig\": 353,\n        \"atl\": 140,\n        \"bos\": 112,\n        \"iad\": 145,\n        \"mad\": 436,\n        \"sin\": 436,\n        \"arn\": 202,\n        \"eze\": 451,\n        \"bom\": 289,\n        \"ewr\": 149,\n        \"scl\": 499,\n        \"hkg\": 470\n      },\n      {\n        \"timestamp\": \"2025-08-21T10:30:00.000Z\",\n        \"arn\": 195,\n        \"sin\": 506,\n        \"mad\": 325,\n        \"iad\": 211,\n        \"bos\": 120,\n        \"hkg\": 349,\n        \"scl\": 505,\n        \"bom\": 289,\n        \"ewr\": 158,\n        \"eze\": 385,\n        \"sea\": 189,\n        \"den\": 235,\n        \"ord\": 169,\n        \"yul\": 120,\n        \"gru\": 203,\n        \"syd\": 413,\n        \"bog\": 486,\n        \"gdl\": 606,\n        \"phx\": 196,\n        \"jnb\": 454,\n        \"fra\": 158,\n        \"lhr\": 170,\n        \"yyz\": 129,\n        \"atl\": 196,\n        \"gig\": 382,\n        \"nrt\": 423,\n        \"mia\": 187,\n        \"lax\": 170,\n        \"otp\": 284,\n        \"dfw\": 189,\n        \"sjc\": 136,\n        \"ams\": 173,\n        \"cdg\": 142\n      },\n      {\n        \"timestamp\": \"2025-08-21T11:00:00.000Z\",\n        \"gru\": 194,\n        \"sea\": 175,\n        \"den\": 234,\n        \"ord\": 174,\n        \"yul\": 118,\n        \"syd\": 429,\n        \"gdl\": 599,\n        \"phx\": 208,\n        \"jnb\": 461,\n        \"bog\": 459,\n        \"lhr\": 146,\n        \"yyz\": 148,\n        \"fra\": 155,\n        \"gig\": 355,\n        \"atl\": 165,\n        \"nrt\": 428,\n        \"mia\": 172,\n        \"dfw\": 217,\n        \"sjc\": 135,\n        \"ams\": 178,\n        \"otp\": 283,\n        \"lax\": 197,\n        \"cdg\": 211,\n        \"arn\": 186,\n        \"mad\": 349,\n        \"sin\": 460,\n        \"iad\": 144,\n        \"bos\": 122,\n        \"scl\": 511,\n        \"hkg\": 326,\n        \"ewr\": 163,\n        \"bom\": 288,\n        \"eze\": 455\n      },\n      {\n        \"timestamp\": \"2025-08-21T11:30:00.000Z\",\n        \"eze\": 459,\n        \"scl\": 515,\n        \"hkg\": 333,\n        \"ewr\": 144,\n        \"bom\": 351,\n        \"iad\": 161,\n        \"bos\": 133,\n        \"arn\": 192,\n        \"mad\": 331,\n        \"sin\": 470,\n        \"sjc\": 145,\n        \"dfw\": 200,\n        \"ams\": 174,\n        \"otp\": 326,\n        \"lax\": 178,\n        \"cdg\": 155,\n        \"gig\": 395,\n        \"atl\": 144,\n        \"mia\": 173,\n        \"nrt\": 419,\n        \"phx\": 207,\n        \"gdl\": 641,\n        \"jnb\": 456,\n        \"bog\": 409,\n        \"lhr\": 198,\n        \"yyz\": 150,\n        \"fra\": 157,\n        \"gru\": 286,\n        \"ord\": 190,\n        \"den\": 229,\n        \"sea\": 220,\n        \"yul\": 116,\n        \"syd\": 334\n      },\n      {\n        \"timestamp\": \"2025-08-21T12:00:00.000Z\",\n        \"sin\": 471,\n        \"mad\": 363,\n        \"arn\": 237,\n        \"bos\": 107,\n        \"iad\": 234,\n        \"ewr\": 128,\n        \"bom\": 297,\n        \"hkg\": 348,\n        \"scl\": 539,\n        \"eze\": 457,\n        \"syd\": 355,\n        \"sea\": 201,\n        \"ord\": 187,\n        \"den\": 219,\n        \"yul\": 114,\n        \"gru\": 303,\n        \"fra\": 158,\n        \"lhr\": 158,\n        \"yyz\": 170,\n        \"bog\": 461,\n        \"gdl\": 582,\n        \"phx\": 201,\n        \"jnb\": 507,\n        \"nrt\": 436,\n        \"mia\": 185,\n        \"atl\": 135,\n        \"gig\": 361,\n        \"cdg\": 172,\n        \"otp\": 305,\n        \"lax\": 180,\n        \"dfw\": 285,\n        \"sjc\": 136,\n        \"ams\": 238\n      },\n      {\n        \"timestamp\": \"2025-08-21T12:30:00.000Z\",\n        \"jnb\": 430,\n        \"gdl\": 529,\n        \"phx\": 195,\n        \"bog\": 483,\n        \"yyz\": 138,\n        \"lhr\": 211,\n        \"fra\": 169,\n        \"gru\": 208,\n        \"yul\": 128,\n        \"sea\": 185,\n        \"ord\": 181,\n        \"den\": 318,\n        \"syd\": 377,\n        \"ams\": 187,\n        \"dfw\": 186,\n        \"sjc\": 133,\n        \"lax\": 160,\n        \"otp\": 278,\n        \"cdg\": 220,\n        \"gig\": 368,\n        \"atl\": 143,\n        \"nrt\": 432,\n        \"mia\": 177,\n        \"iad\": 172,\n        \"bos\": 121,\n        \"arn\": 234,\n        \"mad\": 343,\n        \"sin\": 456,\n        \"eze\": 465,\n        \"scl\": 383,\n        \"hkg\": 364,\n        \"ewr\": 161,\n        \"bom\": 287\n      },\n      {\n        \"timestamp\": \"2025-08-21T13:00:00.000Z\",\n        \"iad\": 179,\n        \"bos\": 167,\n        \"arn\": 202,\n        \"sin\": 405,\n        \"mad\": 344,\n        \"eze\": 459,\n        \"hkg\": 654,\n        \"scl\": 507,\n        \"bom\": 288,\n        \"ewr\": 136,\n        \"bog\": 402,\n        \"jnb\": 524,\n        \"gdl\": 550,\n        \"phx\": 205,\n        \"fra\": 197,\n        \"yyz\": 159,\n        \"lhr\": 174,\n        \"yul\": 129,\n        \"sea\": 273,\n        \"ord\": 177,\n        \"den\": 237,\n        \"gru\": 268,\n        \"syd\": 358,\n        \"otp\": 286,\n        \"lax\": 163,\n        \"ams\": 189,\n        \"dfw\": 176,\n        \"sjc\": 136,\n        \"cdg\": 316,\n        \"atl\": 139,\n        \"gig\": 373,\n        \"nrt\": 380,\n        \"mia\": 179\n      },\n      {\n        \"timestamp\": \"2025-08-21T13:30:00.000Z\",\n        \"gru\": 292,\n        \"yul\": 125,\n        \"den\": 272,\n        \"ord\": 190,\n        \"sea\": 191,\n        \"syd\": 365,\n        \"jnb\": 522,\n        \"phx\": 197,\n        \"gdl\": 605,\n        \"bog\": 412,\n        \"yyz\": 147,\n        \"lhr\": 214,\n        \"fra\": 173,\n        \"gig\": 350,\n        \"atl\": 147,\n        \"mia\": 173,\n        \"nrt\": 469,\n        \"ams\": 184,\n        \"sjc\": 161,\n        \"dfw\": 176,\n        \"otp\": 295,\n        \"lax\": 212,\n        \"cdg\": 169,\n        \"arn\": 190,\n        \"mad\": 336,\n        \"sin\": 546,\n        \"iad\": 220,\n        \"bos\": 126,\n        \"scl\": 506,\n        \"hkg\": 443,\n        \"ewr\": 147,\n        \"bom\": 291,\n        \"eze\": 470\n      },\n      {\n        \"timestamp\": \"2025-08-21T14:00:00.000Z\",\n        \"eze\": 385,\n        \"bom\": 303,\n        \"ewr\": 169,\n        \"scl\": 504,\n        \"hkg\": 390,\n        \"bos\": 156,\n        \"iad\": 261,\n        \"mad\": 341,\n        \"sin\": 430,\n        \"arn\": 194,\n        \"cdg\": 152,\n        \"ams\": 186,\n        \"dfw\": 207,\n        \"sjc\": 137,\n        \"otp\": 311,\n        \"lax\": 160,\n        \"nrt\": 356,\n        \"mia\": 167,\n        \"gig\": 371,\n        \"atl\": 145,\n        \"yyz\": 177,\n        \"lhr\": 208,\n        \"fra\": 179,\n        \"jnb\": 360,\n        \"gdl\": 605,\n        \"phx\": 187,\n        \"bog\": 456,\n        \"syd\": 414,\n        \"gru\": 225,\n        \"yul\": 134,\n        \"sea\": 199,\n        \"den\": 240,\n        \"ord\": 175\n      },\n      {\n        \"timestamp\": \"2025-08-21T14:30:00.000Z\",\n        \"iad\": 171,\n        \"bos\": 214,\n        \"arn\": 213,\n        \"sin\": 526,\n        \"mad\": 329,\n        \"eze\": 382,\n        \"hkg\": 498,\n        \"scl\": 504,\n        \"bom\": 300,\n        \"ewr\": 139,\n        \"bog\": 476,\n        \"jnb\": 551,\n        \"phx\": 287,\n        \"gdl\": 567,\n        \"fra\": 208,\n        \"yyz\": 136,\n        \"lhr\": 144,\n        \"yul\": 111,\n        \"den\": 242,\n        \"ord\": 139,\n        \"sea\": 170,\n        \"gru\": 194,\n        \"syd\": 404,\n        \"lax\": 164,\n        \"otp\": 269,\n        \"ams\": 176,\n        \"sjc\": 138,\n        \"dfw\": 198,\n        \"cdg\": 159,\n        \"atl\": 144,\n        \"gig\": 354,\n        \"mia\": 182,\n        \"nrt\": 446\n      },\n      {\n        \"timestamp\": \"2025-08-21T15:00:00.000Z\",\n        \"scl\": 550,\n        \"hkg\": 446,\n        \"ewr\": 162,\n        \"bom\": 307,\n        \"eze\": 397,\n        \"arn\": 190,\n        \"mad\": 352,\n        \"sin\": 523,\n        \"iad\": 162,\n        \"bos\": 127,\n        \"gig\": 356,\n        \"atl\": 138,\n        \"nrt\": 266,\n        \"mia\": 190,\n        \"dfw\": 195,\n        \"sjc\": 174,\n        \"ams\": 177,\n        \"otp\": 280,\n        \"lax\": 161,\n        \"cdg\": 161,\n        \"gru\": 345,\n        \"sea\": 166,\n        \"den\": 254,\n        \"ord\": 179,\n        \"yul\": 143,\n        \"syd\": 415,\n        \"gdl\": 548,\n        \"phx\": 238,\n        \"jnb\": 464,\n        \"bog\": 407,\n        \"lhr\": 144,\n        \"yyz\": 173,\n        \"fra\": 171\n      },\n      {\n        \"timestamp\": \"2025-08-21T15:30:00.000Z\",\n        \"syd\": 296,\n        \"sea\": 173,\n        \"ord\": 159,\n        \"den\": 234,\n        \"yul\": 124,\n        \"gru\": 201,\n        \"fra\": 207,\n        \"lhr\": 196,\n        \"yyz\": 159,\n        \"bog\": 424,\n        \"gdl\": 618,\n        \"phx\": 249,\n        \"jnb\": 437,\n        \"nrt\": 400,\n        \"mia\": 166,\n        \"atl\": 174,\n        \"gig\": 350,\n        \"cdg\": 206,\n        \"lax\": 160,\n        \"otp\": 277,\n        \"dfw\": 183,\n        \"sjc\": 130,\n        \"ams\": 195,\n        \"sin\": 502,\n        \"mad\": 383,\n        \"arn\": 228,\n        \"bos\": 125,\n        \"iad\": 133,\n        \"bom\": 368,\n        \"ewr\": 190,\n        \"hkg\": 349,\n        \"scl\": 494,\n        \"eze\": 426\n      },\n      {\n        \"timestamp\": \"2025-08-21T16:00:00.000Z\",\n        \"eze\": 370,\n        \"scl\": 511,\n        \"hkg\": 399,\n        \"ewr\": 147,\n        \"bom\": 309,\n        \"iad\": 247,\n        \"bos\": 113,\n        \"arn\": 188,\n        \"mad\": 369,\n        \"sin\": 440,\n        \"sjc\": 132,\n        \"dfw\": 174,\n        \"ams\": 169,\n        \"otp\": 340,\n        \"lax\": 213,\n        \"cdg\": 207,\n        \"gig\": 351,\n        \"atl\": 140,\n        \"mia\": 170,\n        \"nrt\": 295,\n        \"phx\": 297,\n        \"gdl\": 606,\n        \"jnb\": 443,\n        \"bog\": 476,\n        \"lhr\": 155,\n        \"yyz\": 186,\n        \"fra\": 160,\n        \"gru\": 196,\n        \"ord\": 171,\n        \"den\": 292,\n        \"sea\": 178,\n        \"yul\": 155,\n        \"syd\": 442\n      },\n      {\n        \"timestamp\": \"2025-08-21T16:30:00.000Z\",\n        \"bos\": 137,\n        \"iad\": 160,\n        \"sin\": 434,\n        \"mad\": 336,\n        \"arn\": 263,\n        \"eze\": 305,\n        \"ewr\": 244,\n        \"bom\": 302,\n        \"hkg\": 358,\n        \"scl\": 511,\n        \"fra\": 173,\n        \"lhr\": 172,\n        \"yyz\": 137,\n        \"bog\": 392,\n        \"gdl\": 555,\n        \"phx\": 214,\n        \"jnb\": 409,\n        \"syd\": 337,\n        \"sea\": 182,\n        \"ord\": 148,\n        \"den\": 250,\n        \"yul\": 123,\n        \"gru\": 199,\n        \"cdg\": 150,\n        \"lax\": 165,\n        \"otp\": 296,\n        \"dfw\": 223,\n        \"sjc\": 128,\n        \"ams\": 212,\n        \"nrt\": 429,\n        \"mia\": 168,\n        \"atl\": 151,\n        \"gig\": 358\n      },\n      {\n        \"timestamp\": \"2025-08-21T17:00:00.000Z\",\n        \"bom\": 296,\n        \"ewr\": 213,\n        \"scl\": 504,\n        \"hkg\": 395,\n        \"eze\": 462,\n        \"mad\": 320,\n        \"sin\": 458,\n        \"arn\": 202,\n        \"bos\": 113,\n        \"iad\": 141,\n        \"mia\": 151,\n        \"nrt\": 246,\n        \"gig\": 359,\n        \"atl\": 139,\n        \"cdg\": 175,\n        \"ams\": 184,\n        \"sjc\": 157,\n        \"dfw\": 207,\n        \"otp\": 276,\n        \"lax\": 155,\n        \"syd\": 487,\n        \"gru\": 194,\n        \"yul\": 113,\n        \"ord\": 135,\n        \"den\": 251,\n        \"sea\": 157,\n        \"yyz\": 135,\n        \"lhr\": 156,\n        \"fra\": 154,\n        \"jnb\": 464,\n        \"phx\": 192,\n        \"gdl\": 512,\n        \"bog\": 399\n      },\n      {\n        \"timestamp\": \"2025-08-21T17:30:00.000Z\",\n        \"cdg\": 147,\n        \"otp\": 276,\n        \"lax\": 167,\n        \"ams\": 196,\n        \"dfw\": 167,\n        \"sjc\": 134,\n        \"nrt\": 349,\n        \"mia\": 197,\n        \"atl\": 128,\n        \"gig\": 363,\n        \"fra\": 162,\n        \"yyz\": 136,\n        \"lhr\": 207,\n        \"bog\": 430,\n        \"jnb\": 451,\n        \"gdl\": 612,\n        \"phx\": 198,\n        \"syd\": 317,\n        \"yul\": 115,\n        \"sea\": 178,\n        \"den\": 229,\n        \"ord\": 135,\n        \"gru\": 185,\n        \"eze\": 286,\n        \"ewr\": 145,\n        \"bom\": 293,\n        \"hkg\": 358,\n        \"scl\": 344,\n        \"bos\": 136,\n        \"iad\": 141,\n        \"sin\": 541,\n        \"mad\": 323,\n        \"arn\": 198\n      },\n      {\n        \"timestamp\": \"2025-08-21T18:00:00.000Z\",\n        \"iad\": 147,\n        \"bos\": 111,\n        \"arn\": 228,\n        \"mad\": 387,\n        \"sin\": 545,\n        \"eze\": 385,\n        \"scl\": 490,\n        \"hkg\": 386,\n        \"bom\": 295,\n        \"ewr\": 181,\n        \"jnb\": 514,\n        \"gdl\": 662,\n        \"phx\": 217,\n        \"bog\": 385,\n        \"yyz\": 129,\n        \"lhr\": 158,\n        \"fra\": 173,\n        \"gru\": 189,\n        \"yul\": 108,\n        \"sea\": 294,\n        \"ord\": 174,\n        \"den\": 234,\n        \"syd\": 284,\n        \"ams\": 218,\n        \"dfw\": 193,\n        \"sjc\": 151,\n        \"otp\": 286,\n        \"lax\": 155,\n        \"cdg\": 147,\n        \"gig\": 352,\n        \"atl\": 141,\n        \"nrt\": 423,\n        \"mia\": 177\n      },\n      {\n        \"timestamp\": \"2025-08-21T18:30:00.000Z\",\n        \"bom\": 291,\n        \"ewr\": 157,\n        \"scl\": 497,\n        \"hkg\": 341,\n        \"eze\": 450,\n        \"mad\": 343,\n        \"sin\": 543,\n        \"arn\": 183,\n        \"bos\": 124,\n        \"iad\": 190,\n        \"nrt\": 431,\n        \"mia\": 191,\n        \"gig\": 353,\n        \"atl\": 123,\n        \"cdg\": 151,\n        \"ams\": 190,\n        \"dfw\": 242,\n        \"sjc\": 129,\n        \"lax\": 164,\n        \"otp\": 307,\n        \"syd\": 353,\n        \"gru\": 186,\n        \"yul\": 127,\n        \"sea\": 186,\n        \"den\": 239,\n        \"ord\": 137,\n        \"yyz\": 139,\n        \"lhr\": 138,\n        \"fra\": 182,\n        \"jnb\": 473,\n        \"gdl\": 623,\n        \"phx\": 192,\n        \"bog\": 501\n      },\n      {\n        \"timestamp\": \"2025-08-21T19:00:00.000Z\",\n        \"nrt\": 379,\n        \"mia\": 159,\n        \"atl\": 137,\n        \"gig\": 349,\n        \"cdg\": 153,\n        \"otp\": 278,\n        \"lax\": 155,\n        \"ams\": 190,\n        \"dfw\": 200,\n        \"sjc\": 164,\n        \"syd\": 338,\n        \"yul\": 118,\n        \"sea\": 170,\n        \"den\": 246,\n        \"ord\": 159,\n        \"gru\": 197,\n        \"fra\": 160,\n        \"yyz\": 162,\n        \"lhr\": 236,\n        \"bog\": 408,\n        \"jnb\": 473,\n        \"gdl\": 659,\n        \"phx\": 229,\n        \"ewr\": 147,\n        \"bom\": 290,\n        \"hkg\": 330,\n        \"scl\": 435,\n        \"eze\": 297,\n        \"sin\": 467,\n        \"mad\": 333,\n        \"arn\": 192,\n        \"bos\": 128,\n        \"iad\": 240\n      },\n      {\n        \"timestamp\": \"2025-08-21T19:30:00.000Z\",\n        \"sjc\": 139,\n        \"dfw\": 205,\n        \"ams\": 201,\n        \"lax\": 208,\n        \"otp\": 274,\n        \"cdg\": 152,\n        \"gig\": 377,\n        \"atl\": 144,\n        \"mia\": 159,\n        \"nrt\": 423,\n        \"phx\": 200,\n        \"gdl\": 644,\n        \"jnb\": 493,\n        \"bog\": 442,\n        \"lhr\": 157,\n        \"yyz\": 163,\n        \"fra\": 203,\n        \"gru\": 225,\n        \"ord\": 138,\n        \"den\": 226,\n        \"sea\": 161,\n        \"yul\": 117,\n        \"syd\": 405,\n        \"eze\": 454,\n        \"scl\": 491,\n        \"hkg\": 334,\n        \"bom\": 360,\n        \"ewr\": 137,\n        \"iad\": 132,\n        \"bos\": 117,\n        \"arn\": 250,\n        \"mad\": 346,\n        \"sin\": 425\n      },\n      {\n        \"timestamp\": \"2025-08-21T20:00:00.000Z\",\n        \"syd\": 423,\n        \"sea\": 195,\n        \"ord\": 181,\n        \"den\": 240,\n        \"yul\": 126,\n        \"gru\": 214,\n        \"fra\": 159,\n        \"lhr\": 163,\n        \"yyz\": 133,\n        \"bog\": 396,\n        \"gdl\": 641,\n        \"phx\": 231,\n        \"jnb\": 452,\n        \"nrt\": 370,\n        \"mia\": 167,\n        \"atl\": 143,\n        \"gig\": 378,\n        \"cdg\": 165,\n        \"lax\": 245,\n        \"otp\": 287,\n        \"dfw\": 184,\n        \"sjc\": 137,\n        \"ams\": 174,\n        \"sin\": 456,\n        \"mad\": 359,\n        \"arn\": 214,\n        \"bos\": 102,\n        \"iad\": 146,\n        \"bom\": 282,\n        \"ewr\": 128,\n        \"hkg\": 384,\n        \"scl\": 497,\n        \"eze\": 386\n      },\n      {\n        \"timestamp\": \"2025-08-21T20:30:00.000Z\",\n        \"bog\": 461,\n        \"jnb\": 386,\n        \"gdl\": 722,\n        \"phx\": 197,\n        \"fra\": 168,\n        \"yyz\": 138,\n        \"lhr\": 139,\n        \"yul\": 135,\n        \"sea\": 188,\n        \"den\": 253,\n        \"ord\": 161,\n        \"gru\": 237,\n        \"syd\": 304,\n        \"otp\": 363,\n        \"lax\": 204,\n        \"ams\": 188,\n        \"dfw\": 247,\n        \"sjc\": 163,\n        \"cdg\": 149,\n        \"atl\": 128,\n        \"gig\": 361,\n        \"nrt\": 346,\n        \"mia\": 156,\n        \"iad\": 146,\n        \"bos\": 116,\n        \"arn\": 201,\n        \"sin\": 437,\n        \"mad\": 412,\n        \"eze\": 446,\n        \"hkg\": 334,\n        \"scl\": 492,\n        \"ewr\": 130,\n        \"bom\": 321\n      },\n      {\n        \"timestamp\": \"2025-08-21T21:00:00.000Z\",\n        \"iad\": 138,\n        \"bos\": 119,\n        \"arn\": 231,\n        \"mad\": 368,\n        \"sin\": 438,\n        \"eze\": 459,\n        \"scl\": 426,\n        \"hkg\": 470,\n        \"bom\": 283,\n        \"ewr\": 160,\n        \"jnb\": 447,\n        \"gdl\": 629,\n        \"phx\": 207,\n        \"bog\": 481,\n        \"yyz\": 137,\n        \"lhr\": 141,\n        \"fra\": 153,\n        \"gru\": 205,\n        \"yul\": 111,\n        \"sea\": 245,\n        \"den\": 240,\n        \"ord\": 147,\n        \"syd\": 417,\n        \"ams\": 175,\n        \"dfw\": 210,\n        \"sjc\": 133,\n        \"lax\": 154,\n        \"otp\": 284,\n        \"cdg\": 143,\n        \"gig\": 353,\n        \"atl\": 136,\n        \"nrt\": 352,\n        \"mia\": 174\n      },\n      {\n        \"timestamp\": \"2025-08-21T21:30:00.000Z\",\n        \"yul\": 118,\n        \"ord\": 171,\n        \"den\": 227,\n        \"sea\": 177,\n        \"gru\": 193,\n        \"syd\": 353,\n        \"bog\": 500,\n        \"jnb\": 451,\n        \"phx\": 234,\n        \"gdl\": 666,\n        \"fra\": 158,\n        \"yyz\": 149,\n        \"lhr\": 157,\n        \"atl\": 141,\n        \"gig\": 388,\n        \"mia\": 176,\n        \"nrt\": 421,\n        \"lax\": 157,\n        \"otp\": 298,\n        \"ams\": 202,\n        \"sjc\": 142,\n        \"dfw\": 174,\n        \"cdg\": 156,\n        \"arn\": 197,\n        \"sin\": 460,\n        \"mad\": 361,\n        \"iad\": 172,\n        \"bos\": 117,\n        \"hkg\": 387,\n        \"scl\": 508,\n        \"ewr\": 181,\n        \"bom\": 308,\n        \"eze\": 382\n      },\n      {\n        \"timestamp\": \"2025-08-21T22:00:00.000Z\",\n        \"lhr\": 144,\n        \"yyz\": 131,\n        \"fra\": 155,\n        \"gdl\": 660,\n        \"phx\": 191,\n        \"jnb\": 540,\n        \"bog\": 388,\n        \"syd\": 282,\n        \"gru\": 250,\n        \"sea\": 171,\n        \"den\": 228,\n        \"ord\": 145,\n        \"yul\": 117,\n        \"cdg\": 161,\n        \"dfw\": 179,\n        \"sjc\": 138,\n        \"ams\": 196,\n        \"lax\": 164,\n        \"otp\": 276,\n        \"nrt\": 405,\n        \"mia\": 169,\n        \"gig\": 349,\n        \"atl\": 144,\n        \"bos\": 102,\n        \"iad\": 187,\n        \"mad\": 337,\n        \"sin\": 410,\n        \"arn\": 192,\n        \"eze\": 473,\n        \"bom\": 288,\n        \"ewr\": 148,\n        \"scl\": 499,\n        \"hkg\": 331\n      },\n      {\n        \"timestamp\": \"2025-08-21T22:30:00.000Z\",\n        \"otp\": 290,\n        \"lax\": 218,\n        \"sjc\": 131,\n        \"dfw\": 179,\n        \"ams\": 200,\n        \"cdg\": 246,\n        \"atl\": 135,\n        \"gig\": 350,\n        \"mia\": 156,\n        \"nrt\": 360,\n        \"bog\": 417,\n        \"phx\": 208,\n        \"gdl\": 645,\n        \"jnb\": 518,\n        \"fra\": 184,\n        \"lhr\": 146,\n        \"yyz\": 131,\n        \"den\": 232,\n        \"ord\": 180,\n        \"sea\": 195,\n        \"yul\": 152,\n        \"gru\": 332,\n        \"syd\": 420,\n        \"eze\": 405,\n        \"hkg\": 375,\n        \"scl\": 526,\n        \"bom\": 306,\n        \"ewr\": 165,\n        \"iad\": 198,\n        \"bos\": 114,\n        \"arn\": 201,\n        \"sin\": 444,\n        \"mad\": 339\n      },\n      {\n        \"timestamp\": \"2025-08-21T23:00:00.000Z\",\n        \"hkg\": 374,\n        \"scl\": 488,\n        \"ewr\": 151,\n        \"bom\": 299,\n        \"eze\": 449,\n        \"arn\": 197,\n        \"sin\": 436,\n        \"mad\": 336,\n        \"iad\": 200,\n        \"bos\": 112,\n        \"atl\": 175,\n        \"gig\": 349,\n        \"nrt\": 422,\n        \"mia\": 183,\n        \"lax\": 162,\n        \"otp\": 270,\n        \"dfw\": 177,\n        \"sjc\": 145,\n        \"ams\": 190,\n        \"cdg\": 139,\n        \"sea\": 166,\n        \"ord\": 135,\n        \"den\": 234,\n        \"yul\": 124,\n        \"gru\": 281,\n        \"syd\": 323,\n        \"bog\": 403,\n        \"gdl\": 602,\n        \"phx\": 208,\n        \"jnb\": 370,\n        \"fra\": 179,\n        \"lhr\": 146,\n        \"yyz\": 139\n      },\n      {\n        \"timestamp\": \"2025-08-21T23:30:00.000Z\",\n        \"syd\": 325,\n        \"gru\": 229,\n        \"sea\": 179,\n        \"den\": 234,\n        \"ord\": 200,\n        \"yul\": 117,\n        \"lhr\": 141,\n        \"yyz\": 150,\n        \"fra\": 165,\n        \"gdl\": 508,\n        \"phx\": 196,\n        \"jnb\": 393,\n        \"bog\": 409,\n        \"nrt\": 438,\n        \"mia\": 173,\n        \"gig\": 345,\n        \"atl\": 192,\n        \"cdg\": 206,\n        \"dfw\": 207,\n        \"sjc\": 127,\n        \"ams\": 179,\n        \"otp\": 279,\n        \"lax\": 155,\n        \"mad\": 342,\n        \"sin\": 421,\n        \"arn\": 186,\n        \"bos\": 109,\n        \"iad\": 202,\n        \"bom\": 284,\n        \"ewr\": 233,\n        \"scl\": 501,\n        \"hkg\": 394,\n        \"eze\": 388\n      },\n      {\n        \"timestamp\": \"2025-08-22T00:00:00.000Z\",\n        \"yyz\": 138,\n        \"lhr\": 146,\n        \"fra\": 162,\n        \"jnb\": 477,\n        \"gdl\": 631,\n        \"phx\": 205,\n        \"bog\": 440,\n        \"syd\": 417,\n        \"gru\": 200,\n        \"yul\": 101,\n        \"sea\": 174,\n        \"ord\": 157,\n        \"den\": 252,\n        \"cdg\": 139,\n        \"ams\": 172,\n        \"dfw\": 183,\n        \"sjc\": 132,\n        \"otp\": 299,\n        \"lax\": 159,\n        \"nrt\": 422,\n        \"mia\": 153,\n        \"gig\": 356,\n        \"atl\": 203,\n        \"bos\": 124,\n        \"iad\": 234,\n        \"mad\": 348,\n        \"sin\": 437,\n        \"arn\": 204,\n        \"eze\": 451,\n        \"ewr\": 138,\n        \"bom\": 359,\n        \"scl\": 517,\n        \"hkg\": 394\n      },\n      {\n        \"timestamp\": \"2025-08-22T00:30:00.000Z\",\n        \"mad\": 375,\n        \"sin\": 514,\n        \"arn\": 205,\n        \"bos\": 169,\n        \"iad\": 191,\n        \"ewr\": 141,\n        \"bom\": 327,\n        \"scl\": 495,\n        \"hkg\": 333,\n        \"eze\": 459,\n        \"syd\": 415,\n        \"gru\": 230,\n        \"yul\": 170,\n        \"sea\": 168,\n        \"ord\": 155,\n        \"den\": 227,\n        \"yyz\": 156,\n        \"lhr\": 138,\n        \"fra\": 153,\n        \"jnb\": 484,\n        \"gdl\": 567,\n        \"phx\": 198,\n        \"bog\": 473,\n        \"nrt\": 425,\n        \"mia\": 191,\n        \"gig\": 354,\n        \"atl\": 142,\n        \"cdg\": 150,\n        \"ams\": 177,\n        \"dfw\": 185,\n        \"sjc\": 196,\n        \"lax\": 162,\n        \"otp\": 301\n      },\n      {\n        \"timestamp\": \"2025-08-22T01:00:00.000Z\",\n        \"phx\": 199,\n        \"gdl\": 601,\n        \"jnb\": 515,\n        \"bog\": 448,\n        \"lhr\": 180,\n        \"yyz\": 135,\n        \"fra\": 163,\n        \"gru\": 187,\n        \"den\": 307,\n        \"ord\": 181,\n        \"sea\": 219,\n        \"yul\": 189,\n        \"syd\": 290,\n        \"sjc\": 135,\n        \"dfw\": 195,\n        \"ams\": 191,\n        \"otp\": 284,\n        \"lax\": 149,\n        \"cdg\": 144,\n        \"gig\": 350,\n        \"atl\": 150,\n        \"mia\": 168,\n        \"nrt\": 425,\n        \"iad\": 398,\n        \"bos\": 111,\n        \"arn\": 191,\n        \"mad\": 350,\n        \"sin\": 501,\n        \"eze\": 384,\n        \"scl\": 349,\n        \"hkg\": 359,\n        \"bom\": 287,\n        \"ewr\": 179\n      },\n      {\n        \"timestamp\": \"2025-08-22T01:30:00.000Z\",\n        \"arn\": 193,\n        \"mad\": 339,\n        \"sin\": 544,\n        \"iad\": 164,\n        \"bos\": 117,\n        \"scl\": 507,\n        \"hkg\": 337,\n        \"bom\": 339,\n        \"ewr\": 154,\n        \"eze\": 464,\n        \"gru\": 228,\n        \"den\": 234,\n        \"ord\": 157,\n        \"sea\": 171,\n        \"yul\": 129,\n        \"syd\": 414,\n        \"phx\": 194,\n        \"gdl\": 606,\n        \"jnb\": 373,\n        \"bog\": 500,\n        \"lhr\": 159,\n        \"yyz\": 133,\n        \"fra\": 191,\n        \"gig\": 360,\n        \"atl\": 164,\n        \"mia\": 161,\n        \"nrt\": 436,\n        \"sjc\": 160,\n        \"dfw\": 190,\n        \"ams\": 176,\n        \"lax\": 151,\n        \"otp\": 296,\n        \"cdg\": 159\n      },\n      {\n        \"timestamp\": \"2025-08-22T02:00:00.000Z\",\n        \"nrt\": 259,\n        \"mia\": 161,\n        \"gig\": 360,\n        \"atl\": 139,\n        \"cdg\": 161,\n        \"dfw\": 191,\n        \"sjc\": 131,\n        \"ams\": 182,\n        \"otp\": 279,\n        \"lax\": 184,\n        \"syd\": 372,\n        \"gru\": 266,\n        \"sea\": 165,\n        \"den\": 233,\n        \"ord\": 134,\n        \"yul\": 101,\n        \"lhr\": 147,\n        \"yyz\": 180,\n        \"fra\": 148,\n        \"gdl\": 552,\n        \"phx\": 201,\n        \"jnb\": 519,\n        \"bog\": 440,\n        \"bom\": 298,\n        \"ewr\": 159,\n        \"scl\": 492,\n        \"hkg\": 354,\n        \"eze\": 492,\n        \"mad\": 330,\n        \"sin\": 436,\n        \"arn\": 213,\n        \"bos\": 121,\n        \"iad\": 131\n      },\n      {\n        \"timestamp\": \"2025-08-22T02:30:00.000Z\",\n        \"syd\": 297,\n        \"gru\": 206,\n        \"yul\": 118,\n        \"den\": 239,\n        \"ord\": 284,\n        \"sea\": 172,\n        \"yyz\": 142,\n        \"lhr\": 136,\n        \"fra\": 152,\n        \"jnb\": 414,\n        \"phx\": 191,\n        \"gdl\": 597,\n        \"bog\": 493,\n        \"mia\": 158,\n        \"nrt\": 428,\n        \"gig\": 468,\n        \"atl\": 169,\n        \"cdg\": 166,\n        \"ams\": 180,\n        \"sjc\": 134,\n        \"dfw\": 199,\n        \"lax\": 150,\n        \"otp\": 304,\n        \"mad\": 365,\n        \"sin\": 470,\n        \"arn\": 217,\n        \"bos\": 113,\n        \"iad\": 184,\n        \"bom\": 291,\n        \"ewr\": 124,\n        \"scl\": 431,\n        \"hkg\": 406,\n        \"eze\": 462\n      },\n      {\n        \"timestamp\": \"2025-08-22T03:00:00.000Z\",\n        \"ams\": 166,\n        \"dfw\": 204,\n        \"sjc\": 137,\n        \"otp\": 294,\n        \"lax\": 188,\n        \"cdg\": 143,\n        \"gig\": 441,\n        \"atl\": 152,\n        \"nrt\": 432,\n        \"mia\": 186,\n        \"jnb\": 433,\n        \"gdl\": 621,\n        \"phx\": 193,\n        \"bog\": 473,\n        \"yyz\": 146,\n        \"lhr\": 134,\n        \"fra\": 169,\n        \"gru\": 334,\n        \"yul\": 118,\n        \"sea\": 185,\n        \"den\": 238,\n        \"ord\": 149,\n        \"syd\": 457,\n        \"eze\": 451,\n        \"scl\": 439,\n        \"hkg\": 381,\n        \"ewr\": 123,\n        \"bom\": 300,\n        \"iad\": 206,\n        \"bos\": 109,\n        \"arn\": 220,\n        \"mad\": 345,\n        \"sin\": 473\n      },\n      {\n        \"timestamp\": \"2025-08-22T03:30:00.000Z\",\n        \"eze\": 401,\n        \"hkg\": 375,\n        \"scl\": 496,\n        \"ewr\": 121,\n        \"bom\": 308,\n        \"iad\": 136,\n        \"bos\": 122,\n        \"arn\": 187,\n        \"sin\": 459,\n        \"mad\": 328,\n        \"lax\": 166,\n        \"otp\": 288,\n        \"ams\": 180,\n        \"sjc\": 144,\n        \"dfw\": 313,\n        \"cdg\": 162,\n        \"atl\": 168,\n        \"gig\": 360,\n        \"mia\": 154,\n        \"nrt\": 424,\n        \"bog\": 504,\n        \"jnb\": 367,\n        \"phx\": 186,\n        \"gdl\": 542,\n        \"fra\": 213,\n        \"yyz\": 148,\n        \"lhr\": 158,\n        \"yul\": 111,\n        \"ord\": 141,\n        \"den\": 218,\n        \"sea\": 187,\n        \"gru\": 300,\n        \"syd\": 320\n      },\n      {\n        \"timestamp\": \"2025-08-22T04:00:00.000Z\",\n        \"eze\": 446,\n        \"hkg\": 368,\n        \"scl\": 494,\n        \"bom\": 290,\n        \"ewr\": 153,\n        \"iad\": 139,\n        \"bos\": 105,\n        \"arn\": 210,\n        \"sin\": 465,\n        \"mad\": 347,\n        \"lax\": 154,\n        \"otp\": 272,\n        \"ams\": 175,\n        \"dfw\": 195,\n        \"sjc\": 132,\n        \"cdg\": 188,\n        \"atl\": 276,\n        \"gig\": 359,\n        \"nrt\": 421,\n        \"mia\": 211,\n        \"bog\": 439,\n        \"jnb\": 514,\n        \"gdl\": 626,\n        \"phx\": 293,\n        \"fra\": 157,\n        \"yyz\": 205,\n        \"lhr\": 161,\n        \"yul\": 120,\n        \"sea\": 177,\n        \"den\": 245,\n        \"ord\": 188,\n        \"gru\": 209,\n        \"syd\": 417\n      },\n      {\n        \"timestamp\": \"2025-08-22T04:30:00.000Z\",\n        \"iad\": 166,\n        \"bos\": 148,\n        \"arn\": 209,\n        \"sin\": 438,\n        \"mad\": 415,\n        \"eze\": 454,\n        \"hkg\": 323,\n        \"scl\": 433,\n        \"bom\": 433,\n        \"ewr\": 174,\n        \"bog\": 522,\n        \"phx\": 200,\n        \"gdl\": 563,\n        \"jnb\": 482,\n        \"fra\": 157,\n        \"lhr\": 154,\n        \"yyz\": 130,\n        \"den\": 240,\n        \"ord\": 180,\n        \"sea\": 169,\n        \"yul\": 121,\n        \"gru\": 195,\n        \"syd\": 326,\n        \"otp\": 274,\n        \"lax\": 182,\n        \"sjc\": 157,\n        \"dfw\": 177,\n        \"ams\": 191,\n        \"cdg\": 142,\n        \"atl\": 322,\n        \"gig\": 212,\n        \"mia\": 201,\n        \"nrt\": 421\n      },\n      {\n        \"timestamp\": \"2025-08-22T05:00:00.000Z\",\n        \"ewr\": 130,\n        \"bom\": 307,\n        \"hkg\": 533,\n        \"scl\": 496,\n        \"eze\": 390,\n        \"sin\": 429,\n        \"mad\": 348,\n        \"arn\": 221,\n        \"bos\": 146,\n        \"iad\": 137,\n        \"nrt\": 359,\n        \"mia\": 214,\n        \"atl\": 245,\n        \"gig\": 351,\n        \"cdg\": 209,\n        \"lax\": 249,\n        \"otp\": 275,\n        \"dfw\": 305,\n        \"sjc\": 137,\n        \"ams\": 182,\n        \"syd\": 418,\n        \"sea\": 173,\n        \"den\": 261,\n        \"ord\": 156,\n        \"yul\": 113,\n        \"gru\": 219,\n        \"fra\": 152,\n        \"lhr\": 142,\n        \"yyz\": 131,\n        \"bog\": 442,\n        \"gdl\": 573,\n        \"phx\": 196,\n        \"jnb\": 495\n      },\n      {\n        \"timestamp\": \"2025-08-22T05:30:00.000Z\",\n        \"arn\": 209,\n        \"sin\": 454,\n        \"mad\": 339,\n        \"iad\": 126,\n        \"bos\": 103,\n        \"hkg\": 372,\n        \"scl\": 493,\n        \"ewr\": 169,\n        \"bom\": 303,\n        \"eze\": 455,\n        \"yul\": 104,\n        \"sea\": 179,\n        \"den\": 248,\n        \"ord\": 185,\n        \"gru\": 215,\n        \"syd\": 428,\n        \"bog\": 442,\n        \"jnb\": 445,\n        \"gdl\": 589,\n        \"phx\": 199,\n        \"fra\": 149,\n        \"yyz\": 134,\n        \"lhr\": 147,\n        \"atl\": 193,\n        \"gig\": 354,\n        \"nrt\": 265,\n        \"mia\": 181,\n        \"lax\": 149,\n        \"otp\": 282,\n        \"ams\": 174,\n        \"dfw\": 183,\n        \"sjc\": 146,\n        \"cdg\": 142\n      },\n      {\n        \"timestamp\": \"2025-08-22T06:00:00.000Z\",\n        \"mia\": 172,\n        \"nrt\": 358,\n        \"atl\": 181,\n        \"gig\": 353,\n        \"cdg\": 168,\n        \"otp\": 278,\n        \"lax\": 160,\n        \"ams\": 176,\n        \"sjc\": 146,\n        \"dfw\": 171,\n        \"syd\": 303,\n        \"yul\": 123,\n        \"den\": 244,\n        \"ord\": 148,\n        \"sea\": 176,\n        \"gru\": 276,\n        \"fra\": 264,\n        \"yyz\": 160,\n        \"lhr\": 145,\n        \"bog\": 517,\n        \"jnb\": 468,\n        \"phx\": 232,\n        \"gdl\": 569,\n        \"ewr\": 207,\n        \"bom\": 439,\n        \"hkg\": 346,\n        \"scl\": 498,\n        \"eze\": 454,\n        \"sin\": 480,\n        \"mad\": 360,\n        \"arn\": 212,\n        \"bos\": 162,\n        \"iad\": 165\n      },\n      {\n        \"timestamp\": \"2025-08-22T06:30:00.000Z\",\n        \"eze\": 457,\n        \"ewr\": 131,\n        \"bom\": 297,\n        \"hkg\": 373,\n        \"scl\": 505,\n        \"bos\": 115,\n        \"iad\": 137,\n        \"sin\": 430,\n        \"mad\": 342,\n        \"arn\": 238,\n        \"cdg\": 139,\n        \"lax\": 162,\n        \"otp\": 273,\n        \"ams\": 181,\n        \"sjc\": 140,\n        \"dfw\": 197,\n        \"mia\": 218,\n        \"nrt\": 445,\n        \"atl\": 178,\n        \"gig\": 352,\n        \"fra\": 155,\n        \"yyz\": 151,\n        \"lhr\": 151,\n        \"bog\": 418,\n        \"jnb\": 540,\n        \"phx\": 210,\n        \"gdl\": 636,\n        \"syd\": 292,\n        \"yul\": 112,\n        \"den\": 235,\n        \"ord\": 133,\n        \"sea\": 252,\n        \"gru\": 237\n      },\n      {\n        \"timestamp\": \"2025-08-22T07:00:00.000Z\",\n        \"otp\": 281,\n        \"lax\": 234,\n        \"sjc\": 142,\n        \"dfw\": 173,\n        \"ams\": 198,\n        \"cdg\": 222,\n        \"atl\": 123,\n        \"gig\": 349,\n        \"mia\": 173,\n        \"nrt\": 394,\n        \"bog\": 435,\n        \"phx\": 198,\n        \"gdl\": 569,\n        \"jnb\": 526,\n        \"fra\": 171,\n        \"lhr\": 150,\n        \"yyz\": 142,\n        \"den\": 218,\n        \"ord\": 222,\n        \"sea\": 182,\n        \"yul\": 105,\n        \"gru\": 185,\n        \"syd\": 371,\n        \"eze\": 478,\n        \"hkg\": 394,\n        \"scl\": 503,\n        \"bom\": 315,\n        \"ewr\": 133,\n        \"iad\": 129,\n        \"bos\": 107,\n        \"arn\": 197,\n        \"sin\": 468,\n        \"mad\": 455\n      },\n      {\n        \"timestamp\": \"2025-08-22T07:30:00.000Z\",\n        \"hkg\": 429,\n        \"scl\": 500,\n        \"bom\": 329,\n        \"ewr\": 149,\n        \"eze\": 380,\n        \"arn\": 200,\n        \"sin\": 458,\n        \"mad\": 343,\n        \"iad\": 216,\n        \"bos\": 113,\n        \"atl\": 156,\n        \"gig\": 353,\n        \"mia\": 239,\n        \"nrt\": 426,\n        \"lax\": 147,\n        \"otp\": 284,\n        \"sjc\": 129,\n        \"dfw\": 233,\n        \"ams\": 178,\n        \"cdg\": 179,\n        \"den\": 224,\n        \"ord\": 176,\n        \"sea\": 287,\n        \"yul\": 113,\n        \"gru\": 210,\n        \"syd\": 415,\n        \"bog\": 417,\n        \"phx\": 203,\n        \"gdl\": 569,\n        \"jnb\": 444,\n        \"fra\": 162,\n        \"lhr\": 152,\n        \"yyz\": 170\n      },\n      {\n        \"timestamp\": \"2025-08-22T08:00:00.000Z\",\n        \"iad\": 173,\n        \"bos\": 110,\n        \"arn\": 204,\n        \"mad\": 359,\n        \"sin\": 516,\n        \"eze\": 464,\n        \"scl\": 492,\n        \"hkg\": 355,\n        \"bom\": 294,\n        \"ewr\": 159,\n        \"jnb\": 511,\n        \"phx\": 198,\n        \"gdl\": 600,\n        \"bog\": 444,\n        \"yyz\": 150,\n        \"lhr\": 155,\n        \"fra\": 165,\n        \"gru\": 210,\n        \"yul\": 217,\n        \"ord\": 171,\n        \"den\": 233,\n        \"sea\": 192,\n        \"syd\": 311,\n        \"ams\": 206,\n        \"sjc\": 142,\n        \"dfw\": 235,\n        \"otp\": 273,\n        \"lax\": 151,\n        \"cdg\": 148,\n        \"gig\": 354,\n        \"atl\": 191,\n        \"mia\": 170,\n        \"nrt\": 354\n      },\n      {\n        \"timestamp\": \"2025-08-22T08:30:00.000Z\",\n        \"gru\": 273,\n        \"yul\": 103,\n        \"ord\": 174,\n        \"den\": 242,\n        \"sea\": 173,\n        \"syd\": 328,\n        \"jnb\": 480,\n        \"phx\": 212,\n        \"gdl\": 620,\n        \"bog\": 549,\n        \"yyz\": 135,\n        \"lhr\": 149,\n        \"fra\": 169,\n        \"gig\": 380,\n        \"atl\": 140,\n        \"mia\": 161,\n        \"nrt\": 358,\n        \"ams\": 189,\n        \"sjc\": 157,\n        \"dfw\": 193,\n        \"lax\": 190,\n        \"otp\": 284,\n        \"cdg\": 189,\n        \"arn\": 205,\n        \"mad\": 393,\n        \"sin\": 461,\n        \"iad\": 145,\n        \"bos\": 138,\n        \"scl\": 498,\n        \"hkg\": 334,\n        \"bom\": 296,\n        \"ewr\": 143,\n        \"eze\": 458\n      },\n      {\n        \"timestamp\": \"2025-08-22T09:00:00.000Z\",\n        \"mad\": 362,\n        \"sin\": 480,\n        \"arn\": 196,\n        \"bos\": 112,\n        \"iad\": 149,\n        \"ewr\": 143,\n        \"bom\": 356,\n        \"scl\": 494,\n        \"hkg\": 469,\n        \"eze\": 466,\n        \"syd\": 299,\n        \"gru\": 222,\n        \"ord\": 178,\n        \"den\": 232,\n        \"sea\": 172,\n        \"yul\": 120,\n        \"lhr\": 144,\n        \"yyz\": 136,\n        \"fra\": 161,\n        \"phx\": 192,\n        \"gdl\": 499,\n        \"jnb\": 397,\n        \"bog\": 454,\n        \"mia\": 181,\n        \"nrt\": 420,\n        \"gig\": 367,\n        \"atl\": 170,\n        \"cdg\": 160,\n        \"sjc\": 131,\n        \"dfw\": 206,\n        \"ams\": 207,\n        \"otp\": 279,\n        \"lax\": 167\n      },\n      {\n        \"timestamp\": \"2025-08-22T09:30:00.000Z\",\n        \"lhr\": 168,\n        \"yyz\": 135,\n        \"fra\": 178,\n        \"phx\": 200,\n        \"gdl\": 626,\n        \"jnb\": 501,\n        \"bog\": 417,\n        \"syd\": 424,\n        \"gru\": 203,\n        \"ord\": 144,\n        \"den\": 239,\n        \"sea\": 158,\n        \"yul\": 113,\n        \"cdg\": 172,\n        \"sjc\": 134,\n        \"dfw\": 199,\n        \"ams\": 189,\n        \"lax\": 163,\n        \"otp\": 288,\n        \"mia\": 154,\n        \"nrt\": 359,\n        \"gig\": 328,\n        \"atl\": 124,\n        \"bos\": 110,\n        \"iad\": 150,\n        \"mad\": 348,\n        \"sin\": 422,\n        \"arn\": 197,\n        \"eze\": 476,\n        \"ewr\": 147,\n        \"bom\": 296,\n        \"scl\": 444,\n        \"hkg\": 396\n      },\n      {\n        \"timestamp\": \"2025-08-22T10:00:00.000Z\",\n        \"eze\": 456,\n        \"scl\": 495,\n        \"hkg\": 338,\n        \"ewr\": 132,\n        \"bom\": 310,\n        \"iad\": 212,\n        \"bos\": 115,\n        \"arn\": 213,\n        \"mad\": 344,\n        \"sin\": 486,\n        \"dfw\": 167,\n        \"sjc\": 128,\n        \"ams\": 184,\n        \"otp\": 317,\n        \"lax\": 164,\n        \"cdg\": 230,\n        \"gig\": 362,\n        \"atl\": 169,\n        \"nrt\": 420,\n        \"mia\": 152,\n        \"gdl\": 504,\n        \"phx\": 201,\n        \"jnb\": 499,\n        \"bog\": 396,\n        \"lhr\": 156,\n        \"yyz\": 133,\n        \"fra\": 162,\n        \"gru\": 278,\n        \"sea\": 188,\n        \"ord\": 167,\n        \"den\": 230,\n        \"yul\": 113,\n        \"syd\": 285\n      },\n      {\n        \"timestamp\": \"2025-08-22T10:30:00.000Z\",\n        \"arn\": 246,\n        \"mad\": 358,\n        \"sin\": 420,\n        \"iad\": 164,\n        \"bos\": 122,\n        \"scl\": 498,\n        \"hkg\": 411,\n        \"ewr\": 158,\n        \"bom\": 301,\n        \"eze\": 450,\n        \"gru\": 340,\n        \"yul\": 105,\n        \"sea\": 161,\n        \"den\": 257,\n        \"ord\": 221,\n        \"syd\": 366,\n        \"jnb\": 495,\n        \"gdl\": 575,\n        \"phx\": 197,\n        \"bog\": 438,\n        \"yyz\": 139,\n        \"lhr\": 273,\n        \"fra\": 166,\n        \"gig\": 296,\n        \"atl\": 140,\n        \"nrt\": 449,\n        \"mia\": 171,\n        \"ams\": 200,\n        \"dfw\": 206,\n        \"sjc\": 170,\n        \"lax\": 206,\n        \"otp\": 274,\n        \"cdg\": 155\n      },\n      {\n        \"timestamp\": \"2025-08-22T11:00:00.000Z\",\n        \"eze\": 384,\n        \"bom\": 295,\n        \"ewr\": 139,\n        \"scl\": 508,\n        \"hkg\": 493,\n        \"bos\": 108,\n        \"iad\": 228,\n        \"mad\": 330,\n        \"sin\": 514,\n        \"arn\": 207,\n        \"cdg\": 163,\n        \"ams\": 208,\n        \"sjc\": 140,\n        \"dfw\": 222,\n        \"otp\": 276,\n        \"lax\": 150,\n        \"mia\": 165,\n        \"nrt\": 442,\n        \"gig\": 363,\n        \"atl\": 214,\n        \"yyz\": 150,\n        \"lhr\": 152,\n        \"fra\": 174,\n        \"jnb\": 521,\n        \"phx\": 200,\n        \"gdl\": 578,\n        \"bog\": 463,\n        \"syd\": 388,\n        \"gru\": 274,\n        \"yul\": 153,\n        \"den\": 284,\n        \"ord\": 174,\n        \"sea\": 223\n      },\n      {\n        \"timestamp\": \"2025-08-22T11:30:00.000Z\",\n        \"bos\": 126,\n        \"iad\": 265,\n        \"mad\": 347,\n        \"sin\": 467,\n        \"arn\": 203,\n        \"eze\": 483,\n        \"bom\": 321,\n        \"ewr\": 135,\n        \"scl\": 505,\n        \"hkg\": 462,\n        \"lhr\": 149,\n        \"yyz\": 134,\n        \"fra\": 232,\n        \"gdl\": 587,\n        \"phx\": 274,\n        \"jnb\": 479,\n        \"bog\": 540,\n        \"syd\": 297,\n        \"gru\": 207,\n        \"sea\": 183,\n        \"den\": 250,\n        \"ord\": 176,\n        \"yul\": 115,\n        \"cdg\": 285,\n        \"dfw\": 195,\n        \"sjc\": 135,\n        \"ams\": 253,\n        \"lax\": 375,\n        \"otp\": 326,\n        \"nrt\": 445,\n        \"mia\": 179,\n        \"gig\": 374,\n        \"atl\": 160\n      },\n      {\n        \"timestamp\": \"2025-08-22T12:00:00.000Z\",\n        \"bos\": 124,\n        \"iad\": 358,\n        \"mad\": 368,\n        \"sin\": 490,\n        \"arn\": 198,\n        \"eze\": 454,\n        \"ewr\": 175,\n        \"bom\": 291,\n        \"scl\": 394,\n        \"hkg\": 405,\n        \"lhr\": 149,\n        \"yyz\": 129,\n        \"fra\": 168,\n        \"phx\": 191,\n        \"gdl\": 564,\n        \"jnb\": 374,\n        \"bog\": 447,\n        \"syd\": 423,\n        \"gru\": 312,\n        \"ord\": 171,\n        \"den\": 245,\n        \"sea\": 174,\n        \"yul\": 112,\n        \"cdg\": 150,\n        \"sjc\": 149,\n        \"dfw\": 179,\n        \"ams\": 192,\n        \"lax\": 168,\n        \"otp\": 277,\n        \"mia\": 225,\n        \"nrt\": 279,\n        \"gig\": 370,\n        \"atl\": 128\n      },\n      {\n        \"timestamp\": \"2025-08-22T12:30:00.000Z\",\n        \"fra\": 159,\n        \"lhr\": 144,\n        \"yyz\": 145,\n        \"bog\": 442,\n        \"gdl\": 620,\n        \"phx\": 208,\n        \"jnb\": 509,\n        \"syd\": 418,\n        \"sea\": 165,\n        \"den\": 325,\n        \"ord\": 152,\n        \"yul\": 136,\n        \"gru\": 202,\n        \"cdg\": 166,\n        \"otp\": 278,\n        \"lax\": 157,\n        \"dfw\": 183,\n        \"sjc\": 152,\n        \"ams\": 243,\n        \"nrt\": 445,\n        \"mia\": 152,\n        \"atl\": 156,\n        \"gig\": 473,\n        \"bos\": 115,\n        \"iad\": 180,\n        \"sin\": 505,\n        \"mad\": 333,\n        \"arn\": 190,\n        \"eze\": 425,\n        \"ewr\": 142,\n        \"bom\": 297,\n        \"hkg\": 335,\n        \"scl\": 495\n      },\n      {\n        \"timestamp\": \"2025-08-22T13:00:00.000Z\",\n        \"atl\": 126,\n        \"gig\": 367,\n        \"mia\": 163,\n        \"nrt\": 308,\n        \"lax\": 216,\n        \"otp\": 277,\n        \"sjc\": 168,\n        \"dfw\": 209,\n        \"ams\": 194,\n        \"cdg\": 179,\n        \"den\": 250,\n        \"ord\": 186,\n        \"sea\": 179,\n        \"yul\": 115,\n        \"gru\": 230,\n        \"syd\": 420,\n        \"bog\": 526,\n        \"phx\": 204,\n        \"gdl\": 623,\n        \"jnb\": 489,\n        \"fra\": 163,\n        \"lhr\": 171,\n        \"yyz\": 152,\n        \"hkg\": 543,\n        \"scl\": 496,\n        \"bom\": 298,\n        \"ewr\": 164,\n        \"eze\": 462,\n        \"arn\": 220,\n        \"sin\": 444,\n        \"mad\": 387,\n        \"iad\": 138,\n        \"bos\": 124\n      },\n      {\n        \"timestamp\": \"2025-08-22T13:30:00.000Z\",\n        \"yul\": 114,\n        \"sea\": 181,\n        \"den\": 229,\n        \"ord\": 160,\n        \"gru\": 315,\n        \"syd\": 464,\n        \"bog\": 461,\n        \"jnb\": 449,\n        \"gdl\": 562,\n        \"phx\": 210,\n        \"fra\": 175,\n        \"yyz\": 147,\n        \"lhr\": 144,\n        \"atl\": 151,\n        \"gig\": 468,\n        \"nrt\": 319,\n        \"mia\": 166,\n        \"otp\": 289,\n        \"lax\": 213,\n        \"ams\": 213,\n        \"dfw\": 221,\n        \"sjc\": 163,\n        \"cdg\": 187,\n        \"arn\": 201,\n        \"sin\": 551,\n        \"mad\": 391,\n        \"iad\": 231,\n        \"bos\": 120,\n        \"hkg\": 357,\n        \"scl\": 451,\n        \"bom\": 304,\n        \"ewr\": 173,\n        \"eze\": 470\n      },\n      {\n        \"timestamp\": \"2025-08-22T14:00:00.000Z\",\n        \"bom\": 308,\n        \"ewr\": 139,\n        \"hkg\": 377,\n        \"scl\": 496,\n        \"eze\": 402,\n        \"sin\": 468,\n        \"mad\": 519,\n        \"arn\": 326,\n        \"bos\": 132,\n        \"iad\": 273,\n        \"mia\": 177,\n        \"nrt\": 450,\n        \"atl\": 135,\n        \"gig\": 356,\n        \"cdg\": 234,\n        \"lax\": 188,\n        \"otp\": 288,\n        \"ams\": 188,\n        \"sjc\": 188,\n        \"dfw\": 317,\n        \"syd\": 352,\n        \"yul\": 131,\n        \"den\": 272,\n        \"ord\": 138,\n        \"sea\": 177,\n        \"gru\": 284,\n        \"fra\": 185,\n        \"yyz\": 237,\n        \"lhr\": 144,\n        \"bog\": 437,\n        \"jnb\": 458,\n        \"phx\": 260,\n        \"gdl\": 643\n      },\n      {\n        \"timestamp\": \"2025-08-22T14:30:00.000Z\",\n        \"cdg\": 145,\n        \"otp\": 275,\n        \"lax\": 161,\n        \"ams\": 193,\n        \"sjc\": 138,\n        \"dfw\": 174,\n        \"mia\": 165,\n        \"nrt\": 434,\n        \"atl\": 150,\n        \"gig\": 384,\n        \"fra\": 166,\n        \"yyz\": 181,\n        \"lhr\": 173,\n        \"bog\": 516,\n        \"jnb\": 364,\n        \"phx\": 210,\n        \"gdl\": 580,\n        \"syd\": 440,\n        \"yul\": 112,\n        \"den\": 231,\n        \"ord\": 162,\n        \"sea\": 176,\n        \"gru\": 215,\n        \"eze\": 466,\n        \"bom\": 297,\n        \"ewr\": 197,\n        \"hkg\": 496,\n        \"scl\": 510,\n        \"bos\": 130,\n        \"iad\": 443,\n        \"sin\": 523,\n        \"mad\": 368,\n        \"arn\": 219\n      },\n      {\n        \"timestamp\": \"2025-08-22T15:00:00.000Z\",\n        \"sjc\": 141,\n        \"dfw\": 203,\n        \"ams\": 214,\n        \"lax\": 167,\n        \"otp\": 268,\n        \"cdg\": 151,\n        \"gig\": 291,\n        \"atl\": 144,\n        \"mia\": 158,\n        \"nrt\": 425,\n        \"phx\": 197,\n        \"gdl\": 573,\n        \"jnb\": 373,\n        \"bog\": 440,\n        \"lhr\": 189,\n        \"yyz\": 159,\n        \"fra\": 160,\n        \"gru\": 284,\n        \"ord\": 166,\n        \"den\": 238,\n        \"sea\": 180,\n        \"yul\": 166,\n        \"syd\": 337,\n        \"eze\": 458,\n        \"scl\": 505,\n        \"hkg\": 411,\n        \"bom\": 285,\n        \"ewr\": 168,\n        \"iad\": 185,\n        \"bos\": 127,\n        \"arn\": 188,\n        \"mad\": 362,\n        \"sin\": 412\n      },\n      {\n        \"timestamp\": \"2025-08-22T15:30:00.000Z\",\n        \"scl\": 497,\n        \"hkg\": 367,\n        \"bom\": 332,\n        \"ewr\": 176,\n        \"eze\": 449,\n        \"arn\": 192,\n        \"mad\": 378,\n        \"sin\": 489,\n        \"iad\": 177,\n        \"bos\": 121,\n        \"gig\": 356,\n        \"atl\": 154,\n        \"mia\": 189,\n        \"nrt\": 432,\n        \"sjc\": 144,\n        \"dfw\": 208,\n        \"ams\": 195,\n        \"otp\": 284,\n        \"lax\": 162,\n        \"cdg\": 171,\n        \"gru\": 193,\n        \"ord\": 166,\n        \"den\": 238,\n        \"sea\": 189,\n        \"yul\": 151,\n        \"syd\": 318,\n        \"phx\": 192,\n        \"gdl\": 626,\n        \"jnb\": 495,\n        \"bog\": 465,\n        \"lhr\": 150,\n        \"yyz\": 151,\n        \"fra\": 167\n      },\n      {\n        \"timestamp\": \"2025-08-22T16:00:00.000Z\",\n        \"eze\": 463,\n        \"scl\": 496,\n        \"hkg\": 364,\n        \"ewr\": 137,\n        \"bom\": 296,\n        \"iad\": 161,\n        \"bos\": 127,\n        \"arn\": 204,\n        \"mad\": 378,\n        \"sin\": 527,\n        \"ams\": 184,\n        \"sjc\": 132,\n        \"dfw\": 210,\n        \"otp\": 323,\n        \"lax\": 172,\n        \"cdg\": 171,\n        \"gig\": 359,\n        \"atl\": 149,\n        \"mia\": 165,\n        \"nrt\": 430,\n        \"jnb\": 458,\n        \"phx\": 212,\n        \"gdl\": 618,\n        \"bog\": 406,\n        \"yyz\": 154,\n        \"lhr\": 155,\n        \"fra\": 159,\n        \"gru\": 195,\n        \"yul\": 125,\n        \"den\": 245,\n        \"ord\": 157,\n        \"sea\": 178,\n        \"syd\": 331\n      },\n      {\n        \"timestamp\": \"2025-08-22T16:30:00.000Z\",\n        \"fra\": 162,\n        \"yyz\": 129,\n        \"lhr\": 170,\n        \"bog\": 438,\n        \"jnb\": 389,\n        \"phx\": 204,\n        \"gdl\": 623,\n        \"syd\": 376,\n        \"yul\": 111,\n        \"ord\": 151,\n        \"den\": 230,\n        \"sea\": 224,\n        \"gru\": 187,\n        \"cdg\": 179,\n        \"lax\": 157,\n        \"otp\": 273,\n        \"ams\": 202,\n        \"sjc\": 140,\n        \"dfw\": 197,\n        \"mia\": 160,\n        \"nrt\": 354,\n        \"atl\": 138,\n        \"gig\": 371,\n        \"bos\": 116,\n        \"iad\": 145,\n        \"sin\": 480,\n        \"mad\": 365,\n        \"arn\": 201,\n        \"eze\": 450,\n        \"bom\": 290,\n        \"ewr\": 189,\n        \"hkg\": 490,\n        \"scl\": 524\n      },\n      {\n        \"timestamp\": \"2025-08-22T17:00:00.000Z\",\n        \"sin\": 455,\n        \"mad\": 376,\n        \"arn\": 222,\n        \"bos\": 126,\n        \"iad\": 415,\n        \"ewr\": 131,\n        \"bom\": 301,\n        \"hkg\": 508,\n        \"scl\": 499,\n        \"eze\": 454,\n        \"syd\": 381,\n        \"yul\": 108,\n        \"sea\": 180,\n        \"den\": 248,\n        \"ord\": 143,\n        \"gru\": 309,\n        \"fra\": 162,\n        \"yyz\": 146,\n        \"lhr\": 147,\n        \"bog\": 419,\n        \"jnb\": 453,\n        \"gdl\": 620,\n        \"phx\": 197,\n        \"nrt\": 468,\n        \"mia\": 161,\n        \"atl\": 138,\n        \"gig\": 440,\n        \"cdg\": 187,\n        \"otp\": 268,\n        \"lax\": 158,\n        \"ams\": 185,\n        \"dfw\": 217,\n        \"sjc\": 149\n      },\n      {\n        \"timestamp\": \"2025-08-22T17:30:00.000Z\",\n        \"scl\": 499,\n        \"hkg\": 441,\n        \"ewr\": 143,\n        \"bom\": 293,\n        \"eze\": 407,\n        \"arn\": 203,\n        \"mad\": 375,\n        \"sin\": 407,\n        \"iad\": 316,\n        \"bos\": 111,\n        \"gig\": 356,\n        \"atl\": 143,\n        \"mia\": 285,\n        \"nrt\": 433,\n        \"ams\": 197,\n        \"sjc\": 150,\n        \"dfw\": 183,\n        \"lax\": 166,\n        \"otp\": 271,\n        \"cdg\": 184,\n        \"gru\": 206,\n        \"yul\": 131,\n        \"den\": 242,\n        \"ord\": 163,\n        \"sea\": 165,\n        \"syd\": 419,\n        \"jnb\": 444,\n        \"phx\": 196,\n        \"gdl\": 637,\n        \"bog\": 521,\n        \"yyz\": 144,\n        \"lhr\": 160,\n        \"fra\": 179\n      },\n      {\n        \"timestamp\": \"2025-08-22T18:00:00.000Z\",\n        \"eze\": 461,\n        \"bom\": 294,\n        \"ewr\": 163,\n        \"hkg\": 374,\n        \"scl\": 498,\n        \"bos\": 119,\n        \"iad\": 171,\n        \"sin\": 433,\n        \"mad\": 346,\n        \"arn\": 196,\n        \"cdg\": 160,\n        \"lax\": 191,\n        \"otp\": 279,\n        \"dfw\": 173,\n        \"sjc\": 161,\n        \"ams\": 203,\n        \"nrt\": 358,\n        \"mia\": 272,\n        \"atl\": 217,\n        \"gig\": 373,\n        \"fra\": 208,\n        \"lhr\": 164,\n        \"yyz\": 177,\n        \"bog\": 396,\n        \"gdl\": 588,\n        \"phx\": 203,\n        \"jnb\": 381,\n        \"syd\": 299,\n        \"sea\": 199,\n        \"ord\": 145,\n        \"den\": 247,\n        \"yul\": 119,\n        \"gru\": 229\n      },\n      {\n        \"timestamp\": \"2025-08-22T18:30:00.000Z\",\n        \"mia\": 225,\n        \"nrt\": 362,\n        \"gig\": 346,\n        \"atl\": 132,\n        \"cdg\": 175,\n        \"sjc\": 153,\n        \"dfw\": 206,\n        \"ams\": 202,\n        \"lax\": 170,\n        \"otp\": 291,\n        \"syd\": 455,\n        \"gru\": 229,\n        \"den\": 261,\n        \"ord\": 186,\n        \"sea\": 181,\n        \"yul\": 122,\n        \"lhr\": 142,\n        \"yyz\": 142,\n        \"fra\": 149,\n        \"phx\": 209,\n        \"gdl\": 865,\n        \"jnb\": 385,\n        \"bog\": 476,\n        \"ewr\": 128,\n        \"bom\": 292,\n        \"scl\": 488,\n        \"hkg\": 327,\n        \"eze\": 386,\n        \"mad\": 364,\n        \"sin\": 502,\n        \"arn\": 197,\n        \"bos\": 128,\n        \"iad\": 438\n      },\n      {\n        \"timestamp\": \"2025-08-22T19:00:00.000Z\",\n        \"bom\": 287,\n        \"ewr\": 177,\n        \"hkg\": 339,\n        \"scl\": 442,\n        \"eze\": 457,\n        \"sin\": 522,\n        \"mad\": 367,\n        \"arn\": 208,\n        \"bos\": 127,\n        \"iad\": 276,\n        \"mia\": 179,\n        \"nrt\": 352,\n        \"atl\": 140,\n        \"gig\": 393,\n        \"cdg\": 149,\n        \"otp\": 277,\n        \"lax\": 152,\n        \"sjc\": 151,\n        \"dfw\": 176,\n        \"ams\": 180,\n        \"syd\": 393,\n        \"den\": 230,\n        \"ord\": 171,\n        \"sea\": 164,\n        \"yul\": 128,\n        \"gru\": 298,\n        \"fra\": 169,\n        \"lhr\": 141,\n        \"yyz\": 149,\n        \"bog\": 464,\n        \"phx\": 244,\n        \"gdl\": 635,\n        \"jnb\": 436\n      },\n      {\n        \"timestamp\": \"2025-08-22T19:30:00.000Z\",\n        \"gig\": 367,\n        \"atl\": 149,\n        \"nrt\": 483,\n        \"mia\": 196,\n        \"ams\": 202,\n        \"dfw\": 202,\n        \"sjc\": 135,\n        \"lax\": 150,\n        \"otp\": 269,\n        \"cdg\": 180,\n        \"gru\": 218,\n        \"yul\": 123,\n        \"sea\": 167,\n        \"ord\": 147,\n        \"den\": 242,\n        \"syd\": 327,\n        \"jnb\": 473,\n        \"gdl\": 616,\n        \"phx\": 197,\n        \"bog\": 430,\n        \"yyz\": 141,\n        \"lhr\": 170,\n        \"fra\": 171,\n        \"scl\": 443,\n        \"hkg\": 398,\n        \"bom\": 285,\n        \"ewr\": 159,\n        \"eze\": 382,\n        \"arn\": 239,\n        \"mad\": 330,\n        \"sin\": 481,\n        \"iad\": 142,\n        \"bos\": 123\n      },\n      {\n        \"timestamp\": \"2025-08-22T20:00:00.000Z\",\n        \"fra\": 236,\n        \"yyz\": 151,\n        \"lhr\": 155,\n        \"bog\": 425,\n        \"jnb\": 510,\n        \"phx\": 187,\n        \"gdl\": 647,\n        \"syd\": 394,\n        \"yul\": 105,\n        \"ord\": 196,\n        \"den\": 239,\n        \"sea\": 168,\n        \"gru\": 215,\n        \"cdg\": 306,\n        \"lax\": 195,\n        \"otp\": 322,\n        \"ams\": 184,\n        \"sjc\": 137,\n        \"dfw\": 211,\n        \"mia\": 174,\n        \"nrt\": 422,\n        \"atl\": 165,\n        \"gig\": 381,\n        \"bos\": 109,\n        \"iad\": 133,\n        \"sin\": 463,\n        \"mad\": 331,\n        \"arn\": 198,\n        \"eze\": 453,\n        \"bom\": 303,\n        \"ewr\": 228,\n        \"hkg\": 458,\n        \"scl\": 503\n      },\n      {\n        \"timestamp\": \"2025-08-22T20:30:00.000Z\",\n        \"gru\": 285,\n        \"sea\": 171,\n        \"den\": 246,\n        \"ord\": 182,\n        \"yul\": 120,\n        \"syd\": 402,\n        \"gdl\": 556,\n        \"phx\": 201,\n        \"jnb\": 508,\n        \"bog\": 415,\n        \"lhr\": 154,\n        \"yyz\": 171,\n        \"fra\": 154,\n        \"gig\": 356,\n        \"atl\": 140,\n        \"nrt\": 425,\n        \"mia\": 181,\n        \"dfw\": 171,\n        \"sjc\": 141,\n        \"ams\": 202,\n        \"otp\": 293,\n        \"lax\": 159,\n        \"cdg\": 149,\n        \"arn\": 203,\n        \"mad\": 362,\n        \"sin\": 600,\n        \"iad\": 133,\n        \"bos\": 104,\n        \"scl\": 512,\n        \"hkg\": 368,\n        \"ewr\": 186,\n        \"bom\": 291,\n        \"eze\": 452\n      },\n      {\n        \"timestamp\": \"2025-08-22T21:00:00.000Z\",\n        \"arn\": 193,\n        \"sin\": 425,\n        \"mad\": 390,\n        \"iad\": 181,\n        \"bos\": 117,\n        \"hkg\": 399,\n        \"scl\": 500,\n        \"bom\": 295,\n        \"ewr\": 127,\n        \"eze\": 464,\n        \"sea\": 184,\n        \"den\": 341,\n        \"ord\": 161,\n        \"yul\": 115,\n        \"gru\": 209,\n        \"syd\": 416,\n        \"bog\": 439,\n        \"gdl\": 602,\n        \"phx\": 370,\n        \"jnb\": 488,\n        \"fra\": 153,\n        \"lhr\": 160,\n        \"yyz\": 130,\n        \"atl\": 147,\n        \"gig\": 379,\n        \"nrt\": 431,\n        \"mia\": 157,\n        \"lax\": 160,\n        \"otp\": 291,\n        \"dfw\": 195,\n        \"sjc\": 258,\n        \"ams\": 207,\n        \"cdg\": 144\n      },\n      {\n        \"timestamp\": \"2025-08-22T21:30:00.000Z\",\n        \"eze\": 447,\n        \"bom\": 290,\n        \"ewr\": 134,\n        \"hkg\": 505,\n        \"scl\": 494,\n        \"bos\": 108,\n        \"iad\": 425,\n        \"sin\": 471,\n        \"mad\": 338,\n        \"arn\": 198,\n        \"cdg\": 156,\n        \"otp\": 305,\n        \"lax\": 167,\n        \"dfw\": 192,\n        \"sjc\": 166,\n        \"ams\": 175,\n        \"nrt\": 461,\n        \"mia\": 173,\n        \"atl\": 161,\n        \"gig\": 354,\n        \"fra\": 175,\n        \"lhr\": 158,\n        \"yyz\": 149,\n        \"bog\": 465,\n        \"gdl\": 611,\n        \"phx\": 205,\n        \"jnb\": 484,\n        \"syd\": 294,\n        \"sea\": 168,\n        \"ord\": 147,\n        \"den\": 235,\n        \"yul\": 100,\n        \"gru\": 273\n      },\n      {\n        \"timestamp\": \"2025-08-22T22:00:00.000Z\",\n        \"gdl\": 975,\n        \"phx\": 198,\n        \"jnb\": 460,\n        \"bog\": 423,\n        \"lhr\": 168,\n        \"yyz\": 196,\n        \"fra\": 150,\n        \"gru\": 281,\n        \"sea\": 189,\n        \"den\": 234,\n        \"ord\": 179,\n        \"yul\": 103,\n        \"syd\": 378,\n        \"dfw\": 184,\n        \"sjc\": 150,\n        \"ams\": 194,\n        \"otp\": 282,\n        \"lax\": 175,\n        \"cdg\": 160,\n        \"gig\": 357,\n        \"atl\": 138,\n        \"nrt\": 381,\n        \"mia\": 154,\n        \"iad\": 149,\n        \"bos\": 131,\n        \"arn\": 248,\n        \"mad\": 337,\n        \"sin\": 488,\n        \"eze\": 398,\n        \"scl\": 477,\n        \"hkg\": 350,\n        \"ewr\": 140,\n        \"bom\": 286\n      },\n      {\n        \"timestamp\": \"2025-08-22T22:30:00.000Z\",\n        \"arn\": 192,\n        \"sin\": 424,\n        \"mad\": 348,\n        \"iad\": 164,\n        \"bos\": 196,\n        \"hkg\": 473,\n        \"scl\": 502,\n        \"bom\": 294,\n        \"ewr\": 161,\n        \"eze\": 456,\n        \"ord\": 172,\n        \"den\": 234,\n        \"sea\": 181,\n        \"yul\": 135,\n        \"gru\": 199,\n        \"syd\": 430,\n        \"bog\": 413,\n        \"phx\": 192,\n        \"gdl\": 624,\n        \"jnb\": 510,\n        \"fra\": 161,\n        \"lhr\": 210,\n        \"yyz\": 153,\n        \"atl\": 179,\n        \"gig\": 302,\n        \"mia\": 159,\n        \"nrt\": 434,\n        \"otp\": 297,\n        \"lax\": 151,\n        \"sjc\": 140,\n        \"dfw\": 191,\n        \"ams\": 199,\n        \"cdg\": 156\n      },\n      {\n        \"timestamp\": \"2025-08-22T23:00:00.000Z\",\n        \"eze\": 388,\n        \"scl\": 496,\n        \"hkg\": 371,\n        \"ewr\": 169,\n        \"bom\": 284,\n        \"iad\": 251,\n        \"bos\": 132,\n        \"arn\": 195,\n        \"mad\": 352,\n        \"sin\": 416,\n        \"ams\": 184,\n        \"dfw\": 202,\n        \"sjc\": 138,\n        \"lax\": 164,\n        \"otp\": 288,\n        \"cdg\": 151,\n        \"gig\": 366,\n        \"atl\": 334,\n        \"nrt\": 340,\n        \"mia\": 166,\n        \"jnb\": 470,\n        \"gdl\": 631,\n        \"phx\": 221,\n        \"bog\": 386,\n        \"yyz\": 135,\n        \"lhr\": 138,\n        \"fra\": 166,\n        \"gru\": 214,\n        \"yul\": 126,\n        \"sea\": 199,\n        \"ord\": 154,\n        \"den\": 222,\n        \"syd\": 382\n      },\n      {\n        \"timestamp\": \"2025-08-22T23:30:00.000Z\",\n        \"bos\": 112,\n        \"iad\": 241,\n        \"sin\": 469,\n        \"mad\": 353,\n        \"arn\": 187,\n        \"eze\": 451,\n        \"ewr\": 169,\n        \"bom\": 332,\n        \"hkg\": 430,\n        \"scl\": 501,\n        \"fra\": 173,\n        \"yyz\": 134,\n        \"lhr\": 136,\n        \"bog\": 410,\n        \"jnb\": 391,\n        \"phx\": 192,\n        \"gdl\": 635,\n        \"syd\": 369,\n        \"yul\": 121,\n        \"ord\": 150,\n        \"den\": 238,\n        \"sea\": 171,\n        \"gru\": 286,\n        \"cdg\": 149,\n        \"otp\": 299,\n        \"lax\": 168,\n        \"ams\": 184,\n        \"sjc\": 150,\n        \"dfw\": 194,\n        \"mia\": 160,\n        \"nrt\": 423,\n        \"atl\": 153,\n        \"gig\": 350\n      },\n      {\n        \"timestamp\": \"2025-08-23T00:00:00.000Z\",\n        \"bos\": 112,\n        \"iad\": 150,\n        \"sin\": 475,\n        \"mad\": 337,\n        \"arn\": 202,\n        \"eze\": 472,\n        \"ewr\": 130,\n        \"bom\": 288,\n        \"hkg\": 352,\n        \"scl\": 431,\n        \"fra\": 160,\n        \"lhr\": 138,\n        \"yyz\": 140,\n        \"bog\": 414,\n        \"phx\": 193,\n        \"gdl\": 617,\n        \"jnb\": 400,\n        \"syd\": 382,\n        \"den\": 249,\n        \"ord\": 172,\n        \"sea\": 171,\n        \"yul\": 111,\n        \"gru\": 213,\n        \"cdg\": 151,\n        \"otp\": 284,\n        \"lax\": 164,\n        \"sjc\": 137,\n        \"dfw\": 187,\n        \"ams\": 178,\n        \"mia\": 183,\n        \"nrt\": 419,\n        \"atl\": 213,\n        \"gig\": 348\n      },\n      {\n        \"timestamp\": \"2025-08-23T00:30:00.000Z\",\n        \"sjc\": 297,\n        \"dfw\": 208,\n        \"ams\": 196,\n        \"lax\": 155,\n        \"otp\": 303,\n        \"cdg\": 146,\n        \"gig\": 288,\n        \"atl\": 162,\n        \"mia\": 172,\n        \"nrt\": 429,\n        \"phx\": 203,\n        \"gdl\": 605,\n        \"jnb\": 395,\n        \"bog\": 396,\n        \"lhr\": 158,\n        \"yyz\": 210,\n        \"fra\": 157,\n        \"gru\": 257,\n        \"ord\": 177,\n        \"den\": 232,\n        \"sea\": 166,\n        \"yul\": 127,\n        \"syd\": 449,\n        \"eze\": 454,\n        \"scl\": 514,\n        \"hkg\": 320,\n        \"bom\": 294,\n        \"ewr\": 157,\n        \"iad\": 161,\n        \"bos\": 111,\n        \"arn\": 193,\n        \"mad\": 390,\n        \"sin\": 469\n      },\n      {\n        \"timestamp\": \"2025-08-23T01:00:00.000Z\",\n        \"yul\": 106,\n        \"sea\": 175,\n        \"ord\": 152,\n        \"den\": 228,\n        \"gru\": 306,\n        \"syd\": 398,\n        \"bog\": 394,\n        \"jnb\": 509,\n        \"gdl\": 622,\n        \"phx\": 264,\n        \"fra\": 151,\n        \"yyz\": 128,\n        \"lhr\": 204,\n        \"atl\": 184,\n        \"gig\": 369,\n        \"nrt\": 329,\n        \"mia\": 183,\n        \"otp\": 270,\n        \"lax\": 165,\n        \"ams\": 173,\n        \"dfw\": 210,\n        \"sjc\": 130,\n        \"cdg\": 146,\n        \"arn\": 271,\n        \"sin\": 411,\n        \"mad\": 384,\n        \"iad\": 139,\n        \"bos\": 121,\n        \"hkg\": 312,\n        \"scl\": 440,\n        \"ewr\": 132,\n        \"bom\": 328,\n        \"eze\": 305\n      },\n      {\n        \"timestamp\": \"2025-08-23T01:30:00.000Z\",\n        \"mia\": 160,\n        \"nrt\": 364,\n        \"gig\": 293,\n        \"atl\": 172,\n        \"cdg\": 144,\n        \"ams\": 180,\n        \"sjc\": 134,\n        \"dfw\": 198,\n        \"lax\": 171,\n        \"otp\": 305,\n        \"syd\": 422,\n        \"gru\": 289,\n        \"yul\": 130,\n        \"ord\": 139,\n        \"den\": 237,\n        \"sea\": 173,\n        \"yyz\": 138,\n        \"lhr\": 148,\n        \"fra\": 158,\n        \"jnb\": 508,\n        \"phx\": 193,\n        \"gdl\": 632,\n        \"bog\": 497,\n        \"ewr\": 135,\n        \"bom\": 340,\n        \"scl\": 492,\n        \"hkg\": 406,\n        \"eze\": 450,\n        \"mad\": 391,\n        \"sin\": 454,\n        \"arn\": 197,\n        \"bos\": 112,\n        \"iad\": 190\n      },\n      {\n        \"timestamp\": \"2025-08-23T02:00:00.000Z\",\n        \"iad\": 227,\n        \"bos\": 107,\n        \"arn\": 204,\n        \"mad\": 358,\n        \"sin\": 419,\n        \"eze\": 456,\n        \"scl\": 505,\n        \"hkg\": 436,\n        \"bom\": 289,\n        \"ewr\": 133,\n        \"jnb\": 500,\n        \"gdl\": 623,\n        \"phx\": 197,\n        \"bog\": 410,\n        \"yyz\": 133,\n        \"lhr\": 142,\n        \"fra\": 156,\n        \"gru\": 342,\n        \"yul\": 99,\n        \"sea\": 263,\n        \"ord\": 172,\n        \"den\": 239,\n        \"syd\": 316,\n        \"ams\": 259,\n        \"dfw\": 209,\n        \"sjc\": 153,\n        \"lax\": 176,\n        \"otp\": 284,\n        \"cdg\": 158,\n        \"gig\": 376,\n        \"atl\": 194,\n        \"nrt\": 433,\n        \"mia\": 187\n      },\n      {\n        \"timestamp\": \"2025-08-23T02:30:00.000Z\",\n        \"yul\": 114,\n        \"den\": 236,\n        \"ord\": 171,\n        \"sea\": 196,\n        \"gru\": 282,\n        \"syd\": 417,\n        \"bog\": 417,\n        \"jnb\": 386,\n        \"phx\": 194,\n        \"gdl\": 563,\n        \"fra\": 165,\n        \"yyz\": 151,\n        \"lhr\": 157,\n        \"atl\": 200,\n        \"gig\": 369,\n        \"mia\": 175,\n        \"nrt\": 361,\n        \"lax\": 210,\n        \"otp\": 272,\n        \"ams\": 174,\n        \"sjc\": 144,\n        \"dfw\": 192,\n        \"cdg\": 177,\n        \"arn\": 197,\n        \"sin\": 444,\n        \"mad\": 328,\n        \"iad\": 289,\n        \"bos\": 143,\n        \"hkg\": 472,\n        \"scl\": 519,\n        \"ewr\": 179,\n        \"bom\": 323,\n        \"eze\": 475\n      },\n      {\n        \"timestamp\": \"2025-08-23T03:00:00.000Z\",\n        \"arn\": 301,\n        \"mad\": 349,\n        \"sin\": 518,\n        \"iad\": 189,\n        \"bos\": 148,\n        \"scl\": 500,\n        \"hkg\": 478,\n        \"bom\": 363,\n        \"ewr\": 181,\n        \"eze\": 448,\n        \"gru\": 202,\n        \"yul\": 104,\n        \"den\": 237,\n        \"ord\": 191,\n        \"sea\": 185,\n        \"syd\": 376,\n        \"jnb\": 387,\n        \"phx\": 217,\n        \"gdl\": 639,\n        \"bog\": 417,\n        \"yyz\": 133,\n        \"lhr\": 142,\n        \"fra\": 167,\n        \"gig\": 354,\n        \"atl\": 144,\n        \"mia\": 173,\n        \"nrt\": 421,\n        \"ams\": 234,\n        \"sjc\": 140,\n        \"dfw\": 179,\n        \"otp\": 272,\n        \"lax\": 157,\n        \"cdg\": 143\n      },\n      {\n        \"timestamp\": \"2025-08-23T03:30:00.000Z\",\n        \"fra\": 166,\n        \"lhr\": 144,\n        \"yyz\": 193,\n        \"bog\": 431,\n        \"phx\": 198,\n        \"gdl\": 603,\n        \"jnb\": 462,\n        \"syd\": 455,\n        \"den\": 244,\n        \"ord\": 134,\n        \"sea\": 160,\n        \"yul\": 110,\n        \"gru\": 343,\n        \"cdg\": 156,\n        \"lax\": 147,\n        \"otp\": 281,\n        \"sjc\": 157,\n        \"dfw\": 195,\n        \"ams\": 179,\n        \"mia\": 175,\n        \"nrt\": 419,\n        \"atl\": 223,\n        \"gig\": 334,\n        \"bos\": 113,\n        \"iad\": 136,\n        \"sin\": 448,\n        \"mad\": 325,\n        \"arn\": 193,\n        \"eze\": 450,\n        \"bom\": 297,\n        \"ewr\": 193,\n        \"hkg\": 472,\n        \"scl\": 492\n      },\n      {\n        \"timestamp\": \"2025-08-23T04:00:00.000Z\",\n        \"gig\": 343,\n        \"atl\": 237,\n        \"nrt\": 260,\n        \"mia\": 174,\n        \"dfw\": 191,\n        \"sjc\": 131,\n        \"ams\": 179,\n        \"lax\": 162,\n        \"otp\": 306,\n        \"cdg\": 218,\n        \"gru\": 191,\n        \"sea\": 179,\n        \"den\": 272,\n        \"ord\": 135,\n        \"yul\": 106,\n        \"syd\": 445,\n        \"gdl\": 736,\n        \"phx\": 212,\n        \"jnb\": 434,\n        \"bog\": 483,\n        \"lhr\": 166,\n        \"yyz\": 131,\n        \"fra\": 167,\n        \"scl\": 500,\n        \"hkg\": 448,\n        \"bom\": 332,\n        \"ewr\": 179,\n        \"eze\": 454,\n        \"arn\": 187,\n        \"mad\": 374,\n        \"sin\": 416,\n        \"iad\": 135,\n        \"bos\": 194\n      },\n      {\n        \"timestamp\": \"2025-08-23T04:30:00.000Z\",\n        \"nrt\": 366,\n        \"mia\": 222,\n        \"atl\": 278,\n        \"gig\": 357,\n        \"cdg\": 155,\n        \"otp\": 405,\n        \"lax\": 156,\n        \"ams\": 188,\n        \"dfw\": 258,\n        \"sjc\": 159,\n        \"syd\": 293,\n        \"yul\": 121,\n        \"sea\": 162,\n        \"den\": 219,\n        \"ord\": 169,\n        \"gru\": 337,\n        \"fra\": 157,\n        \"yyz\": 140,\n        \"lhr\": 139,\n        \"bog\": 404,\n        \"jnb\": 406,\n        \"gdl\": 607,\n        \"phx\": 202,\n        \"ewr\": 126,\n        \"bom\": 301,\n        \"hkg\": 466,\n        \"scl\": 502,\n        \"eze\": 446,\n        \"sin\": 425,\n        \"mad\": 347,\n        \"arn\": 241,\n        \"bos\": 104,\n        \"iad\": 134\n      },\n      {\n        \"timestamp\": \"2025-08-23T05:00:00.000Z\",\n        \"bom\": 341,\n        \"ewr\": 159,\n        \"scl\": 500,\n        \"hkg\": 403,\n        \"eze\": 383,\n        \"mad\": 361,\n        \"sin\": 445,\n        \"arn\": 200,\n        \"bos\": 96,\n        \"iad\": 140,\n        \"nrt\": 417,\n        \"mia\": 170,\n        \"gig\": 365,\n        \"atl\": 276,\n        \"cdg\": 148,\n        \"ams\": 181,\n        \"dfw\": 186,\n        \"sjc\": 129,\n        \"lax\": 155,\n        \"otp\": 277,\n        \"syd\": 357,\n        \"gru\": 211,\n        \"yul\": 106,\n        \"sea\": 172,\n        \"den\": 355,\n        \"ord\": 147,\n        \"yyz\": 145,\n        \"lhr\": 226,\n        \"fra\": 166,\n        \"jnb\": 470,\n        \"gdl\": 631,\n        \"phx\": 196,\n        \"bog\": 387\n      },\n      {\n        \"timestamp\": \"2025-08-23T05:30:00.000Z\",\n        \"otp\": 282,\n        \"lax\": 149,\n        \"dfw\": 179,\n        \"sjc\": 148,\n        \"ams\": 195,\n        \"cdg\": 198,\n        \"atl\": 245,\n        \"gig\": 344,\n        \"nrt\": 430,\n        \"mia\": 163,\n        \"bog\": 444,\n        \"gdl\": 663,\n        \"phx\": 203,\n        \"jnb\": 510,\n        \"fra\": 190,\n        \"lhr\": 166,\n        \"yyz\": 136,\n        \"sea\": 195,\n        \"den\": 245,\n        \"ord\": 161,\n        \"yul\": 102,\n        \"gru\": 247,\n        \"syd\": 408,\n        \"eze\": 457,\n        \"hkg\": 454,\n        \"scl\": 496,\n        \"bom\": 304,\n        \"ewr\": 145,\n        \"iad\": 146,\n        \"bos\": 111,\n        \"arn\": 193,\n        \"sin\": 520,\n        \"mad\": 365\n      },\n      {\n        \"timestamp\": \"2025-08-23T06:00:00.000Z\",\n        \"bos\": 103,\n        \"iad\": 158,\n        \"mad\": 344,\n        \"sin\": 461,\n        \"arn\": 219,\n        \"eze\": 381,\n        \"ewr\": 128,\n        \"bom\": 296,\n        \"scl\": 431,\n        \"hkg\": 356,\n        \"lhr\": 168,\n        \"yyz\": 135,\n        \"fra\": 225,\n        \"gdl\": 615,\n        \"phx\": 209,\n        \"jnb\": 376,\n        \"bog\": 440,\n        \"syd\": 386,\n        \"gru\": 194,\n        \"sea\": 160,\n        \"ord\": 222,\n        \"den\": 245,\n        \"yul\": 115,\n        \"cdg\": 177,\n        \"dfw\": 177,\n        \"sjc\": 132,\n        \"ams\": 182,\n        \"otp\": 270,\n        \"lax\": 152,\n        \"nrt\": 419,\n        \"mia\": 159,\n        \"gig\": 347,\n        \"atl\": 139\n      },\n      {\n        \"timestamp\": \"2025-08-23T06:30:00.000Z\",\n        \"syd\": 384,\n        \"den\": 240,\n        \"ord\": 149,\n        \"sea\": 182,\n        \"yul\": 109,\n        \"gru\": 265,\n        \"fra\": 153,\n        \"lhr\": 225,\n        \"yyz\": 153,\n        \"bog\": 418,\n        \"phx\": 193,\n        \"gdl\": 570,\n        \"jnb\": 418,\n        \"mia\": 162,\n        \"nrt\": 427,\n        \"atl\": 134,\n        \"gig\": 357,\n        \"cdg\": 143,\n        \"otp\": 442,\n        \"lax\": 164,\n        \"sjc\": 136,\n        \"dfw\": 256,\n        \"ams\": 196,\n        \"sin\": 531,\n        \"mad\": 345,\n        \"arn\": 188,\n        \"bos\": 102,\n        \"iad\": 136,\n        \"bom\": 321,\n        \"ewr\": 153,\n        \"hkg\": 486,\n        \"scl\": 493,\n        \"eze\": 472\n      },\n      {\n        \"timestamp\": \"2025-08-23T07:00:00.000Z\",\n        \"bos\": 99,\n        \"iad\": 146,\n        \"sin\": 444,\n        \"mad\": 340,\n        \"arn\": 366,\n        \"eze\": 459,\n        \"ewr\": 155,\n        \"bom\": 304,\n        \"hkg\": 472,\n        \"scl\": 492,\n        \"fra\": 166,\n        \"lhr\": 148,\n        \"yyz\": 137,\n        \"bog\": 445,\n        \"gdl\": 616,\n        \"phx\": 316,\n        \"jnb\": 466,\n        \"syd\": 420,\n        \"sea\": 218,\n        \"ord\": 150,\n        \"den\": 268,\n        \"yul\": 112,\n        \"gru\": 241,\n        \"cdg\": 157,\n        \"lax\": 209,\n        \"otp\": 282,\n        \"dfw\": 171,\n        \"sjc\": 144,\n        \"ams\": 288,\n        \"nrt\": 437,\n        \"mia\": 185,\n        \"atl\": 167,\n        \"gig\": 449\n      },\n      {\n        \"timestamp\": \"2025-08-23T07:30:00.000Z\",\n        \"eze\": 457,\n        \"scl\": 497,\n        \"hkg\": 384,\n        \"ewr\": 180,\n        \"bom\": 299,\n        \"iad\": 182,\n        \"bos\": 120,\n        \"arn\": 209,\n        \"mad\": 341,\n        \"sin\": 429,\n        \"sjc\": 157,\n        \"dfw\": 194,\n        \"ams\": 185,\n        \"otp\": 311,\n        \"lax\": 158,\n        \"cdg\": 288,\n        \"gig\": 285,\n        \"atl\": 142,\n        \"mia\": 180,\n        \"nrt\": 345,\n        \"phx\": 198,\n        \"gdl\": 615,\n        \"jnb\": 513,\n        \"bog\": 427,\n        \"lhr\": 141,\n        \"yyz\": 131,\n        \"fra\": 175,\n        \"gru\": 210,\n        \"ord\": 160,\n        \"den\": 238,\n        \"sea\": 166,\n        \"yul\": 112,\n        \"syd\": 378\n      },\n      {\n        \"timestamp\": \"2025-08-23T08:00:00.000Z\",\n        \"scl\": 489,\n        \"hkg\": 483,\n        \"bom\": 299,\n        \"ewr\": 184,\n        \"eze\": 470,\n        \"arn\": 197,\n        \"mad\": 324,\n        \"sin\": 412,\n        \"iad\": 135,\n        \"bos\": 114,\n        \"gig\": 366,\n        \"atl\": 149,\n        \"mia\": 171,\n        \"nrt\": 425,\n        \"ams\": 213,\n        \"sjc\": 136,\n        \"dfw\": 177,\n        \"otp\": 377,\n        \"lax\": 164,\n        \"cdg\": 160,\n        \"gru\": 191,\n        \"yul\": 158,\n        \"den\": 222,\n        \"ord\": 180,\n        \"sea\": 209,\n        \"syd\": 301,\n        \"jnb\": 379,\n        \"phx\": 197,\n        \"gdl\": 600,\n        \"bog\": 391,\n        \"yyz\": 153,\n        \"lhr\": 138,\n        \"fra\": 153\n      },\n      {\n        \"timestamp\": \"2025-08-23T08:30:00.000Z\",\n        \"ams\": 201,\n        \"sjc\": 128,\n        \"dfw\": 171,\n        \"lax\": 150,\n        \"otp\": 274,\n        \"cdg\": 200,\n        \"gig\": 351,\n        \"atl\": 132,\n        \"mia\": 170,\n        \"nrt\": 425,\n        \"jnb\": 464,\n        \"phx\": 233,\n        \"gdl\": 495,\n        \"bog\": 421,\n        \"yyz\": 134,\n        \"lhr\": 140,\n        \"fra\": 165,\n        \"gru\": 203,\n        \"yul\": 152,\n        \"den\": 221,\n        \"ord\": 154,\n        \"sea\": 206,\n        \"syd\": 317,\n        \"eze\": 468,\n        \"scl\": 444,\n        \"hkg\": 362,\n        \"bom\": 325,\n        \"ewr\": 126,\n        \"iad\": 133,\n        \"bos\": 108,\n        \"arn\": 187,\n        \"mad\": 321,\n        \"sin\": 483\n      },\n      {\n        \"timestamp\": \"2025-08-23T09:00:00.000Z\",\n        \"eze\": 452,\n        \"ewr\": 143,\n        \"bom\": 295,\n        \"hkg\": 520,\n        \"scl\": 496,\n        \"bos\": 130,\n        \"iad\": 195,\n        \"sin\": 506,\n        \"mad\": 346,\n        \"arn\": 199,\n        \"cdg\": 145,\n        \"otp\": 273,\n        \"lax\": 159,\n        \"dfw\": 177,\n        \"sjc\": 125,\n        \"ams\": 190,\n        \"nrt\": 430,\n        \"mia\": 174,\n        \"atl\": 151,\n        \"gig\": 250,\n        \"fra\": 167,\n        \"lhr\": 266,\n        \"yyz\": 142,\n        \"bog\": 447,\n        \"gdl\": 577,\n        \"phx\": 189,\n        \"jnb\": 483,\n        \"syd\": 417,\n        \"sea\": 168,\n        \"den\": 232,\n        \"ord\": 165,\n        \"yul\": 115,\n        \"gru\": 190\n      },\n      {\n        \"timestamp\": \"2025-08-23T09:30:00.000Z\",\n        \"nrt\": 418,\n        \"mia\": 178,\n        \"atl\": 150,\n        \"gig\": 350,\n        \"cdg\": 149,\n        \"lax\": 152,\n        \"otp\": 286,\n        \"dfw\": 174,\n        \"sjc\": 134,\n        \"ams\": 190,\n        \"syd\": 404,\n        \"sea\": 184,\n        \"den\": 226,\n        \"ord\": 173,\n        \"yul\": 107,\n        \"gru\": 196,\n        \"fra\": 168,\n        \"lhr\": 139,\n        \"yyz\": 151,\n        \"bog\": 438,\n        \"gdl\": 495,\n        \"phx\": 262,\n        \"jnb\": 503,\n        \"ewr\": 185,\n        \"bom\": 302,\n        \"hkg\": 429,\n        \"scl\": 501,\n        \"eze\": 454,\n        \"sin\": 439,\n        \"mad\": 351,\n        \"arn\": 187,\n        \"bos\": 115,\n        \"iad\": 177\n      },\n      {\n        \"timestamp\": \"2025-08-23T10:00:00.000Z\",\n        \"sin\": 459,\n        \"mad\": 353,\n        \"arn\": 205,\n        \"bos\": 109,\n        \"iad\": 138,\n        \"bom\": 284,\n        \"ewr\": 169,\n        \"hkg\": 349,\n        \"scl\": 498,\n        \"eze\": 459,\n        \"syd\": 373,\n        \"yul\": 116,\n        \"sea\": 177,\n        \"ord\": 157,\n        \"den\": 226,\n        \"gru\": 271,\n        \"fra\": 183,\n        \"yyz\": 136,\n        \"lhr\": 152,\n        \"bog\": 491,\n        \"jnb\": 459,\n        \"gdl\": 591,\n        \"phx\": 200,\n        \"nrt\": 420,\n        \"mia\": 164,\n        \"atl\": 137,\n        \"gig\": 365,\n        \"cdg\": 156,\n        \"lax\": 154,\n        \"otp\": 305,\n        \"ams\": 208,\n        \"dfw\": 194,\n        \"sjc\": 157\n      },\n      {\n        \"timestamp\": \"2025-08-23T10:30:00.000Z\",\n        \"fra\": 164,\n        \"yyz\": 138,\n        \"lhr\": 146,\n        \"bog\": 392,\n        \"jnb\": 406,\n        \"gdl\": 622,\n        \"phx\": 199,\n        \"syd\": 418,\n        \"yul\": 128,\n        \"sea\": 166,\n        \"ord\": 136,\n        \"den\": 240,\n        \"gru\": 276,\n        \"cdg\": 157,\n        \"otp\": 276,\n        \"lax\": 157,\n        \"ams\": 178,\n        \"dfw\": 194,\n        \"sjc\": 142,\n        \"nrt\": 433,\n        \"mia\": 168,\n        \"atl\": 146,\n        \"gig\": 363,\n        \"bos\": 115,\n        \"iad\": 387,\n        \"sin\": 451,\n        \"mad\": 337,\n        \"arn\": 206,\n        \"eze\": 449,\n        \"bom\": 286,\n        \"ewr\": 170,\n        \"hkg\": 476,\n        \"scl\": 432\n      },\n      {\n        \"timestamp\": \"2025-08-23T11:00:00.000Z\",\n        \"bos\": 112,\n        \"iad\": 188,\n        \"sin\": 429,\n        \"mad\": 407,\n        \"arn\": 197,\n        \"eze\": 290,\n        \"bom\": 317,\n        \"ewr\": 125,\n        \"hkg\": 456,\n        \"scl\": 499,\n        \"fra\": 164,\n        \"yyz\": 183,\n        \"lhr\": 242,\n        \"bog\": 435,\n        \"jnb\": 457,\n        \"phx\": 241,\n        \"gdl\": 570,\n        \"syd\": 358,\n        \"yul\": 102,\n        \"den\": 223,\n        \"ord\": 191,\n        \"sea\": 169,\n        \"gru\": 237,\n        \"cdg\": 148,\n        \"otp\": 281,\n        \"lax\": 200,\n        \"ams\": 183,\n        \"sjc\": 161,\n        \"dfw\": 192,\n        \"mia\": 156,\n        \"nrt\": 415,\n        \"atl\": 129,\n        \"gig\": 357\n      },\n      {\n        \"timestamp\": \"2025-08-23T11:30:00.000Z\",\n        \"yyz\": 130,\n        \"lhr\": 153,\n        \"fra\": 228,\n        \"jnb\": 510,\n        \"gdl\": 595,\n        \"phx\": 201,\n        \"bog\": 459,\n        \"syd\": 422,\n        \"gru\": 198,\n        \"yul\": 116,\n        \"sea\": 177,\n        \"ord\": 137,\n        \"den\": 255,\n        \"cdg\": 158,\n        \"ams\": 175,\n        \"dfw\": 237,\n        \"sjc\": 148,\n        \"lax\": 163,\n        \"otp\": 272,\n        \"nrt\": 428,\n        \"mia\": 172,\n        \"gig\": 434,\n        \"atl\": 180,\n        \"bos\": 115,\n        \"iad\": 183,\n        \"mad\": 346,\n        \"sin\": 445,\n        \"arn\": 206,\n        \"eze\": 477,\n        \"bom\": 348,\n        \"ewr\": 183,\n        \"scl\": 499,\n        \"hkg\": 345\n      },\n      {\n        \"timestamp\": \"2025-08-23T12:00:00.000Z\",\n        \"bos\": 126,\n        \"iad\": 308,\n        \"mad\": 341,\n        \"sin\": 462,\n        \"arn\": 211,\n        \"eze\": 389,\n        \"bom\": 308,\n        \"ewr\": 182,\n        \"scl\": 490,\n        \"hkg\": 443,\n        \"yyz\": 137,\n        \"lhr\": 179,\n        \"fra\": 163,\n        \"jnb\": 512,\n        \"gdl\": 600,\n        \"phx\": 192,\n        \"bog\": 470,\n        \"syd\": 378,\n        \"gru\": 188,\n        \"yul\": 112,\n        \"sea\": 187,\n        \"ord\": 168,\n        \"den\": 231,\n        \"cdg\": 224,\n        \"ams\": 172,\n        \"dfw\": 202,\n        \"sjc\": 143,\n        \"lax\": 187,\n        \"otp\": 297,\n        \"nrt\": 423,\n        \"mia\": 170,\n        \"gig\": 348,\n        \"atl\": 140\n      },\n      {\n        \"timestamp\": \"2025-08-23T12:30:00.000Z\",\n        \"eze\": 476,\n        \"bom\": 301,\n        \"ewr\": 158,\n        \"scl\": 489,\n        \"hkg\": 397,\n        \"bos\": 103,\n        \"iad\": 225,\n        \"mad\": 342,\n        \"sin\": 462,\n        \"arn\": 190,\n        \"cdg\": 156,\n        \"sjc\": 139,\n        \"dfw\": 263,\n        \"ams\": 193,\n        \"otp\": 352,\n        \"lax\": 150,\n        \"mia\": 162,\n        \"nrt\": 248,\n        \"gig\": 351,\n        \"atl\": 162,\n        \"lhr\": 158,\n        \"yyz\": 152,\n        \"fra\": 160,\n        \"phx\": 209,\n        \"gdl\": 509,\n        \"jnb\": 507,\n        \"bog\": 436,\n        \"syd\": 330,\n        \"gru\": 213,\n        \"ord\": 193,\n        \"den\": 245,\n        \"sea\": 191,\n        \"yul\": 109\n      },\n      {\n        \"timestamp\": \"2025-08-23T13:00:00.000Z\",\n        \"gru\": 190,\n        \"den\": 294,\n        \"ord\": 190,\n        \"sea\": 173,\n        \"yul\": 172,\n        \"syd\": 314,\n        \"phx\": 205,\n        \"gdl\": 622,\n        \"jnb\": 392,\n        \"bog\": 461,\n        \"lhr\": 157,\n        \"yyz\": 157,\n        \"fra\": 262,\n        \"gig\": 353,\n        \"atl\": 129,\n        \"mia\": 184,\n        \"nrt\": 451,\n        \"sjc\": 162,\n        \"dfw\": 192,\n        \"ams\": 178,\n        \"lax\": 163,\n        \"otp\": 310,\n        \"cdg\": 147,\n        \"arn\": 195,\n        \"mad\": 342,\n        \"sin\": 467,\n        \"iad\": 359,\n        \"bos\": 145,\n        \"scl\": 354,\n        \"hkg\": 345,\n        \"bom\": 293,\n        \"ewr\": 159,\n        \"eze\": 450\n      },\n      {\n        \"timestamp\": \"2025-08-23T13:30:00.000Z\",\n        \"ams\": 179,\n        \"sjc\": 142,\n        \"dfw\": 261,\n        \"otp\": 418,\n        \"lax\": 162,\n        \"cdg\": 211,\n        \"gig\": 363,\n        \"atl\": 186,\n        \"mia\": 185,\n        \"nrt\": 372,\n        \"jnb\": 527,\n        \"phx\": 198,\n        \"gdl\": 605,\n        \"bog\": 436,\n        \"yyz\": 162,\n        \"lhr\": 145,\n        \"fra\": 164,\n        \"gru\": 220,\n        \"yul\": 125,\n        \"ord\": 195,\n        \"den\": 246,\n        \"sea\": 181,\n        \"syd\": 422,\n        \"eze\": 451,\n        \"scl\": 494,\n        \"hkg\": 459,\n        \"bom\": 300,\n        \"ewr\": 133,\n        \"iad\": 148,\n        \"bos\": 134,\n        \"arn\": 194,\n        \"mad\": 342,\n        \"sin\": 468\n      },\n      {\n        \"timestamp\": \"2025-08-23T14:00:00.000Z\",\n        \"gig\": 347,\n        \"atl\": 170,\n        \"nrt\": 381,\n        \"mia\": 180,\n        \"ams\": 188,\n        \"dfw\": 186,\n        \"sjc\": 169,\n        \"otp\": 364,\n        \"lax\": 225,\n        \"cdg\": 143,\n        \"gru\": 211,\n        \"yul\": 122,\n        \"sea\": 182,\n        \"den\": 256,\n        \"ord\": 169,\n        \"syd\": 310,\n        \"jnb\": 506,\n        \"gdl\": 495,\n        \"phx\": 228,\n        \"bog\": 486,\n        \"yyz\": 137,\n        \"lhr\": 202,\n        \"fra\": 161,\n        \"scl\": 492,\n        \"hkg\": 384,\n        \"ewr\": 130,\n        \"bom\": 293,\n        \"eze\": 455,\n        \"arn\": 217,\n        \"mad\": 383,\n        \"sin\": 417,\n        \"iad\": 449,\n        \"bos\": 113\n      },\n      {\n        \"timestamp\": \"2025-08-23T14:30:00.000Z\",\n        \"gru\": 296,\n        \"den\": 221,\n        \"ord\": 178,\n        \"sea\": 176,\n        \"yul\": 111,\n        \"syd\": 385,\n        \"phx\": 192,\n        \"gdl\": 601,\n        \"jnb\": 516,\n        \"bog\": 444,\n        \"lhr\": 191,\n        \"yyz\": 127,\n        \"fra\": 175,\n        \"gig\": 353,\n        \"atl\": 128,\n        \"mia\": 167,\n        \"nrt\": 443,\n        \"sjc\": 161,\n        \"dfw\": 185,\n        \"ams\": 178,\n        \"lax\": 155,\n        \"otp\": 297,\n        \"cdg\": 153,\n        \"arn\": 190,\n        \"mad\": 378,\n        \"sin\": 461,\n        \"iad\": 257,\n        \"bos\": 98,\n        \"scl\": 491,\n        \"hkg\": 466,\n        \"ewr\": 139,\n        \"bom\": 282,\n        \"eze\": 383\n      },\n      {\n        \"timestamp\": \"2025-08-23T15:00:00.000Z\",\n        \"arn\": 187,\n        \"mad\": 343,\n        \"sin\": 475,\n        \"iad\": 236,\n        \"bos\": 142,\n        \"scl\": 500,\n        \"hkg\": 464,\n        \"ewr\": 173,\n        \"bom\": 297,\n        \"eze\": 412,\n        \"gru\": 223,\n        \"sea\": 182,\n        \"ord\": 216,\n        \"den\": 235,\n        \"yul\": 123,\n        \"syd\": 407,\n        \"gdl\": 876,\n        \"phx\": 191,\n        \"jnb\": 380,\n        \"bog\": 458,\n        \"lhr\": 141,\n        \"yyz\": 133,\n        \"fra\": 160,\n        \"gig\": 350,\n        \"atl\": 134,\n        \"nrt\": 441,\n        \"mia\": 179,\n        \"dfw\": 191,\n        \"sjc\": 134,\n        \"ams\": 251,\n        \"lax\": 242,\n        \"otp\": 315,\n        \"cdg\": 262\n      },\n      {\n        \"timestamp\": \"2025-08-23T15:30:00.000Z\",\n        \"gdl\": 598,\n        \"phx\": 194,\n        \"jnb\": 380,\n        \"bog\": 505,\n        \"lhr\": 149,\n        \"yyz\": 145,\n        \"fra\": 163,\n        \"gru\": 278,\n        \"sea\": 180,\n        \"ord\": 181,\n        \"den\": 232,\n        \"yul\": 124,\n        \"syd\": 371,\n        \"dfw\": 238,\n        \"sjc\": 135,\n        \"ams\": 175,\n        \"otp\": 294,\n        \"lax\": 174,\n        \"cdg\": 154,\n        \"gig\": 349,\n        \"atl\": 143,\n        \"nrt\": 369,\n        \"mia\": 189,\n        \"iad\": 165,\n        \"bos\": 105,\n        \"arn\": 214,\n        \"mad\": 343,\n        \"sin\": 445,\n        \"eze\": 396,\n        \"scl\": 447,\n        \"hkg\": 340,\n        \"ewr\": 136,\n        \"bom\": 296\n      },\n      {\n        \"timestamp\": \"2025-08-23T16:00:00.000Z\",\n        \"hkg\": 348,\n        \"scl\": 502,\n        \"bom\": 345,\n        \"ewr\": 193,\n        \"eze\": 464,\n        \"arn\": 195,\n        \"sin\": 422,\n        \"mad\": 337,\n        \"iad\": 197,\n        \"bos\": 125,\n        \"atl\": 137,\n        \"gig\": 348,\n        \"mia\": 245,\n        \"nrt\": 430,\n        \"lax\": 177,\n        \"otp\": 309,\n        \"ams\": 213,\n        \"sjc\": 132,\n        \"dfw\": 167,\n        \"cdg\": 153,\n        \"yul\": 126,\n        \"ord\": 184,\n        \"den\": 231,\n        \"sea\": 184,\n        \"gru\": 194,\n        \"syd\": 371,\n        \"bog\": 466,\n        \"jnb\": 520,\n        \"phx\": 212,\n        \"gdl\": 526,\n        \"fra\": 206,\n        \"yyz\": 183,\n        \"lhr\": 153\n      },\n      {\n        \"timestamp\": \"2025-08-23T16:30:00.000Z\",\n        \"otp\": 288,\n        \"lax\": 150,\n        \"ams\": 182,\n        \"sjc\": 184,\n        \"dfw\": 202,\n        \"cdg\": 171,\n        \"atl\": 142,\n        \"gig\": 375,\n        \"mia\": 173,\n        \"nrt\": 458,\n        \"bog\": 419,\n        \"jnb\": 523,\n        \"phx\": 199,\n        \"gdl\": 615,\n        \"fra\": 251,\n        \"yyz\": 133,\n        \"lhr\": 146,\n        \"yul\": 109,\n        \"ord\": 147,\n        \"den\": 231,\n        \"sea\": 171,\n        \"gru\": 199,\n        \"syd\": 407,\n        \"eze\": 390,\n        \"hkg\": 703,\n        \"scl\": 507,\n        \"bom\": 292,\n        \"ewr\": 157,\n        \"iad\": 232,\n        \"bos\": 122,\n        \"arn\": 225,\n        \"sin\": 507,\n        \"mad\": 337\n      },\n      {\n        \"timestamp\": \"2025-08-23T17:00:00.000Z\",\n        \"cdg\": 178,\n        \"lax\": 207,\n        \"otp\": 289,\n        \"dfw\": 189,\n        \"sjc\": 151,\n        \"ams\": 179,\n        \"nrt\": 420,\n        \"mia\": 159,\n        \"atl\": 147,\n        \"gig\": 245,\n        \"fra\": 176,\n        \"lhr\": 164,\n        \"yyz\": 137,\n        \"bog\": 421,\n        \"gdl\": 596,\n        \"phx\": 207,\n        \"jnb\": 404,\n        \"syd\": 343,\n        \"sea\": 169,\n        \"den\": 247,\n        \"ord\": 144,\n        \"yul\": 111,\n        \"gru\": 278,\n        \"eze\": 460,\n        \"bom\": 321,\n        \"ewr\": 129,\n        \"hkg\": 529,\n        \"scl\": 497,\n        \"bos\": 103,\n        \"iad\": 155,\n        \"sin\": 451,\n        \"mad\": 333,\n        \"arn\": 191\n      },\n      {\n        \"timestamp\": \"2025-08-23T17:30:00.000Z\",\n        \"bom\": 298,\n        \"ewr\": 154,\n        \"hkg\": 464,\n        \"scl\": 519,\n        \"eze\": 448,\n        \"sin\": 424,\n        \"mad\": 321,\n        \"arn\": 192,\n        \"bos\": 220,\n        \"iad\": 144,\n        \"nrt\": 426,\n        \"mia\": 181,\n        \"atl\": 163,\n        \"gig\": 353,\n        \"cdg\": 162,\n        \"otp\": 293,\n        \"lax\": 162,\n        \"dfw\": 203,\n        \"sjc\": 146,\n        \"ams\": 209,\n        \"syd\": 418,\n        \"sea\": 172,\n        \"den\": 229,\n        \"ord\": 137,\n        \"yul\": 127,\n        \"gru\": 282,\n        \"fra\": 153,\n        \"lhr\": 142,\n        \"yyz\": 141,\n        \"bog\": 385,\n        \"gdl\": 603,\n        \"phx\": 212,\n        \"jnb\": 381\n      },\n      {\n        \"timestamp\": \"2025-08-23T18:00:00.000Z\",\n        \"lax\": 184,\n        \"otp\": 278,\n        \"sjc\": 141,\n        \"dfw\": 197,\n        \"ams\": 174,\n        \"cdg\": 146,\n        \"atl\": 139,\n        \"gig\": 292,\n        \"mia\": 176,\n        \"nrt\": 269,\n        \"bog\": 414,\n        \"phx\": 274,\n        \"gdl\": 772,\n        \"jnb\": 462,\n        \"fra\": 158,\n        \"lhr\": 207,\n        \"yyz\": 153,\n        \"ord\": 175,\n        \"den\": 301,\n        \"sea\": 180,\n        \"yul\": 107,\n        \"gru\": 223,\n        \"syd\": 395,\n        \"eze\": 460,\n        \"hkg\": 346,\n        \"scl\": 495,\n        \"ewr\": 165,\n        \"bom\": 292,\n        \"iad\": 137,\n        \"bos\": 127,\n        \"arn\": 189,\n        \"sin\": 450,\n        \"mad\": 347\n      },\n      {\n        \"timestamp\": \"2025-08-23T18:30:00.000Z\",\n        \"hkg\": 474,\n        \"scl\": 462,\n        \"ewr\": 150,\n        \"bom\": 280,\n        \"eze\": 447,\n        \"arn\": 208,\n        \"sin\": 447,\n        \"mad\": 370,\n        \"iad\": 144,\n        \"bos\": 123,\n        \"atl\": 143,\n        \"gig\": 348,\n        \"mia\": 276,\n        \"nrt\": 417,\n        \"otp\": 284,\n        \"lax\": 158,\n        \"sjc\": 137,\n        \"dfw\": 227,\n        \"ams\": 209,\n        \"cdg\": 164,\n        \"ord\": 168,\n        \"den\": 273,\n        \"sea\": 181,\n        \"yul\": 117,\n        \"gru\": 220,\n        \"syd\": 417,\n        \"bog\": 438,\n        \"phx\": 206,\n        \"gdl\": 573,\n        \"jnb\": 488,\n        \"fra\": 162,\n        \"lhr\": 141,\n        \"yyz\": 151\n      },\n      {\n        \"timestamp\": \"2025-08-23T19:00:00.000Z\",\n        \"atl\": 139,\n        \"gig\": 353,\n        \"nrt\": 357,\n        \"mia\": 186,\n        \"otp\": 278,\n        \"lax\": 161,\n        \"dfw\": 197,\n        \"sjc\": 149,\n        \"ams\": 171,\n        \"cdg\": 149,\n        \"sea\": 167,\n        \"den\": 249,\n        \"ord\": 185,\n        \"yul\": 115,\n        \"gru\": 201,\n        \"syd\": 398,\n        \"bog\": 502,\n        \"gdl\": 619,\n        \"phx\": 199,\n        \"jnb\": 407,\n        \"fra\": 168,\n        \"lhr\": 148,\n        \"yyz\": 187,\n        \"hkg\": 491,\n        \"scl\": 429,\n        \"ewr\": 139,\n        \"bom\": 293,\n        \"eze\": 454,\n        \"arn\": 195,\n        \"sin\": 440,\n        \"mad\": 348,\n        \"iad\": 149,\n        \"bos\": 109\n      },\n      {\n        \"timestamp\": \"2025-08-23T19:30:00.000Z\",\n        \"yul\": 118,\n        \"den\": 246,\n        \"ord\": 281,\n        \"sea\": 166,\n        \"gru\": 252,\n        \"syd\": 396,\n        \"bog\": 380,\n        \"jnb\": 460,\n        \"phx\": 204,\n        \"gdl\": 644,\n        \"fra\": 161,\n        \"yyz\": 142,\n        \"lhr\": 167,\n        \"atl\": 140,\n        \"gig\": 352,\n        \"mia\": 179,\n        \"nrt\": 422,\n        \"lax\": 166,\n        \"otp\": 299,\n        \"ams\": 178,\n        \"sjc\": 135,\n        \"dfw\": 176,\n        \"cdg\": 156,\n        \"arn\": 200,\n        \"sin\": 430,\n        \"mad\": 322,\n        \"iad\": 451,\n        \"bos\": 148,\n        \"hkg\": 359,\n        \"scl\": 494,\n        \"ewr\": 165,\n        \"bom\": 303,\n        \"eze\": 487\n      },\n      {\n        \"timestamp\": \"2025-08-23T20:00:00.000Z\",\n        \"arn\": 226,\n        \"sin\": 544,\n        \"mad\": 369,\n        \"iad\": 434,\n        \"bos\": 114,\n        \"hkg\": 641,\n        \"scl\": 508,\n        \"ewr\": 185,\n        \"bom\": 283,\n        \"eze\": 457,\n        \"yul\": 112,\n        \"den\": 226,\n        \"ord\": 148,\n        \"sea\": 162,\n        \"gru\": 191,\n        \"syd\": 420,\n        \"bog\": 394,\n        \"jnb\": 485,\n        \"phx\": 222,\n        \"gdl\": 613,\n        \"fra\": 165,\n        \"yyz\": 144,\n        \"lhr\": 148,\n        \"atl\": 155,\n        \"gig\": 359,\n        \"mia\": 168,\n        \"nrt\": 428,\n        \"lax\": 157,\n        \"otp\": 283,\n        \"ams\": 182,\n        \"sjc\": 149,\n        \"dfw\": 194,\n        \"cdg\": 164\n      },\n      {\n        \"timestamp\": \"2025-08-23T20:30:00.000Z\",\n        \"gru\": 188,\n        \"yul\": 116,\n        \"sea\": 178,\n        \"ord\": 190,\n        \"den\": 230,\n        \"syd\": 439,\n        \"jnb\": 461,\n        \"gdl\": 594,\n        \"phx\": 212,\n        \"bog\": 405,\n        \"yyz\": 134,\n        \"lhr\": 136,\n        \"fra\": 162,\n        \"gig\": 355,\n        \"atl\": 138,\n        \"nrt\": 430,\n        \"mia\": 163,\n        \"ams\": 203,\n        \"dfw\": 209,\n        \"sjc\": 188,\n        \"otp\": 282,\n        \"lax\": 154,\n        \"cdg\": 150,\n        \"arn\": 191,\n        \"mad\": 337,\n        \"sin\": 471,\n        \"iad\": 132,\n        \"bos\": 120,\n        \"scl\": 425,\n        \"hkg\": 482,\n        \"ewr\": 132,\n        \"bom\": 296,\n        \"eze\": 383\n      },\n      {\n        \"timestamp\": \"2025-08-23T21:00:00.000Z\",\n        \"eze\": 468,\n        \"ewr\": 183,\n        \"bom\": 294,\n        \"scl\": 496,\n        \"hkg\": 641,\n        \"bos\": 107,\n        \"iad\": 166,\n        \"mad\": 359,\n        \"sin\": 417,\n        \"arn\": 193,\n        \"cdg\": 173,\n        \"ams\": 191,\n        \"dfw\": 201,\n        \"sjc\": 155,\n        \"lax\": 151,\n        \"otp\": 276,\n        \"nrt\": 434,\n        \"mia\": 182,\n        \"gig\": 312,\n        \"atl\": 149,\n        \"yyz\": 174,\n        \"lhr\": 188,\n        \"fra\": 160,\n        \"jnb\": 507,\n        \"gdl\": 627,\n        \"phx\": 192,\n        \"bog\": 440,\n        \"syd\": 418,\n        \"gru\": 248,\n        \"yul\": 119,\n        \"sea\": 164,\n        \"den\": 253,\n        \"ord\": 176\n      },\n      {\n        \"timestamp\": \"2025-08-23T21:30:00.000Z\",\n        \"bos\": 141,\n        \"iad\": 133,\n        \"mad\": 338,\n        \"sin\": 424,\n        \"arn\": 211,\n        \"eze\": 448,\n        \"ewr\": 184,\n        \"bom\": 279,\n        \"scl\": 508,\n        \"hkg\": 360,\n        \"lhr\": 175,\n        \"yyz\": 152,\n        \"fra\": 151,\n        \"phx\": 202,\n        \"gdl\": 618,\n        \"jnb\": 455,\n        \"bog\": 461,\n        \"syd\": 311,\n        \"gru\": 303,\n        \"den\": 239,\n        \"ord\": 172,\n        \"sea\": 164,\n        \"yul\": 109,\n        \"cdg\": 158,\n        \"sjc\": 135,\n        \"dfw\": 172,\n        \"ams\": 171,\n        \"otp\": 279,\n        \"lax\": 176,\n        \"mia\": 184,\n        \"nrt\": 306,\n        \"gig\": 353,\n        \"atl\": 132\n      },\n      {\n        \"timestamp\": \"2025-08-23T22:00:00.000Z\",\n        \"mad\": 337,\n        \"sin\": 376,\n        \"arn\": 197,\n        \"bos\": 119,\n        \"iad\": 174,\n        \"bom\": 319,\n        \"ewr\": 131,\n        \"scl\": 504,\n        \"hkg\": 350,\n        \"eze\": 487,\n        \"syd\": 406,\n        \"gru\": 209,\n        \"sea\": 160,\n        \"ord\": 194,\n        \"den\": 236,\n        \"yul\": 103,\n        \"lhr\": 150,\n        \"yyz\": 183,\n        \"fra\": 154,\n        \"gdl\": 656,\n        \"phx\": 205,\n        \"jnb\": 446,\n        \"bog\": 433,\n        \"nrt\": 372,\n        \"mia\": 180,\n        \"gig\": 352,\n        \"atl\": 172,\n        \"cdg\": 182,\n        \"dfw\": 202,\n        \"sjc\": 132,\n        \"ams\": 173,\n        \"otp\": 283,\n        \"lax\": 163\n      },\n      {\n        \"timestamp\": \"2025-08-23T22:30:00.000Z\",\n        \"eze\": 463,\n        \"bom\": 301,\n        \"ewr\": 131,\n        \"scl\": 436,\n        \"hkg\": 464,\n        \"bos\": 107,\n        \"iad\": 137,\n        \"mad\": 340,\n        \"sin\": 489,\n        \"arn\": 193,\n        \"cdg\": 143,\n        \"ams\": 179,\n        \"dfw\": 196,\n        \"sjc\": 138,\n        \"lax\": 179,\n        \"otp\": 392,\n        \"nrt\": 425,\n        \"mia\": 173,\n        \"gig\": 385,\n        \"atl\": 142,\n        \"yyz\": 132,\n        \"lhr\": 133,\n        \"fra\": 166,\n        \"jnb\": 373,\n        \"gdl\": 618,\n        \"phx\": 262,\n        \"bog\": 489,\n        \"syd\": 388,\n        \"gru\": 191,\n        \"yul\": 105,\n        \"sea\": 170,\n        \"den\": 235,\n        \"ord\": 176\n      },\n      {\n        \"timestamp\": \"2025-08-23T23:00:00.000Z\",\n        \"cdg\": 153,\n        \"ams\": 182,\n        \"sjc\": 223,\n        \"dfw\": 191,\n        \"lax\": 151,\n        \"otp\": 328,\n        \"mia\": 179,\n        \"nrt\": 424,\n        \"gig\": 365,\n        \"atl\": 138,\n        \"yyz\": 145,\n        \"lhr\": 142,\n        \"fra\": 215,\n        \"jnb\": 364,\n        \"phx\": 227,\n        \"gdl\": 614,\n        \"bog\": 497,\n        \"syd\": 346,\n        \"gru\": 200,\n        \"yul\": 112,\n        \"ord\": 179,\n        \"den\": 230,\n        \"sea\": 183,\n        \"eze\": 456,\n        \"bom\": 296,\n        \"ewr\": 180,\n        \"scl\": 497,\n        \"hkg\": 532,\n        \"bos\": 104,\n        \"iad\": 501,\n        \"mad\": 344,\n        \"sin\": 559,\n        \"arn\": 198\n      },\n      {\n        \"timestamp\": \"2025-08-23T23:30:00.000Z\",\n        \"bom\": 289,\n        \"ewr\": 135,\n        \"scl\": 495,\n        \"hkg\": 390,\n        \"eze\": 436,\n        \"mad\": 325,\n        \"sin\": 412,\n        \"arn\": 199,\n        \"bos\": 102,\n        \"iad\": 189,\n        \"mia\": 175,\n        \"nrt\": 427,\n        \"gig\": 344,\n        \"atl\": 156,\n        \"cdg\": 153,\n        \"ams\": 177,\n        \"sjc\": 136,\n        \"dfw\": 209,\n        \"otp\": 328,\n        \"lax\": 151,\n        \"syd\": 344,\n        \"gru\": 275,\n        \"yul\": 107,\n        \"ord\": 140,\n        \"den\": 215,\n        \"sea\": 165,\n        \"yyz\": 139,\n        \"lhr\": 138,\n        \"fra\": 156,\n        \"jnb\": 484,\n        \"phx\": 190,\n        \"gdl\": 535,\n        \"bog\": 399\n      },\n      {\n        \"timestamp\": \"2025-08-24T00:00:00.000Z\",\n        \"eze\": 460,\n        \"ewr\": 188,\n        \"bom\": 286,\n        \"scl\": 442,\n        \"hkg\": 324,\n        \"bos\": 114,\n        \"iad\": 133,\n        \"mad\": 338,\n        \"sin\": 417,\n        \"arn\": 191,\n        \"cdg\": 165,\n        \"sjc\": 133,\n        \"dfw\": 173,\n        \"ams\": 188,\n        \"otp\": 319,\n        \"lax\": 200,\n        \"mia\": 168,\n        \"nrt\": 426,\n        \"gig\": 353,\n        \"atl\": 140,\n        \"lhr\": 148,\n        \"yyz\": 134,\n        \"fra\": 206,\n        \"phx\": 210,\n        \"gdl\": 574,\n        \"jnb\": 463,\n        \"bog\": 421,\n        \"syd\": 308,\n        \"gru\": 320,\n        \"den\": 246,\n        \"ord\": 176,\n        \"sea\": 169,\n        \"yul\": 131\n      },\n      {\n        \"timestamp\": \"2025-08-24T00:30:00.000Z\",\n        \"iad\": 323,\n        \"bos\": 103,\n        \"arn\": 180,\n        \"sin\": 423,\n        \"mad\": 328,\n        \"eze\": 396,\n        \"hkg\": 321,\n        \"scl\": 485,\n        \"ewr\": 158,\n        \"bom\": 288,\n        \"bog\": 366,\n        \"gdl\": 579,\n        \"phx\": 200,\n        \"jnb\": 438,\n        \"fra\": 150,\n        \"lhr\": 160,\n        \"yyz\": 137,\n        \"sea\": 158,\n        \"den\": 243,\n        \"ord\": 193,\n        \"yul\": 116,\n        \"gru\": 206,\n        \"syd\": 301,\n        \"lax\": 156,\n        \"otp\": 279,\n        \"dfw\": 169,\n        \"sjc\": 138,\n        \"ams\": 223,\n        \"cdg\": 140,\n        \"atl\": 139,\n        \"gig\": 396,\n        \"nrt\": 307,\n        \"mia\": 210\n      },\n      {\n        \"timestamp\": \"2025-08-24T01:00:00.000Z\",\n        \"ord\": 132,\n        \"den\": 245,\n        \"sea\": 164,\n        \"yul\": 162,\n        \"gru\": 189,\n        \"syd\": 433,\n        \"bog\": 383,\n        \"phx\": 312,\n        \"gdl\": 574,\n        \"jnb\": 517,\n        \"fra\": 162,\n        \"lhr\": 158,\n        \"yyz\": 151,\n        \"atl\": 155,\n        \"gig\": 341,\n        \"mia\": 154,\n        \"nrt\": 349,\n        \"otp\": 312,\n        \"lax\": 150,\n        \"sjc\": 153,\n        \"dfw\": 183,\n        \"ams\": 191,\n        \"cdg\": 173,\n        \"arn\": 188,\n        \"sin\": 427,\n        \"mad\": 325,\n        \"iad\": 167,\n        \"bos\": 112,\n        \"hkg\": 473,\n        \"scl\": 481,\n        \"bom\": 292,\n        \"ewr\": 125,\n        \"eze\": 467\n      },\n      {\n        \"timestamp\": \"2025-08-24T01:30:00.000Z\",\n        \"ewr\": 129,\n        \"bom\": 288,\n        \"scl\": 503,\n        \"hkg\": 377,\n        \"eze\": 437,\n        \"mad\": 330,\n        \"sin\": 580,\n        \"arn\": 197,\n        \"bos\": 109,\n        \"iad\": 212,\n        \"mia\": 159,\n        \"nrt\": 374,\n        \"gig\": 345,\n        \"atl\": 141,\n        \"cdg\": 173,\n        \"sjc\": 185,\n        \"dfw\": 191,\n        \"ams\": 212,\n        \"lax\": 240,\n        \"otp\": 289,\n        \"syd\": 328,\n        \"gru\": 273,\n        \"den\": 214,\n        \"ord\": 243,\n        \"sea\": 162,\n        \"yul\": 123,\n        \"lhr\": 146,\n        \"yyz\": 131,\n        \"fra\": 150,\n        \"phx\": 188,\n        \"gdl\": 593,\n        \"jnb\": 400,\n        \"bog\": 441\n      },\n      {\n        \"timestamp\": \"2025-08-24T02:00:00.000Z\",\n        \"eze\": 405,\n        \"hkg\": 319,\n        \"scl\": 478,\n        \"bom\": 289,\n        \"ewr\": 120,\n        \"iad\": 204,\n        \"bos\": 105,\n        \"arn\": 192,\n        \"sin\": 451,\n        \"mad\": 374,\n        \"lax\": 156,\n        \"otp\": 289,\n        \"ams\": 174,\n        \"dfw\": 166,\n        \"sjc\": 147,\n        \"cdg\": 161,\n        \"atl\": 164,\n        \"gig\": 359,\n        \"nrt\": 341,\n        \"mia\": 158,\n        \"bog\": 375,\n        \"jnb\": 484,\n        \"gdl\": 586,\n        \"phx\": 198,\n        \"fra\": 151,\n        \"yyz\": 144,\n        \"lhr\": 147,\n        \"yul\": 111,\n        \"sea\": 160,\n        \"ord\": 131,\n        \"den\": 221,\n        \"gru\": 285,\n        \"syd\": 393\n      },\n      {\n        \"timestamp\": \"2025-08-24T02:30:00.000Z\",\n        \"bos\": 119,\n        \"iad\": 133,\n        \"mad\": 370,\n        \"sin\": 429,\n        \"arn\": 186,\n        \"eze\": 440,\n        \"bom\": 274,\n        \"ewr\": 147,\n        \"scl\": 486,\n        \"hkg\": 322,\n        \"yyz\": 150,\n        \"lhr\": 143,\n        \"fra\": 158,\n        \"jnb\": 511,\n        \"phx\": 193,\n        \"gdl\": 583,\n        \"bog\": 477,\n        \"syd\": 346,\n        \"gru\": 210,\n        \"yul\": 105,\n        \"ord\": 141,\n        \"den\": 234,\n        \"sea\": 176,\n        \"cdg\": 165,\n        \"ams\": 234,\n        \"sjc\": 154,\n        \"dfw\": 191,\n        \"otp\": 289,\n        \"lax\": 154,\n        \"mia\": 167,\n        \"nrt\": 348,\n        \"gig\": 350,\n        \"atl\": 249\n      },\n      {\n        \"timestamp\": \"2025-08-24T03:00:00.000Z\",\n        \"nrt\": 422,\n        \"mia\": 191,\n        \"gig\": 352,\n        \"atl\": 174,\n        \"cdg\": 178,\n        \"dfw\": 167,\n        \"sjc\": 131,\n        \"ams\": 174,\n        \"lax\": 153,\n        \"otp\": 329,\n        \"syd\": 372,\n        \"gru\": 200,\n        \"sea\": 208,\n        \"den\": 235,\n        \"ord\": 144,\n        \"yul\": 124,\n        \"lhr\": 139,\n        \"yyz\": 146,\n        \"fra\": 168,\n        \"gdl\": 726,\n        \"phx\": 204,\n        \"jnb\": 387,\n        \"bog\": 385,\n        \"bom\": 313,\n        \"ewr\": 125,\n        \"scl\": 484,\n        \"hkg\": 352,\n        \"eze\": 381,\n        \"mad\": 327,\n        \"sin\": 514,\n        \"arn\": 202,\n        \"bos\": 105,\n        \"iad\": 131\n      },\n      {\n        \"timestamp\": \"2025-08-24T03:30:00.000Z\",\n        \"eze\": 441,\n        \"ewr\": 134,\n        \"bom\": 286,\n        \"hkg\": 317,\n        \"scl\": 490,\n        \"bos\": 111,\n        \"iad\": 140,\n        \"sin\": 452,\n        \"mad\": 418,\n        \"arn\": 203,\n        \"cdg\": 174,\n        \"lax\": 151,\n        \"otp\": 281,\n        \"sjc\": 249,\n        \"dfw\": 200,\n        \"ams\": 175,\n        \"mia\": 165,\n        \"nrt\": 423,\n        \"atl\": 292,\n        \"gig\": 305,\n        \"fra\": 154,\n        \"lhr\": 139,\n        \"yyz\": 141,\n        \"bog\": 373,\n        \"phx\": 273,\n        \"gdl\": 600,\n        \"jnb\": 453,\n        \"syd\": 314,\n        \"ord\": 146,\n        \"den\": 270,\n        \"sea\": 170,\n        \"yul\": 99,\n        \"gru\": 185\n      },\n      {\n        \"timestamp\": \"2025-08-24T04:00:00.000Z\",\n        \"arn\": 188,\n        \"mad\": 381,\n        \"sin\": 453,\n        \"iad\": 136,\n        \"bos\": 105,\n        \"scl\": 504,\n        \"hkg\": 467,\n        \"ewr\": 138,\n        \"bom\": 297,\n        \"eze\": 462,\n        \"gru\": 230,\n        \"sea\": 165,\n        \"ord\": 141,\n        \"den\": 237,\n        \"yul\": 105,\n        \"syd\": 371,\n        \"gdl\": 570,\n        \"phx\": 195,\n        \"jnb\": 456,\n        \"bog\": 376,\n        \"lhr\": 139,\n        \"yyz\": 147,\n        \"fra\": 153,\n        \"gig\": 362,\n        \"atl\": 179,\n        \"nrt\": 429,\n        \"mia\": 157,\n        \"dfw\": 172,\n        \"sjc\": 154,\n        \"ams\": 191,\n        \"lax\": 165,\n        \"otp\": 286,\n        \"cdg\": 155\n      },\n      {\n        \"timestamp\": \"2025-08-24T04:30:00.000Z\",\n        \"fra\": 149,\n        \"yyz\": 149,\n        \"lhr\": 151,\n        \"bog\": 407,\n        \"jnb\": 383,\n        \"gdl\": 550,\n        \"phx\": 193,\n        \"syd\": 423,\n        \"yul\": 108,\n        \"sea\": 169,\n        \"ord\": 159,\n        \"den\": 246,\n        \"gru\": 199,\n        \"cdg\": 154,\n        \"otp\": 317,\n        \"lax\": 149,\n        \"ams\": 185,\n        \"dfw\": 181,\n        \"sjc\": 132,\n        \"nrt\": 385,\n        \"mia\": 159,\n        \"atl\": 206,\n        \"gig\": 368,\n        \"bos\": 107,\n        \"iad\": 136,\n        \"sin\": 455,\n        \"mad\": 344,\n        \"arn\": 205,\n        \"eze\": 442,\n        \"ewr\": 141,\n        \"bom\": 299,\n        \"hkg\": 347,\n        \"scl\": 531\n      },\n      {\n        \"timestamp\": \"2025-08-24T05:00:00.000Z\",\n        \"bos\": 116,\n        \"iad\": 132,\n        \"mad\": 365,\n        \"sin\": 417,\n        \"arn\": 200,\n        \"eze\": 454,\n        \"bom\": 329,\n        \"ewr\": 127,\n        \"scl\": 484,\n        \"hkg\": 359,\n        \"yyz\": 147,\n        \"lhr\": 146,\n        \"fra\": 156,\n        \"jnb\": 389,\n        \"gdl\": 622,\n        \"phx\": 196,\n        \"bog\": 410,\n        \"syd\": 366,\n        \"gru\": 259,\n        \"yul\": 104,\n        \"sea\": 171,\n        \"ord\": 160,\n        \"den\": 219,\n        \"cdg\": 158,\n        \"ams\": 185,\n        \"dfw\": 211,\n        \"sjc\": 154,\n        \"lax\": 183,\n        \"otp\": 285,\n        \"nrt\": 422,\n        \"mia\": 182,\n        \"gig\": 341,\n        \"atl\": 149\n      },\n      {\n        \"timestamp\": \"2025-08-24T05:30:00.000Z\",\n        \"gig\": 343,\n        \"atl\": 148,\n        \"mia\": 153,\n        \"nrt\": 343,\n        \"ams\": 199,\n        \"sjc\": 224,\n        \"dfw\": 178,\n        \"otp\": 278,\n        \"lax\": 213,\n        \"cdg\": 216,\n        \"gru\": 276,\n        \"yul\": 102,\n        \"ord\": 141,\n        \"den\": 269,\n        \"sea\": 172,\n        \"syd\": 442,\n        \"jnb\": 461,\n        \"phx\": 196,\n        \"gdl\": 584,\n        \"bog\": 380,\n        \"yyz\": 194,\n        \"lhr\": 137,\n        \"fra\": 164,\n        \"scl\": 480,\n        \"hkg\": 362,\n        \"ewr\": 122,\n        \"bom\": 296,\n        \"eze\": 436,\n        \"arn\": 199,\n        \"mad\": 329,\n        \"sin\": 420,\n        \"iad\": 136,\n        \"bos\": 104\n      },\n      {\n        \"timestamp\": \"2025-08-24T06:00:00.000Z\",\n        \"sin\": 450,\n        \"mad\": 337,\n        \"arn\": 231,\n        \"bos\": 103,\n        \"iad\": 123,\n        \"bom\": 282,\n        \"ewr\": 128,\n        \"hkg\": 332,\n        \"scl\": 482,\n        \"eze\": 437,\n        \"syd\": 366,\n        \"yul\": 131,\n        \"den\": 235,\n        \"ord\": 152,\n        \"sea\": 177,\n        \"gru\": 180,\n        \"fra\": 152,\n        \"yyz\": 141,\n        \"lhr\": 207,\n        \"bog\": 466,\n        \"jnb\": 507,\n        \"phx\": 235,\n        \"gdl\": 647,\n        \"mia\": 148,\n        \"nrt\": 349,\n        \"atl\": 125,\n        \"gig\": 351,\n        \"cdg\": 150,\n        \"otp\": 300,\n        \"lax\": 155,\n        \"ams\": 185,\n        \"sjc\": 129,\n        \"dfw\": 173\n      },\n      {\n        \"timestamp\": \"2025-08-24T06:30:00.000Z\",\n        \"phx\": 242,\n        \"gdl\": 597,\n        \"jnb\": 512,\n        \"bog\": 480,\n        \"lhr\": 138,\n        \"yyz\": 154,\n        \"fra\": 184,\n        \"gru\": 7780,\n        \"den\": 229,\n        \"ord\": 174,\n        \"sea\": 191,\n        \"yul\": 107,\n        \"syd\": 337,\n        \"sjc\": 164,\n        \"dfw\": 207,\n        \"ams\": 267,\n        \"lax\": 154,\n        \"otp\": 266,\n        \"cdg\": 187,\n        \"gig\": 344,\n        \"atl\": 141,\n        \"mia\": 172,\n        \"nrt\": 430,\n        \"iad\": 138,\n        \"bos\": 109,\n        \"arn\": 201,\n        \"mad\": 343,\n        \"sin\": 451,\n        \"eze\": 444,\n        \"scl\": 485,\n        \"hkg\": 367,\n        \"bom\": 485,\n        \"ewr\": 173\n      },\n      {\n        \"timestamp\": \"2025-08-24T07:00:00.000Z\",\n        \"iad\": 176,\n        \"bos\": 112,\n        \"arn\": 201,\n        \"sin\": 485,\n        \"mad\": 333,\n        \"eze\": 435,\n        \"hkg\": 354,\n        \"scl\": 486,\n        \"ewr\": 128,\n        \"bom\": 338,\n        \"bog\": 629,\n        \"phx\": 194,\n        \"gdl\": 629,\n        \"jnb\": 436,\n        \"fra\": 156,\n        \"lhr\": 154,\n        \"yyz\": 155,\n        \"den\": 224,\n        \"ord\": 132,\n        \"sea\": 163,\n        \"yul\": 120,\n        \"gru\": 333,\n        \"syd\": 418,\n        \"otp\": 271,\n        \"lax\": 189,\n        \"sjc\": 185,\n        \"dfw\": 184,\n        \"ams\": 175,\n        \"cdg\": 148,\n        \"atl\": 124,\n        \"gig\": 353,\n        \"mia\": 187,\n        \"nrt\": 348\n      },\n      {\n        \"timestamp\": \"2025-08-24T07:30:00.000Z\",\n        \"gru\": 308,\n        \"sea\": 169,\n        \"ord\": 155,\n        \"den\": 239,\n        \"yul\": 151,\n        \"syd\": 438,\n        \"gdl\": 623,\n        \"phx\": 192,\n        \"jnb\": 366,\n        \"bog\": 728,\n        \"lhr\": 248,\n        \"yyz\": 128,\n        \"fra\": 266,\n        \"gig\": 353,\n        \"atl\": 152,\n        \"nrt\": 383,\n        \"mia\": 160,\n        \"dfw\": 213,\n        \"sjc\": 136,\n        \"ams\": 175,\n        \"otp\": 304,\n        \"lax\": 232,\n        \"cdg\": 148,\n        \"arn\": 305,\n        \"mad\": 340,\n        \"sin\": 454,\n        \"iad\": 145,\n        \"bos\": 120,\n        \"scl\": 477,\n        \"hkg\": 344,\n        \"bom\": 298,\n        \"ewr\": 124,\n        \"eze\": 459\n      },\n      {\n        \"timestamp\": \"2025-08-24T08:00:00.000Z\",\n        \"eze\": 451,\n        \"ewr\": 208,\n        \"bom\": 364,\n        \"hkg\": 442,\n        \"scl\": 477,\n        \"bos\": 105,\n        \"iad\": 165,\n        \"sin\": 483,\n        \"mad\": 349,\n        \"arn\": 198,\n        \"cdg\": 155,\n        \"lax\": 169,\n        \"otp\": 279,\n        \"sjc\": 160,\n        \"dfw\": 223,\n        \"ams\": 190,\n        \"mia\": 178,\n        \"nrt\": 377,\n        \"atl\": 119,\n        \"gig\": 350,\n        \"fra\": 154,\n        \"lhr\": 174,\n        \"yyz\": 129,\n        \"bog\": 391,\n        \"phx\": 227,\n        \"gdl\": 575,\n        \"jnb\": 491,\n        \"syd\": 418,\n        \"ord\": 167,\n        \"den\": 226,\n        \"sea\": 178,\n        \"yul\": 102,\n        \"gru\": 322\n      },\n      {\n        \"timestamp\": \"2025-08-24T08:30:00.000Z\",\n        \"iad\": 153,\n        \"bos\": 95,\n        \"arn\": 186,\n        \"mad\": 350,\n        \"sin\": 500,\n        \"eze\": 446,\n        \"scl\": 507,\n        \"hkg\": 437,\n        \"ewr\": 135,\n        \"bom\": 293,\n        \"gdl\": 562,\n        \"phx\": 195,\n        \"jnb\": 576,\n        \"bog\": 412,\n        \"lhr\": 177,\n        \"yyz\": 151,\n        \"fra\": 174,\n        \"gru\": 262,\n        \"sea\": 221,\n        \"ord\": 177,\n        \"den\": 245,\n        \"yul\": 103,\n        \"syd\": 336,\n        \"dfw\": 262,\n        \"sjc\": 132,\n        \"ams\": 192,\n        \"otp\": 287,\n        \"lax\": 174,\n        \"cdg\": 152,\n        \"gig\": 310,\n        \"atl\": 148,\n        \"nrt\": 427,\n        \"mia\": 190\n      },\n      {\n        \"timestamp\": \"2025-08-24T09:00:00.000Z\",\n        \"hkg\": 383,\n        \"scl\": 490,\n        \"bom\": 296,\n        \"ewr\": 136,\n        \"eze\": 440,\n        \"arn\": 196,\n        \"sin\": 431,\n        \"mad\": 341,\n        \"iad\": 143,\n        \"bos\": 108,\n        \"atl\": 144,\n        \"gig\": 352,\n        \"mia\": 157,\n        \"nrt\": 307,\n        \"lax\": 162,\n        \"otp\": 289,\n        \"ams\": 189,\n        \"sjc\": 273,\n        \"dfw\": 180,\n        \"cdg\": 158,\n        \"yul\": 113,\n        \"ord\": 179,\n        \"den\": 264,\n        \"sea\": 179,\n        \"gru\": 201,\n        \"syd\": 430,\n        \"bog\": 478,\n        \"jnb\": 520,\n        \"phx\": 189,\n        \"gdl\": 579,\n        \"fra\": 154,\n        \"yyz\": 132,\n        \"lhr\": 155\n      },\n      {\n        \"timestamp\": \"2025-08-24T09:30:00.000Z\",\n        \"syd\": 422,\n        \"gru\": 228,\n        \"yul\": 112,\n        \"den\": 259,\n        \"ord\": 146,\n        \"sea\": 175,\n        \"yyz\": 150,\n        \"lhr\": 145,\n        \"fra\": 158,\n        \"jnb\": 536,\n        \"phx\": 218,\n        \"gdl\": 582,\n        \"bog\": 464,\n        \"mia\": 164,\n        \"nrt\": 340,\n        \"gig\": 365,\n        \"atl\": 131,\n        \"cdg\": 141,\n        \"ams\": 242,\n        \"sjc\": 139,\n        \"dfw\": 173,\n        \"otp\": 279,\n        \"lax\": 152,\n        \"mad\": 342,\n        \"sin\": 485,\n        \"arn\": 194,\n        \"bos\": 110,\n        \"iad\": 164,\n        \"ewr\": 185,\n        \"bom\": 333,\n        \"scl\": 498,\n        \"hkg\": 337,\n        \"eze\": 439\n      },\n      {\n        \"timestamp\": \"2025-08-24T10:00:00.000Z\",\n        \"eze\": 436,\n        \"scl\": 501,\n        \"hkg\": 353,\n        \"bom\": 292,\n        \"ewr\": 151,\n        \"iad\": 161,\n        \"bos\": 117,\n        \"arn\": 204,\n        \"mad\": 339,\n        \"sin\": 446,\n        \"ams\": 196,\n        \"dfw\": 208,\n        \"sjc\": 139,\n        \"otp\": 277,\n        \"lax\": 152,\n        \"cdg\": 148,\n        \"gig\": 349,\n        \"atl\": 124,\n        \"nrt\": 438,\n        \"mia\": 193,\n        \"jnb\": 517,\n        \"gdl\": 738,\n        \"phx\": 194,\n        \"bog\": 372,\n        \"yyz\": 153,\n        \"lhr\": 170,\n        \"fra\": 172,\n        \"gru\": 193,\n        \"yul\": 107,\n        \"sea\": 178,\n        \"den\": 236,\n        \"ord\": 151,\n        \"syd\": 352\n      },\n      {\n        \"timestamp\": \"2025-08-24T10:30:00.000Z\",\n        \"bos\": 117,\n        \"iad\": 217,\n        \"sin\": 433,\n        \"mad\": 372,\n        \"arn\": 199,\n        \"eze\": 458,\n        \"bom\": 311,\n        \"ewr\": 128,\n        \"hkg\": 380,\n        \"scl\": 483,\n        \"fra\": 154,\n        \"yyz\": 140,\n        \"lhr\": 143,\n        \"bog\": 415,\n        \"jnb\": 549,\n        \"phx\": 193,\n        \"gdl\": 614,\n        \"syd\": 356,\n        \"yul\": 117,\n        \"den\": 232,\n        \"ord\": 157,\n        \"sea\": 174,\n        \"gru\": 220,\n        \"cdg\": 161,\n        \"lax\": 155,\n        \"otp\": 270,\n        \"ams\": 180,\n        \"sjc\": 133,\n        \"dfw\": 204,\n        \"mia\": 189,\n        \"nrt\": 431,\n        \"atl\": 133,\n        \"gig\": 353\n      },\n      {\n        \"timestamp\": \"2025-08-24T11:00:00.000Z\",\n        \"ewr\": 131,\n        \"bom\": 295,\n        \"scl\": 478,\n        \"hkg\": 343,\n        \"eze\": 484,\n        \"mad\": 344,\n        \"sin\": 433,\n        \"arn\": 199,\n        \"bos\": 119,\n        \"iad\": 153,\n        \"nrt\": 427,\n        \"mia\": 169,\n        \"gig\": 361,\n        \"atl\": 134,\n        \"cdg\": 152,\n        \"dfw\": 183,\n        \"sjc\": 133,\n        \"ams\": 185,\n        \"otp\": 288,\n        \"lax\": 191,\n        \"syd\": 352,\n        \"gru\": 218,\n        \"sea\": 166,\n        \"den\": 231,\n        \"ord\": 174,\n        \"yul\": 102,\n        \"lhr\": 148,\n        \"yyz\": 178,\n        \"fra\": 174,\n        \"gdl\": 505,\n        \"phx\": 209,\n        \"jnb\": 470,\n        \"bog\": 428\n      },\n      {\n        \"timestamp\": \"2025-08-24T11:30:00.000Z\",\n        \"cdg\": 194,\n        \"otp\": 334,\n        \"lax\": 159,\n        \"sjc\": 233,\n        \"dfw\": 308,\n        \"ams\": 174,\n        \"mia\": 176,\n        \"nrt\": 428,\n        \"atl\": 121,\n        \"gig\": 357,\n        \"fra\": 175,\n        \"lhr\": 148,\n        \"yyz\": 162,\n        \"bog\": 514,\n        \"phx\": 202,\n        \"gdl\": 582,\n        \"jnb\": 515,\n        \"syd\": 415,\n        \"ord\": 155,\n        \"den\": 233,\n        \"sea\": 201,\n        \"yul\": 116,\n        \"gru\": 199,\n        \"eze\": 439,\n        \"bom\": 281,\n        \"ewr\": 164,\n        \"hkg\": 346,\n        \"scl\": 482,\n        \"bos\": 130,\n        \"iad\": 156,\n        \"sin\": 446,\n        \"mad\": 338,\n        \"arn\": 203\n      },\n      {\n        \"timestamp\": \"2025-08-24T12:00:00.000Z\",\n        \"gru\": 267,\n        \"sea\": 173,\n        \"ord\": 183,\n        \"den\": 232,\n        \"yul\": 106,\n        \"syd\": 417,\n        \"gdl\": 617,\n        \"phx\": 204,\n        \"jnb\": 400,\n        \"bog\": 455,\n        \"lhr\": 162,\n        \"yyz\": 148,\n        \"fra\": 158,\n        \"gig\": 358,\n        \"atl\": 128,\n        \"nrt\": 426,\n        \"mia\": 183,\n        \"dfw\": 290,\n        \"sjc\": 147,\n        \"ams\": 179,\n        \"otp\": 286,\n        \"lax\": 159,\n        \"cdg\": 214,\n        \"arn\": 222,\n        \"mad\": 338,\n        \"sin\": 431,\n        \"iad\": 149,\n        \"bos\": 105,\n        \"scl\": 488,\n        \"hkg\": 484,\n        \"bom\": 296,\n        \"ewr\": 182,\n        \"eze\": 435\n      },\n      {\n        \"timestamp\": \"2025-08-24T12:30:00.000Z\",\n        \"dfw\": 187,\n        \"sjc\": 128,\n        \"ams\": 171,\n        \"otp\": 318,\n        \"lax\": 172,\n        \"cdg\": 147,\n        \"gig\": 343,\n        \"atl\": 131,\n        \"nrt\": 347,\n        \"mia\": 159,\n        \"gdl\": 608,\n        \"phx\": 197,\n        \"jnb\": 508,\n        \"bog\": 387,\n        \"lhr\": 144,\n        \"yyz\": 189,\n        \"fra\": 158,\n        \"gru\": 207,\n        \"sea\": 201,\n        \"ord\": 174,\n        \"den\": 222,\n        \"yul\": 106,\n        \"syd\": 396,\n        \"eze\": 458,\n        \"scl\": 495,\n        \"hkg\": 353,\n        \"ewr\": 168,\n        \"bom\": 301,\n        \"iad\": 196,\n        \"bos\": 109,\n        \"arn\": 197,\n        \"mad\": 339,\n        \"sin\": 495\n      },\n      {\n        \"timestamp\": \"2025-08-24T13:00:00.000Z\",\n        \"eze\": 429,\n        \"hkg\": 355,\n        \"scl\": 534,\n        \"bom\": 303,\n        \"ewr\": 127,\n        \"iad\": 198,\n        \"bos\": 108,\n        \"arn\": 191,\n        \"sin\": 440,\n        \"mad\": 337,\n        \"lax\": 153,\n        \"otp\": 276,\n        \"dfw\": 192,\n        \"sjc\": 159,\n        \"ams\": 178,\n        \"cdg\": 147,\n        \"atl\": 169,\n        \"gig\": 349,\n        \"nrt\": 370,\n        \"mia\": 167,\n        \"bog\": 376,\n        \"gdl\": 546,\n        \"phx\": 204,\n        \"jnb\": 462,\n        \"fra\": 153,\n        \"lhr\": 166,\n        \"yyz\": 137,\n        \"sea\": 170,\n        \"ord\": 132,\n        \"den\": 233,\n        \"yul\": 105,\n        \"gru\": 320,\n        \"syd\": 316\n      },\n      {\n        \"timestamp\": \"2025-08-24T13:30:00.000Z\",\n        \"ewr\": 182,\n        \"bom\": 303,\n        \"scl\": 477,\n        \"hkg\": 374,\n        \"eze\": 412,\n        \"mad\": 352,\n        \"sin\": 472,\n        \"arn\": 265,\n        \"bos\": 109,\n        \"iad\": 187,\n        \"mia\": 169,\n        \"nrt\": 433,\n        \"gig\": 343,\n        \"atl\": 152,\n        \"cdg\": 148,\n        \"ams\": 176,\n        \"sjc\": 139,\n        \"dfw\": 192,\n        \"otp\": 284,\n        \"lax\": 178,\n        \"syd\": 364,\n        \"gru\": 196,\n        \"yul\": 161,\n        \"den\": 224,\n        \"ord\": 186,\n        \"sea\": 176,\n        \"yyz\": 156,\n        \"lhr\": 162,\n        \"fra\": 207,\n        \"jnb\": 510,\n        \"phx\": 262,\n        \"gdl\": 599,\n        \"bog\": 443\n      },\n      {\n        \"timestamp\": \"2025-08-24T14:00:00.000Z\",\n        \"yul\": 122,\n        \"ord\": 171,\n        \"den\": 237,\n        \"sea\": 172,\n        \"gru\": 191,\n        \"syd\": 364,\n        \"bog\": 412,\n        \"jnb\": 384,\n        \"phx\": 190,\n        \"gdl\": 647,\n        \"fra\": 159,\n        \"yyz\": 133,\n        \"lhr\": 141,\n        \"atl\": 142,\n        \"gig\": 344,\n        \"mia\": 160,\n        \"nrt\": 444,\n        \"otp\": 270,\n        \"lax\": 157,\n        \"ams\": 178,\n        \"sjc\": 127,\n        \"dfw\": 173,\n        \"cdg\": 150,\n        \"arn\": 231,\n        \"sin\": 439,\n        \"mad\": 341,\n        \"iad\": 139,\n        \"bos\": 133,\n        \"hkg\": 395,\n        \"scl\": 484,\n        \"bom\": 284,\n        \"ewr\": 161,\n        \"eze\": 449\n      },\n      {\n        \"timestamp\": \"2025-08-24T14:30:00.000Z\",\n        \"cdg\": 153,\n        \"lax\": 156,\n        \"otp\": 280,\n        \"ams\": 192,\n        \"sjc\": 142,\n        \"dfw\": 192,\n        \"mia\": 173,\n        \"nrt\": 438,\n        \"atl\": 152,\n        \"gig\": 345,\n        \"fra\": 156,\n        \"yyz\": 135,\n        \"lhr\": 162,\n        \"bog\": 445,\n        \"jnb\": 445,\n        \"phx\": 217,\n        \"gdl\": 599,\n        \"syd\": 377,\n        \"yul\": 125,\n        \"den\": 238,\n        \"ord\": 147,\n        \"sea\": 167,\n        \"gru\": 216,\n        \"eze\": 508,\n        \"bom\": 368,\n        \"ewr\": 163,\n        \"hkg\": 440,\n        \"scl\": 497,\n        \"bos\": 112,\n        \"iad\": 179,\n        \"sin\": 571,\n        \"mad\": 335,\n        \"arn\": 188\n      },\n      {\n        \"timestamp\": \"2025-08-24T15:00:00.000Z\",\n        \"eze\": 439,\n        \"ewr\": 126,\n        \"bom\": 304,\n        \"scl\": 461,\n        \"hkg\": 422,\n        \"bos\": 120,\n        \"iad\": 153,\n        \"mad\": 375,\n        \"sin\": 408,\n        \"arn\": 198,\n        \"cdg\": 144,\n        \"ams\": 185,\n        \"sjc\": 142,\n        \"dfw\": 189,\n        \"otp\": 285,\n        \"lax\": 155,\n        \"mia\": 173,\n        \"nrt\": 266,\n        \"gig\": 347,\n        \"atl\": 127,\n        \"yyz\": 144,\n        \"lhr\": 136,\n        \"fra\": 162,\n        \"jnb\": 474,\n        \"phx\": 199,\n        \"gdl\": 602,\n        \"bog\": 432,\n        \"syd\": 321,\n        \"gru\": 208,\n        \"yul\": 102,\n        \"den\": 259,\n        \"ord\": 167,\n        \"sea\": 182\n      },\n      {\n        \"timestamp\": \"2025-08-24T15:30:00.000Z\",\n        \"nrt\": 361,\n        \"mia\": 154,\n        \"atl\": 154,\n        \"gig\": 364,\n        \"cdg\": 160,\n        \"otp\": 345,\n        \"lax\": 155,\n        \"ams\": 181,\n        \"dfw\": 166,\n        \"sjc\": 130,\n        \"syd\": 416,\n        \"yul\": 104,\n        \"sea\": 182,\n        \"ord\": 139,\n        \"den\": 236,\n        \"gru\": 206,\n        \"fra\": 159,\n        \"yyz\": 154,\n        \"lhr\": 141,\n        \"bog\": 420,\n        \"jnb\": 372,\n        \"gdl\": 602,\n        \"phx\": 204,\n        \"bom\": 348,\n        \"ewr\": 128,\n        \"hkg\": 338,\n        \"scl\": 514,\n        \"eze\": 445,\n        \"sin\": 488,\n        \"mad\": 337,\n        \"arn\": 221,\n        \"bos\": 102,\n        \"iad\": 476\n      },\n      {\n        \"timestamp\": \"2025-08-24T16:00:00.000Z\",\n        \"cdg\": 168,\n        \"otp\": 283,\n        \"lax\": 201,\n        \"dfw\": 197,\n        \"sjc\": 132,\n        \"ams\": 191,\n        \"nrt\": 432,\n        \"mia\": 156,\n        \"atl\": 155,\n        \"gig\": 325,\n        \"fra\": 180,\n        \"lhr\": 136,\n        \"yyz\": 137,\n        \"bog\": 433,\n        \"gdl\": 563,\n        \"phx\": 212,\n        \"jnb\": 394,\n        \"syd\": 305,\n        \"sea\": 174,\n        \"den\": 265,\n        \"ord\": 143,\n        \"yul\": 119,\n        \"gru\": 211,\n        \"eze\": 440,\n        \"ewr\": 133,\n        \"bom\": 301,\n        \"hkg\": 342,\n        \"scl\": 418,\n        \"bos\": 198,\n        \"iad\": 304,\n        \"sin\": 451,\n        \"mad\": 341,\n        \"arn\": 198\n      },\n      {\n        \"timestamp\": \"2025-08-24T16:30:00.000Z\",\n        \"ewr\": 165,\n        \"bom\": 300,\n        \"hkg\": 497,\n        \"scl\": 485,\n        \"eze\": 381,\n        \"sin\": 509,\n        \"mad\": 329,\n        \"arn\": 213,\n        \"bos\": 120,\n        \"iad\": 452,\n        \"nrt\": 287,\n        \"mia\": 181,\n        \"atl\": 142,\n        \"gig\": 365,\n        \"cdg\": 163,\n        \"lax\": 168,\n        \"otp\": 294,\n        \"dfw\": 192,\n        \"sjc\": 137,\n        \"ams\": 186,\n        \"syd\": 388,\n        \"sea\": 170,\n        \"den\": 220,\n        \"ord\": 190,\n        \"yul\": 123,\n        \"gru\": 321,\n        \"fra\": 153,\n        \"lhr\": 143,\n        \"yyz\": 166,\n        \"bog\": 428,\n        \"gdl\": 612,\n        \"phx\": 207,\n        \"jnb\": 433\n      },\n      {\n        \"timestamp\": \"2025-08-24T17:00:00.000Z\",\n        \"otp\": 281,\n        \"lax\": 241,\n        \"ams\": 183,\n        \"sjc\": 155,\n        \"dfw\": 175,\n        \"cdg\": 155,\n        \"atl\": 133,\n        \"gig\": 346,\n        \"mia\": 174,\n        \"nrt\": 390,\n        \"bog\": 375,\n        \"jnb\": 416,\n        \"phx\": 191,\n        \"gdl\": 626,\n        \"fra\": 162,\n        \"yyz\": 136,\n        \"lhr\": 167,\n        \"yul\": 108,\n        \"ord\": 138,\n        \"den\": 238,\n        \"sea\": 204,\n        \"gru\": 216,\n        \"syd\": 415,\n        \"eze\": 459,\n        \"hkg\": 361,\n        \"scl\": 493,\n        \"bom\": 296,\n        \"ewr\": 139,\n        \"iad\": 154,\n        \"bos\": 108,\n        \"arn\": 203,\n        \"sin\": 442,\n        \"mad\": 327\n      },\n      {\n        \"timestamp\": \"2025-08-24T17:30:00.000Z\",\n        \"hkg\": 338,\n        \"scl\": 483,\n        \"bom\": 280,\n        \"ewr\": 127,\n        \"eze\": 447,\n        \"arn\": 263,\n        \"sin\": 486,\n        \"mad\": 323,\n        \"iad\": 194,\n        \"bos\": 127,\n        \"atl\": 138,\n        \"gig\": 344,\n        \"mia\": 171,\n        \"nrt\": 430,\n        \"lax\": 161,\n        \"otp\": 300,\n        \"ams\": 183,\n        \"sjc\": 125,\n        \"dfw\": 176,\n        \"cdg\": 258,\n        \"yul\": 108,\n        \"ord\": 167,\n        \"den\": 274,\n        \"sea\": 184,\n        \"gru\": 204,\n        \"syd\": 418,\n        \"bog\": 404,\n        \"jnb\": 462,\n        \"phx\": 189,\n        \"gdl\": 600,\n        \"fra\": 163,\n        \"yyz\": 141,\n        \"lhr\": 139\n      },\n      {\n        \"timestamp\": \"2025-08-24T18:00:00.000Z\",\n        \"arn\": 205,\n        \"sin\": 462,\n        \"mad\": 342,\n        \"iad\": 175,\n        \"bos\": 125,\n        \"hkg\": 364,\n        \"scl\": 483,\n        \"bom\": 289,\n        \"ewr\": 157,\n        \"eze\": 310,\n        \"sea\": 196,\n        \"ord\": 144,\n        \"den\": 216,\n        \"yul\": 105,\n        \"gru\": 191,\n        \"syd\": 298,\n        \"bog\": 502,\n        \"gdl\": 623,\n        \"phx\": 191,\n        \"jnb\": 525,\n        \"fra\": 191,\n        \"lhr\": 234,\n        \"yyz\": 144,\n        \"atl\": 130,\n        \"gig\": 341,\n        \"nrt\": 360,\n        \"mia\": 164,\n        \"lax\": 157,\n        \"otp\": 280,\n        \"dfw\": 207,\n        \"sjc\": 133,\n        \"ams\": 182,\n        \"cdg\": 148\n      },\n      {\n        \"timestamp\": \"2025-08-24T18:30:00.000Z\",\n        \"bog\": 407,\n        \"gdl\": 607,\n        \"phx\": 439,\n        \"jnb\": 560,\n        \"fra\": 166,\n        \"lhr\": 182,\n        \"yyz\": 162,\n        \"sea\": 189,\n        \"ord\": 154,\n        \"den\": 243,\n        \"yul\": 109,\n        \"gru\": 286,\n        \"syd\": 298,\n        \"otp\": 275,\n        \"lax\": 327,\n        \"dfw\": 167,\n        \"sjc\": 175,\n        \"ams\": 181,\n        \"cdg\": 172,\n        \"atl\": 136,\n        \"gig\": 368,\n        \"nrt\": 428,\n        \"mia\": 181,\n        \"iad\": 157,\n        \"bos\": 118,\n        \"arn\": 197,\n        \"sin\": 458,\n        \"mad\": 347,\n        \"eze\": 441,\n        \"hkg\": 344,\n        \"scl\": 514,\n        \"bom\": 317,\n        \"ewr\": 182\n      },\n      {\n        \"timestamp\": \"2025-08-24T19:00:00.000Z\",\n        \"cdg\": 168,\n        \"dfw\": 210,\n        \"sjc\": 136,\n        \"ams\": 233,\n        \"lax\": 152,\n        \"otp\": 280,\n        \"nrt\": 435,\n        \"mia\": 158,\n        \"gig\": 349,\n        \"atl\": 132,\n        \"lhr\": 139,\n        \"yyz\": 144,\n        \"fra\": 205,\n        \"gdl\": 612,\n        \"phx\": 194,\n        \"jnb\": 494,\n        \"bog\": 413,\n        \"syd\": 415,\n        \"gru\": 307,\n        \"sea\": 177,\n        \"den\": 239,\n        \"ord\": 138,\n        \"yul\": 151,\n        \"eze\": 440,\n        \"ewr\": 135,\n        \"bom\": 340,\n        \"scl\": 481,\n        \"hkg\": 474,\n        \"bos\": 120,\n        \"iad\": 137,\n        \"mad\": 343,\n        \"sin\": 423,\n        \"arn\": 282\n      },\n      {\n        \"timestamp\": \"2025-08-24T19:30:00.000Z\",\n        \"ewr\": 180,\n        \"bom\": 294,\n        \"scl\": 485,\n        \"hkg\": 425,\n        \"eze\": 430,\n        \"mad\": 350,\n        \"sin\": 424,\n        \"arn\": 199,\n        \"bos\": 111,\n        \"iad\": 143,\n        \"nrt\": 441,\n        \"mia\": 245,\n        \"gig\": 348,\n        \"atl\": 137,\n        \"cdg\": 149,\n        \"dfw\": 177,\n        \"sjc\": 141,\n        \"ams\": 176,\n        \"otp\": 281,\n        \"lax\": 152,\n        \"syd\": 294,\n        \"gru\": 253,\n        \"sea\": 183,\n        \"den\": 219,\n        \"ord\": 141,\n        \"yul\": 114,\n        \"lhr\": 166,\n        \"yyz\": 192,\n        \"fra\": 221,\n        \"gdl\": 612,\n        \"phx\": 220,\n        \"jnb\": 463,\n        \"bog\": 426\n      },\n      {\n        \"timestamp\": \"2025-08-24T20:00:00.000Z\",\n        \"iad\": 485,\n        \"bos\": 120,\n        \"arn\": 222,\n        \"mad\": 358,\n        \"sin\": 508,\n        \"eze\": 441,\n        \"scl\": 494,\n        \"hkg\": 494,\n        \"bom\": 306,\n        \"ewr\": 172,\n        \"gdl\": 617,\n        \"phx\": 217,\n        \"jnb\": 531,\n        \"bog\": 385,\n        \"lhr\": 182,\n        \"yyz\": 139,\n        \"fra\": 162,\n        \"gru\": 208,\n        \"sea\": 168,\n        \"ord\": 174,\n        \"den\": 232,\n        \"yul\": 117,\n        \"syd\": 376,\n        \"dfw\": 181,\n        \"sjc\": 149,\n        \"ams\": 276,\n        \"lax\": 156,\n        \"otp\": 339,\n        \"cdg\": 148,\n        \"gig\": 344,\n        \"atl\": 160,\n        \"nrt\": 455,\n        \"mia\": 152\n      },\n      {\n        \"timestamp\": \"2025-08-24T20:30:00.000Z\",\n        \"eze\": 437,\n        \"scl\": 477,\n        \"hkg\": 390,\n        \"bom\": 284,\n        \"ewr\": 129,\n        \"iad\": 152,\n        \"bos\": 109,\n        \"arn\": 193,\n        \"mad\": 331,\n        \"sin\": 438,\n        \"ams\": 174,\n        \"sjc\": 150,\n        \"dfw\": 217,\n        \"otp\": 286,\n        \"lax\": 191,\n        \"cdg\": 175,\n        \"gig\": 367,\n        \"atl\": 146,\n        \"mia\": 186,\n        \"nrt\": 356,\n        \"jnb\": 424,\n        \"phx\": 209,\n        \"gdl\": 647,\n        \"bog\": 384,\n        \"yyz\": 148,\n        \"lhr\": 155,\n        \"fra\": 153,\n        \"gru\": 217,\n        \"yul\": 113,\n        \"ord\": 139,\n        \"den\": 249,\n        \"sea\": 167,\n        \"syd\": 290\n      },\n      {\n        \"timestamp\": \"2025-08-24T21:00:00.000Z\",\n        \"mad\": 376,\n        \"sin\": 451,\n        \"arn\": 213,\n        \"bos\": 120,\n        \"iad\": 149,\n        \"ewr\": 201,\n        \"bom\": 323,\n        \"scl\": 485,\n        \"hkg\": 398,\n        \"eze\": 448,\n        \"syd\": 421,\n        \"gru\": 201,\n        \"yul\": 126,\n        \"sea\": 192,\n        \"ord\": 144,\n        \"den\": 247,\n        \"yyz\": 184,\n        \"lhr\": 148,\n        \"fra\": 157,\n        \"jnb\": 370,\n        \"gdl\": 605,\n        \"phx\": 196,\n        \"bog\": 456,\n        \"nrt\": 472,\n        \"mia\": 223,\n        \"gig\": 346,\n        \"atl\": 148,\n        \"cdg\": 138,\n        \"ams\": 187,\n        \"dfw\": 176,\n        \"sjc\": 134,\n        \"lax\": 165,\n        \"otp\": 277\n      },\n      {\n        \"timestamp\": \"2025-08-24T21:30:00.000Z\",\n        \"mia\": 171,\n        \"nrt\": 422,\n        \"atl\": 179,\n        \"gig\": 380,\n        \"cdg\": 204,\n        \"otp\": 292,\n        \"lax\": 194,\n        \"sjc\": 149,\n        \"dfw\": 179,\n        \"ams\": 223,\n        \"syd\": 308,\n        \"den\": 230,\n        \"ord\": 162,\n        \"sea\": 170,\n        \"yul\": 108,\n        \"gru\": 268,\n        \"fra\": 159,\n        \"lhr\": 148,\n        \"yyz\": 151,\n        \"bog\": 411,\n        \"phx\": 198,\n        \"gdl\": 584,\n        \"jnb\": 480,\n        \"bom\": 288,\n        \"ewr\": 201,\n        \"hkg\": 334,\n        \"scl\": 483,\n        \"eze\": 436,\n        \"sin\": 443,\n        \"mad\": 337,\n        \"arn\": 198,\n        \"bos\": 111,\n        \"iad\": 161\n      },\n      {\n        \"timestamp\": \"2025-08-24T22:00:00.000Z\",\n        \"eze\": 380,\n        \"bom\": 296,\n        \"ewr\": 166,\n        \"hkg\": 492,\n        \"scl\": 476,\n        \"bos\": 128,\n        \"iad\": 127,\n        \"sin\": 527,\n        \"mad\": 354,\n        \"arn\": 196,\n        \"cdg\": 146,\n        \"otp\": 281,\n        \"lax\": 234,\n        \"sjc\": 130,\n        \"dfw\": 190,\n        \"ams\": 177,\n        \"mia\": 171,\n        \"nrt\": 430,\n        \"atl\": 203,\n        \"gig\": 352,\n        \"fra\": 162,\n        \"lhr\": 156,\n        \"yyz\": 162,\n        \"bog\": 415,\n        \"phx\": 202,\n        \"gdl\": 663,\n        \"jnb\": 388,\n        \"syd\": 361,\n        \"den\": 222,\n        \"ord\": 165,\n        \"sea\": 193,\n        \"yul\": 112,\n        \"gru\": 330\n      },\n      {\n        \"timestamp\": \"2025-08-24T22:30:00.000Z\",\n        \"bos\": 194,\n        \"iad\": 295,\n        \"sin\": 458,\n        \"mad\": 333,\n        \"arn\": 189,\n        \"eze\": 441,\n        \"bom\": 281,\n        \"ewr\": 136,\n        \"hkg\": 355,\n        \"scl\": 438,\n        \"fra\": 207,\n        \"yyz\": 220,\n        \"lhr\": 144,\n        \"bog\": 423,\n        \"jnb\": 470,\n        \"gdl\": 638,\n        \"phx\": 194,\n        \"syd\": 361,\n        \"yul\": 103,\n        \"sea\": 165,\n        \"den\": 230,\n        \"ord\": 144,\n        \"gru\": 204,\n        \"cdg\": 147,\n        \"lax\": 174,\n        \"otp\": 273,\n        \"ams\": 196,\n        \"dfw\": 213,\n        \"sjc\": 139,\n        \"nrt\": 363,\n        \"mia\": 164,\n        \"atl\": 182,\n        \"gig\": 355\n      },\n      {\n        \"timestamp\": \"2025-08-24T23:00:00.000Z\",\n        \"hkg\": 339,\n        \"scl\": 496,\n        \"ewr\": 164,\n        \"bom\": 281,\n        \"eze\": 446,\n        \"arn\": 199,\n        \"sin\": 470,\n        \"mad\": 339,\n        \"iad\": 248,\n        \"bos\": 127,\n        \"atl\": 226,\n        \"gig\": 350,\n        \"mia\": 168,\n        \"nrt\": 429,\n        \"otp\": 273,\n        \"lax\": 143,\n        \"ams\": 180,\n        \"sjc\": 153,\n        \"dfw\": 188,\n        \"cdg\": 150,\n        \"yul\": 115,\n        \"den\": 283,\n        \"ord\": 176,\n        \"sea\": 187,\n        \"gru\": 244,\n        \"syd\": 328,\n        \"bog\": 408,\n        \"jnb\": 462,\n        \"phx\": 194,\n        \"gdl\": 532,\n        \"fra\": 183,\n        \"yyz\": 146,\n        \"lhr\": 148\n      },\n      {\n        \"timestamp\": \"2025-08-24T23:30:00.000Z\",\n        \"gig\": 345,\n        \"atl\": 201,\n        \"nrt\": 421,\n        \"mia\": 194,\n        \"ams\": 176,\n        \"dfw\": 188,\n        \"sjc\": 160,\n        \"lax\": 148,\n        \"otp\": 281,\n        \"cdg\": 153,\n        \"gru\": 286,\n        \"yul\": 110,\n        \"sea\": 212,\n        \"ord\": 197,\n        \"den\": 225,\n        \"syd\": 417,\n        \"jnb\": 512,\n        \"gdl\": 642,\n        \"phx\": 196,\n        \"bog\": 440,\n        \"yyz\": 134,\n        \"lhr\": 137,\n        \"fra\": 192,\n        \"scl\": 496,\n        \"hkg\": 353,\n        \"ewr\": 142,\n        \"bom\": 283,\n        \"eze\": 385,\n        \"arn\": 217,\n        \"mad\": 331,\n        \"sin\": 430,\n        \"iad\": 304,\n        \"bos\": 113\n      },\n      {\n        \"timestamp\": \"2025-08-25T00:00:00.000Z\",\n        \"cdg\": 159,\n        \"dfw\": 169,\n        \"sjc\": 140,\n        \"ams\": 186,\n        \"lax\": 155,\n        \"otp\": 289,\n        \"nrt\": 418,\n        \"mia\": 168,\n        \"gig\": 359,\n        \"atl\": 282,\n        \"lhr\": 146,\n        \"yyz\": 147,\n        \"fra\": 184,\n        \"gdl\": 551,\n        \"phx\": 187,\n        \"jnb\": 527,\n        \"bog\": 425,\n        \"syd\": 392,\n        \"gru\": 209,\n        \"sea\": 163,\n        \"ord\": 195,\n        \"den\": 245,\n        \"yul\": 108,\n        \"eze\": 420,\n        \"ewr\": 134,\n        \"bom\": 324,\n        \"scl\": 487,\n        \"hkg\": 365,\n        \"bos\": 116,\n        \"iad\": 160,\n        \"mad\": 349,\n        \"sin\": 444,\n        \"arn\": 196\n      },\n      {\n        \"timestamp\": \"2025-08-25T00:30:00.000Z\",\n        \"ewr\": 129,\n        \"bom\": 297,\n        \"scl\": 485,\n        \"hkg\": 359,\n        \"eze\": 445,\n        \"mad\": 332,\n        \"sin\": 516,\n        \"arn\": 193,\n        \"bos\": 177,\n        \"iad\": 202,\n        \"nrt\": 407,\n        \"mia\": 178,\n        \"gig\": 352,\n        \"atl\": 215,\n        \"cdg\": 150,\n        \"dfw\": 192,\n        \"sjc\": 150,\n        \"ams\": 171,\n        \"otp\": 289,\n        \"lax\": 152,\n        \"syd\": 425,\n        \"gru\": 229,\n        \"sea\": 180,\n        \"ord\": 146,\n        \"den\": 233,\n        \"yul\": 142,\n        \"lhr\": 167,\n        \"yyz\": 136,\n        \"fra\": 161,\n        \"gdl\": 990,\n        \"phx\": 196,\n        \"jnb\": 496,\n        \"bog\": 485\n      },\n      {\n        \"timestamp\": \"2025-08-25T01:00:00.000Z\",\n        \"gig\": 366,\n        \"atl\": 240,\n        \"nrt\": 425,\n        \"mia\": 166,\n        \"ams\": 186,\n        \"dfw\": 188,\n        \"sjc\": 140,\n        \"lax\": 177,\n        \"otp\": 281,\n        \"cdg\": 146,\n        \"gru\": 194,\n        \"yul\": 108,\n        \"sea\": 178,\n        \"ord\": 147,\n        \"den\": 241,\n        \"syd\": 301,\n        \"jnb\": 367,\n        \"gdl\": 607,\n        \"phx\": 198,\n        \"bog\": 378,\n        \"yyz\": 149,\n        \"lhr\": 152,\n        \"fra\": 154,\n        \"scl\": 492,\n        \"hkg\": 372,\n        \"bom\": 294,\n        \"ewr\": 183,\n        \"eze\": 444,\n        \"arn\": 192,\n        \"mad\": 334,\n        \"sin\": 536,\n        \"iad\": 413,\n        \"bos\": 131\n      },\n      {\n        \"timestamp\": \"2025-08-25T01:30:00.000Z\",\n        \"eze\": 442,\n        \"scl\": 559,\n        \"hkg\": 360,\n        \"bom\": 288,\n        \"ewr\": 215,\n        \"iad\": 297,\n        \"bos\": 116,\n        \"arn\": 198,\n        \"mad\": 348,\n        \"sin\": 528,\n        \"ams\": 177,\n        \"dfw\": 201,\n        \"sjc\": 154,\n        \"otp\": 278,\n        \"lax\": 166,\n        \"cdg\": 158,\n        \"gig\": 348,\n        \"atl\": 232,\n        \"nrt\": 342,\n        \"mia\": 195,\n        \"jnb\": 360,\n        \"gdl\": 586,\n        \"phx\": 282,\n        \"bog\": 437,\n        \"yyz\": 141,\n        \"lhr\": 146,\n        \"fra\": 164,\n        \"gru\": 217,\n        \"yul\": 126,\n        \"sea\": 242,\n        \"ord\": 147,\n        \"den\": 238,\n        \"syd\": 389\n      },\n      {\n        \"timestamp\": \"2025-08-25T02:00:00.000Z\",\n        \"arn\": 197,\n        \"mad\": 347,\n        \"sin\": 570,\n        \"iad\": 156,\n        \"bos\": 115,\n        \"scl\": 375,\n        \"hkg\": 318,\n        \"bom\": 292,\n        \"ewr\": 159,\n        \"eze\": 438,\n        \"gru\": 209,\n        \"sea\": 207,\n        \"den\": 229,\n        \"ord\": 139,\n        \"yul\": 107,\n        \"syd\": 339,\n        \"gdl\": 613,\n        \"phx\": 191,\n        \"jnb\": 475,\n        \"bog\": 412,\n        \"lhr\": 150,\n        \"yyz\": 148,\n        \"fra\": 161,\n        \"gig\": 340,\n        \"atl\": 308,\n        \"nrt\": 430,\n        \"mia\": 188,\n        \"dfw\": 199,\n        \"sjc\": 155,\n        \"ams\": 199,\n        \"otp\": 287,\n        \"lax\": 176,\n        \"cdg\": 143\n      },\n      {\n        \"timestamp\": \"2025-08-25T02:30:00.000Z\",\n        \"gdl\": 625,\n        \"phx\": 222,\n        \"jnb\": 363,\n        \"bog\": 429,\n        \"lhr\": 149,\n        \"yyz\": 147,\n        \"fra\": 160,\n        \"gru\": 207,\n        \"sea\": 186,\n        \"den\": 238,\n        \"ord\": 162,\n        \"yul\": 112,\n        \"syd\": 298,\n        \"dfw\": 178,\n        \"sjc\": 230,\n        \"ams\": 192,\n        \"lax\": 170,\n        \"otp\": 273,\n        \"cdg\": 181,\n        \"gig\": 352,\n        \"atl\": 383,\n        \"nrt\": 453,\n        \"mia\": 183,\n        \"iad\": 167,\n        \"bos\": 138,\n        \"arn\": 201,\n        \"mad\": 338,\n        \"sin\": 462,\n        \"eze\": 364,\n        \"scl\": 502,\n        \"hkg\": 318,\n        \"bom\": 292,\n        \"ewr\": 138\n      },\n      {\n        \"timestamp\": \"2025-08-25T03:00:00.000Z\",\n        \"bos\": 126,\n        \"iad\": 195,\n        \"mad\": 354,\n        \"sin\": 454,\n        \"arn\": 216,\n        \"eze\": 435,\n        \"ewr\": 152,\n        \"bom\": 354,\n        \"scl\": 492,\n        \"hkg\": 321,\n        \"yyz\": 137,\n        \"lhr\": 176,\n        \"fra\": 162,\n        \"jnb\": 448,\n        \"gdl\": 598,\n        \"phx\": 202,\n        \"bog\": 442,\n        \"syd\": 420,\n        \"gru\": 209,\n        \"yul\": 127,\n        \"sea\": 179,\n        \"den\": 248,\n        \"ord\": 180,\n        \"cdg\": 148,\n        \"ams\": 187,\n        \"dfw\": 186,\n        \"sjc\": 165,\n        \"otp\": 288,\n        \"lax\": 164,\n        \"nrt\": 422,\n        \"mia\": 177,\n        \"gig\": 347,\n        \"atl\": 340\n      },\n      {\n        \"timestamp\": \"2025-08-25T03:30:00.000Z\",\n        \"syd\": 374,\n        \"gru\": 212,\n        \"yul\": 162,\n        \"sea\": 165,\n        \"den\": 224,\n        \"ord\": 143,\n        \"yyz\": 145,\n        \"lhr\": 139,\n        \"fra\": 167,\n        \"jnb\": 513,\n        \"gdl\": 607,\n        \"phx\": 197,\n        \"bog\": 473,\n        \"nrt\": 348,\n        \"mia\": 160,\n        \"gig\": 351,\n        \"atl\": 397,\n        \"cdg\": 168,\n        \"ams\": 183,\n        \"dfw\": 167,\n        \"sjc\": 179,\n        \"lax\": 145,\n        \"otp\": 284,\n        \"mad\": 392,\n        \"sin\": 449,\n        \"arn\": 199,\n        \"bos\": 115,\n        \"iad\": 159,\n        \"ewr\": 130,\n        \"bom\": 288,\n        \"scl\": 491,\n        \"hkg\": 322,\n        \"eze\": 428\n      },\n      {\n        \"timestamp\": \"2025-08-25T04:00:00.000Z\",\n        \"ams\": 189,\n        \"dfw\": 168,\n        \"sjc\": 164,\n        \"otp\": 275,\n        \"lax\": 232,\n        \"cdg\": 155,\n        \"gig\": 357,\n        \"atl\": 348,\n        \"nrt\": 429,\n        \"mia\": 159,\n        \"jnb\": 494,\n        \"gdl\": 629,\n        \"phx\": 210,\n        \"bog\": 372,\n        \"yyz\": 136,\n        \"lhr\": 149,\n        \"fra\": 201,\n        \"gru\": 237,\n        \"yul\": 107,\n        \"sea\": 173,\n        \"ord\": 178,\n        \"den\": 223,\n        \"syd\": 379,\n        \"eze\": 435,\n        \"scl\": 501,\n        \"hkg\": 314,\n        \"bom\": 286,\n        \"ewr\": 123,\n        \"iad\": 152,\n        \"bos\": 106,\n        \"arn\": 186,\n        \"mad\": 343,\n        \"sin\": 454\n      },\n      {\n        \"timestamp\": \"2025-08-25T04:30:00.000Z\",\n        \"eze\": 436,\n        \"hkg\": 485,\n        \"scl\": 503,\n        \"bom\": 448,\n        \"ewr\": 136,\n        \"iad\": 139,\n        \"bos\": 116,\n        \"arn\": 208,\n        \"sin\": 412,\n        \"mad\": 355,\n        \"lax\": 159,\n        \"otp\": 274,\n        \"ams\": 173,\n        \"sjc\": 141,\n        \"dfw\": 208,\n        \"cdg\": 157,\n        \"atl\": 230,\n        \"gig\": 344,\n        \"mia\": 175,\n        \"nrt\": 443,\n        \"bog\": 423,\n        \"jnb\": 463,\n        \"phx\": 196,\n        \"gdl\": 625,\n        \"fra\": 164,\n        \"yyz\": 145,\n        \"lhr\": 195,\n        \"yul\": 116,\n        \"den\": 226,\n        \"ord\": 155,\n        \"sea\": 169,\n        \"gru\": 202,\n        \"syd\": 312\n      },\n      {\n        \"timestamp\": \"2025-08-25T05:00:00.000Z\",\n        \"sin\": 433,\n        \"mad\": 337,\n        \"arn\": 212,\n        \"bos\": 168,\n        \"iad\": 135,\n        \"ewr\": 126,\n        \"bom\": 358,\n        \"hkg\": 325,\n        \"scl\": 505,\n        \"eze\": 441,\n        \"syd\": 302,\n        \"yul\": 106,\n        \"sea\": 168,\n        \"den\": 227,\n        \"ord\": 165,\n        \"gru\": 236,\n        \"fra\": 156,\n        \"yyz\": 129,\n        \"lhr\": 142,\n        \"bog\": 389,\n        \"jnb\": 511,\n        \"gdl\": 566,\n        \"phx\": 194,\n        \"nrt\": 354,\n        \"mia\": 159,\n        \"atl\": 176,\n        \"gig\": 338,\n        \"cdg\": 223,\n        \"otp\": 291,\n        \"lax\": 159,\n        \"ams\": 176,\n        \"dfw\": 202,\n        \"sjc\": 129\n      },\n      {\n        \"timestamp\": \"2025-08-25T05:30:00.000Z\",\n        \"ewr\": 152,\n        \"bom\": 292,\n        \"hkg\": 380,\n        \"scl\": 493,\n        \"eze\": 443,\n        \"sin\": 487,\n        \"mad\": 331,\n        \"arn\": 236,\n        \"bos\": 109,\n        \"iad\": 140,\n        \"mia\": 184,\n        \"nrt\": 345,\n        \"atl\": 120,\n        \"gig\": 347,\n        \"cdg\": 295,\n        \"lax\": 165,\n        \"otp\": 285,\n        \"sjc\": 134,\n        \"dfw\": 169,\n        \"ams\": 169,\n        \"syd\": 331,\n        \"den\": 227,\n        \"ord\": 144,\n        \"sea\": 209,\n        \"yul\": 101,\n        \"gru\": 274,\n        \"fra\": 165,\n        \"lhr\": 142,\n        \"yyz\": 127,\n        \"bog\": 385,\n        \"phx\": 199,\n        \"gdl\": 605,\n        \"jnb\": 388\n      },\n      {\n        \"timestamp\": \"2025-08-25T06:00:00.000Z\",\n        \"cdg\": 189,\n        \"lax\": 153,\n        \"otp\": 275,\n        \"sjc\": 156,\n        \"dfw\": 205,\n        \"ams\": 186,\n        \"mia\": 161,\n        \"nrt\": 347,\n        \"atl\": 147,\n        \"gig\": 411,\n        \"fra\": 164,\n        \"lhr\": 190,\n        \"yyz\": 142,\n        \"bog\": 458,\n        \"phx\": 187,\n        \"gdl\": 594,\n        \"jnb\": 402,\n        \"syd\": 328,\n        \"den\": 242,\n        \"ord\": 180,\n        \"sea\": 203,\n        \"yul\": 98,\n        \"gru\": 256,\n        \"eze\": 437,\n        \"ewr\": 129,\n        \"bom\": 298,\n        \"hkg\": 438,\n        \"scl\": 497,\n        \"bos\": 114,\n        \"iad\": 150,\n        \"sin\": 449,\n        \"mad\": 580,\n        \"arn\": 208\n      },\n      {\n        \"timestamp\": \"2025-08-25T06:30:00.000Z\",\n        \"otp\": 302,\n        \"lax\": 167,\n        \"dfw\": 165,\n        \"sjc\": 128,\n        \"ams\": 221,\n        \"cdg\": 156,\n        \"atl\": 137,\n        \"gig\": 381,\n        \"nrt\": 366,\n        \"mia\": 161,\n        \"bog\": 414,\n        \"gdl\": 594,\n        \"phx\": 190,\n        \"jnb\": 475,\n        \"fra\": 169,\n        \"lhr\": 143,\n        \"yyz\": 134,\n        \"sea\": 175,\n        \"ord\": 179,\n        \"den\": 221,\n        \"yul\": 107,\n        \"gru\": 237,\n        \"syd\": 323,\n        \"eze\": 440,\n        \"hkg\": 335,\n        \"scl\": 488,\n        \"bom\": 292,\n        \"ewr\": 129,\n        \"iad\": 138,\n        \"bos\": 117,\n        \"arn\": 200,\n        \"sin\": 434,\n        \"mad\": 334\n      },\n      {\n        \"timestamp\": \"2025-08-25T07:00:00.000Z\",\n        \"syd\": 377,\n        \"ord\": 160,\n        \"den\": 223,\n        \"sea\": 167,\n        \"yul\": 108,\n        \"gru\": 262,\n        \"fra\": 167,\n        \"lhr\": 140,\n        \"yyz\": 183,\n        \"bog\": 434,\n        \"phx\": 212,\n        \"gdl\": 595,\n        \"jnb\": 379,\n        \"mia\": 173,\n        \"nrt\": 360,\n        \"atl\": 148,\n        \"gig\": 365,\n        \"cdg\": 275,\n        \"lax\": 163,\n        \"otp\": 283,\n        \"sjc\": 143,\n        \"dfw\": 182,\n        \"ams\": 196,\n        \"sin\": 459,\n        \"mad\": 374,\n        \"arn\": 278,\n        \"bos\": 154,\n        \"iad\": 168,\n        \"ewr\": 129,\n        \"bom\": 297,\n        \"hkg\": 336,\n        \"scl\": 497,\n        \"eze\": 442\n      },\n      {\n        \"timestamp\": \"2025-08-25T07:30:00.000Z\",\n        \"nrt\": 373,\n        \"mia\": 155,\n        \"atl\": 144,\n        \"gig\": 379,\n        \"cdg\": 171,\n        \"otp\": 279,\n        \"lax\": 161,\n        \"ams\": 176,\n        \"dfw\": 195,\n        \"sjc\": 137,\n        \"syd\": 351,\n        \"yul\": 107,\n        \"sea\": 166,\n        \"ord\": 207,\n        \"den\": 229,\n        \"gru\": 246,\n        \"fra\": 178,\n        \"yyz\": 153,\n        \"lhr\": 160,\n        \"bog\": 383,\n        \"jnb\": 456,\n        \"gdl\": 531,\n        \"phx\": 200,\n        \"ewr\": 130,\n        \"bom\": 308,\n        \"hkg\": 398,\n        \"scl\": 432,\n        \"eze\": 443,\n        \"sin\": 454,\n        \"mad\": 357,\n        \"arn\": 325,\n        \"bos\": 105,\n        \"iad\": 130\n      },\n      {\n        \"timestamp\": \"2025-08-25T08:00:00.000Z\",\n        \"sin\": 484,\n        \"mad\": 337,\n        \"arn\": 196,\n        \"bos\": 136,\n        \"iad\": 154,\n        \"bom\": 297,\n        \"ewr\": 162,\n        \"hkg\": 365,\n        \"scl\": 502,\n        \"eze\": 438,\n        \"syd\": 374,\n        \"yul\": 112,\n        \"den\": 241,\n        \"ord\": 154,\n        \"sea\": 174,\n        \"gru\": 213,\n        \"fra\": 155,\n        \"yyz\": 145,\n        \"lhr\": 169,\n        \"bog\": 444,\n        \"jnb\": 517,\n        \"phx\": 203,\n        \"gdl\": 566,\n        \"mia\": 151,\n        \"nrt\": 407,\n        \"atl\": 142,\n        \"gig\": 347,\n        \"cdg\": 157,\n        \"otp\": 278,\n        \"lax\": 157,\n        \"ams\": 195,\n        \"sjc\": 207,\n        \"dfw\": 214\n      },\n      {\n        \"timestamp\": \"2025-08-25T08:30:00.000Z\",\n        \"yyz\": 147,\n        \"lhr\": 143,\n        \"fra\": 159,\n        \"jnb\": 458,\n        \"gdl\": 600,\n        \"phx\": 196,\n        \"bog\": 382,\n        \"syd\": 370,\n        \"gru\": 186,\n        \"yul\": 109,\n        \"sea\": 184,\n        \"ord\": 132,\n        \"den\": 232,\n        \"cdg\": 262,\n        \"ams\": 189,\n        \"dfw\": 177,\n        \"sjc\": 139,\n        \"otp\": 299,\n        \"lax\": 144,\n        \"nrt\": 348,\n        \"mia\": 154,\n        \"gig\": 345,\n        \"atl\": 126,\n        \"bos\": 117,\n        \"iad\": 131,\n        \"mad\": 348,\n        \"sin\": 471,\n        \"arn\": 193,\n        \"eze\": 454,\n        \"ewr\": 161,\n        \"bom\": 304,\n        \"scl\": 494,\n        \"hkg\": 325\n      },\n      {\n        \"timestamp\": \"2025-08-25T09:00:00.000Z\",\n        \"mad\": 346,\n        \"sin\": 476,\n        \"arn\": 215,\n        \"bos\": 109,\n        \"iad\": 157,\n        \"bom\": 310,\n        \"ewr\": 165,\n        \"scl\": 480,\n        \"hkg\": 322,\n        \"eze\": 434,\n        \"syd\": 415,\n        \"gru\": 217,\n        \"yul\": 109,\n        \"den\": 248,\n        \"ord\": 215,\n        \"sea\": 170,\n        \"yyz\": 149,\n        \"lhr\": 177,\n        \"fra\": 162,\n        \"jnb\": 611,\n        \"phx\": 199,\n        \"gdl\": 633,\n        \"bog\": 415,\n        \"mia\": 188,\n        \"nrt\": 425,\n        \"gig\": 353,\n        \"atl\": 185,\n        \"cdg\": 168,\n        \"ams\": 209,\n        \"sjc\": 141,\n        \"dfw\": 230,\n        \"lax\": 147,\n        \"otp\": 281\n      },\n      {\n        \"timestamp\": \"2025-08-25T09:30:00.000Z\",\n        \"hkg\": 431,\n        \"scl\": 482,\n        \"bom\": 288,\n        \"ewr\": 133,\n        \"eze\": 439,\n        \"arn\": 266,\n        \"sin\": 437,\n        \"mad\": 362,\n        \"iad\": 156,\n        \"bos\": 115,\n        \"atl\": 148,\n        \"gig\": 342,\n        \"nrt\": 386,\n        \"mia\": 160,\n        \"otp\": 337,\n        \"lax\": 170,\n        \"ams\": 195,\n        \"dfw\": 171,\n        \"sjc\": 127,\n        \"cdg\": 156,\n        \"yul\": 105,\n        \"sea\": 173,\n        \"den\": 232,\n        \"ord\": 135,\n        \"gru\": 220,\n        \"syd\": 420,\n        \"bog\": 389,\n        \"jnb\": 491,\n        \"gdl\": 586,\n        \"phx\": 202,\n        \"fra\": 165,\n        \"yyz\": 181,\n        \"lhr\": 161\n      },\n      {\n        \"timestamp\": \"2025-08-25T10:00:00.000Z\",\n        \"eze\": 467,\n        \"ewr\": 154,\n        \"bom\": 290,\n        \"scl\": 479,\n        \"hkg\": 354,\n        \"bos\": 123,\n        \"iad\": 149,\n        \"mad\": 345,\n        \"sin\": 458,\n        \"arn\": 220,\n        \"cdg\": 165,\n        \"sjc\": 170,\n        \"dfw\": 173,\n        \"ams\": 170,\n        \"otp\": 332,\n        \"lax\": 161,\n        \"mia\": 155,\n        \"nrt\": 353,\n        \"gig\": 362,\n        \"atl\": 133,\n        \"lhr\": 182,\n        \"yyz\": 143,\n        \"fra\": 215,\n        \"phx\": 188,\n        \"gdl\": 618,\n        \"jnb\": 499,\n        \"bog\": 381,\n        \"syd\": 293,\n        \"gru\": 285,\n        \"ord\": 216,\n        \"den\": 244,\n        \"sea\": 179,\n        \"yul\": 112\n      },\n      {\n        \"timestamp\": \"2025-08-25T10:30:00.000Z\",\n        \"bog\": 412,\n        \"phx\": 194,\n        \"gdl\": 626,\n        \"jnb\": 538,\n        \"fra\": 183,\n        \"lhr\": 139,\n        \"yyz\": 142,\n        \"den\": 258,\n        \"ord\": 150,\n        \"sea\": 173,\n        \"yul\": 185,\n        \"gru\": 277,\n        \"syd\": 373,\n        \"lax\": 156,\n        \"otp\": 278,\n        \"sjc\": 127,\n        \"dfw\": 168,\n        \"ams\": 222,\n        \"cdg\": 146,\n        \"atl\": 139,\n        \"gig\": 357,\n        \"mia\": 174,\n        \"nrt\": 423,\n        \"iad\": 195,\n        \"bos\": 122,\n        \"arn\": 199,\n        \"sin\": 433,\n        \"mad\": 339,\n        \"eze\": 444,\n        \"hkg\": 356,\n        \"scl\": 480,\n        \"bom\": 295,\n        \"ewr\": 143\n      },\n      {\n        \"timestamp\": \"2025-08-25T11:00:00.000Z\",\n        \"ams\": 193,\n        \"sjc\": 126,\n        \"dfw\": 188,\n        \"otp\": 281,\n        \"lax\": 185,\n        \"cdg\": 262,\n        \"gig\": 501,\n        \"atl\": 225,\n        \"mia\": 163,\n        \"nrt\": 427,\n        \"jnb\": 395,\n        \"phx\": 199,\n        \"gdl\": 614,\n        \"bog\": 384,\n        \"yyz\": 140,\n        \"lhr\": 144,\n        \"fra\": 170,\n        \"gru\": 245,\n        \"yul\": 105,\n        \"ord\": 158,\n        \"den\": 253,\n        \"sea\": 175,\n        \"syd\": 384,\n        \"eze\": 443,\n        \"scl\": 486,\n        \"hkg\": 333,\n        \"ewr\": 141,\n        \"bom\": 299,\n        \"iad\": 138,\n        \"bos\": 140,\n        \"arn\": 212,\n        \"mad\": 344,\n        \"sin\": 437\n      },\n      {\n        \"timestamp\": \"2025-08-25T11:30:00.000Z\",\n        \"fra\": 162,\n        \"yyz\": 158,\n        \"lhr\": 170,\n        \"bog\": 384,\n        \"jnb\": 443,\n        \"gdl\": 510,\n        \"phx\": 223,\n        \"syd\": 420,\n        \"yul\": 120,\n        \"sea\": 202,\n        \"ord\": 187,\n        \"den\": 231,\n        \"gru\": 197,\n        \"cdg\": 164,\n        \"lax\": 162,\n        \"otp\": 293,\n        \"ams\": 185,\n        \"dfw\": 228,\n        \"sjc\": 216,\n        \"nrt\": 470,\n        \"mia\": 159,\n        \"atl\": 131,\n        \"gig\": 428,\n        \"bos\": 181,\n        \"iad\": 150,\n        \"sin\": 419,\n        \"mad\": 419,\n        \"arn\": 242,\n        \"eze\": 477,\n        \"ewr\": 143,\n        \"bom\": 294,\n        \"hkg\": 465,\n        \"scl\": 598\n      },\n      {\n        \"timestamp\": \"2025-08-25T12:00:00.000Z\",\n        \"iad\": 147,\n        \"bos\": 114,\n        \"arn\": 211,\n        \"mad\": 369,\n        \"sin\": 478,\n        \"eze\": 443,\n        \"scl\": 484,\n        \"hkg\": 373,\n        \"ewr\": 161,\n        \"bom\": 314,\n        \"gdl\": 628,\n        \"phx\": 193,\n        \"jnb\": 493,\n        \"bog\": 428,\n        \"lhr\": 147,\n        \"yyz\": 140,\n        \"fra\": 211,\n        \"gru\": 255,\n        \"sea\": 172,\n        \"ord\": 159,\n        \"den\": 254,\n        \"yul\": 118,\n        \"syd\": 303,\n        \"dfw\": 192,\n        \"sjc\": 139,\n        \"ams\": 196,\n        \"lax\": 233,\n        \"otp\": 299,\n        \"cdg\": 169,\n        \"gig\": 347,\n        \"atl\": 140,\n        \"nrt\": 367,\n        \"mia\": 150\n      },\n      {\n        \"timestamp\": \"2025-08-25T12:30:00.000Z\",\n        \"den\": 236,\n        \"ord\": 170,\n        \"sea\": 175,\n        \"yul\": 108,\n        \"gru\": 227,\n        \"syd\": 397,\n        \"bog\": 418,\n        \"phx\": 197,\n        \"gdl\": 619,\n        \"jnb\": 489,\n        \"fra\": 172,\n        \"lhr\": 152,\n        \"yyz\": 144,\n        \"atl\": 137,\n        \"gig\": 355,\n        \"mia\": 190,\n        \"nrt\": 374,\n        \"lax\": 150,\n        \"otp\": 330,\n        \"sjc\": 158,\n        \"dfw\": 191,\n        \"ams\": 249,\n        \"cdg\": 181,\n        \"arn\": 275,\n        \"sin\": 433,\n        \"mad\": 380,\n        \"iad\": 187,\n        \"bos\": 153,\n        \"hkg\": 332,\n        \"scl\": 488,\n        \"bom\": 322,\n        \"ewr\": 167,\n        \"eze\": 445\n      },\n      {\n        \"timestamp\": \"2025-08-25T13:00:00.000Z\",\n        \"arn\": 223,\n        \"mad\": 334,\n        \"sin\": 433,\n        \"iad\": 224,\n        \"bos\": 127,\n        \"scl\": 489,\n        \"hkg\": 344,\n        \"ewr\": 131,\n        \"bom\": 289,\n        \"eze\": 451,\n        \"gru\": 202,\n        \"den\": 231,\n        \"ord\": 200,\n        \"sea\": 188,\n        \"yul\": 194,\n        \"syd\": 323,\n        \"phx\": 198,\n        \"gdl\": 637,\n        \"jnb\": 518,\n        \"bog\": 398,\n        \"lhr\": 148,\n        \"yyz\": 144,\n        \"fra\": 166,\n        \"gig\": 352,\n        \"atl\": 137,\n        \"mia\": 154,\n        \"nrt\": 360,\n        \"sjc\": 145,\n        \"dfw\": 190,\n        \"ams\": 178,\n        \"otp\": 303,\n        \"lax\": 164,\n        \"cdg\": 159\n      },\n      {\n        \"timestamp\": \"2025-08-25T13:30:00.000Z\",\n        \"eze\": 423,\n        \"ewr\": 185,\n        \"bom\": 277,\n        \"scl\": 453,\n        \"hkg\": 444,\n        \"bos\": 118,\n        \"iad\": 135,\n        \"mad\": 334,\n        \"sin\": 429,\n        \"arn\": 236,\n        \"cdg\": 246,\n        \"sjc\": 140,\n        \"dfw\": 190,\n        \"ams\": 259,\n        \"lax\": 175,\n        \"otp\": 312,\n        \"mia\": 237,\n        \"nrt\": 356,\n        \"gig\": 352,\n        \"atl\": 137,\n        \"lhr\": 156,\n        \"yyz\": 191,\n        \"fra\": 177,\n        \"phx\": 197,\n        \"gdl\": 595,\n        \"jnb\": 426,\n        \"bog\": 409,\n        \"syd\": 294,\n        \"gru\": 196,\n        \"ord\": 140,\n        \"den\": 252,\n        \"sea\": 158,\n        \"yul\": 105\n      },\n      {\n        \"timestamp\": \"2025-08-25T14:00:00.000Z\",\n        \"bog\": 464,\n        \"phx\": 224,\n        \"gdl\": 771,\n        \"jnb\": 476,\n        \"fra\": 168,\n        \"lhr\": 149,\n        \"yyz\": 142,\n        \"den\": 242,\n        \"ord\": 144,\n        \"sea\": 177,\n        \"yul\": 115,\n        \"gru\": 191,\n        \"syd\": 414,\n        \"lax\": 249,\n        \"otp\": 315,\n        \"sjc\": 144,\n        \"dfw\": 188,\n        \"ams\": 182,\n        \"cdg\": 152,\n        \"atl\": 135,\n        \"gig\": 345,\n        \"mia\": 153,\n        \"nrt\": 351,\n        \"iad\": 174,\n        \"bos\": 132,\n        \"arn\": 195,\n        \"sin\": 517,\n        \"mad\": 342,\n        \"eze\": 440,\n        \"hkg\": 340,\n        \"scl\": 520,\n        \"bom\": 315,\n        \"ewr\": 186\n      },\n      {\n        \"timestamp\": \"2025-08-25T14:30:00.000Z\",\n        \"syd\": 322,\n        \"gru\": 205,\n        \"yul\": 120,\n        \"sea\": 199,\n        \"ord\": 164,\n        \"den\": 246,\n        \"yyz\": 261,\n        \"lhr\": 171,\n        \"fra\": 168,\n        \"jnb\": 466,\n        \"gdl\": 652,\n        \"phx\": 203,\n        \"bog\": 406,\n        \"nrt\": 446,\n        \"mia\": 201,\n        \"gig\": 348,\n        \"atl\": 128,\n        \"cdg\": 180,\n        \"ams\": 180,\n        \"dfw\": 233,\n        \"sjc\": 132,\n        \"otp\": 298,\n        \"lax\": 166,\n        \"mad\": 400,\n        \"sin\": 424,\n        \"arn\": 254,\n        \"bos\": 121,\n        \"iad\": 211,\n        \"ewr\": 139,\n        \"bom\": 289,\n        \"scl\": 485,\n        \"hkg\": 331,\n        \"eze\": 435\n      },\n      {\n        \"timestamp\": \"2025-08-25T15:00:00.000Z\",\n        \"sin\": 432,\n        \"mad\": 338,\n        \"arn\": 226,\n        \"bos\": 206,\n        \"iad\": 257,\n        \"bom\": 302,\n        \"ewr\": 124,\n        \"hkg\": 416,\n        \"scl\": 490,\n        \"eze\": 397,\n        \"syd\": 368,\n        \"yul\": 112,\n        \"sea\": 170,\n        \"ord\": 144,\n        \"den\": 228,\n        \"gru\": 235,\n        \"fra\": 171,\n        \"yyz\": 136,\n        \"lhr\": 143,\n        \"bog\": 410,\n        \"jnb\": 548,\n        \"gdl\": 515,\n        \"phx\": 198,\n        \"nrt\": 298,\n        \"mia\": 176,\n        \"atl\": 164,\n        \"gig\": 393,\n        \"cdg\": 138,\n        \"lax\": 167,\n        \"otp\": 398,\n        \"ams\": 204,\n        \"dfw\": 199,\n        \"sjc\": 135\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"mad\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 306,\n      \"p75Latency\": 319,\n      \"p90Latency\": 339,\n      \"p95Latency\": 358,\n      \"p99Latency\": 451\n    },\n    {\n      \"region\": \"sin\",\n      \"count\": 10075,\n      \"ok\": 10075,\n      \"p50Latency\": 324,\n      \"p75Latency\": 403,\n      \"p90Latency\": 426,\n      \"p95Latency\": 469,\n      \"p99Latency\": 571\n    },\n    {\n      \"region\": \"bos\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 91,\n      \"p75Latency\": 100,\n      \"p90Latency\": 112,\n      \"p95Latency\": 123,\n      \"p99Latency\": 203\n    },\n    {\n      \"region\": \"lhr\",\n      \"count\": 10116,\n      \"ok\": 10116,\n      \"p50Latency\": 125,\n      \"p75Latency\": 133,\n      \"p90Latency\": 144,\n      \"p95Latency\": 159,\n      \"p99Latency\": 279\n    },\n    {\n      \"region\": \"yul\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 92,\n      \"p75Latency\": 101,\n      \"p90Latency\": 112,\n      \"p95Latency\": 123,\n      \"p99Latency\": 186\n    },\n    {\n      \"region\": \"dfw\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 157,\n      \"p75Latency\": 164,\n      \"p90Latency\": 178,\n      \"p95Latency\": 202,\n      \"p99Latency\": 289\n    },\n    {\n      \"region\": \"phx\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 176,\n      \"p75Latency\": 184,\n      \"p90Latency\": 195,\n      \"p95Latency\": 208,\n      \"p99Latency\": 311\n    },\n    {\n      \"region\": \"gig\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 194,\n      \"p75Latency\": 210,\n      \"p90Latency\": 349,\n      \"p95Latency\": 359,\n      \"p99Latency\": 428\n    },\n    {\n      \"region\": \"bog\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 342,\n      \"p75Latency\": 378,\n      \"p90Latency\": 404,\n      \"p95Latency\": 461,\n      \"p99Latency\": 576\n    },\n    {\n      \"region\": \"sea\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 144,\n      \"p75Latency\": 153,\n      \"p90Latency\": 168,\n      \"p95Latency\": 179,\n      \"p99Latency\": 278\n    },\n    {\n      \"region\": \"syd\",\n      \"count\": 10076,\n      \"ok\": 10076,\n      \"p50Latency\": 273,\n      \"p75Latency\": 285,\n      \"p90Latency\": 325,\n      \"p95Latency\": 416,\n      \"p99Latency\": 463\n    },\n    {\n      \"region\": \"gru\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 169,\n      \"p75Latency\": 183,\n      \"p90Latency\": 201,\n      \"p95Latency\": 249,\n      \"p99Latency\": 348\n    },\n    {\n      \"region\": \"scl\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 314,\n      \"p75Latency\": 345,\n      \"p90Latency\": 488,\n      \"p95Latency\": 501,\n      \"p99Latency\": 549\n    },\n    {\n      \"region\": \"ewr\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 113,\n      \"p75Latency\": 122,\n      \"p90Latency\": 133,\n      \"p95Latency\": 162,\n      \"p99Latency\": 221\n    },\n    {\n      \"region\": \"mia\",\n      \"count\": 10074,\n      \"ok\": 10074,\n      \"p50Latency\": 136,\n      \"p75Latency\": 153,\n      \"p90Latency\": 172,\n      \"p95Latency\": 185,\n      \"p99Latency\": 251\n    },\n    {\n      \"region\": \"iad\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 99,\n      \"p75Latency\": 127,\n      \"p90Latency\": 152,\n      \"p95Latency\": 201,\n      \"p99Latency\": 399\n    },\n    {\n      \"region\": \"ams\",\n      \"count\": 10092,\n      \"ok\": 10092,\n      \"p50Latency\": 161,\n      \"p75Latency\": 168,\n      \"p90Latency\": 179,\n      \"p95Latency\": 192,\n      \"p99Latency\": 276\n    },\n    {\n      \"region\": \"arn\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 174,\n      \"p75Latency\": 182,\n      \"p90Latency\": 196,\n      \"p95Latency\": 207,\n      \"p99Latency\": 318\n    },\n    {\n      \"region\": \"atl\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 112,\n      \"p75Latency\": 129,\n      \"p90Latency\": 154,\n      \"p95Latency\": 198,\n      \"p99Latency\": 355\n    },\n    {\n      \"region\": \"yyz\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 121,\n      \"p75Latency\": 128,\n      \"p90Latency\": 140,\n      \"p95Latency\": 152,\n      \"p99Latency\": 243\n    },\n    {\n      \"region\": \"sjc\",\n      \"count\": 10074,\n      \"ok\": 10074,\n      \"p50Latency\": 117,\n      \"p75Latency\": 124,\n      \"p90Latency\": 134,\n      \"p95Latency\": 146,\n      \"p99Latency\": 237\n    },\n    {\n      \"region\": \"den\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 207,\n      \"p75Latency\": 218,\n      \"p90Latency\": 240,\n      \"p95Latency\": 264,\n      \"p99Latency\": 331\n    },\n    {\n      \"region\": \"nrt\",\n      \"count\": 10078,\n      \"ok\": 10078,\n      \"p50Latency\": 210,\n      \"p75Latency\": 243,\n      \"p90Latency\": 342,\n      \"p95Latency\": 431,\n      \"p99Latency\": 484\n    },\n    {\n      \"region\": \"cdg\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 131,\n      \"p75Latency\": 137,\n      \"p90Latency\": 149,\n      \"p95Latency\": 164,\n      \"p99Latency\": 281\n    },\n    {\n      \"region\": \"lax\",\n      \"count\": 10078,\n      \"ok\": 10078,\n      \"p50Latency\": 138,\n      \"p75Latency\": 145,\n      \"p90Latency\": 155,\n      \"p95Latency\": 169,\n      \"p99Latency\": 270\n    },\n    {\n      \"region\": \"ord\",\n      \"count\": 10069,\n      \"ok\": 10069,\n      \"p50Latency\": 122,\n      \"p75Latency\": 132,\n      \"p90Latency\": 146,\n      \"p95Latency\": 176,\n      \"p99Latency\": 247\n    },\n    {\n      \"region\": \"gdl\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 464,\n      \"p75Latency\": 492,\n      \"p90Latency\": 575,\n      \"p95Latency\": 618,\n      \"p99Latency\": 683\n    },\n    {\n      \"region\": \"bom\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 268,\n      \"p75Latency\": 278,\n      \"p90Latency\": 292,\n      \"p95Latency\": 306,\n      \"p99Latency\": 399\n    },\n    {\n      \"region\": \"eze\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 265,\n      \"p75Latency\": 277,\n      \"p90Latency\": 441,\n      \"p95Latency\": 455,\n      \"p99Latency\": 496\n    },\n    {\n      \"region\": \"hkg\",\n      \"count\": 10080,\n      \"ok\": 10080,\n      \"p50Latency\": 259,\n      \"p75Latency\": 315,\n      \"p90Latency\": 339,\n      \"p95Latency\": 398,\n      \"p99Latency\": 586\n    },\n    {\n      \"region\": \"fra\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 142,\n      \"p75Latency\": 148,\n      \"p90Latency\": 159,\n      \"p95Latency\": 172,\n      \"p99Latency\": 264\n    },\n    {\n      \"region\": \"otp\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 242,\n      \"p75Latency\": 264,\n      \"p90Latency\": 278,\n      \"p95Latency\": 294,\n      \"p99Latency\": 374\n    },\n    {\n      \"region\": \"jnb\",\n      \"count\": 10079,\n      \"ok\": 10079,\n      \"p50Latency\": 340,\n      \"p75Latency\": 358,\n      \"p90Latency\": 390,\n      \"p95Latency\": 505,\n      \"p99Latency\": 543\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/monitoring-latency/cloudflare.json",
    "content": "{\n  \"regions\": [\"ams\", \"iad\", \"hkg\", \"jnb\", \"syd\", \"gru\"],\n  \"data\": {\n    \"regions\": [\"ams\", \"gru\", \"hkg\", \"iad\", \"jnb\", \"syd\"],\n    \"data\": [\n      {\n        \"timestamp\": \"Feb 4, 00:00\",\n        \"ams\": 64,\n        \"iad\": 82,\n        \"syd\": 78,\n        \"jnb\": 724,\n        \"gru\": 44,\n        \"hkg\": 37\n      },\n      {\n        \"timestamp\": \"Feb 4, 01:00\",\n        \"ams\": 49,\n        \"iad\": 81,\n        \"syd\": 83,\n        \"jnb\": 648,\n        \"gru\": 73,\n        \"hkg\": 50\n      },\n      {\n        \"timestamp\": \"Feb 4, 02:00\",\n        \"hkg\": 50,\n        \"ams\": 61,\n        \"iad\": 68,\n        \"jnb\": 728,\n        \"syd\": 48,\n        \"gru\": 71\n      },\n      {\n        \"timestamp\": \"Feb 4, 03:00\",\n        \"hkg\": 59,\n        \"jnb\": 646,\n        \"syd\": 83,\n        \"ams\": 50,\n        \"iad\": 56,\n        \"gru\": 39\n      },\n      {\n        \"timestamp\": \"Feb 4, 04:00\",\n        \"hkg\": 50,\n        \"gru\": 42,\n        \"syd\": 84,\n        \"jnb\": 643,\n        \"ams\": 54,\n        \"iad\": 70\n      },\n      {\n        \"timestamp\": \"Feb 4, 05:00\",\n        \"gru\": 126,\n        \"syd\": 87,\n        \"jnb\": 646,\n        \"ams\": 40,\n        \"iad\": 76,\n        \"hkg\": 49\n      },\n      {\n        \"timestamp\": \"Feb 4, 06:00\",\n        \"jnb\": 643,\n        \"syd\": 87,\n        \"ams\": 40,\n        \"iad\": 52,\n        \"gru\": 91,\n        \"hkg\": 63\n      },\n      {\n        \"timestamp\": \"Feb 4, 07:00\",\n        \"hkg\": 58,\n        \"jnb\": 642,\n        \"syd\": 89,\n        \"ams\": 46,\n        \"iad\": 65,\n        \"gru\": 95\n      },\n      {\n        \"timestamp\": \"Feb 4, 08:00\",\n        \"hkg\": 68,\n        \"gru\": 63,\n        \"syd\": 81,\n        \"jnb\": 640,\n        \"ams\": 48,\n        \"iad\": 68\n      },\n      {\n        \"timestamp\": \"Feb 4, 09:00\",\n        \"hkg\": 70,\n        \"iad\": 69,\n        \"ams\": 58,\n        \"jnb\": 648,\n        \"syd\": 51,\n        \"gru\": 38\n      },\n      {\n        \"timestamp\": \"Feb 4, 10:00\",\n        \"hkg\": 54,\n        \"gru\": 116,\n        \"jnb\": 664,\n        \"syd\": 86,\n        \"iad\": 68,\n        \"ams\": 53\n      },\n      {\n        \"timestamp\": \"Feb 4, 11:00\",\n        \"gru\": 69,\n        \"syd\": 101,\n        \"jnb\": 647,\n        \"ams\": 47,\n        \"iad\": 54,\n        \"hkg\": 76\n      },\n      {\n        \"timestamp\": \"Feb 4, 12:00\",\n        \"gru\": 70,\n        \"iad\": 63,\n        \"ams\": 56,\n        \"syd\": 87,\n        \"jnb\": 649,\n        \"hkg\": 62\n      },\n      {\n        \"timestamp\": \"Feb 4, 13:00\",\n        \"syd\": 84,\n        \"jnb\": 642,\n        \"ams\": 55,\n        \"iad\": 36,\n        \"gru\": 99,\n        \"hkg\": 51\n      },\n      {\n        \"timestamp\": \"Feb 4, 14:00\",\n        \"hkg\": 83,\n        \"gru\": 98,\n        \"ams\": 54,\n        \"iad\": 58,\n        \"jnb\": 648,\n        \"syd\": 82\n      },\n      {\n        \"timestamp\": \"Feb 4, 15:00\",\n        \"hkg\": 83,\n        \"iad\": 58,\n        \"ams\": 63,\n        \"syd\": 81,\n        \"jnb\": 749,\n        \"gru\": 72\n      },\n      {\n        \"timestamp\": \"Feb 4, 16:00\",\n        \"ams\": 64,\n        \"iad\": 60,\n        \"syd\": 39,\n        \"jnb\": 731,\n        \"gru\": 99,\n        \"hkg\": 78\n      },\n      {\n        \"timestamp\": \"Feb 4, 17:00\",\n        \"jnb\": 650,\n        \"syd\": 73,\n        \"ams\": 92,\n        \"iad\": 93,\n        \"gru\": 93,\n        \"hkg\": 52\n      },\n      {\n        \"timestamp\": \"Feb 4, 18:00\",\n        \"gru\": 72,\n        \"jnb\": 660,\n        \"syd\": 70,\n        \"ams\": 73,\n        \"iad\": 81,\n        \"hkg\": 72\n      },\n      {\n        \"timestamp\": \"Feb 4, 19:00\",\n        \"syd\": 247,\n        \"jnb\": 674,\n        \"iad\": 74,\n        \"ams\": 102,\n        \"gru\": 95,\n        \"hkg\": 105\n      },\n      {\n        \"timestamp\": \"Feb 4, 20:00\",\n        \"ams\": 68,\n        \"iad\": 68,\n        \"syd\": 133,\n        \"jnb\": 659,\n        \"gru\": 101,\n        \"hkg\": 82\n      },\n      {\n        \"timestamp\": \"Feb 4, 21:00\",\n        \"hkg\": 49,\n        \"syd\": 84,\n        \"jnb\": 714,\n        \"iad\": 72,\n        \"ams\": 64,\n        \"gru\": 54\n      },\n      {\n        \"timestamp\": \"Feb 4, 22:00\",\n        \"hkg\": 68,\n        \"gru\": 77,\n        \"iad\": 63,\n        \"ams\": 51,\n        \"syd\": 83,\n        \"jnb\": 697\n      },\n      {\n        \"timestamp\": \"Feb 4, 23:00\",\n        \"jnb\": 649,\n        \"syd\": 78,\n        \"iad\": 66,\n        \"ams\": 50,\n        \"gru\": 78,\n        \"hkg\": 61\n      },\n      {\n        \"timestamp\": \"Feb 5, 00:00\",\n        \"gru\": 152,\n        \"jnb\": 715,\n        \"syd\": 84,\n        \"ams\": 46,\n        \"iad\": 46,\n        \"hkg\": 44\n      },\n      {\n        \"timestamp\": \"Feb 5, 01:00\",\n        \"gru\": 136,\n        \"iad\": 78,\n        \"ams\": 58,\n        \"syd\": 83,\n        \"jnb\": 640,\n        \"hkg\": 38\n      },\n      {\n        \"timestamp\": \"Feb 5, 02:00\",\n        \"syd\": 52,\n        \"jnb\": 635,\n        \"iad\": 99,\n        \"ams\": 67,\n        \"gru\": 79,\n        \"hkg\": 54\n      },\n      {\n        \"timestamp\": \"Feb 5, 03:00\",\n        \"gru\": 40,\n        \"ams\": 44,\n        \"iad\": 75,\n        \"jnb\": 649,\n        \"syd\": 83,\n        \"hkg\": 50\n      },\n      {\n        \"timestamp\": \"Feb 5, 04:00\",\n        \"gru\": 122,\n        \"jnb\": 723,\n        \"syd\": 87,\n        \"iad\": 58,\n        \"ams\": 48,\n        \"hkg\": 54\n      },\n      {\n        \"timestamp\": \"Feb 5, 05:00\",\n        \"ams\": 60,\n        \"iad\": 59,\n        \"jnb\": 649,\n        \"syd\": 89,\n        \"gru\": 68,\n        \"hkg\": 49\n      },\n      {\n        \"timestamp\": \"Feb 5, 06:00\",\n        \"hkg\": 58,\n        \"jnb\": 654,\n        \"syd\": 85,\n        \"ams\": 38,\n        \"iad\": 56,\n        \"gru\": 65\n      },\n      {\n        \"timestamp\": \"Feb 5, 07:00\",\n        \"hkg\": 67,\n        \"gru\": 68,\n        \"syd\": 85,\n        \"jnb\": 633,\n        \"iad\": 42,\n        \"ams\": 41\n      },\n      {\n        \"timestamp\": \"Feb 5, 08:00\",\n        \"jnb\": 671,\n        \"syd\": 88,\n        \"ams\": 49,\n        \"iad\": 69,\n        \"gru\": 36,\n        \"hkg\": 71\n      },\n      {\n        \"timestamp\": \"Feb 5, 09:00\",\n        \"ams\": 58,\n        \"iad\": 66,\n        \"syd\": 82,\n        \"jnb\": 618,\n        \"gru\": 121,\n        \"hkg\": 52\n      },\n      {\n        \"timestamp\": \"Feb 5, 10:00\",\n        \"hkg\": 57,\n        \"syd\": 86,\n        \"jnb\": 630,\n        \"ams\": 48,\n        \"iad\": 68,\n        \"gru\": 39\n      },\n      {\n        \"timestamp\": \"Feb 5, 11:00\",\n        \"hkg\": 68,\n        \"gru\": 94,\n        \"jnb\": 684,\n        \"syd\": 51,\n        \"iad\": 70,\n        \"ams\": 124\n      },\n      {\n        \"timestamp\": \"Feb 5, 12:00\",\n        \"hkg\": 70,\n        \"gru\": 80,\n        \"ams\": 58,\n        \"iad\": 57,\n        \"jnb\": 661,\n        \"syd\": 93\n      },\n      {\n        \"timestamp\": \"Feb 5, 13:00\",\n        \"hkg\": 78,\n        \"iad\": 68,\n        \"ams\": 87,\n        \"jnb\": 659,\n        \"syd\": 80,\n        \"gru\": 68\n      },\n      {\n        \"timestamp\": \"Feb 5, 14:00\",\n        \"hkg\": 62,\n        \"gru\": 53,\n        \"jnb\": 651,\n        \"syd\": 82,\n        \"iad\": 67,\n        \"ams\": 54\n      },\n      {\n        \"timestamp\": \"Feb 5, 15:00\",\n        \"gru\": 49,\n        \"syd\": 86,\n        \"jnb\": 657,\n        \"ams\": 56,\n        \"iad\": 83,\n        \"hkg\": 73\n      },\n      {\n        \"timestamp\": \"Feb 5, 16:00\",\n        \"syd\": 140,\n        \"jnb\": 658,\n        \"iad\": 110,\n        \"ams\": 67,\n        \"gru\": 77,\n        \"hkg\": 66\n      },\n      {\n        \"timestamp\": \"Feb 5, 17:00\",\n        \"hkg\": 57,\n        \"syd\": 76,\n        \"jnb\": 688,\n        \"iad\": 80,\n        \"ams\": 58,\n        \"gru\": 76\n      },\n      {\n        \"timestamp\": \"Feb 5, 18:00\",\n        \"hkg\": 74,\n        \"gru\": 114,\n        \"jnb\": 668,\n        \"syd\": 79,\n        \"iad\": 88,\n        \"ams\": 77\n      },\n      {\n        \"timestamp\": \"Feb 5, 19:00\",\n        \"gru\": 79,\n        \"ams\": 68,\n        \"iad\": 63,\n        \"jnb\": 749,\n        \"syd\": 105,\n        \"hkg\": 61\n      },\n      {\n        \"timestamp\": \"Feb 5, 20:00\",\n        \"ams\": 64,\n        \"iad\": 59,\n        \"syd\": 132,\n        \"jnb\": 658,\n        \"gru\": 86,\n        \"hkg\": 70\n      },\n      {\n        \"timestamp\": \"Feb 5, 21:00\",\n        \"ams\": 60,\n        \"iad\": 82,\n        \"jnb\": 742,\n        \"syd\": 82,\n        \"gru\": 59,\n        \"hkg\": 64\n      },\n      {\n        \"timestamp\": \"Feb 5, 22:00\",\n        \"gru\": 112,\n        \"syd\": 85,\n        \"jnb\": 665,\n        \"ams\": 56,\n        \"iad\": 72,\n        \"hkg\": 58\n      },\n      {\n        \"timestamp\": \"Feb 5, 23:00\",\n        \"iad\": 48,\n        \"ams\": 49,\n        \"syd\": 90,\n        \"jnb\": 722,\n        \"gru\": 74,\n        \"hkg\": 58\n      },\n      {\n        \"timestamp\": \"Feb 6, 00:00\",\n        \"hkg\": 45,\n        \"iad\": 69,\n        \"ams\": 47,\n        \"jnb\": 655,\n        \"syd\": 84,\n        \"gru\": 89\n      },\n      {\n        \"timestamp\": \"Feb 6, 01:00\",\n        \"hkg\": 38,\n        \"jnb\": 643,\n        \"syd\": 90,\n        \"ams\": 39,\n        \"iad\": 75,\n        \"gru\": 111\n      },\n      {\n        \"timestamp\": \"Feb 6, 02:00\",\n        \"gru\": 50,\n        \"jnb\": 660,\n        \"syd\": 96,\n        \"ams\": 42,\n        \"iad\": 61,\n        \"hkg\": 58\n      },\n      {\n        \"timestamp\": \"Feb 6, 03:00\",\n        \"iad\": 61,\n        \"ams\": 46,\n        \"jnb\": 728,\n        \"syd\": 92,\n        \"gru\": 132,\n        \"hkg\": 56\n      },\n      {\n        \"timestamp\": \"Feb 6, 04:00\",\n        \"hkg\": 59,\n        \"iad\": 52,\n        \"ams\": 42,\n        \"syd\": 60,\n        \"jnb\": 653,\n        \"gru\": 46\n      },\n      {\n        \"timestamp\": \"Feb 6, 05:00\",\n        \"hkg\": 51,\n        \"iad\": 60,\n        \"ams\": 39,\n        \"jnb\": 724,\n        \"syd\": 85,\n        \"gru\": 39\n      },\n      {\n        \"timestamp\": \"Feb 6, 06:00\",\n        \"hkg\": 67,\n        \"gru\": 97,\n        \"iad\": 72,\n        \"ams\": 46,\n        \"syd\": 90,\n        \"jnb\": 686\n      },\n      {\n        \"timestamp\": \"Feb 6, 07:00\",\n        \"hkg\": 50,\n        \"ams\": 52,\n        \"iad\": 54,\n        \"syd\": 88,\n        \"jnb\": 661,\n        \"gru\": 95\n      },\n      {\n        \"timestamp\": \"Feb 6, 08:00\",\n        \"hkg\": 74,\n        \"gru\": 64,\n        \"ams\": 54,\n        \"iad\": 51,\n        \"jnb\": 639,\n        \"syd\": 86\n      },\n      {\n        \"timestamp\": \"Feb 6, 09:00\",\n        \"hkg\": 44,\n        \"gru\": 94,\n        \"jnb\": 729,\n        \"syd\": 86,\n        \"ams\": 82,\n        \"iad\": 54\n      },\n      {\n        \"timestamp\": \"Feb 6, 10:00\",\n        \"hkg\": 78,\n        \"syd\": 68,\n        \"jnb\": 656,\n        \"ams\": 67,\n        \"iad\": 59,\n        \"gru\": 37\n      },\n      {\n        \"timestamp\": \"Feb 6, 11:00\",\n        \"hkg\": 58,\n        \"syd\": 48,\n        \"jnb\": 725,\n        \"ams\": 62,\n        \"iad\": 59,\n        \"gru\": 95\n      },\n      {\n        \"timestamp\": \"Feb 6, 12:00\",\n        \"hkg\": 45,\n        \"gru\": 35,\n        \"jnb\": 656,\n        \"syd\": 62,\n        \"ams\": 69,\n        \"iad\": 45\n      },\n      {\n        \"timestamp\": \"Feb 6, 13:00\",\n        \"hkg\": 63,\n        \"gru\": 69,\n        \"syd\": 64,\n        \"jnb\": 703,\n        \"ams\": 57,\n        \"iad\": 78\n      },\n      {\n        \"timestamp\": \"Feb 6, 14:00\",\n        \"gru\": 45,\n        \"jnb\": 645,\n        \"syd\": 63,\n        \"ams\": 61,\n        \"iad\": 62,\n        \"hkg\": 68\n      },\n      {\n        \"timestamp\": \"Feb 6, 15:00\",\n        \"iad\": 51,\n        \"ams\": 57,\n        \"jnb\": 648,\n        \"syd\": 58,\n        \"gru\": 73,\n        \"hkg\": 78\n      },\n      {\n        \"timestamp\": \"Feb 6, 16:00\",\n        \"gru\": 83,\n        \"syd\": 66,\n        \"jnb\": 646,\n        \"iad\": 67,\n        \"ams\": 73,\n        \"hkg\": 66\n      },\n      {\n        \"timestamp\": \"Feb 6, 17:00\",\n        \"gru\": 82,\n        \"ams\": 77,\n        \"iad\": 84,\n        \"syd\": 82,\n        \"jnb\": 641,\n        \"hkg\": 62\n      },\n      {\n        \"timestamp\": \"Feb 6, 18:00\",\n        \"hkg\": 46,\n        \"gru\": 110,\n        \"ams\": 72,\n        \"iad\": 76,\n        \"jnb\": 665,\n        \"syd\": 81\n      },\n      {\n        \"timestamp\": \"Feb 6, 19:00\",\n        \"hkg\": 112,\n        \"jnb\": 655,\n        \"syd\": 82,\n        \"iad\": 75,\n        \"ams\": 79,\n        \"gru\": 73\n      },\n      {\n        \"timestamp\": \"Feb 6, 20:00\",\n        \"gru\": 80,\n        \"jnb\": 654,\n        \"syd\": 84,\n        \"iad\": 55,\n        \"ams\": 128,\n        \"hkg\": 49\n      },\n      {\n        \"timestamp\": \"Feb 6, 21:00\",\n        \"gru\": 91,\n        \"syd\": 79,\n        \"jnb\": 650,\n        \"iad\": 85,\n        \"ams\": 83,\n        \"hkg\": 94\n      },\n      {\n        \"timestamp\": \"Feb 6, 22:00\",\n        \"jnb\": 652,\n        \"syd\": 86,\n        \"iad\": 84,\n        \"ams\": 62,\n        \"gru\": 81,\n        \"hkg\": 61\n      },\n      {\n        \"timestamp\": \"Feb 6, 23:00\",\n        \"hkg\": 60,\n        \"ams\": 46,\n        \"iad\": 86,\n        \"jnb\": 700,\n        \"syd\": 92,\n        \"gru\": 111\n      },\n      {\n        \"timestamp\": \"Feb 7, 00:00\",\n        \"hkg\": 55,\n        \"gru\": 78,\n        \"ams\": 60,\n        \"iad\": 84,\n        \"syd\": 97,\n        \"jnb\": 643\n      },\n      {\n        \"timestamp\": \"Feb 7, 01:00\",\n        \"hkg\": 36,\n        \"jnb\": 628,\n        \"syd\": 183,\n        \"iad\": 100,\n        \"ams\": 36,\n        \"gru\": 162\n      },\n      {\n        \"timestamp\": \"Feb 7, 02:00\",\n        \"hkg\": 62,\n        \"gru\": 59,\n        \"jnb\": 643,\n        \"syd\": 88,\n        \"ams\": 41,\n        \"iad\": 76\n      },\n      {\n        \"timestamp\": \"Feb 7, 03:00\",\n        \"gru\": 74,\n        \"syd\": 85,\n        \"jnb\": 616,\n        \"iad\": 59,\n        \"ams\": 42,\n        \"hkg\": 67\n      },\n      {\n        \"timestamp\": \"Feb 7, 04:00\",\n        \"iad\": 82,\n        \"ams\": 64,\n        \"syd\": 101,\n        \"jnb\": 649,\n        \"gru\": 44,\n        \"hkg\": 81\n      },\n      {\n        \"timestamp\": \"Feb 7, 05:00\",\n        \"hkg\": 45,\n        \"jnb\": 638,\n        \"syd\": 87,\n        \"iad\": 74,\n        \"ams\": 40,\n        \"gru\": 68\n      },\n      {\n        \"timestamp\": \"Feb 7, 06:00\",\n        \"hkg\": 56,\n        \"ams\": 51,\n        \"iad\": 60,\n        \"jnb\": 633,\n        \"syd\": 92,\n        \"gru\": 68\n      },\n      {\n        \"timestamp\": \"Feb 7, 07:00\",\n        \"hkg\": 45,\n        \"gru\": 119,\n        \"iad\": 38,\n        \"ams\": 48,\n        \"syd\": 84,\n        \"jnb\": 642\n      },\n      {\n        \"timestamp\": \"Feb 7, 08:00\",\n        \"gru\": 65,\n        \"syd\": 87,\n        \"jnb\": 653,\n        \"iad\": 80,\n        \"ams\": 42,\n        \"hkg\": 73\n      },\n      {\n        \"timestamp\": \"Feb 7, 09:00\",\n        \"gru\": 143,\n        \"iad\": 69,\n        \"ams\": 113,\n        \"jnb\": 722,\n        \"syd\": 95,\n        \"hkg\": 78\n      },\n      {\n        \"timestamp\": \"Feb 7, 10:00\",\n        \"jnb\": 646,\n        \"syd\": 70,\n        \"ams\": 68,\n        \"iad\": 80,\n        \"gru\": 114,\n        \"hkg\": 64\n      },\n      {\n        \"timestamp\": \"Feb 7, 11:00\",\n        \"gru\": 67,\n        \"syd\": 72,\n        \"jnb\": 662,\n        \"iad\": 68,\n        \"ams\": 63,\n        \"hkg\": 66\n      },\n      {\n        \"timestamp\": \"Feb 7, 12:00\",\n        \"hkg\": 57,\n        \"gru\": 105,\n        \"iad\": 77,\n        \"ams\": 64,\n        \"syd\": 135,\n        \"jnb\": 661\n      },\n      {\n        \"timestamp\": \"Feb 7, 13:00\",\n        \"hkg\": 67,\n        \"syd\": 136,\n        \"jnb\": 661,\n        \"ams\": 62,\n        \"iad\": 66,\n        \"gru\": 47\n      },\n      {\n        \"timestamp\": \"Feb 7, 14:00\",\n        \"hkg\": 59,\n        \"iad\": 65,\n        \"ams\": 54,\n        \"syd\": 79,\n        \"jnb\": 656,\n        \"gru\": 48\n      },\n      {\n        \"timestamp\": \"Feb 7, 15:00\",\n        \"gru\": 70,\n        \"ams\": 64,\n        \"iad\": 109,\n        \"syd\": 76,\n        \"jnb\": 702,\n        \"hkg\": 71\n      },\n      {\n        \"timestamp\": \"Feb 7, 16:00\",\n        \"syd\": 78,\n        \"jnb\": 655,\n        \"ams\": 56,\n        \"iad\": 94,\n        \"gru\": 75,\n        \"hkg\": 65\n      },\n      {\n        \"timestamp\": \"Feb 7, 17:00\",\n        \"iad\": 82,\n        \"ams\": 68,\n        \"jnb\": 654,\n        \"syd\": 78,\n        \"gru\": 111,\n        \"hkg\": 51\n      },\n      {\n        \"timestamp\": \"Feb 7, 18:00\",\n        \"gru\": 84,\n        \"ams\": 86,\n        \"iad\": 73,\n        \"jnb\": 656,\n        \"syd\": 76,\n        \"hkg\": 91\n      },\n      {\n        \"timestamp\": \"Feb 7, 19:00\",\n        \"syd\": 83,\n        \"jnb\": 656,\n        \"iad\": 83,\n        \"ams\": 65,\n        \"gru\": 141,\n        \"hkg\": 55\n      },\n      {\n        \"timestamp\": \"Feb 7, 20:00\",\n        \"gru\": 86,\n        \"iad\": 61,\n        \"ams\": 80,\n        \"syd\": 166,\n        \"jnb\": 661,\n        \"hkg\": 222\n      },\n      {\n        \"timestamp\": \"Feb 7, 21:00\",\n        \"hkg\": 63,\n        \"gru\": 87,\n        \"syd\": 84,\n        \"jnb\": 662,\n        \"ams\": 62,\n        \"iad\": 113\n      },\n      {\n        \"timestamp\": \"Feb 7, 22:00\",\n        \"hkg\": 58,\n        \"gru\": 109,\n        \"iad\": 99,\n        \"ams\": 52,\n        \"syd\": 84,\n        \"jnb\": 707\n      },\n      {\n        \"timestamp\": \"Feb 7, 23:00\",\n        \"hkg\": 55,\n        \"ams\": 45,\n        \"iad\": 70,\n        \"jnb\": 653,\n        \"syd\": 88,\n        \"gru\": 55\n      },\n      {\n        \"timestamp\": \"Feb 8, 00:00\",\n        \"hkg\": 55,\n        \"syd\": 85,\n        \"jnb\": 640,\n        \"ams\": 42,\n        \"iad\": 63,\n        \"gru\": 82\n      },\n      {\n        \"timestamp\": \"Feb 8, 01:00\",\n        \"hkg\": 50,\n        \"ams\": 45,\n        \"iad\": 62,\n        \"jnb\": 631,\n        \"syd\": 87,\n        \"gru\": 82\n      },\n      {\n        \"timestamp\": \"Feb 8, 02:00\",\n        \"iad\": 64,\n        \"ams\": 42,\n        \"jnb\": 647,\n        \"syd\": 89,\n        \"gru\": 136,\n        \"hkg\": 45\n      },\n      {\n        \"timestamp\": \"Feb 8, 03:00\",\n        \"gru\": 80,\n        \"ams\": 55,\n        \"iad\": 60,\n        \"syd\": 95,\n        \"jnb\": 639,\n        \"hkg\": 50\n      },\n      {\n        \"timestamp\": \"Feb 8, 04:00\",\n        \"ams\": 40,\n        \"iad\": 66,\n        \"syd\": 83,\n        \"jnb\": 720,\n        \"gru\": 130,\n        \"hkg\": 71\n      },\n      {\n        \"timestamp\": \"Feb 8, 05:00\",\n        \"gru\": 71,\n        \"syd\": 48,\n        \"jnb\": 642,\n        \"iad\": 64,\n        \"ams\": 41,\n        \"hkg\": 54\n      },\n      {\n        \"timestamp\": \"Feb 8, 06:00\",\n        \"gru\": 100,\n        \"ams\": 42,\n        \"iad\": 54,\n        \"syd\": 89,\n        \"jnb\": 643,\n        \"hkg\": 73\n      },\n      {\n        \"timestamp\": \"Feb 8, 07:00\",\n        \"hkg\": 48,\n        \"gru\": 66,\n        \"iad\": 60,\n        \"ams\": 47,\n        \"jnb\": 654,\n        \"syd\": 87\n      },\n      {\n        \"timestamp\": \"Feb 8, 08:00\",\n        \"hkg\": 59,\n        \"jnb\": 657,\n        \"syd\": 88,\n        \"iad\": 72,\n        \"ams\": 44,\n        \"gru\": 94\n      },\n      {\n        \"timestamp\": \"Feb 8, 09:00\",\n        \"gru\": 36,\n        \"jnb\": 650,\n        \"syd\": 92,\n        \"ams\": 49,\n        \"iad\": 54,\n        \"hkg\": 58\n      },\n      {\n        \"timestamp\": \"Feb 8, 10:00\",\n        \"syd\": 85,\n        \"jnb\": 653,\n        \"ams\": 54,\n        \"iad\": 38,\n        \"gru\": 131,\n        \"hkg\": 45\n      },\n      {\n        \"timestamp\": \"Feb 8, 11:00\",\n        \"hkg\": 56,\n        \"syd\": 85,\n        \"jnb\": 649,\n        \"ams\": 52,\n        \"iad\": 41,\n        \"gru\": 73\n      },\n      {\n        \"timestamp\": \"Feb 8, 12:00\",\n        \"hkg\": 71,\n        \"gru\": 38,\n        \"jnb\": 730,\n        \"syd\": 87,\n        \"ams\": 56,\n        \"iad\": 44\n      },\n      {\n        \"timestamp\": \"Feb 8, 13:00\",\n        \"hkg\": 59,\n        \"iad\": 109,\n        \"ams\": 49,\n        \"syd\": 84,\n        \"jnb\": 658,\n        \"gru\": 185\n      },\n      {\n        \"timestamp\": \"Feb 8, 14:00\",\n        \"hkg\": 53,\n        \"gru\": 68,\n        \"iad\": 64,\n        \"ams\": 96,\n        \"jnb\": 658,\n        \"syd\": 48\n      },\n      {\n        \"timestamp\": \"Feb 8, 15:00\",\n        \"hkg\": 66,\n        \"gru\": 85,\n        \"jnb\": 662,\n        \"syd\": 80,\n        \"iad\": 83,\n        \"ams\": 59\n      },\n      {\n        \"timestamp\": \"Feb 8, 16:00\",\n        \"gru\": 78,\n        \"syd\": 80,\n        \"jnb\": 745,\n        \"iad\": 114,\n        \"ams\": 76,\n        \"hkg\": 53\n      },\n      {\n        \"timestamp\": \"Feb 8, 17:00\",\n        \"gru\": 102,\n        \"syd\": 84,\n        \"jnb\": 661,\n        \"iad\": 88,\n        \"ams\": 59,\n        \"hkg\": 71\n      },\n      {\n        \"timestamp\": \"Feb 8, 18:00\",\n        \"hkg\": 79,\n        \"gru\": 105,\n        \"syd\": 80,\n        \"jnb\": 655,\n        \"ams\": 60,\n        \"iad\": 89\n      },\n      {\n        \"timestamp\": \"Feb 8, 19:00\",\n        \"hkg\": 78,\n        \"syd\": 80,\n        \"jnb\": 663,\n        \"ams\": 75,\n        \"iad\": 60,\n        \"gru\": 111\n      },\n      {\n        \"timestamp\": \"Feb 8, 20:00\",\n        \"jnb\": 649,\n        \"syd\": 111,\n        \"ams\": 61,\n        \"iad\": 88,\n        \"gru\": 89,\n        \"hkg\": 64\n      },\n      {\n        \"timestamp\": \"Feb 8, 21:00\",\n        \"gru\": 132,\n        \"syd\": 87,\n        \"jnb\": 647,\n        \"iad\": 81,\n        \"ams\": 64,\n        \"hkg\": 291\n      },\n      {\n        \"timestamp\": \"Feb 8, 22:00\",\n        \"iad\": 54,\n        \"ams\": 48,\n        \"jnb\": 660,\n        \"syd\": 86,\n        \"gru\": 103,\n        \"hkg\": 37\n      },\n      {\n        \"timestamp\": \"Feb 8, 23:00\",\n        \"jnb\": 645,\n        \"syd\": 50,\n        \"iad\": 57,\n        \"ams\": 48,\n        \"gru\": 112,\n        \"hkg\": 48\n      },\n      {\n        \"timestamp\": \"Feb 9, 00:00\",\n        \"gru\": 102,\n        \"syd\": 94,\n        \"jnb\": 640,\n        \"iad\": 67,\n        \"ams\": 47,\n        \"hkg\": 64\n      },\n      {\n        \"timestamp\": \"Feb 9, 01:00\",\n        \"gru\": 58,\n        \"syd\": 86,\n        \"jnb\": 646,\n        \"ams\": 44,\n        \"iad\": 72,\n        \"hkg\": 65\n      },\n      {\n        \"timestamp\": \"Feb 9, 02:00\",\n        \"jnb\": 722,\n        \"syd\": 96,\n        \"ams\": 49,\n        \"iad\": 68,\n        \"gru\": 78,\n        \"hkg\": 49\n      },\n      {\n        \"timestamp\": \"Feb 9, 03:00\",\n        \"hkg\": 64,\n        \"gru\": 55,\n        \"syd\": 94,\n        \"jnb\": 636,\n        \"ams\": 42,\n        \"iad\": 71\n      },\n      {\n        \"timestamp\": \"Feb 9, 04:00\",\n        \"hkg\": 45,\n        \"jnb\": 642,\n        \"syd\": 97,\n        \"ams\": 43,\n        \"iad\": 62,\n        \"gru\": 73\n      },\n      {\n        \"timestamp\": \"Feb 9, 05:00\",\n        \"syd\": 84,\n        \"jnb\": 643,\n        \"iad\": 61,\n        \"ams\": 42,\n        \"gru\": 66,\n        \"hkg\": 55\n      },\n      {\n        \"timestamp\": \"Feb 9, 06:00\",\n        \"gru\": 77,\n        \"jnb\": 621,\n        \"syd\": 87,\n        \"iad\": 62,\n        \"ams\": 40,\n        \"hkg\": 63\n      },\n      {\n        \"timestamp\": \"Feb 9, 07:00\",\n        \"gru\": 42,\n        \"iad\": 69,\n        \"ams\": 42,\n        \"jnb\": 652,\n        \"syd\": 87,\n        \"hkg\": 56\n      },\n      {\n        \"timestamp\": \"Feb 9, 08:00\",\n        \"syd\": 42,\n        \"jnb\": 733,\n        \"iad\": 56,\n        \"ams\": 50,\n        \"gru\": 63,\n        \"hkg\": 60\n      },\n      {\n        \"timestamp\": \"Feb 9, 09:00\",\n        \"syd\": 78,\n        \"jnb\": 640,\n        \"iad\": 71,\n        \"ams\": 49,\n        \"gru\": 63,\n        \"hkg\": 57\n      },\n      {\n        \"timestamp\": \"Feb 9, 10:00\",\n        \"hkg\": 59,\n        \"jnb\": 663,\n        \"syd\": 78,\n        \"iad\": 64,\n        \"ams\": 58,\n        \"gru\": 71\n      },\n      {\n        \"timestamp\": \"Feb 9, 11:00\",\n        \"hkg\": 60,\n        \"gru\": 65,\n        \"jnb\": 655,\n        \"syd\": 86,\n        \"iad\": 51,\n        \"ams\": 69\n      },\n      {\n        \"timestamp\": \"Feb 9, 12:00\",\n        \"hkg\": 73,\n        \"gru\": 69,\n        \"ams\": 53,\n        \"iad\": 62,\n        \"syd\": 82,\n        \"jnb\": 736\n      },\n      {\n        \"timestamp\": \"Feb 9, 13:00\",\n        \"hkg\": 58,\n        \"iad\": 56,\n        \"ams\": 56,\n        \"jnb\": 650,\n        \"syd\": 85,\n        \"gru\": 47\n      },\n      {\n        \"timestamp\": \"Feb 9, 14:00\",\n        \"iad\": 53,\n        \"ams\": 75,\n        \"syd\": 82,\n        \"jnb\": 649,\n        \"gru\": 72,\n        \"hkg\": 58\n      },\n      {\n        \"timestamp\": \"Feb 9, 15:00\",\n        \"syd\": 84,\n        \"jnb\": 786,\n        \"iad\": 68,\n        \"ams\": 52,\n        \"gru\": 66,\n        \"hkg\": 57\n      },\n      {\n        \"timestamp\": \"Feb 9, 16:00\",\n        \"gru\": 170,\n        \"jnb\": 942,\n        \"syd\": 43,\n        \"iad\": 113,\n        \"ams\": 54,\n        \"hkg\": 52\n      },\n      {\n        \"timestamp\": \"Feb 9, 17:00\",\n        \"ams\": 53,\n        \"iad\": 80,\n        \"syd\": 43,\n        \"jnb\": 916,\n        \"gru\": 116,\n        \"hkg\": 52\n      },\n      {\n        \"timestamp\": \"Feb 9, 18:00\",\n        \"gru\": 86,\n        \"syd\": 77,\n        \"jnb\": 925,\n        \"ams\": 50,\n        \"iad\": 67,\n        \"hkg\": 45\n      },\n      {\n        \"timestamp\": \"Feb 9, 19:00\",\n        \"hkg\": 44,\n        \"gru\": 52,\n        \"jnb\": 921,\n        \"syd\": 82,\n        \"iad\": 68,\n        \"ams\": 56\n      },\n      {\n        \"timestamp\": \"Feb 9, 20:00\",\n        \"hkg\": 53,\n        \"jnb\": 919,\n        \"syd\": 79,\n        \"ams\": 72,\n        \"iad\": 74,\n        \"gru\": 126\n      },\n      {\n        \"timestamp\": \"Feb 9, 21:00\",\n        \"gru\": 73,\n        \"iad\": 79,\n        \"ams\": 47,\n        \"syd\": 125,\n        \"jnb\": 964,\n        \"hkg\": 212\n      },\n      {\n        \"timestamp\": \"Feb 9, 22:00\",\n        \"syd\": 77,\n        \"jnb\": 935,\n        \"iad\": 81,\n        \"ams\": 42,\n        \"gru\": 45,\n        \"hkg\": 87\n      },\n      {\n        \"timestamp\": \"Feb 9, 23:00\",\n        \"hkg\": 57,\n        \"jnb\": 923,\n        \"syd\": 92,\n        \"ams\": 45,\n        \"iad\": 58,\n        \"gru\": 130\n      },\n      {\n        \"timestamp\": \"Feb 10, 00:00\",\n        \"hkg\": 47,\n        \"iad\": 88,\n        \"ams\": 45,\n        \"jnb\": 918,\n        \"syd\": 81,\n        \"gru\": 87\n      },\n      {\n        \"timestamp\": \"Feb 10, 01:00\",\n        \"hkg\": 45,\n        \"syd\": 86,\n        \"jnb\": 1016,\n        \"iad\": 74,\n        \"ams\": 40,\n        \"gru\": 75\n      },\n      {\n        \"timestamp\": \"Feb 10, 02:00\",\n        \"hkg\": 79,\n        \"gru\": 112,\n        \"syd\": 82,\n        \"jnb\": 906,\n        \"iad\": 54,\n        \"ams\": 45\n      },\n      {\n        \"timestamp\": \"Feb 10, 03:00\",\n        \"hkg\": 45,\n        \"jnb\": 920,\n        \"syd\": 80,\n        \"ams\": 50,\n        \"iad\": 64,\n        \"gru\": 99\n      },\n      {\n        \"timestamp\": \"Feb 10, 04:00\",\n        \"jnb\": 934,\n        \"syd\": 50,\n        \"iad\": 71,\n        \"ams\": 44,\n        \"gru\": 72,\n        \"hkg\": 63\n      },\n      {\n        \"timestamp\": \"Feb 10, 05:00\",\n        \"gru\": 46,\n        \"ams\": 43,\n        \"iad\": 65,\n        \"jnb\": 992,\n        \"syd\": 86,\n        \"hkg\": 54\n      },\n      {\n        \"timestamp\": \"Feb 10, 06:00\",\n        \"hkg\": 49,\n        \"gru\": 62,\n        \"syd\": 95,\n        \"jnb\": 901,\n        \"iad\": 78,\n        \"ams\": 41\n      },\n      {\n        \"timestamp\": \"Feb 10, 07:00\",\n        \"hkg\": 57,\n        \"jnb\": 918,\n        \"syd\": 82,\n        \"ams\": 42,\n        \"iad\": 52,\n        \"gru\": 66\n      },\n      {\n        \"timestamp\": \"Feb 10, 08:00\",\n        \"hkg\": 44,\n        \"iad\": 56,\n        \"ams\": 44,\n        \"jnb\": 916,\n        \"syd\": 85,\n        \"gru\": 65\n      },\n      {\n        \"timestamp\": \"Feb 10, 09:00\",\n        \"hkg\": 56,\n        \"syd\": 87,\n        \"jnb\": 918,\n        \"ams\": 44,\n        \"iad\": 53,\n        \"gru\": 63\n      },\n      {\n        \"timestamp\": \"Feb 10, 10:00\",\n        \"hkg\": 59,\n        \"gru\": 88,\n        \"ams\": 49,\n        \"iad\": 68,\n        \"syd\": 84,\n        \"jnb\": 929\n      },\n      {\n        \"timestamp\": \"Feb 10, 11:00\",\n        \"hkg\": 61,\n        \"jnb\": 925,\n        \"syd\": 91,\n        \"iad\": 67,\n        \"ams\": 56,\n        \"gru\": 68\n      },\n      {\n        \"timestamp\": \"Feb 10, 12:00\",\n        \"hkg\": 50,\n        \"gru\": 67,\n        \"jnb\": 904,\n        \"syd\": 77,\n        \"ams\": 52,\n        \"iad\": 63\n      },\n      {\n        \"timestamp\": \"Feb 10, 13:00\",\n        \"gru\": 98,\n        \"iad\": 57,\n        \"ams\": 51,\n        \"jnb\": 934,\n        \"syd\": 38,\n        \"hkg\": 54\n      },\n      {\n        \"timestamp\": \"Feb 10, 14:00\",\n        \"jnb\": 928,\n        \"syd\": 81,\n        \"iad\": 69,\n        \"ams\": 134,\n        \"gru\": 104,\n        \"hkg\": 56\n      },\n      {\n        \"timestamp\": \"Feb 10, 15:00\",\n        \"hkg\": 57,\n        \"gru\": 63,\n        \"jnb\": 1006,\n        \"syd\": 84,\n        \"ams\": 49,\n        \"iad\": 59\n      },\n      {\n        \"timestamp\": \"Feb 10, 16:00\",\n        \"hkg\": 62,\n        \"gru\": 108,\n        \"iad\": 86,\n        \"ams\": 61,\n        \"jnb\": 921,\n        \"syd\": 83\n      },\n      {\n        \"timestamp\": \"Feb 10, 17:00\",\n        \"hkg\": 78,\n        \"gru\": 77,\n        \"syd\": 77,\n        \"jnb\": 910,\n        \"iad\": 71,\n        \"ams\": 60\n      },\n      {\n        \"timestamp\": \"Feb 10, 18:00\",\n        \"gru\": 77,\n        \"iad\": 58,\n        \"ams\": 78,\n        \"syd\": 81,\n        \"jnb\": 920,\n        \"hkg\": 66\n      },\n      {\n        \"timestamp\": \"Feb 10, 19:00\",\n        \"ams\": 57,\n        \"iad\": 71,\n        \"jnb\": 902,\n        \"syd\": 84,\n        \"gru\": 128,\n        \"hkg\": 66\n      },\n      {\n        \"timestamp\": \"Feb 10, 20:00\",\n        \"gru\": 46,\n        \"jnb\": 929,\n        \"syd\": 114,\n        \"iad\": 58,\n        \"ams\": 133,\n        \"hkg\": 52\n      },\n      {\n        \"timestamp\": \"Feb 10, 21:00\",\n        \"ams\": 74,\n        \"iad\": 72,\n        \"jnb\": 915,\n        \"syd\": 134,\n        \"gru\": 122,\n        \"hkg\": 49\n      },\n      {\n        \"timestamp\": \"Feb 10, 22:00\",\n        \"hkg\": 92,\n        \"jnb\": 925,\n        \"syd\": 83,\n        \"ams\": 48,\n        \"iad\": 65,\n        \"gru\": 81\n      },\n      {\n        \"timestamp\": \"Feb 10, 23:00\",\n        \"hkg\": 50,\n        \"gru\": 73,\n        \"syd\": 85,\n        \"jnb\": 996,\n        \"iad\": 59,\n        \"ams\": 45\n      },\n      {\n        \"timestamp\": \"Feb 11, 00:00\",\n        \"hkg\": 49,\n        \"gru\": 53,\n        \"ams\": 59,\n        \"iad\": 58,\n        \"syd\": 81,\n        \"jnb\": 894\n      },\n      {\n        \"timestamp\": \"Feb 11, 01:00\",\n        \"hkg\": 47,\n        \"jnb\": 914,\n        \"syd\": 88,\n        \"ams\": 45,\n        \"iad\": 71,\n        \"gru\": 53\n      },\n      {\n        \"timestamp\": \"Feb 11, 02:00\",\n        \"hkg\": 44,\n        \"gru\": 81,\n        \"syd\": 89,\n        \"jnb\": 885,\n        \"ams\": 80,\n        \"iad\": 75\n      },\n      {\n        \"timestamp\": \"Feb 11, 03:00\",\n        \"gru\": 44,\n        \"iad\": 75,\n        \"ams\": 54,\n        \"syd\": 86,\n        \"jnb\": 883,\n        \"hkg\": 72\n      },\n      {\n        \"timestamp\": \"Feb 11, 04:00\",\n        \"iad\": 73,\n        \"ams\": 51,\n        \"jnb\": 892,\n        \"syd\": 90,\n        \"gru\": 48,\n        \"hkg\": 74\n      },\n      {\n        \"timestamp\": \"Feb 11, 05:00\",\n        \"gru\": 92,\n        \"iad\": 60,\n        \"ams\": 44,\n        \"syd\": 83,\n        \"jnb\": 906,\n        \"hkg\": 60\n      },\n      {\n        \"timestamp\": \"Feb 11, 06:00\",\n        \"iad\": 60,\n        \"ams\": 54,\n        \"jnb\": 916,\n        \"syd\": 86,\n        \"gru\": 63,\n        \"hkg\": 58\n      },\n      {\n        \"timestamp\": \"Feb 11, 07:00\",\n        \"hkg\": 44,\n        \"jnb\": 982,\n        \"syd\": 89,\n        \"ams\": 62,\n        \"iad\": 58,\n        \"gru\": 66\n      },\n      {\n        \"timestamp\": \"Feb 11, 08:00\",\n        \"hkg\": 50,\n        \"gru\": 65,\n        \"syd\": 89,\n        \"jnb\": 924,\n        \"ams\": 48,\n        \"iad\": 62\n      },\n      {\n        \"timestamp\": \"Feb 11, 09:00\",\n        \"hkg\": 54,\n        \"syd\": 120,\n        \"jnb\": 942,\n        \"iad\": 56,\n        \"ams\": 44,\n        \"gru\": 122\n      },\n      {\n        \"timestamp\": \"Feb 11, 10:00\",\n        \"gru\": 37,\n        \"syd\": 94,\n        \"jnb\": 921,\n        \"iad\": 56,\n        \"ams\": 52,\n        \"hkg\": 56\n      },\n      {\n        \"timestamp\": \"Feb 11, 11:00\",\n        \"ams\": 59,\n        \"iad\": 57,\n        \"syd\": 90,\n        \"jnb\": 912,\n        \"gru\": 92,\n        \"hkg\": 53\n      },\n      {\n        \"timestamp\": \"Feb 11, 12:00\",\n        \"hkg\": 54,\n        \"ams\": 144,\n        \"iad\": 56,\n        \"jnb\": 921,\n        \"syd\": 50,\n        \"gru\": 62\n      },\n      {\n        \"timestamp\": \"Feb 11, 13:00\",\n        \"hkg\": 46,\n        \"gru\": 50,\n        \"syd\": 84,\n        \"jnb\": 916,\n        \"ams\": 58,\n        \"iad\": 66\n      },\n      {\n        \"timestamp\": \"Feb 11, 14:00\",\n        \"hkg\": 45,\n        \"ams\": 55,\n        \"iad\": 74,\n        \"jnb\": 920,\n        \"syd\": 80,\n        \"gru\": 92\n      },\n      {\n        \"timestamp\": \"Feb 11, 15:00\",\n        \"hkg\": 58,\n        \"gru\": 75,\n        \"jnb\": 894,\n        \"syd\": 81,\n        \"iad\": 52,\n        \"ams\": 63\n      },\n      {\n        \"timestamp\": \"Feb 11, 16:00\",\n        \"gru\": 64,\n        \"syd\": 84,\n        \"jnb\": 927,\n        \"iad\": 64,\n        \"ams\": 109,\n        \"hkg\": 53\n      },\n      {\n        \"timestamp\": \"Feb 11, 17:00\",\n        \"hkg\": 68,\n        \"gru\": 43,\n        \"syd\": 78,\n        \"jnb\": 920,\n        \"iad\": 67,\n        \"ams\": 68\n      },\n      {\n        \"timestamp\": \"Feb 11, 18:00\",\n        \"hkg\": 46,\n        \"jnb\": 1003,\n        \"syd\": 76,\n        \"iad\": 82,\n        \"ams\": 95,\n        \"gru\": 68\n      },\n      {\n        \"timestamp\": \"Feb 11, 19:00\",\n        \"iad\": 70,\n        \"ams\": 68,\n        \"syd\": 80,\n        \"jnb\": 934,\n        \"gru\": 106,\n        \"hkg\": 45\n      },\n      {\n        \"timestamp\": \"Feb 11, 20:00\",\n        \"gru\": 73,\n        \"iad\": 42,\n        \"ams\": 63,\n        \"jnb\": 935,\n        \"syd\": 83,\n        \"hkg\": 67\n      },\n      {\n        \"timestamp\": \"Feb 11, 21:00\",\n        \"hkg\": 56,\n        \"gru\": 96,\n        \"syd\": 88,\n        \"jnb\": 929,\n        \"iad\": 70,\n        \"ams\": 82\n      },\n      {\n        \"timestamp\": \"Feb 11, 22:00\",\n        \"hkg\": 38,\n        \"jnb\": 993,\n        \"syd\": 127,\n        \"iad\": 80,\n        \"ams\": 75,\n        \"gru\": 40\n      },\n      {\n        \"timestamp\": \"Feb 11, 23:00\",\n        \"hkg\": 98,\n        \"gru\": 69,\n        \"jnb\": 925,\n        \"syd\": 54,\n        \"ams\": 76,\n        \"iad\": 62\n      },\n      {\n        \"timestamp\": \"Feb 12, 00:00\",\n        \"hkg\": 39,\n        \"syd\": 85,\n        \"jnb\": 913,\n        \"ams\": 68,\n        \"iad\": 59,\n        \"gru\": 99\n      },\n      {\n        \"timestamp\": \"Feb 12, 01:00\",\n        \"hkg\": 54,\n        \"gru\": 47,\n        \"syd\": 92,\n        \"jnb\": 926,\n        \"iad\": 70,\n        \"ams\": 41\n      },\n      {\n        \"timestamp\": \"Feb 12, 02:00\",\n        \"gru\": 83,\n        \"jnb\": 997,\n        \"syd\": 87,\n        \"iad\": 68,\n        \"ams\": 40,\n        \"hkg\": 48\n      },\n      {\n        \"timestamp\": \"Feb 12, 03:00\",\n        \"ams\": 51,\n        \"iad\": 59,\n        \"jnb\": 919,\n        \"syd\": 87,\n        \"gru\": 38,\n        \"hkg\": 50\n      },\n      {\n        \"timestamp\": \"Feb 12, 04:00\",\n        \"hkg\": 68,\n        \"gru\": 45,\n        \"ams\": 98,\n        \"iad\": 58,\n        \"jnb\": 918,\n        \"syd\": 125\n      },\n      {\n        \"timestamp\": \"Feb 12, 05:00\",\n        \"hkg\": 51,\n        \"syd\": 86,\n        \"jnb\": 914,\n        \"ams\": 86,\n        \"iad\": 78,\n        \"gru\": 124\n      },\n      {\n        \"timestamp\": \"Feb 12, 06:00\",\n        \"jnb\": 913,\n        \"syd\": 464,\n        \"ams\": 443,\n        \"iad\": 56,\n        \"gru\": 88,\n        \"hkg\": 44\n      },\n      {\n        \"timestamp\": \"Feb 12, 07:00\",\n        \"gru\": 63,\n        \"iad\": 54,\n        \"ams\": 100,\n        \"jnb\": 912,\n        \"syd\": 92,\n        \"hkg\": 48\n      },\n      {\n        \"timestamp\": \"Feb 12, 08:00\",\n        \"syd\": 46,\n        \"jnb\": 910,\n        \"iad\": 54,\n        \"ams\": 185,\n        \"gru\": 34,\n        \"hkg\": 62\n      },\n      {\n        \"timestamp\": \"Feb 12, 09:00\",\n        \"gru\": 88,\n        \"jnb\": 914,\n        \"syd\": 210,\n        \"ams\": 351,\n        \"iad\": 56,\n        \"hkg\": 58\n      },\n      {\n        \"timestamp\": \"Feb 12, 10:00\",\n        \"gru\": 33,\n        \"iad\": 56,\n        \"ams\": 59,\n        \"jnb\": 928,\n        \"syd\": 90,\n        \"hkg\": 64\n      },\n      {\n        \"timestamp\": \"Feb 12, 11:00\",\n        \"hkg\": 62,\n        \"gru\": 93,\n        \"ams\": 62,\n        \"iad\": 64,\n        \"syd\": 90,\n        \"jnb\": 928\n      },\n      {\n        \"timestamp\": \"Feb 12, 12:00\",\n        \"hkg\": 62,\n        \"syd\": 118,\n        \"jnb\": 1041,\n        \"ams\": 57,\n        \"iad\": 48,\n        \"gru\": 42\n      },\n      {\n        \"timestamp\": \"Feb 12, 13:00\",\n        \"hkg\": 72,\n        \"syd\": 81,\n        \"jnb\": 926,\n        \"ams\": 58,\n        \"iad\": 58,\n        \"gru\": 80\n      },\n      {\n        \"timestamp\": \"Feb 12, 14:00\",\n        \"hkg\": 57,\n        \"gru\": 77,\n        \"syd\": 80,\n        \"jnb\": 1012,\n        \"iad\": 66,\n        \"ams\": 151\n      },\n      {\n        \"timestamp\": \"Feb 12, 15:00\",\n        \"gru\": 104,\n        \"jnb\": 937,\n        \"syd\": 80,\n        \"ams\": 252,\n        \"iad\": 63,\n        \"hkg\": 116\n      },\n      {\n        \"timestamp\": \"Feb 12, 16:00\",\n        \"ams\": 131,\n        \"iad\": 99,\n        \"jnb\": 729,\n        \"syd\": 74,\n        \"gru\": 68,\n        \"hkg\": 80\n      },\n      {\n        \"timestamp\": \"Feb 12, 17:00\",\n        \"ams\": 68,\n        \"iad\": 70,\n        \"syd\": 43,\n        \"jnb\": 665,\n        \"gru\": 102,\n        \"hkg\": 79\n      },\n      {\n        \"timestamp\": \"Feb 12, 18:00\",\n        \"syd\": 81,\n        \"jnb\": 657,\n        \"iad\": 78,\n        \"ams\": 69,\n        \"gru\": 105,\n        \"hkg\": 55\n      },\n      {\n        \"timestamp\": \"Feb 12, 19:00\",\n        \"gru\": 80,\n        \"jnb\": 672,\n        \"syd\": 82,\n        \"ams\": 116,\n        \"iad\": 57,\n        \"hkg\": 50\n      },\n      {\n        \"timestamp\": \"Feb 12, 20:00\",\n        \"hkg\": 59,\n        \"gru\": 47,\n        \"ams\": 66,\n        \"iad\": 67,\n        \"jnb\": 734,\n        \"syd\": 115\n      },\n      {\n        \"timestamp\": \"Feb 12, 21:00\",\n        \"hkg\": 48,\n        \"jnb\": 640,\n        \"syd\": 81,\n        \"iad\": 75,\n        \"ams\": 103,\n        \"gru\": 109\n      },\n      {\n        \"timestamp\": \"Feb 12, 22:00\",\n        \"gru\": 228,\n        \"syd\": 131,\n        \"jnb\": 798,\n        \"ams\": 54,\n        \"iad\": 82,\n        \"hkg\": 62\n      },\n      {\n        \"timestamp\": \"Feb 12, 23:00\",\n        \"jnb\": 621,\n        \"syd\": 74,\n        \"iad\": 79,\n        \"ams\": 53,\n        \"gru\": 106,\n        \"hkg\": 103\n      },\n      {\n        \"timestamp\": \"Feb 13, 00:00\",\n        \"hkg\": 51,\n        \"iad\": 76,\n        \"ams\": 50,\n        \"jnb\": 642,\n        \"syd\": 52,\n        \"gru\": 77\n      },\n      {\n        \"timestamp\": \"Feb 13, 01:00\",\n        \"gru\": 131,\n        \"syd\": 92,\n        \"jnb\": 640,\n        \"ams\": 38,\n        \"iad\": 71,\n        \"hkg\": 72\n      },\n      {\n        \"timestamp\": \"Feb 13, 02:00\",\n        \"gru\": 83,\n        \"iad\": 85,\n        \"ams\": 68,\n        \"syd\": 85,\n        \"jnb\": 749,\n        \"hkg\": 51\n      },\n      {\n        \"timestamp\": \"Feb 13, 03:00\",\n        \"jnb\": 714,\n        \"syd\": 84,\n        \"ams\": 37,\n        \"iad\": 63,\n        \"gru\": 42,\n        \"hkg\": 54\n      },\n      {\n        \"timestamp\": \"Feb 13, 04:00\",\n        \"gru\": 40,\n        \"ams\": 61,\n        \"iad\": 85,\n        \"jnb\": 641,\n        \"syd\": 92,\n        \"hkg\": 62\n      },\n      {\n        \"timestamp\": \"Feb 13, 05:00\",\n        \"hkg\": 73,\n        \"gru\": 34,\n        \"jnb\": 641,\n        \"syd\": 87,\n        \"iad\": 72,\n        \"ams\": 44\n      },\n      {\n        \"timestamp\": \"Feb 13, 06:00\",\n        \"hkg\": 46,\n        \"jnb\": 711,\n        \"syd\": 88,\n        \"ams\": 47,\n        \"iad\": 61,\n        \"gru\": 39\n      },\n      {\n        \"timestamp\": \"Feb 13, 07:00\",\n        \"jnb\": 718,\n        \"syd\": 106,\n        \"ams\": 48,\n        \"iad\": 76,\n        \"gru\": 34,\n        \"hkg\": 63\n      },\n      {\n        \"timestamp\": \"Feb 13, 08:00\",\n        \"gru\": 40,\n        \"ams\": 47,\n        \"iad\": 49,\n        \"jnb\": 629,\n        \"syd\": 48,\n        \"hkg\": 66\n      },\n      {\n        \"timestamp\": \"Feb 13, 09:00\",\n        \"hkg\": 54,\n        \"syd\": 86,\n        \"jnb\": 658,\n        \"ams\": 83,\n        \"iad\": 72,\n        \"gru\": 68\n      },\n      {\n        \"timestamp\": \"Feb 13, 10:00\",\n        \"hkg\": 56,\n        \"iad\": 76,\n        \"ams\": 52,\n        \"syd\": 83,\n        \"jnb\": 653,\n        \"gru\": 118\n      },\n      {\n        \"timestamp\": \"Feb 13, 11:00\",\n        \"hkg\": 59,\n        \"gru\": 219,\n        \"ams\": 100,\n        \"iad\": 56,\n        \"jnb\": 660,\n        \"syd\": 88\n      },\n      {\n        \"timestamp\": \"Feb 13, 12:00\",\n        \"hkg\": 43,\n        \"ams\": 115,\n        \"iad\": 54,\n        \"jnb\": 675,\n        \"syd\": 90,\n        \"gru\": 62\n      },\n      {\n        \"timestamp\": \"Feb 13, 13:00\",\n        \"hkg\": 60,\n        \"gru\": 98,\n        \"jnb\": 668,\n        \"syd\": 84,\n        \"iad\": 69,\n        \"ams\": 48\n      },\n      {\n        \"timestamp\": \"Feb 13, 14:00\",\n        \"gru\": 126,\n        \"jnb\": 660,\n        \"syd\": 81,\n        \"ams\": 55,\n        \"iad\": 74,\n        \"hkg\": 73\n      },\n      {\n        \"timestamp\": \"Feb 13, 15:00\",\n        \"syd\": 85,\n        \"jnb\": 787,\n        \"iad\": 59,\n        \"ams\": 78,\n        \"gru\": 38,\n        \"hkg\": 69\n      },\n      {\n        \"timestamp\": \"Feb 13, 16:00\",\n        \"hkg\": 65,\n        \"gru\": 86,\n        \"iad\": 84,\n        \"ams\": 78,\n        \"jnb\": 659,\n        \"syd\": 103\n      },\n      {\n        \"timestamp\": \"Feb 13, 17:00\",\n        \"hkg\": 83,\n        \"ams\": 69,\n        \"iad\": 74,\n        \"syd\": 78,\n        \"jnb\": 885,\n        \"gru\": 100\n      },\n      {\n        \"timestamp\": \"Feb 13, 18:00\",\n        \"ams\": 156,\n        \"iad\": 56,\n        \"jnb\": 712,\n        \"syd\": 76,\n        \"gru\": 50,\n        \"hkg\": 61\n      },\n      {\n        \"timestamp\": \"Feb 13, 19:00\",\n        \"jnb\": 650,\n        \"syd\": 38,\n        \"ams\": 71,\n        \"iad\": 78,\n        \"gru\": 81,\n        \"hkg\": 62\n      },\n      {\n        \"timestamp\": \"Feb 13, 20:00\",\n        \"gru\": 46,\n        \"syd\": 134,\n        \"jnb\": 787,\n        \"ams\": 87,\n        \"iad\": 106,\n        \"hkg\": 64\n      },\n      {\n        \"timestamp\": \"Feb 13, 21:00\",\n        \"iad\": 88,\n        \"ams\": 70,\n        \"jnb\": 668,\n        \"syd\": 85,\n        \"gru\": 101,\n        \"hkg\": 46\n      },\n      {\n        \"timestamp\": \"Feb 13, 22:00\",\n        \"gru\": 130,\n        \"iad\": 94,\n        \"ams\": 105,\n        \"syd\": 127,\n        \"jnb\": 650,\n        \"hkg\": 48\n      },\n      {\n        \"timestamp\": \"Feb 13, 23:00\",\n        \"hkg\": 103,\n        \"gru\": 104,\n        \"iad\": 94,\n        \"ams\": 47,\n        \"syd\": 89,\n        \"jnb\": 659\n      },\n      {\n        \"timestamp\": \"Feb 14, 00:00\",\n        \"hkg\": 59,\n        \"iad\": 90,\n        \"ams\": 48,\n        \"jnb\": 648,\n        \"syd\": 90,\n        \"gru\": 148\n      },\n      {\n        \"timestamp\": \"Feb 14, 01:00\",\n        \"hkg\": 70,\n        \"gru\": 54,\n        \"iad\": 64,\n        \"ams\": 47,\n        \"syd\": 86,\n        \"jnb\": 744\n      },\n      {\n        \"timestamp\": \"Feb 14, 02:00\",\n        \"hkg\": 86,\n        \"iad\": 62,\n        \"ams\": 50,\n        \"jnb\": 637,\n        \"syd\": 88,\n        \"gru\": 78\n      },\n      {\n        \"timestamp\": \"Feb 14, 03:00\",\n        \"hkg\": 44,\n        \"jnb\": 798,\n        \"syd\": 98,\n        \"iad\": 74,\n        \"ams\": 39,\n        \"gru\": 80\n      },\n      {\n        \"timestamp\": \"Feb 14, 04:00\",\n        \"syd\": 87,\n        \"jnb\": 633,\n        \"iad\": 60,\n        \"ams\": 52,\n        \"gru\": 43,\n        \"hkg\": 64\n      },\n      {\n        \"timestamp\": \"Feb 14, 05:00\",\n        \"gru\": 38,\n        \"jnb\": 738,\n        \"syd\": 96,\n        \"ams\": 45,\n        \"iad\": 75,\n        \"hkg\": 68\n      },\n      {\n        \"timestamp\": \"Feb 14, 06:00\",\n        \"ams\": 44,\n        \"iad\": 66,\n        \"syd\": 52,\n        \"jnb\": 724,\n        \"gru\": 75,\n        \"hkg\": 71\n      },\n      {\n        \"timestamp\": \"Feb 14, 07:00\",\n        \"gru\": 63,\n        \"ams\": 57,\n        \"iad\": 67,\n        \"syd\": 100,\n        \"jnb\": 776,\n        \"hkg\": 51\n      },\n      {\n        \"timestamp\": \"Feb 14, 08:00\",\n        \"hkg\": 75,\n        \"gru\": 40,\n        \"ams\": 41,\n        \"iad\": 71,\n        \"jnb\": 651,\n        \"syd\": 83\n      },\n      {\n        \"timestamp\": \"Feb 14, 09:00\",\n        \"iad\": 70,\n        \"ams\": 80,\n        \"jnb\": 750,\n        \"syd\": 87,\n        \"gru\": 67,\n        \"hkg\": 63\n      },\n      {\n        \"timestamp\": \"Feb 14, 10:00\",\n        \"hkg\": 64,\n        \"gru\": 66,\n        \"iad\": 53,\n        \"ams\": 51,\n        \"jnb\": 655,\n        \"syd\": 84\n      },\n      {\n        \"timestamp\": \"Feb 14, 11:00\",\n        \"hkg\": 42,\n        \"gru\": 68,\n        \"jnb\": 812,\n        \"syd\": 84,\n        \"iad\": 68,\n        \"ams\": 56\n      },\n      {\n        \"timestamp\": \"Feb 14, 12:00\",\n        \"hkg\": 58,\n        \"syd\": 83,\n        \"jnb\": 652,\n        \"iad\": 67,\n        \"ams\": 57,\n        \"gru\": 98\n      },\n      {\n        \"timestamp\": \"Feb 14, 13:00\",\n        \"jnb\": 680,\n        \"syd\": 86,\n        \"ams\": 50,\n        \"iad\": 64,\n        \"gru\": 122,\n        \"hkg\": 78\n      },\n      {\n        \"timestamp\": \"Feb 14, 14:00\",\n        \"gru\": 165,\n        \"syd\": 80,\n        \"jnb\": 716,\n        \"ams\": 49,\n        \"iad\": 83,\n        \"hkg\": 96\n      },\n      {\n        \"timestamp\": \"Feb 14, 15:00\",\n        \"ams\": 81,\n        \"iad\": 84,\n        \"syd\": 82,\n        \"jnb\": 724,\n        \"gru\": 75,\n        \"hkg\": 85\n      },\n      {\n        \"timestamp\": \"Feb 14, 16:00\",\n        \"gru\": 52,\n        \"ams\": 47,\n        \"iad\": 76,\n        \"jnb\": 685,\n        \"syd\": 80,\n        \"hkg\": 63\n      },\n      {\n        \"timestamp\": \"Feb 14, 17:00\",\n        \"hkg\": 78,\n        \"ams\": 53,\n        \"iad\": 79,\n        \"jnb\": 686,\n        \"syd\": 73,\n        \"gru\": 53\n      },\n      {\n        \"timestamp\": \"Feb 14, 18:00\",\n        \"hkg\": 74,\n        \"gru\": 84,\n        \"ams\": 54,\n        \"iad\": 71,\n        \"syd\": 39,\n        \"jnb\": 659\n      },\n      {\n        \"timestamp\": \"Feb 14, 19:00\",\n        \"hkg\": 59,\n        \"gru\": 93,\n        \"syd\": 79,\n        \"jnb\": 643,\n        \"ams\": 49,\n        \"iad\": 98\n      },\n      {\n        \"timestamp\": \"Feb 14, 20:00\",\n        \"gru\": 70,\n        \"syd\": 73,\n        \"jnb\": 653,\n        \"iad\": 49,\n        \"ams\": 55,\n        \"hkg\": 36\n      },\n      {\n        \"timestamp\": \"Feb 14, 21:00\",\n        \"jnb\": 619,\n        \"syd\": 82,\n        \"ams\": 58,\n        \"iad\": 73,\n        \"gru\": 136,\n        \"hkg\": 70\n      },\n      {\n        \"timestamp\": \"Feb 14, 22:00\",\n        \"hkg\": 40,\n        \"syd\": 132,\n        \"jnb\": 685,\n        \"ams\": 72,\n        \"iad\": 58,\n        \"gru\": 109\n      },\n      {\n        \"timestamp\": \"Feb 14, 23:00\",\n        \"hkg\": 51,\n        \"gru\": 98,\n        \"syd\": 90,\n        \"jnb\": 682,\n        \"ams\": 42,\n        \"iad\": 73\n      },\n      {\n        \"timestamp\": \"Feb 15, 00:00\",\n        \"hkg\": 72,\n        \"ams\": 49,\n        \"iad\": 81,\n        \"jnb\": 643,\n        \"syd\": 102,\n        \"gru\": 123\n      },\n      {\n        \"timestamp\": \"Feb 15, 01:00\",\n        \"hkg\": 65,\n        \"gru\": 85,\n        \"iad\": 76,\n        \"ams\": 38,\n        \"syd\": 90,\n        \"jnb\": 666\n      },\n      {\n        \"timestamp\": \"Feb 15, 02:00\",\n        \"gru\": 90,\n        \"syd\": 91,\n        \"jnb\": 640,\n        \"iad\": 59,\n        \"ams\": 38,\n        \"hkg\": 58\n      },\n      {\n        \"timestamp\": \"Feb 15, 03:00\",\n        \"jnb\": 636,\n        \"syd\": 90,\n        \"ams\": 44,\n        \"iad\": 76,\n        \"gru\": 77,\n        \"hkg\": 61\n      },\n      {\n        \"timestamp\": \"Feb 15, 04:00\",\n        \"iad\": 68,\n        \"ams\": 50,\n        \"jnb\": 642,\n        \"syd\": 92,\n        \"gru\": 45,\n        \"hkg\": 56\n      },\n      {\n        \"timestamp\": \"Feb 15, 05:00\",\n        \"hkg\": 64,\n        \"gru\": 128,\n        \"syd\": 90,\n        \"jnb\": 640,\n        \"ams\": 49,\n        \"iad\": 69\n      },\n      {\n        \"timestamp\": \"Feb 15, 06:00\",\n        \"hkg\": 69,\n        \"ams\": 44,\n        \"iad\": 55,\n        \"syd\": 58,\n        \"jnb\": 644,\n        \"gru\": 70\n      },\n      {\n        \"timestamp\": \"Feb 15, 07:00\",\n        \"iad\": 66,\n        \"ams\": 44,\n        \"jnb\": 642,\n        \"syd\": 90,\n        \"gru\": 67,\n        \"hkg\": 78\n      },\n      {\n        \"timestamp\": \"Feb 15, 08:00\",\n        \"gru\": 38,\n        \"ams\": 45,\n        \"iad\": 55,\n        \"jnb\": 644,\n        \"syd\": 89,\n        \"hkg\": 65\n      },\n      {\n        \"timestamp\": \"Feb 15, 09:00\",\n        \"gru\": 68,\n        \"ams\": 52,\n        \"iad\": 58,\n        \"syd\": 88,\n        \"jnb\": 653,\n        \"hkg\": 65\n      },\n      {\n        \"timestamp\": \"Feb 15, 10:00\",\n        \"syd\": 89,\n        \"jnb\": 652,\n        \"ams\": 62,\n        \"iad\": 70,\n        \"gru\": 116,\n        \"hkg\": 70\n      },\n      {\n        \"timestamp\": \"Feb 15, 11:00\",\n        \"hkg\": 87,\n        \"jnb\": 694,\n        \"syd\": 88,\n        \"iad\": 63,\n        \"ams\": 58,\n        \"gru\": 41\n      },\n      {\n        \"timestamp\": \"Feb 15, 12:00\",\n        \"hkg\": 66,\n        \"ams\": 50,\n        \"iad\": 51,\n        \"jnb\": 650,\n        \"syd\": 86,\n        \"gru\": 99\n      },\n      {\n        \"timestamp\": \"Feb 15, 13:00\",\n        \"hkg\": 73,\n        \"gru\": 100,\n        \"jnb\": 767,\n        \"syd\": 85,\n        \"iad\": 58,\n        \"ams\": 49\n      },\n      {\n        \"timestamp\": \"Feb 15, 14:00\",\n        \"hkg\": 95,\n        \"gru\": 47,\n        \"jnb\": 712,\n        \"syd\": 79,\n        \"ams\": 50,\n        \"iad\": 78\n      },\n      {\n        \"timestamp\": \"Feb 15, 15:00\",\n        \"hkg\": 103,\n        \"syd\": 80,\n        \"jnb\": 689,\n        \"iad\": 82,\n        \"ams\": 66,\n        \"gru\": 92\n      },\n      {\n        \"timestamp\": \"Feb 15, 16:00\",\n        \"syd\": 148,\n        \"jnb\": 625,\n        \"ams\": 68,\n        \"iad\": 57,\n        \"gru\": 168,\n        \"hkg\": 74\n      },\n      {\n        \"timestamp\": \"Feb 17, 10:00\",\n        \"hkg\": 123,\n        \"iad\": 123,\n        \"ams\": 57,\n        \"syd\": 550,\n        \"jnb\": 695,\n        \"gru\": 39\n      },\n      {\n        \"timestamp\": \"Feb 17, 11:00\",\n        \"gru\": 68,\n        \"jnb\": 640,\n        \"syd\": 49,\n        \"ams\": 47,\n        \"iad\": 83,\n        \"hkg\": 57\n      },\n      {\n        \"timestamp\": \"Feb 17, 12:00\",\n        \"syd\": 93,\n        \"jnb\": 639,\n        \"ams\": 47,\n        \"iad\": 127,\n        \"gru\": 74,\n        \"hkg\": 46\n      },\n      {\n        \"timestamp\": \"Feb 17, 13:00\",\n        \"jnb\": 648,\n        \"syd\": 87,\n        \"ams\": 48,\n        \"iad\": 118,\n        \"gru\": 68,\n        \"hkg\": 53\n      },\n      {\n        \"timestamp\": \"Feb 17, 14:00\",\n        \"hkg\": 79,\n        \"syd\": 86,\n        \"jnb\": 655,\n        \"ams\": 50,\n        \"iad\": 117,\n        \"gru\": 74\n      },\n      {\n        \"timestamp\": \"Feb 17, 15:00\",\n        \"hkg\": 71,\n        \"gru\": 80,\n        \"iad\": 93,\n        \"ams\": 56,\n        \"syd\": 86,\n        \"jnb\": 663\n      },\n      {\n        \"timestamp\": \"Feb 17, 16:00\",\n        \"hkg\": 72,\n        \"jnb\": 750,\n        \"syd\": 80,\n        \"iad\": 84,\n        \"ams\": 48,\n        \"gru\": 72\n      },\n      {\n        \"timestamp\": \"Feb 17, 17:00\",\n        \"hkg\": 52,\n        \"gru\": 78,\n        \"syd\": 83,\n        \"jnb\": 657,\n        \"ams\": 54,\n        \"iad\": 66\n      },\n      {\n        \"timestamp\": \"Feb 17, 18:00\",\n        \"gru\": 50,\n        \"syd\": 96,\n        \"jnb\": 702,\n        \"iad\": 90,\n        \"ams\": 126,\n        \"hkg\": 54\n      },\n      {\n        \"timestamp\": \"Feb 17, 19:00\",\n        \"jnb\": 683,\n        \"syd\": 73,\n        \"ams\": 58,\n        \"iad\": 104,\n        \"gru\": 42,\n        \"hkg\": 58\n      },\n      {\n        \"timestamp\": \"Feb 17, 20:00\",\n        \"hkg\": 85,\n        \"iad\": 78,\n        \"ams\": 60,\n        \"syd\": 81,\n        \"jnb\": 744,\n        \"gru\": 55\n      },\n      {\n        \"timestamp\": \"Feb 17, 21:00\",\n        \"hkg\": 78,\n        \"gru\": 76,\n        \"syd\": 87,\n        \"jnb\": 674,\n        \"ams\": 67,\n        \"iad\": 64\n      },\n      {\n        \"timestamp\": \"Feb 17, 22:00\",\n        \"hkg\": 52,\n        \"gru\": 110,\n        \"iad\": 73,\n        \"ams\": 55,\n        \"syd\": 81,\n        \"jnb\": 708\n      },\n      {\n        \"timestamp\": \"Feb 17, 23:00\",\n        \"gru\": 46,\n        \"ams\": 46,\n        \"iad\": 76,\n        \"jnb\": 614,\n        \"syd\": 83,\n        \"hkg\": 46\n      },\n      {\n        \"timestamp\": \"Feb 18, 00:00\",\n        \"jnb\": 610,\n        \"syd\": 81,\n        \"ams\": 64,\n        \"iad\": 72,\n        \"gru\": 46,\n        \"hkg\": 52\n      },\n      {\n        \"timestamp\": \"Feb 18, 01:00\",\n        \"syd\": 92,\n        \"jnb\": 617,\n        \"ams\": 60,\n        \"iad\": 73,\n        \"gru\": 110,\n        \"hkg\": 62\n      },\n      {\n        \"timestamp\": \"Feb 18, 02:00\",\n        \"gru\": 108,\n        \"syd\": 90,\n        \"jnb\": 610,\n        \"iad\": 70,\n        \"ams\": 62,\n        \"hkg\": 61\n      },\n      {\n        \"timestamp\": \"Feb 18, 03:00\",\n        \"hkg\": 68,\n        \"gru\": 73,\n        \"jnb\": 606,\n        \"syd\": 88,\n        \"ams\": 39,\n        \"iad\": 78\n      },\n      {\n        \"timestamp\": \"Feb 18, 04:00\",\n        \"hkg\": 59,\n        \"ams\": 46,\n        \"iad\": 72,\n        \"jnb\": 622,\n        \"syd\": 86,\n        \"gru\": 71\n      },\n      {\n        \"timestamp\": \"Feb 18, 05:00\",\n        \"gru\": 70,\n        \"syd\": 87,\n        \"jnb\": 644,\n        \"iad\": 72,\n        \"ams\": 43,\n        \"hkg\": 71\n      },\n      {\n        \"timestamp\": \"Feb 18, 06:00\",\n        \"gru\": 66,\n        \"ams\": 64,\n        \"iad\": 56,\n        \"syd\": 94,\n        \"jnb\": 643,\n        \"hkg\": 68\n      },\n      {\n        \"timestamp\": \"Feb 18, 07:00\",\n        \"iad\": 44,\n        \"ams\": 48,\n        \"jnb\": 643,\n        \"syd\": 92,\n        \"gru\": 38,\n        \"hkg\": 68\n      },\n      {\n        \"timestamp\": \"Feb 18, 08:00\",\n        \"hkg\": 75,\n        \"jnb\": 658,\n        \"syd\": 120,\n        \"iad\": 65,\n        \"ams\": 47,\n        \"gru\": 66\n      },\n      {\n        \"timestamp\": \"Feb 18, 09:00\",\n        \"gru\": 66,\n        \"ams\": 59,\n        \"iad\": 71,\n        \"syd\": 89,\n        \"jnb\": 649,\n        \"hkg\": 66\n      },\n      {\n        \"timestamp\": \"Feb 18, 10:00\",\n        \"syd\": 97,\n        \"jnb\": 647,\n        \"iad\": 61,\n        \"ams\": 54,\n        \"gru\": 66,\n        \"hkg\": 70\n      },\n      {\n        \"timestamp\": \"Feb 18, 11:00\",\n        \"gru\": 73,\n        \"jnb\": 664,\n        \"syd\": 129,\n        \"ams\": 70,\n        \"iad\": 66,\n        \"hkg\": 77\n      },\n      {\n        \"timestamp\": \"Feb 18, 12:00\",\n        \"hkg\": 43,\n        \"gru\": 41,\n        \"ams\": 61,\n        \"iad\": 67,\n        \"jnb\": 639,\n        \"syd\": 119\n      },\n      {\n        \"timestamp\": \"Feb 18, 13:00\",\n        \"hkg\": 58,\n        \"jnb\": 635,\n        \"syd\": 91,\n        \"iad\": 75,\n        \"ams\": 59,\n        \"gru\": 73\n      },\n      {\n        \"timestamp\": \"Feb 18, 14:00\",\n        \"hkg\": 98,\n        \"ams\": 66,\n        \"iad\": 54,\n        \"jnb\": 664,\n        \"syd\": 82,\n        \"gru\": 74\n      },\n      {\n        \"timestamp\": \"Feb 18, 15:00\",\n        \"hkg\": 65,\n        \"syd\": 79,\n        \"jnb\": 687,\n        \"ams\": 79,\n        \"iad\": 77,\n        \"gru\": 130\n      },\n      {\n        \"timestamp\": \"Feb 18, 16:00\",\n        \"hkg\": 59,\n        \"gru\": 146,\n        \"ams\": 95,\n        \"iad\": 69,\n        \"syd\": 85,\n        \"jnb\": 727\n      },\n      {\n        \"timestamp\": \"Feb 18, 17:00\",\n        \"jnb\": 659,\n        \"syd\": 82,\n        \"ams\": 69,\n        \"iad\": 74,\n        \"gru\": 116,\n        \"hkg\": 74\n      },\n      {\n        \"timestamp\": \"Feb 18, 18:00\",\n        \"gru\": 77,\n        \"jnb\": 646,\n        \"syd\": 93,\n        \"iad\": 89,\n        \"ams\": 76,\n        \"hkg\": 46\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"syd\",\n      \"avgLatency\": 89,\n      \"p75Latency\": 60,\n      \"p90Latency\": 268,\n      \"p95Latency\": 287,\n      \"p99Latency\": 310\n    },\n    {\n      \"region\": \"gru\",\n      \"avgLatency\": 77,\n      \"p75Latency\": 60,\n      \"p90Latency\": 211,\n      \"p95Latency\": 219,\n      \"p99Latency\": 296\n    },\n    {\n      \"region\": \"iad\",\n      \"avgLatency\": 74,\n      \"p75Latency\": 120,\n      \"p90Latency\": 132,\n      \"p95Latency\": 139,\n      \"p99Latency\": 177\n    },\n    {\n      \"region\": \"ams\",\n      \"avgLatency\": 57,\n      \"p75Latency\": 60,\n      \"p90Latency\": 73,\n      \"p95Latency\": 88,\n      \"p99Latency\": 169\n    },\n    {\n      \"region\": \"hkg\",\n      \"avgLatency\": 67,\n      \"p75Latency\": 92,\n      \"p90Latency\": 111,\n      \"p95Latency\": 123,\n      \"p99Latency\": 158\n    },\n    {\n      \"region\": \"jnb\",\n      \"avgLatency\": 660,\n      \"p75Latency\": 705,\n      \"p90Latency\": 731,\n      \"p95Latency\": 748,\n      \"p99Latency\": 1110\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/monitoring-latency/fly.json",
    "content": "{\n  \"regions\": [\"ams\", \"iad\", \"hkg\", \"jnb\", \"syd\", \"gru\"],\n  \"data\": {\n    \"regions\": [\"ams\", \"gru\", \"hkg\", \"iad\", \"jnb\", \"syd\"],\n    \"data\": [\n      {\n        \"timestamp\": \"Feb 4, 00:00\",\n        \"ams\": 1489,\n        \"iad\": 1498,\n        \"syd\": 1465,\n        \"jnb\": 1434,\n        \"gru\": 1405,\n        \"hkg\": 1512\n      },\n      {\n        \"timestamp\": \"Feb 4, 01:00\",\n        \"ams\": 1482,\n        \"iad\": 1477,\n        \"syd\": 1565,\n        \"jnb\": 1448,\n        \"gru\": 1559,\n        \"hkg\": 1658\n      },\n      {\n        \"timestamp\": \"Feb 4, 02:00\",\n        \"hkg\": 1511,\n        \"ams\": 1488,\n        \"iad\": 1488,\n        \"jnb\": 1444,\n        \"syd\": 1749,\n        \"gru\": 1424\n      },\n      {\n        \"timestamp\": \"Feb 4, 03:00\",\n        \"hkg\": 1636,\n        \"jnb\": 1427,\n        \"syd\": 1377,\n        \"ams\": 1484,\n        \"iad\": 1458,\n        \"gru\": 1524\n      },\n      {\n        \"timestamp\": \"Feb 4, 04:00\",\n        \"hkg\": 1500,\n        \"gru\": 1411,\n        \"syd\": 1294,\n        \"jnb\": 1580,\n        \"ams\": 1474,\n        \"iad\": 1482\n      },\n      {\n        \"timestamp\": \"Feb 4, 05:00\",\n        \"gru\": 1435,\n        \"syd\": 1281,\n        \"jnb\": 1463,\n        \"ams\": 1490,\n        \"iad\": 1481,\n        \"hkg\": 1537\n      },\n      {\n        \"timestamp\": \"Feb 4, 06:00\",\n        \"jnb\": 1434,\n        \"syd\": 1435,\n        \"ams\": 1476,\n        \"iad\": 1466,\n        \"gru\": 1363,\n        \"hkg\": 1475\n      },\n      {\n        \"timestamp\": \"Feb 4, 07:00\",\n        \"hkg\": 1664,\n        \"jnb\": 1458,\n        \"syd\": 1430,\n        \"ams\": 1487,\n        \"iad\": 1496,\n        \"gru\": 1401\n      },\n      {\n        \"timestamp\": \"Feb 4, 08:00\",\n        \"hkg\": 1493,\n        \"gru\": 1389,\n        \"syd\": 1260,\n        \"jnb\": 1433,\n        \"ams\": 1476,\n        \"iad\": 1456\n      },\n      {\n        \"timestamp\": \"Feb 4, 09:00\",\n        \"hkg\": 1510,\n        \"iad\": 1508,\n        \"ams\": 1489,\n        \"jnb\": 1474,\n        \"syd\": 1274,\n        \"gru\": 1302\n      },\n      {\n        \"timestamp\": \"Feb 4, 10:00\",\n        \"hkg\": 1360,\n        \"gru\": 1564,\n        \"jnb\": 1586,\n        \"syd\": 1452,\n        \"iad\": 1478,\n        \"ams\": 1478\n      },\n      {\n        \"timestamp\": \"Feb 4, 11:00\",\n        \"gru\": 1391,\n        \"syd\": 1545,\n        \"jnb\": 1577,\n        \"ams\": 1487,\n        \"iad\": 1464,\n        \"hkg\": 1486\n      },\n      {\n        \"timestamp\": \"Feb 4, 12:00\",\n        \"gru\": 1400,\n        \"iad\": 1506,\n        \"ams\": 1494,\n        \"syd\": 1432,\n        \"jnb\": 1468,\n        \"hkg\": 1534\n      },\n      {\n        \"timestamp\": \"Feb 4, 13:00\",\n        \"syd\": 1630,\n        \"jnb\": 1474,\n        \"ams\": 1485,\n        \"iad\": 1478,\n        \"gru\": 1433,\n        \"hkg\": 1720\n      },\n      {\n        \"timestamp\": \"Feb 4, 14:00\",\n        \"hkg\": 1928,\n        \"gru\": 1822,\n        \"ams\": 1499,\n        \"iad\": 1477,\n        \"jnb\": 1456,\n        \"syd\": 2190\n      },\n      {\n        \"timestamp\": \"Feb 4, 15:00\",\n        \"hkg\": 1558,\n        \"iad\": 1524,\n        \"ams\": 1462,\n        \"syd\": 1816,\n        \"jnb\": 1489,\n        \"gru\": 1430\n      },\n      {\n        \"timestamp\": \"Feb 4, 16:00\",\n        \"ams\": 1497,\n        \"iad\": 1488,\n        \"syd\": 1376,\n        \"jnb\": 1450,\n        \"gru\": 1424,\n        \"hkg\": 1521\n      },\n      {\n        \"timestamp\": \"Feb 4, 17:00\",\n        \"jnb\": 1433,\n        \"syd\": 1617,\n        \"ams\": 1506,\n        \"iad\": 1477,\n        \"gru\": 1394,\n        \"hkg\": 1523\n      },\n      {\n        \"timestamp\": \"Feb 4, 18:00\",\n        \"gru\": 1448,\n        \"jnb\": 1601,\n        \"syd\": 1604,\n        \"ams\": 1507,\n        \"iad\": 1514,\n        \"hkg\": 1540\n      },\n      {\n        \"timestamp\": \"Feb 4, 19:00\",\n        \"syd\": 1278,\n        \"jnb\": 1446,\n        \"iad\": 1441,\n        \"ams\": 1494,\n        \"gru\": 1407,\n        \"hkg\": 1536\n      },\n      {\n        \"timestamp\": \"Feb 4, 20:00\",\n        \"ams\": 1484,\n        \"iad\": 1496,\n        \"syd\": 1614,\n        \"jnb\": 1454,\n        \"gru\": 1433,\n        \"hkg\": 1524\n      },\n      {\n        \"timestamp\": \"Feb 4, 21:00\",\n        \"hkg\": 1686,\n        \"syd\": 1889,\n        \"jnb\": 1454,\n        \"iad\": 1496,\n        \"ams\": 1486,\n        \"gru\": 1902\n      },\n      {\n        \"timestamp\": \"Feb 4, 22:00\",\n        \"hkg\": 1552,\n        \"gru\": 1611,\n        \"iad\": 1515,\n        \"ams\": 1485,\n        \"syd\": 1476,\n        \"jnb\": 1451\n      },\n      {\n        \"timestamp\": \"Feb 4, 23:00\",\n        \"jnb\": 1442,\n        \"syd\": 1600,\n        \"iad\": 1486,\n        \"ams\": 1497,\n        \"gru\": 1410,\n        \"hkg\": 1506\n      },\n      {\n        \"timestamp\": \"Feb 5, 00:00\",\n        \"gru\": 1528,\n        \"jnb\": 1444,\n        \"syd\": 1614,\n        \"ams\": 1484,\n        \"iad\": 1486,\n        \"hkg\": 1504\n      },\n      {\n        \"timestamp\": \"Feb 5, 01:00\",\n        \"gru\": 1532,\n        \"iad\": 1486,\n        \"ams\": 1493,\n        \"syd\": 1588,\n        \"jnb\": 1451,\n        \"hkg\": 1521\n      },\n      {\n        \"timestamp\": \"Feb 5, 02:00\",\n        \"syd\": 1588,\n        \"jnb\": 1282,\n        \"iad\": 1456,\n        \"ams\": 1478,\n        \"gru\": 1406,\n        \"hkg\": 1498\n      },\n      {\n        \"timestamp\": \"Feb 5, 03:00\",\n        \"gru\": 1419,\n        \"ams\": 1481,\n        \"iad\": 1477,\n        \"jnb\": 1431,\n        \"syd\": 1586,\n        \"hkg\": 1512\n      },\n      {\n        \"timestamp\": \"Feb 5, 04:00\",\n        \"gru\": 1411,\n        \"jnb\": 1447,\n        \"syd\": 1606,\n        \"iad\": 1477,\n        \"ams\": 1484,\n        \"hkg\": 1522\n      },\n      {\n        \"timestamp\": \"Feb 5, 05:00\",\n        \"ams\": 1474,\n        \"iad\": 1464,\n        \"jnb\": 1437,\n        \"syd\": 1647,\n        \"gru\": 1370,\n        \"hkg\": 1492\n      },\n      {\n        \"timestamp\": \"Feb 5, 06:00\",\n        \"hkg\": 1354,\n        \"jnb\": 1447,\n        \"syd\": 1122,\n        \"ams\": 1495,\n        \"iad\": 1484,\n        \"gru\": 1227\n      },\n      {\n        \"timestamp\": \"Feb 5, 07:00\",\n        \"hkg\": 1498,\n        \"gru\": 1415,\n        \"syd\": 1406,\n        \"jnb\": 1451,\n        \"iad\": 1477,\n        \"ams\": 1486\n      },\n      {\n        \"timestamp\": \"Feb 5, 08:00\",\n        \"jnb\": 1596,\n        \"syd\": 1760,\n        \"ams\": 1490,\n        \"iad\": 1470,\n        \"gru\": 1576,\n        \"hkg\": 1560\n      },\n      {\n        \"timestamp\": \"Feb 5, 09:00\",\n        \"ams\": 1490,\n        \"iad\": 1470,\n        \"syd\": 1239,\n        \"jnb\": 1404,\n        \"gru\": 1226,\n        \"hkg\": 2302\n      },\n      {\n        \"timestamp\": \"Feb 5, 10:00\",\n        \"hkg\": 1451,\n        \"syd\": 1264,\n        \"jnb\": 1410,\n        \"ams\": 1485,\n        \"iad\": 1460,\n        \"gru\": 1400\n      },\n      {\n        \"timestamp\": \"Feb 5, 11:00\",\n        \"hkg\": 1443,\n        \"gru\": 1248,\n        \"jnb\": 1443,\n        \"syd\": 1437,\n        \"iad\": 1476,\n        \"ams\": 1483\n      },\n      {\n        \"timestamp\": \"Feb 5, 12:00\",\n        \"hkg\": 1512,\n        \"gru\": 1412,\n        \"ams\": 1510,\n        \"iad\": 1520,\n        \"jnb\": 1458,\n        \"syd\": 1457\n      },\n      {\n        \"timestamp\": \"Feb 5, 13:00\",\n        \"hkg\": 1481,\n        \"iad\": 1483,\n        \"ams\": 1515,\n        \"jnb\": 1442,\n        \"syd\": 1443,\n        \"gru\": 1274\n      },\n      {\n        \"timestamp\": \"Feb 5, 14:00\",\n        \"hkg\": 1450,\n        \"gru\": 1415,\n        \"jnb\": 1413,\n        \"syd\": 1421,\n        \"iad\": 1465,\n        \"ams\": 1492\n      },\n      {\n        \"timestamp\": \"Feb 5, 15:00\",\n        \"gru\": 1414,\n        \"syd\": 1610,\n        \"jnb\": 1437,\n        \"ams\": 1494,\n        \"iad\": 1468,\n        \"hkg\": 1255\n      },\n      {\n        \"timestamp\": \"Feb 5, 16:00\",\n        \"syd\": 1440,\n        \"jnb\": 1465,\n        \"iad\": 1492,\n        \"ams\": 1507,\n        \"gru\": 1434,\n        \"hkg\": 1468\n      },\n      {\n        \"timestamp\": \"Feb 5, 17:00\",\n        \"hkg\": 1440,\n        \"syd\": 1616,\n        \"jnb\": 1444,\n        \"iad\": 1502,\n        \"ams\": 1512,\n        \"gru\": 1436\n      },\n      {\n        \"timestamp\": \"Feb 5, 18:00\",\n        \"hkg\": 1460,\n        \"gru\": 1420,\n        \"jnb\": 1448,\n        \"syd\": 1611,\n        \"iad\": 1495,\n        \"ams\": 1502\n      },\n      {\n        \"timestamp\": \"Feb 5, 19:00\",\n        \"gru\": 1660,\n        \"ams\": 1566,\n        \"iad\": 1550,\n        \"jnb\": 1513,\n        \"syd\": 1631,\n        \"hkg\": 1525\n      },\n      {\n        \"timestamp\": \"Feb 5, 20:00\",\n        \"ams\": 1499,\n        \"iad\": 1473,\n        \"syd\": 1432,\n        \"jnb\": 1247,\n        \"gru\": 1375,\n        \"hkg\": 1452\n      },\n      {\n        \"timestamp\": \"Feb 5, 21:00\",\n        \"ams\": 1495,\n        \"iad\": 1470,\n        \"jnb\": 1619,\n        \"syd\": 1582,\n        \"gru\": 1553,\n        \"hkg\": 1464\n      },\n      {\n        \"timestamp\": \"Feb 5, 22:00\",\n        \"gru\": 1563,\n        \"syd\": 1436,\n        \"jnb\": 1569,\n        \"ams\": 1494,\n        \"iad\": 1435,\n        \"hkg\": 1442\n      },\n      {\n        \"timestamp\": \"Feb 5, 23:00\",\n        \"iad\": 1471,\n        \"ams\": 1499,\n        \"syd\": 1424,\n        \"jnb\": 1446,\n        \"gru\": 1421,\n        \"hkg\": 1466\n      },\n      {\n        \"timestamp\": \"Feb 6, 00:00\",\n        \"hkg\": 1463,\n        \"iad\": 1482,\n        \"ams\": 1485,\n        \"jnb\": 1438,\n        \"syd\": 1242,\n        \"gru\": 1417\n      },\n      {\n        \"timestamp\": \"Feb 6, 01:00\",\n        \"hkg\": 1437,\n        \"jnb\": 1426,\n        \"syd\": 1460,\n        \"ams\": 1485,\n        \"iad\": 1474,\n        \"gru\": 1408\n      },\n      {\n        \"timestamp\": \"Feb 6, 02:00\",\n        \"gru\": 1392,\n        \"jnb\": 1428,\n        \"syd\": 1408,\n        \"ams\": 1483,\n        \"iad\": 1463,\n        \"hkg\": 1449\n      },\n      {\n        \"timestamp\": \"Feb 6, 03:00\",\n        \"iad\": 1473,\n        \"ams\": 1489,\n        \"jnb\": 1612,\n        \"syd\": 1529,\n        \"gru\": 1539,\n        \"hkg\": 1435\n      },\n      {\n        \"timestamp\": \"Feb 6, 04:00\",\n        \"hkg\": 1512,\n        \"iad\": 1540,\n        \"ams\": 1495,\n        \"syd\": 1604,\n        \"jnb\": 1662,\n        \"gru\": 1477\n      },\n      {\n        \"timestamp\": \"Feb 6, 05:00\",\n        \"hkg\": 1452,\n        \"iad\": 1498,\n        \"ams\": 1437,\n        \"jnb\": 1458,\n        \"syd\": 1439,\n        \"gru\": 1430\n      },\n      {\n        \"timestamp\": \"Feb 6, 06:00\",\n        \"hkg\": 1442,\n        \"gru\": 1400,\n        \"iad\": 1443,\n        \"ams\": 1486,\n        \"syd\": 1398,\n        \"jnb\": 1414\n      },\n      {\n        \"timestamp\": \"Feb 6, 07:00\",\n        \"hkg\": 1454,\n        \"ams\": 1486,\n        \"iad\": 1473,\n        \"syd\": 1309,\n        \"jnb\": 1444,\n        \"gru\": 1386\n      },\n      {\n        \"timestamp\": \"Feb 6, 08:00\",\n        \"hkg\": 1468,\n        \"gru\": 1451,\n        \"ams\": 1512,\n        \"iad\": 1466,\n        \"jnb\": 1464,\n        \"syd\": 1391\n      },\n      {\n        \"timestamp\": \"Feb 6, 09:00\",\n        \"hkg\": 1440,\n        \"gru\": 1410,\n        \"jnb\": 1400,\n        \"syd\": 1300,\n        \"ams\": 1479,\n        \"iad\": 1437\n      },\n      {\n        \"timestamp\": \"Feb 6, 10:00\",\n        \"hkg\": 1466,\n        \"syd\": 1619,\n        \"jnb\": 1450,\n        \"ams\": 1495,\n        \"iad\": 1486,\n        \"gru\": 1390\n      },\n      {\n        \"timestamp\": \"Feb 6, 11:00\",\n        \"hkg\": 1495,\n        \"syd\": 1451,\n        \"jnb\": 1491,\n        \"ams\": 1485,\n        \"iad\": 1518,\n        \"gru\": 1461\n      },\n      {\n        \"timestamp\": \"Feb 6, 12:00\",\n        \"hkg\": 1441,\n        \"gru\": 1402,\n        \"jnb\": 1454,\n        \"syd\": 1419,\n        \"ams\": 1514,\n        \"iad\": 1482\n      },\n      {\n        \"timestamp\": \"Feb 6, 13:00\",\n        \"hkg\": 1456,\n        \"gru\": 1425,\n        \"syd\": 1428,\n        \"jnb\": 1471,\n        \"ams\": 1502,\n        \"iad\": 1493\n      },\n      {\n        \"timestamp\": \"Feb 6, 14:00\",\n        \"gru\": 1448,\n        \"jnb\": 1471,\n        \"syd\": 1304,\n        \"ams\": 1494,\n        \"iad\": 1515,\n        \"hkg\": 1471\n      },\n      {\n        \"timestamp\": \"Feb 6, 15:00\",\n        \"iad\": 1466,\n        \"ams\": 1486,\n        \"jnb\": 1312,\n        \"syd\": 1436,\n        \"gru\": 1528,\n        \"hkg\": 1451\n      },\n      {\n        \"timestamp\": \"Feb 6, 16:00\",\n        \"gru\": 1408,\n        \"syd\": 1273,\n        \"jnb\": 1243,\n        \"iad\": 1486,\n        \"ams\": 1496,\n        \"hkg\": 1470\n      },\n      {\n        \"timestamp\": \"Feb 6, 17:00\",\n        \"gru\": 1430,\n        \"ams\": 1497,\n        \"iad\": 1523,\n        \"syd\": 1427,\n        \"jnb\": 1444,\n        \"hkg\": 1471\n      },\n      {\n        \"timestamp\": \"Feb 6, 18:00\",\n        \"hkg\": 1550,\n        \"gru\": 1424,\n        \"ams\": 1291,\n        \"iad\": 1294,\n        \"jnb\": 1275,\n        \"syd\": 1447\n      },\n      {\n        \"timestamp\": \"Feb 6, 19:00\",\n        \"hkg\": 1474,\n        \"jnb\": 1464,\n        \"syd\": 1288,\n        \"iad\": 1504,\n        \"ams\": 1510,\n        \"gru\": 1431\n      },\n      {\n        \"timestamp\": \"Feb 6, 20:00\",\n        \"gru\": 1442,\n        \"jnb\": 1475,\n        \"syd\": 1447,\n        \"iad\": 1509,\n        \"ams\": 1520,\n        \"hkg\": 1457\n      },\n      {\n        \"timestamp\": \"Feb 6, 21:00\",\n        \"gru\": 1424,\n        \"syd\": 1414,\n        \"jnb\": 1303,\n        \"iad\": 1470,\n        \"ams\": 1499,\n        \"hkg\": 1462\n      },\n      {\n        \"timestamp\": \"Feb 6, 22:00\",\n        \"jnb\": 1438,\n        \"syd\": 1610,\n        \"iad\": 1506,\n        \"ams\": 1525,\n        \"gru\": 1597,\n        \"hkg\": 1496\n      },\n      {\n        \"timestamp\": \"Feb 6, 23:00\",\n        \"hkg\": 1279,\n        \"ams\": 1505,\n        \"iad\": 1458,\n        \"jnb\": 1438,\n        \"syd\": 1440,\n        \"gru\": 1428\n      },\n      {\n        \"timestamp\": \"Feb 7, 00:00\",\n        \"hkg\": 1472,\n        \"gru\": 1431,\n        \"ams\": 1512,\n        \"iad\": 1509,\n        \"syd\": 1452,\n        \"jnb\": 1476\n      },\n      {\n        \"timestamp\": \"Feb 7, 01:00\",\n        \"hkg\": 1455,\n        \"jnb\": 1416,\n        \"syd\": 1409,\n        \"iad\": 1477,\n        \"ams\": 1490,\n        \"gru\": 1408\n      },\n      {\n        \"timestamp\": \"Feb 7, 02:00\",\n        \"hkg\": 1281,\n        \"gru\": 1239,\n        \"jnb\": 1250,\n        \"syd\": 1456,\n        \"ams\": 1259,\n        \"iad\": 1272\n      },\n      {\n        \"timestamp\": \"Feb 7, 03:00\",\n        \"gru\": 1395,\n        \"syd\": 1411,\n        \"jnb\": 1453,\n        \"iad\": 1498,\n        \"ams\": 1501,\n        \"hkg\": 1452\n      },\n      {\n        \"timestamp\": \"Feb 7, 04:00\",\n        \"iad\": 1496,\n        \"ams\": 1495,\n        \"syd\": 1523,\n        \"jnb\": 1448,\n        \"gru\": 1386,\n        \"hkg\": 1461\n      },\n      {\n        \"timestamp\": \"Feb 7, 05:00\",\n        \"hkg\": 1479,\n        \"jnb\": 1461,\n        \"syd\": 1289,\n        \"iad\": 1508,\n        \"ams\": 1507,\n        \"gru\": 1431\n      },\n      {\n        \"timestamp\": \"Feb 7, 06:00\",\n        \"hkg\": 1439,\n        \"ams\": 1497,\n        \"iad\": 1498,\n        \"jnb\": 1471,\n        \"syd\": 1275,\n        \"gru\": 1415\n      },\n      {\n        \"timestamp\": \"Feb 7, 07:00\",\n        \"hkg\": 1462,\n        \"gru\": 1425,\n        \"iad\": 1488,\n        \"ams\": 1512,\n        \"syd\": 1375,\n        \"jnb\": 1464\n      },\n      {\n        \"timestamp\": \"Feb 7, 08:00\",\n        \"gru\": 1405,\n        \"syd\": 1845,\n        \"jnb\": 1455,\n        \"iad\": 1486,\n        \"ams\": 1494,\n        \"hkg\": 1465\n      },\n      {\n        \"timestamp\": \"Feb 7, 09:00\",\n        \"gru\": 1473,\n        \"iad\": 1541,\n        \"ams\": 1518,\n        \"jnb\": 1492,\n        \"syd\": 1482,\n        \"hkg\": 1504\n      },\n      {\n        \"timestamp\": \"Feb 7, 10:00\",\n        \"jnb\": 1312,\n        \"syd\": 1491,\n        \"ams\": 1263,\n        \"iad\": 1299,\n        \"gru\": 1283,\n        \"hkg\": 1308\n      },\n      {\n        \"timestamp\": \"Feb 7, 11:00\",\n        \"gru\": 1447,\n        \"syd\": 1435,\n        \"jnb\": 1472,\n        \"iad\": 1510,\n        \"ams\": 1515,\n        \"hkg\": 1465\n      },\n      {\n        \"timestamp\": \"Feb 7, 12:00\",\n        \"hkg\": 1451,\n        \"gru\": 1607,\n        \"iad\": 1511,\n        \"ams\": 1514,\n        \"syd\": 1630,\n        \"jnb\": 1454\n      },\n      {\n        \"timestamp\": \"Feb 7, 13:00\",\n        \"hkg\": 1462,\n        \"syd\": 1398,\n        \"jnb\": 1427,\n        \"ams\": 1508,\n        \"iad\": 1487,\n        \"gru\": 1421\n      },\n      {\n        \"timestamp\": \"Feb 7, 14:00\",\n        \"hkg\": 1448,\n        \"iad\": 1504,\n        \"ams\": 1512,\n        \"syd\": 1437,\n        \"jnb\": 1468,\n        \"gru\": 1272\n      },\n      {\n        \"timestamp\": \"Feb 7, 15:00\",\n        \"gru\": 1423,\n        \"ams\": 1506,\n        \"iad\": 1498,\n        \"syd\": 1594,\n        \"jnb\": 1619,\n        \"hkg\": 1446\n      },\n      {\n        \"timestamp\": \"Feb 7, 16:00\",\n        \"syd\": 1804,\n        \"jnb\": 1285,\n        \"ams\": 1505,\n        \"iad\": 1491,\n        \"gru\": 1427,\n        \"hkg\": 1486\n      },\n      {\n        \"timestamp\": \"Feb 7, 17:00\",\n        \"iad\": 1509,\n        \"ams\": 1510,\n        \"jnb\": 1311,\n        \"syd\": 1446,\n        \"gru\": 1463,\n        \"hkg\": 1502\n      },\n      {\n        \"timestamp\": \"Feb 7, 18:00\",\n        \"gru\": 1438,\n        \"ams\": 1519,\n        \"iad\": 1497,\n        \"jnb\": 1470,\n        \"syd\": 1604,\n        \"hkg\": 1482\n      },\n      {\n        \"timestamp\": \"Feb 7, 19:00\",\n        \"syd\": 1412,\n        \"jnb\": 1491,\n        \"iad\": 1520,\n        \"ams\": 1530,\n        \"gru\": 1457,\n        \"hkg\": 1495\n      },\n      {\n        \"timestamp\": \"Feb 7, 20:00\",\n        \"gru\": 1430,\n        \"iad\": 1497,\n        \"ams\": 1505,\n        \"syd\": 1435,\n        \"jnb\": 1454,\n        \"hkg\": 1452\n      },\n      {\n        \"timestamp\": \"Feb 7, 21:00\",\n        \"hkg\": 1464,\n        \"gru\": 1436,\n        \"syd\": 1412,\n        \"jnb\": 1474,\n        \"ams\": 1523,\n        \"iad\": 1503\n      },\n      {\n        \"timestamp\": \"Feb 7, 22:00\",\n        \"hkg\": 1486,\n        \"gru\": 1458,\n        \"iad\": 1513,\n        \"ams\": 1530,\n        \"syd\": 1839,\n        \"jnb\": 1634\n      },\n      {\n        \"timestamp\": \"Feb 7, 23:00\",\n        \"hkg\": 1492,\n        \"ams\": 1522,\n        \"iad\": 1534,\n        \"jnb\": 1477,\n        \"syd\": 1446,\n        \"gru\": 1457\n      },\n      {\n        \"timestamp\": \"Feb 8, 00:00\",\n        \"hkg\": 1520,\n        \"syd\": 1682,\n        \"jnb\": 1478,\n        \"ams\": 1510,\n        \"iad\": 1540,\n        \"gru\": 1616\n      },\n      {\n        \"timestamp\": \"Feb 8, 01:00\",\n        \"hkg\": 1423,\n        \"ams\": 1514,\n        \"iad\": 1496,\n        \"jnb\": 1642,\n        \"syd\": 1613,\n        \"gru\": 1422\n      },\n      {\n        \"timestamp\": \"Feb 8, 02:00\",\n        \"iad\": 1520,\n        \"ams\": 1521,\n        \"jnb\": 1659,\n        \"syd\": 1475,\n        \"gru\": 1428,\n        \"hkg\": 1484\n      },\n      {\n        \"timestamp\": \"Feb 8, 03:00\",\n        \"gru\": 1609,\n        \"ams\": 1520,\n        \"iad\": 1451,\n        \"syd\": 1639,\n        \"jnb\": 1607,\n        \"hkg\": 1478\n      },\n      {\n        \"timestamp\": \"Feb 8, 04:00\",\n        \"ams\": 1508,\n        \"iad\": 1387,\n        \"syd\": 1437,\n        \"jnb\": 1464,\n        \"gru\": 1412,\n        \"hkg\": 1422\n      },\n      {\n        \"timestamp\": \"Feb 8, 05:00\",\n        \"gru\": 1556,\n        \"syd\": 1611,\n        \"jnb\": 1642,\n        \"iad\": 1523,\n        \"ams\": 1514,\n        \"hkg\": 1490\n      },\n      {\n        \"timestamp\": \"Feb 8, 06:00\",\n        \"gru\": 1334,\n        \"ams\": 1506,\n        \"iad\": 1498,\n        \"syd\": 1706,\n        \"jnb\": 1621,\n        \"hkg\": 1472\n      },\n      {\n        \"timestamp\": \"Feb 8, 07:00\",\n        \"hkg\": 1463,\n        \"gru\": 1425,\n        \"iad\": 1494,\n        \"ams\": 1502,\n        \"jnb\": 1441,\n        \"syd\": 1281\n      },\n      {\n        \"timestamp\": \"Feb 8, 08:00\",\n        \"hkg\": 1424,\n        \"jnb\": 1511,\n        \"syd\": 1673,\n        \"iad\": 1524,\n        \"ams\": 1481,\n        \"gru\": 1472\n      },\n      {\n        \"timestamp\": \"Feb 8, 09:00\",\n        \"gru\": 1284,\n        \"jnb\": 1289,\n        \"syd\": 1490,\n        \"ams\": 1278,\n        \"iad\": 1297,\n        \"hkg\": 1277\n      },\n      {\n        \"timestamp\": \"Feb 8, 10:00\",\n        \"syd\": 1332,\n        \"jnb\": 1291,\n        \"ams\": 1275,\n        \"iad\": 1299,\n        \"gru\": 1268,\n        \"hkg\": 1304\n      },\n      {\n        \"timestamp\": \"Feb 8, 11:00\",\n        \"hkg\": 1473,\n        \"syd\": 1268,\n        \"jnb\": 1469,\n        \"ams\": 1512,\n        \"iad\": 1501,\n        \"gru\": 1438\n      },\n      {\n        \"timestamp\": \"Feb 8, 12:00\",\n        \"hkg\": 1468,\n        \"gru\": 1565,\n        \"jnb\": 1426,\n        \"syd\": 1765,\n        \"ams\": 1507,\n        \"iad\": 1497\n      },\n      {\n        \"timestamp\": \"Feb 8, 13:00\",\n        \"hkg\": 1474,\n        \"iad\": 1505,\n        \"ams\": 1514,\n        \"syd\": 1554,\n        \"jnb\": 1441,\n        \"gru\": 1424\n      },\n      {\n        \"timestamp\": \"Feb 8, 14:00\",\n        \"hkg\": 1451,\n        \"gru\": 1447,\n        \"iad\": 1486,\n        \"ams\": 1532,\n        \"jnb\": 1478,\n        \"syd\": 1644\n      },\n      {\n        \"timestamp\": \"Feb 8, 15:00\",\n        \"hkg\": 1471,\n        \"gru\": 1440,\n        \"jnb\": 1426,\n        \"syd\": 1436,\n        \"iad\": 1494,\n        \"ams\": 1512\n      },\n      {\n        \"timestamp\": \"Feb 8, 16:00\",\n        \"gru\": 1272,\n        \"syd\": 1574,\n        \"jnb\": 1451,\n        \"iad\": 1286,\n        \"ams\": 1267,\n        \"hkg\": 1307\n      },\n      {\n        \"timestamp\": \"Feb 8, 17:00\",\n        \"gru\": 1300,\n        \"syd\": 1419,\n        \"jnb\": 1481,\n        \"iad\": 1521,\n        \"ams\": 1512,\n        \"hkg\": 1503\n      },\n      {\n        \"timestamp\": \"Feb 8, 18:00\",\n        \"hkg\": 1458,\n        \"gru\": 1437,\n        \"syd\": 1585,\n        \"jnb\": 1270,\n        \"ams\": 1518,\n        \"iad\": 1488\n      },\n      {\n        \"timestamp\": \"Feb 8, 19:00\",\n        \"hkg\": 1531,\n        \"syd\": 1866,\n        \"jnb\": 1515,\n        \"ams\": 1552,\n        \"iad\": 1564,\n        \"gru\": 1490\n      },\n      {\n        \"timestamp\": \"Feb 8, 20:00\",\n        \"jnb\": 1250,\n        \"syd\": 1315,\n        \"ams\": 1316,\n        \"iad\": 1331,\n        \"gru\": 1276,\n        \"hkg\": 1326\n      },\n      {\n        \"timestamp\": \"Feb 8, 21:00\",\n        \"gru\": 1303,\n        \"syd\": 1861,\n        \"jnb\": 1486,\n        \"iad\": 1534,\n        \"ams\": 1546,\n        \"hkg\": 1512\n      },\n      {\n        \"timestamp\": \"Feb 8, 22:00\",\n        \"iad\": 1505,\n        \"ams\": 1513,\n        \"jnb\": 1468,\n        \"syd\": 1789,\n        \"gru\": 1594,\n        \"hkg\": 1471\n      },\n      {\n        \"timestamp\": \"Feb 8, 23:00\",\n        \"jnb\": 1481,\n        \"syd\": 1444,\n        \"iad\": 1491,\n        \"ams\": 1530,\n        \"gru\": 1407,\n        \"hkg\": 1476\n      },\n      {\n        \"timestamp\": \"Feb 9, 00:00\",\n        \"gru\": 1458,\n        \"syd\": 2006,\n        \"jnb\": 1479,\n        \"iad\": 1517,\n        \"ams\": 1524,\n        \"hkg\": 1471\n      },\n      {\n        \"timestamp\": \"Feb 9, 01:00\",\n        \"gru\": 1449,\n        \"syd\": 1638,\n        \"jnb\": 1488,\n        \"ams\": 1522,\n        \"iad\": 1515,\n        \"hkg\": 1487\n      },\n      {\n        \"timestamp\": \"Feb 9, 02:00\",\n        \"jnb\": 1520,\n        \"syd\": 1330,\n        \"ams\": 1546,\n        \"iad\": 1552,\n        \"gru\": 1481,\n        \"hkg\": 1530\n      },\n      {\n        \"timestamp\": \"Feb 9, 03:00\",\n        \"hkg\": 1494,\n        \"gru\": 1465,\n        \"syd\": 1660,\n        \"jnb\": 1490,\n        \"ams\": 1540,\n        \"iad\": 1536\n      },\n      {\n        \"timestamp\": \"Feb 9, 04:00\",\n        \"hkg\": 1481,\n        \"jnb\": 1482,\n        \"syd\": 1451,\n        \"ams\": 1521,\n        \"iad\": 1514,\n        \"gru\": 1444\n      },\n      {\n        \"timestamp\": \"Feb 9, 05:00\",\n        \"syd\": 1423,\n        \"jnb\": 1471,\n        \"iad\": 1504,\n        \"ams\": 1515,\n        \"gru\": 1433,\n        \"hkg\": 1457\n      },\n      {\n        \"timestamp\": \"Feb 9, 06:00\",\n        \"gru\": 1432,\n        \"jnb\": 1450,\n        \"syd\": 1398,\n        \"iad\": 1533,\n        \"ams\": 1514,\n        \"hkg\": 1418\n      },\n      {\n        \"timestamp\": \"Feb 9, 07:00\",\n        \"gru\": 1412,\n        \"iad\": 1441,\n        \"ams\": 1518,\n        \"jnb\": 1477,\n        \"syd\": 1886,\n        \"hkg\": 1479\n      },\n      {\n        \"timestamp\": \"Feb 9, 08:00\",\n        \"syd\": 1698,\n        \"jnb\": 1678,\n        \"iad\": 1522,\n        \"ams\": 1523,\n        \"gru\": 1828,\n        \"hkg\": 1482\n      },\n      {\n        \"timestamp\": \"Feb 9, 09:00\",\n        \"syd\": 1509,\n        \"jnb\": 1531,\n        \"iad\": 1561,\n        \"ams\": 1539,\n        \"gru\": 1474,\n        \"hkg\": 1554\n      },\n      {\n        \"timestamp\": \"Feb 9, 10:00\",\n        \"hkg\": 1494,\n        \"jnb\": 1504,\n        \"syd\": 1500,\n        \"iad\": 1510,\n        \"ams\": 1528,\n        \"gru\": 1287\n      },\n      {\n        \"timestamp\": \"Feb 9, 11:00\",\n        \"hkg\": 1485,\n        \"gru\": 1300,\n        \"jnb\": 1424,\n        \"syd\": 1443,\n        \"iad\": 1534,\n        \"ams\": 1544\n      },\n      {\n        \"timestamp\": \"Feb 9, 12:00\",\n        \"hkg\": 1520,\n        \"gru\": 1482,\n        \"ams\": 1572,\n        \"iad\": 1563,\n        \"syd\": 1678,\n        \"jnb\": 1515\n      },\n      {\n        \"timestamp\": \"Feb 9, 13:00\",\n        \"hkg\": 1521,\n        \"iad\": 1524,\n        \"ams\": 1554,\n        \"jnb\": 1511,\n        \"syd\": 1477,\n        \"gru\": 1275\n      },\n      {\n        \"timestamp\": \"Feb 9, 14:00\",\n        \"iad\": 1542,\n        \"ams\": 1551,\n        \"syd\": 1652,\n        \"jnb\": 1498,\n        \"gru\": 1429,\n        \"hkg\": 1508\n      },\n      {\n        \"timestamp\": \"Feb 9, 15:00\",\n        \"syd\": 1468,\n        \"jnb\": 1466,\n        \"iad\": 1504,\n        \"ams\": 1518,\n        \"gru\": 1427,\n        \"hkg\": 1476\n      },\n      {\n        \"timestamp\": \"Feb 9, 16:00\",\n        \"gru\": 1278,\n        \"jnb\": 1265,\n        \"syd\": 1619,\n        \"iad\": 1512,\n        \"ams\": 1531,\n        \"hkg\": 1316\n      },\n      {\n        \"timestamp\": \"Feb 9, 17:00\",\n        \"ams\": 1533,\n        \"iad\": 1521,\n        \"syd\": 1450,\n        \"jnb\": 1479,\n        \"gru\": 1462,\n        \"hkg\": 1488\n      },\n      {\n        \"timestamp\": \"Feb 9, 18:00\",\n        \"gru\": 1626,\n        \"syd\": 1628,\n        \"jnb\": 1622,\n        \"ams\": 1536,\n        \"iad\": 1513,\n        \"hkg\": 1499\n      },\n      {\n        \"timestamp\": \"Feb 9, 19:00\",\n        \"hkg\": 1524,\n        \"gru\": 1481,\n        \"jnb\": 1457,\n        \"syd\": 1644,\n        \"iad\": 1548,\n        \"ams\": 1550\n      },\n      {\n        \"timestamp\": \"Feb 9, 20:00\",\n        \"hkg\": 1311,\n        \"jnb\": 1484,\n        \"syd\": 1458,\n        \"ams\": 1525,\n        \"iad\": 1525,\n        \"gru\": 1385\n      },\n      {\n        \"timestamp\": \"Feb 9, 21:00\",\n        \"gru\": 1469,\n        \"iad\": 1520,\n        \"ams\": 1543,\n        \"syd\": 1472,\n        \"jnb\": 1508,\n        \"hkg\": 1519\n      },\n      {\n        \"timestamp\": \"Feb 9, 22:00\",\n        \"syd\": 1318,\n        \"jnb\": 1347,\n        \"iad\": 1523,\n        \"ams\": 1520,\n        \"gru\": 1464,\n        \"hkg\": 1494\n      },\n      {\n        \"timestamp\": \"Feb 9, 23:00\",\n        \"hkg\": 1460,\n        \"jnb\": 1463,\n        \"syd\": 1442,\n        \"ams\": 1519,\n        \"iad\": 1500,\n        \"gru\": 1446\n      },\n      {\n        \"timestamp\": \"Feb 10, 00:00\",\n        \"hkg\": 1489,\n        \"iad\": 1514,\n        \"ams\": 1521,\n        \"jnb\": 1650,\n        \"syd\": 1460,\n        \"gru\": 1444\n      },\n      {\n        \"timestamp\": \"Feb 10, 01:00\",\n        \"hkg\": 1486,\n        \"syd\": 1439,\n        \"jnb\": 1320,\n        \"iad\": 1505,\n        \"ams\": 1515,\n        \"gru\": 1270\n      },\n      {\n        \"timestamp\": \"Feb 10, 02:00\",\n        \"hkg\": 1526,\n        \"gru\": 1289,\n        \"syd\": 1280,\n        \"jnb\": 1521,\n        \"iad\": 1547,\n        \"ams\": 1526\n      },\n      {\n        \"timestamp\": \"Feb 10, 03:00\",\n        \"hkg\": 1418,\n        \"jnb\": 1285,\n        \"syd\": 1450,\n        \"ams\": 1257,\n        \"iad\": 1282,\n        \"gru\": 1300\n      },\n      {\n        \"timestamp\": \"Feb 10, 04:00\",\n        \"jnb\": 1480,\n        \"syd\": 1654,\n        \"iad\": 1528,\n        \"ams\": 1533,\n        \"gru\": 1462,\n        \"hkg\": 1507\n      },\n      {\n        \"timestamp\": \"Feb 10, 05:00\",\n        \"gru\": 1277,\n        \"ams\": 1516,\n        \"iad\": 1507,\n        \"jnb\": 1471,\n        \"syd\": 1488,\n        \"hkg\": 1488\n      },\n      {\n        \"timestamp\": \"Feb 10, 06:00\",\n        \"hkg\": 1488,\n        \"gru\": 1428,\n        \"syd\": 1604,\n        \"jnb\": 1612,\n        \"iad\": 1514,\n        \"ams\": 1518\n      },\n      {\n        \"timestamp\": \"Feb 10, 07:00\",\n        \"hkg\": 1485,\n        \"jnb\": 1483,\n        \"syd\": 1427,\n        \"ams\": 1516,\n        \"iad\": 1497,\n        \"gru\": 1439\n      },\n      {\n        \"timestamp\": \"Feb 10, 08:00\",\n        \"hkg\": 1526,\n        \"iad\": 1544,\n        \"ams\": 1540,\n        \"jnb\": 1514,\n        \"syd\": 1675,\n        \"gru\": 1487\n      },\n      {\n        \"timestamp\": \"Feb 10, 09:00\",\n        \"hkg\": 1454,\n        \"syd\": 1466,\n        \"jnb\": 1293,\n        \"ams\": 1266,\n        \"iad\": 1236,\n        \"gru\": 1270\n      },\n      {\n        \"timestamp\": \"Feb 10, 10:00\",\n        \"hkg\": 1455,\n        \"gru\": 1412,\n        \"ams\": 1268,\n        \"iad\": 1247,\n        \"syd\": 1298,\n        \"jnb\": 1290\n      },\n      {\n        \"timestamp\": \"Feb 10, 11:00\",\n        \"hkg\": 1506,\n        \"jnb\": 1318,\n        \"syd\": 1464,\n        \"iad\": 1530,\n        \"ams\": 1511,\n        \"gru\": 1420\n      },\n      {\n        \"timestamp\": \"Feb 10, 12:00\",\n        \"hkg\": 1346,\n        \"gru\": 1308,\n        \"jnb\": 1300,\n        \"syd\": 1503,\n        \"ams\": 1344,\n        \"iad\": 1318\n      },\n      {\n        \"timestamp\": \"Feb 10, 13:00\",\n        \"gru\": 1457,\n        \"iad\": 1520,\n        \"ams\": 1536,\n        \"jnb\": 1483,\n        \"syd\": 1450,\n        \"hkg\": 1500\n      },\n      {\n        \"timestamp\": \"Feb 10, 14:00\",\n        \"jnb\": 1478,\n        \"syd\": 1456,\n        \"iad\": 1532,\n        \"ams\": 1527,\n        \"gru\": 1452,\n        \"hkg\": 1463\n      },\n      {\n        \"timestamp\": \"Feb 10, 15:00\",\n        \"hkg\": 1496,\n        \"gru\": 1447,\n        \"jnb\": 1470,\n        \"syd\": 1636,\n        \"ams\": 1519,\n        \"iad\": 1511\n      },\n      {\n        \"timestamp\": \"Feb 10, 16:00\",\n        \"hkg\": 1328,\n        \"gru\": 1289,\n        \"iad\": 1293,\n        \"ams\": 1288,\n        \"jnb\": 1297,\n        \"syd\": 1466\n      },\n      {\n        \"timestamp\": \"Feb 10, 17:00\",\n        \"hkg\": 1515,\n        \"gru\": 1462,\n        \"syd\": 1636,\n        \"jnb\": 1394,\n        \"iad\": 1518,\n        \"ams\": 1529\n      },\n      {\n        \"timestamp\": \"Feb 10, 18:00\",\n        \"gru\": 1462,\n        \"iad\": 1292,\n        \"ams\": 1303,\n        \"syd\": 1499,\n        \"jnb\": 1320,\n        \"hkg\": 1346\n      },\n      {\n        \"timestamp\": \"Feb 10, 19:00\",\n        \"ams\": 1570,\n        \"iad\": 1595,\n        \"jnb\": 1524,\n        \"syd\": 1492,\n        \"gru\": 1506,\n        \"hkg\": 1562\n      },\n      {\n        \"timestamp\": \"Feb 10, 20:00\",\n        \"gru\": 1262,\n        \"jnb\": 1455,\n        \"syd\": 1298,\n        \"iad\": 1518,\n        \"ams\": 1514,\n        \"hkg\": 1494\n      },\n      {\n        \"timestamp\": \"Feb 10, 21:00\",\n        \"ams\": 1529,\n        \"iad\": 1521,\n        \"jnb\": 1454,\n        \"syd\": 1456,\n        \"gru\": 1299,\n        \"hkg\": 1500\n      },\n      {\n        \"timestamp\": \"Feb 10, 22:00\",\n        \"hkg\": 1669,\n        \"jnb\": 1483,\n        \"syd\": 1614,\n        \"ams\": 1522,\n        \"iad\": 1517,\n        \"gru\": 1445\n      },\n      {\n        \"timestamp\": \"Feb 10, 23:00\",\n        \"hkg\": 1498,\n        \"gru\": 1574,\n        \"syd\": 1628,\n        \"jnb\": 1493,\n        \"iad\": 1533,\n        \"ams\": 1528\n      },\n      {\n        \"timestamp\": \"Feb 11, 00:00\",\n        \"hkg\": 1499,\n        \"gru\": 1349,\n        \"ams\": 1519,\n        \"iad\": 1500,\n        \"syd\": 1433,\n        \"jnb\": 1478\n      },\n      {\n        \"timestamp\": \"Feb 11, 01:00\",\n        \"hkg\": 1516,\n        \"jnb\": 1497,\n        \"syd\": 1456,\n        \"ams\": 1523,\n        \"iad\": 1518,\n        \"gru\": 1419\n      },\n      {\n        \"timestamp\": \"Feb 11, 02:00\",\n        \"hkg\": 1315,\n        \"gru\": 1488,\n        \"syd\": 1919,\n        \"jnb\": 1317,\n        \"ams\": 1284,\n        \"iad\": 1333\n      },\n      {\n        \"timestamp\": \"Feb 11, 03:00\",\n        \"gru\": 1480,\n        \"iad\": 1500,\n        \"ams\": 1555,\n        \"syd\": 1928,\n        \"jnb\": 1768,\n        \"hkg\": 1733\n      },\n      {\n        \"timestamp\": \"Feb 11, 04:00\",\n        \"iad\": 1525,\n        \"ams\": 1534,\n        \"jnb\": 1496,\n        \"syd\": 1514,\n        \"gru\": 1617,\n        \"hkg\": 1518\n      },\n      {\n        \"timestamp\": \"Feb 11, 05:00\",\n        \"gru\": 1452,\n        \"iad\": 1505,\n        \"ams\": 1526,\n        \"syd\": 1448,\n        \"jnb\": 1483,\n        \"hkg\": 1500\n      },\n      {\n        \"timestamp\": \"Feb 11, 06:00\",\n        \"iad\": 1499,\n        \"ams\": 1536,\n        \"jnb\": 1458,\n        \"syd\": 1479,\n        \"gru\": 1297,\n        \"hkg\": 1410\n      },\n      {\n        \"timestamp\": \"Feb 11, 07:00\",\n        \"hkg\": 1498,\n        \"jnb\": 1624,\n        \"syd\": 1447,\n        \"ams\": 1524,\n        \"iad\": 1514,\n        \"gru\": 1581\n      },\n      {\n        \"timestamp\": \"Feb 11, 08:00\",\n        \"hkg\": 1497,\n        \"gru\": 1451,\n        \"syd\": 1306,\n        \"jnb\": 1372,\n        \"ams\": 1525,\n        \"iad\": 1520\n      },\n      {\n        \"timestamp\": \"Feb 11, 09:00\",\n        \"hkg\": 1482,\n        \"syd\": 1654,\n        \"jnb\": 1474,\n        \"iad\": 1515,\n        \"ams\": 1520,\n        \"gru\": 1443\n      },\n      {\n        \"timestamp\": \"Feb 11, 10:00\",\n        \"gru\": 1264,\n        \"syd\": 1564,\n        \"jnb\": 1478,\n        \"iad\": 1511,\n        \"ams\": 1532,\n        \"hkg\": 1503\n      },\n      {\n        \"timestamp\": \"Feb 11, 11:00\",\n        \"ams\": 1574,\n        \"iad\": 1550,\n        \"syd\": 1654,\n        \"jnb\": 1501,\n        \"gru\": 1485,\n        \"hkg\": 1676\n      },\n      {\n        \"timestamp\": \"Feb 11, 12:00\",\n        \"hkg\": 1422,\n        \"ams\": 1533,\n        \"iad\": 1553,\n        \"jnb\": 1374,\n        \"syd\": 1600,\n        \"gru\": 1379\n      },\n      {\n        \"timestamp\": \"Feb 11, 13:00\",\n        \"hkg\": 1530,\n        \"gru\": 1608,\n        \"syd\": 1497,\n        \"jnb\": 1656,\n        \"ams\": 1564,\n        \"iad\": 1544\n      },\n      {\n        \"timestamp\": \"Feb 11, 14:00\",\n        \"hkg\": 1510,\n        \"ams\": 1549,\n        \"iad\": 1524,\n        \"jnb\": 1484,\n        \"syd\": 1458,\n        \"gru\": 1454\n      },\n      {\n        \"timestamp\": \"Feb 11, 15:00\",\n        \"hkg\": 1506,\n        \"gru\": 1460,\n        \"jnb\": 1459,\n        \"syd\": 1461,\n        \"iad\": 1529,\n        \"ams\": 1532\n      },\n      {\n        \"timestamp\": \"Feb 11, 16:00\",\n        \"gru\": 1611,\n        \"syd\": 2035,\n        \"jnb\": 1471,\n        \"iad\": 1535,\n        \"ams\": 1540,\n        \"hkg\": 1510\n      },\n      {\n        \"timestamp\": \"Feb 11, 17:00\",\n        \"hkg\": 1341,\n        \"gru\": 1282,\n        \"syd\": 1482,\n        \"jnb\": 1258,\n        \"iad\": 1309,\n        \"ams\": 1276\n      },\n      {\n        \"timestamp\": \"Feb 11, 18:00\",\n        \"hkg\": 1514,\n        \"jnb\": 1500,\n        \"syd\": 1465,\n        \"iad\": 1526,\n        \"ams\": 1531,\n        \"gru\": 1470\n      },\n      {\n        \"timestamp\": \"Feb 11, 19:00\",\n        \"iad\": 1542,\n        \"ams\": 1534,\n        \"syd\": 1457,\n        \"jnb\": 1488,\n        \"gru\": 1440,\n        \"hkg\": 1466\n      },\n      {\n        \"timestamp\": \"Feb 11, 20:00\",\n        \"gru\": 1457,\n        \"iad\": 1317,\n        \"ams\": 1294,\n        \"jnb\": 1274,\n        \"syd\": 1487,\n        \"hkg\": 1305\n      },\n      {\n        \"timestamp\": \"Feb 11, 21:00\",\n        \"hkg\": 1315,\n        \"gru\": 1575,\n        \"syd\": 2010,\n        \"jnb\": 1437,\n        \"iad\": 1263,\n        \"ams\": 1268\n      },\n      {\n        \"timestamp\": \"Feb 11, 22:00\",\n        \"hkg\": 1539,\n        \"jnb\": 1518,\n        \"syd\": 1332,\n        \"iad\": 1537,\n        \"ams\": 1543,\n        \"gru\": 1476\n      },\n      {\n        \"timestamp\": \"Feb 11, 23:00\",\n        \"hkg\": 1554,\n        \"gru\": 1651,\n        \"jnb\": 1518,\n        \"syd\": 1634,\n        \"ams\": 1564,\n        \"iad\": 1565\n      },\n      {\n        \"timestamp\": \"Feb 12, 00:00\",\n        \"hkg\": 1491,\n        \"syd\": 1477,\n        \"jnb\": 1517,\n        \"ams\": 1535,\n        \"iad\": 1543,\n        \"gru\": 1418\n      },\n      {\n        \"timestamp\": \"Feb 12, 01:00\",\n        \"hkg\": 1498,\n        \"gru\": 1459,\n        \"syd\": 1470,\n        \"jnb\": 1494,\n        \"iad\": 1518,\n        \"ams\": 1550\n      },\n      {\n        \"timestamp\": \"Feb 12, 02:00\",\n        \"gru\": 1606,\n        \"jnb\": 1484,\n        \"syd\": 1580,\n        \"iad\": 1512,\n        \"ams\": 1532,\n        \"hkg\": 1499\n      },\n      {\n        \"timestamp\": \"Feb 12, 03:00\",\n        \"ams\": 1564,\n        \"iad\": 1535,\n        \"jnb\": 1494,\n        \"syd\": 1450,\n        \"gru\": 1469,\n        \"hkg\": 1523\n      },\n      {\n        \"timestamp\": \"Feb 12, 04:00\",\n        \"hkg\": 1539,\n        \"gru\": 1380,\n        \"ams\": 1561,\n        \"iad\": 1564,\n        \"jnb\": 1530,\n        \"syd\": 1497\n      },\n      {\n        \"timestamp\": \"Feb 12, 05:00\",\n        \"hkg\": 1531,\n        \"syd\": 1663,\n        \"jnb\": 1515,\n        \"ams\": 1544,\n        \"iad\": 1546,\n        \"gru\": 1471\n      },\n      {\n        \"timestamp\": \"Feb 12, 06:00\",\n        \"jnb\": 1482,\n        \"syd\": 1398,\n        \"ams\": 1534,\n        \"iad\": 1517,\n        \"gru\": 1446,\n        \"hkg\": 1454\n      },\n      {\n        \"timestamp\": \"Feb 12, 07:00\",\n        \"gru\": 1462,\n        \"iad\": 1529,\n        \"ams\": 1532,\n        \"jnb\": 1489,\n        \"syd\": 1872,\n        \"hkg\": 1516\n      },\n      {\n        \"timestamp\": \"Feb 12, 08:00\",\n        \"syd\": 1470,\n        \"jnb\": 1502,\n        \"iad\": 1519,\n        \"ams\": 1538,\n        \"gru\": 1468,\n        \"hkg\": 1522\n      },\n      {\n        \"timestamp\": \"Feb 12, 09:00\",\n        \"gru\": 1446,\n        \"jnb\": 1507,\n        \"syd\": 1301,\n        \"ams\": 1466,\n        \"iad\": 1544,\n        \"hkg\": 1532\n      },\n      {\n        \"timestamp\": \"Feb 12, 10:00\",\n        \"gru\": 1425,\n        \"iad\": 1519,\n        \"ams\": 1523,\n        \"jnb\": 1495,\n        \"syd\": 1463,\n        \"hkg\": 1516\n      },\n      {\n        \"timestamp\": \"Feb 12, 11:00\",\n        \"hkg\": 1521,\n        \"gru\": 1444,\n        \"ams\": 1542,\n        \"iad\": 1533,\n        \"syd\": 1470,\n        \"jnb\": 1502\n      },\n      {\n        \"timestamp\": \"Feb 12, 12:00\",\n        \"hkg\": 1530,\n        \"syd\": 1493,\n        \"jnb\": 1313,\n        \"ams\": 1545,\n        \"iad\": 1535,\n        \"gru\": 1484\n      },\n      {\n        \"timestamp\": \"Feb 12, 13:00\",\n        \"hkg\": 1537,\n        \"syd\": 1670,\n        \"jnb\": 1517,\n        \"ams\": 1561,\n        \"iad\": 1565,\n        \"gru\": 1470\n      },\n      {\n        \"timestamp\": \"Feb 12, 14:00\",\n        \"hkg\": 1561,\n        \"gru\": 1544,\n        \"syd\": 1501,\n        \"jnb\": 1558,\n        \"iad\": 1602,\n        \"ams\": 1578\n      },\n      {\n        \"timestamp\": \"Feb 12, 15:00\",\n        \"gru\": 1543,\n        \"jnb\": 1574,\n        \"syd\": 1525,\n        \"ams\": 1617,\n        \"iad\": 1603,\n        \"hkg\": 1372\n      },\n      {\n        \"timestamp\": \"Feb 12, 16:00\",\n        \"ams\": 1381,\n        \"iad\": 1355,\n        \"jnb\": 1464,\n        \"syd\": 1470,\n        \"gru\": 1454,\n        \"hkg\": 1373\n      },\n      {\n        \"timestamp\": \"Feb 12, 17:00\",\n        \"ams\": 1562,\n        \"iad\": 1550,\n        \"syd\": 1491,\n        \"jnb\": 1507,\n        \"gru\": 1490,\n        \"hkg\": 1538\n      },\n      {\n        \"timestamp\": \"Feb 12, 18:00\",\n        \"syd\": 1465,\n        \"jnb\": 1498,\n        \"iad\": 1530,\n        \"ams\": 1534,\n        \"gru\": 1427,\n        \"hkg\": 1516\n      },\n      {\n        \"timestamp\": \"Feb 12, 19:00\",\n        \"gru\": 1473,\n        \"jnb\": 1333,\n        \"syd\": 1487,\n        \"ams\": 1558,\n        \"iad\": 1553,\n        \"hkg\": 1541\n      },\n      {\n        \"timestamp\": \"Feb 12, 20:00\",\n        \"hkg\": 1521,\n        \"gru\": 1473,\n        \"ams\": 1557,\n        \"iad\": 1546,\n        \"jnb\": 1510,\n        \"syd\": 1480\n      },\n      {\n        \"timestamp\": \"Feb 12, 21:00\",\n        \"hkg\": 1531,\n        \"jnb\": 1484,\n        \"syd\": 1477,\n        \"iad\": 1546,\n        \"ams\": 1544,\n        \"gru\": 1462\n      },\n      {\n        \"timestamp\": \"Feb 12, 22:00\",\n        \"gru\": 1453,\n        \"syd\": 1454,\n        \"jnb\": 1414,\n        \"ams\": 1534,\n        \"iad\": 1498,\n        \"hkg\": 1509\n      },\n      {\n        \"timestamp\": \"Feb 12, 23:00\",\n        \"jnb\": 1219,\n        \"syd\": 1248,\n        \"iad\": 1222,\n        \"ams\": 1194,\n        \"gru\": 1188,\n        \"hkg\": 1264\n      },\n      {\n        \"timestamp\": \"Feb 13, 00:00\",\n        \"hkg\": 1514,\n        \"iad\": 1528,\n        \"ams\": 1526,\n        \"jnb\": 1492,\n        \"syd\": 1415,\n        \"gru\": 1456\n      },\n      {\n        \"timestamp\": \"Feb 13, 01:00\",\n        \"gru\": 1288,\n        \"syd\": 1478,\n        \"jnb\": 1474,\n        \"ams\": 1294,\n        \"iad\": 1287,\n        \"hkg\": 1340\n      },\n      {\n        \"timestamp\": \"Feb 13, 02:00\",\n        \"gru\": 1491,\n        \"iad\": 1566,\n        \"ams\": 1547,\n        \"syd\": 1734,\n        \"jnb\": 1526,\n        \"hkg\": 1545\n      },\n      {\n        \"timestamp\": \"Feb 13, 03:00\",\n        \"jnb\": 1655,\n        \"syd\": 1636,\n        \"ams\": 1534,\n        \"iad\": 1538,\n        \"gru\": 1636,\n        \"hkg\": 1527\n      },\n      {\n        \"timestamp\": \"Feb 13, 04:00\",\n        \"gru\": 1502,\n        \"ams\": 1558,\n        \"iad\": 1539,\n        \"jnb\": 1515,\n        \"syd\": 1502,\n        \"hkg\": 1541\n      },\n      {\n        \"timestamp\": \"Feb 13, 05:00\",\n        \"hkg\": 1514,\n        \"gru\": 1459,\n        \"jnb\": 1496,\n        \"syd\": 1454,\n        \"iad\": 1527,\n        \"ams\": 1538\n      },\n      {\n        \"timestamp\": \"Feb 13, 06:00\",\n        \"hkg\": 1094,\n        \"jnb\": 1105,\n        \"syd\": 1352,\n        \"ams\": 1025,\n        \"iad\": 1069,\n        \"gru\": 1290\n      },\n      {\n        \"timestamp\": \"Feb 13, 07:00\",\n        \"jnb\": 1493,\n        \"syd\": 1459,\n        \"ams\": 1525,\n        \"iad\": 1510,\n        \"gru\": 1412,\n        \"hkg\": 1491\n      },\n      {\n        \"timestamp\": \"Feb 13, 08:00\",\n        \"gru\": 1291,\n        \"ams\": 1323,\n        \"iad\": 1332,\n        \"jnb\": 1295,\n        \"syd\": 1307,\n        \"hkg\": 1353\n      },\n      {\n        \"timestamp\": \"Feb 13, 09:00\",\n        \"hkg\": 1549,\n        \"syd\": 1463,\n        \"jnb\": 1540,\n        \"ams\": 1540,\n        \"iad\": 1553,\n        \"gru\": 1481\n      },\n      {\n        \"timestamp\": \"Feb 13, 10:00\",\n        \"hkg\": 1110,\n        \"iad\": 1085,\n        \"ams\": 1036,\n        \"syd\": 1573,\n        \"jnb\": 1279,\n        \"gru\": 1114\n      },\n      {\n        \"timestamp\": \"Feb 13, 11:00\",\n        \"hkg\": 1550,\n        \"gru\": 1498,\n        \"ams\": 1524,\n        \"iad\": 1518,\n        \"jnb\": 1535,\n        \"syd\": 1499\n      },\n      {\n        \"timestamp\": \"Feb 13, 12:00\",\n        \"hkg\": 943,\n        \"ams\": 1050,\n        \"iad\": 1060,\n        \"jnb\": 1092,\n        \"syd\": 1272,\n        \"gru\": 1085\n      },\n      {\n        \"timestamp\": \"Feb 13, 13:00\",\n        \"hkg\": 1536,\n        \"gru\": 1460,\n        \"jnb\": 1467,\n        \"syd\": 1484,\n        \"iad\": 1554,\n        \"ams\": 1526\n      },\n      {\n        \"timestamp\": \"Feb 13, 14:00\",\n        \"gru\": 1454,\n        \"jnb\": 1612,\n        \"syd\": 1608,\n        \"ams\": 1529,\n        \"iad\": 1516,\n        \"hkg\": 1489\n      },\n      {\n        \"timestamp\": \"Feb 13, 15:00\",\n        \"syd\": 1801,\n        \"jnb\": 1514,\n        \"iad\": 1534,\n        \"ams\": 1562,\n        \"gru\": 1493,\n        \"hkg\": 1569\n      },\n      {\n        \"timestamp\": \"Feb 13, 16:00\",\n        \"hkg\": 1326,\n        \"gru\": 1295,\n        \"iad\": 1347,\n        \"ams\": 1316,\n        \"jnb\": 1310,\n        \"syd\": 1313\n      },\n      {\n        \"timestamp\": \"Feb 13, 17:00\",\n        \"hkg\": 1200,\n        \"ams\": 1037,\n        \"iad\": 1110,\n        \"syd\": 1557,\n        \"jnb\": 1165,\n        \"gru\": 1133\n      },\n      {\n        \"timestamp\": \"Feb 13, 18:00\",\n        \"ams\": 1290,\n        \"iad\": 1300,\n        \"jnb\": 1114,\n        \"syd\": 1487,\n        \"gru\": 1280,\n        \"hkg\": 1302\n      },\n      {\n        \"timestamp\": \"Feb 13, 19:00\",\n        \"jnb\": 1312,\n        \"syd\": 1465,\n        \"ams\": 1540,\n        \"iad\": 1530,\n        \"gru\": 1474,\n        \"hkg\": 1520\n      },\n      {\n        \"timestamp\": \"Feb 13, 20:00\",\n        \"gru\": 1458,\n        \"syd\": 1459,\n        \"jnb\": 1482,\n        \"ams\": 1532,\n        \"iad\": 1523,\n        \"hkg\": 1512\n      },\n      {\n        \"timestamp\": \"Feb 13, 21:00\",\n        \"iad\": 1534,\n        \"ams\": 1535,\n        \"jnb\": 1436,\n        \"syd\": 1440,\n        \"gru\": 1464,\n        \"hkg\": 1506\n      },\n      {\n        \"timestamp\": \"Feb 13, 22:00\",\n        \"gru\": 1503,\n        \"iad\": 1557,\n        \"ams\": 1534,\n        \"syd\": 1494,\n        \"jnb\": 1504,\n        \"hkg\": 1540\n      },\n      {\n        \"timestamp\": \"Feb 13, 23:00\",\n        \"hkg\": 1513,\n        \"gru\": 1630,\n        \"iad\": 1526,\n        \"ams\": 1545,\n        \"syd\": 1614,\n        \"jnb\": 1620\n      },\n      {\n        \"timestamp\": \"Feb 14, 00:00\",\n        \"hkg\": 1510,\n        \"iad\": 1528,\n        \"ams\": 1538,\n        \"jnb\": 1493,\n        \"syd\": 1418,\n        \"gru\": 1601\n      },\n      {\n        \"timestamp\": \"Feb 14, 01:00\",\n        \"hkg\": 1350,\n        \"gru\": 1774,\n        \"iad\": 1321,\n        \"ams\": 1302,\n        \"syd\": 1701,\n        \"jnb\": 1325\n      },\n      {\n        \"timestamp\": \"Feb 14, 02:00\",\n        \"hkg\": 1522,\n        \"iad\": 1532,\n        \"ams\": 1540,\n        \"jnb\": 1498,\n        \"syd\": 1462,\n        \"gru\": 1482\n      },\n      {\n        \"timestamp\": \"Feb 14, 03:00\",\n        \"hkg\": 1509,\n        \"jnb\": 1493,\n        \"syd\": 1325,\n        \"iad\": 1523,\n        \"ams\": 1534,\n        \"gru\": 1591\n      },\n      {\n        \"timestamp\": \"Feb 14, 04:00\",\n        \"syd\": 1310,\n        \"jnb\": 1326,\n        \"iad\": 1333,\n        \"ams\": 1320,\n        \"gru\": 1497,\n        \"hkg\": 1336\n      },\n      {\n        \"timestamp\": \"Feb 14, 05:00\",\n        \"gru\": 1544,\n        \"jnb\": 1345,\n        \"syd\": 1558,\n        \"ams\": 1239,\n        \"iad\": 1267,\n        \"hkg\": 1307\n      },\n      {\n        \"timestamp\": \"Feb 14, 06:00\",\n        \"ams\": 1532,\n        \"iad\": 1511,\n        \"syd\": 1459,\n        \"jnb\": 1493,\n        \"gru\": 1464,\n        \"hkg\": 1480\n      },\n      {\n        \"timestamp\": \"Feb 14, 07:00\",\n        \"gru\": 1484,\n        \"ams\": 1556,\n        \"iad\": 1543,\n        \"syd\": 1480,\n        \"jnb\": 1509,\n        \"hkg\": 1526\n      },\n      {\n        \"timestamp\": \"Feb 14, 08:00\",\n        \"hkg\": 1569,\n        \"gru\": 1479,\n        \"ams\": 1590,\n        \"iad\": 1566,\n        \"jnb\": 1551,\n        \"syd\": 1515\n      },\n      {\n        \"timestamp\": \"Feb 14, 09:00\",\n        \"iad\": 1530,\n        \"ams\": 1550,\n        \"jnb\": 1496,\n        \"syd\": 1462,\n        \"gru\": 1406,\n        \"hkg\": 1518\n      },\n      {\n        \"timestamp\": \"Feb 14, 10:00\",\n        \"hkg\": 1532,\n        \"gru\": 1491,\n        \"iad\": 1527,\n        \"ams\": 1585,\n        \"jnb\": 1492,\n        \"syd\": 1497\n      },\n      {\n        \"timestamp\": \"Feb 14, 11:00\",\n        \"hkg\": 1546,\n        \"gru\": 1506,\n        \"jnb\": 1540,\n        \"syd\": 1493,\n        \"iad\": 1570,\n        \"ams\": 1516\n      },\n      {\n        \"timestamp\": \"Feb 14, 12:00\",\n        \"hkg\": 1520,\n        \"syd\": 1654,\n        \"jnb\": 1522,\n        \"iad\": 1548,\n        \"ams\": 1552,\n        \"gru\": 1475\n      },\n      {\n        \"timestamp\": \"Feb 14, 13:00\",\n        \"jnb\": 1477,\n        \"syd\": 1485,\n        \"ams\": 1296,\n        \"iad\": 1304,\n        \"gru\": 1457,\n        \"hkg\": 1345\n      },\n      {\n        \"timestamp\": \"Feb 14, 14:00\",\n        \"gru\": 1531,\n        \"syd\": 1528,\n        \"jnb\": 1558,\n        \"ams\": 1547,\n        \"iad\": 1535,\n        \"hkg\": 1543\n      },\n      {\n        \"timestamp\": \"Feb 14, 15:00\",\n        \"ams\": 1538,\n        \"iad\": 1532,\n        \"syd\": 1470,\n        \"jnb\": 1506,\n        \"gru\": 1474,\n        \"hkg\": 1487\n      },\n      {\n        \"timestamp\": \"Feb 14, 16:00\",\n        \"gru\": 1441,\n        \"ams\": 1562,\n        \"iad\": 1501,\n        \"jnb\": 1509,\n        \"syd\": 1236,\n        \"hkg\": 1538\n      },\n      {\n        \"timestamp\": \"Feb 14, 17:00\",\n        \"hkg\": 1519,\n        \"ams\": 1541,\n        \"iad\": 1521,\n        \"jnb\": 1463,\n        \"syd\": 1636,\n        \"gru\": 1587\n      },\n      {\n        \"timestamp\": \"Feb 14, 18:00\",\n        \"hkg\": 1534,\n        \"gru\": 1485,\n        \"ams\": 1571,\n        \"iad\": 1552,\n        \"syd\": 1841,\n        \"jnb\": 1508\n      },\n      {\n        \"timestamp\": \"Feb 14, 19:00\",\n        \"hkg\": 1382,\n        \"gru\": 1356,\n        \"syd\": 1564,\n        \"jnb\": 1350,\n        \"ams\": 1333,\n        \"iad\": 1337\n      },\n      {\n        \"timestamp\": \"Feb 14, 20:00\",\n        \"gru\": 1331,\n        \"syd\": 1342,\n        \"jnb\": 1324,\n        \"iad\": 1341,\n        \"ams\": 1320,\n        \"hkg\": 1369\n      },\n      {\n        \"timestamp\": \"Feb 14, 21:00\",\n        \"jnb\": 1510,\n        \"syd\": 1670,\n        \"ams\": 1575,\n        \"iad\": 1564,\n        \"gru\": 1477,\n        \"hkg\": 1548\n      },\n      {\n        \"timestamp\": \"Feb 14, 22:00\",\n        \"hkg\": 1520,\n        \"syd\": 1496,\n        \"jnb\": 1526,\n        \"ams\": 1573,\n        \"iad\": 1574,\n        \"gru\": 1488\n      },\n      {\n        \"timestamp\": \"Feb 14, 23:00\",\n        \"hkg\": 1504,\n        \"gru\": 1423,\n        \"syd\": 1446,\n        \"jnb\": 1492,\n        \"ams\": 1535,\n        \"iad\": 1498\n      },\n      {\n        \"timestamp\": \"Feb 15, 00:00\",\n        \"hkg\": 1507,\n        \"ams\": 1528,\n        \"iad\": 1508,\n        \"jnb\": 1482,\n        \"syd\": 1282,\n        \"gru\": 1444\n      },\n      {\n        \"timestamp\": \"Feb 15, 01:00\",\n        \"hkg\": 1490,\n        \"gru\": 1475,\n        \"iad\": 1537,\n        \"ams\": 1546,\n        \"syd\": 1493,\n        \"jnb\": 1506\n      },\n      {\n        \"timestamp\": \"Feb 15, 02:00\",\n        \"gru\": 1472,\n        \"syd\": 1636,\n        \"jnb\": 1492,\n        \"iad\": 1502,\n        \"ams\": 1540,\n        \"hkg\": 1661\n      },\n      {\n        \"timestamp\": \"Feb 15, 03:00\",\n        \"jnb\": 1503,\n        \"syd\": 1640,\n        \"ams\": 1545,\n        \"iad\": 1537,\n        \"gru\": 1437,\n        \"hkg\": 1523\n      },\n      {\n        \"timestamp\": \"Feb 15, 04:00\",\n        \"iad\": 1576,\n        \"ams\": 1553,\n        \"jnb\": 1516,\n        \"syd\": 1487,\n        \"gru\": 1490,\n        \"hkg\": 1561\n      },\n      {\n        \"timestamp\": \"Feb 15, 05:00\",\n        \"hkg\": 1516,\n        \"gru\": 1472,\n        \"syd\": 1656,\n        \"jnb\": 1510,\n        \"ams\": 1548,\n        \"iad\": 1544\n      },\n      {\n        \"timestamp\": \"Feb 15, 06:00\",\n        \"hkg\": 1512,\n        \"ams\": 1563,\n        \"iad\": 1540,\n        \"syd\": 1504,\n        \"jnb\": 1500,\n        \"gru\": 1476\n      },\n      {\n        \"timestamp\": \"Feb 15, 07:00\",\n        \"iad\": 1533,\n        \"ams\": 1543,\n        \"jnb\": 1475,\n        \"syd\": 1310,\n        \"gru\": 1460,\n        \"hkg\": 1522\n      },\n      {\n        \"timestamp\": \"Feb 15, 08:00\",\n        \"gru\": 1297,\n        \"ams\": 1541,\n        \"iad\": 1495,\n        \"jnb\": 1496,\n        \"syd\": 1456,\n        \"hkg\": 1518\n      },\n      {\n        \"timestamp\": \"Feb 15, 09:00\",\n        \"gru\": 1485,\n        \"ams\": 1546,\n        \"iad\": 1552,\n        \"syd\": 1487,\n        \"jnb\": 1496,\n        \"hkg\": 1538\n      },\n      {\n        \"timestamp\": \"Feb 15, 10:00\",\n        \"syd\": 1698,\n        \"jnb\": 1532,\n        \"ams\": 1582,\n        \"iad\": 1570,\n        \"gru\": 1498,\n        \"hkg\": 1528\n      },\n      {\n        \"timestamp\": \"Feb 15, 11:00\",\n        \"hkg\": 1497,\n        \"jnb\": 1500,\n        \"syd\": 1481,\n        \"iad\": 1542,\n        \"ams\": 1549,\n        \"gru\": 1466\n      },\n      {\n        \"timestamp\": \"Feb 15, 12:00\",\n        \"hkg\": 1514,\n        \"ams\": 1561,\n        \"iad\": 1545,\n        \"jnb\": 1523,\n        \"syd\": 1662,\n        \"gru\": 1453\n      },\n      {\n        \"timestamp\": \"Feb 15, 13:00\",\n        \"hkg\": 1328,\n        \"gru\": 1291,\n        \"jnb\": 1287,\n        \"syd\": 1149,\n        \"iad\": 1296,\n        \"ams\": 1293\n      },\n      {\n        \"timestamp\": \"Feb 15, 14:00\",\n        \"hkg\": 1530,\n        \"gru\": 1470,\n        \"jnb\": 1405,\n        \"syd\": 1483,\n        \"ams\": 1570,\n        \"iad\": 1551\n      },\n      {\n        \"timestamp\": \"Feb 15, 15:00\",\n        \"hkg\": 1527,\n        \"syd\": 1455,\n        \"jnb\": 1496,\n        \"iad\": 1532,\n        \"ams\": 1571,\n        \"gru\": 1472\n      },\n      {\n        \"timestamp\": \"Feb 15, 16:00\",\n        \"syd\": 1457,\n        \"jnb\": 1512,\n        \"ams\": 1578,\n        \"iad\": 1554,\n        \"gru\": 1469,\n        \"hkg\": 1522\n      },\n      {\n        \"timestamp\": \"Feb 17, 10:00\",\n        \"hkg\": 1600,\n        \"iad\": 1627,\n        \"ams\": 1621,\n        \"syd\": 1553,\n        \"jnb\": 1579,\n        \"gru\": 1559\n      },\n      {\n        \"timestamp\": \"Feb 17, 11:00\",\n        \"gru\": 1620,\n        \"jnb\": 1612,\n        \"syd\": 1785,\n        \"ams\": 1545,\n        \"iad\": 1536,\n        \"hkg\": 1508\n      },\n      {\n        \"timestamp\": \"Feb 17, 12:00\",\n        \"syd\": 1884,\n        \"jnb\": 1502,\n        \"ams\": 1566,\n        \"iad\": 1552,\n        \"gru\": 1491,\n        \"hkg\": 1551\n      },\n      {\n        \"timestamp\": \"Feb 17, 13:00\",\n        \"jnb\": 1498,\n        \"syd\": 1680,\n        \"ams\": 1544,\n        \"iad\": 1560,\n        \"gru\": 1637,\n        \"hkg\": 1527\n      },\n      {\n        \"timestamp\": \"Feb 17, 14:00\",\n        \"hkg\": 1539,\n        \"syd\": 1456,\n        \"jnb\": 1511,\n        \"ams\": 1552,\n        \"iad\": 1516,\n        \"gru\": 1464\n      },\n      {\n        \"timestamp\": \"Feb 17, 15:00\",\n        \"hkg\": 1511,\n        \"gru\": 1456,\n        \"iad\": 1551,\n        \"ams\": 1558,\n        \"syd\": 1480,\n        \"jnb\": 1430\n      },\n      {\n        \"timestamp\": \"Feb 17, 16:00\",\n        \"hkg\": 1570,\n        \"jnb\": 1496,\n        \"syd\": 1712,\n        \"iad\": 1545,\n        \"ams\": 1547,\n        \"gru\": 1471\n      },\n      {\n        \"timestamp\": \"Feb 17, 17:00\",\n        \"hkg\": 1568,\n        \"gru\": 1507,\n        \"syd\": 1493,\n        \"jnb\": 1298,\n        \"ams\": 1529,\n        \"iad\": 1595\n      },\n      {\n        \"timestamp\": \"Feb 17, 18:00\",\n        \"gru\": 1506,\n        \"syd\": 1490,\n        \"jnb\": 1519,\n        \"iad\": 1563,\n        \"ams\": 1544,\n        \"hkg\": 1577\n      },\n      {\n        \"timestamp\": \"Feb 17, 19:00\",\n        \"jnb\": 1540,\n        \"syd\": 1503,\n        \"ams\": 1548,\n        \"iad\": 1596,\n        \"gru\": 1527,\n        \"hkg\": 1526\n      },\n      {\n        \"timestamp\": \"Feb 17, 20:00\",\n        \"hkg\": 1557,\n        \"iad\": 1542,\n        \"ams\": 1542,\n        \"syd\": 1842,\n        \"jnb\": 1498,\n        \"gru\": 1475\n      },\n      {\n        \"timestamp\": \"Feb 17, 21:00\",\n        \"hkg\": 1544,\n        \"gru\": 1503,\n        \"syd\": 1852,\n        \"jnb\": 1506,\n        \"ams\": 1557,\n        \"iad\": 1538\n      },\n      {\n        \"timestamp\": \"Feb 17, 22:00\",\n        \"hkg\": 1554,\n        \"gru\": 1484,\n        \"iad\": 1582,\n        \"ams\": 1542,\n        \"syd\": 1619,\n        \"jnb\": 1512\n      },\n      {\n        \"timestamp\": \"Feb 17, 23:00\",\n        \"gru\": 1503,\n        \"ams\": 1559,\n        \"iad\": 1537,\n        \"jnb\": 1488,\n        \"syd\": 1690,\n        \"hkg\": 1546\n      },\n      {\n        \"timestamp\": \"Feb 18, 00:00\",\n        \"jnb\": 1536,\n        \"syd\": 1678,\n        \"ams\": 1571,\n        \"iad\": 1592,\n        \"gru\": 1366,\n        \"hkg\": 1701\n      },\n      {\n        \"timestamp\": \"Feb 18, 01:00\",\n        \"syd\": 1646,\n        \"jnb\": 1502,\n        \"ams\": 1550,\n        \"iad\": 1554,\n        \"gru\": 1457,\n        \"hkg\": 1526\n      },\n      {\n        \"timestamp\": \"Feb 18, 02:00\",\n        \"gru\": 1485,\n        \"syd\": 1346,\n        \"jnb\": 1636,\n        \"iad\": 1526,\n        \"ams\": 1539,\n        \"hkg\": 1532\n      },\n      {\n        \"timestamp\": \"Feb 18, 03:00\",\n        \"hkg\": 1471,\n        \"gru\": 1480,\n        \"jnb\": 1523,\n        \"syd\": 1659,\n        \"ams\": 1538,\n        \"iad\": 1550\n      },\n      {\n        \"timestamp\": \"Feb 18, 04:00\",\n        \"hkg\": 1520,\n        \"ams\": 1552,\n        \"iad\": 1548,\n        \"jnb\": 1515,\n        \"syd\": 1326,\n        \"gru\": 1495\n      },\n      {\n        \"timestamp\": \"Feb 18, 05:00\",\n        \"gru\": 1448,\n        \"syd\": 1484,\n        \"jnb\": 1504,\n        \"iad\": 1542,\n        \"ams\": 1549,\n        \"hkg\": 1538\n      },\n      {\n        \"timestamp\": \"Feb 18, 06:00\",\n        \"gru\": 1501,\n        \"ams\": 1575,\n        \"iad\": 1563,\n        \"syd\": 1398,\n        \"jnb\": 1537,\n        \"hkg\": 1542\n      },\n      {\n        \"timestamp\": \"Feb 18, 07:00\",\n        \"iad\": 1542,\n        \"ams\": 1556,\n        \"jnb\": 1508,\n        \"syd\": 1483,\n        \"gru\": 1496,\n        \"hkg\": 1537\n      },\n      {\n        \"timestamp\": \"Feb 18, 08:00\",\n        \"hkg\": 1528,\n        \"jnb\": 1482,\n        \"syd\": 1328,\n        \"iad\": 1545,\n        \"ams\": 1551,\n        \"gru\": 1490\n      },\n      {\n        \"timestamp\": \"Feb 18, 09:00\",\n        \"gru\": 1495,\n        \"ams\": 1549,\n        \"iad\": 1560,\n        \"syd\": 1548,\n        \"jnb\": 1489,\n        \"hkg\": 1538\n      },\n      {\n        \"timestamp\": \"Feb 18, 10:00\",\n        \"syd\": 1291,\n        \"jnb\": 1454,\n        \"iad\": 1557,\n        \"ams\": 1554,\n        \"gru\": 1499,\n        \"hkg\": 1523\n      },\n      {\n        \"timestamp\": \"Feb 18, 11:00\",\n        \"gru\": 1518,\n        \"jnb\": 1524,\n        \"syd\": 1838,\n        \"ams\": 1585,\n        \"iad\": 1551,\n        \"hkg\": 1707\n      },\n      {\n        \"timestamp\": \"Feb 18, 12:00\",\n        \"hkg\": 1565,\n        \"gru\": 1546,\n        \"ams\": 1584,\n        \"iad\": 1566,\n        \"jnb\": 1570,\n        \"syd\": 1494\n      },\n      {\n        \"timestamp\": \"Feb 18, 13:00\",\n        \"hkg\": 1578,\n        \"jnb\": 1559,\n        \"syd\": 1549,\n        \"iad\": 1613,\n        \"ams\": 1589,\n        \"gru\": 1548\n      },\n      {\n        \"timestamp\": \"Feb 18, 14:00\",\n        \"hkg\": 1573,\n        \"ams\": 1553,\n        \"iad\": 1577,\n        \"jnb\": 1543,\n        \"syd\": 1513,\n        \"gru\": 1495\n      },\n      {\n        \"timestamp\": \"Feb 18, 15:00\",\n        \"hkg\": 1546,\n        \"syd\": 1479,\n        \"jnb\": 1492,\n        \"ams\": 1547,\n        \"iad\": 1560,\n        \"gru\": 1319\n      },\n      {\n        \"timestamp\": \"Feb 18, 16:00\",\n        \"hkg\": 1561,\n        \"gru\": 1513,\n        \"ams\": 1552,\n        \"iad\": 1568,\n        \"syd\": 1498,\n        \"jnb\": 1518\n      },\n      {\n        \"timestamp\": \"Feb 18, 17:00\",\n        \"jnb\": 1502,\n        \"syd\": 1466,\n        \"ams\": 1544,\n        \"iad\": 1546,\n        \"gru\": 1474,\n        \"hkg\": 1538\n      },\n      {\n        \"timestamp\": \"Feb 18, 18:00\",\n        \"gru\": 1522,\n        \"jnb\": 1540,\n        \"syd\": 1308,\n        \"iad\": 1595,\n        \"ams\": 1553,\n        \"hkg\": 1551\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"syd\",\n      \"avgLatency\": 1542,\n      \"p75Latency\": 1514,\n      \"p90Latency\": 1666,\n      \"p95Latency\": 2560,\n      \"p99Latency\": 2641\n    },\n    {\n      \"region\": \"gru\",\n      \"avgLatency\": 1478,\n      \"p75Latency\": 1510,\n      \"p90Latency\": 1542,\n      \"p95Latency\": 1567,\n      \"p99Latency\": 1878\n    },\n    {\n      \"region\": \"iad\",\n      \"avgLatency\": 1547,\n      \"p75Latency\": 1572,\n      \"p90Latency\": 1599,\n      \"p95Latency\": 1627,\n      \"p99Latency\": 1716\n    },\n    {\n      \"region\": \"ams\",\n      \"avgLatency\": 1548,\n      \"p75Latency\": 1562,\n      \"p90Latency\": 1584,\n      \"p95Latency\": 1606,\n      \"p99Latency\": 1654\n    },\n    {\n      \"region\": \"hkg\",\n      \"avgLatency\": 1541,\n      \"p75Latency\": 1556,\n      \"p90Latency\": 1600,\n      \"p95Latency\": 1624,\n      \"p99Latency\": 1975\n    },\n    {\n      \"region\": \"jnb\",\n      \"avgLatency\": 1501,\n      \"p75Latency\": 1530,\n      \"p90Latency\": 1555,\n      \"p95Latency\": 1591,\n      \"p99Latency\": 1689\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/monitoring-latency/koyeb.json",
    "content": "{\n  \"regions\": [\"ams\", \"iad\", \"hkg\", \"jnb\", \"syd\", \"gru\"],\n  \"data\": {\n    \"regions\": [\"ams\", \"gru\", \"hkg\", \"iad\", \"jnb\", \"syd\"],\n    \"data\": [\n      {\n        \"timestamp\": \"Feb 4, 00:00\",\n        \"ams\": 142,\n        \"iad\": 173,\n        \"syd\": 786,\n        \"jnb\": 900,\n        \"gru\": 640,\n        \"hkg\": 369\n      },\n      {\n        \"timestamp\": \"Feb 4, 01:00\",\n        \"ams\": 136,\n        \"iad\": 426,\n        \"syd\": 770,\n        \"jnb\": 841,\n        \"gru\": 732,\n        \"hkg\": 375\n      },\n      {\n        \"timestamp\": \"Feb 4, 02:00\",\n        \"hkg\": 351,\n        \"ams\": 131,\n        \"iad\": 170,\n        \"jnb\": 880,\n        \"syd\": 698,\n        \"gru\": 678\n      },\n      {\n        \"timestamp\": \"Feb 4, 03:00\",\n        \"hkg\": 371,\n        \"jnb\": 897,\n        \"syd\": 793,\n        \"ams\": 145,\n        \"iad\": 170,\n        \"gru\": 668\n      },\n      {\n        \"timestamp\": \"Feb 4, 04:00\",\n        \"hkg\": 369,\n        \"gru\": 693,\n        \"syd\": 668,\n        \"jnb\": 834,\n        \"ams\": 138,\n        \"iad\": 172\n      },\n      {\n        \"timestamp\": \"Feb 4, 05:00\",\n        \"gru\": 630,\n        \"syd\": 665,\n        \"jnb\": 951,\n        \"ams\": 145,\n        \"iad\": 174,\n        \"hkg\": 375\n      },\n      {\n        \"timestamp\": \"Feb 4, 06:00\",\n        \"jnb\": 842,\n        \"syd\": 687,\n        \"ams\": 161,\n        \"iad\": 170,\n        \"gru\": 644,\n        \"hkg\": 366\n      },\n      {\n        \"timestamp\": \"Feb 4, 07:00\",\n        \"hkg\": 381,\n        \"jnb\": 846,\n        \"syd\": 590,\n        \"ams\": 145,\n        \"iad\": 169,\n        \"gru\": 604\n      },\n      {\n        \"timestamp\": \"Feb 4, 08:00\",\n        \"hkg\": 358,\n        \"gru\": 696,\n        \"syd\": 693,\n        \"jnb\": 923,\n        \"ams\": 148,\n        \"iad\": 166\n      },\n      {\n        \"timestamp\": \"Feb 4, 09:00\",\n        \"hkg\": 380,\n        \"iad\": 168,\n        \"ams\": 164,\n        \"jnb\": 911,\n        \"syd\": 697,\n        \"gru\": 665\n      },\n      {\n        \"timestamp\": \"Feb 4, 10:00\",\n        \"hkg\": 376,\n        \"gru\": 662,\n        \"jnb\": 903,\n        \"syd\": 812,\n        \"iad\": 166,\n        \"ams\": 146\n      },\n      {\n        \"timestamp\": \"Feb 4, 11:00\",\n        \"gru\": 681,\n        \"syd\": 749,\n        \"jnb\": 884,\n        \"ams\": 138,\n        \"iad\": 170,\n        \"hkg\": 364\n      },\n      {\n        \"timestamp\": \"Feb 4, 12:00\",\n        \"gru\": 644,\n        \"iad\": 168,\n        \"ams\": 144,\n        \"syd\": 675,\n        \"jnb\": 1045,\n        \"hkg\": 371\n      },\n      {\n        \"timestamp\": \"Feb 4, 13:00\",\n        \"syd\": 676,\n        \"jnb\": 866,\n        \"ams\": 165,\n        \"iad\": 167,\n        \"gru\": 667,\n        \"hkg\": 381\n      },\n      {\n        \"timestamp\": \"Feb 4, 14:00\",\n        \"hkg\": 381,\n        \"gru\": 677,\n        \"ams\": 154,\n        \"iad\": 185,\n        \"jnb\": 876,\n        \"syd\": 791\n      },\n      {\n        \"timestamp\": \"Feb 4, 15:00\",\n        \"hkg\": 377,\n        \"iad\": 164,\n        \"ams\": 166,\n        \"syd\": 659,\n        \"jnb\": 895,\n        \"gru\": 662\n      },\n      {\n        \"timestamp\": \"Feb 4, 16:00\",\n        \"ams\": 153,\n        \"iad\": 166,\n        \"syd\": 744,\n        \"jnb\": 864,\n        \"gru\": 662,\n        \"hkg\": 410\n      },\n      {\n        \"timestamp\": \"Feb 4, 17:00\",\n        \"jnb\": 886,\n        \"syd\": 614,\n        \"ams\": 159,\n        \"iad\": 175,\n        \"gru\": 705,\n        \"hkg\": 376\n      },\n      {\n        \"timestamp\": \"Feb 4, 18:00\",\n        \"gru\": 662,\n        \"jnb\": 938,\n        \"syd\": 731,\n        \"ams\": 155,\n        \"iad\": 174,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 4, 19:00\",\n        \"syd\": 674,\n        \"jnb\": 991,\n        \"iad\": 182,\n        \"ams\": 186,\n        \"gru\": 677,\n        \"hkg\": 350\n      },\n      {\n        \"timestamp\": \"Feb 4, 20:00\",\n        \"ams\": 175,\n        \"iad\": 166,\n        \"syd\": 690,\n        \"jnb\": 816,\n        \"gru\": 694,\n        \"hkg\": 374\n      },\n      {\n        \"timestamp\": \"Feb 4, 21:00\",\n        \"hkg\": 722,\n        \"syd\": 620,\n        \"jnb\": 817,\n        \"iad\": 174,\n        \"ams\": 161,\n        \"gru\": 670\n      },\n      {\n        \"timestamp\": \"Feb 4, 22:00\",\n        \"hkg\": 381,\n        \"gru\": 730,\n        \"iad\": 172,\n        \"ams\": 170,\n        \"syd\": 682,\n        \"jnb\": 892\n      },\n      {\n        \"timestamp\": \"Feb 4, 23:00\",\n        \"jnb\": 940,\n        \"syd\": 743,\n        \"iad\": 174,\n        \"ams\": 162,\n        \"gru\": 681,\n        \"hkg\": 374\n      },\n      {\n        \"timestamp\": \"Feb 5, 00:00\",\n        \"gru\": 670,\n        \"jnb\": 890,\n        \"syd\": 665,\n        \"ams\": 162,\n        \"iad\": 483,\n        \"hkg\": 396\n      },\n      {\n        \"timestamp\": \"Feb 5, 01:00\",\n        \"gru\": 647,\n        \"iad\": 168,\n        \"ams\": 162,\n        \"syd\": 644,\n        \"jnb\": 833,\n        \"hkg\": 377\n      },\n      {\n        \"timestamp\": \"Feb 5, 02:00\",\n        \"syd\": 666,\n        \"jnb\": 905,\n        \"iad\": 178,\n        \"ams\": 146,\n        \"gru\": 735,\n        \"hkg\": 384\n      },\n      {\n        \"timestamp\": \"Feb 5, 03:00\",\n        \"gru\": 724,\n        \"ams\": 131,\n        \"iad\": 167,\n        \"jnb\": 846,\n        \"syd\": 647,\n        \"hkg\": 372\n      },\n      {\n        \"timestamp\": \"Feb 5, 04:00\",\n        \"gru\": 653,\n        \"jnb\": 835,\n        \"syd\": 714,\n        \"iad\": 169,\n        \"ams\": 138,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 5, 05:00\",\n        \"ams\": 146,\n        \"iad\": 167,\n        \"jnb\": 936,\n        \"syd\": 762,\n        \"gru\": 638,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 5, 06:00\",\n        \"hkg\": 370,\n        \"jnb\": 942,\n        \"syd\": 722,\n        \"ams\": 145,\n        \"iad\": 164,\n        \"gru\": 677\n      },\n      {\n        \"timestamp\": \"Feb 5, 07:00\",\n        \"hkg\": 381,\n        \"gru\": 692,\n        \"syd\": 566,\n        \"jnb\": 894,\n        \"iad\": 166,\n        \"ams\": 152\n      },\n      {\n        \"timestamp\": \"Feb 5, 08:00\",\n        \"jnb\": 6466,\n        \"syd\": 612,\n        \"ams\": 148,\n        \"iad\": 160,\n        \"gru\": 632,\n        \"hkg\": 436\n      },\n      {\n        \"timestamp\": \"Feb 5, 09:00\",\n        \"ams\": 152,\n        \"iad\": 164,\n        \"syd\": 715,\n        \"jnb\": 988,\n        \"gru\": 677,\n        \"hkg\": 476\n      },\n      {\n        \"timestamp\": \"Feb 5, 10:00\",\n        \"hkg\": 388,\n        \"syd\": 621,\n        \"jnb\": 929,\n        \"ams\": 137,\n        \"iad\": 177,\n        \"gru\": 668\n      },\n      {\n        \"timestamp\": \"Feb 5, 11:00\",\n        \"hkg\": 410,\n        \"gru\": 712,\n        \"jnb\": 902,\n        \"syd\": 756,\n        \"iad\": 626,\n        \"ams\": 170\n      },\n      {\n        \"timestamp\": \"Feb 5, 12:00\",\n        \"hkg\": 386,\n        \"gru\": 716,\n        \"ams\": 179,\n        \"iad\": 162,\n        \"jnb\": 1020,\n        \"syd\": 748\n      },\n      {\n        \"timestamp\": \"Feb 5, 13:00\",\n        \"hkg\": 369,\n        \"iad\": 178,\n        \"ams\": 172,\n        \"jnb\": 930,\n        \"syd\": 747,\n        \"gru\": 648\n      },\n      {\n        \"timestamp\": \"Feb 5, 14:00\",\n        \"hkg\": 380,\n        \"gru\": 754,\n        \"jnb\": 949,\n        \"syd\": 650,\n        \"iad\": 178,\n        \"ams\": 159\n      },\n      {\n        \"timestamp\": \"Feb 5, 15:00\",\n        \"gru\": 698,\n        \"syd\": 628,\n        \"jnb\": 875,\n        \"ams\": 162,\n        \"iad\": 180,\n        \"hkg\": 390\n      },\n      {\n        \"timestamp\": \"Feb 5, 16:00\",\n        \"syd\": 660,\n        \"jnb\": 882,\n        \"iad\": 181,\n        \"ams\": 155,\n        \"gru\": 631,\n        \"hkg\": 388\n      },\n      {\n        \"timestamp\": \"Feb 5, 17:00\",\n        \"hkg\": 366,\n        \"syd\": 707,\n        \"jnb\": 851,\n        \"iad\": 180,\n        \"ams\": 181,\n        \"gru\": 688\n      },\n      {\n        \"timestamp\": \"Feb 5, 18:00\",\n        \"hkg\": 362,\n        \"gru\": 686,\n        \"jnb\": 871,\n        \"syd\": 717,\n        \"iad\": 181,\n        \"ams\": 177\n      },\n      {\n        \"timestamp\": \"Feb 5, 19:00\",\n        \"gru\": 638,\n        \"ams\": 180,\n        \"iad\": 181,\n        \"jnb\": 912,\n        \"syd\": 614,\n        \"hkg\": 353\n      },\n      {\n        \"timestamp\": \"Feb 5, 20:00\",\n        \"ams\": 148,\n        \"iad\": 180,\n        \"syd\": 664,\n        \"jnb\": 808,\n        \"gru\": 714,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 5, 21:00\",\n        \"ams\": 189,\n        \"iad\": 179,\n        \"jnb\": 866,\n        \"syd\": 627,\n        \"gru\": 731,\n        \"hkg\": 376\n      },\n      {\n        \"timestamp\": \"Feb 5, 22:00\",\n        \"gru\": 725,\n        \"syd\": 723,\n        \"jnb\": 859,\n        \"ams\": 159,\n        \"iad\": 236,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 5, 23:00\",\n        \"iad\": 177,\n        \"ams\": 141,\n        \"syd\": 674,\n        \"jnb\": 841,\n        \"gru\": 643,\n        \"hkg\": 367\n      },\n      {\n        \"timestamp\": \"Feb 6, 00:00\",\n        \"hkg\": 372,\n        \"iad\": 172,\n        \"ams\": 137,\n        \"jnb\": 937,\n        \"syd\": 806,\n        \"gru\": 704\n      },\n      {\n        \"timestamp\": \"Feb 6, 01:00\",\n        \"hkg\": 363,\n        \"jnb\": 885,\n        \"syd\": 612,\n        \"ams\": 154,\n        \"iad\": 171,\n        \"gru\": 675\n      },\n      {\n        \"timestamp\": \"Feb 6, 02:00\",\n        \"gru\": 700,\n        \"jnb\": 873,\n        \"syd\": 751,\n        \"ams\": 171,\n        \"iad\": 178,\n        \"hkg\": 347\n      },\n      {\n        \"timestamp\": \"Feb 6, 03:00\",\n        \"iad\": 184,\n        \"ams\": 141,\n        \"jnb\": 945,\n        \"syd\": 748,\n        \"gru\": 731,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 6, 04:00\",\n        \"hkg\": 385,\n        \"iad\": 167,\n        \"ams\": 149,\n        \"syd\": 612,\n        \"jnb\": 910,\n        \"gru\": 774\n      },\n      {\n        \"timestamp\": \"Feb 6, 05:00\",\n        \"hkg\": 532,\n        \"iad\": 163,\n        \"ams\": 163,\n        \"jnb\": 927,\n        \"syd\": 638,\n        \"gru\": 694\n      },\n      {\n        \"timestamp\": \"Feb 6, 06:00\",\n        \"hkg\": 378,\n        \"gru\": 676,\n        \"iad\": 166,\n        \"ams\": 159,\n        \"syd\": 670,\n        \"jnb\": 1006\n      },\n      {\n        \"timestamp\": \"Feb 6, 07:00\",\n        \"hkg\": 341,\n        \"ams\": 155,\n        \"iad\": 157,\n        \"syd\": 779,\n        \"jnb\": 1624,\n        \"gru\": 670\n      },\n      {\n        \"timestamp\": \"Feb 6, 08:00\",\n        \"hkg\": 371,\n        \"gru\": 659,\n        \"ams\": 145,\n        \"iad\": 218,\n        \"jnb\": 1467,\n        \"syd\": 615\n      },\n      {\n        \"timestamp\": \"Feb 6, 09:00\",\n        \"hkg\": 365,\n        \"gru\": 681,\n        \"jnb\": 1459,\n        \"syd\": 690,\n        \"ams\": 162,\n        \"iad\": 167\n      },\n      {\n        \"timestamp\": \"Feb 6, 10:00\",\n        \"hkg\": 368,\n        \"syd\": 658,\n        \"jnb\": 1406,\n        \"ams\": 141,\n        \"iad\": 167,\n        \"gru\": 674\n      },\n      {\n        \"timestamp\": \"Feb 6, 11:00\",\n        \"hkg\": 382,\n        \"syd\": 792,\n        \"jnb\": 1345,\n        \"ams\": 156,\n        \"iad\": 162,\n        \"gru\": 650\n      },\n      {\n        \"timestamp\": \"Feb 6, 12:00\",\n        \"hkg\": 376,\n        \"gru\": 692,\n        \"jnb\": 1471,\n        \"syd\": 708,\n        \"ams\": 202,\n        \"iad\": 166\n      },\n      {\n        \"timestamp\": \"Feb 6, 13:00\",\n        \"hkg\": 378,\n        \"gru\": 700,\n        \"syd\": 709,\n        \"jnb\": 1482,\n        \"ams\": 154,\n        \"iad\": 170\n      },\n      {\n        \"timestamp\": \"Feb 6, 14:00\",\n        \"gru\": 662,\n        \"jnb\": 1530,\n        \"syd\": 724,\n        \"ams\": 146,\n        \"iad\": 167,\n        \"hkg\": 411\n      },\n      {\n        \"timestamp\": \"Feb 6, 15:00\",\n        \"iad\": 181,\n        \"ams\": 156,\n        \"jnb\": 1548,\n        \"syd\": 728,\n        \"gru\": 774,\n        \"hkg\": 382\n      },\n      {\n        \"timestamp\": \"Feb 6, 16:00\",\n        \"gru\": 663,\n        \"syd\": 734,\n        \"jnb\": 1468,\n        \"iad\": 168,\n        \"ams\": 194,\n        \"hkg\": 367\n      },\n      {\n        \"timestamp\": \"Feb 6, 17:00\",\n        \"gru\": 713,\n        \"ams\": 161,\n        \"iad\": 167,\n        \"syd\": 704,\n        \"jnb\": 1370,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 6, 18:00\",\n        \"hkg\": 356,\n        \"gru\": 725,\n        \"ams\": 173,\n        \"iad\": 170,\n        \"jnb\": 1609,\n        \"syd\": 667\n      },\n      {\n        \"timestamp\": \"Feb 6, 19:00\",\n        \"hkg\": 352,\n        \"jnb\": 1512,\n        \"syd\": 659,\n        \"iad\": 180,\n        \"ams\": 176,\n        \"gru\": 669\n      },\n      {\n        \"timestamp\": \"Feb 6, 20:00\",\n        \"gru\": 635,\n        \"jnb\": 1492,\n        \"syd\": 823,\n        \"iad\": 178,\n        \"ams\": 195,\n        \"hkg\": 377\n      },\n      {\n        \"timestamp\": \"Feb 6, 21:00\",\n        \"gru\": 718,\n        \"syd\": 704,\n        \"jnb\": 1405,\n        \"iad\": 174,\n        \"ams\": 196,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 6, 22:00\",\n        \"jnb\": 1460,\n        \"syd\": 710,\n        \"iad\": 177,\n        \"ams\": 162,\n        \"gru\": 663,\n        \"hkg\": 372\n      },\n      {\n        \"timestamp\": \"Feb 6, 23:00\",\n        \"hkg\": 372,\n        \"ams\": 139,\n        \"iad\": 180,\n        \"jnb\": 1466,\n        \"syd\": 696,\n        \"gru\": 653\n      },\n      {\n        \"timestamp\": \"Feb 7, 00:00\",\n        \"hkg\": 367,\n        \"gru\": 728,\n        \"ams\": 163,\n        \"iad\": 174,\n        \"syd\": 852,\n        \"jnb\": 1600\n      },\n      {\n        \"timestamp\": \"Feb 7, 01:00\",\n        \"hkg\": 384,\n        \"jnb\": 1302,\n        \"syd\": 544,\n        \"iad\": 168,\n        \"ams\": 178,\n        \"gru\": 782\n      },\n      {\n        \"timestamp\": \"Feb 7, 02:00\",\n        \"hkg\": 370,\n        \"gru\": 642,\n        \"jnb\": 1403,\n        \"syd\": 694,\n        \"ams\": 159,\n        \"iad\": 178\n      },\n      {\n        \"timestamp\": \"Feb 7, 03:00\",\n        \"gru\": 708,\n        \"syd\": 652,\n        \"jnb\": 1346,\n        \"iad\": 172,\n        \"ams\": 138,\n        \"hkg\": 377\n      },\n      {\n        \"timestamp\": \"Feb 7, 04:00\",\n        \"iad\": 181,\n        \"ams\": 159,\n        \"syd\": 786,\n        \"jnb\": 1361,\n        \"gru\": 688,\n        \"hkg\": 386\n      },\n      {\n        \"timestamp\": \"Feb 7, 05:00\",\n        \"hkg\": 370,\n        \"jnb\": 1382,\n        \"syd\": 614,\n        \"iad\": 165,\n        \"ams\": 138,\n        \"gru\": 618\n      },\n      {\n        \"timestamp\": \"Feb 7, 06:00\",\n        \"hkg\": 376,\n        \"ams\": 166,\n        \"iad\": 170,\n        \"jnb\": 1540,\n        \"syd\": 704,\n        \"gru\": 676\n      },\n      {\n        \"timestamp\": \"Feb 7, 07:00\",\n        \"hkg\": 365,\n        \"gru\": 653,\n        \"iad\": 168,\n        \"ams\": 137,\n        \"syd\": 648,\n        \"jnb\": 1192\n      },\n      {\n        \"timestamp\": \"Feb 7, 08:00\",\n        \"gru\": 684,\n        \"syd\": 744,\n        \"jnb\": 1561,\n        \"iad\": 208,\n        \"ams\": 177,\n        \"hkg\": 359\n      },\n      {\n        \"timestamp\": \"Feb 7, 09:00\",\n        \"gru\": 691,\n        \"iad\": 177,\n        \"ams\": 188,\n        \"jnb\": 3767,\n        \"syd\": 663,\n        \"hkg\": 387\n      },\n      {\n        \"timestamp\": \"Feb 7, 10:00\",\n        \"jnb\": 927,\n        \"syd\": 701,\n        \"ams\": 165,\n        \"iad\": 166,\n        \"gru\": 705,\n        \"hkg\": 376\n      },\n      {\n        \"timestamp\": \"Feb 7, 11:00\",\n        \"gru\": 649,\n        \"syd\": 639,\n        \"jnb\": 927,\n        \"iad\": 168,\n        \"ams\": 203,\n        \"hkg\": 357\n      },\n      {\n        \"timestamp\": \"Feb 7, 12:00\",\n        \"hkg\": 370,\n        \"gru\": 691,\n        \"iad\": 171,\n        \"ams\": 174,\n        \"syd\": 761,\n        \"jnb\": 1248\n      },\n      {\n        \"timestamp\": \"Feb 7, 13:00\",\n        \"hkg\": 375,\n        \"syd\": 769,\n        \"jnb\": 954,\n        \"ams\": 156,\n        \"iad\": 176,\n        \"gru\": 732\n      },\n      {\n        \"timestamp\": \"Feb 7, 14:00\",\n        \"hkg\": 380,\n        \"iad\": 167,\n        \"ams\": 151,\n        \"syd\": 748,\n        \"jnb\": 918,\n        \"gru\": 700\n      },\n      {\n        \"timestamp\": \"Feb 7, 15:00\",\n        \"gru\": 742,\n        \"ams\": 138,\n        \"iad\": 196,\n        \"syd\": 684,\n        \"jnb\": 886,\n        \"hkg\": 392\n      },\n      {\n        \"timestamp\": \"Feb 7, 16:00\",\n        \"syd\": 635,\n        \"jnb\": 948,\n        \"ams\": 166,\n        \"iad\": 182,\n        \"gru\": 698,\n        \"hkg\": 377\n      },\n      {\n        \"timestamp\": \"Feb 7, 17:00\",\n        \"iad\": 197,\n        \"ams\": 172,\n        \"jnb\": 899,\n        \"syd\": 692,\n        \"gru\": 658,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 7, 18:00\",\n        \"gru\": 710,\n        \"ams\": 163,\n        \"iad\": 196,\n        \"jnb\": 880,\n        \"syd\": 595,\n        \"hkg\": 364\n      },\n      {\n        \"timestamp\": \"Feb 7, 19:00\",\n        \"syd\": 968,\n        \"jnb\": 870,\n        \"iad\": 184,\n        \"ams\": 160,\n        \"gru\": 697,\n        \"hkg\": 368\n      },\n      {\n        \"timestamp\": \"Feb 7, 20:00\",\n        \"gru\": 692,\n        \"iad\": 180,\n        \"ams\": 180,\n        \"syd\": 686,\n        \"jnb\": 915,\n        \"hkg\": 343\n      },\n      {\n        \"timestamp\": \"Feb 7, 21:00\",\n        \"hkg\": 348,\n        \"gru\": 740,\n        \"syd\": 660,\n        \"jnb\": 930,\n        \"ams\": 155,\n        \"iad\": 182\n      },\n      {\n        \"timestamp\": \"Feb 7, 22:00\",\n        \"hkg\": 362,\n        \"gru\": 727,\n        \"iad\": 201,\n        \"ams\": 251,\n        \"syd\": 752,\n        \"jnb\": 854\n      },\n      {\n        \"timestamp\": \"Feb 7, 23:00\",\n        \"hkg\": 456,\n        \"ams\": 171,\n        \"iad\": 173,\n        \"jnb\": 931,\n        \"syd\": 749,\n        \"gru\": 752\n      },\n      {\n        \"timestamp\": \"Feb 8, 00:00\",\n        \"hkg\": 346,\n        \"syd\": 787,\n        \"jnb\": 907,\n        \"ams\": 148,\n        \"iad\": 187,\n        \"gru\": 691\n      },\n      {\n        \"timestamp\": \"Feb 8, 01:00\",\n        \"hkg\": 357,\n        \"ams\": 132,\n        \"iad\": 178,\n        \"jnb\": 880,\n        \"syd\": 755,\n        \"gru\": 686\n      },\n      {\n        \"timestamp\": \"Feb 8, 02:00\",\n        \"iad\": 169,\n        \"ams\": 199,\n        \"jnb\": 898,\n        \"syd\": 839,\n        \"gru\": 676,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 8, 03:00\",\n        \"gru\": 663,\n        \"ams\": 184,\n        \"iad\": 172,\n        \"syd\": 744,\n        \"jnb\": 809,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 8, 04:00\",\n        \"ams\": 176,\n        \"iad\": 170,\n        \"syd\": 648,\n        \"jnb\": 894,\n        \"gru\": 724,\n        \"hkg\": 344\n      },\n      {\n        \"timestamp\": \"Feb 8, 05:00\",\n        \"gru\": 735,\n        \"syd\": 649,\n        \"jnb\": 885,\n        \"iad\": 168,\n        \"ams\": 132,\n        \"hkg\": 365\n      },\n      {\n        \"timestamp\": \"Feb 8, 06:00\",\n        \"gru\": 682,\n        \"ams\": 137,\n        \"iad\": 170,\n        \"syd\": 671,\n        \"jnb\": 744,\n        \"hkg\": 348\n      },\n      {\n        \"timestamp\": \"Feb 8, 07:00\",\n        \"hkg\": 351,\n        \"gru\": 733,\n        \"iad\": 170,\n        \"ams\": 158,\n        \"jnb\": 857,\n        \"syd\": 740\n      },\n      {\n        \"timestamp\": \"Feb 8, 08:00\",\n        \"hkg\": 364,\n        \"jnb\": 920,\n        \"syd\": 716,\n        \"iad\": 168,\n        \"ams\": 167,\n        \"gru\": 686\n      },\n      {\n        \"timestamp\": \"Feb 8, 09:00\",\n        \"gru\": 649,\n        \"jnb\": 831,\n        \"syd\": 684,\n        \"ams\": 151,\n        \"iad\": 174,\n        \"hkg\": 355\n      },\n      {\n        \"timestamp\": \"Feb 8, 10:00\",\n        \"syd\": 692,\n        \"jnb\": 849,\n        \"ams\": 204,\n        \"iad\": 171,\n        \"gru\": 766,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 8, 11:00\",\n        \"hkg\": 354,\n        \"syd\": 681,\n        \"jnb\": 934,\n        \"ams\": 198,\n        \"iad\": 176,\n        \"gru\": 700\n      },\n      {\n        \"timestamp\": \"Feb 8, 12:00\",\n        \"hkg\": 365,\n        \"gru\": 687,\n        \"jnb\": 1001,\n        \"syd\": 715,\n        \"ams\": 172,\n        \"iad\": 177\n      },\n      {\n        \"timestamp\": \"Feb 8, 13:00\",\n        \"hkg\": 350,\n        \"iad\": 176,\n        \"ams\": 186,\n        \"syd\": 519,\n        \"jnb\": 909,\n        \"gru\": 704\n      },\n      {\n        \"timestamp\": \"Feb 8, 14:00\",\n        \"hkg\": 372,\n        \"gru\": 659,\n        \"iad\": 182,\n        \"ams\": 169,\n        \"jnb\": 858,\n        \"syd\": 849\n      },\n      {\n        \"timestamp\": \"Feb 8, 15:00\",\n        \"hkg\": 364,\n        \"gru\": 681,\n        \"jnb\": 922,\n        \"syd\": 687,\n        \"iad\": 184,\n        \"ams\": 186\n      },\n      {\n        \"timestamp\": \"Feb 8, 16:00\",\n        \"gru\": 716,\n        \"syd\": 622,\n        \"jnb\": 902,\n        \"iad\": 177,\n        \"ams\": 163,\n        \"hkg\": 356\n      },\n      {\n        \"timestamp\": \"Feb 8, 17:00\",\n        \"gru\": 709,\n        \"syd\": 671,\n        \"jnb\": 877,\n        \"iad\": 184,\n        \"ams\": 171,\n        \"hkg\": 394\n      },\n      {\n        \"timestamp\": \"Feb 8, 18:00\",\n        \"hkg\": 364,\n        \"gru\": 686,\n        \"syd\": 641,\n        \"jnb\": 891,\n        \"ams\": 170,\n        \"iad\": 188\n      },\n      {\n        \"timestamp\": \"Feb 8, 19:00\",\n        \"hkg\": 351,\n        \"syd\": 740,\n        \"jnb\": 906,\n        \"ams\": 155,\n        \"iad\": 188,\n        \"gru\": 684\n      },\n      {\n        \"timestamp\": \"Feb 8, 20:00\",\n        \"jnb\": 838,\n        \"syd\": 668,\n        \"ams\": 154,\n        \"iad\": 167,\n        \"gru\": 660,\n        \"hkg\": 369\n      },\n      {\n        \"timestamp\": \"Feb 8, 21:00\",\n        \"gru\": 684,\n        \"syd\": 599,\n        \"jnb\": 908,\n        \"iad\": 192,\n        \"ams\": 160,\n        \"hkg\": 357\n      },\n      {\n        \"timestamp\": \"Feb 8, 22:00\",\n        \"iad\": 186,\n        \"ams\": 160,\n        \"jnb\": 880,\n        \"syd\": 661,\n        \"gru\": 683,\n        \"hkg\": 354\n      },\n      {\n        \"timestamp\": \"Feb 8, 23:00\",\n        \"jnb\": 870,\n        \"syd\": 777,\n        \"iad\": 187,\n        \"ams\": 177,\n        \"gru\": 806,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 9, 00:00\",\n        \"gru\": 683,\n        \"syd\": 674,\n        \"jnb\": 861,\n        \"iad\": 171,\n        \"ams\": 161,\n        \"hkg\": 354\n      },\n      {\n        \"timestamp\": \"Feb 9, 01:00\",\n        \"gru\": 720,\n        \"syd\": 634,\n        \"jnb\": 840,\n        \"ams\": 138,\n        \"iad\": 176,\n        \"hkg\": 338\n      },\n      {\n        \"timestamp\": \"Feb 9, 02:00\",\n        \"jnb\": 812,\n        \"syd\": 723,\n        \"ams\": 433,\n        \"iad\": 186,\n        \"gru\": 669,\n        \"hkg\": 347\n      },\n      {\n        \"timestamp\": \"Feb 9, 03:00\",\n        \"hkg\": 361,\n        \"gru\": 676,\n        \"syd\": 718,\n        \"jnb\": 774,\n        \"ams\": 140,\n        \"iad\": 171\n      },\n      {\n        \"timestamp\": \"Feb 9, 04:00\",\n        \"hkg\": 360,\n        \"jnb\": 908,\n        \"syd\": 716,\n        \"ams\": 146,\n        \"iad\": 167,\n        \"gru\": 620\n      },\n      {\n        \"timestamp\": \"Feb 9, 05:00\",\n        \"syd\": 626,\n        \"jnb\": 932,\n        \"iad\": 168,\n        \"ams\": 176,\n        \"gru\": 644,\n        \"hkg\": 373\n      },\n      {\n        \"timestamp\": \"Feb 9, 06:00\",\n        \"gru\": 727,\n        \"jnb\": 827,\n        \"syd\": 744,\n        \"iad\": 170,\n        \"ams\": 148,\n        \"hkg\": 354\n      },\n      {\n        \"timestamp\": \"Feb 9, 07:00\",\n        \"gru\": 676,\n        \"iad\": 168,\n        \"ams\": 170,\n        \"jnb\": 900,\n        \"syd\": 640,\n        \"hkg\": 368\n      },\n      {\n        \"timestamp\": \"Feb 9, 08:00\",\n        \"syd\": 755,\n        \"jnb\": 866,\n        \"iad\": 161,\n        \"ams\": 149,\n        \"gru\": 683,\n        \"hkg\": 366\n      },\n      {\n        \"timestamp\": \"Feb 9, 09:00\",\n        \"syd\": 692,\n        \"jnb\": 883,\n        \"iad\": 173,\n        \"ams\": 150,\n        \"gru\": 670,\n        \"hkg\": 368\n      },\n      {\n        \"timestamp\": \"Feb 9, 10:00\",\n        \"hkg\": 359,\n        \"jnb\": 882,\n        \"syd\": 4936,\n        \"iad\": 170,\n        \"ams\": 165,\n        \"gru\": 701\n      },\n      {\n        \"timestamp\": \"Feb 9, 11:00\",\n        \"hkg\": 350,\n        \"gru\": 728,\n        \"jnb\": 869,\n        \"syd\": 628,\n        \"iad\": 168,\n        \"ams\": 171\n      },\n      {\n        \"timestamp\": \"Feb 9, 12:00\",\n        \"hkg\": 366,\n        \"gru\": 686,\n        \"ams\": 171,\n        \"iad\": 171,\n        \"syd\": 725,\n        \"jnb\": 1057\n      },\n      {\n        \"timestamp\": \"Feb 9, 13:00\",\n        \"hkg\": 361,\n        \"iad\": 172,\n        \"ams\": 190,\n        \"jnb\": 904,\n        \"syd\": 785,\n        \"gru\": 696\n      },\n      {\n        \"timestamp\": \"Feb 9, 14:00\",\n        \"iad\": 165,\n        \"ams\": 207,\n        \"syd\": 692,\n        \"jnb\": 868,\n        \"gru\": 668,\n        \"hkg\": 354\n      },\n      {\n        \"timestamp\": \"Feb 9, 15:00\",\n        \"syd\": 742,\n        \"jnb\": 838,\n        \"iad\": 180,\n        \"ams\": 172,\n        \"gru\": 684,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 9, 16:00\",\n        \"gru\": 685,\n        \"jnb\": 5243,\n        \"syd\": 712,\n        \"iad\": 185,\n        \"ams\": 158,\n        \"hkg\": 384\n      },\n      {\n        \"timestamp\": \"Feb 9, 17:00\",\n        \"ams\": 174,\n        \"iad\": 192,\n        \"syd\": 9398,\n        \"jnb\": 886,\n        \"gru\": 687,\n        \"hkg\": 348\n      },\n      {\n        \"timestamp\": \"Feb 9, 18:00\",\n        \"gru\": 706,\n        \"syd\": 677,\n        \"jnb\": 878,\n        \"ams\": 156,\n        \"iad\": 177,\n        \"hkg\": 346\n      },\n      {\n        \"timestamp\": \"Feb 9, 19:00\",\n        \"hkg\": 361,\n        \"gru\": 696,\n        \"jnb\": 920,\n        \"syd\": 564,\n        \"iad\": 174,\n        \"ams\": 218\n      },\n      {\n        \"timestamp\": \"Feb 9, 20:00\",\n        \"hkg\": 365,\n        \"jnb\": 860,\n        \"syd\": 673,\n        \"ams\": 148,\n        \"iad\": 174,\n        \"gru\": 763\n      },\n      {\n        \"timestamp\": \"Feb 9, 21:00\",\n        \"gru\": 746,\n        \"iad\": 181,\n        \"ams\": 166,\n        \"syd\": 666,\n        \"jnb\": 842,\n        \"hkg\": 377\n      },\n      {\n        \"timestamp\": \"Feb 9, 22:00\",\n        \"syd\": 696,\n        \"jnb\": 911,\n        \"iad\": 184,\n        \"ams\": 155,\n        \"gru\": 730,\n        \"hkg\": 345\n      },\n      {\n        \"timestamp\": \"Feb 9, 23:00\",\n        \"hkg\": 341,\n        \"jnb\": 777,\n        \"syd\": 700,\n        \"ams\": 163,\n        \"iad\": 176,\n        \"gru\": 717\n      },\n      {\n        \"timestamp\": \"Feb 10, 00:00\",\n        \"hkg\": 360,\n        \"iad\": 172,\n        \"ams\": 133,\n        \"jnb\": 877,\n        \"syd\": 691,\n        \"gru\": 673\n      },\n      {\n        \"timestamp\": \"Feb 10, 01:00\",\n        \"hkg\": 359,\n        \"syd\": 707,\n        \"jnb\": 866,\n        \"iad\": 177,\n        \"ams\": 181,\n        \"gru\": 717\n      },\n      {\n        \"timestamp\": \"Feb 10, 02:00\",\n        \"hkg\": 367,\n        \"gru\": 734,\n        \"syd\": 641,\n        \"jnb\": 887,\n        \"iad\": 176,\n        \"ams\": 170\n      },\n      {\n        \"timestamp\": \"Feb 10, 03:00\",\n        \"hkg\": 346,\n        \"jnb\": 915,\n        \"syd\": 740,\n        \"ams\": 142,\n        \"iad\": 180,\n        \"gru\": 732\n      },\n      {\n        \"timestamp\": \"Feb 10, 04:00\",\n        \"jnb\": 822,\n        \"syd\": 595,\n        \"iad\": 160,\n        \"ams\": 142,\n        \"gru\": 674,\n        \"hkg\": 342\n      },\n      {\n        \"timestamp\": \"Feb 10, 05:00\",\n        \"gru\": 689,\n        \"ams\": 142,\n        \"iad\": 222,\n        \"jnb\": 830,\n        \"syd\": 725,\n        \"hkg\": 343\n      },\n      {\n        \"timestamp\": \"Feb 10, 06:00\",\n        \"hkg\": 356,\n        \"gru\": 678,\n        \"syd\": 4115,\n        \"jnb\": 774,\n        \"iad\": 160,\n        \"ams\": 142\n      },\n      {\n        \"timestamp\": \"Feb 10, 07:00\",\n        \"hkg\": 344,\n        \"jnb\": 850,\n        \"syd\": 569,\n        \"ams\": 135,\n        \"iad\": 163,\n        \"gru\": 702\n      },\n      {\n        \"timestamp\": \"Feb 10, 08:00\",\n        \"hkg\": 392,\n        \"iad\": 171,\n        \"ams\": 166,\n        \"jnb\": 772,\n        \"syd\": 667,\n        \"gru\": 683\n      },\n      {\n        \"timestamp\": \"Feb 10, 09:00\",\n        \"hkg\": 397,\n        \"syd\": 767,\n        \"jnb\": 854,\n        \"ams\": 146,\n        \"iad\": 165,\n        \"gru\": 694\n      },\n      {\n        \"timestamp\": \"Feb 10, 10:00\",\n        \"hkg\": 351,\n        \"gru\": 676,\n        \"ams\": 169,\n        \"iad\": 159,\n        \"syd\": 733,\n        \"jnb\": 847\n      },\n      {\n        \"timestamp\": \"Feb 10, 11:00\",\n        \"hkg\": 350,\n        \"jnb\": 830,\n        \"syd\": 602,\n        \"iad\": 163,\n        \"ams\": 160,\n        \"gru\": 663\n      },\n      {\n        \"timestamp\": \"Feb 10, 12:00\",\n        \"hkg\": 360,\n        \"gru\": 658,\n        \"jnb\": 959,\n        \"syd\": 731,\n        \"ams\": 144,\n        \"iad\": 165\n      },\n      {\n        \"timestamp\": \"Feb 10, 13:00\",\n        \"gru\": 692,\n        \"iad\": 160,\n        \"ams\": 167,\n        \"jnb\": 954,\n        \"syd\": 741,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 10, 14:00\",\n        \"jnb\": 920,\n        \"syd\": 648,\n        \"iad\": 162,\n        \"ams\": 174,\n        \"gru\": 661,\n        \"hkg\": 350\n      },\n      {\n        \"timestamp\": \"Feb 10, 15:00\",\n        \"hkg\": 362,\n        \"gru\": 698,\n        \"jnb\": 849,\n        \"syd\": 662,\n        \"ams\": 169,\n        \"iad\": 171\n      },\n      {\n        \"timestamp\": \"Feb 10, 16:00\",\n        \"hkg\": 340,\n        \"gru\": 677,\n        \"iad\": 172,\n        \"ams\": 197,\n        \"jnb\": 836,\n        \"syd\": 625\n      },\n      {\n        \"timestamp\": \"Feb 10, 17:00\",\n        \"hkg\": 358,\n        \"gru\": 806,\n        \"syd\": 794,\n        \"jnb\": 839,\n        \"iad\": 172,\n        \"ams\": 197\n      },\n      {\n        \"timestamp\": \"Feb 10, 18:00\",\n        \"gru\": 682,\n        \"iad\": 168,\n        \"ams\": 197,\n        \"syd\": 582,\n        \"jnb\": 879,\n        \"hkg\": 344\n      },\n      {\n        \"timestamp\": \"Feb 10, 19:00\",\n        \"ams\": 147,\n        \"iad\": 166,\n        \"jnb\": 838,\n        \"syd\": 720,\n        \"gru\": 698,\n        \"hkg\": 345\n      },\n      {\n        \"timestamp\": \"Feb 10, 20:00\",\n        \"gru\": 677,\n        \"jnb\": 882,\n        \"syd\": 647,\n        \"iad\": 168,\n        \"ams\": 146,\n        \"hkg\": 343\n      },\n      {\n        \"timestamp\": \"Feb 10, 21:00\",\n        \"ams\": 157,\n        \"iad\": 169,\n        \"jnb\": 883,\n        \"syd\": 713,\n        \"gru\": 830,\n        \"hkg\": 345\n      },\n      {\n        \"timestamp\": \"Feb 10, 22:00\",\n        \"hkg\": 349,\n        \"jnb\": 774,\n        \"syd\": 674,\n        \"ams\": 142,\n        \"iad\": 160,\n        \"gru\": 718\n      },\n      {\n        \"timestamp\": \"Feb 10, 23:00\",\n        \"hkg\": 351,\n        \"gru\": 656,\n        \"syd\": 752,\n        \"jnb\": 925,\n        \"iad\": 172,\n        \"ams\": 156\n      },\n      {\n        \"timestamp\": \"Feb 11, 00:00\",\n        \"hkg\": 324,\n        \"gru\": 710,\n        \"ams\": 135,\n        \"iad\": 180,\n        \"syd\": 584,\n        \"jnb\": 828\n      },\n      {\n        \"timestamp\": \"Feb 11, 01:00\",\n        \"hkg\": 349,\n        \"jnb\": 789,\n        \"syd\": 683,\n        \"ams\": 142,\n        \"iad\": 161,\n        \"gru\": 631\n      },\n      {\n        \"timestamp\": \"Feb 11, 02:00\",\n        \"hkg\": 356,\n        \"gru\": 662,\n        \"syd\": 644,\n        \"jnb\": 740,\n        \"ams\": 178,\n        \"iad\": 165\n      },\n      {\n        \"timestamp\": \"Feb 11, 03:00\",\n        \"gru\": 634,\n        \"iad\": 169,\n        \"ams\": 201,\n        \"syd\": 721,\n        \"jnb\": 784,\n        \"hkg\": 355\n      },\n      {\n        \"timestamp\": \"Feb 11, 04:00\",\n        \"iad\": 164,\n        \"ams\": 133,\n        \"jnb\": 840,\n        \"syd\": 665,\n        \"gru\": 712,\n        \"hkg\": 341\n      },\n      {\n        \"timestamp\": \"Feb 11, 05:00\",\n        \"gru\": 696,\n        \"iad\": 170,\n        \"ams\": 140,\n        \"syd\": 792,\n        \"jnb\": 873,\n        \"hkg\": 366\n      },\n      {\n        \"timestamp\": \"Feb 11, 06:00\",\n        \"iad\": 167,\n        \"ams\": 142,\n        \"jnb\": 905,\n        \"syd\": 684,\n        \"gru\": 749,\n        \"hkg\": 339\n      },\n      {\n        \"timestamp\": \"Feb 11, 07:00\",\n        \"hkg\": 355,\n        \"jnb\": 872,\n        \"syd\": 640,\n        \"ams\": 187,\n        \"iad\": 169,\n        \"gru\": 711\n      },\n      {\n        \"timestamp\": \"Feb 11, 08:00\",\n        \"hkg\": 344,\n        \"gru\": 652,\n        \"syd\": 709,\n        \"jnb\": 844,\n        \"ams\": 179,\n        \"iad\": 160\n      },\n      {\n        \"timestamp\": \"Feb 11, 09:00\",\n        \"hkg\": 374,\n        \"syd\": 657,\n        \"jnb\": 865,\n        \"iad\": 166,\n        \"ams\": 143,\n        \"gru\": 710\n      },\n      {\n        \"timestamp\": \"Feb 11, 10:00\",\n        \"gru\": 760,\n        \"syd\": 783,\n        \"jnb\": 811,\n        \"iad\": 165,\n        \"ams\": 198,\n        \"hkg\": 342\n      },\n      {\n        \"timestamp\": \"Feb 11, 11:00\",\n        \"ams\": 194,\n        \"iad\": 168,\n        \"syd\": 714,\n        \"jnb\": 933,\n        \"gru\": 689,\n        \"hkg\": 346\n      },\n      {\n        \"timestamp\": \"Feb 11, 12:00\",\n        \"hkg\": 352,\n        \"ams\": 280,\n        \"iad\": 166,\n        \"jnb\": 922,\n        \"syd\": 649,\n        \"gru\": 666\n      },\n      {\n        \"timestamp\": \"Feb 11, 13:00\",\n        \"hkg\": 357,\n        \"gru\": 684,\n        \"syd\": 665,\n        \"jnb\": 890,\n        \"ams\": 166,\n        \"iad\": 166\n      },\n      {\n        \"timestamp\": \"Feb 11, 14:00\",\n        \"hkg\": 360,\n        \"ams\": 176,\n        \"iad\": 167,\n        \"jnb\": 812,\n        \"syd\": 3391,\n        \"gru\": 656\n      },\n      {\n        \"timestamp\": \"Feb 11, 15:00\",\n        \"hkg\": 352,\n        \"gru\": 730,\n        \"jnb\": 786,\n        \"syd\": 671,\n        \"iad\": 169,\n        \"ams\": 180\n      },\n      {\n        \"timestamp\": \"Feb 11, 16:00\",\n        \"gru\": 730,\n        \"syd\": 728,\n        \"jnb\": 861,\n        \"iad\": 174,\n        \"ams\": 218,\n        \"hkg\": 366\n      },\n      {\n        \"timestamp\": \"Feb 11, 17:00\",\n        \"hkg\": 342,\n        \"gru\": 701,\n        \"syd\": 8049,\n        \"jnb\": 868,\n        \"iad\": 177,\n        \"ams\": 168\n      },\n      {\n        \"timestamp\": \"Feb 11, 18:00\",\n        \"hkg\": 376,\n        \"jnb\": 1115,\n        \"syd\": 784,\n        \"iad\": 173,\n        \"ams\": 236,\n        \"gru\": 683\n      },\n      {\n        \"timestamp\": \"Feb 11, 19:00\",\n        \"iad\": 176,\n        \"ams\": 208,\n        \"syd\": 680,\n        \"jnb\": 1540,\n        \"gru\": 716,\n        \"hkg\": 343\n      },\n      {\n        \"timestamp\": \"Feb 11, 20:00\",\n        \"gru\": 684,\n        \"iad\": 170,\n        \"ams\": 162,\n        \"jnb\": 1553,\n        \"syd\": 774,\n        \"hkg\": 345\n      },\n      {\n        \"timestamp\": \"Feb 11, 21:00\",\n        \"hkg\": 344,\n        \"gru\": 714,\n        \"syd\": 590,\n        \"jnb\": 1320,\n        \"iad\": 176,\n        \"ams\": 154\n      },\n      {\n        \"timestamp\": \"Feb 11, 22:00\",\n        \"hkg\": 345,\n        \"jnb\": 1380,\n        \"syd\": 702,\n        \"iad\": 178,\n        \"ams\": 229,\n        \"gru\": 685\n      },\n      {\n        \"timestamp\": \"Feb 11, 23:00\",\n        \"hkg\": 345,\n        \"gru\": 691,\n        \"jnb\": 1521,\n        \"syd\": 684,\n        \"ams\": 206,\n        \"iad\": 166\n      },\n      {\n        \"timestamp\": \"Feb 12, 00:00\",\n        \"hkg\": 360,\n        \"syd\": 615,\n        \"jnb\": 1498,\n        \"ams\": 174,\n        \"iad\": 173,\n        \"gru\": 691\n      },\n      {\n        \"timestamp\": \"Feb 12, 01:00\",\n        \"hkg\": 330,\n        \"gru\": 722,\n        \"syd\": 586,\n        \"jnb\": 1478,\n        \"iad\": 168,\n        \"ams\": 148\n      },\n      {\n        \"timestamp\": \"Feb 12, 02:00\",\n        \"gru\": 680,\n        \"jnb\": 1466,\n        \"syd\": 832,\n        \"iad\": 167,\n        \"ams\": 137,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 12, 03:00\",\n        \"ams\": 182,\n        \"iad\": 169,\n        \"jnb\": 1548,\n        \"syd\": 636,\n        \"gru\": 728,\n        \"hkg\": 376\n      },\n      {\n        \"timestamp\": \"Feb 12, 04:00\",\n        \"hkg\": 350,\n        \"gru\": 681,\n        \"ams\": 216,\n        \"iad\": 161,\n        \"jnb\": 1223,\n        \"syd\": 664\n      },\n      {\n        \"timestamp\": \"Feb 12, 05:00\",\n        \"hkg\": 348,\n        \"syd\": 693,\n        \"jnb\": 7838,\n        \"ams\": 184,\n        \"iad\": 161,\n        \"gru\": 714\n      },\n      {\n        \"timestamp\": \"Feb 12, 06:00\",\n        \"jnb\": 2068,\n        \"syd\": 800,\n        \"ams\": 3691,\n        \"iad\": 166,\n        \"gru\": 655,\n        \"hkg\": 339\n      },\n      {\n        \"timestamp\": \"Feb 12, 07:00\",\n        \"gru\": 656,\n        \"iad\": 158,\n        \"ams\": 191,\n        \"jnb\": 824,\n        \"syd\": 727,\n        \"hkg\": 359\n      },\n      {\n        \"timestamp\": \"Feb 12, 08:00\",\n        \"syd\": 770,\n        \"jnb\": 814,\n        \"iad\": 164,\n        \"ams\": 166,\n        \"gru\": 687,\n        \"hkg\": 336\n      },\n      {\n        \"timestamp\": \"Feb 12, 09:00\",\n        \"gru\": 675,\n        \"jnb\": 852,\n        \"syd\": 673,\n        \"ams\": 177,\n        \"iad\": 165,\n        \"hkg\": 354\n      },\n      {\n        \"timestamp\": \"Feb 12, 10:00\",\n        \"gru\": 728,\n        \"iad\": 163,\n        \"ams\": 175,\n        \"jnb\": 826,\n        \"syd\": 678,\n        \"hkg\": 359\n      },\n      {\n        \"timestamp\": \"Feb 12, 11:00\",\n        \"hkg\": 384,\n        \"gru\": 699,\n        \"ams\": 176,\n        \"iad\": 180,\n        \"syd\": 622,\n        \"jnb\": 877\n      },\n      {\n        \"timestamp\": \"Feb 12, 12:00\",\n        \"hkg\": 345,\n        \"syd\": 724,\n        \"jnb\": 1046,\n        \"ams\": 200,\n        \"iad\": 170,\n        \"gru\": 670\n      },\n      {\n        \"timestamp\": \"Feb 12, 13:00\",\n        \"hkg\": 356,\n        \"syd\": 630,\n        \"jnb\": 962,\n        \"ams\": 212,\n        \"iad\": 170,\n        \"gru\": 675\n      },\n      {\n        \"timestamp\": \"Feb 12, 14:00\",\n        \"hkg\": 360,\n        \"gru\": 678,\n        \"syd\": 672,\n        \"jnb\": 901,\n        \"iad\": 167,\n        \"ams\": 284\n      },\n      {\n        \"timestamp\": \"Feb 12, 15:00\",\n        \"gru\": 710,\n        \"jnb\": 865,\n        \"syd\": 630,\n        \"ams\": 241,\n        \"iad\": 177,\n        \"hkg\": 409\n      },\n      {\n        \"timestamp\": \"Feb 12, 16:00\",\n        \"ams\": 199,\n        \"iad\": 164,\n        \"jnb\": 880,\n        \"syd\": 609,\n        \"gru\": 683,\n        \"hkg\": 400\n      },\n      {\n        \"timestamp\": \"Feb 12, 17:00\",\n        \"ams\": 187,\n        \"iad\": 178,\n        \"syd\": 712,\n        \"jnb\": 1004,\n        \"gru\": 730,\n        \"hkg\": 378\n      },\n      {\n        \"timestamp\": \"Feb 12, 18:00\",\n        \"syd\": 589,\n        \"jnb\": 863,\n        \"iad\": 170,\n        \"ams\": 194,\n        \"gru\": 686,\n        \"hkg\": 348\n      },\n      {\n        \"timestamp\": \"Feb 12, 19:00\",\n        \"gru\": 696,\n        \"jnb\": 973,\n        \"syd\": 676,\n        \"ams\": 185,\n        \"iad\": 184,\n        \"hkg\": 367\n      },\n      {\n        \"timestamp\": \"Feb 12, 20:00\",\n        \"hkg\": 369,\n        \"gru\": 686,\n        \"ams\": 192,\n        \"iad\": 178,\n        \"jnb\": 957,\n        \"syd\": 725\n      },\n      {\n        \"timestamp\": \"Feb 12, 21:00\",\n        \"hkg\": 386,\n        \"jnb\": 955,\n        \"syd\": 698,\n        \"iad\": 173,\n        \"ams\": 185,\n        \"gru\": 710\n      },\n      {\n        \"timestamp\": \"Feb 12, 22:00\",\n        \"gru\": 666,\n        \"syd\": 672,\n        \"jnb\": 794,\n        \"ams\": 178,\n        \"iad\": 172,\n        \"hkg\": 340\n      },\n      {\n        \"timestamp\": \"Feb 12, 23:00\",\n        \"jnb\": 840,\n        \"syd\": 754,\n        \"iad\": 168,\n        \"ams\": 153,\n        \"gru\": 643,\n        \"hkg\": 343\n      },\n      {\n        \"timestamp\": \"Feb 13, 00:00\",\n        \"hkg\": 352,\n        \"iad\": 166,\n        \"ams\": 179,\n        \"jnb\": 879,\n        \"syd\": 730,\n        \"gru\": 653\n      },\n      {\n        \"timestamp\": \"Feb 13, 01:00\",\n        \"gru\": 740,\n        \"syd\": 691,\n        \"jnb\": 828,\n        \"ams\": 165,\n        \"iad\": 169,\n        \"hkg\": 345\n      },\n      {\n        \"timestamp\": \"Feb 13, 02:00\",\n        \"gru\": 720,\n        \"iad\": 164,\n        \"ams\": 130,\n        \"syd\": 623,\n        \"jnb\": 867,\n        \"hkg\": 344\n      },\n      {\n        \"timestamp\": \"Feb 13, 03:00\",\n        \"jnb\": 894,\n        \"syd\": 811,\n        \"ams\": 148,\n        \"iad\": 172,\n        \"gru\": 652,\n        \"hkg\": 351\n      },\n      {\n        \"timestamp\": \"Feb 13, 04:00\",\n        \"gru\": 778,\n        \"ams\": 182,\n        \"iad\": 172,\n        \"jnb\": 904,\n        \"syd\": 734,\n        \"hkg\": 362\n      },\n      {\n        \"timestamp\": \"Feb 13, 05:00\",\n        \"hkg\": 398,\n        \"gru\": 751,\n        \"jnb\": 868,\n        \"syd\": 636,\n        \"iad\": 163,\n        \"ams\": 166\n      },\n      {\n        \"timestamp\": \"Feb 13, 06:00\",\n        \"hkg\": 365,\n        \"jnb\": 886,\n        \"syd\": 632,\n        \"ams\": 174,\n        \"iad\": 174,\n        \"gru\": 713\n      },\n      {\n        \"timestamp\": \"Feb 13, 07:00\",\n        \"jnb\": 854,\n        \"syd\": 752,\n        \"ams\": 166,\n        \"iad\": 164,\n        \"gru\": 676,\n        \"hkg\": 376\n      },\n      {\n        \"timestamp\": \"Feb 13, 08:00\",\n        \"gru\": 608,\n        \"ams\": 138,\n        \"iad\": 150,\n        \"jnb\": 778,\n        \"syd\": 667,\n        \"hkg\": 350\n      },\n      {\n        \"timestamp\": \"Feb 13, 09:00\",\n        \"hkg\": 368,\n        \"syd\": 743,\n        \"jnb\": 892,\n        \"ams\": 207,\n        \"iad\": 168,\n        \"gru\": 643\n      },\n      {\n        \"timestamp\": \"Feb 13, 10:00\",\n        \"hkg\": 372,\n        \"iad\": 164,\n        \"ams\": 184,\n        \"syd\": 646,\n        \"jnb\": 828,\n        \"gru\": 680\n      },\n      {\n        \"timestamp\": \"Feb 13, 11:00\",\n        \"hkg\": 354,\n        \"gru\": 731,\n        \"ams\": 183,\n        \"iad\": 441,\n        \"jnb\": 858,\n        \"syd\": 638\n      },\n      {\n        \"timestamp\": \"Feb 13, 12:00\",\n        \"hkg\": 362,\n        \"ams\": 168,\n        \"iad\": 164,\n        \"jnb\": 973,\n        \"syd\": 687,\n        \"gru\": 654\n      },\n      {\n        \"timestamp\": \"Feb 13, 13:00\",\n        \"hkg\": 357,\n        \"gru\": 706,\n        \"jnb\": 976,\n        \"syd\": 731,\n        \"iad\": 162,\n        \"ams\": 146\n      },\n      {\n        \"timestamp\": \"Feb 13, 14:00\",\n        \"gru\": 681,\n        \"jnb\": 887,\n        \"syd\": 752,\n        \"ams\": 179,\n        \"iad\": 162,\n        \"hkg\": 356\n      },\n      {\n        \"timestamp\": \"Feb 13, 15:00\",\n        \"syd\": 694,\n        \"jnb\": 808,\n        \"iad\": 177,\n        \"ams\": 530,\n        \"gru\": 666,\n        \"hkg\": 369\n      },\n      {\n        \"timestamp\": \"Feb 13, 16:00\",\n        \"hkg\": 375,\n        \"gru\": 702,\n        \"iad\": 191,\n        \"ams\": 156,\n        \"jnb\": 798,\n        \"syd\": 665\n      },\n      {\n        \"timestamp\": \"Feb 13, 17:00\",\n        \"hkg\": 354,\n        \"ams\": 201,\n        \"iad\": 182,\n        \"syd\": 640,\n        \"jnb\": 817,\n        \"gru\": 700\n      },\n      {\n        \"timestamp\": \"Feb 13, 18:00\",\n        \"ams\": 155,\n        \"iad\": 190,\n        \"jnb\": 815,\n        \"syd\": 651,\n        \"gru\": 691,\n        \"hkg\": 350\n      },\n      {\n        \"timestamp\": \"Feb 13, 19:00\",\n        \"jnb\": 872,\n        \"syd\": 651,\n        \"ams\": 169,\n        \"iad\": 177,\n        \"gru\": 708,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 13, 20:00\",\n        \"gru\": 696,\n        \"syd\": 604,\n        \"jnb\": 948,\n        \"ams\": 168,\n        \"iad\": 186,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 13, 21:00\",\n        \"iad\": 180,\n        \"ams\": 148,\n        \"jnb\": 924,\n        \"syd\": 793,\n        \"gru\": 681,\n        \"hkg\": 366\n      },\n      {\n        \"timestamp\": \"Feb 13, 22:00\",\n        \"gru\": 695,\n        \"iad\": 168,\n        \"ams\": 166,\n        \"syd\": 726,\n        \"jnb\": 1160,\n        \"hkg\": 344\n      },\n      {\n        \"timestamp\": \"Feb 13, 23:00\",\n        \"hkg\": 325,\n        \"gru\": 660,\n        \"iad\": 171,\n        \"ams\": 135,\n        \"syd\": 717,\n        \"jnb\": 823\n      },\n      {\n        \"timestamp\": \"Feb 14, 00:00\",\n        \"hkg\": 343,\n        \"iad\": 167,\n        \"ams\": 125,\n        \"jnb\": 838,\n        \"syd\": 671,\n        \"gru\": 696\n      },\n      {\n        \"timestamp\": \"Feb 14, 01:00\",\n        \"hkg\": 351,\n        \"gru\": 770,\n        \"iad\": 173,\n        \"ams\": 137,\n        \"syd\": 768,\n        \"jnb\": 898\n      },\n      {\n        \"timestamp\": \"Feb 14, 02:00\",\n        \"hkg\": 350,\n        \"iad\": 171,\n        \"ams\": 119,\n        \"jnb\": 784,\n        \"syd\": 720,\n        \"gru\": 637\n      },\n      {\n        \"timestamp\": \"Feb 14, 03:00\",\n        \"hkg\": 348,\n        \"jnb\": 679,\n        \"syd\": 716,\n        \"iad\": 164,\n        \"ams\": 133,\n        \"gru\": 683\n      },\n      {\n        \"timestamp\": \"Feb 14, 04:00\",\n        \"syd\": 823,\n        \"jnb\": 803,\n        \"iad\": 169,\n        \"ams\": 126,\n        \"gru\": 688,\n        \"hkg\": 362\n      },\n      {\n        \"timestamp\": \"Feb 14, 05:00\",\n        \"gru\": 660,\n        \"jnb\": 790,\n        \"syd\": 654,\n        \"ams\": 126,\n        \"iad\": 168,\n        \"hkg\": 334\n      },\n      {\n        \"timestamp\": \"Feb 14, 06:00\",\n        \"ams\": 127,\n        \"iad\": 163,\n        \"syd\": 653,\n        \"jnb\": 860,\n        \"gru\": 682,\n        \"hkg\": 354\n      },\n      {\n        \"timestamp\": \"Feb 14, 07:00\",\n        \"gru\": 644,\n        \"ams\": 120,\n        \"iad\": 163,\n        \"syd\": 730,\n        \"jnb\": 833,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 14, 08:00\",\n        \"hkg\": 348,\n        \"gru\": 700,\n        \"ams\": 128,\n        \"iad\": 165,\n        \"jnb\": 872,\n        \"syd\": 687\n      },\n      {\n        \"timestamp\": \"Feb 14, 09:00\",\n        \"iad\": 165,\n        \"ams\": 141,\n        \"jnb\": 841,\n        \"syd\": 645,\n        \"gru\": 675,\n        \"hkg\": 348\n      },\n      {\n        \"timestamp\": \"Feb 14, 10:00\",\n        \"hkg\": 354,\n        \"gru\": 703,\n        \"iad\": 162,\n        \"ams\": 151,\n        \"jnb\": 1348,\n        \"syd\": 730\n      },\n      {\n        \"timestamp\": \"Feb 14, 11:00\",\n        \"hkg\": 350,\n        \"gru\": 708,\n        \"jnb\": 1546,\n        \"syd\": 680,\n        \"iad\": 166,\n        \"ams\": 140\n      },\n      {\n        \"timestamp\": \"Feb 14, 12:00\",\n        \"hkg\": 361,\n        \"syd\": 630,\n        \"jnb\": 1355,\n        \"iad\": 170,\n        \"ams\": 156,\n        \"gru\": 682\n      },\n      {\n        \"timestamp\": \"Feb 14, 13:00\",\n        \"jnb\": 1374,\n        \"syd\": 673,\n        \"ams\": 135,\n        \"iad\": 168,\n        \"gru\": 684,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 14, 14:00\",\n        \"gru\": 700,\n        \"syd\": 641,\n        \"jnb\": 1362,\n        \"ams\": 150,\n        \"iad\": 170,\n        \"hkg\": 369\n      },\n      {\n        \"timestamp\": \"Feb 14, 15:00\",\n        \"ams\": 128,\n        \"iad\": 172,\n        \"syd\": 6737,\n        \"jnb\": 1423,\n        \"gru\": 690,\n        \"hkg\": 369\n      },\n      {\n        \"timestamp\": \"Feb 14, 16:00\",\n        \"gru\": 744,\n        \"ams\": 148,\n        \"iad\": 170,\n        \"jnb\": 1438,\n        \"syd\": 5441,\n        \"hkg\": 361\n      },\n      {\n        \"timestamp\": \"Feb 14, 17:00\",\n        \"hkg\": 340,\n        \"ams\": 156,\n        \"iad\": 171,\n        \"jnb\": 1440,\n        \"syd\": 821,\n        \"gru\": 660\n      },\n      {\n        \"timestamp\": \"Feb 14, 18:00\",\n        \"hkg\": 365,\n        \"gru\": 697,\n        \"ams\": 158,\n        \"iad\": 172,\n        \"syd\": 639,\n        \"jnb\": 1550\n      },\n      {\n        \"timestamp\": \"Feb 14, 19:00\",\n        \"hkg\": 350,\n        \"gru\": 693,\n        \"syd\": 595,\n        \"jnb\": 1537,\n        \"ams\": 165,\n        \"iad\": 170\n      },\n      {\n        \"timestamp\": \"Feb 14, 20:00\",\n        \"gru\": 643,\n        \"syd\": 749,\n        \"jnb\": 1497,\n        \"iad\": 176,\n        \"ams\": 165,\n        \"hkg\": 364\n      },\n      {\n        \"timestamp\": \"Feb 14, 21:00\",\n        \"jnb\": 1487,\n        \"syd\": 632,\n        \"ams\": 200,\n        \"iad\": 185,\n        \"gru\": 683,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 14, 22:00\",\n        \"hkg\": 336,\n        \"syd\": 779,\n        \"jnb\": 1302,\n        \"ams\": 223,\n        \"iad\": 178,\n        \"gru\": 716\n      },\n      {\n        \"timestamp\": \"Feb 14, 23:00\",\n        \"hkg\": 347,\n        \"gru\": 746,\n        \"syd\": 673,\n        \"jnb\": 1548,\n        \"ams\": 166,\n        \"iad\": 166\n      },\n      {\n        \"timestamp\": \"Feb 15, 00:00\",\n        \"hkg\": 335,\n        \"ams\": 159,\n        \"iad\": 172,\n        \"jnb\": 953,\n        \"syd\": 704,\n        \"gru\": 675\n      },\n      {\n        \"timestamp\": \"Feb 15, 01:00\",\n        \"hkg\": 335,\n        \"gru\": 670,\n        \"iad\": 168,\n        \"ams\": 214,\n        \"syd\": 590,\n        \"jnb\": 978\n      },\n      {\n        \"timestamp\": \"Feb 15, 02:00\",\n        \"gru\": 731,\n        \"syd\": 652,\n        \"jnb\": 824,\n        \"iad\": 163,\n        \"ams\": 124,\n        \"hkg\": 364\n      },\n      {\n        \"timestamp\": \"Feb 15, 03:00\",\n        \"jnb\": 882,\n        \"syd\": 749,\n        \"ams\": 155,\n        \"iad\": 171,\n        \"gru\": 691,\n        \"hkg\": 356\n      },\n      {\n        \"timestamp\": \"Feb 15, 04:00\",\n        \"iad\": 163,\n        \"ams\": 170,\n        \"jnb\": 878,\n        \"syd\": 676,\n        \"gru\": 680,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 15, 05:00\",\n        \"hkg\": 346,\n        \"gru\": 708,\n        \"syd\": 679,\n        \"jnb\": 883,\n        \"ams\": 160,\n        \"iad\": 167\n      },\n      {\n        \"timestamp\": \"Feb 15, 06:00\",\n        \"hkg\": 377,\n        \"ams\": 131,\n        \"iad\": 172,\n        \"syd\": 676,\n        \"jnb\": 910,\n        \"gru\": 726\n      },\n      {\n        \"timestamp\": \"Feb 15, 07:00\",\n        \"iad\": 169,\n        \"ams\": 135,\n        \"jnb\": 925,\n        \"syd\": 700,\n        \"gru\": 702,\n        \"hkg\": 387\n      },\n      {\n        \"timestamp\": \"Feb 15, 08:00\",\n        \"gru\": 718,\n        \"ams\": 154,\n        \"iad\": 162,\n        \"jnb\": 928,\n        \"syd\": 791,\n        \"hkg\": 367\n      },\n      {\n        \"timestamp\": \"Feb 15, 09:00\",\n        \"gru\": 706,\n        \"ams\": 161,\n        \"iad\": 160,\n        \"syd\": 663,\n        \"jnb\": 942,\n        \"hkg\": 397\n      },\n      {\n        \"timestamp\": \"Feb 15, 10:00\",\n        \"syd\": 802,\n        \"jnb\": 832,\n        \"ams\": 151,\n        \"iad\": 174,\n        \"gru\": 686,\n        \"hkg\": 372\n      },\n      {\n        \"timestamp\": \"Feb 15, 11:00\",\n        \"hkg\": 365,\n        \"jnb\": 864,\n        \"syd\": 656,\n        \"iad\": 158,\n        \"ams\": 154,\n        \"gru\": 691\n      },\n      {\n        \"timestamp\": \"Feb 15, 12:00\",\n        \"hkg\": 382,\n        \"ams\": 150,\n        \"iad\": 162,\n        \"jnb\": 910,\n        \"syd\": 616,\n        \"gru\": 685\n      },\n      {\n        \"timestamp\": \"Feb 15, 13:00\",\n        \"hkg\": 374,\n        \"gru\": 687,\n        \"jnb\": 897,\n        \"syd\": 601,\n        \"iad\": 164,\n        \"ams\": 142\n      },\n      {\n        \"timestamp\": \"Feb 15, 14:00\",\n        \"hkg\": 368,\n        \"gru\": 680,\n        \"jnb\": 903,\n        \"syd\": 731,\n        \"ams\": 130,\n        \"iad\": 174\n      },\n      {\n        \"timestamp\": \"Feb 15, 15:00\",\n        \"hkg\": 361,\n        \"syd\": 622,\n        \"jnb\": 859,\n        \"iad\": 171,\n        \"ams\": 160,\n        \"gru\": 665\n      },\n      {\n        \"timestamp\": \"Feb 15, 16:00\",\n        \"syd\": 584,\n        \"jnb\": 788,\n        \"ams\": 155,\n        \"iad\": 170,\n        \"gru\": 692,\n        \"hkg\": 355\n      },\n      {\n        \"timestamp\": \"Feb 17, 10:00\",\n        \"hkg\": 370,\n        \"iad\": 170,\n        \"ams\": 229,\n        \"syd\": 834,\n        \"jnb\": 1435,\n        \"gru\": 904\n      },\n      {\n        \"timestamp\": \"Feb 17, 11:00\",\n        \"gru\": 650,\n        \"jnb\": 1493,\n        \"syd\": 762,\n        \"ams\": 135,\n        \"iad\": 184,\n        \"hkg\": 356\n      },\n      {\n        \"timestamp\": \"Feb 17, 12:00\",\n        \"syd\": 748,\n        \"jnb\": 1436,\n        \"ams\": 158,\n        \"iad\": 182,\n        \"gru\": 736,\n        \"hkg\": 343\n      },\n      {\n        \"timestamp\": \"Feb 17, 13:00\",\n        \"jnb\": 1375,\n        \"syd\": 664,\n        \"ams\": 154,\n        \"iad\": 181,\n        \"gru\": 658,\n        \"hkg\": 365\n      },\n      {\n        \"timestamp\": \"Feb 17, 14:00\",\n        \"hkg\": 364,\n        \"syd\": 562,\n        \"jnb\": 1404,\n        \"ams\": 160,\n        \"iad\": 178,\n        \"gru\": 683\n      },\n      {\n        \"timestamp\": \"Feb 17, 15:00\",\n        \"hkg\": 357,\n        \"gru\": 635,\n        \"iad\": 175,\n        \"ams\": 152,\n        \"syd\": 728,\n        \"jnb\": 1522\n      },\n      {\n        \"timestamp\": \"Feb 17, 16:00\",\n        \"hkg\": 351,\n        \"jnb\": 1239,\n        \"syd\": 601,\n        \"iad\": 168,\n        \"ams\": 137,\n        \"gru\": 714\n      },\n      {\n        \"timestamp\": \"Feb 17, 17:00\",\n        \"hkg\": 358,\n        \"gru\": 679,\n        \"syd\": 699,\n        \"jnb\": 1457,\n        \"ams\": 139,\n        \"iad\": 174\n      },\n      {\n        \"timestamp\": \"Feb 17, 18:00\",\n        \"gru\": 645,\n        \"syd\": 719,\n        \"jnb\": 1445,\n        \"iad\": 173,\n        \"ams\": 153,\n        \"hkg\": 350\n      },\n      {\n        \"timestamp\": \"Feb 17, 19:00\",\n        \"jnb\": 1639,\n        \"syd\": 675,\n        \"ams\": 157,\n        \"iad\": 173,\n        \"gru\": 685,\n        \"hkg\": 334\n      },\n      {\n        \"timestamp\": \"Feb 17, 20:00\",\n        \"hkg\": 320,\n        \"iad\": 172,\n        \"ams\": 152,\n        \"syd\": 657,\n        \"jnb\": 1386,\n        \"gru\": 686\n      },\n      {\n        \"timestamp\": \"Feb 17, 21:00\",\n        \"hkg\": 337,\n        \"gru\": 685,\n        \"syd\": 604,\n        \"jnb\": 1509,\n        \"ams\": 151,\n        \"iad\": 171\n      },\n      {\n        \"timestamp\": \"Feb 17, 22:00\",\n        \"hkg\": 363,\n        \"gru\": 658,\n        \"iad\": 173,\n        \"ams\": 136,\n        \"syd\": 758,\n        \"jnb\": 1281\n      },\n      {\n        \"timestamp\": \"Feb 17, 23:00\",\n        \"gru\": 672,\n        \"ams\": 118,\n        \"iad\": 166,\n        \"jnb\": 1456,\n        \"syd\": 668,\n        \"hkg\": 329\n      },\n      {\n        \"timestamp\": \"Feb 18, 00:00\",\n        \"jnb\": 1198,\n        \"syd\": 664,\n        \"ams\": 124,\n        \"iad\": 169,\n        \"gru\": 650,\n        \"hkg\": 337\n      },\n      {\n        \"timestamp\": \"Feb 18, 01:00\",\n        \"syd\": 631,\n        \"jnb\": 1305,\n        \"ams\": 128,\n        \"iad\": 163,\n        \"gru\": 641,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 18, 02:00\",\n        \"gru\": 704,\n        \"syd\": 682,\n        \"jnb\": 1412,\n        \"iad\": 164,\n        \"ams\": 119,\n        \"hkg\": 340\n      },\n      {\n        \"timestamp\": \"Feb 18, 03:00\",\n        \"hkg\": 357,\n        \"gru\": 690,\n        \"jnb\": 1329,\n        \"syd\": 714,\n        \"ams\": 134,\n        \"iad\": 160\n      },\n      {\n        \"timestamp\": \"Feb 18, 04:00\",\n        \"hkg\": 334,\n        \"ams\": 128,\n        \"iad\": 162,\n        \"jnb\": 1252,\n        \"syd\": 720,\n        \"gru\": 632\n      },\n      {\n        \"timestamp\": \"Feb 18, 05:00\",\n        \"gru\": 664,\n        \"syd\": 659,\n        \"jnb\": 1445,\n        \"iad\": 172,\n        \"ams\": 125,\n        \"hkg\": 343\n      },\n      {\n        \"timestamp\": \"Feb 18, 06:00\",\n        \"gru\": 639,\n        \"ams\": 136,\n        \"iad\": 158,\n        \"syd\": 615,\n        \"jnb\": 1291,\n        \"hkg\": 355\n      },\n      {\n        \"timestamp\": \"Feb 18, 07:00\",\n        \"iad\": 161,\n        \"ams\": 128,\n        \"jnb\": 1240,\n        \"syd\": 724,\n        \"gru\": 654,\n        \"hkg\": 368\n      },\n      {\n        \"timestamp\": \"Feb 18, 08:00\",\n        \"hkg\": 366,\n        \"jnb\": 1442,\n        \"syd\": 706,\n        \"iad\": 159,\n        \"ams\": 145,\n        \"gru\": 668\n      },\n      {\n        \"timestamp\": \"Feb 18, 09:00\",\n        \"gru\": 657,\n        \"ams\": 136,\n        \"iad\": 158,\n        \"syd\": 699,\n        \"jnb\": 1166,\n        \"hkg\": 354\n      },\n      {\n        \"timestamp\": \"Feb 18, 10:00\",\n        \"syd\": 855,\n        \"jnb\": 1591,\n        \"iad\": 158,\n        \"ams\": 132,\n        \"gru\": 622,\n        \"hkg\": 336\n      },\n      {\n        \"timestamp\": \"Feb 18, 11:00\",\n        \"gru\": 663,\n        \"jnb\": 1461,\n        \"syd\": 798,\n        \"ams\": 132,\n        \"iad\": 154,\n        \"hkg\": 366\n      },\n      {\n        \"timestamp\": \"Feb 18, 12:00\",\n        \"hkg\": 331,\n        \"gru\": 697,\n        \"ams\": 146,\n        \"iad\": 157,\n        \"jnb\": 1483,\n        \"syd\": 563\n      },\n      {\n        \"timestamp\": \"Feb 18, 13:00\",\n        \"hkg\": 363,\n        \"jnb\": 1547,\n        \"syd\": 855,\n        \"iad\": 160,\n        \"ams\": 165,\n        \"gru\": 712\n      },\n      {\n        \"timestamp\": \"Feb 18, 14:00\",\n        \"hkg\": 355,\n        \"ams\": 162,\n        \"iad\": 166,\n        \"jnb\": 1407,\n        \"syd\": 631,\n        \"gru\": 746\n      },\n      {\n        \"timestamp\": \"Feb 18, 15:00\",\n        \"hkg\": 346,\n        \"syd\": 764,\n        \"jnb\": 1550,\n        \"ams\": 187,\n        \"iad\": 167,\n        \"gru\": 779\n      },\n      {\n        \"timestamp\": \"Feb 18, 16:00\",\n        \"hkg\": 344,\n        \"gru\": 697,\n        \"ams\": 166,\n        \"iad\": 175,\n        \"syd\": 711,\n        \"jnb\": 1298\n      },\n      {\n        \"timestamp\": \"Feb 18, 17:00\",\n        \"jnb\": 1252,\n        \"syd\": 740,\n        \"ams\": 208,\n        \"iad\": 176,\n        \"gru\": 730,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 18, 18:00\",\n        \"gru\": 677,\n        \"jnb\": 1442,\n        \"syd\": 689,\n        \"iad\": 167,\n        \"ams\": 154,\n        \"hkg\": 370\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"syd\",\n      \"avgLatency\": 691,\n      \"p75Latency\": 825,\n      \"p90Latency\": 862,\n      \"p95Latency\": 869,\n      \"p99Latency\": 915\n    },\n    {\n      \"region\": \"gru\",\n      \"avgLatency\": 685,\n      \"p75Latency\": 705,\n      \"p90Latency\": 781,\n      \"p95Latency\": 815,\n      \"p99Latency\": 989\n    },\n    {\n      \"region\": \"iad\",\n      \"avgLatency\": 168,\n      \"p75Latency\": 173,\n      \"p90Latency\": 186,\n      \"p95Latency\": 195,\n      \"p99Latency\": 213\n    },\n    {\n      \"region\": \"ams\",\n      \"avgLatency\": 148,\n      \"p75Latency\": 158,\n      \"p90Latency\": 206,\n      \"p95Latency\": 225,\n      \"p99Latency\": 250\n    },\n    {\n      \"region\": \"hkg\",\n      \"avgLatency\": 356,\n      \"p75Latency\": 372,\n      \"p90Latency\": 386,\n      \"p95Latency\": 395,\n      \"p99Latency\": 416\n    },\n    {\n      \"region\": \"jnb\",\n      \"avgLatency\": 1229,\n      \"p75Latency\": 1532,\n      \"p90Latency\": 1685,\n      \"p95Latency\": 1744,\n      \"p99Latency\": 1812\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/monitoring-latency/railway.json",
    "content": "{\n  \"regions\": [\"ams\", \"iad\", \"hkg\", \"jnb\", \"syd\", \"gru\"],\n  \"data\": {\n    \"regions\": [\"ams\", \"gru\", \"hkg\", \"iad\", \"jnb\", \"syd\"],\n    \"data\": [\n      {\n        \"timestamp\": \"Feb 4, 00:00\",\n        \"ams\": 176,\n        \"iad\": 84,\n        \"syd\": 487,\n        \"jnb\": 659,\n        \"gru\": 456,\n        \"hkg\": 292\n      },\n      {\n        \"timestamp\": \"Feb 4, 01:00\",\n        \"ams\": 176,\n        \"iad\": 84,\n        \"syd\": 497,\n        \"jnb\": 691,\n        \"gru\": 437,\n        \"hkg\": 299\n      },\n      {\n        \"timestamp\": \"Feb 4, 02:00\",\n        \"hkg\": 293,\n        \"ams\": 183,\n        \"iad\": 88,\n        \"jnb\": 665,\n        \"syd\": 504,\n        \"gru\": 418\n      },\n      {\n        \"timestamp\": \"Feb 4, 03:00\",\n        \"hkg\": 304,\n        \"jnb\": 680,\n        \"syd\": 463,\n        \"ams\": 175,\n        \"iad\": 80,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 4, 04:00\",\n        \"hkg\": 301,\n        \"gru\": 428,\n        \"syd\": 517,\n        \"jnb\": 672,\n        \"ams\": 173,\n        \"iad\": 80\n      },\n      {\n        \"timestamp\": \"Feb 4, 05:00\",\n        \"gru\": 427,\n        \"syd\": 500,\n        \"jnb\": 657,\n        \"ams\": 174,\n        \"iad\": 84,\n        \"hkg\": 291\n      },\n      {\n        \"timestamp\": \"Feb 4, 06:00\",\n        \"jnb\": 849,\n        \"syd\": 526,\n        \"ams\": 179,\n        \"iad\": 90,\n        \"gru\": 417,\n        \"hkg\": 288\n      },\n      {\n        \"timestamp\": \"Feb 4, 07:00\",\n        \"hkg\": 304,\n        \"jnb\": 656,\n        \"syd\": 471,\n        \"ams\": 210,\n        \"iad\": 82,\n        \"gru\": 418\n      },\n      {\n        \"timestamp\": \"Feb 4, 08:00\",\n        \"hkg\": 303,\n        \"gru\": 438,\n        \"syd\": 463,\n        \"jnb\": 688,\n        \"ams\": 198,\n        \"iad\": 90\n      },\n      {\n        \"timestamp\": \"Feb 4, 09:00\",\n        \"hkg\": 372,\n        \"iad\": 84,\n        \"ams\": 182,\n        \"jnb\": 657,\n        \"syd\": 512,\n        \"gru\": 422\n      },\n      {\n        \"timestamp\": \"Feb 4, 10:00\",\n        \"hkg\": 302,\n        \"gru\": 419,\n        \"jnb\": 687,\n        \"syd\": 488,\n        \"iad\": 86,\n        \"ams\": 204\n      },\n      {\n        \"timestamp\": \"Feb 4, 11:00\",\n        \"gru\": 417,\n        \"syd\": 483,\n        \"jnb\": 659,\n        \"ams\": 177,\n        \"iad\": 83,\n        \"hkg\": 317\n      },\n      {\n        \"timestamp\": \"Feb 4, 12:00\",\n        \"gru\": 462,\n        \"iad\": 82,\n        \"ams\": 177,\n        \"syd\": 502,\n        \"jnb\": 694,\n        \"hkg\": 305\n      },\n      {\n        \"timestamp\": \"Feb 4, 13:00\",\n        \"syd\": 486,\n        \"jnb\": 657,\n        \"ams\": 184,\n        \"iad\": 88,\n        \"gru\": 437,\n        \"hkg\": 298\n      },\n      {\n        \"timestamp\": \"Feb 4, 14:00\",\n        \"hkg\": 292,\n        \"gru\": 424,\n        \"ams\": 196,\n        \"iad\": 78,\n        \"jnb\": 693,\n        \"syd\": 465\n      },\n      {\n        \"timestamp\": \"Feb 4, 15:00\",\n        \"hkg\": 295,\n        \"iad\": 79,\n        \"ams\": 181,\n        \"syd\": 460,\n        \"jnb\": 694,\n        \"gru\": 436\n      },\n      {\n        \"timestamp\": \"Feb 4, 16:00\",\n        \"ams\": 174,\n        \"iad\": 79,\n        \"syd\": 502,\n        \"jnb\": 660,\n        \"gru\": 425,\n        \"hkg\": 287\n      },\n      {\n        \"timestamp\": \"Feb 4, 17:00\",\n        \"jnb\": 657,\n        \"syd\": 513,\n        \"ams\": 200,\n        \"iad\": 80,\n        \"gru\": 416,\n        \"hkg\": 316\n      },\n      {\n        \"timestamp\": \"Feb 4, 18:00\",\n        \"gru\": 427,\n        \"jnb\": 661,\n        \"syd\": 470,\n        \"ams\": 178,\n        \"iad\": 82,\n        \"hkg\": 299\n      },\n      {\n        \"timestamp\": \"Feb 4, 19:00\",\n        \"syd\": 466,\n        \"jnb\": 708,\n        \"iad\": 85,\n        \"ams\": 179,\n        \"gru\": 422,\n        \"hkg\": 286\n      },\n      {\n        \"timestamp\": \"Feb 4, 20:00\",\n        \"ams\": 176,\n        \"iad\": 81,\n        \"syd\": 462,\n        \"jnb\": 692,\n        \"gru\": 440,\n        \"hkg\": 285\n      },\n      {\n        \"timestamp\": \"Feb 4, 21:00\",\n        \"hkg\": 305,\n        \"syd\": 514,\n        \"jnb\": 658,\n        \"iad\": 88,\n        \"ams\": 178,\n        \"gru\": 417\n      },\n      {\n        \"timestamp\": \"Feb 4, 22:00\",\n        \"hkg\": 302,\n        \"gru\": 417,\n        \"iad\": 90,\n        \"ams\": 178,\n        \"syd\": 462,\n        \"jnb\": 680\n      },\n      {\n        \"timestamp\": \"Feb 4, 23:00\",\n        \"jnb\": 685,\n        \"syd\": 528,\n        \"iad\": 83,\n        \"ams\": 181,\n        \"gru\": 438,\n        \"hkg\": 302\n      },\n      {\n        \"timestamp\": \"Feb 5, 00:00\",\n        \"gru\": 416,\n        \"jnb\": 677,\n        \"syd\": 520,\n        \"ams\": 178,\n        \"iad\": 81,\n        \"hkg\": 286\n      },\n      {\n        \"timestamp\": \"Feb 5, 01:00\",\n        \"gru\": 416,\n        \"iad\": 80,\n        \"ams\": 196,\n        \"syd\": 462,\n        \"jnb\": 682,\n        \"hkg\": 319\n      },\n      {\n        \"timestamp\": \"Feb 5, 02:00\",\n        \"syd\": 478,\n        \"jnb\": 660,\n        \"iad\": 81,\n        \"ams\": 180,\n        \"gru\": 415,\n        \"hkg\": 291\n      },\n      {\n        \"timestamp\": \"Feb 5, 03:00\",\n        \"gru\": 416,\n        \"ams\": 174,\n        \"iad\": 78,\n        \"jnb\": 657,\n        \"syd\": 461,\n        \"hkg\": 287\n      },\n      {\n        \"timestamp\": \"Feb 5, 04:00\",\n        \"gru\": 416,\n        \"jnb\": 657,\n        \"syd\": 488,\n        \"iad\": 84,\n        \"ams\": 178,\n        \"hkg\": 304\n      },\n      {\n        \"timestamp\": \"Feb 5, 05:00\",\n        \"ams\": 198,\n        \"iad\": 79,\n        \"jnb\": 685,\n        \"syd\": 486,\n        \"gru\": 423,\n        \"hkg\": 290\n      },\n      {\n        \"timestamp\": \"Feb 5, 06:00\",\n        \"hkg\": 288,\n        \"jnb\": 682,\n        \"syd\": 458,\n        \"ams\": 180,\n        \"iad\": 89,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 5, 07:00\",\n        \"hkg\": 307,\n        \"gru\": 447,\n        \"syd\": 470,\n        \"jnb\": 657,\n        \"iad\": 85,\n        \"ams\": 189\n      },\n      {\n        \"timestamp\": \"Feb 5, 08:00\",\n        \"jnb\": 660,\n        \"syd\": 466,\n        \"ams\": 184,\n        \"iad\": 79,\n        \"gru\": 416,\n        \"hkg\": 292\n      },\n      {\n        \"timestamp\": \"Feb 5, 09:00\",\n        \"ams\": 178,\n        \"iad\": 89,\n        \"syd\": 472,\n        \"jnb\": 738,\n        \"gru\": 436,\n        \"hkg\": 306\n      },\n      {\n        \"timestamp\": \"Feb 5, 10:00\",\n        \"hkg\": 310,\n        \"syd\": 505,\n        \"jnb\": 698,\n        \"ams\": 178,\n        \"iad\": 237,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 5, 11:00\",\n        \"hkg\": 319,\n        \"gru\": 416,\n        \"jnb\": 664,\n        \"syd\": 465,\n        \"iad\": 84,\n        \"ams\": 203\n      },\n      {\n        \"timestamp\": \"Feb 5, 12:00\",\n        \"hkg\": 302,\n        \"gru\": 425,\n        \"ams\": 201,\n        \"iad\": 84,\n        \"jnb\": 701,\n        \"syd\": 468\n      },\n      {\n        \"timestamp\": \"Feb 5, 13:00\",\n        \"hkg\": 311,\n        \"iad\": 84,\n        \"ams\": 204,\n        \"jnb\": 690,\n        \"syd\": 482,\n        \"gru\": 425\n      },\n      {\n        \"timestamp\": \"Feb 5, 14:00\",\n        \"hkg\": 289,\n        \"gru\": 417,\n        \"jnb\": 678,\n        \"syd\": 466,\n        \"iad\": 84,\n        \"ams\": 179\n      },\n      {\n        \"timestamp\": \"Feb 5, 15:00\",\n        \"gru\": 489,\n        \"syd\": 465,\n        \"jnb\": 882,\n        \"ams\": 202,\n        \"iad\": 154,\n        \"hkg\": 417\n      },\n      {\n        \"timestamp\": \"Feb 5, 16:00\",\n        \"syd\": 464,\n        \"jnb\": 690,\n        \"iad\": 83,\n        \"ams\": 182,\n        \"gru\": 418,\n        \"hkg\": 295\n      },\n      {\n        \"timestamp\": \"Feb 5, 17:00\",\n        \"hkg\": 294,\n        \"syd\": 483,\n        \"jnb\": 660,\n        \"iad\": 82,\n        \"ams\": 178,\n        \"gru\": 522\n      },\n      {\n        \"timestamp\": \"Feb 5, 18:00\",\n        \"hkg\": 289,\n        \"gru\": 418,\n        \"jnb\": 710,\n        \"syd\": 464,\n        \"iad\": 82,\n        \"ams\": 198\n      },\n      {\n        \"timestamp\": \"Feb 5, 19:00\",\n        \"gru\": 419,\n        \"ams\": 185,\n        \"iad\": 84,\n        \"jnb\": 659,\n        \"syd\": 494,\n        \"hkg\": 310\n      },\n      {\n        \"timestamp\": \"Feb 5, 20:00\",\n        \"ams\": 177,\n        \"iad\": 82,\n        \"syd\": 461,\n        \"jnb\": 663,\n        \"gru\": 460,\n        \"hkg\": 284\n      },\n      {\n        \"timestamp\": \"Feb 5, 21:00\",\n        \"ams\": 192,\n        \"iad\": 83,\n        \"jnb\": 680,\n        \"syd\": 494,\n        \"gru\": 454,\n        \"hkg\": 289\n      },\n      {\n        \"timestamp\": \"Feb 5, 22:00\",\n        \"gru\": 424,\n        \"syd\": 464,\n        \"jnb\": 658,\n        \"ams\": 188,\n        \"iad\": 83,\n        \"hkg\": 290\n      },\n      {\n        \"timestamp\": \"Feb 5, 23:00\",\n        \"iad\": 82,\n        \"ams\": 201,\n        \"syd\": 466,\n        \"jnb\": 691,\n        \"gru\": 457,\n        \"hkg\": 366\n      },\n      {\n        \"timestamp\": \"Feb 6, 00:00\",\n        \"hkg\": 289,\n        \"iad\": 80,\n        \"ams\": 180,\n        \"jnb\": 656,\n        \"syd\": 486,\n        \"gru\": 438\n      },\n      {\n        \"timestamp\": \"Feb 6, 01:00\",\n        \"hkg\": 287,\n        \"jnb\": 692,\n        \"syd\": 464,\n        \"ams\": 179,\n        \"iad\": 83,\n        \"gru\": 417\n      },\n      {\n        \"timestamp\": \"Feb 6, 02:00\",\n        \"gru\": 460,\n        \"jnb\": 657,\n        \"syd\": 479,\n        \"ams\": 185,\n        \"iad\": 88,\n        \"hkg\": 313\n      },\n      {\n        \"timestamp\": \"Feb 6, 03:00\",\n        \"iad\": 77,\n        \"ams\": 185,\n        \"jnb\": 700,\n        \"syd\": 467,\n        \"gru\": 420,\n        \"hkg\": 293\n      },\n      {\n        \"timestamp\": \"Feb 6, 04:00\",\n        \"hkg\": 296,\n        \"iad\": 85,\n        \"ams\": 168,\n        \"syd\": 514,\n        \"jnb\": 652,\n        \"gru\": 435\n      },\n      {\n        \"timestamp\": \"Feb 6, 05:00\",\n        \"hkg\": 322,\n        \"iad\": 82,\n        \"ams\": 166,\n        \"jnb\": 651,\n        \"syd\": 471,\n        \"gru\": 423\n      },\n      {\n        \"timestamp\": \"Feb 6, 06:00\",\n        \"hkg\": 295,\n        \"gru\": 450,\n        \"iad\": 78,\n        \"ams\": 168,\n        \"syd\": 504,\n        \"jnb\": 650\n      },\n      {\n        \"timestamp\": \"Feb 6, 07:00\",\n        \"hkg\": 291,\n        \"ams\": 167,\n        \"iad\": 78,\n        \"syd\": 466,\n        \"jnb\": 651,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 6, 08:00\",\n        \"hkg\": 294,\n        \"gru\": 415,\n        \"ams\": 178,\n        \"iad\": 80,\n        \"jnb\": 651,\n        \"syd\": 465\n      },\n      {\n        \"timestamp\": \"Feb 6, 09:00\",\n        \"hkg\": 296,\n        \"gru\": 464,\n        \"jnb\": 651,\n        \"syd\": 480,\n        \"ams\": 170,\n        \"iad\": 77\n      },\n      {\n        \"timestamp\": \"Feb 6, 10:00\",\n        \"hkg\": 288,\n        \"syd\": 483,\n        \"jnb\": 651,\n        \"ams\": 186,\n        \"iad\": 79,\n        \"gru\": 434\n      },\n      {\n        \"timestamp\": \"Feb 6, 11:00\",\n        \"hkg\": 289,\n        \"syd\": 461,\n        \"jnb\": 653,\n        \"ams\": 169,\n        \"iad\": 82,\n        \"gru\": 438\n      },\n      {\n        \"timestamp\": \"Feb 6, 12:00\",\n        \"hkg\": 302,\n        \"gru\": 417,\n        \"jnb\": 680,\n        \"syd\": 500,\n        \"ams\": 170,\n        \"iad\": 87\n      },\n      {\n        \"timestamp\": \"Feb 6, 13:00\",\n        \"hkg\": 299,\n        \"gru\": 438,\n        \"syd\": 480,\n        \"jnb\": 650,\n        \"ams\": 167,\n        \"iad\": 78\n      },\n      {\n        \"timestamp\": \"Feb 6, 14:00\",\n        \"gru\": 419,\n        \"jnb\": 652,\n        \"syd\": 461,\n        \"ams\": 166,\n        \"iad\": 100,\n        \"hkg\": 300\n      },\n      {\n        \"timestamp\": \"Feb 6, 15:00\",\n        \"iad\": 78,\n        \"ams\": 168,\n        \"jnb\": 672,\n        \"syd\": 465,\n        \"gru\": 477,\n        \"hkg\": 343\n      },\n      {\n        \"timestamp\": \"Feb 6, 16:00\",\n        \"gru\": 418,\n        \"syd\": 487,\n        \"jnb\": 655,\n        \"iad\": 83,\n        \"ams\": 179,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Feb 6, 17:00\",\n        \"gru\": 426,\n        \"ams\": 183,\n        \"iad\": 87,\n        \"syd\": 481,\n        \"jnb\": 648,\n        \"hkg\": 301\n      },\n      {\n        \"timestamp\": \"Feb 6, 18:00\",\n        \"hkg\": 308,\n        \"gru\": 498,\n        \"ams\": 184,\n        \"iad\": 95,\n        \"jnb\": 734,\n        \"syd\": 483\n      },\n      {\n        \"timestamp\": \"Feb 6, 19:00\",\n        \"hkg\": 300,\n        \"jnb\": 677,\n        \"syd\": 460,\n        \"iad\": 79,\n        \"ams\": 171,\n        \"gru\": 437\n      },\n      {\n        \"timestamp\": \"Feb 6, 20:00\",\n        \"gru\": 441,\n        \"jnb\": 684,\n        \"syd\": 480,\n        \"iad\": 80,\n        \"ams\": 179,\n        \"hkg\": 314\n      },\n      {\n        \"timestamp\": \"Feb 6, 21:00\",\n        \"gru\": 434,\n        \"syd\": 508,\n        \"jnb\": 684,\n        \"iad\": 102,\n        \"ams\": 175,\n        \"hkg\": 308\n      },\n      {\n        \"timestamp\": \"Feb 6, 22:00\",\n        \"jnb\": 706,\n        \"syd\": 5418,\n        \"iad\": 1044,\n        \"ams\": 181,\n        \"gru\": 2103,\n        \"hkg\": 4239\n      },\n      {\n        \"timestamp\": \"Feb 6, 23:00\",\n        \"hkg\": 298,\n        \"ams\": 169,\n        \"iad\": 90,\n        \"jnb\": 710,\n        \"syd\": 502,\n        \"gru\": 469\n      },\n      {\n        \"timestamp\": \"Feb 7, 00:00\",\n        \"hkg\": 316,\n        \"gru\": 466,\n        \"ams\": 168,\n        \"iad\": 90,\n        \"syd\": 502,\n        \"jnb\": 712\n      },\n      {\n        \"timestamp\": \"Feb 7, 01:00\",\n        \"hkg\": 286,\n        \"jnb\": 652,\n        \"syd\": 462,\n        \"iad\": 80,\n        \"ams\": 166,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 7, 02:00\",\n        \"hkg\": 308,\n        \"gru\": 451,\n        \"jnb\": 712,\n        \"syd\": 513,\n        \"ams\": 175,\n        \"iad\": 90\n      },\n      {\n        \"timestamp\": \"Feb 7, 03:00\",\n        \"gru\": 460,\n        \"syd\": 491,\n        \"jnb\": 677,\n        \"iad\": 83,\n        \"ams\": 171,\n        \"hkg\": 291\n      },\n      {\n        \"timestamp\": \"Feb 7, 04:00\",\n        \"iad\": 82,\n        \"ams\": 166,\n        \"syd\": 463,\n        \"jnb\": 650,\n        \"gru\": 413,\n        \"hkg\": 287\n      },\n      {\n        \"timestamp\": \"Feb 7, 05:00\",\n        \"hkg\": 295,\n        \"jnb\": 772,\n        \"syd\": 496,\n        \"iad\": 89,\n        \"ams\": 173,\n        \"gru\": 471\n      },\n      {\n        \"timestamp\": \"Feb 7, 06:00\",\n        \"hkg\": 295,\n        \"ams\": 168,\n        \"iad\": 78,\n        \"jnb\": 649,\n        \"syd\": 488,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 7, 07:00\",\n        \"hkg\": 292,\n        \"gru\": 416,\n        \"iad\": 89,\n        \"ams\": 174,\n        \"syd\": 476,\n        \"jnb\": 662\n      },\n      {\n        \"timestamp\": \"Feb 7, 08:00\",\n        \"gru\": 422,\n        \"syd\": 462,\n        \"jnb\": 649,\n        \"iad\": 78,\n        \"ams\": 175,\n        \"hkg\": 429\n      },\n      {\n        \"timestamp\": \"Feb 7, 09:00\",\n        \"gru\": 471,\n        \"iad\": 82,\n        \"ams\": 186,\n        \"jnb\": 648,\n        \"syd\": 462,\n        \"hkg\": 289\n      },\n      {\n        \"timestamp\": \"Feb 7, 10:00\",\n        \"jnb\": 652,\n        \"syd\": 486,\n        \"ams\": 167,\n        \"iad\": 90,\n        \"gru\": 435,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Feb 7, 11:00\",\n        \"gru\": 420,\n        \"syd\": 462,\n        \"jnb\": 649,\n        \"iad\": 88,\n        \"ams\": 205,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Feb 7, 12:00\",\n        \"hkg\": 292,\n        \"gru\": 435,\n        \"iad\": 88,\n        \"ams\": 195,\n        \"syd\": 465,\n        \"jnb\": 679\n      },\n      {\n        \"timestamp\": \"Feb 7, 13:00\",\n        \"hkg\": 293,\n        \"syd\": 465,\n        \"jnb\": 705,\n        \"ams\": 175,\n        \"iad\": 88,\n        \"gru\": 439\n      },\n      {\n        \"timestamp\": \"Feb 7, 14:00\",\n        \"hkg\": 298,\n        \"iad\": 84,\n        \"ams\": 176,\n        \"syd\": 504,\n        \"jnb\": 681,\n        \"gru\": 448\n      },\n      {\n        \"timestamp\": \"Feb 7, 15:00\",\n        \"gru\": 445,\n        \"ams\": 195,\n        \"iad\": 83,\n        \"syd\": 463,\n        \"jnb\": 679,\n        \"hkg\": 307\n      },\n      {\n        \"timestamp\": \"Feb 7, 16:00\",\n        \"syd\": 486,\n        \"jnb\": 650,\n        \"ams\": 176,\n        \"iad\": 81,\n        \"gru\": 455,\n        \"hkg\": 288\n      },\n      {\n        \"timestamp\": \"Feb 7, 17:00\",\n        \"iad\": 88,\n        \"ams\": 188,\n        \"jnb\": 739,\n        \"syd\": 462,\n        \"gru\": 437,\n        \"hkg\": 289\n      },\n      {\n        \"timestamp\": \"Feb 7, 18:00\",\n        \"gru\": 497,\n        \"ams\": 170,\n        \"iad\": 82,\n        \"jnb\": 678,\n        \"syd\": 506,\n        \"hkg\": 308\n      },\n      {\n        \"timestamp\": \"Feb 7, 19:00\",\n        \"syd\": 462,\n        \"jnb\": 660,\n        \"iad\": 80,\n        \"ams\": 166,\n        \"gru\": 425,\n        \"hkg\": 287\n      },\n      {\n        \"timestamp\": \"Feb 7, 20:00\",\n        \"gru\": 438,\n        \"iad\": 90,\n        \"ams\": 175,\n        \"syd\": 460,\n        \"jnb\": 679,\n        \"hkg\": 297\n      },\n      {\n        \"timestamp\": \"Feb 7, 21:00\",\n        \"hkg\": 326,\n        \"gru\": 490,\n        \"syd\": 514,\n        \"jnb\": 709,\n        \"ams\": 171,\n        \"iad\": 96\n      },\n      {\n        \"timestamp\": \"Feb 7, 22:00\",\n        \"hkg\": 290,\n        \"gru\": 424,\n        \"iad\": 82,\n        \"ams\": 190,\n        \"syd\": 492,\n        \"jnb\": 650\n      },\n      {\n        \"timestamp\": \"Feb 7, 23:00\",\n        \"hkg\": 290,\n        \"ams\": 172,\n        \"iad\": 80,\n        \"jnb\": 652,\n        \"syd\": 484,\n        \"gru\": 418\n      },\n      {\n        \"timestamp\": \"Feb 8, 00:00\",\n        \"hkg\": 291,\n        \"syd\": 479,\n        \"jnb\": 681,\n        \"ams\": 172,\n        \"iad\": 81,\n        \"gru\": 441\n      },\n      {\n        \"timestamp\": \"Feb 8, 01:00\",\n        \"hkg\": 292,\n        \"ams\": 174,\n        \"iad\": 79,\n        \"jnb\": 683,\n        \"syd\": 460,\n        \"gru\": 436\n      },\n      {\n        \"timestamp\": \"Feb 8, 02:00\",\n        \"iad\": 81,\n        \"ams\": 188,\n        \"jnb\": 652,\n        \"syd\": 509,\n        \"gru\": 436,\n        \"hkg\": 285\n      },\n      {\n        \"timestamp\": \"Feb 8, 03:00\",\n        \"gru\": 417,\n        \"ams\": 169,\n        \"iad\": 94,\n        \"syd\": 487,\n        \"jnb\": 650,\n        \"hkg\": 292\n      },\n      {\n        \"timestamp\": \"Feb 8, 04:00\",\n        \"ams\": 176,\n        \"iad\": 82,\n        \"syd\": 467,\n        \"jnb\": 653,\n        \"gru\": 416,\n        \"hkg\": 288\n      },\n      {\n        \"timestamp\": \"Feb 8, 05:00\",\n        \"gru\": 425,\n        \"syd\": 461,\n        \"jnb\": 654,\n        \"iad\": 97,\n        \"ams\": 177,\n        \"hkg\": 288\n      },\n      {\n        \"timestamp\": \"Feb 8, 06:00\",\n        \"gru\": 415,\n        \"ams\": 166,\n        \"iad\": 78,\n        \"syd\": 484,\n        \"jnb\": 651,\n        \"hkg\": 379\n      },\n      {\n        \"timestamp\": \"Feb 8, 07:00\",\n        \"hkg\": 289,\n        \"gru\": 415,\n        \"iad\": 78,\n        \"ams\": 167,\n        \"jnb\": 650,\n        \"syd\": 462\n      },\n      {\n        \"timestamp\": \"Feb 8, 08:00\",\n        \"hkg\": 288,\n        \"jnb\": 662,\n        \"syd\": 463,\n        \"iad\": 78,\n        \"ams\": 167,\n        \"gru\": 417\n      },\n      {\n        \"timestamp\": \"Feb 8, 09:00\",\n        \"gru\": 415,\n        \"jnb\": 679,\n        \"syd\": 465,\n        \"ams\": 167,\n        \"iad\": 82,\n        \"hkg\": 286\n      },\n      {\n        \"timestamp\": \"Feb 8, 10:00\",\n        \"syd\": 464,\n        \"jnb\": 653,\n        \"ams\": 188,\n        \"iad\": 77,\n        \"gru\": 426,\n        \"hkg\": 300\n      },\n      {\n        \"timestamp\": \"Feb 8, 11:00\",\n        \"hkg\": 300,\n        \"syd\": 479,\n        \"jnb\": 652,\n        \"ams\": 166,\n        \"iad\": 84,\n        \"gru\": 436\n      },\n      {\n        \"timestamp\": \"Feb 8, 12:00\",\n        \"hkg\": 285,\n        \"gru\": 417,\n        \"jnb\": 653,\n        \"syd\": 464,\n        \"ams\": 175,\n        \"iad\": 79\n      },\n      {\n        \"timestamp\": \"Feb 8, 13:00\",\n        \"hkg\": 284,\n        \"iad\": 80,\n        \"ams\": 167,\n        \"syd\": 466,\n        \"jnb\": 651,\n        \"gru\": 418\n      },\n      {\n        \"timestamp\": \"Feb 8, 14:00\",\n        \"hkg\": 285,\n        \"gru\": 416,\n        \"iad\": 80,\n        \"ams\": 180,\n        \"jnb\": 650,\n        \"syd\": 466\n      },\n      {\n        \"timestamp\": \"Feb 8, 15:00\",\n        \"hkg\": 288,\n        \"gru\": 418,\n        \"jnb\": 650,\n        \"syd\": 498,\n        \"iad\": 78,\n        \"ams\": 170\n      },\n      {\n        \"timestamp\": \"Feb 8, 16:00\",\n        \"gru\": 422,\n        \"syd\": 464,\n        \"jnb\": 649,\n        \"iad\": 86,\n        \"ams\": 177,\n        \"hkg\": 303\n      },\n      {\n        \"timestamp\": \"Feb 8, 17:00\",\n        \"gru\": 418,\n        \"syd\": 466,\n        \"jnb\": 649,\n        \"iad\": 80,\n        \"ams\": 336,\n        \"hkg\": 288\n      },\n      {\n        \"timestamp\": \"Feb 8, 18:00\",\n        \"hkg\": 305,\n        \"gru\": 436,\n        \"syd\": 507,\n        \"jnb\": 678,\n        \"ams\": 178,\n        \"iad\": 224\n      },\n      {\n        \"timestamp\": \"Feb 8, 19:00\",\n        \"hkg\": 290,\n        \"syd\": 466,\n        \"jnb\": 653,\n        \"ams\": 172,\n        \"iad\": 82,\n        \"gru\": 417\n      },\n      {\n        \"timestamp\": \"Feb 8, 20:00\",\n        \"jnb\": 857,\n        \"syd\": 497,\n        \"ams\": 176,\n        \"iad\": 85,\n        \"gru\": 443,\n        \"hkg\": 291\n      },\n      {\n        \"timestamp\": \"Feb 8, 21:00\",\n        \"gru\": 417,\n        \"syd\": 462,\n        \"jnb\": 650,\n        \"iad\": 78,\n        \"ams\": 175,\n        \"hkg\": 298\n      },\n      {\n        \"timestamp\": \"Feb 8, 22:00\",\n        \"iad\": 79,\n        \"ams\": 166,\n        \"jnb\": 648,\n        \"syd\": 467,\n        \"gru\": 418,\n        \"hkg\": 288\n      },\n      {\n        \"timestamp\": \"Feb 8, 23:00\",\n        \"jnb\": 652,\n        \"syd\": 484,\n        \"iad\": 80,\n        \"ams\": 172,\n        \"gru\": 419,\n        \"hkg\": 305\n      },\n      {\n        \"timestamp\": \"Feb 9, 00:00\",\n        \"gru\": 416,\n        \"syd\": 464,\n        \"jnb\": 651,\n        \"iad\": 80,\n        \"ams\": 170,\n        \"hkg\": 285\n      },\n      {\n        \"timestamp\": \"Feb 9, 01:00\",\n        \"gru\": 416,\n        \"syd\": 478,\n        \"jnb\": 674,\n        \"ams\": 168,\n        \"iad\": 79,\n        \"hkg\": 284\n      },\n      {\n        \"timestamp\": \"Feb 9, 02:00\",\n        \"jnb\": 652,\n        \"syd\": 460,\n        \"ams\": 175,\n        \"iad\": 79,\n        \"gru\": 416,\n        \"hkg\": 289\n      },\n      {\n        \"timestamp\": \"Feb 9, 03:00\",\n        \"hkg\": 287,\n        \"gru\": 436,\n        \"syd\": 464,\n        \"jnb\": 650,\n        \"ams\": 172,\n        \"iad\": 79\n      },\n      {\n        \"timestamp\": \"Feb 9, 04:00\",\n        \"hkg\": 313,\n        \"jnb\": 658,\n        \"syd\": 466,\n        \"ams\": 165,\n        \"iad\": 78,\n        \"gru\": 421\n      },\n      {\n        \"timestamp\": \"Feb 9, 05:00\",\n        \"syd\": 463,\n        \"jnb\": 650,\n        \"iad\": 78,\n        \"ams\": 173,\n        \"gru\": 416,\n        \"hkg\": 287\n      },\n      {\n        \"timestamp\": \"Feb 9, 06:00\",\n        \"gru\": 416,\n        \"jnb\": 711,\n        \"syd\": 515,\n        \"iad\": 86,\n        \"ams\": 169,\n        \"hkg\": 291\n      },\n      {\n        \"timestamp\": \"Feb 9, 07:00\",\n        \"gru\": 414,\n        \"iad\": 78,\n        \"ams\": 167,\n        \"jnb\": 651,\n        \"syd\": 460,\n        \"hkg\": 287\n      },\n      {\n        \"timestamp\": \"Feb 9, 08:00\",\n        \"syd\": 465,\n        \"jnb\": 648,\n        \"iad\": 78,\n        \"ams\": 171,\n        \"gru\": 417,\n        \"hkg\": 288\n      },\n      {\n        \"timestamp\": \"Feb 9, 09:00\",\n        \"syd\": 464,\n        \"jnb\": 654,\n        \"iad\": 78,\n        \"ams\": 168,\n        \"gru\": 416,\n        \"hkg\": 287\n      },\n      {\n        \"timestamp\": \"Feb 9, 10:00\",\n        \"hkg\": 287,\n        \"jnb\": 652,\n        \"syd\": 473,\n        \"iad\": 79,\n        \"ams\": 173,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 9, 11:00\",\n        \"hkg\": 285,\n        \"gru\": 416,\n        \"jnb\": 652,\n        \"syd\": 491,\n        \"iad\": 80,\n        \"ams\": 183\n      },\n      {\n        \"timestamp\": \"Feb 9, 12:00\",\n        \"hkg\": 287,\n        \"gru\": 444,\n        \"ams\": 169,\n        \"iad\": 78,\n        \"syd\": 466,\n        \"jnb\": 652\n      },\n      {\n        \"timestamp\": \"Feb 9, 13:00\",\n        \"hkg\": 286,\n        \"iad\": 78,\n        \"ams\": 170,\n        \"jnb\": 653,\n        \"syd\": 482,\n        \"gru\": 421\n      },\n      {\n        \"timestamp\": \"Feb 9, 14:00\",\n        \"iad\": 78,\n        \"ams\": 189,\n        \"syd\": 468,\n        \"jnb\": 650,\n        \"gru\": 416,\n        \"hkg\": 297\n      },\n      {\n        \"timestamp\": \"Feb 9, 15:00\",\n        \"syd\": 483,\n        \"jnb\": 649,\n        \"iad\": 79,\n        \"ams\": 174,\n        \"gru\": 417,\n        \"hkg\": 314\n      },\n      {\n        \"timestamp\": \"Feb 9, 16:00\",\n        \"gru\": 455,\n        \"jnb\": 690,\n        \"syd\": 489,\n        \"iad\": 85,\n        \"ams\": 169,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Feb 9, 17:00\",\n        \"ams\": 169,\n        \"iad\": 78,\n        \"syd\": 475,\n        \"jnb\": 652,\n        \"gru\": 417,\n        \"hkg\": 285\n      },\n      {\n        \"timestamp\": \"Feb 9, 18:00\",\n        \"gru\": 416,\n        \"syd\": 460,\n        \"jnb\": 654,\n        \"ams\": 168,\n        \"iad\": 78,\n        \"hkg\": 305\n      },\n      {\n        \"timestamp\": \"Feb 9, 19:00\",\n        \"hkg\": 286,\n        \"gru\": 418,\n        \"jnb\": 656,\n        \"syd\": 617,\n        \"iad\": 79,\n        \"ams\": 172\n      },\n      {\n        \"timestamp\": \"Feb 9, 20:00\",\n        \"hkg\": 286,\n        \"jnb\": 650,\n        \"syd\": 461,\n        \"ams\": 170,\n        \"iad\": 88,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 9, 21:00\",\n        \"gru\": 415,\n        \"iad\": 83,\n        \"ams\": 184,\n        \"syd\": 467,\n        \"jnb\": 652,\n        \"hkg\": 297\n      },\n      {\n        \"timestamp\": \"Feb 9, 22:00\",\n        \"syd\": 458,\n        \"jnb\": 651,\n        \"iad\": 78,\n        \"ams\": 168,\n        \"gru\": 418,\n        \"hkg\": 286\n      },\n      {\n        \"timestamp\": \"Feb 9, 23:00\",\n        \"hkg\": 287,\n        \"jnb\": 678,\n        \"syd\": 474,\n        \"ams\": 166,\n        \"iad\": 78,\n        \"gru\": 417\n      },\n      {\n        \"timestamp\": \"Feb 10, 00:00\",\n        \"hkg\": 285,\n        \"iad\": 85,\n        \"ams\": 166,\n        \"jnb\": 649,\n        \"syd\": 464,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 10, 01:00\",\n        \"hkg\": 285,\n        \"syd\": 484,\n        \"jnb\": 650,\n        \"iad\": 78,\n        \"ams\": 166,\n        \"gru\": 419\n      },\n      {\n        \"timestamp\": \"Feb 10, 02:00\",\n        \"hkg\": 309,\n        \"gru\": 416,\n        \"syd\": 465,\n        \"jnb\": 667,\n        \"iad\": 86,\n        \"ams\": 167\n      },\n      {\n        \"timestamp\": \"Feb 10, 03:00\",\n        \"hkg\": 312,\n        \"jnb\": 649,\n        \"syd\": 462,\n        \"ams\": 170,\n        \"iad\": 78,\n        \"gru\": 425\n      },\n      {\n        \"timestamp\": \"Feb 10, 04:00\",\n        \"jnb\": 652,\n        \"syd\": 464,\n        \"iad\": 87,\n        \"ams\": 166,\n        \"gru\": 416,\n        \"hkg\": 285\n      },\n      {\n        \"timestamp\": \"Feb 10, 05:00\",\n        \"gru\": 415,\n        \"ams\": 167,\n        \"iad\": 81,\n        \"jnb\": 650,\n        \"syd\": 464,\n        \"hkg\": 283\n      },\n      {\n        \"timestamp\": \"Feb 10, 06:00\",\n        \"hkg\": 288,\n        \"gru\": 432,\n        \"syd\": 489,\n        \"jnb\": 650,\n        \"iad\": 77,\n        \"ams\": 167\n      },\n      {\n        \"timestamp\": \"Feb 10, 07:00\",\n        \"hkg\": 287,\n        \"jnb\": 649,\n        \"syd\": 464,\n        \"ams\": 168,\n        \"iad\": 77,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 10, 08:00\",\n        \"hkg\": 285,\n        \"iad\": 78,\n        \"ams\": 168,\n        \"jnb\": 647,\n        \"syd\": 470,\n        \"gru\": 417\n      },\n      {\n        \"timestamp\": \"Feb 10, 09:00\",\n        \"hkg\": 284,\n        \"syd\": 478,\n        \"jnb\": 648,\n        \"ams\": 167,\n        \"iad\": 79,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 10, 10:00\",\n        \"hkg\": 317,\n        \"gru\": 416,\n        \"ams\": 169,\n        \"iad\": 78,\n        \"syd\": 479,\n        \"jnb\": 650\n      },\n      {\n        \"timestamp\": \"Feb 10, 11:00\",\n        \"hkg\": 288,\n        \"jnb\": 684,\n        \"syd\": 465,\n        \"iad\": 78,\n        \"ams\": 167,\n        \"gru\": 435\n      },\n      {\n        \"timestamp\": \"Feb 10, 12:00\",\n        \"hkg\": 288,\n        \"gru\": 417,\n        \"jnb\": 694,\n        \"syd\": 508,\n        \"ams\": 167,\n        \"iad\": 94\n      },\n      {\n        \"timestamp\": \"Feb 10, 13:00\",\n        \"gru\": 418,\n        \"iad\": 96,\n        \"ams\": 171,\n        \"jnb\": 687,\n        \"syd\": 473,\n        \"hkg\": 290\n      },\n      {\n        \"timestamp\": \"Feb 10, 14:00\",\n        \"jnb\": 686,\n        \"syd\": 461,\n        \"iad\": 84,\n        \"ams\": 172,\n        \"gru\": 440,\n        \"hkg\": 293\n      },\n      {\n        \"timestamp\": \"Feb 10, 15:00\",\n        \"hkg\": 286,\n        \"gru\": 417,\n        \"jnb\": 649,\n        \"syd\": 462,\n        \"ams\": 176,\n        \"iad\": 77\n      },\n      {\n        \"timestamp\": \"Feb 10, 16:00\",\n        \"hkg\": 287,\n        \"gru\": 438,\n        \"iad\": 80,\n        \"ams\": 166,\n        \"jnb\": 676,\n        \"syd\": 484\n      },\n      {\n        \"timestamp\": \"Feb 10, 17:00\",\n        \"hkg\": 297,\n        \"gru\": 415,\n        \"syd\": 555,\n        \"jnb\": 674,\n        \"iad\": 84,\n        \"ams\": 214\n      },\n      {\n        \"timestamp\": \"Feb 10, 18:00\",\n        \"gru\": 417,\n        \"iad\": 77,\n        \"ams\": 377,\n        \"syd\": 547,\n        \"jnb\": 828,\n        \"hkg\": 420\n      },\n      {\n        \"timestamp\": \"Feb 10, 19:00\",\n        \"ams\": 404,\n        \"iad\": 82,\n        \"jnb\": 871,\n        \"syd\": 568,\n        \"gru\": 416,\n        \"hkg\": 400\n      },\n      {\n        \"timestamp\": \"Feb 10, 20:00\",\n        \"gru\": 424,\n        \"jnb\": 728,\n        \"syd\": 541,\n        \"iad\": 83,\n        \"ams\": 196,\n        \"hkg\": 426\n      },\n      {\n        \"timestamp\": \"Feb 10, 21:00\",\n        \"ams\": 362,\n        \"iad\": 80,\n        \"jnb\": 713,\n        \"syd\": 487,\n        \"gru\": 434,\n        \"hkg\": 349\n      },\n      {\n        \"timestamp\": \"Feb 10, 22:00\",\n        \"hkg\": 434,\n        \"jnb\": 647,\n        \"syd\": 486,\n        \"ams\": 187,\n        \"iad\": 78,\n        \"gru\": 423\n      },\n      {\n        \"timestamp\": \"Feb 10, 23:00\",\n        \"hkg\": 337,\n        \"gru\": 416,\n        \"syd\": 538,\n        \"jnb\": 648,\n        \"iad\": 76,\n        \"ams\": 167\n      },\n      {\n        \"timestamp\": \"Feb 11, 00:00\",\n        \"hkg\": 310,\n        \"gru\": 438,\n        \"ams\": 195,\n        \"iad\": 77,\n        \"syd\": 543,\n        \"jnb\": 721\n      },\n      {\n        \"timestamp\": \"Feb 11, 01:00\",\n        \"hkg\": 812,\n        \"jnb\": 648,\n        \"syd\": 506,\n        \"ams\": 210,\n        \"iad\": 89,\n        \"gru\": 414\n      },\n      {\n        \"timestamp\": \"Feb 11, 02:00\",\n        \"hkg\": 366,\n        \"gru\": 420,\n        \"syd\": 556,\n        \"jnb\": 675,\n        \"ams\": 258,\n        \"iad\": 81\n      },\n      {\n        \"timestamp\": \"Feb 11, 03:00\",\n        \"gru\": 415,\n        \"iad\": 78,\n        \"ams\": 266,\n        \"syd\": 472,\n        \"jnb\": 733,\n        \"hkg\": 414\n      },\n      {\n        \"timestamp\": \"Feb 11, 04:00\",\n        \"iad\": 79,\n        \"ams\": 189,\n        \"jnb\": 737,\n        \"syd\": 514,\n        \"gru\": 434,\n        \"hkg\": 315\n      },\n      {\n        \"timestamp\": \"Feb 11, 05:00\",\n        \"gru\": 434,\n        \"iad\": 93,\n        \"ams\": 192,\n        \"syd\": 562,\n        \"jnb\": 673,\n        \"hkg\": 328\n      },\n      {\n        \"timestamp\": \"Feb 11, 06:00\",\n        \"iad\": 95,\n        \"ams\": 196,\n        \"jnb\": 658,\n        \"syd\": 576,\n        \"gru\": 416,\n        \"hkg\": 338\n      },\n      {\n        \"timestamp\": \"Feb 11, 07:00\",\n        \"hkg\": 347,\n        \"jnb\": 685,\n        \"syd\": 578,\n        \"ams\": 425,\n        \"iad\": 88,\n        \"gru\": 414\n      },\n      {\n        \"timestamp\": \"Feb 11, 08:00\",\n        \"hkg\": 398,\n        \"gru\": 423,\n        \"syd\": 565,\n        \"jnb\": 669,\n        \"ams\": 210,\n        \"iad\": 86\n      },\n      {\n        \"timestamp\": \"Feb 11, 09:00\",\n        \"hkg\": 406,\n        \"syd\": 519,\n        \"jnb\": 683,\n        \"iad\": 79,\n        \"ams\": 208,\n        \"gru\": 423\n      },\n      {\n        \"timestamp\": \"Feb 11, 10:00\",\n        \"gru\": 416,\n        \"syd\": 534,\n        \"jnb\": 676,\n        \"iad\": 76,\n        \"ams\": 223,\n        \"hkg\": 329\n      },\n      {\n        \"timestamp\": \"Feb 11, 11:00\",\n        \"ams\": 195,\n        \"iad\": 77,\n        \"syd\": 541,\n        \"jnb\": 866,\n        \"gru\": 414,\n        \"hkg\": 325\n      },\n      {\n        \"timestamp\": \"Feb 11, 12:00\",\n        \"hkg\": 384,\n        \"ams\": 770,\n        \"iad\": 80,\n        \"jnb\": 714,\n        \"syd\": 530,\n        \"gru\": 423\n      },\n      {\n        \"timestamp\": \"Feb 11, 13:00\",\n        \"hkg\": 418,\n        \"gru\": 414,\n        \"syd\": 596,\n        \"jnb\": 672,\n        \"ams\": 736,\n        \"iad\": 81\n      },\n      {\n        \"timestamp\": \"Feb 11, 14:00\",\n        \"hkg\": 408,\n        \"ams\": 214,\n        \"iad\": 80,\n        \"jnb\": 668,\n        \"syd\": 559,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 11, 15:00\",\n        \"hkg\": 327,\n        \"gru\": 418,\n        \"jnb\": 1195,\n        \"syd\": 543,\n        \"iad\": 83,\n        \"ams\": 212\n      },\n      {\n        \"timestamp\": \"Feb 11, 16:00\",\n        \"gru\": 415,\n        \"syd\": 534,\n        \"jnb\": 885,\n        \"iad\": 85,\n        \"ams\": 186,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 11, 17:00\",\n        \"hkg\": 357,\n        \"gru\": 415,\n        \"syd\": 590,\n        \"jnb\": 670,\n        \"iad\": 83,\n        \"ams\": 209\n      },\n      {\n        \"timestamp\": \"Feb 11, 18:00\",\n        \"hkg\": 350,\n        \"jnb\": 3586,\n        \"syd\": 554,\n        \"iad\": 80,\n        \"ams\": 406,\n        \"gru\": 414\n      },\n      {\n        \"timestamp\": \"Feb 11, 19:00\",\n        \"iad\": 78,\n        \"ams\": 226,\n        \"syd\": 588,\n        \"jnb\": 3298,\n        \"gru\": 415,\n        \"hkg\": 358\n      },\n      {\n        \"timestamp\": \"Feb 11, 20:00\",\n        \"gru\": 415,\n        \"iad\": 78,\n        \"ams\": 723,\n        \"jnb\": 843,\n        \"syd\": 500,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 11, 21:00\",\n        \"hkg\": 368,\n        \"gru\": 424,\n        \"syd\": 568,\n        \"jnb\": 881,\n        \"iad\": 86,\n        \"ams\": 217\n      },\n      {\n        \"timestamp\": \"Feb 11, 22:00\",\n        \"hkg\": 330,\n        \"jnb\": 646,\n        \"syd\": 522,\n        \"iad\": 89,\n        \"ams\": 331,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 11, 23:00\",\n        \"hkg\": 331,\n        \"gru\": 416,\n        \"jnb\": 690,\n        \"syd\": 525,\n        \"ams\": 264,\n        \"iad\": 76\n      },\n      {\n        \"timestamp\": \"Feb 12, 00:00\",\n        \"hkg\": 312,\n        \"syd\": 490,\n        \"jnb\": 706,\n        \"ams\": 235,\n        \"iad\": 84,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 12, 01:00\",\n        \"hkg\": 314,\n        \"gru\": 424,\n        \"syd\": 514,\n        \"jnb\": 671,\n        \"iad\": 83,\n        \"ams\": 212\n      },\n      {\n        \"timestamp\": \"Feb 12, 02:00\",\n        \"gru\": 415,\n        \"jnb\": 673,\n        \"syd\": 536,\n        \"iad\": 86,\n        \"ams\": 217,\n        \"hkg\": 338\n      },\n      {\n        \"timestamp\": \"Feb 12, 03:00\",\n        \"ams\": 185,\n        \"iad\": 79,\n        \"jnb\": 648,\n        \"syd\": 528,\n        \"gru\": 414,\n        \"hkg\": 348\n      },\n      {\n        \"timestamp\": \"Feb 12, 04:00\",\n        \"hkg\": 357,\n        \"gru\": 434,\n        \"ams\": 236,\n        \"iad\": 78,\n        \"jnb\": 692,\n        \"syd\": 549\n      },\n      {\n        \"timestamp\": \"Feb 12, 05:00\",\n        \"hkg\": 384,\n        \"syd\": 460,\n        \"jnb\": 693,\n        \"ams\": 224,\n        \"iad\": 78,\n        \"gru\": 432\n      },\n      {\n        \"timestamp\": \"Feb 12, 06:00\",\n        \"jnb\": 653,\n        \"syd\": 486,\n        \"ams\": 173,\n        \"iad\": 86,\n        \"gru\": 415,\n        \"hkg\": 377\n      },\n      {\n        \"timestamp\": \"Feb 12, 07:00\",\n        \"gru\": 441,\n        \"iad\": 98,\n        \"ams\": 209,\n        \"jnb\": 670,\n        \"syd\": 509,\n        \"hkg\": 381\n      },\n      {\n        \"timestamp\": \"Feb 12, 08:00\",\n        \"syd\": 503,\n        \"jnb\": 706,\n        \"iad\": 78,\n        \"ams\": 203,\n        \"gru\": 415,\n        \"hkg\": 349\n      },\n      {\n        \"timestamp\": \"Feb 12, 09:00\",\n        \"gru\": 416,\n        \"jnb\": 662,\n        \"syd\": 545,\n        \"ams\": 188,\n        \"iad\": 76,\n        \"hkg\": 329\n      },\n      {\n        \"timestamp\": \"Feb 12, 10:00\",\n        \"gru\": 414,\n        \"iad\": 78,\n        \"ams\": 196,\n        \"jnb\": 711,\n        \"syd\": 516,\n        \"hkg\": 364\n      },\n      {\n        \"timestamp\": \"Feb 12, 11:00\",\n        \"hkg\": 362,\n        \"gru\": 440,\n        \"ams\": 188,\n        \"iad\": 83,\n        \"syd\": 598,\n        \"jnb\": 647\n      },\n      {\n        \"timestamp\": \"Feb 12, 12:00\",\n        \"hkg\": 356,\n        \"syd\": 554,\n        \"jnb\": 718,\n        \"ams\": 214,\n        \"iad\": 76,\n        \"gru\": 428\n      },\n      {\n        \"timestamp\": \"Feb 12, 13:00\",\n        \"hkg\": 384,\n        \"syd\": 538,\n        \"jnb\": 690,\n        \"ams\": 386,\n        \"iad\": 86,\n        \"gru\": 414\n      },\n      {\n        \"timestamp\": \"Feb 12, 14:00\",\n        \"hkg\": 347,\n        \"gru\": 417,\n        \"syd\": 521,\n        \"jnb\": 658,\n        \"iad\": 79,\n        \"ams\": 273\n      },\n      {\n        \"timestamp\": \"Feb 12, 15:00\",\n        \"gru\": 436,\n        \"jnb\": 1427,\n        \"syd\": 572,\n        \"ams\": 272,\n        \"iad\": 80,\n        \"hkg\": 416\n      },\n      {\n        \"timestamp\": \"Feb 12, 16:00\",\n        \"ams\": 1859,\n        \"iad\": 87,\n        \"jnb\": 760,\n        \"syd\": 534,\n        \"gru\": 443,\n        \"hkg\": 366\n      },\n      {\n        \"timestamp\": \"Feb 12, 17:00\",\n        \"ams\": 209,\n        \"iad\": 80,\n        \"syd\": 525,\n        \"jnb\": 925,\n        \"gru\": 438,\n        \"hkg\": 338\n      },\n      {\n        \"timestamp\": \"Feb 12, 18:00\",\n        \"syd\": 561,\n        \"jnb\": 1236,\n        \"iad\": 84,\n        \"ams\": 1210,\n        \"gru\": 434,\n        \"hkg\": 303\n      },\n      {\n        \"timestamp\": \"Feb 12, 19:00\",\n        \"gru\": 422,\n        \"jnb\": 1261,\n        \"syd\": 529,\n        \"ams\": 360,\n        \"iad\": 82,\n        \"hkg\": 340\n      },\n      {\n        \"timestamp\": \"Feb 12, 20:00\",\n        \"hkg\": 285,\n        \"gru\": 424,\n        \"ams\": 376,\n        \"iad\": 79,\n        \"jnb\": 670,\n        \"syd\": 489\n      },\n      {\n        \"timestamp\": \"Feb 12, 21:00\",\n        \"hkg\": 337,\n        \"jnb\": 672,\n        \"syd\": 522,\n        \"iad\": 80,\n        \"ams\": 192,\n        \"gru\": 418\n      },\n      {\n        \"timestamp\": \"Feb 12, 22:00\",\n        \"gru\": 422,\n        \"syd\": 492,\n        \"jnb\": 683,\n        \"ams\": 199,\n        \"iad\": 79,\n        \"hkg\": 311\n      },\n      {\n        \"timestamp\": \"Feb 12, 23:00\",\n        \"jnb\": 711,\n        \"syd\": 582,\n        \"iad\": 91,\n        \"ams\": 216,\n        \"gru\": 470,\n        \"hkg\": 338\n      },\n      {\n        \"timestamp\": \"Feb 13, 00:00\",\n        \"hkg\": 377,\n        \"iad\": 77,\n        \"ams\": 182,\n        \"jnb\": 671,\n        \"syd\": 515,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 13, 01:00\",\n        \"gru\": 422,\n        \"syd\": 515,\n        \"jnb\": 685,\n        \"ams\": 216,\n        \"iad\": 83,\n        \"hkg\": 314\n      },\n      {\n        \"timestamp\": \"Feb 13, 02:00\",\n        \"gru\": 416,\n        \"iad\": 80,\n        \"ams\": 195,\n        \"syd\": 516,\n        \"jnb\": 657,\n        \"hkg\": 327\n      },\n      {\n        \"timestamp\": \"Feb 13, 03:00\",\n        \"jnb\": 672,\n        \"syd\": 529,\n        \"ams\": 174,\n        \"iad\": 89,\n        \"gru\": 418,\n        \"hkg\": 373\n      },\n      {\n        \"timestamp\": \"Feb 13, 04:00\",\n        \"gru\": 416,\n        \"ams\": 224,\n        \"iad\": 82,\n        \"jnb\": 698,\n        \"syd\": 533,\n        \"hkg\": 329\n      },\n      {\n        \"timestamp\": \"Feb 13, 05:00\",\n        \"hkg\": 344,\n        \"gru\": 416,\n        \"jnb\": 648,\n        \"syd\": 540,\n        \"iad\": 85,\n        \"ams\": 177\n      },\n      {\n        \"timestamp\": \"Feb 13, 06:00\",\n        \"hkg\": 311,\n        \"jnb\": 670,\n        \"syd\": 546,\n        \"ams\": 241,\n        \"iad\": 88,\n        \"gru\": 444\n      },\n      {\n        \"timestamp\": \"Feb 13, 07:00\",\n        \"jnb\": 690,\n        \"syd\": 499,\n        \"ams\": 206,\n        \"iad\": 97,\n        \"gru\": 414,\n        \"hkg\": 369\n      },\n      {\n        \"timestamp\": \"Feb 13, 08:00\",\n        \"gru\": 455,\n        \"ams\": 221,\n        \"iad\": 89,\n        \"jnb\": 725,\n        \"syd\": 574,\n        \"hkg\": 347\n      },\n      {\n        \"timestamp\": \"Feb 13, 09:00\",\n        \"hkg\": 368,\n        \"syd\": 520,\n        \"jnb\": 672,\n        \"ams\": 187,\n        \"iad\": 84,\n        \"gru\": 423\n      },\n      {\n        \"timestamp\": \"Feb 13, 10:00\",\n        \"hkg\": 339,\n        \"iad\": 75,\n        \"ams\": 197,\n        \"syd\": 524,\n        \"jnb\": 695,\n        \"gru\": 414\n      },\n      {\n        \"timestamp\": \"Feb 13, 11:00\",\n        \"hkg\": 350,\n        \"gru\": 437,\n        \"ams\": 207,\n        \"iad\": 77,\n        \"jnb\": 702,\n        \"syd\": 554\n      },\n      {\n        \"timestamp\": \"Feb 13, 12:00\",\n        \"hkg\": 382,\n        \"ams\": 195,\n        \"iad\": 83,\n        \"jnb\": 669,\n        \"syd\": 592,\n        \"gru\": 445\n      },\n      {\n        \"timestamp\": \"Feb 13, 13:00\",\n        \"hkg\": 354,\n        \"gru\": 460,\n        \"jnb\": 676,\n        \"syd\": 552,\n        \"iad\": 97,\n        \"ams\": 190\n      },\n      {\n        \"timestamp\": \"Feb 13, 14:00\",\n        \"gru\": 417,\n        \"jnb\": 678,\n        \"syd\": 532,\n        \"ams\": 169,\n        \"iad\": 78,\n        \"hkg\": 346\n      },\n      {\n        \"timestamp\": \"Feb 13, 15:00\",\n        \"syd\": 484,\n        \"jnb\": 704,\n        \"iad\": 80,\n        \"ams\": 229,\n        \"gru\": 424,\n        \"hkg\": 349\n      },\n      {\n        \"timestamp\": \"Feb 13, 16:00\",\n        \"hkg\": 361,\n        \"gru\": 456,\n        \"iad\": 92,\n        \"ams\": 195,\n        \"jnb\": 728,\n        \"syd\": 509\n      },\n      {\n        \"timestamp\": \"Feb 13, 17:00\",\n        \"hkg\": 311,\n        \"ams\": 176,\n        \"iad\": 82,\n        \"syd\": 486,\n        \"jnb\": 673,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 13, 18:00\",\n        \"ams\": 222,\n        \"iad\": 78,\n        \"jnb\": 683,\n        \"syd\": 544,\n        \"gru\": 415,\n        \"hkg\": 356\n      },\n      {\n        \"timestamp\": \"Feb 13, 19:00\",\n        \"jnb\": 670,\n        \"syd\": 565,\n        \"ams\": 236,\n        \"iad\": 88,\n        \"gru\": 415,\n        \"hkg\": 381\n      },\n      {\n        \"timestamp\": \"Feb 13, 20:00\",\n        \"gru\": 426,\n        \"syd\": 601,\n        \"jnb\": 703,\n        \"ams\": 172,\n        \"iad\": 85,\n        \"hkg\": 365\n      },\n      {\n        \"timestamp\": \"Feb 13, 21:00\",\n        \"iad\": 76,\n        \"ams\": 188,\n        \"jnb\": 774,\n        \"syd\": 524,\n        \"gru\": 428,\n        \"hkg\": 402\n      },\n      {\n        \"timestamp\": \"Feb 13, 22:00\",\n        \"gru\": 416,\n        \"iad\": 199,\n        \"ams\": 196,\n        \"syd\": 527,\n        \"jnb\": 700,\n        \"hkg\": 362\n      },\n      {\n        \"timestamp\": \"Feb 13, 23:00\",\n        \"hkg\": 348,\n        \"gru\": 427,\n        \"iad\": 76,\n        \"ams\": 176,\n        \"syd\": 499,\n        \"jnb\": 649\n      },\n      {\n        \"timestamp\": \"Feb 14, 00:00\",\n        \"hkg\": 342,\n        \"iad\": 76,\n        \"ams\": 210,\n        \"jnb\": 720,\n        \"syd\": 563,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 14, 01:00\",\n        \"hkg\": 318,\n        \"gru\": 457,\n        \"iad\": 87,\n        \"ams\": 170,\n        \"syd\": 547,\n        \"jnb\": 682\n      },\n      {\n        \"timestamp\": \"Feb 14, 02:00\",\n        \"hkg\": 379,\n        \"iad\": 86,\n        \"ams\": 202,\n        \"jnb\": 662,\n        \"syd\": 509,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 14, 03:00\",\n        \"hkg\": 347,\n        \"jnb\": 680,\n        \"syd\": 572,\n        \"iad\": 90,\n        \"ams\": 167,\n        \"gru\": 436\n      },\n      {\n        \"timestamp\": \"Feb 14, 04:00\",\n        \"syd\": 553,\n        \"jnb\": 718,\n        \"iad\": 86,\n        \"ams\": 199,\n        \"gru\": 448,\n        \"hkg\": 372\n      },\n      {\n        \"timestamp\": \"Feb 14, 05:00\",\n        \"gru\": 413,\n        \"jnb\": 674,\n        \"syd\": 548,\n        \"ams\": 211,\n        \"iad\": 77,\n        \"hkg\": 363\n      },\n      {\n        \"timestamp\": \"Feb 14, 06:00\",\n        \"ams\": 184,\n        \"iad\": 76,\n        \"syd\": 535,\n        \"jnb\": 690,\n        \"gru\": 420,\n        \"hkg\": 364\n      },\n      {\n        \"timestamp\": \"Feb 14, 07:00\",\n        \"gru\": 414,\n        \"ams\": 201,\n        \"iad\": 76,\n        \"syd\": 525,\n        \"jnb\": 717,\n        \"hkg\": 381\n      },\n      {\n        \"timestamp\": \"Feb 14, 08:00\",\n        \"hkg\": 374,\n        \"gru\": 418,\n        \"ams\": 163,\n        \"iad\": 79,\n        \"jnb\": 658,\n        \"syd\": 557\n      },\n      {\n        \"timestamp\": \"Feb 14, 09:00\",\n        \"iad\": 80,\n        \"ams\": 187,\n        \"jnb\": 656,\n        \"syd\": 520,\n        \"gru\": 423,\n        \"hkg\": 366\n      },\n      {\n        \"timestamp\": \"Feb 14, 10:00\",\n        \"hkg\": 290,\n        \"gru\": 413,\n        \"iad\": 77,\n        \"ams\": 170,\n        \"jnb\": 717,\n        \"syd\": 563\n      },\n      {\n        \"timestamp\": \"Feb 14, 11:00\",\n        \"hkg\": 328,\n        \"gru\": 437,\n        \"jnb\": 664,\n        \"syd\": 481,\n        \"iad\": 76,\n        \"ams\": 227\n      },\n      {\n        \"timestamp\": \"Feb 14, 12:00\",\n        \"hkg\": 353,\n        \"syd\": 511,\n        \"jnb\": 699,\n        \"iad\": 75,\n        \"ams\": 184,\n        \"gru\": 424\n      },\n      {\n        \"timestamp\": \"Feb 14, 13:00\",\n        \"jnb\": 708,\n        \"syd\": 518,\n        \"ams\": 227,\n        \"iad\": 86,\n        \"gru\": 414,\n        \"hkg\": 326\n      },\n      {\n        \"timestamp\": \"Feb 14, 14:00\",\n        \"gru\": 425,\n        \"syd\": 531,\n        \"jnb\": 673,\n        \"ams\": 196,\n        \"iad\": 83,\n        \"hkg\": 357\n      },\n      {\n        \"timestamp\": \"Feb 14, 15:00\",\n        \"ams\": 242,\n        \"iad\": 86,\n        \"syd\": 516,\n        \"jnb\": 665,\n        \"gru\": 414,\n        \"hkg\": 372\n      },\n      {\n        \"timestamp\": \"Feb 14, 16:00\",\n        \"gru\": 436,\n        \"ams\": 189,\n        \"iad\": 99,\n        \"jnb\": 714,\n        \"syd\": 536,\n        \"hkg\": 350\n      },\n      {\n        \"timestamp\": \"Feb 14, 17:00\",\n        \"hkg\": 350,\n        \"ams\": 212,\n        \"iad\": 78,\n        \"jnb\": 1005,\n        \"syd\": 498,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 14, 18:00\",\n        \"hkg\": 388,\n        \"gru\": 468,\n        \"ams\": 202,\n        \"iad\": 85,\n        \"syd\": 566,\n        \"jnb\": 743\n      },\n      {\n        \"timestamp\": \"Feb 14, 19:00\",\n        \"hkg\": 390,\n        \"gru\": 415,\n        \"syd\": 512,\n        \"jnb\": 703,\n        \"ams\": 209,\n        \"iad\": 76\n      },\n      {\n        \"timestamp\": \"Feb 14, 20:00\",\n        \"gru\": 437,\n        \"syd\": 516,\n        \"jnb\": 684,\n        \"iad\": 88,\n        \"ams\": 193,\n        \"hkg\": 388\n      },\n      {\n        \"timestamp\": \"Feb 14, 21:00\",\n        \"jnb\": 697,\n        \"syd\": 572,\n        \"ams\": 203,\n        \"iad\": 95,\n        \"gru\": 417,\n        \"hkg\": 420\n      },\n      {\n        \"timestamp\": \"Feb 14, 22:00\",\n        \"hkg\": 348,\n        \"syd\": 536,\n        \"jnb\": 675,\n        \"ams\": 216,\n        \"iad\": 87,\n        \"gru\": 425\n      },\n      {\n        \"timestamp\": \"Feb 14, 23:00\",\n        \"hkg\": 319,\n        \"gru\": 414,\n        \"syd\": 583,\n        \"jnb\": 690,\n        \"ams\": 206,\n        \"iad\": 77\n      },\n      {\n        \"timestamp\": \"Feb 15, 00:00\",\n        \"hkg\": 321,\n        \"ams\": 201,\n        \"iad\": 77,\n        \"jnb\": 661,\n        \"syd\": 506,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 15, 01:00\",\n        \"hkg\": 308,\n        \"gru\": 425,\n        \"iad\": 78,\n        \"ams\": 208,\n        \"syd\": 486,\n        \"jnb\": 679\n      },\n      {\n        \"timestamp\": \"Feb 15, 02:00\",\n        \"gru\": 432,\n        \"syd\": 556,\n        \"jnb\": 718,\n        \"iad\": 80,\n        \"ams\": 236,\n        \"hkg\": 351\n      },\n      {\n        \"timestamp\": \"Feb 15, 03:00\",\n        \"jnb\": 722,\n        \"syd\": 491,\n        \"ams\": 198,\n        \"iad\": 75,\n        \"gru\": 416,\n        \"hkg\": 337\n      },\n      {\n        \"timestamp\": \"Feb 15, 04:00\",\n        \"iad\": 78,\n        \"ams\": 198,\n        \"jnb\": 665,\n        \"syd\": 488,\n        \"gru\": 420,\n        \"hkg\": 531\n      },\n      {\n        \"timestamp\": \"Feb 15, 05:00\",\n        \"hkg\": 356,\n        \"gru\": 432,\n        \"syd\": 566,\n        \"jnb\": 672,\n        \"ams\": 177,\n        \"iad\": 79\n      },\n      {\n        \"timestamp\": \"Feb 15, 06:00\",\n        \"hkg\": 323,\n        \"ams\": 196,\n        \"iad\": 84,\n        \"syd\": 542,\n        \"jnb\": 670,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 15, 07:00\",\n        \"iad\": 94,\n        \"ams\": 201,\n        \"jnb\": 694,\n        \"syd\": 641,\n        \"gru\": 415,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 15, 08:00\",\n        \"gru\": 416,\n        \"ams\": 214,\n        \"iad\": 82,\n        \"jnb\": 691,\n        \"syd\": 528,\n        \"hkg\": 349\n      },\n      {\n        \"timestamp\": \"Feb 15, 09:00\",\n        \"gru\": 414,\n        \"ams\": 205,\n        \"iad\": 82,\n        \"syd\": 508,\n        \"jnb\": 652,\n        \"hkg\": 340\n      },\n      {\n        \"timestamp\": \"Feb 15, 10:00\",\n        \"syd\": 581,\n        \"jnb\": 690,\n        \"ams\": 268,\n        \"iad\": 79,\n        \"gru\": 434,\n        \"hkg\": 376\n      },\n      {\n        \"timestamp\": \"Feb 15, 11:00\",\n        \"hkg\": 333,\n        \"jnb\": 690,\n        \"syd\": 523,\n        \"iad\": 87,\n        \"ams\": 220,\n        \"gru\": 423\n      },\n      {\n        \"timestamp\": \"Feb 15, 12:00\",\n        \"hkg\": 329,\n        \"ams\": 231,\n        \"iad\": 83,\n        \"jnb\": 671,\n        \"syd\": 564,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 15, 13:00\",\n        \"hkg\": 392,\n        \"gru\": 431,\n        \"jnb\": 649,\n        \"syd\": 518,\n        \"iad\": 84,\n        \"ams\": 217\n      },\n      {\n        \"timestamp\": \"Feb 15, 14:00\",\n        \"hkg\": 409,\n        \"gru\": 417,\n        \"jnb\": 660,\n        \"syd\": 504,\n        \"ams\": 173,\n        \"iad\": 77\n      },\n      {\n        \"timestamp\": \"Feb 15, 15:00\",\n        \"hkg\": 360,\n        \"syd\": 511,\n        \"jnb\": 741,\n        \"iad\": 84,\n        \"ams\": 216,\n        \"gru\": 440\n      },\n      {\n        \"timestamp\": \"Feb 15, 16:00\",\n        \"syd\": 476,\n        \"jnb\": 820,\n        \"ams\": 277,\n        \"iad\": 90,\n        \"gru\": 470,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 17, 10:00\",\n        \"hkg\": 280,\n        \"iad\": 78,\n        \"ams\": 169,\n        \"syd\": 467,\n        \"jnb\": 656,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 17, 11:00\",\n        \"gru\": 422,\n        \"jnb\": 659,\n        \"syd\": 478,\n        \"ams\": 210,\n        \"iad\": 86,\n        \"hkg\": 293\n      },\n      {\n        \"timestamp\": \"Feb 17, 12:00\",\n        \"syd\": 466,\n        \"jnb\": 659,\n        \"ams\": 172,\n        \"iad\": 77,\n        \"gru\": 418,\n        \"hkg\": 348\n      },\n      {\n        \"timestamp\": \"Feb 17, 13:00\",\n        \"jnb\": 665,\n        \"syd\": 482,\n        \"ams\": 210,\n        \"iad\": 82,\n        \"gru\": 414,\n        \"hkg\": 314\n      },\n      {\n        \"timestamp\": \"Feb 17, 14:00\",\n        \"hkg\": 299,\n        \"syd\": 467,\n        \"jnb\": 692,\n        \"ams\": 173,\n        \"iad\": 83,\n        \"gru\": 414\n      },\n      {\n        \"timestamp\": \"Feb 17, 15:00\",\n        \"hkg\": 308,\n        \"gru\": 442,\n        \"iad\": 82,\n        \"ams\": 178,\n        \"syd\": 499,\n        \"jnb\": 703\n      },\n      {\n        \"timestamp\": \"Feb 17, 16:00\",\n        \"hkg\": 290,\n        \"jnb\": 658,\n        \"syd\": 476,\n        \"iad\": 77,\n        \"ams\": 171,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 17, 17:00\",\n        \"hkg\": 288,\n        \"gru\": 416,\n        \"syd\": 464,\n        \"jnb\": 657,\n        \"ams\": 187,\n        \"iad\": 81\n      },\n      {\n        \"timestamp\": \"Feb 17, 18:00\",\n        \"gru\": 414,\n        \"syd\": 460,\n        \"jnb\": 657,\n        \"iad\": 82,\n        \"ams\": 173,\n        \"hkg\": 320\n      },\n      {\n        \"timestamp\": \"Feb 17, 19:00\",\n        \"jnb\": 682,\n        \"syd\": 481,\n        \"ams\": 172,\n        \"iad\": 76,\n        \"gru\": 415,\n        \"hkg\": 305\n      },\n      {\n        \"timestamp\": \"Feb 17, 20:00\",\n        \"hkg\": 288,\n        \"iad\": 80,\n        \"ams\": 172,\n        \"syd\": 463,\n        \"jnb\": 798,\n        \"gru\": 416\n      },\n      {\n        \"timestamp\": \"Feb 17, 21:00\",\n        \"hkg\": 312,\n        \"gru\": 416,\n        \"syd\": 553,\n        \"jnb\": 668,\n        \"ams\": 172,\n        \"iad\": 76\n      },\n      {\n        \"timestamp\": \"Feb 17, 22:00\",\n        \"hkg\": 286,\n        \"gru\": 414,\n        \"iad\": 80,\n        \"ams\": 180,\n        \"syd\": 462,\n        \"jnb\": 664\n      },\n      {\n        \"timestamp\": \"Feb 17, 23:00\",\n        \"gru\": 414,\n        \"ams\": 187,\n        \"iad\": 80,\n        \"jnb\": 663,\n        \"syd\": 490,\n        \"hkg\": 302\n      },\n      {\n        \"timestamp\": \"Feb 18, 00:00\",\n        \"jnb\": 658,\n        \"syd\": 500,\n        \"ams\": 174,\n        \"iad\": 77,\n        \"gru\": 428,\n        \"hkg\": 285\n      },\n      {\n        \"timestamp\": \"Feb 18, 01:00\",\n        \"syd\": 509,\n        \"jnb\": 663,\n        \"ams\": 179,\n        \"iad\": 78,\n        \"gru\": 435,\n        \"hkg\": 327\n      },\n      {\n        \"timestamp\": \"Feb 18, 02:00\",\n        \"gru\": 414,\n        \"syd\": 534,\n        \"jnb\": 654,\n        \"iad\": 79,\n        \"ams\": 178,\n        \"hkg\": 323\n      },\n      {\n        \"timestamp\": \"Feb 18, 03:00\",\n        \"hkg\": 341,\n        \"gru\": 432,\n        \"jnb\": 658,\n        \"syd\": 510,\n        \"ams\": 191,\n        \"iad\": 75\n      },\n      {\n        \"timestamp\": \"Feb 18, 04:00\",\n        \"hkg\": 324,\n        \"ams\": 170,\n        \"iad\": 75,\n        \"jnb\": 682,\n        \"syd\": 515,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 18, 05:00\",\n        \"gru\": 414,\n        \"syd\": 494,\n        \"jnb\": 656,\n        \"iad\": 91,\n        \"ams\": 177,\n        \"hkg\": 314\n      },\n      {\n        \"timestamp\": \"Feb 18, 06:00\",\n        \"gru\": 414,\n        \"ams\": 185,\n        \"iad\": 76,\n        \"syd\": 471,\n        \"jnb\": 654,\n        \"hkg\": 282\n      },\n      {\n        \"timestamp\": \"Feb 18, 07:00\",\n        \"iad\": 76,\n        \"ams\": 198,\n        \"jnb\": 654,\n        \"syd\": 484,\n        \"gru\": 414,\n        \"hkg\": 286\n      },\n      {\n        \"timestamp\": \"Feb 18, 08:00\",\n        \"hkg\": 303,\n        \"jnb\": 655,\n        \"syd\": 463,\n        \"iad\": 76,\n        \"ams\": 194,\n        \"gru\": 434\n      },\n      {\n        \"timestamp\": \"Feb 18, 09:00\",\n        \"gru\": 550,\n        \"ams\": 172,\n        \"iad\": 89,\n        \"syd\": 457,\n        \"jnb\": 677,\n        \"hkg\": 290\n      },\n      {\n        \"timestamp\": \"Feb 18, 10:00\",\n        \"syd\": 466,\n        \"jnb\": 682,\n        \"iad\": 80,\n        \"ams\": 180,\n        \"gru\": 413,\n        \"hkg\": 318\n      },\n      {\n        \"timestamp\": \"Feb 18, 11:00\",\n        \"gru\": 414,\n        \"jnb\": 658,\n        \"syd\": 463,\n        \"ams\": 174,\n        \"iad\": 80,\n        \"hkg\": 284\n      },\n      {\n        \"timestamp\": \"Feb 18, 12:00\",\n        \"hkg\": 330,\n        \"gru\": 414,\n        \"ams\": 179,\n        \"iad\": 83,\n        \"jnb\": 653,\n        \"syd\": 496\n      },\n      {\n        \"timestamp\": \"Feb 18, 13:00\",\n        \"hkg\": 287,\n        \"jnb\": 659,\n        \"syd\": 512,\n        \"iad\": 77,\n        \"ams\": 177,\n        \"gru\": 413\n      },\n      {\n        \"timestamp\": \"Feb 18, 14:00\",\n        \"hkg\": 313,\n        \"ams\": 172,\n        \"iad\": 75,\n        \"jnb\": 655,\n        \"syd\": 464,\n        \"gru\": 414\n      },\n      {\n        \"timestamp\": \"Feb 18, 15:00\",\n        \"hkg\": 318,\n        \"syd\": 468,\n        \"jnb\": 658,\n        \"ams\": 177,\n        \"iad\": 77,\n        \"gru\": 415\n      },\n      {\n        \"timestamp\": \"Feb 18, 16:00\",\n        \"hkg\": 313,\n        \"gru\": 415,\n        \"ams\": 173,\n        \"iad\": 77,\n        \"syd\": 462,\n        \"jnb\": 666\n      },\n      {\n        \"timestamp\": \"Feb 18, 17:00\",\n        \"jnb\": 659,\n        \"syd\": 491,\n        \"ams\": 179,\n        \"iad\": 76,\n        \"gru\": 414,\n        \"hkg\": 283\n      },\n      {\n        \"timestamp\": \"Feb 18, 18:00\",\n        \"gru\": 414,\n        \"jnb\": 694,\n        \"syd\": 476,\n        \"iad\": 84,\n        \"ams\": 174,\n        \"hkg\": 298\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"syd\",\n      \"avgLatency\": 499,\n      \"p75Latency\": 532,\n      \"p90Latency\": 607,\n      \"p95Latency\": 620,\n      \"p99Latency\": 728\n    },\n    {\n      \"region\": \"gru\",\n      \"avgLatency\": 423,\n      \"p75Latency\": 417,\n      \"p90Latency\": 422,\n      \"p95Latency\": 468,\n      \"p99Latency\": 541\n    },\n    {\n      \"region\": \"iad\",\n      \"avgLatency\": 80,\n      \"p75Latency\": 79,\n      \"p90Latency\": 91,\n      \"p95Latency\": 107,\n      \"p99Latency\": 131\n    },\n    {\n      \"region\": \"ams\",\n      \"avgLatency\": 191,\n      \"p75Latency\": 181,\n      \"p90Latency\": 259,\n      \"p95Latency\": 295,\n      \"p99Latency\": 316\n    },\n    {\n      \"region\": \"hkg\",\n      \"avgLatency\": 325,\n      \"p75Latency\": 348,\n      \"p90Latency\": 439,\n      \"p95Latency\": 443,\n      \"p99Latency\": 467\n    },\n    {\n      \"region\": \"jnb\",\n      \"avgLatency\": 675,\n      \"p75Latency\": 660,\n      \"p90Latency\": 739,\n      \"p95Latency\": 786,\n      \"p99Latency\": 893\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/monitoring-latency/render.json",
    "content": "{\n  \"regions\": [\"ams\", \"iad\", \"hkg\", \"jnb\", \"syd\", \"gru\"],\n  \"data\": {\n    \"regions\": [\"ams\", \"gru\", \"hkg\", \"iad\", \"jnb\", \"syd\"],\n    \"data\": [\n      {\n        \"timestamp\": \"Feb 4, 00:00\",\n        \"ams\": 80,\n        \"iad\": 277,\n        \"syd\": 515,\n        \"jnb\": 579,\n        \"gru\": 393,\n        \"hkg\": 408\n      },\n      {\n        \"timestamp\": \"Feb 4, 01:00\",\n        \"ams\": 109,\n        \"iad\": 162,\n        \"syd\": 598,\n        \"jnb\": 592,\n        \"gru\": 326,\n        \"hkg\": 340\n      },\n      {\n        \"timestamp\": \"Feb 4, 02:00\",\n        \"hkg\": 303,\n        \"ams\": 89,\n        \"iad\": 216,\n        \"jnb\": 567,\n        \"syd\": 445,\n        \"gru\": 442\n      },\n      {\n        \"timestamp\": \"Feb 4, 03:00\",\n        \"hkg\": 379,\n        \"jnb\": 591,\n        \"syd\": 521,\n        \"ams\": 86,\n        \"iad\": 276,\n        \"gru\": 307\n      },\n      {\n        \"timestamp\": \"Feb 4, 04:00\",\n        \"hkg\": 278,\n        \"gru\": 390,\n        \"syd\": 417,\n        \"jnb\": 560,\n        \"ams\": 84,\n        \"iad\": 218\n      },\n      {\n        \"timestamp\": \"Feb 4, 05:00\",\n        \"gru\": 352,\n        \"syd\": 443,\n        \"jnb\": 591,\n        \"ams\": 80,\n        \"iad\": 183,\n        \"hkg\": 384\n      },\n      {\n        \"timestamp\": \"Feb 4, 06:00\",\n        \"jnb\": 558,\n        \"syd\": 532,\n        \"ams\": 83,\n        \"iad\": 152,\n        \"gru\": 416,\n        \"hkg\": 374\n      },\n      {\n        \"timestamp\": \"Feb 4, 07:00\",\n        \"hkg\": 371,\n        \"jnb\": 583,\n        \"syd\": 508,\n        \"ams\": 79,\n        \"iad\": 214,\n        \"gru\": 348\n      },\n      {\n        \"timestamp\": \"Feb 4, 08:00\",\n        \"hkg\": 364,\n        \"gru\": 283,\n        \"syd\": 386,\n        \"jnb\": 561,\n        \"ams\": 89,\n        \"iad\": 173\n      },\n      {\n        \"timestamp\": \"Feb 4, 09:00\",\n        \"hkg\": 345,\n        \"iad\": 158,\n        \"ams\": 98,\n        \"jnb\": 590,\n        \"syd\": 596,\n        \"gru\": 354\n      },\n      {\n        \"timestamp\": \"Feb 4, 10:00\",\n        \"hkg\": 365,\n        \"gru\": 270,\n        \"jnb\": 608,\n        \"syd\": 443,\n        \"iad\": 176,\n        \"ams\": 84\n      },\n      {\n        \"timestamp\": \"Feb 4, 11:00\",\n        \"gru\": 389,\n        \"syd\": 412,\n        \"jnb\": 606,\n        \"ams\": 92,\n        \"iad\": 217,\n        \"hkg\": 431\n      },\n      {\n        \"timestamp\": \"Feb 4, 12:00\",\n        \"gru\": 372,\n        \"iad\": 236,\n        \"ams\": 103,\n        \"syd\": 574,\n        \"jnb\": 571,\n        \"hkg\": 323\n      },\n      {\n        \"timestamp\": \"Feb 4, 13:00\",\n        \"syd\": 504,\n        \"jnb\": 640,\n        \"ams\": 105,\n        \"iad\": 250,\n        \"gru\": 374,\n        \"hkg\": 303\n      },\n      {\n        \"timestamp\": \"Feb 4, 14:00\",\n        \"hkg\": 350,\n        \"gru\": 296,\n        \"ams\": 110,\n        \"iad\": 148,\n        \"jnb\": 570,\n        \"syd\": 392\n      },\n      {\n        \"timestamp\": \"Feb 4, 15:00\",\n        \"hkg\": 329,\n        \"iad\": 278,\n        \"ams\": 126,\n        \"syd\": 483,\n        \"jnb\": 588,\n        \"gru\": 287\n      },\n      {\n        \"timestamp\": \"Feb 4, 16:00\",\n        \"ams\": 118,\n        \"iad\": 180,\n        \"syd\": 407,\n        \"jnb\": 576,\n        \"gru\": 346,\n        \"hkg\": 332\n      },\n      {\n        \"timestamp\": \"Feb 4, 17:00\",\n        \"jnb\": 712,\n        \"syd\": 434,\n        \"ams\": 174,\n        \"iad\": 182,\n        \"gru\": 378,\n        \"hkg\": 306\n      },\n      {\n        \"timestamp\": \"Feb 4, 18:00\",\n        \"gru\": 410,\n        \"jnb\": 610,\n        \"syd\": 442,\n        \"ams\": 116,\n        \"iad\": 250,\n        \"hkg\": 380\n      },\n      {\n        \"timestamp\": \"Feb 4, 19:00\",\n        \"syd\": 430,\n        \"jnb\": 623,\n        \"iad\": 162,\n        \"ams\": 230,\n        \"gru\": 300,\n        \"hkg\": 330\n      },\n      {\n        \"timestamp\": \"Feb 4, 20:00\",\n        \"ams\": 102,\n        \"iad\": 209,\n        \"syd\": 441,\n        \"jnb\": 611,\n        \"gru\": 278,\n        \"hkg\": 368\n      },\n      {\n        \"timestamp\": \"Feb 4, 21:00\",\n        \"hkg\": 313,\n        \"syd\": 404,\n        \"jnb\": 613,\n        \"iad\": 234,\n        \"ams\": 89,\n        \"gru\": 385\n      },\n      {\n        \"timestamp\": \"Feb 4, 22:00\",\n        \"hkg\": 444,\n        \"gru\": 415,\n        \"iad\": 215,\n        \"ams\": 189,\n        \"syd\": 431,\n        \"jnb\": 575\n      },\n      {\n        \"timestamp\": \"Feb 4, 23:00\",\n        \"jnb\": 582,\n        \"syd\": 426,\n        \"iad\": 214,\n        \"ams\": 97,\n        \"gru\": 360,\n        \"hkg\": 270\n      },\n      {\n        \"timestamp\": \"Feb 5, 00:00\",\n        \"gru\": 309,\n        \"jnb\": 585,\n        \"syd\": 524,\n        \"ams\": 92,\n        \"iad\": 223,\n        \"hkg\": 393\n      },\n      {\n        \"timestamp\": \"Feb 5, 01:00\",\n        \"gru\": 284,\n        \"iad\": 185,\n        \"ams\": 86,\n        \"syd\": 536,\n        \"jnb\": 561,\n        \"hkg\": 273\n      },\n      {\n        \"timestamp\": \"Feb 5, 02:00\",\n        \"syd\": 439,\n        \"jnb\": 558,\n        \"iad\": 170,\n        \"ams\": 81,\n        \"gru\": 310,\n        \"hkg\": 324\n      },\n      {\n        \"timestamp\": \"Feb 5, 03:00\",\n        \"gru\": 354,\n        \"ams\": 78,\n        \"iad\": 185,\n        \"jnb\": 581,\n        \"syd\": 452,\n        \"hkg\": 334\n      },\n      {\n        \"timestamp\": \"Feb 5, 04:00\",\n        \"gru\": 308,\n        \"jnb\": 595,\n        \"syd\": 510,\n        \"iad\": 214,\n        \"ams\": 108,\n        \"hkg\": 325\n      },\n      {\n        \"timestamp\": \"Feb 5, 05:00\",\n        \"ams\": 82,\n        \"iad\": 268,\n        \"jnb\": 564,\n        \"syd\": 513,\n        \"gru\": 356,\n        \"hkg\": 387\n      },\n      {\n        \"timestamp\": \"Feb 5, 06:00\",\n        \"hkg\": 299,\n        \"jnb\": 568,\n        \"syd\": 539,\n        \"ams\": 80,\n        \"iad\": 227,\n        \"gru\": 274\n      },\n      {\n        \"timestamp\": \"Feb 5, 07:00\",\n        \"hkg\": 324,\n        \"gru\": 439,\n        \"syd\": 480,\n        \"jnb\": 567,\n        \"iad\": 148,\n        \"ams\": 82\n      },\n      {\n        \"timestamp\": \"Feb 5, 08:00\",\n        \"jnb\": 568,\n        \"syd\": 528,\n        \"ams\": 84,\n        \"iad\": 174,\n        \"gru\": 380,\n        \"hkg\": 333\n      },\n      {\n        \"timestamp\": \"Feb 5, 09:00\",\n        \"ams\": 85,\n        \"iad\": 227,\n        \"syd\": 612,\n        \"jnb\": 618,\n        \"gru\": 346,\n        \"hkg\": 405\n      },\n      {\n        \"timestamp\": \"Feb 5, 10:00\",\n        \"hkg\": 360,\n        \"syd\": 541,\n        \"jnb\": 564,\n        \"ams\": 86,\n        \"iad\": 177,\n        \"gru\": 352\n      },\n      {\n        \"timestamp\": \"Feb 5, 11:00\",\n        \"hkg\": 417,\n        \"gru\": 435,\n        \"jnb\": 583,\n        \"syd\": 468,\n        \"iad\": 178,\n        \"ams\": 102\n      },\n      {\n        \"timestamp\": \"Feb 5, 12:00\",\n        \"hkg\": 300,\n        \"gru\": 358,\n        \"ams\": 105,\n        \"iad\": 207,\n        \"jnb\": 564,\n        \"syd\": 427\n      },\n      {\n        \"timestamp\": \"Feb 5, 13:00\",\n        \"hkg\": 370,\n        \"iad\": 188,\n        \"ams\": 114,\n        \"jnb\": 602,\n        \"syd\": 420,\n        \"gru\": 292\n      },\n      {\n        \"timestamp\": \"Feb 5, 14:00\",\n        \"hkg\": 375,\n        \"gru\": 439,\n        \"jnb\": 580,\n        \"syd\": 590,\n        \"iad\": 156,\n        \"ams\": 91\n      },\n      {\n        \"timestamp\": \"Feb 5, 15:00\",\n        \"gru\": 374,\n        \"syd\": 570,\n        \"jnb\": 587,\n        \"ams\": 104,\n        \"iad\": 223,\n        \"hkg\": 315\n      },\n      {\n        \"timestamp\": \"Feb 5, 16:00\",\n        \"syd\": 421,\n        \"jnb\": 603,\n        \"iad\": 203,\n        \"ams\": 94,\n        \"gru\": 299,\n        \"hkg\": 305\n      },\n      {\n        \"timestamp\": \"Feb 5, 17:00\",\n        \"hkg\": 306,\n        \"syd\": 495,\n        \"jnb\": 639,\n        \"iad\": 260,\n        \"ams\": 106,\n        \"gru\": 436\n      },\n      {\n        \"timestamp\": \"Feb 5, 18:00\",\n        \"hkg\": 374,\n        \"gru\": 294,\n        \"jnb\": 570,\n        \"syd\": 496,\n        \"iad\": 228,\n        \"ams\": 92\n      },\n      {\n        \"timestamp\": \"Feb 5, 19:00\",\n        \"gru\": 356,\n        \"ams\": 114,\n        \"iad\": 172,\n        \"jnb\": 577,\n        \"syd\": 412,\n        \"hkg\": 367\n      },\n      {\n        \"timestamp\": \"Feb 5, 20:00\",\n        \"ams\": 101,\n        \"iad\": 261,\n        \"syd\": 438,\n        \"jnb\": 623,\n        \"gru\": 411,\n        \"hkg\": 275\n      },\n      {\n        \"timestamp\": \"Feb 5, 21:00\",\n        \"ams\": 96,\n        \"iad\": 244,\n        \"jnb\": 596,\n        \"syd\": 610,\n        \"gru\": 317,\n        \"hkg\": 301\n      },\n      {\n        \"timestamp\": \"Feb 5, 22:00\",\n        \"gru\": 369,\n        \"syd\": 466,\n        \"jnb\": 616,\n        \"ams\": 89,\n        \"iad\": 188,\n        \"hkg\": 388\n      },\n      {\n        \"timestamp\": \"Feb 5, 23:00\",\n        \"iad\": 175,\n        \"ams\": 86,\n        \"syd\": 541,\n        \"jnb\": 735,\n        \"gru\": 376,\n        \"hkg\": 291\n      },\n      {\n        \"timestamp\": \"Feb 6, 00:00\",\n        \"hkg\": 302,\n        \"iad\": 220,\n        \"ams\": 90,\n        \"jnb\": 572,\n        \"syd\": 429,\n        \"gru\": 306\n      },\n      {\n        \"timestamp\": \"Feb 6, 01:00\",\n        \"hkg\": 286,\n        \"jnb\": 596,\n        \"syd\": 443,\n        \"ams\": 92,\n        \"iad\": 240,\n        \"gru\": 360\n      },\n      {\n        \"timestamp\": \"Feb 6, 02:00\",\n        \"gru\": 389,\n        \"jnb\": 568,\n        \"syd\": 443,\n        \"ams\": 82,\n        \"iad\": 271,\n        \"hkg\": 304\n      },\n      {\n        \"timestamp\": \"Feb 6, 03:00\",\n        \"iad\": 167,\n        \"ams\": 84,\n        \"jnb\": 582,\n        \"syd\": 506,\n        \"gru\": 344,\n        \"hkg\": 407\n      },\n      {\n        \"timestamp\": \"Feb 6, 04:00\",\n        \"hkg\": 331,\n        \"iad\": 203,\n        \"ams\": 79,\n        \"syd\": 552,\n        \"jnb\": 563,\n        \"gru\": 300\n      },\n      {\n        \"timestamp\": \"Feb 6, 05:00\",\n        \"hkg\": 481,\n        \"iad\": 242,\n        \"ams\": 78,\n        \"jnb\": 610,\n        \"syd\": 446,\n        \"gru\": 297\n      },\n      {\n        \"timestamp\": \"Feb 6, 06:00\",\n        \"hkg\": 292,\n        \"gru\": 319,\n        \"iad\": 149,\n        \"ams\": 78,\n        \"syd\": 446,\n        \"jnb\": 554\n      },\n      {\n        \"timestamp\": \"Feb 6, 07:00\",\n        \"hkg\": 286,\n        \"ams\": 80,\n        \"iad\": 185,\n        \"syd\": 458,\n        \"jnb\": 586,\n        \"gru\": 279\n      },\n      {\n        \"timestamp\": \"Feb 6, 08:00\",\n        \"hkg\": 322,\n        \"gru\": 326,\n        \"ams\": 95,\n        \"iad\": 237,\n        \"jnb\": 582,\n        \"syd\": 530\n      },\n      {\n        \"timestamp\": \"Feb 6, 09:00\",\n        \"hkg\": 290,\n        \"gru\": 306,\n        \"jnb\": 570,\n        \"syd\": 456,\n        \"ams\": 97,\n        \"iad\": 181\n      },\n      {\n        \"timestamp\": \"Feb 6, 10:00\",\n        \"hkg\": 360,\n        \"syd\": 516,\n        \"jnb\": 575,\n        \"ams\": 130,\n        \"iad\": 214,\n        \"gru\": 280\n      },\n      {\n        \"timestamp\": \"Feb 6, 11:00\",\n        \"hkg\": 283,\n        \"syd\": 400,\n        \"jnb\": 566,\n        \"ams\": 108,\n        \"iad\": 202,\n        \"gru\": 332\n      },\n      {\n        \"timestamp\": \"Feb 6, 12:00\",\n        \"hkg\": 450,\n        \"gru\": 335,\n        \"jnb\": 574,\n        \"syd\": 426,\n        \"ams\": 101,\n        \"iad\": 207\n      },\n      {\n        \"timestamp\": \"Feb 6, 13:00\",\n        \"hkg\": 396,\n        \"gru\": 362,\n        \"syd\": 522,\n        \"jnb\": 568,\n        \"ams\": 98,\n        \"iad\": 197\n      },\n      {\n        \"timestamp\": \"Feb 6, 14:00\",\n        \"gru\": 426,\n        \"jnb\": 571,\n        \"syd\": 422,\n        \"ams\": 97,\n        \"iad\": 216,\n        \"hkg\": 364\n      },\n      {\n        \"timestamp\": \"Feb 6, 15:00\",\n        \"iad\": 277,\n        \"ams\": 112,\n        \"jnb\": 578,\n        \"syd\": 524,\n        \"gru\": 320,\n        \"hkg\": 401\n      },\n      {\n        \"timestamp\": \"Feb 6, 16:00\",\n        \"gru\": 574,\n        \"syd\": 424,\n        \"jnb\": 600,\n        \"iad\": 258,\n        \"ams\": 106,\n        \"hkg\": 586\n      },\n      {\n        \"timestamp\": \"Feb 6, 17:00\",\n        \"gru\": 429,\n        \"ams\": 125,\n        \"iad\": 249,\n        \"syd\": 416,\n        \"jnb\": 615,\n        \"hkg\": 310\n      },\n      {\n        \"timestamp\": \"Feb 6, 18:00\",\n        \"hkg\": 300,\n        \"gru\": 356,\n        \"ams\": 148,\n        \"iad\": 242,\n        \"jnb\": 596,\n        \"syd\": 528\n      },\n      {\n        \"timestamp\": \"Feb 6, 19:00\",\n        \"hkg\": 287,\n        \"jnb\": 590,\n        \"syd\": 416,\n        \"iad\": 206,\n        \"ams\": 111,\n        \"gru\": 282\n      },\n      {\n        \"timestamp\": \"Feb 6, 20:00\",\n        \"gru\": 364,\n        \"jnb\": 584,\n        \"syd\": 538,\n        \"iad\": 189,\n        \"ams\": 116,\n        \"hkg\": 319\n      },\n      {\n        \"timestamp\": \"Feb 6, 21:00\",\n        \"gru\": 314,\n        \"syd\": 407,\n        \"jnb\": 595,\n        \"iad\": 182,\n        \"ams\": 115,\n        \"hkg\": 359\n      },\n      {\n        \"timestamp\": \"Feb 6, 22:00\",\n        \"jnb\": 604,\n        \"syd\": 444,\n        \"iad\": 263,\n        \"ams\": 115,\n        \"gru\": 450,\n        \"hkg\": 276\n      },\n      {\n        \"timestamp\": \"Feb 6, 23:00\",\n        \"hkg\": 459,\n        \"ams\": 90,\n        \"iad\": 216,\n        \"jnb\": 603,\n        \"syd\": 455,\n        \"gru\": 371\n      },\n      {\n        \"timestamp\": \"Feb 7, 00:00\",\n        \"hkg\": 288,\n        \"gru\": 335,\n        \"ams\": 82,\n        \"iad\": 254,\n        \"syd\": 618,\n        \"jnb\": 567\n      },\n      {\n        \"timestamp\": \"Feb 7, 01:00\",\n        \"hkg\": 316,\n        \"jnb\": 603,\n        \"syd\": 429,\n        \"iad\": 150,\n        \"ams\": 84,\n        \"gru\": 571\n      },\n      {\n        \"timestamp\": \"Feb 7, 02:00\",\n        \"hkg\": 3871,\n        \"gru\": 3649,\n        \"jnb\": 3905,\n        \"syd\": 3757,\n        \"ams\": 3588,\n        \"iad\": 3716\n      },\n      {\n        \"timestamp\": \"Feb 7, 03:00\",\n        \"gru\": 371,\n        \"syd\": 411,\n        \"jnb\": 595,\n        \"iad\": 221,\n        \"ams\": 84,\n        \"hkg\": 369\n      },\n      {\n        \"timestamp\": \"Feb 7, 04:00\",\n        \"iad\": 228,\n        \"ams\": 83,\n        \"syd\": 440,\n        \"jnb\": 598,\n        \"gru\": 320,\n        \"hkg\": 404\n      },\n      {\n        \"timestamp\": \"Feb 7, 05:00\",\n        \"hkg\": 5563,\n        \"jnb\": 5587,\n        \"syd\": 5626,\n        \"iad\": 5454,\n        \"ams\": 5370,\n        \"gru\": 5528\n      },\n      {\n        \"timestamp\": \"Feb 7, 06:00\",\n        \"hkg\": 466,\n        \"ams\": 81,\n        \"iad\": 206,\n        \"jnb\": 553,\n        \"syd\": 616,\n        \"gru\": 338\n      },\n      {\n        \"timestamp\": \"Feb 7, 07:00\",\n        \"hkg\": 441,\n        \"gru\": 389,\n        \"iad\": 238,\n        \"ams\": 88,\n        \"syd\": 430,\n        \"jnb\": 620\n      },\n      {\n        \"timestamp\": \"Feb 7, 08:00\",\n        \"gru\": 314,\n        \"syd\": 608,\n        \"jnb\": 572,\n        \"iad\": 204,\n        \"ams\": 86,\n        \"hkg\": 459\n      },\n      {\n        \"timestamp\": \"Feb 7, 09:00\",\n        \"gru\": 401,\n        \"iad\": 202,\n        \"ams\": 101,\n        \"jnb\": 561,\n        \"syd\": 406,\n        \"hkg\": 488\n      },\n      {\n        \"timestamp\": \"Feb 7, 10:00\",\n        \"jnb\": 631,\n        \"syd\": 504,\n        \"ams\": 99,\n        \"iad\": 248,\n        \"gru\": 301,\n        \"hkg\": 324\n      },\n      {\n        \"timestamp\": \"Feb 7, 11:00\",\n        \"gru\": 307,\n        \"syd\": 477,\n        \"jnb\": 608,\n        \"iad\": 216,\n        \"ams\": 249,\n        \"hkg\": 530\n      },\n      {\n        \"timestamp\": \"Feb 7, 12:00\",\n        \"hkg\": 309,\n        \"gru\": 335,\n        \"iad\": 180,\n        \"ams\": 92,\n        \"syd\": 856,\n        \"jnb\": 567\n      },\n      {\n        \"timestamp\": \"Feb 7, 13:00\",\n        \"hkg\": 346,\n        \"syd\": 527,\n        \"jnb\": 572,\n        \"ams\": 82,\n        \"iad\": 296,\n        \"gru\": 348\n      },\n      {\n        \"timestamp\": \"Feb 7, 14:00\",\n        \"hkg\": 300,\n        \"iad\": 262,\n        \"ams\": 107,\n        \"syd\": 536,\n        \"jnb\": 616,\n        \"gru\": 453\n      },\n      {\n        \"timestamp\": \"Feb 7, 15:00\",\n        \"gru\": 432,\n        \"ams\": 99,\n        \"iad\": 222,\n        \"syd\": 500,\n        \"jnb\": 593,\n        \"hkg\": 347\n      },\n      {\n        \"timestamp\": \"Feb 7, 16:00\",\n        \"syd\": 433,\n        \"jnb\": 595,\n        \"ams\": 91,\n        \"iad\": 289,\n        \"gru\": 295,\n        \"hkg\": 382\n      },\n      {\n        \"timestamp\": \"Feb 7, 17:00\",\n        \"iad\": 204,\n        \"ams\": 117,\n        \"jnb\": 591,\n        \"syd\": 437,\n        \"gru\": 399,\n        \"hkg\": 285\n      },\n      {\n        \"timestamp\": \"Feb 7, 18:00\",\n        \"gru\": 324,\n        \"ams\": 138,\n        \"iad\": 224,\n        \"jnb\": 584,\n        \"syd\": 525,\n        \"hkg\": 338\n      },\n      {\n        \"timestamp\": \"Feb 7, 19:00\",\n        \"syd\": 508,\n        \"jnb\": 594,\n        \"iad\": 252,\n        \"ams\": 114,\n        \"gru\": 452,\n        \"hkg\": 302\n      },\n      {\n        \"timestamp\": \"Feb 7, 20:00\",\n        \"gru\": 371,\n        \"iad\": 290,\n        \"ams\": 112,\n        \"syd\": 445,\n        \"jnb\": 614,\n        \"hkg\": 284\n      },\n      {\n        \"timestamp\": \"Feb 7, 21:00\",\n        \"hkg\": 366,\n        \"gru\": 367,\n        \"syd\": 614,\n        \"jnb\": 603,\n        \"ams\": 96,\n        \"iad\": 202\n      },\n      {\n        \"timestamp\": \"Feb 7, 22:00\",\n        \"hkg\": 342,\n        \"gru\": 375,\n        \"iad\": 260,\n        \"ams\": 104,\n        \"syd\": 430,\n        \"jnb\": 707\n      },\n      {\n        \"timestamp\": \"Feb 7, 23:00\",\n        \"hkg\": 341,\n        \"ams\": 85,\n        \"iad\": 225,\n        \"jnb\": 616,\n        \"syd\": 444,\n        \"gru\": 422\n      },\n      {\n        \"timestamp\": \"Feb 8, 00:00\",\n        \"hkg\": 302,\n        \"syd\": 442,\n        \"jnb\": 592,\n        \"ams\": 92,\n        \"iad\": 245,\n        \"gru\": 313\n      },\n      {\n        \"timestamp\": \"Feb 8, 01:00\",\n        \"hkg\": 383,\n        \"ams\": 176,\n        \"iad\": 267,\n        \"jnb\": 678,\n        \"syd\": 455,\n        \"gru\": 398\n      },\n      {\n        \"timestamp\": \"Feb 8, 02:00\",\n        \"iad\": 1828,\n        \"ams\": 1791,\n        \"jnb\": 2230,\n        \"syd\": 2200,\n        \"gru\": 2043,\n        \"hkg\": 2009\n      },\n      {\n        \"timestamp\": \"Feb 8, 03:00\",\n        \"gru\": 300,\n        \"ams\": 110,\n        \"iad\": 279,\n        \"syd\": 399,\n        \"jnb\": 589,\n        \"hkg\": 421\n      },\n      {\n        \"timestamp\": \"Feb 8, 04:00\",\n        \"ams\": 79,\n        \"iad\": 190,\n        \"syd\": 430,\n        \"jnb\": 574,\n        \"gru\": 362,\n        \"hkg\": 310\n      },\n      {\n        \"timestamp\": \"Feb 8, 05:00\",\n        \"gru\": 408,\n        \"syd\": 409,\n        \"jnb\": 588,\n        \"iad\": 266,\n        \"ams\": 77,\n        \"hkg\": 368\n      },\n      {\n        \"timestamp\": \"Feb 8, 06:00\",\n        \"gru\": 288,\n        \"ams\": 78,\n        \"iad\": 195,\n        \"syd\": 2958,\n        \"jnb\": 3148,\n        \"hkg\": 2840\n      },\n      {\n        \"timestamp\": \"Feb 8, 07:00\",\n        \"hkg\": 324,\n        \"gru\": 356,\n        \"iad\": 246,\n        \"ams\": 84,\n        \"jnb\": 589,\n        \"syd\": 507\n      },\n      {\n        \"timestamp\": \"Feb 8, 08:00\",\n        \"hkg\": 383,\n        \"jnb\": 565,\n        \"syd\": 423,\n        \"iad\": 274,\n        \"ams\": 86,\n        \"gru\": 320\n      },\n      {\n        \"timestamp\": \"Feb 8, 09:00\",\n        \"gru\": 374,\n        \"jnb\": 574,\n        \"syd\": 430,\n        \"ams\": 83,\n        \"iad\": 236,\n        \"hkg\": 344\n      },\n      {\n        \"timestamp\": \"Feb 8, 10:00\",\n        \"syd\": 566,\n        \"jnb\": 572,\n        \"ams\": 101,\n        \"iad\": 254,\n        \"gru\": 366,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Feb 8, 11:00\",\n        \"hkg\": 338,\n        \"syd\": 460,\n        \"jnb\": 570,\n        \"ams\": 88,\n        \"iad\": 251,\n        \"gru\": 363\n      },\n      {\n        \"timestamp\": \"Feb 8, 12:00\",\n        \"hkg\": 285,\n        \"gru\": 275,\n        \"jnb\": 594,\n        \"syd\": 478,\n        \"ams\": 89,\n        \"iad\": 205\n      },\n      {\n        \"timestamp\": \"Feb 8, 13:00\",\n        \"hkg\": 312,\n        \"iad\": 268,\n        \"ams\": 84,\n        \"syd\": 526,\n        \"jnb\": 626,\n        \"gru\": 293\n      },\n      {\n        \"timestamp\": \"Feb 8, 14:00\",\n        \"hkg\": 321,\n        \"gru\": 399,\n        \"iad\": 204,\n        \"ams\": 116,\n        \"jnb\": 600,\n        \"syd\": 604\n      },\n      {\n        \"timestamp\": \"Feb 8, 15:00\",\n        \"hkg\": 322,\n        \"gru\": 318,\n        \"jnb\": 577,\n        \"syd\": 438,\n        \"iad\": 270,\n        \"ams\": 102\n      },\n      {\n        \"timestamp\": \"Feb 8, 16:00\",\n        \"gru\": 318,\n        \"syd\": 434,\n        \"jnb\": 596,\n        \"iad\": 251,\n        \"ams\": 110,\n        \"hkg\": 310\n      },\n      {\n        \"timestamp\": \"Feb 8, 17:00\",\n        \"gru\": 404,\n        \"syd\": 376,\n        \"jnb\": 602,\n        \"iad\": 201,\n        \"ams\": 94,\n        \"hkg\": 396\n      },\n      {\n        \"timestamp\": \"Feb 8, 18:00\",\n        \"hkg\": 344,\n        \"gru\": 313,\n        \"syd\": 442,\n        \"jnb\": 573,\n        \"ams\": 119,\n        \"iad\": 284\n      },\n      {\n        \"timestamp\": \"Feb 8, 19:00\",\n        \"hkg\": 426,\n        \"syd\": 431,\n        \"jnb\": 572,\n        \"ams\": 108,\n        \"iad\": 219,\n        \"gru\": 305\n      },\n      {\n        \"timestamp\": \"Feb 8, 20:00\",\n        \"jnb\": 591,\n        \"syd\": 478,\n        \"ams\": 102,\n        \"iad\": 210,\n        \"gru\": 360,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Feb 8, 21:00\",\n        \"gru\": 335,\n        \"syd\": 466,\n        \"jnb\": 595,\n        \"iad\": 250,\n        \"ams\": 104,\n        \"hkg\": 298\n      },\n      {\n        \"timestamp\": \"Feb 8, 22:00\",\n        \"iad\": 285,\n        \"ams\": 93,\n        \"jnb\": 574,\n        \"syd\": 354,\n        \"gru\": 390,\n        \"hkg\": 441\n      },\n      {\n        \"timestamp\": \"Feb 8, 23:00\",\n        \"jnb\": 557,\n        \"syd\": 414,\n        \"iad\": 246,\n        \"ams\": 99,\n        \"gru\": 334,\n        \"hkg\": 374\n      },\n      {\n        \"timestamp\": \"Feb 9, 00:00\",\n        \"gru\": 340,\n        \"syd\": 312,\n        \"jnb\": 593,\n        \"iad\": 314,\n        \"ams\": 88,\n        \"hkg\": 291\n      },\n      {\n        \"timestamp\": \"Feb 9, 01:00\",\n        \"gru\": 292,\n        \"syd\": 351,\n        \"jnb\": 558,\n        \"ams\": 79,\n        \"iad\": 221,\n        \"hkg\": 360\n      },\n      {\n        \"timestamp\": \"Feb 9, 02:00\",\n        \"jnb\": 560,\n        \"syd\": 355,\n        \"ams\": 90,\n        \"iad\": 220,\n        \"gru\": 410,\n        \"hkg\": 288\n      },\n      {\n        \"timestamp\": \"Feb 9, 03:00\",\n        \"hkg\": 284,\n        \"gru\": 363,\n        \"syd\": 368,\n        \"jnb\": 555,\n        \"ams\": 85,\n        \"iad\": 290\n      },\n      {\n        \"timestamp\": \"Feb 9, 04:00\",\n        \"hkg\": 457,\n        \"jnb\": 609,\n        \"syd\": 356,\n        \"ams\": 79,\n        \"iad\": 226,\n        \"gru\": 390\n      },\n      {\n        \"timestamp\": \"Feb 9, 05:00\",\n        \"syd\": 342,\n        \"jnb\": 583,\n        \"iad\": 216,\n        \"ams\": 86,\n        \"gru\": 384,\n        \"hkg\": 293\n      },\n      {\n        \"timestamp\": \"Feb 9, 06:00\",\n        \"gru\": 286,\n        \"jnb\": 562,\n        \"syd\": 383,\n        \"iad\": 181,\n        \"ams\": 72,\n        \"hkg\": 387\n      },\n      {\n        \"timestamp\": \"Feb 9, 07:00\",\n        \"gru\": 372,\n        \"iad\": 207,\n        \"ams\": 89,\n        \"jnb\": 582,\n        \"syd\": 417,\n        \"hkg\": 318\n      },\n      {\n        \"timestamp\": \"Feb 9, 08:00\",\n        \"syd\": 472,\n        \"jnb\": 559,\n        \"iad\": 200,\n        \"ams\": 91,\n        \"gru\": 334,\n        \"hkg\": 340\n      },\n      {\n        \"timestamp\": \"Feb 9, 09:00\",\n        \"syd\": 464,\n        \"jnb\": 560,\n        \"iad\": 280,\n        \"ams\": 83,\n        \"gru\": 333,\n        \"hkg\": 352\n      },\n      {\n        \"timestamp\": \"Feb 9, 10:00\",\n        \"hkg\": 320,\n        \"jnb\": 557,\n        \"syd\": 346,\n        \"iad\": 245,\n        \"ams\": 80,\n        \"gru\": 411\n      },\n      {\n        \"timestamp\": \"Feb 9, 11:00\",\n        \"hkg\": 309,\n        \"gru\": 357,\n        \"jnb\": 564,\n        \"syd\": 374,\n        \"iad\": 187,\n        \"ams\": 97\n      },\n      {\n        \"timestamp\": \"Feb 9, 12:00\",\n        \"hkg\": 386,\n        \"gru\": 338,\n        \"ams\": 83,\n        \"iad\": 149,\n        \"syd\": 396,\n        \"jnb\": 654\n      },\n      {\n        \"timestamp\": \"Feb 9, 13:00\",\n        \"hkg\": 388,\n        \"iad\": 246,\n        \"ams\": 82,\n        \"jnb\": 639,\n        \"syd\": 431,\n        \"gru\": 358\n      },\n      {\n        \"timestamp\": \"Feb 9, 14:00\",\n        \"iad\": 218,\n        \"ams\": 102,\n        \"syd\": 378,\n        \"jnb\": 621,\n        \"gru\": 313,\n        \"hkg\": 387\n      },\n      {\n        \"timestamp\": \"Feb 9, 15:00\",\n        \"syd\": 379,\n        \"jnb\": 595,\n        \"iad\": 224,\n        \"ams\": 92,\n        \"gru\": 332,\n        \"hkg\": 304\n      },\n      {\n        \"timestamp\": \"Feb 9, 16:00\",\n        \"gru\": 268,\n        \"jnb\": 632,\n        \"syd\": 491,\n        \"iad\": 253,\n        \"ams\": 86,\n        \"hkg\": 299\n      },\n      {\n        \"timestamp\": \"Feb 9, 17:00\",\n        \"ams\": 92,\n        \"iad\": 275,\n        \"syd\": 394,\n        \"jnb\": 599,\n        \"gru\": 261,\n        \"hkg\": 462\n      },\n      {\n        \"timestamp\": \"Feb 9, 18:00\",\n        \"gru\": 330,\n        \"syd\": 366,\n        \"jnb\": 622,\n        \"ams\": 90,\n        \"iad\": 214,\n        \"hkg\": 386\n      },\n      {\n        \"timestamp\": \"Feb 9, 19:00\",\n        \"hkg\": 350,\n        \"gru\": 366,\n        \"jnb\": 657,\n        \"syd\": 352,\n        \"iad\": 248,\n        \"ams\": 91\n      },\n      {\n        \"timestamp\": \"Feb 9, 20:00\",\n        \"hkg\": 420,\n        \"jnb\": 572,\n        \"syd\": 335,\n        \"ams\": 89,\n        \"iad\": 324,\n        \"gru\": 362\n      },\n      {\n        \"timestamp\": \"Feb 9, 21:00\",\n        \"gru\": 348,\n        \"iad\": 196,\n        \"ams\": 101,\n        \"syd\": 433,\n        \"jnb\": 574,\n        \"hkg\": 452\n      },\n      {\n        \"timestamp\": \"Feb 9, 22:00\",\n        \"syd\": 311,\n        \"jnb\": 571,\n        \"iad\": 232,\n        \"ams\": 73,\n        \"gru\": 311,\n        \"hkg\": 384\n      },\n      {\n        \"timestamp\": \"Feb 9, 23:00\",\n        \"hkg\": 393,\n        \"jnb\": 579,\n        \"syd\": 334,\n        \"ams\": 83,\n        \"iad\": 185,\n        \"gru\": 280\n      },\n      {\n        \"timestamp\": \"Feb 10, 00:00\",\n        \"hkg\": 338,\n        \"iad\": 280,\n        \"ams\": 80,\n        \"jnb\": 566,\n        \"syd\": 451,\n        \"gru\": 424\n      },\n      {\n        \"timestamp\": \"Feb 10, 01:00\",\n        \"hkg\": 284,\n        \"syd\": 431,\n        \"jnb\": 560,\n        \"iad\": 209,\n        \"ams\": 76,\n        \"gru\": 291\n      },\n      {\n        \"timestamp\": \"Feb 10, 02:00\",\n        \"hkg\": 2876,\n        \"gru\": 5579,\n        \"syd\": 371,\n        \"jnb\": 3130,\n        \"iad\": 215,\n        \"ams\": 75\n      },\n      {\n        \"timestamp\": \"Feb 10, 03:00\",\n        \"hkg\": 424,\n        \"jnb\": 559,\n        \"syd\": 329,\n        \"ams\": 74,\n        \"iad\": 152,\n        \"gru\": 650\n      },\n      {\n        \"timestamp\": \"Feb 10, 04:00\",\n        \"jnb\": 609,\n        \"syd\": 372,\n        \"iad\": 185,\n        \"ams\": 78,\n        \"gru\": 295,\n        \"hkg\": 284\n      },\n      {\n        \"timestamp\": \"Feb 10, 05:00\",\n        \"gru\": 417,\n        \"ams\": 76,\n        \"iad\": 161,\n        \"jnb\": 605,\n        \"syd\": 578,\n        \"hkg\": 349\n      },\n      {\n        \"timestamp\": \"Feb 10, 06:00\",\n        \"hkg\": 330,\n        \"gru\": 312,\n        \"syd\": 449,\n        \"jnb\": 602,\n        \"iad\": 182,\n        \"ams\": 76\n      },\n      {\n        \"timestamp\": \"Feb 10, 07:00\",\n        \"hkg\": 352,\n        \"jnb\": 593,\n        \"syd\": 330,\n        \"ams\": 70,\n        \"iad\": 158,\n        \"gru\": 303\n      },\n      {\n        \"timestamp\": \"Feb 10, 08:00\",\n        \"hkg\": 368,\n        \"iad\": 166,\n        \"ams\": 77,\n        \"jnb\": 558,\n        \"syd\": 520,\n        \"gru\": 332\n      },\n      {\n        \"timestamp\": \"Feb 10, 09:00\",\n        \"hkg\": 373,\n        \"syd\": 389,\n        \"jnb\": 587,\n        \"ams\": 76,\n        \"iad\": 181,\n        \"gru\": 421\n      },\n      {\n        \"timestamp\": \"Feb 10, 10:00\",\n        \"hkg\": 366,\n        \"gru\": 322,\n        \"ams\": 86,\n        \"iad\": 176,\n        \"syd\": 408,\n        \"jnb\": 562\n      },\n      {\n        \"timestamp\": \"Feb 10, 11:00\",\n        \"hkg\": 350,\n        \"jnb\": 563,\n        \"syd\": 348,\n        \"iad\": 184,\n        \"ams\": 80,\n        \"gru\": 357\n      },\n      {\n        \"timestamp\": \"Feb 10, 12:00\",\n        \"hkg\": 394,\n        \"gru\": 414,\n        \"jnb\": 608,\n        \"syd\": 472,\n        \"ams\": 83,\n        \"iad\": 153\n      },\n      {\n        \"timestamp\": \"Feb 10, 13:00\",\n        \"gru\": 307,\n        \"iad\": 152,\n        \"ams\": 91,\n        \"jnb\": 634,\n        \"syd\": 435,\n        \"hkg\": 299\n      },\n      {\n        \"timestamp\": \"Feb 10, 14:00\",\n        \"jnb\": 569,\n        \"syd\": 364,\n        \"iad\": 214,\n        \"ams\": 85,\n        \"gru\": 328,\n        \"hkg\": 374\n      },\n      {\n        \"timestamp\": \"Feb 10, 15:00\",\n        \"hkg\": 396,\n        \"gru\": 288,\n        \"jnb\": 581,\n        \"syd\": 372,\n        \"ams\": 76,\n        \"iad\": 180\n      },\n      {\n        \"timestamp\": \"Feb 10, 16:00\",\n        \"hkg\": 308,\n        \"gru\": 311,\n        \"iad\": 159,\n        \"ams\": 86,\n        \"jnb\": 578,\n        \"syd\": 408\n      },\n      {\n        \"timestamp\": \"Feb 10, 17:00\",\n        \"hkg\": 405,\n        \"gru\": 260,\n        \"syd\": 445,\n        \"jnb\": 573,\n        \"iad\": 164,\n        \"ams\": 91\n      },\n      {\n        \"timestamp\": \"Feb 10, 18:00\",\n        \"gru\": 312,\n        \"iad\": 226,\n        \"ams\": 100,\n        \"syd\": 325,\n        \"jnb\": 578,\n        \"hkg\": 311\n      },\n      {\n        \"timestamp\": \"Feb 10, 19:00\",\n        \"ams\": 93,\n        \"iad\": 155,\n        \"jnb\": 591,\n        \"syd\": 548,\n        \"gru\": 419,\n        \"hkg\": 289\n      },\n      {\n        \"timestamp\": \"Feb 10, 20:00\",\n        \"gru\": 290,\n        \"jnb\": 621,\n        \"syd\": 368,\n        \"iad\": 193,\n        \"ams\": 93,\n        \"hkg\": 276\n      },\n      {\n        \"timestamp\": \"Feb 10, 21:00\",\n        \"ams\": 88,\n        \"iad\": 186,\n        \"jnb\": 570,\n        \"syd\": 355,\n        \"gru\": 380,\n        \"hkg\": 429\n      },\n      {\n        \"timestamp\": \"Feb 10, 22:00\",\n        \"hkg\": 453,\n        \"jnb\": 558,\n        \"syd\": 526,\n        \"ams\": 78,\n        \"iad\": 189,\n        \"gru\": 402\n      },\n      {\n        \"timestamp\": \"Feb 10, 23:00\",\n        \"hkg\": 282,\n        \"gru\": 338,\n        \"syd\": 357,\n        \"jnb\": 580,\n        \"iad\": 184,\n        \"ams\": 74\n      },\n      {\n        \"timestamp\": \"Feb 11, 00:00\",\n        \"hkg\": 303,\n        \"gru\": 467,\n        \"ams\": 74,\n        \"iad\": 194,\n        \"syd\": 449,\n        \"jnb\": 583\n      },\n      {\n        \"timestamp\": \"Feb 11, 01:00\",\n        \"hkg\": 386,\n        \"jnb\": 555,\n        \"syd\": 349,\n        \"ams\": 70,\n        \"iad\": 159,\n        \"gru\": 310\n      },\n      {\n        \"timestamp\": \"Feb 11, 02:00\",\n        \"hkg\": 323,\n        \"gru\": 306,\n        \"syd\": 385,\n        \"jnb\": 556,\n        \"ams\": 88,\n        \"iad\": 184\n      },\n      {\n        \"timestamp\": \"Feb 11, 03:00\",\n        \"gru\": 310,\n        \"iad\": 180,\n        \"ams\": 123,\n        \"syd\": 397,\n        \"jnb\": 607,\n        \"hkg\": 440\n      },\n      {\n        \"timestamp\": \"Feb 11, 04:00\",\n        \"iad\": 240,\n        \"ams\": 84,\n        \"jnb\": 554,\n        \"syd\": 433,\n        \"gru\": 373,\n        \"hkg\": 364\n      },\n      {\n        \"timestamp\": \"Feb 11, 05:00\",\n        \"gru\": 271,\n        \"iad\": 156,\n        \"ams\": 91,\n        \"syd\": 373,\n        \"jnb\": 587,\n        \"hkg\": 298\n      },\n      {\n        \"timestamp\": \"Feb 11, 06:00\",\n        \"iad\": 178,\n        \"ams\": 101,\n        \"jnb\": 582,\n        \"syd\": 482,\n        \"gru\": 674,\n        \"hkg\": 323\n      },\n      {\n        \"timestamp\": \"Feb 11, 07:00\",\n        \"hkg\": 399,\n        \"jnb\": 659,\n        \"syd\": 371,\n        \"ams\": 115,\n        \"iad\": 210,\n        \"gru\": 285\n      },\n      {\n        \"timestamp\": \"Feb 11, 08:00\",\n        \"hkg\": 426,\n        \"gru\": 322,\n        \"syd\": 464,\n        \"jnb\": 585,\n        \"ams\": 89,\n        \"iad\": 146\n      },\n      {\n        \"timestamp\": \"Feb 11, 09:00\",\n        \"hkg\": 347,\n        \"syd\": 357,\n        \"jnb\": 567,\n        \"iad\": 180,\n        \"ams\": 76,\n        \"gru\": 290\n      },\n      {\n        \"timestamp\": \"Feb 11, 10:00\",\n        \"gru\": 390,\n        \"syd\": 383,\n        \"jnb\": 567,\n        \"iad\": 178,\n        \"ams\": 83,\n        \"hkg\": 341\n      },\n      {\n        \"timestamp\": \"Feb 11, 11:00\",\n        \"ams\": 81,\n        \"iad\": 186,\n        \"syd\": 335,\n        \"jnb\": 598,\n        \"gru\": 284,\n        \"hkg\": 516\n      },\n      {\n        \"timestamp\": \"Feb 11, 12:00\",\n        \"hkg\": 324,\n        \"ams\": 166,\n        \"iad\": 214,\n        \"jnb\": 595,\n        \"syd\": 408,\n        \"gru\": 435\n      },\n      {\n        \"timestamp\": \"Feb 11, 13:00\",\n        \"hkg\": 273,\n        \"gru\": 408,\n        \"syd\": 366,\n        \"jnb\": 562,\n        \"ams\": 96,\n        \"iad\": 146\n      },\n      {\n        \"timestamp\": \"Feb 11, 14:00\",\n        \"hkg\": 355,\n        \"ams\": 89,\n        \"iad\": 189,\n        \"jnb\": 565,\n        \"syd\": 417,\n        \"gru\": 273\n      },\n      {\n        \"timestamp\": \"Feb 11, 15:00\",\n        \"hkg\": 383,\n        \"gru\": 389,\n        \"jnb\": 570,\n        \"syd\": 342,\n        \"iad\": 182,\n        \"ams\": 84\n      },\n      {\n        \"timestamp\": \"Feb 11, 16:00\",\n        \"gru\": 474,\n        \"syd\": 355,\n        \"jnb\": 591,\n        \"iad\": 211,\n        \"ams\": 107,\n        \"hkg\": 328\n      },\n      {\n        \"timestamp\": \"Feb 11, 17:00\",\n        \"hkg\": 414,\n        \"gru\": 392,\n        \"syd\": 376,\n        \"jnb\": 566,\n        \"iad\": 162,\n        \"ams\": 105\n      },\n      {\n        \"timestamp\": \"Feb 11, 18:00\",\n        \"hkg\": 358,\n        \"jnb\": 596,\n        \"syd\": 446,\n        \"iad\": 215,\n        \"ams\": 95,\n        \"gru\": 306\n      },\n      {\n        \"timestamp\": \"Feb 11, 19:00\",\n        \"iad\": 156,\n        \"ams\": 94,\n        \"syd\": 446,\n        \"jnb\": 623,\n        \"gru\": 390,\n        \"hkg\": 323\n      },\n      {\n        \"timestamp\": \"Feb 11, 20:00\",\n        \"gru\": 430,\n        \"iad\": 160,\n        \"ams\": 102,\n        \"jnb\": 597,\n        \"syd\": 568,\n        \"hkg\": 2969\n      },\n      {\n        \"timestamp\": \"Feb 11, 21:00\",\n        \"hkg\": 382,\n        \"gru\": 374,\n        \"syd\": 325,\n        \"jnb\": 568,\n        \"iad\": 186,\n        \"ams\": 132\n      },\n      {\n        \"timestamp\": \"Feb 11, 22:00\",\n        \"hkg\": 297,\n        \"jnb\": 562,\n        \"syd\": 402,\n        \"iad\": 185,\n        \"ams\": 138,\n        \"gru\": 309\n      },\n      {\n        \"timestamp\": \"Feb 11, 23:00\",\n        \"hkg\": 429,\n        \"gru\": 308,\n        \"jnb\": 575,\n        \"syd\": 476,\n        \"ams\": 120,\n        \"iad\": 152\n      },\n      {\n        \"timestamp\": \"Feb 12, 00:00\",\n        \"hkg\": 370,\n        \"syd\": 353,\n        \"jnb\": 588,\n        \"ams\": 151,\n        \"iad\": 157,\n        \"gru\": 434\n      },\n      {\n        \"timestamp\": \"Feb 12, 01:00\",\n        \"hkg\": 352,\n        \"gru\": 366,\n        \"syd\": 367,\n        \"jnb\": 562,\n        \"iad\": 156,\n        \"ams\": 82\n      },\n      {\n        \"timestamp\": \"Feb 12, 02:00\",\n        \"gru\": 315,\n        \"jnb\": 589,\n        \"syd\": 438,\n        \"iad\": 194,\n        \"ams\": 78,\n        \"hkg\": 309\n      },\n      {\n        \"timestamp\": \"Feb 12, 03:00\",\n        \"ams\": 88,\n        \"iad\": 154,\n        \"jnb\": 550,\n        \"syd\": 428,\n        \"gru\": 327,\n        \"hkg\": 298\n      },\n      {\n        \"timestamp\": \"Feb 12, 04:00\",\n        \"hkg\": 326,\n        \"gru\": 403,\n        \"ams\": 102,\n        \"iad\": 148,\n        \"jnb\": 585,\n        \"syd\": 377\n      },\n      {\n        \"timestamp\": \"Feb 12, 05:00\",\n        \"hkg\": 300,\n        \"syd\": 359,\n        \"jnb\": 550,\n        \"ams\": 115,\n        \"iad\": 159,\n        \"gru\": 391\n      },\n      {\n        \"timestamp\": \"Feb 12, 06:00\",\n        \"jnb\": 574,\n        \"syd\": 358,\n        \"ams\": 119,\n        \"iad\": 146,\n        \"gru\": 356,\n        \"hkg\": 342\n      },\n      {\n        \"timestamp\": \"Feb 12, 07:00\",\n        \"gru\": 280,\n        \"iad\": 178,\n        \"ams\": 82,\n        \"jnb\": 564,\n        \"syd\": 507,\n        \"hkg\": 322\n      },\n      {\n        \"timestamp\": \"Feb 12, 08:00\",\n        \"syd\": 382,\n        \"jnb\": 564,\n        \"iad\": 178,\n        \"ams\": 98,\n        \"gru\": 375,\n        \"hkg\": 383\n      },\n      {\n        \"timestamp\": \"Feb 12, 09:00\",\n        \"gru\": 286,\n        \"jnb\": 561,\n        \"syd\": 396,\n        \"ams\": 79,\n        \"iad\": 149,\n        \"hkg\": 289\n      },\n      {\n        \"timestamp\": \"Feb 12, 10:00\",\n        \"gru\": 336,\n        \"iad\": 147,\n        \"ams\": 87,\n        \"jnb\": 614,\n        \"syd\": 528,\n        \"hkg\": 279\n      },\n      {\n        \"timestamp\": \"Feb 12, 11:00\",\n        \"hkg\": 430,\n        \"gru\": 308,\n        \"ams\": 92,\n        \"iad\": 181,\n        \"syd\": 365,\n        \"jnb\": 565\n      },\n      {\n        \"timestamp\": \"Feb 12, 12:00\",\n        \"hkg\": 369,\n        \"syd\": 438,\n        \"jnb\": 588,\n        \"ams\": 101,\n        \"iad\": 147,\n        \"gru\": 340\n      },\n      {\n        \"timestamp\": \"Feb 12, 13:00\",\n        \"hkg\": 389,\n        \"syd\": 467,\n        \"jnb\": 601,\n        \"ams\": 107,\n        \"iad\": 149,\n        \"gru\": 371\n      },\n      {\n        \"timestamp\": \"Feb 12, 14:00\",\n        \"hkg\": 392,\n        \"gru\": 2931,\n        \"syd\": 432,\n        \"jnb\": 596,\n        \"iad\": 2778,\n        \"ams\": 140\n      },\n      {\n        \"timestamp\": \"Feb 12, 15:00\",\n        \"gru\": 288,\n        \"jnb\": 642,\n        \"syd\": 388,\n        \"ams\": 145,\n        \"iad\": 157,\n        \"hkg\": 400\n      },\n      {\n        \"timestamp\": \"Feb 12, 16:00\",\n        \"ams\": 120,\n        \"iad\": 238,\n        \"jnb\": 598,\n        \"syd\": 389,\n        \"gru\": 377,\n        \"hkg\": 422\n      },\n      {\n        \"timestamp\": \"Feb 12, 17:00\",\n        \"ams\": 109,\n        \"iad\": 176,\n        \"syd\": 389,\n        \"jnb\": 618,\n        \"gru\": 392,\n        \"hkg\": 319\n      },\n      {\n        \"timestamp\": \"Feb 12, 18:00\",\n        \"syd\": 536,\n        \"jnb\": 604,\n        \"iad\": 170,\n        \"ams\": 87,\n        \"gru\": 332,\n        \"hkg\": 308\n      },\n      {\n        \"timestamp\": \"Feb 12, 19:00\",\n        \"gru\": 379,\n        \"jnb\": 612,\n        \"syd\": 374,\n        \"ams\": 95,\n        \"iad\": 168,\n        \"hkg\": 386\n      },\n      {\n        \"timestamp\": \"Feb 12, 20:00\",\n        \"hkg\": 361,\n        \"gru\": 452,\n        \"ams\": 112,\n        \"iad\": 158,\n        \"jnb\": 576,\n        \"syd\": 470\n      },\n      {\n        \"timestamp\": \"Feb 12, 21:00\",\n        \"hkg\": 484,\n        \"jnb\": 570,\n        \"syd\": 364,\n        \"iad\": 191,\n        \"ams\": 94,\n        \"gru\": 379\n      },\n      {\n        \"timestamp\": \"Feb 12, 22:00\",\n        \"gru\": 298,\n        \"syd\": 479,\n        \"jnb\": 642,\n        \"ams\": 94,\n        \"iad\": 188,\n        \"hkg\": 286\n      },\n      {\n        \"timestamp\": \"Feb 12, 23:00\",\n        \"jnb\": 584,\n        \"syd\": 401,\n        \"iad\": 184,\n        \"ams\": 84,\n        \"gru\": 358,\n        \"hkg\": 316\n      },\n      {\n        \"timestamp\": \"Feb 13, 00:00\",\n        \"hkg\": 437,\n        \"iad\": 155,\n        \"ams\": 77,\n        \"jnb\": 578,\n        \"syd\": 445,\n        \"gru\": 430\n      },\n      {\n        \"timestamp\": \"Feb 13, 01:00\",\n        \"gru\": 294,\n        \"syd\": 394,\n        \"jnb\": 579,\n        \"ams\": 96,\n        \"iad\": 160,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Feb 13, 02:00\",\n        \"gru\": 289,\n        \"iad\": 158,\n        \"ams\": 82,\n        \"syd\": 404,\n        \"jnb\": 601,\n        \"hkg\": 318\n      },\n      {\n        \"timestamp\": \"Feb 13, 03:00\",\n        \"jnb\": 558,\n        \"syd\": 362,\n        \"ams\": 82,\n        \"iad\": 163,\n        \"gru\": 304,\n        \"hkg\": 321\n      },\n      {\n        \"timestamp\": \"Feb 13, 04:00\",\n        \"gru\": 462,\n        \"ams\": 87,\n        \"iad\": 153,\n        \"jnb\": 582,\n        \"syd\": 585,\n        \"hkg\": 264\n      },\n      {\n        \"timestamp\": \"Feb 13, 05:00\",\n        \"hkg\": 348,\n        \"gru\": 364,\n        \"jnb\": 567,\n        \"syd\": 463,\n        \"iad\": 200,\n        \"ams\": 124\n      },\n      {\n        \"timestamp\": \"Feb 13, 06:00\",\n        \"hkg\": 426,\n        \"jnb\": 580,\n        \"syd\": 501,\n        \"ams\": 88,\n        \"iad\": 142,\n        \"gru\": 453\n      },\n      {\n        \"timestamp\": \"Feb 13, 07:00\",\n        \"jnb\": 586,\n        \"syd\": 483,\n        \"ams\": 78,\n        \"iad\": 179,\n        \"gru\": 351,\n        \"hkg\": 401\n      },\n      {\n        \"timestamp\": \"Feb 13, 08:00\",\n        \"gru\": 295,\n        \"ams\": 92,\n        \"iad\": 176,\n        \"jnb\": 563,\n        \"syd\": 435,\n        \"hkg\": 344\n      },\n      {\n        \"timestamp\": \"Feb 13, 09:00\",\n        \"hkg\": 377,\n        \"syd\": 403,\n        \"jnb\": 578,\n        \"ams\": 97,\n        \"iad\": 148,\n        \"gru\": 562\n      },\n      {\n        \"timestamp\": \"Feb 13, 10:00\",\n        \"hkg\": 307,\n        \"iad\": 147,\n        \"ams\": 94,\n        \"syd\": 392,\n        \"jnb\": 565,\n        \"gru\": 354\n      },\n      {\n        \"timestamp\": \"Feb 13, 11:00\",\n        \"hkg\": 402,\n        \"gru\": 386,\n        \"ams\": 129,\n        \"iad\": 182,\n        \"jnb\": 567,\n        \"syd\": 552\n      },\n      {\n        \"timestamp\": \"Feb 13, 12:00\",\n        \"hkg\": 413,\n        \"ams\": 111,\n        \"iad\": 148,\n        \"jnb\": 620,\n        \"syd\": 479,\n        \"gru\": 341\n      },\n      {\n        \"timestamp\": \"Feb 13, 13:00\",\n        \"hkg\": 342,\n        \"gru\": 376,\n        \"jnb\": 603,\n        \"syd\": 407,\n        \"iad\": 187,\n        \"ams\": 93\n      },\n      {\n        \"timestamp\": \"Feb 13, 14:00\",\n        \"gru\": 414,\n        \"jnb\": 691,\n        \"syd\": 400,\n        \"ams\": 106,\n        \"iad\": 181,\n        \"hkg\": 297\n      },\n      {\n        \"timestamp\": \"Feb 13, 15:00\",\n        \"syd\": 331,\n        \"jnb\": 585,\n        \"iad\": 232,\n        \"ams\": 138,\n        \"gru\": 376,\n        \"hkg\": 403\n      },\n      {\n        \"timestamp\": \"Feb 13, 16:00\",\n        \"hkg\": 6906,\n        \"gru\": 6822,\n        \"iad\": 6801,\n        \"ams\": 6745,\n        \"jnb\": 7075,\n        \"syd\": 6918\n      },\n      {\n        \"timestamp\": \"Feb 13, 17:00\",\n        \"hkg\": 342,\n        \"ams\": 122,\n        \"iad\": 166,\n        \"syd\": 413,\n        \"jnb\": 777,\n        \"gru\": 439\n      },\n      {\n        \"timestamp\": \"Feb 13, 18:00\",\n        \"ams\": 118,\n        \"iad\": 192,\n        \"jnb\": 630,\n        \"syd\": 359,\n        \"gru\": 342,\n        \"hkg\": 328\n      },\n      {\n        \"timestamp\": \"Feb 13, 19:00\",\n        \"jnb\": 612,\n        \"syd\": 414,\n        \"ams\": 126,\n        \"iad\": 227,\n        \"gru\": 279,\n        \"hkg\": 302\n      },\n      {\n        \"timestamp\": \"Feb 13, 20:00\",\n        \"gru\": 323,\n        \"syd\": 426,\n        \"jnb\": 627,\n        \"ams\": 234,\n        \"iad\": 208,\n        \"hkg\": 300\n      },\n      {\n        \"timestamp\": \"Feb 13, 21:00\",\n        \"iad\": 161,\n        \"ams\": 107,\n        \"jnb\": 1070,\n        \"syd\": 494,\n        \"gru\": 391,\n        \"hkg\": 373\n      },\n      {\n        \"timestamp\": \"Feb 13, 22:00\",\n        \"gru\": 351,\n        \"iad\": 167,\n        \"ams\": 117,\n        \"syd\": 337,\n        \"jnb\": 709,\n        \"hkg\": 328\n      },\n      {\n        \"timestamp\": \"Feb 13, 23:00\",\n        \"hkg\": 352,\n        \"gru\": 298,\n        \"iad\": 163,\n        \"ams\": 86,\n        \"syd\": 452,\n        \"jnb\": 655\n      },\n      {\n        \"timestamp\": \"Feb 14, 00:00\",\n        \"hkg\": 319,\n        \"iad\": 151,\n        \"ams\": 89,\n        \"jnb\": 571,\n        \"syd\": 351,\n        \"gru\": 299\n      },\n      {\n        \"timestamp\": \"Feb 14, 01:00\",\n        \"hkg\": 438,\n        \"gru\": 394,\n        \"iad\": 159,\n        \"ams\": 84,\n        \"syd\": 373,\n        \"jnb\": 632\n      },\n      {\n        \"timestamp\": \"Feb 14, 02:00\",\n        \"hkg\": 397,\n        \"iad\": 170,\n        \"ams\": 93,\n        \"jnb\": 573,\n        \"syd\": 350,\n        \"gru\": 326\n      },\n      {\n        \"timestamp\": \"Feb 14, 03:00\",\n        \"hkg\": 441,\n        \"jnb\": 568,\n        \"syd\": 472,\n        \"iad\": 196,\n        \"ams\": 74,\n        \"gru\": 377\n      },\n      {\n        \"timestamp\": \"Feb 14, 04:00\",\n        \"syd\": 436,\n        \"jnb\": 557,\n        \"iad\": 161,\n        \"ams\": 79,\n        \"gru\": 332,\n        \"hkg\": 301\n      },\n      {\n        \"timestamp\": \"Feb 14, 05:00\",\n        \"gru\": 4516,\n        \"jnb\": 4770,\n        \"syd\": 4708,\n        \"ams\": 4297,\n        \"iad\": 4353,\n        \"hkg\": 4479\n      },\n      {\n        \"timestamp\": \"Feb 14, 06:00\",\n        \"ams\": 85,\n        \"iad\": 180,\n        \"syd\": 398,\n        \"jnb\": 585,\n        \"gru\": 350,\n        \"hkg\": 370\n      },\n      {\n        \"timestamp\": \"Feb 14, 07:00\",\n        \"gru\": 329,\n        \"ams\": 78,\n        \"iad\": 147,\n        \"syd\": 541,\n        \"jnb\": 593,\n        \"hkg\": 290\n      },\n      {\n        \"timestamp\": \"Feb 14, 08:00\",\n        \"hkg\": 330,\n        \"gru\": 341,\n        \"ams\": 84,\n        \"iad\": 152,\n        \"jnb\": 574,\n        \"syd\": 416\n      },\n      {\n        \"timestamp\": \"Feb 14, 09:00\",\n        \"iad\": 189,\n        \"ams\": 95,\n        \"jnb\": 593,\n        \"syd\": 415,\n        \"gru\": 289,\n        \"hkg\": 349\n      },\n      {\n        \"timestamp\": \"Feb 14, 10:00\",\n        \"hkg\": 343,\n        \"gru\": 391,\n        \"iad\": 150,\n        \"ams\": 92,\n        \"jnb\": 605,\n        \"syd\": 340\n      },\n      {\n        \"timestamp\": \"Feb 14, 11:00\",\n        \"hkg\": 359,\n        \"gru\": 289,\n        \"jnb\": 663,\n        \"syd\": 382,\n        \"iad\": 182,\n        \"ams\": 87\n      },\n      {\n        \"timestamp\": \"Feb 14, 12:00\",\n        \"hkg\": 386,\n        \"syd\": 434,\n        \"jnb\": 606,\n        \"iad\": 219,\n        \"ams\": 100,\n        \"gru\": 394\n      },\n      {\n        \"timestamp\": \"Feb 14, 13:00\",\n        \"jnb\": 646,\n        \"syd\": 460,\n        \"ams\": 80,\n        \"iad\": 150,\n        \"gru\": 277,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Feb 14, 14:00\",\n        \"gru\": 362,\n        \"syd\": 436,\n        \"jnb\": 667,\n        \"ams\": 101,\n        \"iad\": 195,\n        \"hkg\": 454\n      },\n      {\n        \"timestamp\": \"Feb 14, 15:00\",\n        \"ams\": 87,\n        \"iad\": 205,\n        \"syd\": 434,\n        \"jnb\": 632,\n        \"gru\": 360,\n        \"hkg\": 320\n      },\n      {\n        \"timestamp\": \"Feb 14, 16:00\",\n        \"gru\": 290,\n        \"ams\": 92,\n        \"iad\": 164,\n        \"jnb\": 576,\n        \"syd\": 326,\n        \"hkg\": 313\n      },\n      {\n        \"timestamp\": \"Feb 14, 17:00\",\n        \"hkg\": 292,\n        \"ams\": 88,\n        \"iad\": 153,\n        \"jnb\": 583,\n        \"syd\": 329,\n        \"gru\": 349\n      },\n      {\n        \"timestamp\": \"Feb 14, 18:00\",\n        \"hkg\": 527,\n        \"gru\": 415,\n        \"ams\": 88,\n        \"iad\": 213,\n        \"syd\": 496,\n        \"jnb\": 636\n      },\n      {\n        \"timestamp\": \"Feb 14, 19:00\",\n        \"hkg\": 322,\n        \"gru\": 384,\n        \"syd\": 468,\n        \"jnb\": 589,\n        \"ams\": 89,\n        \"iad\": 160\n      },\n      {\n        \"timestamp\": \"Feb 14, 20:00\",\n        \"gru\": 356,\n        \"syd\": 355,\n        \"jnb\": 599,\n        \"iad\": 214,\n        \"ams\": 97,\n        \"hkg\": 304\n      },\n      {\n        \"timestamp\": \"Feb 14, 21:00\",\n        \"jnb\": 703,\n        \"syd\": 421,\n        \"ams\": 86,\n        \"iad\": 192,\n        \"gru\": 337,\n        \"hkg\": 440\n      },\n      {\n        \"timestamp\": \"Feb 14, 22:00\",\n        \"hkg\": 308,\n        \"syd\": 446,\n        \"jnb\": 572,\n        \"ams\": 96,\n        \"iad\": 161,\n        \"gru\": 352\n      },\n      {\n        \"timestamp\": \"Feb 14, 23:00\",\n        \"hkg\": 307,\n        \"gru\": 306,\n        \"syd\": 316,\n        \"jnb\": 588,\n        \"ams\": 87,\n        \"iad\": 188\n      },\n      {\n        \"timestamp\": \"Feb 15, 00:00\",\n        \"hkg\": 355,\n        \"ams\": 78,\n        \"iad\": 225,\n        \"jnb\": 557,\n        \"syd\": 428,\n        \"gru\": 397\n      },\n      {\n        \"timestamp\": \"Feb 15, 01:00\",\n        \"hkg\": 392,\n        \"gru\": 366,\n        \"iad\": 184,\n        \"ams\": 78,\n        \"syd\": 450,\n        \"jnb\": 546\n      },\n      {\n        \"timestamp\": \"Feb 15, 02:00\",\n        \"gru\": 286,\n        \"syd\": 441,\n        \"jnb\": 589,\n        \"iad\": 159,\n        \"ams\": 72,\n        \"hkg\": 317\n      },\n      {\n        \"timestamp\": \"Feb 15, 03:00\",\n        \"jnb\": 558,\n        \"syd\": 377,\n        \"ams\": 77,\n        \"iad\": 189,\n        \"gru\": 356,\n        \"hkg\": 310\n      },\n      {\n        \"timestamp\": \"Feb 15, 04:00\",\n        \"iad\": 167,\n        \"ams\": 70,\n        \"jnb\": 584,\n        \"syd\": 502,\n        \"gru\": 327,\n        \"hkg\": 320\n      },\n      {\n        \"timestamp\": \"Feb 15, 05:00\",\n        \"hkg\": 364,\n        \"gru\": 287,\n        \"syd\": 346,\n        \"jnb\": 558,\n        \"ams\": 76,\n        \"iad\": 180\n      },\n      {\n        \"timestamp\": \"Feb 15, 06:00\",\n        \"hkg\": 410,\n        \"ams\": 72,\n        \"iad\": 220,\n        \"syd\": 344,\n        \"jnb\": 609,\n        \"gru\": 307\n      },\n      {\n        \"timestamp\": \"Feb 15, 07:00\",\n        \"iad\": 151,\n        \"ams\": 91,\n        \"jnb\": 554,\n        \"syd\": 614,\n        \"gru\": 492,\n        \"hkg\": 382\n      },\n      {\n        \"timestamp\": \"Feb 15, 08:00\",\n        \"gru\": 368,\n        \"ams\": 85,\n        \"iad\": 172,\n        \"jnb\": 578,\n        \"syd\": 382,\n        \"hkg\": 324\n      },\n      {\n        \"timestamp\": \"Feb 15, 09:00\",\n        \"gru\": 588,\n        \"ams\": 85,\n        \"iad\": 177,\n        \"syd\": 440,\n        \"jnb\": 565,\n        \"hkg\": 387\n      },\n      {\n        \"timestamp\": \"Feb 15, 10:00\",\n        \"syd\": 463,\n        \"jnb\": 598,\n        \"ams\": 95,\n        \"iad\": 153,\n        \"gru\": 304,\n        \"hkg\": 374\n      },\n      {\n        \"timestamp\": \"Feb 15, 11:00\",\n        \"hkg\": 384,\n        \"jnb\": 608,\n        \"syd\": 435,\n        \"iad\": 218,\n        \"ams\": 90,\n        \"gru\": 406\n      },\n      {\n        \"timestamp\": \"Feb 15, 12:00\",\n        \"hkg\": 358,\n        \"ams\": 105,\n        \"iad\": 185,\n        \"jnb\": 570,\n        \"syd\": 373,\n        \"gru\": 348\n      },\n      {\n        \"timestamp\": \"Feb 15, 13:00\",\n        \"hkg\": 287,\n        \"gru\": 366,\n        \"jnb\": 618,\n        \"syd\": 414,\n        \"iad\": 152,\n        \"ams\": 85\n      },\n      {\n        \"timestamp\": \"Feb 15, 14:00\",\n        \"hkg\": 321,\n        \"gru\": 289,\n        \"jnb\": 600,\n        \"syd\": 480,\n        \"ams\": 84,\n        \"iad\": 165\n      },\n      {\n        \"timestamp\": \"Feb 15, 15:00\",\n        \"hkg\": 369,\n        \"syd\": 467,\n        \"jnb\": 576,\n        \"iad\": 205,\n        \"ams\": 91,\n        \"gru\": 299\n      },\n      {\n        \"timestamp\": \"Feb 15, 16:00\",\n        \"syd\": 572,\n        \"jnb\": 770,\n        \"ams\": 94,\n        \"iad\": 264,\n        \"gru\": 378,\n        \"hkg\": 480\n      },\n      {\n        \"timestamp\": \"Feb 17, 10:00\",\n        \"hkg\": 31423,\n        \"iad\": 31244,\n        \"ams\": 31251,\n        \"syd\": 31331,\n        \"jnb\": 31573,\n        \"gru\": 31387\n      },\n      {\n        \"timestamp\": \"Feb 17, 11:00\",\n        \"gru\": 375,\n        \"jnb\": 565,\n        \"syd\": 464,\n        \"ams\": 108,\n        \"iad\": 192,\n        \"hkg\": 363\n      },\n      {\n        \"timestamp\": \"Feb 17, 12:00\",\n        \"syd\": 441,\n        \"jnb\": 586,\n        \"ams\": 80,\n        \"iad\": 154,\n        \"gru\": 321,\n        \"hkg\": 398\n      },\n      {\n        \"timestamp\": \"Feb 17, 13:00\",\n        \"jnb\": 565,\n        \"syd\": 368,\n        \"ams\": 89,\n        \"iad\": 192,\n        \"gru\": 381,\n        \"hkg\": 266\n      },\n      {\n        \"timestamp\": \"Feb 17, 14:00\",\n        \"hkg\": 297,\n        \"syd\": 375,\n        \"jnb\": 575,\n        \"ams\": 81,\n        \"iad\": 153,\n        \"gru\": 345\n      },\n      {\n        \"timestamp\": \"Feb 17, 15:00\",\n        \"hkg\": 325,\n        \"gru\": 380,\n        \"iad\": 220,\n        \"ams\": 78,\n        \"syd\": 347,\n        \"jnb\": 594\n      },\n      {\n        \"timestamp\": \"Feb 17, 16:00\",\n        \"hkg\": 328,\n        \"jnb\": 608,\n        \"syd\": 487,\n        \"iad\": 183,\n        \"ams\": 83,\n        \"gru\": 292\n      },\n      {\n        \"timestamp\": \"Feb 17, 17:00\",\n        \"hkg\": 290,\n        \"gru\": 371,\n        \"syd\": 465,\n        \"jnb\": 574,\n        \"ams\": 95,\n        \"iad\": 163\n      },\n      {\n        \"timestamp\": \"Feb 17, 18:00\",\n        \"gru\": 285,\n        \"syd\": 420,\n        \"jnb\": 578,\n        \"iad\": 182,\n        \"ams\": 106,\n        \"hkg\": 403\n      },\n      {\n        \"timestamp\": \"Feb 17, 19:00\",\n        \"jnb\": 647,\n        \"syd\": 310,\n        \"ams\": 111,\n        \"iad\": 159,\n        \"gru\": 296,\n        \"hkg\": 306\n      },\n      {\n        \"timestamp\": \"Feb 17, 20:00\",\n        \"hkg\": 364,\n        \"iad\": 161,\n        \"ams\": 85,\n        \"syd\": 482,\n        \"jnb\": 644,\n        \"gru\": 292\n      },\n      {\n        \"timestamp\": \"Feb 17, 21:00\",\n        \"hkg\": 341,\n        \"gru\": 404,\n        \"syd\": 438,\n        \"jnb\": 562,\n        \"ams\": 85,\n        \"iad\": 160\n      },\n      {\n        \"timestamp\": \"Feb 17, 22:00\",\n        \"hkg\": 312,\n        \"gru\": 354,\n        \"iad\": 160,\n        \"ams\": 81,\n        \"syd\": 447,\n        \"jnb\": 587\n      },\n      {\n        \"timestamp\": \"Feb 17, 23:00\",\n        \"gru\": 376,\n        \"ams\": 79,\n        \"iad\": 156,\n        \"jnb\": 584,\n        \"syd\": 452,\n        \"hkg\": 504\n      },\n      {\n        \"timestamp\": \"Feb 18, 00:00\",\n        \"jnb\": 628,\n        \"syd\": 495,\n        \"ams\": 76,\n        \"iad\": 161,\n        \"gru\": 310,\n        \"hkg\": 401\n      },\n      {\n        \"timestamp\": \"Feb 18, 01:00\",\n        \"syd\": 481,\n        \"jnb\": 589,\n        \"ams\": 78,\n        \"iad\": 188,\n        \"gru\": 365,\n        \"hkg\": 297\n      },\n      {\n        \"timestamp\": \"Feb 18, 02:00\",\n        \"gru\": 437,\n        \"syd\": 519,\n        \"jnb\": 558,\n        \"iad\": 154,\n        \"ams\": 74,\n        \"hkg\": 363\n      },\n      {\n        \"timestamp\": \"Feb 18, 03:00\",\n        \"hkg\": 289,\n        \"gru\": 393,\n        \"jnb\": 608,\n        \"syd\": 460,\n        \"ams\": 77,\n        \"iad\": 150\n      },\n      {\n        \"timestamp\": \"Feb 18, 04:00\",\n        \"hkg\": 302,\n        \"ams\": 74,\n        \"iad\": 152,\n        \"jnb\": 566,\n        \"syd\": 370,\n        \"gru\": 323\n      },\n      {\n        \"timestamp\": \"Feb 18, 05:00\",\n        \"gru\": 361,\n        \"syd\": 335,\n        \"jnb\": 582,\n        \"iad\": 186,\n        \"ams\": 76,\n        \"hkg\": 338\n      },\n      {\n        \"timestamp\": \"Feb 18, 06:00\",\n        \"gru\": 485,\n        \"ams\": 80,\n        \"iad\": 185,\n        \"syd\": 444,\n        \"jnb\": 554,\n        \"hkg\": 320\n      },\n      {\n        \"timestamp\": \"Feb 18, 07:00\",\n        \"iad\": 150,\n        \"ams\": 83,\n        \"jnb\": 563,\n        \"syd\": 346,\n        \"gru\": 410,\n        \"hkg\": 423\n      },\n      {\n        \"timestamp\": \"Feb 18, 08:00\",\n        \"hkg\": 303,\n        \"jnb\": 559,\n        \"syd\": 416,\n        \"iad\": 147,\n        \"ams\": 94,\n        \"gru\": 365\n      },\n      {\n        \"timestamp\": \"Feb 18, 09:00\",\n        \"gru\": 310,\n        \"ams\": 84,\n        \"iad\": 149,\n        \"syd\": 449,\n        \"jnb\": 564,\n        \"hkg\": 402\n      },\n      {\n        \"timestamp\": \"Feb 18, 10:00\",\n        \"syd\": 508,\n        \"jnb\": 570,\n        \"iad\": 177,\n        \"ams\": 97,\n        \"gru\": 317,\n        \"hkg\": 380\n      },\n      {\n        \"timestamp\": \"Feb 18, 11:00\",\n        \"gru\": 2272,\n        \"jnb\": 2497,\n        \"syd\": 2356,\n        \"ams\": 2135,\n        \"iad\": 2157,\n        \"hkg\": 2367\n      },\n      {\n        \"timestamp\": \"Feb 18, 12:00\",\n        \"hkg\": 317,\n        \"gru\": 504,\n        \"ams\": 117,\n        \"iad\": 151,\n        \"jnb\": 621,\n        \"syd\": 330\n      },\n      {\n        \"timestamp\": \"Feb 18, 13:00\",\n        \"hkg\": 20398,\n        \"jnb\": 20505,\n        \"syd\": 20479,\n        \"iad\": 20369,\n        \"ams\": 20298,\n        \"gru\": 20304\n      },\n      {\n        \"timestamp\": \"Feb 18, 14:00\",\n        \"hkg\": 321,\n        \"ams\": 101,\n        \"iad\": 181,\n        \"jnb\": 603,\n        \"syd\": 373,\n        \"gru\": 312\n      },\n      {\n        \"timestamp\": \"Feb 18, 15:00\",\n        \"hkg\": 321,\n        \"syd\": 578,\n        \"jnb\": 587,\n        \"ams\": 97,\n        \"iad\": 150,\n        \"gru\": 491\n      },\n      {\n        \"timestamp\": \"Feb 18, 16:00\",\n        \"hkg\": 314,\n        \"gru\": 313,\n        \"ams\": 101,\n        \"iad\": 181,\n        \"syd\": 441,\n        \"jnb\": 577\n      },\n      {\n        \"timestamp\": \"Feb 18, 17:00\",\n        \"jnb\": 589,\n        \"syd\": 364,\n        \"ams\": 95,\n        \"iad\": 158,\n        \"gru\": 468,\n        \"hkg\": 439\n      },\n      {\n        \"timestamp\": \"Feb 18, 18:00\",\n        \"gru\": 285,\n        \"jnb\": 588,\n        \"syd\": 698,\n        \"iad\": 206,\n        \"ams\": 104,\n        \"hkg\": 394\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"syd\",\n      \"avgLatency\": 949,\n      \"p75Latency\": 433,\n      \"p90Latency\": 828,\n      \"p95Latency\": 945,\n      \"p99Latency\": 16696\n    },\n    {\n      \"region\": \"gru\",\n      \"avgLatency\": 873,\n      \"p75Latency\": 370,\n      \"p90Latency\": 663,\n      \"p95Latency\": 753,\n      \"p99Latency\": 15979\n    },\n    {\n      \"region\": \"iad\",\n      \"avgLatency\": 690,\n      \"p75Latency\": 165,\n      \"p90Latency\": 312,\n      \"p95Latency\": 343,\n      \"p99Latency\": 16552\n    },\n    {\n      \"region\": \"ams\",\n      \"avgLatency\": 607,\n      \"p75Latency\": 95,\n      \"p90Latency\": 111,\n      \"p95Latency\": 127,\n      \"p99Latency\": 16675\n    },\n    {\n      \"region\": \"hkg\",\n      \"avgLatency\": 866,\n      \"p75Latency\": 384,\n      \"p90Latency\": 655,\n      \"p95Latency\": 746,\n      \"p99Latency\": 16681\n    },\n    {\n      \"region\": \"jnb\",\n      \"avgLatency\": 1096,\n      \"p75Latency\": 587,\n      \"p90Latency\": 706,\n      \"p95Latency\": 730,\n      \"p99Latency\": 16295\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/monitoring-vercel/vercel-cold.json",
    "content": "{\n  \"regions\": [\"ams\", \"iad\", \"hkg\", \"jnb\", \"syd\", \"gru\"],\n  \"data\": {\n    \"regions\": [\"ams\", \"gru\", \"hkg\", \"iad\", \"jnb\", \"syd\"],\n    \"data\": [\n      {\n        \"timestamp\": \"Mar 10, 18:00\",\n        \"gru\": 956,\n        \"ams\": 930,\n        \"iad\": 778,\n        \"jnb\": 1055,\n        \"hkg\": 857,\n        \"syd\": 906\n      },\n      {\n        \"timestamp\": \"Mar 10, 19:00\",\n        \"iad\": 759,\n        \"ams\": 834,\n        \"gru\": 830,\n        \"syd\": 875,\n        \"hkg\": 868,\n        \"jnb\": 951\n      },\n      {\n        \"timestamp\": \"Mar 10, 20:00\",\n        \"gru\": 892,\n        \"ams\": 876,\n        \"iad\": 675,\n        \"syd\": 847,\n        \"jnb\": 962,\n        \"hkg\": 878\n      },\n      {\n        \"timestamp\": \"Mar 10, 21:00\",\n        \"syd\": 959,\n        \"jnb\": 968,\n        \"hkg\": 946,\n        \"iad\": 772,\n        \"gru\": 834,\n        \"ams\": 818\n      },\n      {\n        \"timestamp\": \"Mar 10, 22:00\",\n        \"ams\": 986,\n        \"gru\": 853,\n        \"iad\": 804,\n        \"syd\": 874,\n        \"hkg\": 854,\n        \"jnb\": 1017\n      },\n      {\n        \"timestamp\": \"Mar 10, 23:00\",\n        \"iad\": 731,\n        \"ams\": 818,\n        \"gru\": 880,\n        \"hkg\": 885,\n        \"jnb\": 1008,\n        \"syd\": 891\n      },\n      {\n        \"timestamp\": \"Mar 11, 00:00\",\n        \"jnb\": 1008,\n        \"hkg\": 895,\n        \"syd\": 881,\n        \"iad\": 762,\n        \"gru\": 777,\n        \"ams\": 855\n      },\n      {\n        \"timestamp\": \"Mar 11, 01:00\",\n        \"syd\": 867,\n        \"jnb\": 932,\n        \"hkg\": 927,\n        \"iad\": 688,\n        \"gru\": 1015,\n        \"ams\": 848\n      },\n      {\n        \"timestamp\": \"Mar 11, 02:00\",\n        \"syd\": 861,\n        \"hkg\": 848,\n        \"jnb\": 1008,\n        \"ams\": 846,\n        \"gru\": 890,\n        \"iad\": 782\n      },\n      {\n        \"timestamp\": \"Mar 11, 03:00\",\n        \"hkg\": 948,\n        \"jnb\": 1022,\n        \"syd\": 970,\n        \"iad\": 814,\n        \"ams\": 812,\n        \"gru\": 783\n      },\n      {\n        \"timestamp\": \"Mar 11, 04:00\",\n        \"iad\": 714,\n        \"gru\": 814,\n        \"ams\": 858,\n        \"jnb\": 982,\n        \"hkg\": 895,\n        \"syd\": 830\n      },\n      {\n        \"timestamp\": \"Mar 11, 05:00\",\n        \"syd\": 850,\n        \"hkg\": 902,\n        \"jnb\": 1004,\n        \"ams\": 817,\n        \"gru\": 756,\n        \"iad\": 693\n      },\n      {\n        \"timestamp\": \"Mar 11, 06:00\",\n        \"iad\": 682,\n        \"gru\": 624,\n        \"ams\": 808,\n        \"syd\": 910,\n        \"jnb\": 974,\n        \"hkg\": 880\n      },\n      {\n        \"timestamp\": \"Mar 11, 07:00\",\n        \"ams\": 815,\n        \"gru\": 821,\n        \"iad\": 661,\n        \"hkg\": 924,\n        \"jnb\": 952,\n        \"syd\": 848\n      },\n      {\n        \"timestamp\": \"Mar 11, 08:00\",\n        \"hkg\": 898,\n        \"jnb\": 976,\n        \"syd\": 866,\n        \"ams\": 792,\n        \"gru\": 829,\n        \"iad\": 768\n      },\n      {\n        \"timestamp\": \"Mar 11, 09:00\",\n        \"syd\": 894,\n        \"jnb\": 1004,\n        \"hkg\": 926,\n        \"iad\": 690,\n        \"gru\": 815,\n        \"ams\": 832\n      },\n      {\n        \"timestamp\": \"Mar 11, 10:00\",\n        \"iad\": 702,\n        \"ams\": 828,\n        \"gru\": 762,\n        \"hkg\": 885,\n        \"jnb\": 968,\n        \"syd\": 874\n      },\n      {\n        \"timestamp\": \"Mar 11, 11:00\",\n        \"iad\": 734,\n        \"ams\": 766,\n        \"gru\": 814,\n        \"hkg\": 872,\n        \"jnb\": 1001,\n        \"syd\": 866\n      },\n      {\n        \"timestamp\": \"Mar 11, 12:00\",\n        \"iad\": 718,\n        \"gru\": 838,\n        \"ams\": 784,\n        \"syd\": 862,\n        \"jnb\": 1007,\n        \"hkg\": 942\n      },\n      {\n        \"timestamp\": \"Mar 11, 13:00\",\n        \"ams\": 780,\n        \"gru\": 872,\n        \"iad\": 708,\n        \"hkg\": 804,\n        \"jnb\": 968,\n        \"syd\": 858\n      },\n      {\n        \"timestamp\": \"Mar 11, 14:00\",\n        \"iad\": 729,\n        \"ams\": 821,\n        \"gru\": 880,\n        \"hkg\": 957,\n        \"jnb\": 956,\n        \"syd\": 842\n      },\n      {\n        \"timestamp\": \"Mar 11, 15:00\",\n        \"hkg\": 912,\n        \"jnb\": 1019,\n        \"syd\": 876,\n        \"iad\": 733,\n        \"ams\": 827,\n        \"gru\": 803\n      },\n      {\n        \"timestamp\": \"Mar 11, 16:00\",\n        \"syd\": 855,\n        \"hkg\": 995,\n        \"jnb\": 970,\n        \"iad\": 813,\n        \"ams\": 786,\n        \"gru\": 814\n      },\n      {\n        \"timestamp\": \"Mar 11, 17:00\",\n        \"syd\": 865,\n        \"jnb\": 1012,\n        \"hkg\": 920,\n        \"iad\": 753,\n        \"gru\": 977,\n        \"ams\": 806\n      },\n      {\n        \"timestamp\": \"Mar 11, 18:00\",\n        \"jnb\": 1100,\n        \"hkg\": 906,\n        \"syd\": 917,\n        \"iad\": 764,\n        \"gru\": 876,\n        \"ams\": 878\n      },\n      {\n        \"timestamp\": \"Mar 11, 19:00\",\n        \"iad\": 740,\n        \"ams\": 818,\n        \"gru\": 880,\n        \"hkg\": 921,\n        \"jnb\": 996,\n        \"syd\": 852\n      },\n      {\n        \"timestamp\": \"Mar 11, 20:00\",\n        \"iad\": 754,\n        \"ams\": 914,\n        \"gru\": 886,\n        \"syd\": 872,\n        \"hkg\": 922,\n        \"jnb\": 956\n      },\n      {\n        \"timestamp\": \"Mar 11, 21:00\",\n        \"ams\": 870,\n        \"gru\": 850,\n        \"iad\": 681,\n        \"hkg\": 942,\n        \"jnb\": 1062,\n        \"syd\": 827\n      },\n      {\n        \"timestamp\": \"Mar 11, 22:00\",\n        \"ams\": 898,\n        \"gru\": 838,\n        \"iad\": 757,\n        \"syd\": 897,\n        \"hkg\": 890,\n        \"jnb\": 986\n      },\n      {\n        \"timestamp\": \"Mar 11, 23:00\",\n        \"syd\": 875,\n        \"hkg\": 897,\n        \"jnb\": 1002,\n        \"ams\": 858,\n        \"gru\": 894,\n        \"iad\": 748\n      },\n      {\n        \"timestamp\": \"Mar 12, 00:00\",\n        \"syd\": 846,\n        \"jnb\": 983,\n        \"hkg\": 1010,\n        \"gru\": 766,\n        \"ams\": 833,\n        \"iad\": 750\n      },\n      {\n        \"timestamp\": \"Mar 12, 01:00\",\n        \"syd\": 911,\n        \"jnb\": 1064,\n        \"hkg\": 940,\n        \"iad\": 742,\n        \"gru\": 802,\n        \"ams\": 895\n      },\n      {\n        \"timestamp\": \"Mar 12, 02:00\",\n        \"hkg\": 868,\n        \"jnb\": 948,\n        \"syd\": 852,\n        \"iad\": 652,\n        \"ams\": 812,\n        \"gru\": 737\n      },\n      {\n        \"timestamp\": \"Mar 12, 03:00\",\n        \"hkg\": 857,\n        \"jnb\": 930,\n        \"syd\": 816,\n        \"iad\": 664,\n        \"ams\": 813,\n        \"gru\": 835\n      },\n      {\n        \"timestamp\": \"Mar 12, 04:00\",\n        \"iad\": 648,\n        \"gru\": 839,\n        \"ams\": 894,\n        \"syd\": 870,\n        \"jnb\": 984,\n        \"hkg\": 913\n      },\n      {\n        \"timestamp\": \"Mar 12, 05:00\",\n        \"gru\": 830,\n        \"iad\": 672,\n        \"hkg\": 878,\n        \"jnb\": 1000,\n        \"syd\": 918,\n        \"ams\": 816\n      },\n      {\n        \"timestamp\": \"Mar 12, 06:00\",\n        \"gru\": 821,\n        \"ams\": 908,\n        \"iad\": 802,\n        \"syd\": 848,\n        \"jnb\": 989,\n        \"hkg\": 878\n      },\n      {\n        \"timestamp\": \"Mar 12, 07:00\",\n        \"syd\": 836,\n        \"jnb\": 834,\n        \"hkg\": 739,\n        \"gru\": 775,\n        \"ams\": 806,\n        \"iad\": 664\n      },\n      {\n        \"timestamp\": \"Mar 12, 08:00\",\n        \"jnb\": 1005,\n        \"hkg\": 927,\n        \"syd\": 916,\n        \"gru\": 814,\n        \"ams\": 792,\n        \"iad\": 736\n      },\n      {\n        \"timestamp\": \"Mar 12, 09:00\",\n        \"ams\": 831,\n        \"gru\": 738,\n        \"iad\": 747,\n        \"hkg\": 852,\n        \"jnb\": 1082,\n        \"syd\": 856\n      },\n      {\n        \"timestamp\": \"Mar 12, 10:00\",\n        \"ams\": 838,\n        \"gru\": 854,\n        \"iad\": 670,\n        \"syd\": 866,\n        \"hkg\": 830,\n        \"jnb\": 932\n      },\n      {\n        \"timestamp\": \"Mar 12, 11:00\",\n        \"gru\": 859,\n        \"ams\": 748,\n        \"iad\": 662,\n        \"syd\": 855,\n        \"jnb\": 937,\n        \"hkg\": 871\n      },\n      {\n        \"timestamp\": \"Mar 12, 12:00\",\n        \"gru\": 910,\n        \"ams\": 802,\n        \"iad\": 724,\n        \"jnb\": 998,\n        \"hkg\": 934,\n        \"syd\": 868\n      },\n      {\n        \"timestamp\": \"Mar 12, 13:00\",\n        \"syd\": 834,\n        \"jnb\": 1050,\n        \"hkg\": 914,\n        \"iad\": 710,\n        \"gru\": 902,\n        \"ams\": 898\n      },\n      {\n        \"timestamp\": \"Mar 12, 14:00\",\n        \"jnb\": 1096,\n        \"hkg\": 880,\n        \"syd\": 879,\n        \"iad\": 716,\n        \"gru\": 880,\n        \"ams\": 820\n      },\n      {\n        \"timestamp\": \"Mar 12, 15:00\",\n        \"iad\": 720,\n        \"gru\": 824,\n        \"ams\": 823,\n        \"jnb\": 688,\n        \"hkg\": 686,\n        \"syd\": 816\n      },\n      {\n        \"timestamp\": \"Mar 12, 16:00\",\n        \"iad\": 766,\n        \"ams\": 890,\n        \"gru\": 812,\n        \"syd\": 850,\n        \"hkg\": 950,\n        \"jnb\": 997\n      },\n      {\n        \"timestamp\": \"Mar 12, 17:00\",\n        \"gru\": 1000,\n        \"ams\": 955,\n        \"iad\": 740,\n        \"jnb\": 1070,\n        \"hkg\": 1034,\n        \"syd\": 896\n      },\n      {\n        \"timestamp\": \"Mar 12, 18:00\",\n        \"syd\": 836,\n        \"jnb\": 904,\n        \"hkg\": 883,\n        \"gru\": 760,\n        \"ams\": 832,\n        \"iad\": 746\n      },\n      {\n        \"timestamp\": \"Mar 12, 19:00\",\n        \"hkg\": 960,\n        \"jnb\": 920,\n        \"syd\": 1010,\n        \"iad\": 758,\n        \"ams\": 856,\n        \"gru\": 852\n      },\n      {\n        \"timestamp\": \"Mar 12, 20:00\",\n        \"hkg\": 946,\n        \"jnb\": 996,\n        \"syd\": 848,\n        \"ams\": 788,\n        \"gru\": 788,\n        \"iad\": 726\n      },\n      {\n        \"timestamp\": \"Mar 12, 21:00\",\n        \"iad\": 770,\n        \"ams\": 807,\n        \"gru\": 776,\n        \"hkg\": 900,\n        \"jnb\": 1054,\n        \"syd\": 880\n      },\n      {\n        \"timestamp\": \"Mar 12, 22:00\",\n        \"iad\": 760,\n        \"ams\": 852,\n        \"gru\": 986,\n        \"hkg\": 906,\n        \"jnb\": 1069,\n        \"syd\": 932\n      },\n      {\n        \"timestamp\": \"Mar 12, 23:00\",\n        \"gru\": 752,\n        \"ams\": 798,\n        \"iad\": 718,\n        \"syd\": 893,\n        \"jnb\": 979,\n        \"hkg\": 888\n      },\n      {\n        \"timestamp\": \"Mar 13, 00:00\",\n        \"syd\": 862,\n        \"hkg\": 1028,\n        \"jnb\": 957,\n        \"iad\": 694,\n        \"ams\": 812,\n        \"gru\": 853\n      },\n      {\n        \"timestamp\": \"Mar 13, 01:00\",\n        \"iad\": 700,\n        \"ams\": 860,\n        \"gru\": 842,\n        \"hkg\": 896,\n        \"jnb\": 1004,\n        \"syd\": 932\n      },\n      {\n        \"timestamp\": \"Mar 13, 02:00\",\n        \"jnb\": 896,\n        \"hkg\": 910,\n        \"syd\": 977,\n        \"iad\": 717,\n        \"gru\": 786,\n        \"ams\": 816\n      },\n      {\n        \"timestamp\": \"Mar 13, 03:00\",\n        \"hkg\": 922,\n        \"jnb\": 994,\n        \"syd\": 865,\n        \"iad\": 708,\n        \"ams\": 841,\n        \"gru\": 767\n      },\n      {\n        \"timestamp\": \"Mar 13, 04:00\",\n        \"jnb\": 976,\n        \"hkg\": 894,\n        \"syd\": 886,\n        \"gru\": 906,\n        \"ams\": 830,\n        \"iad\": 710\n      },\n      {\n        \"timestamp\": \"Mar 13, 05:00\",\n        \"iad\": 684,\n        \"ams\": 887,\n        \"gru\": 814,\n        \"hkg\": 890,\n        \"jnb\": 907,\n        \"syd\": 835\n      },\n      {\n        \"timestamp\": \"Mar 13, 06:00\",\n        \"jnb\": 966,\n        \"hkg\": 964,\n        \"syd\": 871,\n        \"iad\": 784,\n        \"gru\": 872,\n        \"ams\": 990\n      },\n      {\n        \"timestamp\": \"Mar 13, 07:00\",\n        \"gru\": 814,\n        \"ams\": 880,\n        \"iad\": 722,\n        \"syd\": 925,\n        \"jnb\": 910,\n        \"hkg\": 938\n      },\n      {\n        \"timestamp\": \"Mar 13, 08:00\",\n        \"syd\": 848,\n        \"hkg\": 872,\n        \"jnb\": 1026,\n        \"iad\": 714,\n        \"ams\": 850,\n        \"gru\": 864\n      },\n      {\n        \"timestamp\": \"Mar 13, 09:00\",\n        \"gru\": 785,\n        \"ams\": 869,\n        \"iad\": 706,\n        \"jnb\": 960,\n        \"hkg\": 936,\n        \"syd\": 824\n      },\n      {\n        \"timestamp\": \"Mar 13, 10:00\",\n        \"jnb\": 1010,\n        \"hkg\": 896,\n        \"syd\": 880,\n        \"gru\": 770,\n        \"ams\": 820,\n        \"iad\": 725\n      },\n      {\n        \"timestamp\": \"Mar 13, 11:00\",\n        \"syd\": 838,\n        \"hkg\": 822,\n        \"jnb\": 1019,\n        \"iad\": 713,\n        \"ams\": 843,\n        \"gru\": 834\n      },\n      {\n        \"timestamp\": \"Mar 13, 12:00\",\n        \"hkg\": 890,\n        \"jnb\": 988,\n        \"syd\": 944,\n        \"ams\": 770,\n        \"gru\": 808,\n        \"iad\": 710\n      },\n      {\n        \"timestamp\": \"Mar 13, 13:00\",\n        \"iad\": 710,\n        \"ams\": 838,\n        \"gru\": 880,\n        \"hkg\": 874,\n        \"jnb\": 1024,\n        \"syd\": 858\n      },\n      {\n        \"timestamp\": \"Mar 13, 14:00\",\n        \"hkg\": 882,\n        \"jnb\": 978,\n        \"syd\": 912,\n        \"iad\": 784,\n        \"ams\": 852,\n        \"gru\": 843\n      },\n      {\n        \"timestamp\": \"Mar 13, 15:00\",\n        \"syd\": 878,\n        \"jnb\": 904,\n        \"hkg\": 894,\n        \"gru\": 868,\n        \"ams\": 840,\n        \"iad\": 732\n      },\n      {\n        \"timestamp\": \"Mar 13, 16:00\",\n        \"iad\": 714,\n        \"ams\": 882,\n        \"gru\": 934,\n        \"syd\": 872,\n        \"hkg\": 922,\n        \"jnb\": 1030\n      },\n      {\n        \"timestamp\": \"Mar 13, 17:00\",\n        \"iad\": 742,\n        \"ams\": 858,\n        \"gru\": 836,\n        \"hkg\": 892,\n        \"jnb\": 1008,\n        \"syd\": 836\n      },\n      {\n        \"timestamp\": \"Mar 13, 18:00\",\n        \"jnb\": 954,\n        \"hkg\": 878,\n        \"syd\": 851,\n        \"iad\": 792,\n        \"gru\": 794,\n        \"ams\": 792\n      },\n      {\n        \"timestamp\": \"Mar 13, 19:00\",\n        \"syd\": 835,\n        \"jnb\": 1072,\n        \"hkg\": 1044,\n        \"gru\": 1083,\n        \"ams\": 934,\n        \"iad\": 814\n      },\n      {\n        \"timestamp\": \"Mar 13, 20:00\",\n        \"syd\": 961,\n        \"hkg\": 944,\n        \"jnb\": 981,\n        \"iad\": 704,\n        \"ams\": 840,\n        \"gru\": 865\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"syd\",\n      \"count\": 147,\n      \"ok\": 147,\n      \"lastTimestamp\": 1710358249474,\n      \"p50Latency\": 866,\n      \"p75Latency\": 892,\n      \"p90Latency\": 957,\n      \"p95Latency\": 996,\n      \"p99Latency\": 1044\n    },\n    {\n      \"region\": \"gru\",\n      \"count\": 147,\n      \"ok\": 147,\n      \"lastTimestamp\": 1710358249474,\n      \"p50Latency\": 823,\n      \"p75Latency\": 894,\n      \"p90Latency\": 954,\n      \"p95Latency\": 994,\n      \"p99Latency\": 1173\n    },\n    {\n      \"region\": \"iad\",\n      \"count\": 147,\n      \"ok\": 147,\n      \"lastTimestamp\": 1710358249474,\n      \"p50Latency\": 719,\n      \"p75Latency\": 761,\n      \"p90Latency\": 802,\n      \"p95Latency\": 822,\n      \"p99Latency\": 894\n    },\n    {\n      \"region\": \"ams\",\n      \"count\": 147,\n      \"ok\": 147,\n      \"lastTimestamp\": 1710358249474,\n      \"p50Latency\": 832,\n      \"p75Latency\": 870,\n      \"p90Latency\": 919,\n      \"p95Latency\": 958,\n      \"p99Latency\": 1015\n    },\n    {\n      \"region\": \"hkg\",\n      \"count\": 147,\n      \"ok\": 147,\n      \"lastTimestamp\": 1710358249474,\n      \"p50Latency\": 901,\n      \"p75Latency\": 934,\n      \"p90Latency\": 976,\n      \"p95Latency\": 1024,\n      \"p99Latency\": 1073\n    },\n    {\n      \"region\": \"jnb\",\n      \"count\": 147,\n      \"ok\": 147,\n      \"lastTimestamp\": 1710358249474,\n      \"p50Latency\": 991,\n      \"p75Latency\": 1016,\n      \"p90Latency\": 1066,\n      \"p95Latency\": 1128,\n      \"p99Latency\": 1211\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/monitoring-vercel/vercel-edge.json",
    "content": "{\n  \"regions\": [\"ams\", \"iad\", \"hkg\", \"jnb\", \"syd\", \"gru\"],\n  \"data\": {\n    \"regions\": [\"ams\", \"gru\", \"hkg\", \"iad\", \"jnb\", \"syd\"],\n    \"data\": [\n      {\n        \"timestamp\": \"Mar 10, 18:00\",\n        \"gru\": 106,\n        \"ams\": 145,\n        \"iad\": 117,\n        \"jnb\": 128,\n        \"hkg\": 89,\n        \"syd\": 78\n      },\n      {\n        \"timestamp\": \"Mar 10, 19:00\",\n        \"iad\": 126,\n        \"ams\": 156,\n        \"gru\": 120,\n        \"syd\": 87,\n        \"hkg\": 96,\n        \"jnb\": 117\n      },\n      {\n        \"timestamp\": \"Mar 10, 20:00\",\n        \"gru\": 94,\n        \"ams\": 160,\n        \"iad\": 123,\n        \"syd\": 86,\n        \"jnb\": 125,\n        \"hkg\": 94\n      },\n      {\n        \"timestamp\": \"Mar 10, 21:00\",\n        \"syd\": 83,\n        \"jnb\": 119,\n        \"hkg\": 81,\n        \"iad\": 116,\n        \"gru\": 116,\n        \"ams\": 160\n      },\n      {\n        \"timestamp\": \"Mar 10, 22:00\",\n        \"ams\": 135,\n        \"gru\": 112,\n        \"iad\": 112,\n        \"syd\": 96,\n        \"hkg\": 94,\n        \"jnb\": 108\n      },\n      {\n        \"timestamp\": \"Mar 10, 23:00\",\n        \"iad\": 109,\n        \"ams\": 107,\n        \"gru\": 110,\n        \"hkg\": 94,\n        \"jnb\": 119,\n        \"syd\": 98\n      },\n      {\n        \"timestamp\": \"Mar 11, 00:00\",\n        \"jnb\": 99,\n        \"hkg\": 92,\n        \"syd\": 101,\n        \"iad\": 116,\n        \"gru\": 134,\n        \"ams\": 103\n      },\n      {\n        \"timestamp\": \"Mar 11, 01:00\",\n        \"syd\": 94,\n        \"jnb\": 112,\n        \"hkg\": 98,\n        \"iad\": 112,\n        \"gru\": 132,\n        \"ams\": 100\n      },\n      {\n        \"timestamp\": \"Mar 11, 02:00\",\n        \"syd\": 102,\n        \"hkg\": 101,\n        \"jnb\": 101,\n        \"ams\": 105,\n        \"gru\": 116,\n        \"iad\": 116\n      },\n      {\n        \"timestamp\": \"Mar 11, 03:00\",\n        \"hkg\": 105,\n        \"jnb\": 97,\n        \"syd\": 108,\n        \"iad\": 108,\n        \"ams\": 100,\n        \"gru\": 121\n      },\n      {\n        \"timestamp\": \"Mar 11, 04:00\",\n        \"iad\": 102,\n        \"gru\": 89,\n        \"ams\": 107,\n        \"jnb\": 107,\n        \"hkg\": 113,\n        \"syd\": 112\n      },\n      {\n        \"timestamp\": \"Mar 11, 05:00\",\n        \"syd\": 112,\n        \"hkg\": 104,\n        \"jnb\": 120,\n        \"ams\": 102,\n        \"gru\": 76,\n        \"iad\": 100\n      },\n      {\n        \"timestamp\": \"Mar 11, 06:00\",\n        \"iad\": 94,\n        \"gru\": 78,\n        \"ams\": 111,\n        \"syd\": 102,\n        \"jnb\": 130,\n        \"hkg\": 112\n      },\n      {\n        \"timestamp\": \"Mar 11, 07:00\",\n        \"ams\": 108,\n        \"gru\": 82,\n        \"iad\": 100,\n        \"hkg\": 115,\n        \"jnb\": 134,\n        \"syd\": 100\n      },\n      {\n        \"timestamp\": \"Mar 11, 08:00\",\n        \"hkg\": 108,\n        \"jnb\": 124,\n        \"syd\": 96,\n        \"ams\": 122,\n        \"gru\": 77,\n        \"iad\": 104\n      },\n      {\n        \"timestamp\": \"Mar 11, 09:00\",\n        \"syd\": 98,\n        \"jnb\": 128,\n        \"hkg\": 114,\n        \"iad\": 120,\n        \"gru\": 87,\n        \"ams\": 118\n      },\n      {\n        \"timestamp\": \"Mar 11, 10:00\",\n        \"iad\": 102,\n        \"ams\": 142,\n        \"gru\": 80,\n        \"hkg\": 115,\n        \"jnb\": 146,\n        \"syd\": 105\n      },\n      {\n        \"timestamp\": \"Mar 11, 11:00\",\n        \"iad\": 124,\n        \"ams\": 150,\n        \"gru\": 119,\n        \"hkg\": 118,\n        \"jnb\": 134,\n        \"syd\": 95\n      },\n      {\n        \"timestamp\": \"Mar 11, 12:00\",\n        \"iad\": 100,\n        \"gru\": 55,\n        \"ams\": 140,\n        \"syd\": 102,\n        \"jnb\": 130,\n        \"hkg\": 106\n      },\n      {\n        \"timestamp\": \"Mar 11, 13:00\",\n        \"ams\": 156,\n        \"gru\": 110,\n        \"iad\": 132,\n        \"hkg\": 122,\n        \"jnb\": 153,\n        \"syd\": 96\n      },\n      {\n        \"timestamp\": \"Mar 11, 14:00\",\n        \"iad\": 119,\n        \"ams\": 143,\n        \"gru\": 98,\n        \"hkg\": 130,\n        \"jnb\": 134,\n        \"syd\": 97\n      },\n      {\n        \"timestamp\": \"Mar 11, 15:00\",\n        \"hkg\": 128,\n        \"jnb\": 178,\n        \"syd\": 82,\n        \"iad\": 151,\n        \"ams\": 148,\n        \"gru\": 108\n      },\n      {\n        \"timestamp\": \"Mar 11, 16:00\",\n        \"syd\": 90,\n        \"hkg\": 128,\n        \"jnb\": 138,\n        \"iad\": 130,\n        \"ams\": 140,\n        \"gru\": 94\n      },\n      {\n        \"timestamp\": \"Mar 11, 17:00\",\n        \"syd\": 84,\n        \"jnb\": 135,\n        \"hkg\": 119,\n        \"iad\": 127,\n        \"gru\": 133,\n        \"ams\": 146\n      },\n      {\n        \"timestamp\": \"Mar 11, 18:00\",\n        \"jnb\": 146,\n        \"hkg\": 96,\n        \"syd\": 86,\n        \"iad\": 122,\n        \"gru\": 108,\n        \"ams\": 139\n      },\n      {\n        \"timestamp\": \"Mar 11, 19:00\",\n        \"iad\": 104,\n        \"ams\": 180,\n        \"gru\": 118,\n        \"hkg\": 99,\n        \"jnb\": 127,\n        \"syd\": 85\n      },\n      {\n        \"timestamp\": \"Mar 11, 20:00\",\n        \"iad\": 136,\n        \"ams\": 137,\n        \"gru\": 109,\n        \"syd\": 86,\n        \"hkg\": 90,\n        \"jnb\": 128\n      },\n      {\n        \"timestamp\": \"Mar 11, 21:00\",\n        \"ams\": 138,\n        \"gru\": 134,\n        \"iad\": 94,\n        \"hkg\": 92,\n        \"jnb\": 115,\n        \"syd\": 94\n      },\n      {\n        \"timestamp\": \"Mar 11, 22:00\",\n        \"ams\": 126,\n        \"gru\": 132,\n        \"iad\": 124,\n        \"syd\": 98,\n        \"hkg\": 90,\n        \"jnb\": 162\n      },\n      {\n        \"timestamp\": \"Mar 11, 23:00\",\n        \"syd\": 104,\n        \"hkg\": 92,\n        \"jnb\": 110,\n        \"ams\": 123,\n        \"gru\": 118,\n        \"iad\": 124\n      },\n      {\n        \"timestamp\": \"Mar 12, 00:00\",\n        \"syd\": 97,\n        \"jnb\": 106,\n        \"hkg\": 92,\n        \"gru\": 157,\n        \"ams\": 112,\n        \"iad\": 121\n      },\n      {\n        \"timestamp\": \"Mar 12, 01:00\",\n        \"syd\": 113,\n        \"jnb\": 102,\n        \"hkg\": 106,\n        \"iad\": 120,\n        \"gru\": 171,\n        \"ams\": 108\n      },\n      {\n        \"timestamp\": \"Mar 12, 02:00\",\n        \"hkg\": 101,\n        \"jnb\": 76,\n        \"syd\": 106,\n        \"iad\": 113,\n        \"ams\": 97,\n        \"gru\": 132\n      },\n      {\n        \"timestamp\": \"Mar 12, 03:00\",\n        \"hkg\": 118,\n        \"jnb\": 111,\n        \"syd\": 110,\n        \"iad\": 126,\n        \"ams\": 104,\n        \"gru\": 112\n      },\n      {\n        \"timestamp\": \"Mar 12, 04:00\",\n        \"iad\": 104,\n        \"gru\": 90,\n        \"ams\": 100,\n        \"syd\": 112,\n        \"jnb\": 94,\n        \"hkg\": 114\n      },\n      {\n        \"timestamp\": \"Mar 12, 05:00\",\n        \"gru\": 91,\n        \"iad\": 106,\n        \"hkg\": 126,\n        \"jnb\": 104,\n        \"syd\": 116,\n        \"ams\": 110\n      },\n      {\n        \"timestamp\": \"Mar 12, 06:00\",\n        \"gru\": 86,\n        \"ams\": 110,\n        \"iad\": 94,\n        \"syd\": 108,\n        \"jnb\": 120,\n        \"hkg\": 116\n      },\n      {\n        \"timestamp\": \"Mar 12, 07:00\",\n        \"syd\": 104,\n        \"jnb\": 102,\n        \"hkg\": 116,\n        \"gru\": 89,\n        \"ams\": 119,\n        \"iad\": 90\n      },\n      {\n        \"timestamp\": \"Mar 12, 08:00\",\n        \"jnb\": 121,\n        \"hkg\": 113,\n        \"syd\": 100,\n        \"gru\": 84,\n        \"ams\": 138,\n        \"iad\": 97\n      },\n      {\n        \"timestamp\": \"Mar 12, 09:00\",\n        \"ams\": 154,\n        \"gru\": 80,\n        \"iad\": 104,\n        \"hkg\": 126,\n        \"jnb\": 114,\n        \"syd\": 106\n      },\n      {\n        \"timestamp\": \"Mar 12, 10:00\",\n        \"ams\": 128,\n        \"gru\": 63,\n        \"iad\": 102,\n        \"syd\": 112,\n        \"hkg\": 115,\n        \"jnb\": 148\n      },\n      {\n        \"timestamp\": \"Mar 12, 11:00\",\n        \"gru\": 112,\n        \"ams\": 144,\n        \"iad\": 100,\n        \"syd\": 110,\n        \"jnb\": 114,\n        \"hkg\": 106\n      },\n      {\n        \"timestamp\": \"Mar 12, 12:00\",\n        \"gru\": 87,\n        \"ams\": 134,\n        \"iad\": 126,\n        \"jnb\": 134,\n        \"hkg\": 120,\n        \"syd\": 104\n      },\n      {\n        \"timestamp\": \"Mar 12, 13:00\",\n        \"syd\": 88,\n        \"jnb\": 143,\n        \"hkg\": 115,\n        \"iad\": 114,\n        \"gru\": 106,\n        \"ams\": 142\n      },\n      {\n        \"timestamp\": \"Mar 12, 14:00\",\n        \"jnb\": 142,\n        \"hkg\": 134,\n        \"syd\": 88,\n        \"iad\": 124,\n        \"gru\": 112,\n        \"ams\": 142\n      },\n      {\n        \"timestamp\": \"Mar 12, 15:00\",\n        \"iad\": 149,\n        \"gru\": 114,\n        \"ams\": 160,\n        \"jnb\": 132,\n        \"hkg\": 146,\n        \"syd\": 89\n      },\n      {\n        \"timestamp\": \"Mar 12, 16:00\",\n        \"iad\": 110,\n        \"ams\": 143,\n        \"gru\": 108,\n        \"syd\": 85,\n        \"hkg\": 116,\n        \"jnb\": 96\n      },\n      {\n        \"timestamp\": \"Mar 12, 17:00\",\n        \"gru\": 170,\n        \"ams\": 152,\n        \"iad\": 144,\n        \"jnb\": 142,\n        \"hkg\": 120,\n        \"syd\": 82\n      },\n      {\n        \"timestamp\": \"Mar 12, 18:00\",\n        \"syd\": 88,\n        \"jnb\": 125,\n        \"hkg\": 105,\n        \"gru\": 153,\n        \"ams\": 152,\n        \"iad\": 119\n      },\n      {\n        \"timestamp\": \"Mar 12, 19:00\",\n        \"hkg\": 116,\n        \"jnb\": 149,\n        \"syd\": 84,\n        \"iad\": 137,\n        \"ams\": 159,\n        \"gru\": 134\n      },\n      {\n        \"timestamp\": \"Mar 12, 20:00\",\n        \"hkg\": 94,\n        \"jnb\": 127,\n        \"syd\": 90,\n        \"ams\": 160,\n        \"gru\": 130,\n        \"iad\": 128\n      },\n      {\n        \"timestamp\": \"Mar 12, 21:00\",\n        \"iad\": 134,\n        \"ams\": 182,\n        \"gru\": 116,\n        \"hkg\": 96,\n        \"jnb\": 136,\n        \"syd\": 87\n      },\n      {\n        \"timestamp\": \"Mar 12, 22:00\",\n        \"iad\": 134,\n        \"ams\": 136,\n        \"gru\": 126,\n        \"hkg\": 92,\n        \"jnb\": 110,\n        \"syd\": 96\n      },\n      {\n        \"timestamp\": \"Mar 12, 23:00\",\n        \"gru\": 118,\n        \"ams\": 110,\n        \"iad\": 128,\n        \"syd\": 104,\n        \"jnb\": 122,\n        \"hkg\": 98\n      },\n      {\n        \"timestamp\": \"Mar 13, 00:00\",\n        \"syd\": 100,\n        \"hkg\": 98,\n        \"jnb\": 102,\n        \"iad\": 128,\n        \"ams\": 107,\n        \"gru\": 126\n      },\n      {\n        \"timestamp\": \"Mar 13, 01:00\",\n        \"iad\": 130,\n        \"ams\": 100,\n        \"gru\": 172,\n        \"hkg\": 108,\n        \"jnb\": 124,\n        \"syd\": 114\n      },\n      {\n        \"timestamp\": \"Mar 13, 02:00\",\n        \"jnb\": 102,\n        \"hkg\": 114,\n        \"syd\": 109,\n        \"iad\": 112,\n        \"gru\": 116,\n        \"ams\": 102\n      },\n      {\n        \"timestamp\": \"Mar 13, 03:00\",\n        \"hkg\": 118,\n        \"jnb\": 100,\n        \"syd\": 113,\n        \"iad\": 120,\n        \"ams\": 109,\n        \"gru\": 136\n      },\n      {\n        \"timestamp\": \"Mar 13, 04:00\",\n        \"jnb\": 102,\n        \"hkg\": 120,\n        \"syd\": 96,\n        \"gru\": 151,\n        \"ams\": 102,\n        \"iad\": 108\n      },\n      {\n        \"timestamp\": \"Mar 13, 05:00\",\n        \"iad\": 108,\n        \"ams\": 104,\n        \"gru\": 128,\n        \"hkg\": 114,\n        \"jnb\": 80,\n        \"syd\": 106\n      },\n      {\n        \"timestamp\": \"Mar 13, 06:00\",\n        \"jnb\": 113,\n        \"hkg\": 127,\n        \"syd\": 108,\n        \"iad\": 106,\n        \"gru\": 102,\n        \"ams\": 114\n      },\n      {\n        \"timestamp\": \"Mar 13, 07:00\",\n        \"gru\": 84,\n        \"ams\": 120,\n        \"iad\": 102,\n        \"syd\": 101,\n        \"jnb\": 120,\n        \"hkg\": 120\n      },\n      {\n        \"timestamp\": \"Mar 13, 08:00\",\n        \"syd\": 106,\n        \"hkg\": 119,\n        \"jnb\": 134,\n        \"iad\": 104,\n        \"ams\": 122,\n        \"gru\": 96\n      },\n      {\n        \"timestamp\": \"Mar 13, 09:00\",\n        \"gru\": 92,\n        \"ams\": 136,\n        \"iad\": 114,\n        \"jnb\": 140,\n        \"hkg\": 120,\n        \"syd\": 113\n      },\n      {\n        \"timestamp\": \"Mar 13, 10:00\",\n        \"jnb\": 138,\n        \"hkg\": 116,\n        \"syd\": 106,\n        \"gru\": 136,\n        \"ams\": 133,\n        \"iad\": 90\n      },\n      {\n        \"timestamp\": \"Mar 13, 11:00\",\n        \"syd\": 113,\n        \"hkg\": 118,\n        \"jnb\": 148,\n        \"iad\": 114,\n        \"ams\": 140,\n        \"gru\": 116\n      },\n      {\n        \"timestamp\": \"Mar 13, 12:00\",\n        \"hkg\": 129,\n        \"jnb\": 170,\n        \"syd\": 102,\n        \"ams\": 136,\n        \"gru\": 126,\n        \"iad\": 117\n      },\n      {\n        \"timestamp\": \"Mar 13, 13:00\",\n        \"iad\": 116,\n        \"ams\": 140,\n        \"gru\": 116,\n        \"hkg\": 136,\n        \"jnb\": 138,\n        \"syd\": 85\n      },\n      {\n        \"timestamp\": \"Mar 13, 14:00\",\n        \"hkg\": 142,\n        \"jnb\": 134,\n        \"syd\": 94,\n        \"iad\": 130,\n        \"ams\": 140,\n        \"gru\": 126\n      },\n      {\n        \"timestamp\": \"Mar 13, 15:00\",\n        \"syd\": 88,\n        \"jnb\": 157,\n        \"hkg\": 152,\n        \"gru\": 229,\n        \"ams\": 177,\n        \"iad\": 148\n      },\n      {\n        \"timestamp\": \"Mar 13, 16:00\",\n        \"iad\": 152,\n        \"ams\": 163,\n        \"gru\": 127,\n        \"syd\": 85,\n        \"hkg\": 121,\n        \"jnb\": 130\n      },\n      {\n        \"timestamp\": \"Mar 13, 17:00\",\n        \"iad\": 126,\n        \"ams\": 144,\n        \"gru\": 140,\n        \"hkg\": 127,\n        \"jnb\": 140,\n        \"syd\": 91\n      },\n      {\n        \"timestamp\": \"Mar 13, 18:00\",\n        \"jnb\": 154,\n        \"hkg\": 116,\n        \"syd\": 87,\n        \"iad\": 122,\n        \"gru\": 152,\n        \"ams\": 153\n      },\n      {\n        \"timestamp\": \"Mar 13, 19:00\",\n        \"syd\": 87,\n        \"jnb\": 164,\n        \"hkg\": 106,\n        \"gru\": 161,\n        \"ams\": 151,\n        \"iad\": 138\n      },\n      {\n        \"timestamp\": \"Mar 13, 20:00\",\n        \"syd\": 88,\n        \"hkg\": 95,\n        \"jnb\": 126,\n        \"iad\": 130,\n        \"ams\": 158,\n        \"gru\": 163\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"syd\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 96,\n      \"p75Latency\": 110,\n      \"p90Latency\": 124,\n      \"p95Latency\": 146,\n      \"p99Latency\": 347\n    },\n    {\n      \"region\": \"gru\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 112,\n      \"p75Latency\": 144,\n      \"p90Latency\": 195,\n      \"p95Latency\": 240,\n      \"p99Latency\": 348\n    },\n    {\n      \"region\": \"iad\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 116,\n      \"p75Latency\": 133,\n      \"p90Latency\": 152,\n      \"p95Latency\": 168,\n      \"p99Latency\": 259\n    },\n    {\n      \"region\": \"ams\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 132,\n      \"p75Latency\": 152,\n      \"p90Latency\": 177,\n      \"p95Latency\": 203,\n      \"p99Latency\": 373\n    },\n    {\n      \"region\": \"hkg\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 111,\n      \"p75Latency\": 125,\n      \"p90Latency\": 148,\n      \"p95Latency\": 162,\n      \"p99Latency\": 272\n    },\n    {\n      \"region\": \"jnb\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 125,\n      \"p75Latency\": 148,\n      \"p90Latency\": 186,\n      \"p95Latency\": 210,\n      \"p99Latency\": 349\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/monitoring-vercel/vercel-roulette.json",
    "content": "{\n  \"regions\": [\"ams\", \"iad\", \"hkg\", \"jnb\", \"syd\", \"gru\"],\n  \"data\": {\n    \"regions\": [\"ams\", \"gru\", \"hkg\", \"iad\", \"jnb\", \"syd\"],\n    \"data\": [\n      {\n        \"timestamp\": \"Mar 10, 18:00\",\n        \"gru\": 179,\n        \"ams\": 179,\n        \"iad\": 61,\n        \"jnb\": 317,\n        \"hkg\": 292,\n        \"syd\": 249\n      },\n      {\n        \"timestamp\": \"Mar 10, 19:00\",\n        \"iad\": 460,\n        \"ams\": 238,\n        \"gru\": 484,\n        \"syd\": 286,\n        \"hkg\": 402,\n        \"jnb\": 357\n      },\n      {\n        \"timestamp\": \"Mar 10, 20:00\",\n        \"gru\": 526,\n        \"ams\": 557,\n        \"iad\": 396,\n        \"syd\": 562,\n        \"jnb\": 678,\n        \"hkg\": 588\n      },\n      {\n        \"timestamp\": \"Mar 10, 21:00\",\n        \"syd\": 599,\n        \"jnb\": 629,\n        \"hkg\": 710,\n        \"iad\": 390,\n        \"gru\": 506,\n        \"ams\": 526\n      },\n      {\n        \"timestamp\": \"Mar 10, 22:00\",\n        \"ams\": 504,\n        \"gru\": 504,\n        \"iad\": 424,\n        \"syd\": 560,\n        \"hkg\": 584,\n        \"jnb\": 662\n      },\n      {\n        \"timestamp\": \"Mar 10, 23:00\",\n        \"iad\": 413,\n        \"ams\": 534,\n        \"gru\": 478,\n        \"hkg\": 562,\n        \"jnb\": 692,\n        \"syd\": 531\n      },\n      {\n        \"timestamp\": \"Mar 11, 00:00\",\n        \"jnb\": 375,\n        \"hkg\": 286,\n        \"syd\": 266,\n        \"iad\": 88,\n        \"gru\": 284,\n        \"ams\": 226\n      },\n      {\n        \"timestamp\": \"Mar 11, 01:00\",\n        \"syd\": 281,\n        \"jnb\": 374,\n        \"hkg\": 286,\n        \"iad\": 133,\n        \"gru\": 228,\n        \"ams\": 292\n      },\n      {\n        \"timestamp\": \"Mar 11, 02:00\",\n        \"syd\": 518,\n        \"hkg\": 586,\n        \"jnb\": 672,\n        \"ams\": 522,\n        \"gru\": 502,\n        \"iad\": 429\n      },\n      {\n        \"timestamp\": \"Mar 11, 03:00\",\n        \"hkg\": 548,\n        \"jnb\": 663,\n        \"syd\": 561,\n        \"iad\": 426,\n        \"ams\": 520,\n        \"gru\": 515\n      },\n      {\n        \"timestamp\": \"Mar 11, 04:00\",\n        \"iad\": 129,\n        \"gru\": 194,\n        \"ams\": 222,\n        \"jnb\": 384,\n        \"hkg\": 282,\n        \"syd\": 260\n      },\n      {\n        \"timestamp\": \"Mar 11, 05:00\",\n        \"syd\": 266,\n        \"hkg\": 304,\n        \"jnb\": 374,\n        \"ams\": 194,\n        \"gru\": 244,\n        \"iad\": 107\n      },\n      {\n        \"timestamp\": \"Mar 11, 06:00\",\n        \"iad\": 110,\n        \"gru\": 263,\n        \"ams\": 216,\n        \"syd\": 252,\n        \"jnb\": 416,\n        \"hkg\": 299\n      },\n      {\n        \"timestamp\": \"Mar 11, 07:00\",\n        \"ams\": 572,\n        \"gru\": 462,\n        \"iad\": 428,\n        \"hkg\": 606,\n        \"jnb\": 614,\n        \"syd\": 562\n      },\n      {\n        \"timestamp\": \"Mar 11, 08:00\",\n        \"hkg\": 604,\n        \"jnb\": 690,\n        \"syd\": 548,\n        \"ams\": 513,\n        \"gru\": 469,\n        \"iad\": 414\n      },\n      {\n        \"timestamp\": \"Mar 11, 09:00\",\n        \"syd\": 772,\n        \"jnb\": 598,\n        \"hkg\": 581,\n        \"iad\": 406,\n        \"gru\": 528,\n        \"ams\": 498\n      },\n      {\n        \"timestamp\": \"Mar 11, 10:00\",\n        \"iad\": 440,\n        \"ams\": 520,\n        \"gru\": 518,\n        \"hkg\": 609,\n        \"jnb\": 683,\n        \"syd\": 560\n      },\n      {\n        \"timestamp\": \"Mar 11, 11:00\",\n        \"iad\": 116,\n        \"ams\": 243,\n        \"gru\": 285,\n        \"hkg\": 272,\n        \"jnb\": 628,\n        \"syd\": 272\n      },\n      {\n        \"timestamp\": \"Mar 11, 12:00\",\n        \"iad\": 414,\n        \"gru\": 459,\n        \"ams\": 666,\n        \"syd\": 602,\n        \"jnb\": 672,\n        \"hkg\": 590\n      },\n      {\n        \"timestamp\": \"Mar 11, 13:00\",\n        \"ams\": 230,\n        \"gru\": 458,\n        \"iad\": 140,\n        \"hkg\": 502,\n        \"jnb\": 422,\n        \"syd\": 254\n      },\n      {\n        \"timestamp\": \"Mar 11, 14:00\",\n        \"iad\": 110,\n        \"ams\": 202,\n        \"gru\": 230,\n        \"hkg\": 306,\n        \"jnb\": 388,\n        \"syd\": 266\n      },\n      {\n        \"timestamp\": \"Mar 11, 15:00\",\n        \"hkg\": 586,\n        \"jnb\": 644,\n        \"syd\": 536,\n        \"iad\": 430,\n        \"ams\": 536,\n        \"gru\": 512\n      },\n      {\n        \"timestamp\": \"Mar 11, 16:00\",\n        \"syd\": 269,\n        \"hkg\": 300,\n        \"jnb\": 440,\n        \"iad\": 122,\n        \"ams\": 222,\n        \"gru\": 230\n      },\n      {\n        \"timestamp\": \"Mar 11, 17:00\",\n        \"syd\": 256,\n        \"jnb\": 332,\n        \"hkg\": 300,\n        \"iad\": 100,\n        \"gru\": 429,\n        \"ams\": 216\n      },\n      {\n        \"timestamp\": \"Mar 11, 18:00\",\n        \"jnb\": 390,\n        \"hkg\": 265,\n        \"syd\": 246,\n        \"iad\": 65,\n        \"gru\": 211,\n        \"ams\": 190\n      },\n      {\n        \"timestamp\": \"Mar 11, 19:00\",\n        \"iad\": 118,\n        \"ams\": 259,\n        \"gru\": 300,\n        \"hkg\": 350,\n        \"jnb\": 389,\n        \"syd\": 256\n      },\n      {\n        \"timestamp\": \"Mar 11, 20:00\",\n        \"iad\": 114,\n        \"ams\": 218,\n        \"gru\": 176,\n        \"syd\": 313,\n        \"hkg\": 286,\n        \"jnb\": 332\n      },\n      {\n        \"timestamp\": \"Mar 11, 21:00\",\n        \"ams\": 238,\n        \"gru\": 174,\n        \"iad\": 110,\n        \"hkg\": 294,\n        \"jnb\": 662,\n        \"syd\": 293\n      },\n      {\n        \"timestamp\": \"Mar 11, 22:00\",\n        \"ams\": 230,\n        \"gru\": 226,\n        \"iad\": 118,\n        \"syd\": 247,\n        \"hkg\": 289,\n        \"jnb\": 384\n      },\n      {\n        \"timestamp\": \"Mar 11, 23:00\",\n        \"syd\": 250,\n        \"hkg\": 302,\n        \"jnb\": 391,\n        \"ams\": 212,\n        \"gru\": 172,\n        \"iad\": 104\n      },\n      {\n        \"timestamp\": \"Mar 12, 00:00\",\n        \"syd\": 278,\n        \"jnb\": 330,\n        \"hkg\": 288,\n        \"gru\": 220,\n        \"ams\": 210,\n        \"iad\": 96\n      },\n      {\n        \"timestamp\": \"Mar 12, 01:00\",\n        \"syd\": 284,\n        \"jnb\": 405,\n        \"hkg\": 294,\n        \"iad\": 120,\n        \"gru\": 514,\n        \"ams\": 242\n      },\n      {\n        \"timestamp\": \"Mar 12, 02:00\",\n        \"hkg\": 288,\n        \"jnb\": 380,\n        \"syd\": 256,\n        \"iad\": 80,\n        \"ams\": 233,\n        \"gru\": 181\n      },\n      {\n        \"timestamp\": \"Mar 12, 03:00\",\n        \"hkg\": 297,\n        \"jnb\": 373,\n        \"syd\": 260,\n        \"iad\": 92,\n        \"ams\": 202,\n        \"gru\": 189\n      },\n      {\n        \"timestamp\": \"Mar 12, 04:00\",\n        \"iad\": 134,\n        \"gru\": 186,\n        \"ams\": 236,\n        \"syd\": 251,\n        \"jnb\": 400,\n        \"hkg\": 271\n      },\n      {\n        \"timestamp\": \"Mar 12, 05:00\",\n        \"gru\": 175,\n        \"iad\": 112,\n        \"hkg\": 272,\n        \"jnb\": 404,\n        \"syd\": 258,\n        \"ams\": 222\n      },\n      {\n        \"timestamp\": \"Mar 12, 06:00\",\n        \"gru\": 220,\n        \"ams\": 202,\n        \"iad\": 86,\n        \"syd\": 252,\n        \"jnb\": 383,\n        \"hkg\": 306\n      },\n      {\n        \"timestamp\": \"Mar 12, 07:00\",\n        \"syd\": 246,\n        \"jnb\": 406,\n        \"hkg\": 288,\n        \"gru\": 171,\n        \"ams\": 185,\n        \"iad\": 64\n      },\n      {\n        \"timestamp\": \"Mar 12, 08:00\",\n        \"jnb\": 333,\n        \"hkg\": 280,\n        \"syd\": 244,\n        \"gru\": 266,\n        \"ams\": 188,\n        \"iad\": 86\n      },\n      {\n        \"timestamp\": \"Mar 12, 09:00\",\n        \"ams\": 226,\n        \"gru\": 174,\n        \"iad\": 82,\n        \"hkg\": 283,\n        \"jnb\": 351,\n        \"syd\": 264\n      },\n      {\n        \"timestamp\": \"Mar 12, 10:00\",\n        \"ams\": 200,\n        \"gru\": 172,\n        \"iad\": 94,\n        \"syd\": 248,\n        \"hkg\": 277,\n        \"jnb\": 392\n      },\n      {\n        \"timestamp\": \"Mar 12, 11:00\",\n        \"gru\": 278,\n        \"ams\": 252,\n        \"iad\": 116,\n        \"syd\": 268,\n        \"jnb\": 657,\n        \"hkg\": 296\n      },\n      {\n        \"timestamp\": \"Mar 12, 12:00\",\n        \"gru\": 276,\n        \"ams\": 211,\n        \"iad\": 109,\n        \"jnb\": 382,\n        \"hkg\": 289,\n        \"syd\": 270\n      },\n      {\n        \"timestamp\": \"Mar 12, 13:00\",\n        \"syd\": 324,\n        \"jnb\": 381,\n        \"hkg\": 302,\n        \"iad\": 108,\n        \"gru\": 525,\n        \"ams\": 243\n      },\n      {\n        \"timestamp\": \"Mar 12, 14:00\",\n        \"jnb\": 388,\n        \"hkg\": 264,\n        \"syd\": 256,\n        \"iad\": 112,\n        \"gru\": 248,\n        \"ams\": 244\n      },\n      {\n        \"timestamp\": \"Mar 12, 15:00\",\n        \"iad\": 101,\n        \"gru\": 232,\n        \"ams\": 326,\n        \"jnb\": 387,\n        \"hkg\": 260,\n        \"syd\": 262\n      },\n      {\n        \"timestamp\": \"Mar 12, 16:00\",\n        \"iad\": 132,\n        \"ams\": 244,\n        \"gru\": 238,\n        \"syd\": 256,\n        \"hkg\": 535,\n        \"jnb\": 374\n      },\n      {\n        \"timestamp\": \"Mar 12, 17:00\",\n        \"gru\": 439,\n        \"ams\": 255,\n        \"iad\": 120,\n        \"jnb\": 382,\n        \"hkg\": 308,\n        \"syd\": 247\n      },\n      {\n        \"timestamp\": \"Mar 12, 18:00\",\n        \"syd\": 248,\n        \"jnb\": 390,\n        \"hkg\": 308,\n        \"gru\": 294,\n        \"ams\": 185,\n        \"iad\": 120\n      },\n      {\n        \"timestamp\": \"Mar 12, 19:00\",\n        \"hkg\": 268,\n        \"jnb\": 555,\n        \"syd\": 259,\n        \"iad\": 81,\n        \"ams\": 231,\n        \"gru\": 276\n      },\n      {\n        \"timestamp\": \"Mar 12, 20:00\",\n        \"hkg\": 270,\n        \"jnb\": 390,\n        \"syd\": 253,\n        \"ams\": 172,\n        \"gru\": 225,\n        \"iad\": 73\n      },\n      {\n        \"timestamp\": \"Mar 12, 21:00\",\n        \"iad\": 122,\n        \"ams\": 206,\n        \"gru\": 176,\n        \"hkg\": 302,\n        \"jnb\": 373,\n        \"syd\": 275\n      },\n      {\n        \"timestamp\": \"Mar 12, 22:00\",\n        \"iad\": 108,\n        \"ams\": 196,\n        \"gru\": 294,\n        \"hkg\": 280,\n        \"jnb\": 366,\n        \"syd\": 262\n      },\n      {\n        \"timestamp\": \"Mar 12, 23:00\",\n        \"gru\": 188,\n        \"ams\": 184,\n        \"iad\": 88,\n        \"syd\": 259,\n        \"jnb\": 379,\n        \"hkg\": 262\n      },\n      {\n        \"timestamp\": \"Mar 13, 00:00\",\n        \"syd\": 254,\n        \"hkg\": 266,\n        \"jnb\": 592,\n        \"iad\": 82,\n        \"ams\": 182,\n        \"gru\": 242\n      },\n      {\n        \"timestamp\": \"Mar 13, 01:00\",\n        \"iad\": 110,\n        \"ams\": 222,\n        \"gru\": 332,\n        \"hkg\": 292,\n        \"jnb\": 377,\n        \"syd\": 264\n      },\n      {\n        \"timestamp\": \"Mar 13, 02:00\",\n        \"jnb\": 378,\n        \"hkg\": 314,\n        \"syd\": 278,\n        \"iad\": 104,\n        \"gru\": 236,\n        \"ams\": 202\n      },\n      {\n        \"timestamp\": \"Mar 13, 03:00\",\n        \"hkg\": 302,\n        \"jnb\": 384,\n        \"syd\": 285,\n        \"iad\": 132,\n        \"ams\": 228,\n        \"gru\": 276\n      },\n      {\n        \"timestamp\": \"Mar 13, 04:00\",\n        \"jnb\": 354,\n        \"hkg\": 364,\n        \"syd\": 250,\n        \"gru\": 380,\n        \"ams\": 220,\n        \"iad\": 109\n      },\n      {\n        \"timestamp\": \"Mar 13, 05:00\",\n        \"iad\": 128,\n        \"ams\": 213,\n        \"gru\": 278,\n        \"hkg\": 300,\n        \"jnb\": 362,\n        \"syd\": 252\n      },\n      {\n        \"timestamp\": \"Mar 13, 06:00\",\n        \"jnb\": 362,\n        \"hkg\": 278,\n        \"syd\": 258,\n        \"iad\": 73,\n        \"gru\": 226,\n        \"ams\": 186\n      },\n      {\n        \"timestamp\": \"Mar 13, 07:00\",\n        \"gru\": 238,\n        \"ams\": 700,\n        \"iad\": 169,\n        \"syd\": 274,\n        \"jnb\": 412,\n        \"hkg\": 328\n      },\n      {\n        \"timestamp\": \"Mar 13, 08:00\",\n        \"syd\": 248,\n        \"hkg\": 299,\n        \"jnb\": 398,\n        \"iad\": 104,\n        \"ams\": 254,\n        \"gru\": 336\n      },\n      {\n        \"timestamp\": \"Mar 13, 09:00\",\n        \"gru\": 236,\n        \"ams\": 194,\n        \"iad\": 80,\n        \"jnb\": 398,\n        \"hkg\": 318,\n        \"syd\": 258\n      },\n      {\n        \"timestamp\": \"Mar 13, 10:00\",\n        \"jnb\": 398,\n        \"hkg\": 274,\n        \"syd\": 250,\n        \"gru\": 273,\n        \"ams\": 270,\n        \"iad\": 96\n      },\n      {\n        \"timestamp\": \"Mar 13, 11:00\",\n        \"syd\": 250,\n        \"hkg\": 284,\n        \"jnb\": 386,\n        \"iad\": 104,\n        \"ams\": 202,\n        \"gru\": 270\n      },\n      {\n        \"timestamp\": \"Mar 13, 12:00\",\n        \"hkg\": 282,\n        \"jnb\": 380,\n        \"syd\": 254,\n        \"ams\": 206,\n        \"gru\": 300,\n        \"iad\": 98\n      },\n      {\n        \"timestamp\": \"Mar 13, 13:00\",\n        \"iad\": 89,\n        \"ams\": 221,\n        \"gru\": 358,\n        \"hkg\": 299,\n        \"jnb\": 356,\n        \"syd\": 252\n      },\n      {\n        \"timestamp\": \"Mar 13, 14:00\",\n        \"hkg\": 290,\n        \"jnb\": 373,\n        \"syd\": 251,\n        \"iad\": 142,\n        \"ams\": 269,\n        \"gru\": 242\n      },\n      {\n        \"timestamp\": \"Mar 13, 15:00\",\n        \"syd\": 266,\n        \"jnb\": 426,\n        \"hkg\": 304,\n        \"gru\": 282,\n        \"ams\": 216,\n        \"iad\": 102\n      },\n      {\n        \"timestamp\": \"Mar 13, 16:00\",\n        \"iad\": 110,\n        \"ams\": 216,\n        \"gru\": 275,\n        \"syd\": 258,\n        \"hkg\": 300,\n        \"jnb\": 379\n      },\n      {\n        \"timestamp\": \"Mar 13, 17:00\",\n        \"iad\": 92,\n        \"ams\": 221,\n        \"gru\": 243,\n        \"hkg\": 300,\n        \"jnb\": 388,\n        \"syd\": 248\n      },\n      {\n        \"timestamp\": \"Mar 13, 18:00\",\n        \"jnb\": 335,\n        \"hkg\": 302,\n        \"syd\": 258,\n        \"iad\": 172,\n        \"gru\": 288,\n        \"ams\": 202\n      },\n      {\n        \"timestamp\": \"Mar 13, 19:00\",\n        \"syd\": 350,\n        \"jnb\": 600,\n        \"hkg\": 306,\n        \"gru\": 264,\n        \"ams\": 241,\n        \"iad\": 111\n      },\n      {\n        \"timestamp\": \"Mar 13, 20:00\",\n        \"syd\": 258,\n        \"hkg\": 290,\n        \"jnb\": 334,\n        \"iad\": 144,\n        \"ams\": 230,\n        \"gru\": 353\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"syd\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 258,\n      \"p75Latency\": 516,\n      \"p90Latency\": 881,\n      \"p95Latency\": 914,\n      \"p99Latency\": 1027\n    },\n    {\n      \"region\": \"gru\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 269,\n      \"p75Latency\": 673,\n      \"p90Latency\": 851,\n      \"p95Latency\": 916,\n      \"p99Latency\": 1040\n    },\n    {\n      \"region\": \"iad\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 113,\n      \"p75Latency\": 216,\n      \"p90Latency\": 743,\n      \"p95Latency\": 777,\n      \"p99Latency\": 831\n    },\n    {\n      \"region\": \"ams\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 225,\n      \"p75Latency\": 338,\n      \"p90Latency\": 837,\n      \"p95Latency\": 872,\n      \"p99Latency\": 986\n    },\n    {\n      \"region\": \"hkg\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 295,\n      \"p75Latency\": 504,\n      \"p90Latency\": 909,\n      \"p95Latency\": 948,\n      \"p99Latency\": 1063\n    },\n    {\n      \"region\": \"jnb\",\n      \"count\": 447,\n      \"ok\": 447,\n      \"lastTimestamp\": 1710359410395,\n      \"p50Latency\": 385,\n      \"p75Latency\": 803,\n      \"p90Latency\": 991,\n      \"p95Latency\": 1027,\n      \"p99Latency\": 1139\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/assets/posts/monitoring-vercel/vercel-warm.json",
    "content": "{\n  \"regions\": [\"ams\", \"iad\", \"hkg\", \"jnb\", \"syd\", \"gru\"],\n  \"data\": {\n    \"regions\": [\"ams\", \"gru\", \"hkg\", \"iad\", \"jnb\", \"syd\"],\n    \"data\": [\n      {\n        \"timestamp\": \"Mar 10, 18:00\",\n        \"gru\": 198,\n        \"ams\": 202,\n        \"iad\": 68,\n        \"jnb\": 388,\n        \"hkg\": 302,\n        \"syd\": 247\n      },\n      {\n        \"timestamp\": \"Mar 10, 19:00\",\n        \"iad\": 68,\n        \"ams\": 172,\n        \"gru\": 164,\n        \"syd\": 245,\n        \"hkg\": 271,\n        \"jnb\": 384\n      },\n      {\n        \"timestamp\": \"Mar 10, 20:00\",\n        \"gru\": 184,\n        \"ams\": 178,\n        \"iad\": 63,\n        \"syd\": 248,\n        \"jnb\": 374,\n        \"hkg\": 296\n      },\n      {\n        \"timestamp\": \"Mar 10, 21:00\",\n        \"syd\": 252,\n        \"jnb\": 372,\n        \"hkg\": 268,\n        \"iad\": 62,\n        \"gru\": 172,\n        \"ams\": 170\n      },\n      {\n        \"timestamp\": \"Mar 10, 22:00\",\n        \"ams\": 174,\n        \"gru\": 162,\n        \"iad\": 62,\n        \"syd\": 243,\n        \"hkg\": 292,\n        \"jnb\": 368\n      },\n      {\n        \"timestamp\": \"Mar 10, 23:00\",\n        \"iad\": 58,\n        \"ams\": 170,\n        \"gru\": 181,\n        \"hkg\": 294,\n        \"jnb\": 374,\n        \"syd\": 246\n      },\n      {\n        \"timestamp\": \"Mar 11, 00:00\",\n        \"jnb\": 343,\n        \"hkg\": 274,\n        \"syd\": 247,\n        \"iad\": 70,\n        \"gru\": 184,\n        \"ams\": 166\n      },\n      {\n        \"timestamp\": \"Mar 11, 01:00\",\n        \"syd\": 246,\n        \"jnb\": 370,\n        \"hkg\": 316,\n        \"iad\": 96,\n        \"gru\": 167,\n        \"ams\": 168\n      },\n      {\n        \"timestamp\": \"Mar 11, 02:00\",\n        \"syd\": 248,\n        \"hkg\": 277,\n        \"jnb\": 370,\n        \"ams\": 166,\n        \"gru\": 267,\n        \"iad\": 64\n      },\n      {\n        \"timestamp\": \"Mar 11, 03:00\",\n        \"hkg\": 268,\n        \"jnb\": 371,\n        \"syd\": 256,\n        \"iad\": 62,\n        \"ams\": 168,\n        \"gru\": 174\n      },\n      {\n        \"timestamp\": \"Mar 11, 04:00\",\n        \"iad\": 60,\n        \"gru\": 168,\n        \"ams\": 173,\n        \"jnb\": 335,\n        \"hkg\": 282,\n        \"syd\": 250\n      },\n      {\n        \"timestamp\": \"Mar 11, 05:00\",\n        \"syd\": 246,\n        \"hkg\": 287,\n        \"jnb\": 374,\n        \"ams\": 170,\n        \"gru\": 264,\n        \"iad\": 67\n      },\n      {\n        \"timestamp\": \"Mar 11, 06:00\",\n        \"iad\": 55,\n        \"gru\": 169,\n        \"ams\": 170,\n        \"syd\": 242,\n        \"jnb\": 368,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Mar 11, 07:00\",\n        \"ams\": 213,\n        \"gru\": 268,\n        \"iad\": 86,\n        \"hkg\": 279,\n        \"jnb\": 360,\n        \"syd\": 263\n      },\n      {\n        \"timestamp\": \"Mar 11, 08:00\",\n        \"hkg\": 270,\n        \"jnb\": 370,\n        \"syd\": 250,\n        \"ams\": 172,\n        \"gru\": 172,\n        \"iad\": 72\n      },\n      {\n        \"timestamp\": \"Mar 11, 09:00\",\n        \"syd\": 247,\n        \"jnb\": 372,\n        \"hkg\": 291,\n        \"iad\": 56,\n        \"gru\": 184,\n        \"ams\": 178\n      },\n      {\n        \"timestamp\": \"Mar 11, 10:00\",\n        \"iad\": 57,\n        \"ams\": 170,\n        \"gru\": 167,\n        \"hkg\": 266,\n        \"jnb\": 368,\n        \"syd\": 246\n      },\n      {\n        \"timestamp\": \"Mar 11, 11:00\",\n        \"iad\": 58,\n        \"ams\": 192,\n        \"gru\": 172,\n        \"hkg\": 268,\n        \"jnb\": 378,\n        \"syd\": 252\n      },\n      {\n        \"timestamp\": \"Mar 11, 12:00\",\n        \"iad\": 86,\n        \"gru\": 240,\n        \"ams\": 178,\n        \"syd\": 249,\n        \"jnb\": 374,\n        \"hkg\": 281\n      },\n      {\n        \"timestamp\": \"Mar 11, 13:00\",\n        \"ams\": 173,\n        \"gru\": 223,\n        \"iad\": 80,\n        \"hkg\": 284,\n        \"jnb\": 380,\n        \"syd\": 246\n      },\n      {\n        \"timestamp\": \"Mar 11, 14:00\",\n        \"iad\": 72,\n        \"ams\": 193,\n        \"gru\": 180,\n        \"hkg\": 296,\n        \"jnb\": 388,\n        \"syd\": 254\n      },\n      {\n        \"timestamp\": \"Mar 11, 15:00\",\n        \"hkg\": 286,\n        \"jnb\": 382,\n        \"syd\": 256,\n        \"iad\": 64,\n        \"ams\": 169,\n        \"gru\": 191\n      },\n      {\n        \"timestamp\": \"Mar 11, 16:00\",\n        \"syd\": 258,\n        \"hkg\": 339,\n        \"jnb\": 420,\n        \"iad\": 80,\n        \"ams\": 186,\n        \"gru\": 174\n      },\n      {\n        \"timestamp\": \"Mar 11, 17:00\",\n        \"syd\": 259,\n        \"jnb\": 383,\n        \"hkg\": 301,\n        \"iad\": 76,\n        \"gru\": 184,\n        \"ams\": 209\n      },\n      {\n        \"timestamp\": \"Mar 11, 18:00\",\n        \"jnb\": 384,\n        \"hkg\": 294,\n        \"syd\": 258,\n        \"iad\": 126,\n        \"gru\": 214,\n        \"ams\": 222\n      },\n      {\n        \"timestamp\": \"Mar 11, 19:00\",\n        \"iad\": 64,\n        \"ams\": 168,\n        \"gru\": 188,\n        \"hkg\": 292,\n        \"jnb\": 358,\n        \"syd\": 250\n      },\n      {\n        \"timestamp\": \"Mar 11, 20:00\",\n        \"iad\": 60,\n        \"ams\": 175,\n        \"gru\": 260,\n        \"syd\": 245,\n        \"hkg\": 294,\n        \"jnb\": 352\n      },\n      {\n        \"timestamp\": \"Mar 11, 21:00\",\n        \"ams\": 191,\n        \"gru\": 173,\n        \"iad\": 65,\n        \"hkg\": 286,\n        \"jnb\": 387,\n        \"syd\": 278\n      },\n      {\n        \"timestamp\": \"Mar 11, 22:00\",\n        \"ams\": 179,\n        \"gru\": 222,\n        \"iad\": 65,\n        \"syd\": 248,\n        \"hkg\": 273,\n        \"jnb\": 376\n      },\n      {\n        \"timestamp\": \"Mar 11, 23:00\",\n        \"syd\": 256,\n        \"hkg\": 303,\n        \"jnb\": 374,\n        \"ams\": 170,\n        \"gru\": 222,\n        \"iad\": 63\n      },\n      {\n        \"timestamp\": \"Mar 12, 00:00\",\n        \"syd\": 254,\n        \"jnb\": 362,\n        \"hkg\": 294,\n        \"gru\": 271,\n        \"ams\": 180,\n        \"iad\": 70\n      },\n      {\n        \"timestamp\": \"Mar 12, 01:00\",\n        \"syd\": 252,\n        \"jnb\": 414,\n        \"hkg\": 300,\n        \"iad\": 92,\n        \"gru\": 182,\n        \"ams\": 184\n      },\n      {\n        \"timestamp\": \"Mar 12, 02:00\",\n        \"hkg\": 263,\n        \"jnb\": 372,\n        \"syd\": 246,\n        \"iad\": 57,\n        \"ams\": 168,\n        \"gru\": 216\n      },\n      {\n        \"timestamp\": \"Mar 12, 03:00\",\n        \"hkg\": 290,\n        \"jnb\": 368,\n        \"syd\": 248,\n        \"iad\": 58,\n        \"ams\": 172,\n        \"gru\": 218\n      },\n      {\n        \"timestamp\": \"Mar 12, 04:00\",\n        \"iad\": 56,\n        \"gru\": 167,\n        \"ams\": 166,\n        \"syd\": 247,\n        \"jnb\": 370,\n        \"hkg\": 274\n      },\n      {\n        \"timestamp\": \"Mar 12, 05:00\",\n        \"gru\": 262,\n        \"iad\": 57,\n        \"hkg\": 265,\n        \"jnb\": 323,\n        \"syd\": 246,\n        \"ams\": 170\n      },\n      {\n        \"timestamp\": \"Mar 12, 06:00\",\n        \"gru\": 193,\n        \"ams\": 164,\n        \"iad\": 56,\n        \"syd\": 252,\n        \"jnb\": 338,\n        \"hkg\": 284\n      },\n      {\n        \"timestamp\": \"Mar 12, 07:00\",\n        \"syd\": 245,\n        \"jnb\": 379,\n        \"hkg\": 296,\n        \"gru\": 176,\n        \"ams\": 174,\n        \"iad\": 56\n      },\n      {\n        \"timestamp\": \"Mar 12, 08:00\",\n        \"jnb\": 369,\n        \"hkg\": 294,\n        \"syd\": 252,\n        \"gru\": 263,\n        \"ams\": 176,\n        \"iad\": 60\n      },\n      {\n        \"timestamp\": \"Mar 12, 09:00\",\n        \"ams\": 175,\n        \"gru\": 164,\n        \"iad\": 60,\n        \"hkg\": 280,\n        \"jnb\": 384,\n        \"syd\": 258\n      },\n      {\n        \"timestamp\": \"Mar 12, 10:00\",\n        \"ams\": 175,\n        \"gru\": 189,\n        \"iad\": 56,\n        \"syd\": 251,\n        \"hkg\": 264,\n        \"jnb\": 381\n      },\n      {\n        \"timestamp\": \"Mar 12, 11:00\",\n        \"gru\": 262,\n        \"ams\": 172,\n        \"iad\": 56,\n        \"syd\": 244,\n        \"jnb\": 378,\n        \"hkg\": 271\n      },\n      {\n        \"timestamp\": \"Mar 12, 12:00\",\n        \"gru\": 266,\n        \"ams\": 168,\n        \"iad\": 58,\n        \"jnb\": 384,\n        \"hkg\": 280,\n        \"syd\": 248\n      },\n      {\n        \"timestamp\": \"Mar 12, 13:00\",\n        \"syd\": 250,\n        \"jnb\": 357,\n        \"hkg\": 289,\n        \"iad\": 58,\n        \"gru\": 222,\n        \"ams\": 170\n      },\n      {\n        \"timestamp\": \"Mar 12, 14:00\",\n        \"jnb\": 351,\n        \"hkg\": 288,\n        \"syd\": 256,\n        \"iad\": 58,\n        \"gru\": 196,\n        \"ams\": 168\n      },\n      {\n        \"timestamp\": \"Mar 12, 15:00\",\n        \"iad\": 64,\n        \"gru\": 244,\n        \"ams\": 175,\n        \"jnb\": 376,\n        \"hkg\": 284,\n        \"syd\": 242\n      },\n      {\n        \"timestamp\": \"Mar 12, 16:00\",\n        \"iad\": 62,\n        \"ams\": 180,\n        \"gru\": 268,\n        \"syd\": 242,\n        \"hkg\": 286,\n        \"jnb\": 378\n      },\n      {\n        \"timestamp\": \"Mar 12, 17:00\",\n        \"gru\": 276,\n        \"ams\": 180,\n        \"iad\": 64,\n        \"jnb\": 378,\n        \"hkg\": 280,\n        \"syd\": 248\n      },\n      {\n        \"timestamp\": \"Mar 12, 18:00\",\n        \"syd\": 242,\n        \"jnb\": 374,\n        \"hkg\": 275,\n        \"gru\": 178,\n        \"ams\": 164,\n        \"iad\": 60\n      },\n      {\n        \"timestamp\": \"Mar 12, 19:00\",\n        \"hkg\": 268,\n        \"jnb\": 385,\n        \"syd\": 247,\n        \"iad\": 66,\n        \"ams\": 174,\n        \"gru\": 175\n      },\n      {\n        \"timestamp\": \"Mar 12, 20:00\",\n        \"hkg\": 279,\n        \"jnb\": 374,\n        \"syd\": 254,\n        \"ams\": 168,\n        \"gru\": 178,\n        \"iad\": 62\n      },\n      {\n        \"timestamp\": \"Mar 12, 21:00\",\n        \"iad\": 88,\n        \"ams\": 200,\n        \"gru\": 286,\n        \"hkg\": 302,\n        \"jnb\": 378,\n        \"syd\": 252\n      },\n      {\n        \"timestamp\": \"Mar 12, 22:00\",\n        \"iad\": 67,\n        \"ams\": 166,\n        \"gru\": 275,\n        \"hkg\": 282,\n        \"jnb\": 374,\n        \"syd\": 249\n      },\n      {\n        \"timestamp\": \"Mar 12, 23:00\",\n        \"gru\": 272,\n        \"ams\": 195,\n        \"iad\": 60,\n        \"syd\": 255,\n        \"jnb\": 370,\n        \"hkg\": 294\n      },\n      {\n        \"timestamp\": \"Mar 13, 00:00\",\n        \"syd\": 253,\n        \"hkg\": 290,\n        \"jnb\": 339,\n        \"iad\": 72,\n        \"ams\": 208,\n        \"gru\": 266\n      },\n      {\n        \"timestamp\": \"Mar 13, 01:00\",\n        \"iad\": 67,\n        \"ams\": 170,\n        \"gru\": 228,\n        \"hkg\": 348,\n        \"jnb\": 332,\n        \"syd\": 252\n      },\n      {\n        \"timestamp\": \"Mar 13, 02:00\",\n        \"jnb\": 374,\n        \"hkg\": 299,\n        \"syd\": 250,\n        \"iad\": 58,\n        \"gru\": 274,\n        \"ams\": 163\n      },\n      {\n        \"timestamp\": \"Mar 13, 03:00\",\n        \"hkg\": 296,\n        \"jnb\": 374,\n        \"syd\": 247,\n        \"iad\": 56,\n        \"ams\": 167,\n        \"gru\": 264\n      },\n      {\n        \"timestamp\": \"Mar 13, 04:00\",\n        \"jnb\": 374,\n        \"hkg\": 282,\n        \"syd\": 257,\n        \"gru\": 220,\n        \"ams\": 172,\n        \"iad\": 56\n      },\n      {\n        \"timestamp\": \"Mar 13, 05:00\",\n        \"iad\": 56,\n        \"ams\": 165,\n        \"gru\": 272,\n        \"hkg\": 268,\n        \"jnb\": 348,\n        \"syd\": 252\n      },\n      {\n        \"timestamp\": \"Mar 13, 06:00\",\n        \"jnb\": 383,\n        \"hkg\": 306,\n        \"syd\": 247,\n        \"iad\": 64,\n        \"gru\": 170,\n        \"ams\": 217\n      },\n      {\n        \"timestamp\": \"Mar 13, 07:00\",\n        \"gru\": 272,\n        \"ams\": 179,\n        \"iad\": 58,\n        \"syd\": 250,\n        \"jnb\": 352,\n        \"hkg\": 284\n      },\n      {\n        \"timestamp\": \"Mar 13, 08:00\",\n        \"syd\": 240,\n        \"hkg\": 256,\n        \"jnb\": 378,\n        \"iad\": 61,\n        \"ams\": 187,\n        \"gru\": 271\n      },\n      {\n        \"timestamp\": \"Mar 13, 09:00\",\n        \"gru\": 262,\n        \"ams\": 178,\n        \"iad\": 60,\n        \"jnb\": 382,\n        \"hkg\": 261,\n        \"syd\": 250\n      },\n      {\n        \"timestamp\": \"Mar 13, 10:00\",\n        \"jnb\": 376,\n        \"hkg\": 272,\n        \"syd\": 248,\n        \"gru\": 269,\n        \"ams\": 172,\n        \"iad\": 56\n      },\n      {\n        \"timestamp\": \"Mar 13, 11:00\",\n        \"syd\": 250,\n        \"hkg\": 280,\n        \"jnb\": 382,\n        \"iad\": 60,\n        \"ams\": 176,\n        \"gru\": 240\n      },\n      {\n        \"timestamp\": \"Mar 13, 12:00\",\n        \"hkg\": 275,\n        \"jnb\": 372,\n        \"syd\": 246,\n        \"ams\": 174,\n        \"gru\": 266,\n        \"iad\": 57\n      },\n      {\n        \"timestamp\": \"Mar 13, 13:00\",\n        \"iad\": 70,\n        \"ams\": 178,\n        \"gru\": 276,\n        \"hkg\": 286,\n        \"jnb\": 371,\n        \"syd\": 246\n      },\n      {\n        \"timestamp\": \"Mar 13, 14:00\",\n        \"hkg\": 298,\n        \"jnb\": 393,\n        \"syd\": 246,\n        \"iad\": 80,\n        \"ams\": 196,\n        \"gru\": 173\n      },\n      {\n        \"timestamp\": \"Mar 13, 15:00\",\n        \"syd\": 246,\n        \"jnb\": 382,\n        \"hkg\": 272,\n        \"gru\": 219,\n        \"ams\": 178,\n        \"iad\": 60\n      },\n      {\n        \"timestamp\": \"Mar 13, 16:00\",\n        \"iad\": 62,\n        \"ams\": 184,\n        \"gru\": 271,\n        \"syd\": 243,\n        \"hkg\": 278,\n        \"jnb\": 346\n      },\n      {\n        \"timestamp\": \"Mar 13, 17:00\",\n        \"iad\": 66,\n        \"ams\": 180,\n        \"gru\": 253,\n        \"hkg\": 299,\n        \"jnb\": 380,\n        \"syd\": 244\n      },\n      {\n        \"timestamp\": \"Mar 13, 18:00\",\n        \"jnb\": 371,\n        \"hkg\": 294,\n        \"syd\": 250,\n        \"iad\": 62,\n        \"gru\": 240,\n        \"ams\": 176\n      },\n      {\n        \"timestamp\": \"Mar 13, 19:00\",\n        \"syd\": 247,\n        \"jnb\": 338,\n        \"hkg\": 272,\n        \"gru\": 178,\n        \"ams\": 169,\n        \"iad\": 66\n      },\n      {\n        \"timestamp\": \"Mar 13, 20:00\",\n        \"syd\": 250,\n        \"hkg\": 292,\n        \"jnb\": 374,\n        \"iad\": 66,\n        \"ams\": 170,\n        \"gru\": 224\n      }\n    ]\n  },\n  \"metricsByRegion\": [\n    {\n      \"region\": \"syd\",\n      \"count\": 891,\n      \"ok\": 891,\n      \"lastTimestamp\": 1710359730476,\n      \"p50Latency\": 248,\n      \"p75Latency\": 260,\n      \"p90Latency\": 294,\n      \"p95Latency\": 347,\n      \"p99Latency\": 886\n    },\n    {\n      \"region\": \"gru\",\n      \"count\": 891,\n      \"ok\": 891,\n      \"lastTimestamp\": 1710359730476,\n      \"p50Latency\": 190,\n      \"p75Latency\": 275,\n      \"p90Latency\": 296,\n      \"p95Latency\": 369,\n      \"p99Latency\": 705\n    },\n    {\n      \"region\": \"iad\",\n      \"count\": 891,\n      \"ok\": 891,\n      \"lastTimestamp\": 1710359730476,\n      \"p50Latency\": 62,\n      \"p75Latency\": 77,\n      \"p90Latency\": 122,\n      \"p95Latency\": 358,\n      \"p99Latency\": 767\n    },\n    {\n      \"region\": \"ams\",\n      \"count\": 891,\n      \"ok\": 891,\n      \"lastTimestamp\": 1710359730476,\n      \"p50Latency\": 173,\n      \"p75Latency\": 191,\n      \"p90Latency\": 261,\n      \"p95Latency\": 782,\n      \"p99Latency\": 869\n    },\n    {\n      \"region\": \"hkg\",\n      \"count\": 891,\n      \"ok\": 891,\n      \"lastTimestamp\": 1710359730476,\n      \"p50Latency\": 287,\n      \"p75Latency\": 308,\n      \"p90Latency\": 431,\n      \"p95Latency\": 470,\n      \"p99Latency\": 959\n    },\n    {\n      \"region\": \"jnb\",\n      \"count\": 891,\n      \"ok\": 891,\n      \"lastTimestamp\": 1710359730476,\n      \"p50Latency\": 374,\n      \"p75Latency\": 390,\n      \"p90Latency\": 439,\n      \"p95Latency\": 522,\n      \"p99Latency\": 1003\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/public/llms.txt",
    "content": "# openstatus\n\nOpenstatus is an open-source uptime monitoring and status page platform founded in 2023 by Thibault Le Ouay Ducasse and Maximilian Kaske. It monitors websites, APIs, and services from 28 regions globally across multiple cloud providers (Fly.io, Koyeb, Railway). Openstatus is bootstrapped, not VC-funded, and available both as a managed SaaS and for self-hosting.\n\n## Who it's for\n\n- Development teams that want transparent incident communication\n- Companies that need multi-region uptime monitoring\n- Teams that prefer infrastructure-as-code workflows (monitoring as code via YAML)\n- Organizations that require self-hosted monitoring behind a firewall (private locations)\n- Open-source projects and startups looking for a free or affordable monitoring solution\n\n## Pricing\n\n- **Hobby** — $0/month: 1 monitor, 6 regions, 10m check interval, 1 status page, 3 page components, 14-day data retention\n- **Starter** — $30/month: 20 monitors, 28 regions, 1m check interval, 1 status page, 20 components, 3-month retention, subscribers, custom domain, WhatsApp/SMS/PagerDuty alerts\n- **Pro** — $100/month: 50 monitors, 28 regions, 30s check interval, 5 status pages, 50 components, 12-month retention, private locations, OTel exporter, 20 notification channels\n\nPricing is available in USD, EUR, and INR.\n\n## Key Features\n\n- **28-region monitoring**: Parallel checks across Europe, North America, South America, Asia, Africa, and Oceania — no round-robin, all regions fire simultaneously\n- **Multi-cloud**: Monitors run on Fly.io, Koyeb, and Railway for true cloud diversity\n- **Status Pages**: Branded public or password-protected pages with custom domains, themes, maintenance windows, and subscriber notifications (email, RSS, SSH)\n- **API Monitoring**: Assertions, thresholds, status code checks, header and body validation\n- **Monitoring as Code**: Define monitors in YAML, manage via CLI or GitHub Actions\n- **Private Locations**: 8.5MB Docker image for monitoring internal services behind firewalls\n- **Alerting**: Email, Slack, Discord, webhook, WhatsApp, SMS, PagerDuty, OpsGenie, Grafana OnCall\n- **OpenTelemetry**: Export synthetic check metrics to any OTLP endpoint\n- **SDK**: Node.js SDK available on JSR (@openstatus/sdk-node)\n- **Open-source**: AGPL-3.0-licensed, self-hostable, 8k+ GitHub stars\n\n## Key Differentiators\n\n- Open-source and bootstrapped (no VC funding)\n- Parallel scheduling strategy — all selected regions check simultaneously (vs. round-robin competitors)\n- Unlimited team members on paid plans\n- Status page subscribers included (not a paid add-on)\n- Private status pages included in team plan (not an additional charge)\n- Self-hosting option with full feature parity\n\n## Comparisons\n\n- [openstatus vs BetterStack](https://www.openstatus.dev/compare/betterstack)\n- [openstatus vs Checkly](https://www.openstatus.dev/compare/checkly)\n- [openstatus vs UptimeRobot](https://www.openstatus.dev/compare/uptime-robot)\n- [openstatus vs Uptime Kuma](https://www.openstatus.dev/compare/uptime-kuma)\n\n## Links\n\n- [About](https://www.openstatus.dev/about)\n- [Pricing](https://www.openstatus.dev/pricing)\n- [Changelog](https://www.openstatus.dev/changelog)\n- [Global Speed Checker](https://www.openstatus.dev/play/checker)\n- [Theme Store](https://themes.openstatus.dev)\n- [Dashboard](https://app.openstatus.dev)\n- [Documentation](https://docs.openstatus.dev/)\n- [Documentation llms docs](https://docs.openstatus.dev/llms.txt)\n- [Documentation llms docs full](https://docs.openstatus.dev/llms-full.txt)\n- [GitHub](https://github.com/openstatushq/openstatus)\n- [SDK](https://jsr.io/@openstatus/sdk-node)\n- [API](https://api.openstatus.dev/openapi)\n"
  },
  {
    "path": "apps/web/sentry.edge.config.ts",
    "content": "// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).\n// The config you add here will be used whenever one of the edge features is loaded.\n// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\nimport * as Sentry from \"@sentry/nextjs\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { env } from \"@/env\";\n\n// tRPC error codes that should not be reported to Sentry (expected client errors)\nconst IGNORED_TRPC_CODES: TRPCError[\"code\"][] = [\n  \"UNAUTHORIZED\",\n  \"NOT_FOUND\",\n  \"BAD_REQUEST\",\n];\n\nSentry.init({\n  dsn: env.NEXT_PUBLIC_SENTRY_DSN,\n\n  // Adjust this value in production, or use tracesSampler for greater control\n  tracesSampleRate: 0,\n\n  // Setting this option to true will print useful information to the console while you're setting up Sentry.\n  debug: false,\n  integrations: [Sentry.captureConsoleIntegration({ levels: [\"error\"] })],\n\n  beforeSend(event, hint) {\n    if (\n      hint.originalException instanceof TRPCError &&\n      IGNORED_TRPC_CODES.includes(hint.originalException.code)\n    ) {\n      return null;\n    }\n    return event;\n  },\n});\n"
  },
  {
    "path": "apps/web/sentry.server.config.ts",
    "content": "// This file configures the initialization of Sentry on the server.\n// The config you add here will be used whenever the server handles a request.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\n\nimport * as Sentry from \"@sentry/nextjs\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { env } from \"@/env\";\n\n// tRPC error codes that should not be reported to Sentry (expected client errors)\nconst IGNORED_TRPC_CODES: TRPCError[\"code\"][] = [\n  \"UNAUTHORIZED\",\n  \"NOT_FOUND\",\n  \"BAD_REQUEST\",\n];\n\nSentry.init({\n  dsn: env.NEXT_PUBLIC_SENTRY_DSN,\n\n  // Adjust this value in production, or use tracesSampler for greater control\n  tracesSampleRate: 0.2,\n\n  // Setting this option to true will print useful information to the console while you're setting up Sentry.\n  debug: false,\n  integrations: [Sentry.captureConsoleIntegration({ levels: [\"error\"] })],\n\n  beforeSend(event, hint) {\n    if (\n      hint.originalException instanceof TRPCError &&\n      IGNORED_TRPC_CODES.includes(hint.originalException.code)\n    ) {\n      return null;\n    }\n    return event;\n  },\n});\n"
  },
  {
    "path": "apps/web/src/app/(landing)/(redirect)/bsky/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function BlueskyRedirect() {\n  return redirect(\"https://bsky.app/profile/openstatus.dev\");\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/(redirect)/cal/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function CalRedirect() {\n  return redirect(\"https://cal.com/team/openstatus/30min\");\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/(redirect)/discord/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function DiscordRedirect() {\n  return redirect(\"https://discord.gg/dHD4JtSfsn\");\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/(redirect)/docs/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function DiscordRedirect() {\n  return redirect(\"https://docs.openstatus.dev\");\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/(redirect)/github/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function GithubRedirect() {\n  return redirect(\"https://github.com/openstatusHQ/openstatus\");\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/(redirect)/linkedin/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function LinkedinRedirect() {\n  return redirect(\"https://www.linkedin.com/company/openstatus\");\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/(redirect)/schema.json/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function SchemaJsonRedirect() {\n  return redirect(\n    \"https://github.com/openstatusHQ/json-schema/releases/latest/download/schema.json\",\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/(redirect)/twitter/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function TwitterRedirect() {\n  return redirect(\"https://twitter.com/openstatusHQ\");\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/(redirect)/youtube/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function YoutubeRedirect() {\n  return redirect(\"https://www.youtube.com/@OpenStatusHQ\");\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/[slug]/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getMainPages } from \"@/content/utils\";\nimport { getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDFAQPage,\n  getJsonLDHowTo,\n  getJsonLDOrganization,\n  getJsonLDProduct,\n  getJsonLDSoftwareApplication,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport { notFound } from \"next/navigation\";\n\nexport const dynamicParams = false;\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata | undefined> {\n  const { slug } = await params;\n  const page = getMainPages().find((page) => page.slug === slug);\n  if (!page) {\n    return;\n  }\n\n  const metadata = getPageMetadata(page);\n\n  return metadata;\n}\n\nexport async function generateStaticParams() {\n  const pages = getMainPages();\n\n  return pages.map((page) => ({\n    slug: page.slug,\n  }));\n}\n\nexport default async function Page({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const page = getMainPages().find((page) => page.slug === slug);\n\n  if (!page) {\n    notFound();\n  }\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDOrganization(),\n    getJsonLDSoftwareApplication(),\n    getJsonLDProduct(),\n    getJsonLDWebPage(page),\n    getJsonLDHowTo(page),\n    getJsonLDFAQPage(page),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{page.metadata.title}</h1>\n      <p className=\"text-lg\">{page.metadata.description}</p>\n      <CustomMDX source={page.content} />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/blog/[slug]/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getBlogPosts } from \"@/content/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBlogPosting,\n  getJsonLDBreadcrumbList,\n  getJsonLDFAQPage,\n  getJsonLDHowTo,\n  getJsonLDOrganization,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport Image from \"next/image\";\nimport { notFound } from \"next/navigation\";\nimport { ContentMetadata } from \"../../content-metadata\";\nimport { ContentPagination } from \"../../content-pagination\";\n\nexport const dynamicParams = false;\n\nexport async function generateStaticParams() {\n  const posts = getBlogPosts();\n\n  return posts.map((post) => ({\n    slug: post.slug,\n  }));\n}\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata | undefined> {\n  const { slug } = await params;\n  const post = getBlogPosts().find((post) => post.slug === slug);\n  if (!post) {\n    return;\n  }\n\n  const metadata = getPageMetadata(post, \"blog\");\n\n  return metadata;\n}\n\nexport default async function Blog({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const posts = getBlogPosts().sort(\n    (a, b) =>\n      b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(),\n  );\n  const postIndex = posts.findIndex((post) => post.slug === slug);\n  const post = posts[postIndex];\n  const previousPost = posts[postIndex - 1];\n  const nextPost = posts[postIndex + 1];\n\n  if (!post) {\n    notFound();\n  }\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDOrganization(),\n    getJsonLDWebPage(post),\n    getJsonLDBlogPosting(post, \"blog\"),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Blog\", url: `${BASE_URL}/blog` },\n      { name: post.metadata.title, url: `${BASE_URL}/blog/${slug}` },\n    ]),\n    getJsonLDHowTo(post),\n    getJsonLDFAQPage(post),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{post.metadata.title}</h1>\n      <ContentMetadata data={post} />\n      {post.metadata.image ? (\n        <div className=\"relative aspect-video w-full overflow-hidden border border-border\">\n          <Image\n            src={post.metadata.image}\n            alt={post.metadata.title}\n            fill\n            className=\"object-contain\"\n          />\n        </div>\n      ) : null}\n      <CustomMDX source={post.content} />\n      <ContentPagination\n        previousPost={previousPost}\n        nextPost={nextPost}\n        prefix=\"/blog\"\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/blog/category/[slug]/page.tsx",
    "content": "import { ContentCategory } from \"@/app/(landing)/content-category\";\nimport { ContentList } from \"@/app/(landing)/content-list\";\nimport { getBlogPosts } from \"@/content/utils\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n  const { slug } = await params;\n  const TITLE = `Blog - ${slug.charAt(0).toUpperCase()}${slug.slice(1)}`;\n  const DESCRIPTION = \"All the latest articles and news from openstatus.\";\n\n  return {\n    ...defaultMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    alternates: {\n      canonical: `/blog/category/${slug}`,\n    },\n    openGraph: {\n      ...ogMetadata,\n      title: TITLE,\n      description: DESCRIPTION,\n    },\n    twitter: {\n      ...twitterMetadata,\n      title: TITLE,\n      description: DESCRIPTION,\n      images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n    },\n  };\n}\n\nexport const dynamicParams = false;\n\nexport async function generateStaticParams() {\n  const posts = getBlogPosts();\n  const categories = [...new Set(posts.map((post) => post.metadata.category))];\n\n  return categories.map((category) => ({\n    slug: category.toLowerCase(),\n  }));\n}\n\nexport default async function BlogCategoryPage({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const allBlogs = getBlogPosts();\n  const filteredBlogs = allBlogs.filter(\n    (post) => post.metadata.category.toLowerCase() === slug.toLowerCase(),\n  );\n\n  return (\n    <div className=\"prose dark:prose-invert max-w-none\">\n      <h1 className=\"capitalize\">Blog | {slug}</h1>\n      <ContentCategory data={allBlogs} prefix=\"/blog\" />\n      <ContentList data={filteredBlogs} prefix=\"/blog\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/blog/category/page.tsx",
    "content": "import { components } from \"@/content/mdx\";\nimport { getBlogPosts } from \"@/content/utils\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport { ContentBoxLink, ContentBoxTitle } from \"../../content-box\";\n\nconst TITLE = \"Blog Categories\";\nconst DESCRIPTION = \"Browse all blog categories from openstatus.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/blog/category\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default function BlogCategoryIndex() {\n  const posts = getBlogPosts();\n  const categories = [...new Set(posts.map((post) => post.metadata.category))];\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <h1>Blog Categories</h1>\n      <components.Grid cols={2}>\n        {categories.map((category) => (\n          <ContentBoxLink\n            key={category}\n            href={`/blog/category/${category.toLowerCase()}`}\n          >\n            <ContentBoxTitle className=\"capitalize\">{category}</ContentBoxTitle>\n          </ContentBoxLink>\n        ))}\n      </components.Grid>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/blog/feed.xml/route.ts",
    "content": "import { getBlogPosts } from \"@/content/utils\";\nimport { Feed } from \"feed\";\nimport { getAuthor } from \"src/data/author\";\n\nexport async function GET() {\n  const feed = new Feed({\n    id: \"https://www.openstatus.dev/blog\",\n    title: \"OpenStatus - Blog\",\n    description: \"OpenStatus blog feed\",\n    generator: \"RSS for Node and Next.js\",\n    feedLinks: {\n      rss: \"https://www.openstatus.dev/blog/feed.xml\",\n    },\n    link: \"https://www.openstatus.dev\",\n    author: {\n      name: \"OpenStatus Team\",\n      email: \"ping@openstatus.dev\",\n      link: \"https://openstatus.dev\",\n    },\n    copyright: `Copyright ${new Date().getFullYear().toString()}, OpenStatus`,\n    language: \"en-US\",\n    updated: new Date(),\n    ttl: 60,\n  });\n\n  const allPosts = getBlogPosts();\n\n  allPosts\n    .sort(\n      (a, b) =>\n        new Date(b.metadata.publishedAt).getTime() -\n        new Date(a.metadata.publishedAt).getTime(),\n    )\n    .map((post) => {\n      const author = getAuthor(post.metadata.author);\n      return feed.addItem({\n        id: `https://www.openstatus.dev/blog/${post.slug}`,\n        title: post.metadata.title,\n        description: post.metadata.description,\n        link: `https://www.openstatus.dev/blog/${post.slug}`,\n        author: [\n          typeof author === \"string\"\n            ? { name: author }\n            : {\n                name: author.name,\n                link: author.url,\n              },\n        ],\n        image: post.metadata.image\n          ? `https://www.openstatus.dev${post.metadata.image}`\n          : undefined,\n        date: post.metadata.publishedAt,\n      });\n    });\n  return new Response(feed.rss2(), {\n    headers: {\n      \"Content-Type\": \"application/xml; charset=utf-8\",\n    },\n  });\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/blog/page.tsx",
    "content": "import { getBlogPosts } from \"@/content/utils\";\nimport { defaultMetadata, ogMetadata } from \"@/lib/metadata/shared-metadata\";\nimport { twitterMetadata } from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport Link from \"next/link\";\nimport { ContentCategory } from \"../content-category\";\nimport { ContentList } from \"../content-list\";\n\nconst TITLE = \"Blog - Engineering, Product & Monitoring Insights\";\nconst DESCRIPTION =\n  \"Read engineering deep dives, product updates, and monitoring best practices from the openstatus team. Learn about uptime monitoring, incident communication, and building reliable systems.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/blog\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default function BlogListPage() {\n  const allBlogs = getBlogPosts();\n  return (\n    <div className=\"prose dark:prose-invert max-w-none\">\n      <h1>Blog</h1>\n      <ContentCategory data={allBlogs} prefix=\"/blog\" />\n      <p>\n        Get the{\" \"}\n        <Link href=\"https://www.openstatus.dev/blog/feed.xml\" target=\"_blank\">\n          RSS feed\n        </Link>\n      </p>\n      <ContentList data={allBlogs} prefix=\"/blog\" withCategory />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/changelog/[slug]/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getChangelogPosts } from \"@/content/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBlogPosting,\n  getJsonLDBreadcrumbList,\n  getJsonLDFAQPage,\n  getJsonLDHowTo,\n  getJsonLDOrganization,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport Image from \"next/image\";\nimport { notFound } from \"next/navigation\";\nimport { ContentMetadata } from \"../../content-metadata\";\nimport { ContentPagination } from \"../../content-pagination\";\n\nexport const dynamicParams = false;\n\nexport async function generateStaticParams() {\n  const posts = getChangelogPosts();\n\n  return posts.map((post) => ({\n    slug: post.slug,\n  }));\n}\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata | undefined> {\n  const { slug } = await params;\n  const post = getChangelogPosts().find((post) => post.slug === slug);\n  if (!post) {\n    return;\n  }\n\n  const metadata = getPageMetadata(post, \"changelog\");\n\n  return metadata;\n}\n\nexport default async function Changelog({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const posts = getChangelogPosts().sort(\n    (a, b) =>\n      b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(),\n  );\n  const postIndex = posts.findIndex((post) => post.slug === slug);\n  const post = posts[postIndex];\n  const previousPost = posts[postIndex - 1];\n  const nextPost = posts[postIndex + 1];\n\n  if (!post) {\n    notFound();\n  }\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDOrganization(),\n    getJsonLDWebPage(post),\n    getJsonLDBlogPosting(post, \"changelog\"),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Changelog\", url: `${BASE_URL}/changelog` },\n      { name: post.metadata.title, url: `${BASE_URL}/changelog/${slug}` },\n    ]),\n    getJsonLDHowTo(post),\n    getJsonLDFAQPage(post),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{post.metadata.title}</h1>\n      <ContentMetadata data={post} />\n      {post.metadata.image ? (\n        <div className=\"relative aspect-video w-full overflow-hidden border border-border\">\n          <Image\n            src={post.metadata.image}\n            alt={post.metadata.title}\n            fill\n            className=\"object-contain\"\n          />\n        </div>\n      ) : null}\n      <CustomMDX source={post.content} />\n      <ContentPagination\n        previousPost={previousPost}\n        nextPost={nextPost}\n        prefix=\"/changelog\"\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/changelog/category/[slug]/page.tsx",
    "content": "import { ContentCategory } from \"@/app/(landing)/content-category\";\nimport { ContentList } from \"@/app/(landing)/content-list\";\nimport { getChangelogPosts } from \"@/content/utils\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n  const { slug } = await params;\n  const TITLE = `Changelog - ${slug.charAt(0).toUpperCase()}${slug.slice(1)}`;\n  const DESCRIPTION = \"All the latest changes and updates to openstatus.\";\n\n  return {\n    ...defaultMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    alternates: {\n      canonical: `/changelog/category/${slug}`,\n    },\n    openGraph: {\n      ...ogMetadata,\n      title: TITLE,\n      description: DESCRIPTION,\n    },\n    twitter: {\n      ...twitterMetadata,\n      title: TITLE,\n      description: DESCRIPTION,\n      images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n    },\n  };\n}\n\nexport const dynamicParams = false;\n\nexport async function generateStaticParams() {\n  const posts = getChangelogPosts();\n  const categories = [...new Set(posts.map((post) => post.metadata.category))];\n\n  return categories.map((category) => ({\n    slug: category.toLowerCase(),\n  }));\n}\n\nexport default async function ChangelogCategoryPage({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const allChangelogs = getChangelogPosts();\n  const filteredChangelogs = allChangelogs.filter(\n    (changelog) =>\n      changelog.metadata.category.toLowerCase() === slug.toLowerCase(),\n  );\n\n  return (\n    <div className=\"prose dark:prose-invert max-w-none\">\n      <h1 className=\"capitalize\">Changelog | {slug}</h1>\n      <ContentCategory data={allChangelogs} prefix=\"/changelog\" />\n      <ContentList data={filteredChangelogs} prefix=\"/changelog\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/changelog/category/page.tsx",
    "content": "import { components } from \"@/content/mdx\";\nimport { getChangelogPosts } from \"@/content/utils\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport { ContentBoxLink, ContentBoxTitle } from \"../../content-box\";\n\nconst TITLE = \"Changelog Categories\";\nconst DESCRIPTION = \"Browse all changelog categories from openstatus.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/changelog/category\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default function ChangelogCategoryIndex() {\n  const posts = getChangelogPosts();\n  const categories = [...new Set(posts.map((post) => post.metadata.category))];\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <h1>Changelog Categories</h1>\n      <components.Grid cols={2}>\n        {categories.map((category) => (\n          <ContentBoxLink\n            key={category}\n            href={`/changelog/category/${category.toLowerCase()}`}\n          >\n            <ContentBoxTitle className=\"capitalize\">{category}</ContentBoxTitle>\n          </ContentBoxLink>\n        ))}\n      </components.Grid>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/changelog/feed.xml/route.ts",
    "content": "import { getChangelogPosts } from \"@/content/utils\";\nimport { Feed } from \"feed\";\nimport { getAuthor } from \"src/data/author\";\n\nexport async function GET() {\n  const feed = new Feed({\n    id: \"https://www.openstatus.dev/changelog\",\n    title: \"OpenStatus - Changelog\",\n    description: \"OpenStatus changelog feed\",\n    generator: \"RSS for Node and Next.js\",\n    feedLinks: {\n      rss: \"https://www.openstatus.dev/changelog/feed.xml\",\n    },\n    link: \"https://www.openstatus.dev\",\n    author: {\n      name: \"OpenStatus Team\",\n      email: \"ping@openstatus.dev\",\n      link: \"https://openstatus.dev\",\n    },\n    copyright: `Copyright ${new Date().getFullYear().toString()}, OpenStatus`,\n    language: \"en-US\",\n    updated: new Date(),\n    ttl: 60,\n  });\n\n  const allChangelogs = getChangelogPosts();\n\n  allChangelogs\n    .sort(\n      (a, b) =>\n        new Date(b.metadata.publishedAt).getTime() -\n        new Date(a.metadata.publishedAt).getTime(),\n    )\n    .map((post) => {\n      const author = getAuthor(post.metadata.author);\n      return feed.addItem({\n        id: `https://www.openstatus.dev/changelog/${post.slug}`,\n        title: post.metadata.title,\n        description: post.metadata.description,\n        link: `https://www.openstatus.dev/changelog/${post.slug}`,\n        author: [\n          typeof author === \"string\"\n            ? { name: author }\n            : {\n                name: author.name,\n                link: author.url,\n              },\n        ],\n        image: post.metadata.image\n          ? `https://www.openstatus.dev${post.metadata.image}`\n          : undefined,\n        date: post.metadata.publishedAt,\n      });\n    });\n  return new Response(feed.rss2(), {\n    headers: {\n      \"Content-Type\": \"application/xml; charset=utf-8\",\n    },\n  });\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/changelog/page.tsx",
    "content": "import { getChangelogPosts } from \"@/content/utils\";\nimport { defaultMetadata, ogMetadata } from \"@/lib/metadata/shared-metadata\";\nimport { twitterMetadata } from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport Link from \"next/link\";\nimport { ContentCategory } from \"../content-category\";\nimport { ContentList } from \"../content-list\";\n\nconst TITLE = \"Changelog\";\nconst DESCRIPTION = \"All the latest changes and updates to openstatus.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/changelog\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default function ChangelogListPage() {\n  const allChangelogs = getChangelogPosts();\n  return (\n    <div className=\"prose dark:prose-invert max-w-none\">\n      <h1>Changelog</h1>\n      <ContentCategory data={allChangelogs} prefix=\"/changelog\" />\n      <p>\n        Get the{\" \"}\n        <Link\n          href=\"https://www.openstatus.dev/changelog/feed.xml\"\n          target=\"_blank\"\n        >\n          RSS feed\n        </Link>\n      </p>\n      <ContentList data={allChangelogs} prefix=\"/changelog\" withCategory />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/compare/[slug]/page.tsx",
    "content": "import { ContentPagination } from \"@/app/(landing)/content-pagination\";\nimport { CustomMDX } from \"@/content/mdx\";\nimport { getComparePages } from \"@/content/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBlogPosting,\n  getJsonLDBreadcrumbList,\n  getJsonLDFAQPage,\n  getJsonLDHowTo,\n  getJsonLDOrganization,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport { notFound } from \"next/navigation\";\n\nexport const dynamicParams = false;\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata | undefined> {\n  const { slug } = await params;\n  const post = getComparePages().find((post) => post.slug === slug);\n\n  if (!post) {\n    return;\n  }\n\n  const metadata = getPageMetadata(post, \"compare\");\n\n  return metadata;\n}\n\nexport async function generateStaticParams() {\n  const posts = getComparePages();\n\n  return posts.map((post) => ({\n    slug: post.slug,\n  }));\n}\n\nexport default async function Blog({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const posts = getComparePages().sort(\n    (a, b) =>\n      b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(),\n  );\n  const postIndex = posts.findIndex((post) => post.slug === slug);\n  const post = posts[postIndex];\n  const previousPost = posts[postIndex - 1];\n  const nextPost = posts[postIndex + 1];\n\n  if (!post) {\n    notFound();\n  }\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDOrganization(),\n    getJsonLDWebPage(post),\n    getJsonLDBlogPosting(post, \"compare\"),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Compare\", url: `${BASE_URL}/compare` },\n      { name: post.metadata.title, url: `${BASE_URL}/compare/${slug}` },\n    ]),\n    getJsonLDHowTo(post),\n    getJsonLDFAQPage(post),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{post.metadata.title}</h1>\n      <p className=\"text-lg\">{post.metadata.description}</p>\n      <CustomMDX source={post.content} />\n      <ContentPagination\n        previousPost={previousPost}\n        nextPost={nextPost}\n        prefix=\"/compare\"\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/compare/page.tsx",
    "content": "import { components } from \"@/content/mdx\";\nimport { getComparePages } from \"@/content/utils\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport {\n  ContentBoxDescription,\n  ContentBoxLink,\n  ContentBoxTitle,\n  ContentBoxUrl,\n} from \"../content-box\";\n\nconst TITLE = \"Compare Uptime Monitoring & Status Page Alternatives\";\nconst DESCRIPTION =\n  \"See how openstatus compares to BetterStack, UptimeRobot, Checkly, Instatus, and other monitoring tools. Side-by-side feature and pricing comparisons to help you choose the right solution.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/compare\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default function Page() {\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <h1>Compare openstatus with uptime and status page solutions</h1>\n      <components.Grid cols={2}>\n        {getComparePages().map((page) => (\n          <ContentBoxLink key={page.slug} href={`/compare/${page.slug}`}>\n            <ContentBoxTitle>{page.metadata.title}</ContentBoxTitle>\n            <ContentBoxDescription>\n              {page.metadata.description}\n            </ContentBoxDescription>\n            <ContentBoxUrl url=\"Read more\" />\n          </ContentBoxLink>\n        ))}\n      </components.Grid>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/content-box.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport Link from \"next/link\";\nimport type React from \"react\";\n\nexport function ContentBoxLink({\n  href,\n  children,\n  className,\n  target,\n  rel,\n  ...props\n}: React.ComponentProps<typeof Link>) {\n  const hrefString = typeof href === \"string\" ? href : href.toString();\n  const isExternal = hrefString.startsWith(\"http\");\n\n  return (\n    <Link\n      href={href}\n      target={target ?? (isExternal ? \"_blank\" : undefined)}\n      rel={rel ?? (isExternal ? \"noopener noreferrer\" : undefined)}\n      className={cn(\"group no-underline! hover:bg-muted\", className)}\n      {...props}\n    >\n      {children}\n    </Link>\n  );\n}\n\nexport function ContentBoxTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(className)} {...props}>\n      <strong>{children}</strong>\n    </p>\n  );\n}\n\nexport function ContentBoxDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p className={cn(\"text-muted-foreground\", className)} {...props}>\n      {children}\n    </p>\n  );\n}\n\nexport function ContentBoxUrl({\n  url,\n  className,\n  ...props\n}: {\n  url: string;\n  className?: string;\n} & Omit<React.ComponentProps<\"div\">, \"children\">) {\n  const displayUrl = url.replace(/^https:\\/\\/(www\\.)?/, \"\");\n\n  return (\n    <div\n      className={cn(\n        \"underline decoration-2 decoration-muted-foreground/50 underline-offset-2 transition-all group-hover:decoration-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {displayUrl}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/content-category.tsx",
    "content": "import type { MDXData } from \"@/content/utils\";\nimport Link from \"next/link\";\nimport { Fragment } from \"react\";\n\nexport function ContentCategory({\n  data,\n  prefix,\n}: {\n  data: MDXData[];\n  prefix: string;\n}) {\n  const categories = [...new Set(data.map((page) => page.metadata.category))];\n  return (\n    <p>\n      <Link href={prefix}>All</Link>\n      <span className=\"text-muted-foreground\">{\" | \"}</span>\n      {categories.map((category, index) => (\n        <Fragment key={category}>\n          <Link\n            href={`${prefix}/category/${category.toLowerCase()}`}\n            className=\"capitalize\"\n          >\n            {category}\n          </Link>\n          {index < categories.length - 1 ? <span>{\" | \"}</span> : null}\n        </Fragment>\n      ))}\n    </p>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/content-list.tsx",
    "content": "import { type MDXData, formatDate } from \"@/content/utils\";\nimport { cn } from \"@/lib/utils\";\nimport Link from \"next/link\";\n\nexport function ContentList({\n  data,\n  prefix,\n  withCategory = false,\n}: {\n  data: MDXData[];\n  prefix: string;\n  withCategory?: boolean;\n}) {\n  return (\n    <ContentListContainer>\n      {data\n        .sort((a, b) => {\n          if (\n            new Date(a.metadata.publishedAt) > new Date(b.metadata.publishedAt)\n          ) {\n            return -1;\n          }\n          return 1;\n        })\n        .map((post) => (\n          <ContentListLink key={post.slug} href={`${prefix}/${post.slug}`}>\n            <ContentListItem>\n              <ContentListItemDate>\n                {formatDate(post.metadata.publishedAt, false)}\n              </ContentListItemDate>\n              <ContentListItemTitle>{post.metadata.title}</ContentListItemTitle>\n              {withCategory ? (\n                <span className=\"text-muted-foreground md:ml-auto\">\n                  [{post.metadata.category}]\n                </span>\n              ) : null}\n            </ContentListItem>\n          </ContentListLink>\n        ))}\n    </ContentListContainer>\n  );\n}\n\nexport function ContentListContainer({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"section\">) {\n  return (\n    <section\n      className={cn(\"prose dark:prose-invert max-w-none\", className)}\n      {...props}\n    >\n      <div>{children}</div>\n    </section>\n  );\n}\n\nexport function ContentListItem({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex w-full flex-col space-x-0 md:flex-row md:space-x-2\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function ContentListLink({\n  href,\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof Link>) {\n  return (\n    <Link\n      href={href}\n      className={cn(\"no-underline! flex flex-col hover:bg-muted\", className)}\n      {...props}\n    >\n      {children}\n    </Link>\n  );\n}\n\nexport function ContentListItemTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      className={cn(\"truncate text-foreground tracking-tight\", className)}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport function ContentListItemDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span className={cn(\"text-muted-foreground\", className)} {...props}>\n      {children}\n    </span>\n  );\n}\n\nexport function ContentListItemDate({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      className={cn(\n        \"text-nowrap text-muted-foreground tabular-nums\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/content-metadata.tsx",
    "content": "import { type MDXData, formatDate } from \"@/content/utils\";\nimport { getAuthor } from \"@/data/author\";\n\nexport function ContentMetadata({ data }: { data: MDXData }) {\n  return (\n    <p className=\"flex flex-wrap items-center gap-2.5 divide-x divide-border text-muted-foreground\">\n      {formatDate(data.metadata.publishedAt)} | by{\" \"}\n      <Author author={data.metadata.author} /> | [{data.metadata.category}]\n    </p>\n  );\n}\n\nfunction Author({ author }: { author: string }) {\n  const authorData = getAuthor(author);\n  if (typeof authorData === \"string\") {\n    return author;\n  }\n\n  return (\n    <a href={authorData.url} target=\"_blank\" rel=\"noopener noreferrer\">\n      {authorData.name}\n    </a>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/content-pagination.tsx",
    "content": "import { Link } from \"@/content/link\";\nimport type { MDXData } from \"@/content/utils\";\n\nexport function ContentPagination({\n  previousPost,\n  nextPost,\n  prefix,\n}: {\n  previousPost?: MDXData;\n  nextPost?: MDXData;\n  prefix: string;\n}) {\n  return (\n    <div className=\"flex items-center justify-between gap-8\">\n      <p className=\"flex-1 truncate\">\n        {previousPost ? (\n          <Link\n            href={`${prefix}/${previousPost.slug}`}\n            className=\"max-w-40 truncate\"\n          >\n            {previousPost.metadata.title}\n          </Link>\n        ) : null}\n      </p>\n      <p className=\"flex-1 truncate text-right\">\n        {nextPost ? (\n          <Link\n            href={`${prefix}/${nextPost.slug}`}\n            className=\"max-w-40 truncate\"\n          >\n            {nextPost.metadata.title}\n          </Link>\n        ) : null}\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/guides/[slug]/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getGuides } from \"@/content/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBlogPosting,\n  getJsonLDBreadcrumbList,\n  getJsonLDFAQPage,\n  getJsonLDHowTo,\n  getJsonLDOrganization,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport Image from \"next/image\";\nimport { notFound } from \"next/navigation\";\nimport { ContentMetadata } from \"../../content-metadata\";\nimport { ContentPagination } from \"../../content-pagination\";\n\nexport const dynamicParams = false;\n\nexport async function generateStaticParams() {\n  const posts = getGuides();\n\n  return posts.map((post) => ({\n    slug: post.slug,\n  }));\n}\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata | undefined> {\n  const { slug } = await params;\n  const post = getGuides().find((post) => post.slug === slug);\n  if (!post) {\n    return;\n  }\n\n  const metadata = getPageMetadata(post, \"guides\");\n\n  return metadata;\n}\n\nexport default async function Guide({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const posts = getGuides().sort(\n    (a, b) =>\n      b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(),\n  );\n  const postIndex = posts.findIndex((post) => post.slug === slug);\n  const post = posts[postIndex];\n  const previousPost = posts[postIndex - 1];\n  const nextPost = posts[postIndex + 1];\n\n  if (!post) {\n    notFound();\n  }\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDOrganization(),\n    getJsonLDWebPage(post),\n    getJsonLDBlogPosting(post, \"guides\"),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Guides\", url: `${BASE_URL}/guides` },\n      { name: post.metadata.title, url: `${BASE_URL}/guides/${slug}` },\n    ]),\n    getJsonLDHowTo(post),\n    getJsonLDFAQPage(post),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{post.metadata.title}</h1>\n      <ContentMetadata data={post} />\n      {post.metadata.image ? (\n        <div className=\"relative aspect-video w-full overflow-hidden border border-border\">\n          <Image\n            src={post.metadata.image}\n            alt={post.metadata.title}\n            fill\n            className=\"object-contain\"\n          />\n        </div>\n      ) : null}\n      <CustomMDX source={post.content} />\n      <ContentPagination\n        previousPost={previousPost}\n        nextPost={nextPost}\n        prefix=\"/guides\"\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/guides/category/[slug]/page.tsx",
    "content": "import { ContentCategory } from \"@/app/(landing)/content-category\";\nimport { ContentList } from \"@/app/(landing)/content-list\";\nimport { getGuides } from \"@/content/utils\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n  const { slug } = await params;\n  const TITLE = `Guides - ${slug.charAt(0).toUpperCase()}${slug.slice(1)}`;\n  const DESCRIPTION = \"All the latest guides from openstatus.\";\n\n  return {\n    ...defaultMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    alternates: {\n      canonical: `/guides/category/${slug}`,\n    },\n    openGraph: {\n      ...ogMetadata,\n      title: TITLE,\n      description: DESCRIPTION,\n    },\n    twitter: {\n      ...twitterMetadata,\n      title: TITLE,\n      description: DESCRIPTION,\n      images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n    },\n  };\n}\n\nexport const dynamicParams = false;\n\nexport async function generateStaticParams() {\n  const posts = getGuides();\n  const categories = [...new Set(posts.map((post) => post.metadata.category))];\n\n  return categories.map((category) => ({\n    slug: category.toLowerCase(),\n  }));\n}\n\nexport default async function GuideCategoryPage({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const allGuides = getGuides();\n  const filteredGuides = allGuides.filter(\n    (post) => post.metadata.category.toLowerCase() === slug.toLowerCase(),\n  );\n\n  return (\n    <div className=\"prose dark:prose-invert max-w-none\">\n      <h1 className=\"capitalize\">Guides | {slug}</h1>\n      <ContentCategory data={allGuides} prefix=\"/guides\" />\n      <ContentList data={filteredGuides} prefix=\"/guides\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/guides/category/page.tsx",
    "content": "import { components } from \"@/content/mdx\";\nimport { getGuides } from \"@/content/utils\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport { ContentBoxLink, ContentBoxTitle } from \"../../content-box\";\n\nconst TITLE = \"Guides Categories\";\nconst DESCRIPTION = \"Browse all guides categories from openstatus.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/guides/category\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default function GuidesCategoryIndex() {\n  const posts = getGuides();\n  const categories = [...new Set(posts.map((post) => post.metadata.category))];\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <h1>Guides Categories</h1>\n      <components.Grid cols={2}>\n        {categories.map((category) => (\n          <ContentBoxLink\n            key={category}\n            href={`/guides/category/${category.toLowerCase()}`}\n          >\n            <ContentBoxTitle className=\"capitalize\">{category}</ContentBoxTitle>\n          </ContentBoxLink>\n        ))}\n      </components.Grid>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/guides/page.tsx",
    "content": "import { getGuides } from \"@/content/utils\";\nimport { defaultMetadata, ogMetadata } from \"@/lib/metadata/shared-metadata\";\nimport { twitterMetadata } from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport { ContentCategory } from \"../content-category\";\nimport { ContentList } from \"../content-list\";\n\nconst TITLE = \"Guides\";\nconst DESCRIPTION = \"All the latest guides from openstatus.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/guides\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default function GuidesListPage() {\n  const allGuides = getGuides();\n  return (\n    <div className=\"prose dark:prose-invert max-w-none\">\n      <h1>Guides</h1>\n      <ContentCategory data={allGuides} prefix=\"/guides\" />\n      <ContentList data={allGuides} prefix=\"/guides\" withCategory />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/layout.tsx",
    "content": "import { Footer } from \"@/content/footer\";\nimport { Header } from \"@/content/header\";\nimport { SubNav } from \"@/content/sub-nav\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <div className=\"mx-auto flex min-h-screen max-w-5xl flex-col gap-4 font-mono\">\n      <Header />\n      <SubNav />\n      <main className=\"flex-1 px-4 py-4\">{children}</main>\n      <Footer />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/oss-friends/page.tsx",
    "content": "import { components } from \"@/content/mdx\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport { z } from \"zod\";\nimport {\n  ContentBoxDescription,\n  ContentBoxLink,\n  ContentBoxTitle,\n  ContentBoxUrl,\n} from \"../content-box\";\n\nconst TITLE = \"OSS Friends\";\nconst DESCRIPTION = \"List of all our awesome open source friends.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/oss-friends\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nconst OSSFriendSchema = z.object({\n  href: z.string(),\n  name: z.string(),\n  description: z.string(),\n});\n\nexport default async function Page() {\n  const res = await fetch(\"https://formbricks.com/api/oss-friends\");\n  const data = await res.json();\n  const openSourceFriends = z.array(OSSFriendSchema).parse(data.data);\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <h1>OSS Friends</h1>\n      <components.Grid cols={2}>\n        {openSourceFriends.map((friend) => (\n          <ContentBoxLink key={friend.href} href={friend.href}>\n            <ContentBoxTitle>{friend.name}</ContentBoxTitle>\n            <ContentBoxDescription>{friend.description}</ContentBoxDescription>\n            <ContentBoxUrl url={friend.href} />\n          </ContentBoxLink>\n        ))}\n      </components.Grid>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getHomePage } from \"@/content/utils\";\nimport { defaultMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDFAQPage,\n  getJsonLDHowTo,\n  getJsonLDOrganization,\n  getJsonLDProduct,\n  getJsonLDSoftwareApplication,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\n\nexport const metadata: Metadata = defaultMetadata;\n\nexport default function Page() {\n  const homePage = getHomePage();\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDOrganization(),\n    getJsonLDProduct(),\n    getJsonLDSoftwareApplication(),\n    getJsonLDWebPage(homePage),\n    getJsonLDHowTo(homePage),\n    getJsonLDFAQPage(homePage),\n  ]);\n\n  return (\n    <div className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{homePage.metadata.title}</h1>\n      <p className=\"text-lg\">{homePage.metadata.description}</p>\n      <CustomMDX source={homePage.content} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/checker/[slug]/client.tsx",
    "content": "\"use client\";\n\nimport { IconCloudProvider } from \"@/components/icon-cloud-provider\";\nimport {\n  type CachedRegionChecker,\n  getTimingPhases,\n  regionFormatter,\n  timestampFormatter,\n} from \"@/lib/checker/utils\";\nimport { cn } from \"@/lib/utils\";\nimport { type Region, regionDict } from \"@openstatus/regions\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@openstatus/ui/components/ui/dialog\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { useState } from \"react\";\nimport { handleExportCSV } from \"../utils\";\n\nconst STATUS_CODES = {\n  \"1\": \"text-muted-foreground\",\n  \"2\": \"text-success\",\n  \"3\": \"text-info\",\n  \"4\": \"text-warning\",\n  \"5\": \"text-destructive\",\n};\n\ninterface TableProps {\n  data: CachedRegionChecker;\n}\n\nexport function Table({ data }: TableProps) {\n  const [input, setInput] = useState(\"\");\n  const [sort, setSort] = useState<{\n    value: \"latency\" | \"status\" | \"region\";\n    desc: boolean;\n  }>({ value: \"latency\", desc: false });\n\n  // Filter successful checks and calculate timing phases\n  const checks = data.checks\n    .filter((check) => check.state === \"success\" && check.timing)\n    .map((check) => {\n      const timing = getTimingPhases(check.timing);\n      return {\n        ...check,\n        timingPhases: timing,\n      };\n    });\n\n  const filteredAndSorted = checks\n    .filter((check) => {\n      const regionInfo = regionDict[check.region as Region];\n      if (!regionInfo) return false;\n      return [\n        regionInfo.code,\n        regionInfo.location,\n        regionInfo.flag,\n        regionInfo.continent,\n        regionInfo.provider,\n      ].some((value) => value?.toLowerCase().includes(input.toLowerCase()));\n    })\n    .sort((a, b) => {\n      if (sort.value === \"status\") {\n        return sort.desc ? b.status - a.status : a.status - b.status;\n      }\n      if (sort.value === \"latency\") {\n        return sort.desc ? b.latency - a.latency : a.latency - b.latency;\n      }\n      return sort.desc\n        ? b.region.localeCompare(a.region)\n        : a.region.localeCompare(b.region);\n    });\n\n  return (\n    <div>\n      <div className=\"flex flex-col gap-2 sm:flex-row\">\n        <Input\n          value={input}\n          onChange={(e) => setInput(e.target.value)}\n          placeholder=\"Search by region, flag, location code, cloud provider or continent\"\n          className=\"h-auto! flex-1 rounded-none p-4 text-base md:text-base\"\n        />\n        <Button\n          variant=\"outline\"\n          className=\"h-auto! rounded-none p-4 text-base\"\n          onClick={() => handleExportCSV(checks, data.url)}\n        >\n          Export to CSV\n        </Button>\n      </div>\n      <div className=\"table-wrapper\">\n        <table>\n          <thead>\n            <tr>\n              <th className=\"w-12\" />\n              <th>Region</th>\n              <th>Status</th>\n              <th>DNS</th>\n              <th>Connect</th>\n              <th>TLS</th>\n              <th>TTFB</th>\n              <th className=\"p-0! text-right!\">\n                <TableSort\n                  onClick={() =>\n                    setSort({ value: \"latency\", desc: !sort.desc })\n                  }\n                  direction={\n                    sort.value === \"latency\"\n                      ? sort.desc\n                        ? \"desc\"\n                        : \"asc\"\n                      : undefined\n                  }\n                >\n                  Latency\n                </TableSort>\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            {filteredAndSorted.length === 0 ? (\n              <tr>\n                <td\n                  colSpan={8}\n                  className=\"border border-border border-dashed text-center\"\n                >\n                  No data available\n                </td>\n              </tr>\n            ) : (\n              filteredAndSorted.map((check) => {\n                const regionInfo = regionDict[check.region as Region];\n                if (!regionInfo) return null;\n\n                const { dns, connection, tls, ttfb } = check.timingPhases;\n\n                return (\n                  <InfoDialog key={check.region} check={check}>\n                    <tr className=\"hover:bg-muted/50\">\n                      <td>\n                        <IconCloudProvider\n                          provider={regionInfo.provider}\n                          className=\"size-4\"\n                        />\n                      </td>\n                      <td>\n                        {regionInfo.flag} {regionInfo.code}{\" \"}\n                        <span className=\"text-muted-foreground\">\n                          {regionInfo.location}\n                        </span>\n                      </td>\n                      <td\n                        className={cn(\n                          STATUS_CODES[\n                            check.status.toString()[0] as keyof typeof STATUS_CODES\n                          ],\n                        )}\n                      >\n                        {check.status}\n                      </td>\n                      <td>\n                        {Intl.NumberFormat(\"en-US\", {\n                          maximumFractionDigits: 0,\n                        }).format(dns)}\n                        ms\n                      </td>\n                      <td>\n                        {Intl.NumberFormat(\"en-US\", {\n                          maximumFractionDigits: 0,\n                        }).format(connection)}\n                        ms\n                      </td>\n                      <td>\n                        {Intl.NumberFormat(\"en-US\", {\n                          maximumFractionDigits: 0,\n                        }).format(tls)}\n                        ms\n                      </td>\n                      <td>\n                        {Intl.NumberFormat(\"en-US\", {\n                          maximumFractionDigits: 0,\n                        }).format(ttfb)}\n                        ms\n                      </td>\n                      <td className=\"text-right!\">\n                        {Intl.NumberFormat(\"en-US\", {\n                          maximumFractionDigits: 0,\n                        }).format(check.latency)}\n                        ms\n                      </td>\n                    </tr>\n                  </InfoDialog>\n                );\n              })\n            )}\n          </tbody>\n          <caption>\n            Results of your check ({filteredAndSorted.length} / {checks.length}{\" \"}\n            regions)\n          </caption>\n        </table>\n      </div>\n    </div>\n  );\n}\n\nfunction TableSort({\n  children,\n  className,\n  direction,\n  ...props\n}: React.ComponentProps<typeof Button> & { direction?: \"asc\" | \"desc\" }) {\n  return (\n    <Button\n      variant=\"ghost\"\n      className={cn(\n        \"h-auto! w-full rounded-none p-4 text-base md:text-base\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <span className=\"ml-2 flex flex-col\">\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"shrink-0 text-[9px]\",\n            direction === \"asc\"\n              ? \"text-accent-foreground\"\n              : \"text-muted-foreground\",\n          )}\n        >\n          ▲\n        </span>\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"shrink-0 text-[9px]\",\n            direction === \"desc\"\n              ? \"text-accent-foreground\"\n              : \"text-muted-foreground\",\n          )}\n        >\n          ▼\n        </span>\n      </span>\n    </Button>\n  );\n}\n\nfunction InfoDialog({\n  check,\n  children,\n}: {\n  check: CachedRegionChecker[\"checks\"][number];\n  children: React.ReactNode;\n}) {\n  const headers = check.headers || {};\n  const regionInfo = regionDict[check.region];\n  return (\n    <Dialog>\n      <DialogTrigger asChild>{children}</DialogTrigger>\n      <DialogContent className=\"max-h-[90vh] overflow-y-auto overflow-x-hidden rounded-none! font-mono sm:max-w-5xl\">\n        <DialogHeader>\n          <DialogTitle>Response Details</DialogTitle>\n          <DialogDescription>\n            Basic informations like header and latency about the response.\n          </DialogDescription>\n        </DialogHeader>\n        <div className=\"prose dark:prose-invert min-w-0 max-w-none\">\n          <table>\n            <tbody>\n              <tr>\n                <td>Region</td>\n                <td>\n                  {regionFormatter(check.region, \"long\")}, {regionInfo.provider}{\" \"}\n                  <IconCloudProvider\n                    provider={regionInfo.provider}\n                    className=\"inline size-4\"\n                  />\n                </td>\n              </tr>\n              <tr>\n                <td>Timestamp</td>\n                <td>{timestampFormatter(check.timestamp)}</td>\n              </tr>\n              <tr>\n                <td>Status</td>\n                <td\n                  className={cn(\n                    STATUS_CODES[\n                      check.status.toString()[0] as keyof typeof STATUS_CODES\n                    ],\n                  )}\n                >\n                  {check.status}\n                </td>\n              </tr>\n              <tr>\n                <td>Latency</td>\n                <td>\n                  {Intl.NumberFormat(\"en-US\", {\n                    maximumFractionDigits: 0,\n                  }).format(check.latency)}\n                  ms\n                </td>\n              </tr>\n            </tbody>\n          </table>\n          <Tabs defaultValue=\"raw\">\n            <TabsList className=\"h-auto w-full rounded-none\">\n              <TabsTrigger\n                value=\"raw\"\n                className=\"h-auto w-full truncate rounded-none p-4\"\n              >\n                Raw\n              </TabsTrigger>\n              <TabsTrigger\n                value=\"table\"\n                className=\"h-auto w-full truncate rounded-none p-4\"\n              >\n                Table\n              </TabsTrigger>\n            </TabsList>\n            <TabsContent value=\"table\" className=\"min-w-0 overflow-x-auto\">\n              <div className=\"min-w-0\">\n                <table className=\"my-0!\">\n                  <thead>\n                    <tr>\n                      <th className=\"whitespace-nowrap\">Header</th>\n                      <th className=\"break-words\">Value</th>\n                    </tr>\n                  </thead>\n                  <tbody>\n                    {Object.entries(headers).map(([key, value]) => (\n                      <tr key={key}>\n                        <td className=\"whitespace-nowrap font-medium\">{key}</td>\n                        <td className=\"max-w-md break-words\">{value}</td>\n                      </tr>\n                    ))}\n                  </tbody>\n                </table>\n              </div>\n            </TabsContent>\n            <TabsContent value=\"raw\" className=\"min-w-0 overflow-x-auto\">\n              {/* NOTE: we can make it a readOnly textarea*/}\n              <pre className=\"my-0! whitespace-pre-wrap break-words\">\n                {Object.entries(headers).map(([key, value]) => (\n                  <code key={key} className=\"block break-words\">\n                    {key}: {value}\n                  </code>\n                ))}\n              </pre>\n            </TabsContent>\n          </Tabs>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/checker/[slug]/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getToolsPage } from \"@/content/utils\";\nimport { mockCheckAllRegions } from \"@/lib/checker/mock\";\nimport {\n  getCheckerDataById,\n  latencyFormatter,\n  regionFormatter,\n} from \"@/lib/checker/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBreadcrumbList,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport { redirect } from \"next/navigation\";\nimport { Table } from \"./client\";\n\nfunction formatDate(date: Date) {\n  return date.toLocaleDateString(\"en-US\", {\n    year: \"numeric\",\n    month: \"long\",\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"numeric\",\n    second: \"numeric\",\n  });\n}\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata | undefined> {\n  const { slug } = await params;\n  const page = getToolsPage(\"checker-slug\");\n\n  const metadata = {\n    ...getPageMetadata(page, \"play\"),\n    alternates: { canonical: `${BASE_URL}/play/checker/${slug}` },\n  };\n\n  const data =\n    process.env.NODE_ENV === \"development\"\n      ? await mockCheckAllRegions()\n      : await getCheckerDataById(slug);\n\n  if (!data) return metadata;\n\n  const regions = data.checks.sort((a, b) => a.latency - b.latency);\n  const fastestRegion = regions[0];\n  const slowestRegion = regions[regions.length - 1];\n\n  const TITLE = data.url;\n  const DESCRIPTION = `${formatDate(\n    new Date(data.timestamp),\n  )} | Fastest: ${regionFormatter(fastestRegion.region)} (${latencyFormatter(\n    fastestRegion.latency,\n  )}) | Slowest: ${regionFormatter(slowestRegion.region)} (${latencyFormatter(\n    slowestRegion.latency,\n  )})`;\n\n  return {\n    ...metadata,\n    twitter: {\n      ...metadata.twitter,\n      images: [\n        `/api/og?title=${encodeURIComponent(\n          TITLE,\n        )}&description=${encodeURIComponent(DESCRIPTION)}&category=checker`,\n      ],\n    },\n    openGraph: {\n      ...metadata.openGraph,\n      images: [\n        `/api/og?title=${encodeURIComponent(\n          TITLE,\n        )}&description=${encodeURIComponent(DESCRIPTION)}&category=checker`,\n      ],\n    },\n  };\n}\n\nexport default async function Page({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const page = getToolsPage(\"checker-slug\");\n\n  const data =\n    process.env.NODE_ENV === \"development\"\n      ? await mockCheckAllRegions()\n      : await getCheckerDataById(slug);\n\n  if (!data) redirect(\"/play/checker\");\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDWebPage(page),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Playground\", url: `${BASE_URL}/play` },\n      { name: \"Speed Checker\", url: `${BASE_URL}/play/checker` },\n      { name: data.url, url: `${BASE_URL}/play/checker/${slug}` },\n    ]),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{data.url}</h1>\n      <p className=\"text-lg\">{formatDate(new Date(data.timestamp))}</p>\n      <Table data={data} />\n      <CustomMDX source={page.content} />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/checker/api/route.ts",
    "content": "import { mockCheckRegion } from \"@/lib/checker/mock\";\nimport {\n  type Method,\n  checkRegion,\n  getTimingPhases,\n  storeBaseCheckerData,\n  storeCheckerData,\n} from \"@/lib/checker/utils\";\nimport { getClientIP, ratelimit } from \"@/lib/ratelimit\";\nimport { iteratorToStream, yieldMany } from \"@/lib/stream\";\nimport { wait } from \"@/lib/utils\";\nimport { Events, setupAnalytics } from \"@openstatus/analytics\";\nimport { AVAILABLE_REGIONS } from \"@openstatus/regions\";\nimport { after } from \"next/server\";\nimport { z } from \"zod\";\n\nexport const runtime = \"edge\";\n\nconst RATE_LIMIT_WINDOW = 60; // 60 seconds\nconst MAX_REQUESTS_PER_WINDOW = 3;\n\n// Request schema validation\nconst playCheckerRequestSchema = z.object({\n  url: z.url(\"Invalid URL format\"),\n  method: z\n    .enum([\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"HEAD\", \"OPTIONS\"])\n    .default(\"GET\"),\n  headers: z\n    .array(\n      z.object({\n        key: z.string(),\n        value: z.string(),\n      }),\n    )\n    .optional(),\n  body: z.string().optional(),\n});\n\ntype PlayCheckerRequest = z.infer<typeof playCheckerRequestSchema>;\n\n// Error response types\ntype ErrorCode =\n  | \"RATE_LIMIT_EXCEEDED\"\n  | \"INVALID_REQUEST\"\n  | \"NO_CLIENT_IP\"\n  | \"INTERNAL_ERROR\";\n\ninterface ErrorResponse {\n  error: string;\n  code: ErrorCode;\n  details?: Record<string, unknown>;\n  limit?: number;\n  remaining?: number;\n  reset?: number;\n}\n\nfunction createErrorResponse(\n  code: ErrorCode,\n  error: string,\n  status: number,\n  details?: Record<string, unknown>,\n  headers?: Record<string, string>,\n): Response {\n  const response: ErrorResponse = {\n    error,\n    code,\n    ...details,\n  };\n\n  return new Response(JSON.stringify(response), {\n    status,\n    headers: {\n      \"Content-Type\": \"application/json\",\n      ...headers,\n    },\n  });\n}\n\nconst encoder = new TextEncoder();\n\nasync function* makeIterator({\n  url,\n  method,\n  id,\n  compact,\n}: {\n  url: string;\n  method: Method;\n  id: string;\n  // used for openstatushq/skills:global-speed-checker to reduce size of the response\n  compact: boolean;\n}) {\n  // Create an array to store all the promises\n  const promises = AVAILABLE_REGIONS.map(async (region, index) => {\n    try {\n      console.log(`Checking ${region}...`);\n      // Perform the fetch operation\n      const check =\n        process.env.NODE_ENV === \"production\"\n          ? await checkRegion({ url, region, method })\n          : await mockCheckRegion(region);\n\n      if (\"body\" in check) {\n        check.body = undefined; // Drop the body to avoid storing it in Redis Cache\n      }\n\n      if (check.state === \"success\") {\n        await storeCheckerData({ check, id });\n      }\n\n      // compact mode: remove headers and body and use calculated timing\n      const result =\n        compact && check.state === \"success\"\n          ? (() => {\n              const { headers: _h, body: _b, timing, ...rest } = check;\n              return { ...rest, timing: getTimingPhases(timing) };\n            })()\n          : check;\n\n      return encoder.encode(\n        `${JSON.stringify({\n          ...result,\n          index,\n        })}\\n`,\n      );\n    } catch (error) {\n      console.log(error);\n      return encoder.encode(\"\");\n    }\n  });\n\n  yield* yieldMany(promises);\n  // return the id as the last value\n  yield* generator(id);\n}\n\nasync function* generator(id: string) {\n  // wait for 200ms to avoid racing condition with the last check\n  await wait(200);\n\n  yield await Promise.resolve(encoder.encode(id));\n}\n\nexport async function POST(request: Request) {\n  // Parse and validate request body\n  let requestData: PlayCheckerRequest;\n\n  try {\n    const json = await request.json();\n    const parsed = playCheckerRequestSchema.safeParse(json);\n\n    if (!parsed.success) {\n      return createErrorResponse(\n        \"INVALID_REQUEST\",\n        \"Invalid request format\",\n        400,\n        {\n          details: {\n            issues: parsed.error.issues.map((issue) => ({\n              field: issue.path.join(\".\"),\n              message: issue.message,\n            })),\n          },\n        },\n      );\n    }\n\n    requestData = parsed.data;\n  } catch (_error) {\n    return createErrorResponse(\n      \"INVALID_REQUEST\",\n      \"Invalid JSON in request body\",\n      400,\n    );\n  }\n\n  const { url, method } = requestData;\n\n  const urlObject = new URL(url);\n\n  if (\n    urlObject.hostname.includes(\"openstatus.dev\") &&\n    urlObject.pathname.startsWith(\"/play/checker/api\")\n  ) {\n    return createErrorResponse(\n      \"INVALID_REQUEST\",\n      \"Self-requests are not allowed\",\n      400,\n    );\n  }\n\n  try {\n    const blacklistPatterns = (process.env.BLACKLIST_URL ?? \"\")\n      .split(\",\")\n      .map((p) => p.trim())\n      .filter(Boolean);\n\n    if (blacklistPatterns.some((pattern) => new RegExp(pattern).test(url))) {\n      return createErrorResponse(\n        \"INVALID_REQUEST\",\n        \"This URL is not allowed\",\n        403,\n      );\n    }\n  } catch (error) {\n    console.error(\"Error checking blacklist\", error);\n  }\n\n  // Rate limiting check\n  const clientIP = getClientIP(request.headers);\n\n  if (!clientIP) {\n    return createErrorResponse(\n      \"NO_CLIENT_IP\",\n      \"Unable to determine client IP address\",\n      400,\n    );\n  }\n\n  const rateLimitResult = await ratelimit(`play-checker:${clientIP}`, {\n    window: RATE_LIMIT_WINDOW,\n    limit: MAX_REQUESTS_PER_WINDOW,\n  });\n\n  if (!rateLimitResult.success) {\n    return createErrorResponse(\n      \"RATE_LIMIT_EXCEEDED\",\n      `You have exceeded the rate limit of ${MAX_REQUESTS_PER_WINDOW} requests per ${RATE_LIMIT_WINDOW} seconds`,\n      429,\n      {\n        limit: rateLimitResult.limit,\n        remaining: rateLimitResult.remaining,\n        reset: rateLimitResult.reset,\n      },\n      {\n        \"X-RateLimit-Limit\": rateLimitResult.limit.toString(),\n        \"X-RateLimit-Remaining\": rateLimitResult.remaining.toString(),\n        \"X-RateLimit-Reset\": rateLimitResult.reset.toString(),\n        \"Retry-After\": Math.ceil(\n          (rateLimitResult.reset - Date.now()) / 1000,\n        ).toString(),\n      },\n    );\n  }\n\n  const compact = new URL(request.url).searchParams.get(\"compact\") === \"true\";\n  const uuid = crypto.randomUUID().replace(/-/g, \"\");\n  after(async () => {\n    const analytics = await setupAnalytics({});\n    const additionalProps = { url, uuid, compact };\n    await analytics.track({ ...Events.GlobalSpeedChecker, ...additionalProps });\n  });\n  await storeBaseCheckerData({ url, method, id: uuid });\n\n  const iterator = makeIterator({\n    url,\n    method,\n    id: uuid,\n    compact,\n  });\n  const stream = iteratorToStream(iterator);\n  return new Response(stream, {\n    headers: {\n      \"X-RateLimit-Limit\": rateLimitResult.limit.toString(),\n      \"X-RateLimit-Remaining\": rateLimitResult.remaining.toString(),\n      \"X-RateLimit-Reset\": rateLimitResult.reset.toString(),\n    },\n  });\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/checker/client.tsx",
    "content": "\"use client\";\n\nimport { IconCloudProvider } from \"@/components/icon-cloud-provider\";\nimport {\n  type Timing,\n  is32CharHex,\n  latencyFormatter,\n  regionCheckerSchema,\n  regionFormatter,\n} from \"@/lib/checker/utils\";\nimport { toast } from \"@/lib/toast\";\nimport { cn, notEmpty } from \"@/lib/utils\";\nimport {\n  AVAILABLE_REGIONS,\n  type Region,\n  regionDict,\n} from \"@openstatus/regions\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { useQueryStates } from \"nuqs\";\nimport {\n  createContext,\n  useContext,\n  useEffect,\n  useState,\n  useTransition,\n} from \"react\";\nimport { searchParamsParsers } from \"./search-params\";\nimport { handleExportCSV } from \"./utils\";\n\ntype Values = {\n  region: string;\n  latency: number;\n  status: number;\n  timing: Timing;\n};\n\ntype CheckerContextType = {\n  values: Values[];\n  setValues: React.Dispatch<React.SetStateAction<Values[]>>;\n  id: string | null;\n  setId: React.Dispatch<React.SetStateAction<string | null>>;\n};\n\nconst CheckerContext = createContext<CheckerContextType>({\n  values: [],\n  setValues: () => {},\n  id: null,\n  setId: () => {},\n});\n\nexport function CheckerProvider({\n  children,\n  defaultValues = [],\n}: {\n  children: React.ReactNode;\n  defaultValues?: Values[];\n}) {\n  const [values, setValues] = useState<Values[]>(defaultValues);\n  const [{ id: urlId }, setSearchParams] = useQueryStates(searchParamsParsers);\n  const [id, setId] = useState<string | null>(urlId);\n\n  // Sync local ID state with URL search params\n  useEffect(() => {\n    setId(urlId);\n  }, [urlId]);\n\n  // Helper function to update both local state and URL\n  const updateId: React.Dispatch<React.SetStateAction<string | null>> = async (\n    newId,\n  ) => {\n    const value = typeof newId === \"function\" ? newId(id) : newId;\n    setId(value);\n    await setSearchParams({ id: value });\n  };\n\n  return (\n    <CheckerContext.Provider value={{ values, setValues, id, setId: updateId }}>\n      {children}\n    </CheckerContext.Provider>\n  );\n}\n\nexport function useCheckerContext() {\n  const context = useContext(CheckerContext);\n  if (!context) {\n    throw new Error(\"useCheckerContext must be used within a CheckerProvider\");\n  }\n  return context;\n}\n\nexport function Form({\n  defaultMethod = \"GET\",\n  defaultUrl = \"\",\n}: {\n  defaultMethod?: string;\n  defaultUrl?: string;\n}) {\n  const { setValues, setId } = useCheckerContext();\n  const [isPending, startTransition] = useTransition();\n  const router = useRouter();\n\n  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n    const formData = new FormData(event.target as HTMLFormElement);\n    const url = formData.get(\"url\") as string;\n    const method = formData.get(\"method\") as string;\n\n    // Validate URL\n    try {\n      new URL(url);\n    } catch {\n      // Invalid URL, could add error handling here\n      toast.error(\"Invalid URL\");\n      return;\n    }\n\n    // Reset values and ID\n    setValues([]);\n    setId(null); // This will also clear the URL param\n\n    startTransition(async () => {\n      async function fetchAndReadStream() {\n        let toastId: string | number | undefined;\n        try {\n          toastId = toast.loading(\"Loading data from regions...\", {\n            duration: Number.POSITIVE_INFINITY,\n            closeButton: false,\n          });\n\n          const abortController = new AbortController();\n          const timeoutId = setTimeout(() => abortController.abort(), 10_000);\n\n          const response = await fetch(\"/play/checker/api\", {\n            method: \"POST\",\n            headers: {\n              \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({ url, method }),\n            signal: abortController.signal,\n          });\n\n          if (!response.ok) {\n            try {\n              const json = await response.json();\n              toast.error(json.error, {\n                id: toastId,\n                className: \"text-destructive!\",\n              });\n              return;\n            } catch {\n              toast.error(\"Failed to fetch data\", {\n                id: toastId,\n                description: \"Please try again.\",\n                className: \"text-destructive!\",\n              });\n              return;\n            }\n          }\n\n          clearTimeout(timeoutId);\n\n          const reader = response?.body?.getReader();\n          if (!reader) return;\n\n          const decoder = new TextDecoder();\n          let done = false;\n\n          while (!done) {\n            const { value, done: streamDone } = await reader.read();\n            done = streamDone;\n            if (value) {\n              const decoded = decoder.decode(value, { stream: true });\n              if (!decoded) continue;\n\n              const array = decoded.split(\"\\n\").filter(Boolean);\n\n              const results = array\n                .map((item) => {\n                  try {\n                    // Store the ID if it's a 32-char hex string\n                    if (is32CharHex(item)) {\n                      setId(item);\n                      toast.success(\"Data is available!\", {\n                        id: toastId,\n                        description: \"Learn about the response details.\",\n                        action: {\n                          label: \"Details\",\n                          onClick: () => router.push(`/play/checker/${item}`),\n                        },\n                        duration: 4000,\n                      });\n                      return null;\n                    }\n\n                    const parsed = JSON.parse(item);\n                    const validation = regionCheckerSchema.safeParse(parsed);\n                    if (!validation.success) return null;\n\n                    const check = validation.data;\n                    // Only process successful checks\n                    if (check.state === \"success\") {\n                      return {\n                        region: check.region,\n                        latency: check.latency,\n                        status: check.status,\n                        timing: check.timing,\n                      };\n                    }\n                    return null;\n                  } catch {\n                    return null;\n                  }\n                })\n                .filter(notEmpty);\n\n              if (results.length > 0) {\n                setValues((prev) => [...prev, ...results]);\n                toast.loading(\n                  `Checking ${regionFormatter(\n                    results[0].region,\n                    \"long\",\n                  )} (${latencyFormatter(results[0].latency)})`,\n                  {\n                    id: toastId,\n                  },\n                );\n              }\n            }\n          }\n        } catch (error) {\n          console.error(\"Error fetching data:\", error);\n          if (error instanceof Error && error.name === \"AbortError\") {\n            toast.error(\"Request timeout\", {\n              id: toastId,\n              description:\n                \"The request took too long and was aborted after 7 seconds.\",\n              className: \"text-destructive!\",\n            });\n          }\n        }\n      }\n\n      await fetchAndReadStream();\n    });\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <div className=\"grid grid-cols-3 gap-2 sm:grid-cols-4 md:grid-cols-5\">\n        <div className=\"col-span-1\">\n          <Select name=\"method\" defaultValue={defaultMethod}>\n            <SelectTrigger className=\"h-auto! w-full rounded-none p-4 text-base\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent className=\"rounded-none\">\n              {[\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"].map((method) => (\n                <SelectItem\n                  key={method}\n                  value={method}\n                  className=\"rounded-none px-2 py-3\"\n                >\n                  {method}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n        <div className=\"col-span-2 md:col-span-3\">\n          <Input\n            name=\"url\"\n            placeholder=\"https://openstatus.dev\"\n            className=\"h-auto! rounded-none p-4 text-base md:text-base\"\n            defaultValue={defaultUrl}\n            required\n          />\n        </div>\n        <div className=\"col-span-3 sm:col-span-1\">\n          <Button\n            type=\"submit\"\n            variant=\"default\"\n            className=\"h-full w-full rounded-none p-4 text-base\"\n            disabled={isPending}\n          >\n            {isPending ? \"Submitting...\" : \"Submit\"}\n          </Button>\n        </div>\n        {/* TOOD: add button to details */}\n      </div>\n    </form>\n  );\n}\n\nexport function ResultTable() {\n  const { values } = useCheckerContext();\n  return (\n    <div className=\"table-wrapper\">\n      <table>\n        <thead>\n          <tr>\n            <th className=\"w-12\" />\n            <th className=\"w-12\" />\n            <th>Region</th>\n            <th className=\"text-right!\">Latency</th>\n          </tr>\n        </thead>\n        <tbody>\n          {values.length === 0 ? (\n            <tr>\n              <td>\n                <IconCloudProvider\n                  provider=\"globe\"\n                  className=\"size-4 text-muted-foreground\"\n                />\n              </td>\n              <td>\n                <div className=\"size-4 bg-muted-foreground\" />\n              </td>\n              <td>\n                <br />\n              </td>\n              <td>\n                <br />\n              </td>\n            </tr>\n          ) : (\n            values.map((value) => {\n              const regionConfig = regionDict[value.region as Region];\n              return (\n                <tr key={value.region}>\n                  <td>\n                    <IconCloudProvider\n                      provider={regionConfig.provider}\n                      className=\"size-4\"\n                    />\n                  </td>\n                  <td>\n                    <div\n                      className={cn(\n                        \"size-4\",\n                        STATUS_CODES[\n                          value.status.toString()[0] as keyof typeof STATUS_CODES\n                        ],\n                      )}\n                    />\n                  </td>\n                  <td>\n                    {regionConfig.flag} {regionConfig.code}{\" \"}\n                    <span className=\"text-muted-foreground\">\n                      {regionConfig.location}\n                    </span>\n                  </td>\n                  <td className=\"text-right!\">\n                    {Intl.NumberFormat(\"en-US\", {\n                      maximumFractionDigits: 0,\n                    }).format(value.latency)}\n                    ms\n                  </td>\n                </tr>\n              );\n            })\n          )}\n        </tbody>\n        <caption>\n          Results of your check ({values.length} / {AVAILABLE_REGIONS.length}{\" \"}\n          regions)\n        </caption>\n      </table>\n    </div>\n  );\n}\n\nconst STATUS_CODES = {\n  \"1\": \"bg-muted-foreground\",\n  \"2\": \"bg-success\",\n  \"3\": \"bg-info\",\n  \"4\": \"bg-warning\",\n  \"5\": \"bg-destructive\",\n};\n\nexport function ResponseStatus() {\n  return (\n    <div className=\"flex gap-2\">\n      {Object.entries(STATUS_CODES).map(([code, className]) => (\n        <div key={code} className={cn(\"text-background text-base\", className)}>\n          {code}xx\n        </div>\n      ))}\n    </div>\n  );\n}\n\nexport function DetailsButtonLink() {\n  const { values, id } = useCheckerContext();\n\n  // Only show button if we have all regions and an ID\n  if (!id || values.length === 0) {\n    return null;\n  }\n  return (\n    <Button\n      variant=\"default\"\n      className=\"h-auto! flex-1 rounded-none p-4 text-base\"\n      asChild\n    >\n      <Link\n        href={`/play/checker/${id}`}\n        className=\"no-underline! w-full text-background!\"\n      >\n        Response details\n      </Link>\n    </Button>\n  );\n}\n\nexport function ExportToCSVButton() {\n  const { values } = useCheckerContext();\n\n  if (values.length === 0) {\n    return null;\n  }\n\n  return (\n    <Button\n      variant=\"outline\"\n      className=\"h-auto! flex-1 cursor-pointer rounded-none p-4 text-base\"\n      onClick={() => handleExportCSV(values)}\n    >\n      Export to CSV\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/checker/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getToolsPage } from \"@/content/utils\";\nimport { mockCheckAllRegions } from \"@/lib/checker/mock\";\nimport { getCheckerDataById } from \"@/lib/checker/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBreadcrumbList,\n  getJsonLDFAQPage,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport {\n  CheckerProvider,\n  DetailsButtonLink,\n  ExportToCSVButton,\n  Form,\n  ResponseStatus,\n  ResultTable,\n} from \"./client\";\nimport { searchParamsCache } from \"./search-params\";\n\nexport function generateMetadata(): Metadata {\n  const page = getToolsPage(\"checker\");\n  return getPageMetadata(page, \"play\");\n}\n\nexport default async function Page(props: {\n  searchParams: Promise<{ id: string }>;\n}) {\n  const page = getToolsPage(\"checker\");\n\n  const searchParams = await props.searchParams;\n  const { id } = searchParamsCache.parse(searchParams);\n\n  const data = id\n    ? process.env.NODE_ENV === \"development\"\n      ? await mockCheckAllRegions()\n      : await getCheckerDataById(id)\n    : null;\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDWebPage(page),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Playground\", url: `${BASE_URL}/play` },\n      { name: page.metadata.title, url: `${BASE_URL}/play/checker` },\n    ]),\n    getJsonLDFAQPage(page),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{page.metadata.title}</h1>\n      <p className=\"text-lg\">{page.metadata.description}</p>\n      <CheckerProvider\n        defaultValues={data?.checks.sort((a, b) => a.latency - b.latency)}\n      >\n        <Form defaultMethod={data?.method} defaultUrl={data?.url} />\n        <ResponseStatus />\n        <ResultTable />\n        <div className=\"flex w-full gap-2\">\n          <ExportToCSVButton />\n          <DetailsButtonLink />\n        </div>\n      </CheckerProvider>\n      <CustomMDX source={page.content} />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/checker/search-params.ts",
    "content": "import { createSearchParamsCache, parseAsString } from \"nuqs/server\";\n\nexport const searchParamsParsers = {\n  id: parseAsString,\n};\n\nexport const searchParamsCache = createSearchParamsCache(searchParamsParsers);\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/checker/utils.ts",
    "content": "import { type Timing, getTimingPhases } from \"@/lib/checker/utils\";\nimport { toast } from \"@/lib/toast\";\nimport { type Region, regionDict } from \"@openstatus/regions\";\n\nexport type CheckerRow = {\n  region: string;\n  latency: number;\n  status: number;\n  timing: Timing;\n};\n\nfunction escapeCSV(value: string): string {\n  // Escape values that contain commas, quotes, or newlines by wrapping in quotes\n  if (value.includes(\",\") || value.includes('\"') || value.includes(\"\\n\")) {\n    return `\"${value.replace(/\"/g, '\"\"')}\"`;\n  }\n  return value;\n}\n\nexport function convertToCSV(rows: CheckerRow[]): string {\n  const headers = [\n    \"Region Code\",\n    \"Location\",\n    \"Provider\",\n    \"Latency (ms)\",\n    \"Status\",\n    \"DNS (ms)\",\n    \"Connect (ms)\",\n    \"TLS (ms)\",\n    \"TTFB (ms)\",\n    \"Transfer (ms)\",\n  ];\n  const csvRows = rows.map((row) => {\n    const regionConfig = regionDict[row.region as Region];\n    const timing = getTimingPhases(row.timing);\n    return [\n      escapeCSV(regionConfig.code),\n      escapeCSV(regionConfig.location),\n      escapeCSV(regionConfig.provider),\n      row.latency.toString(),\n      row.status.toString(),\n      timing?.dns.toString() ?? \"\",\n      timing?.connection.toString() ?? \"\",\n      timing?.tls.toString() ?? \"\",\n      timing?.ttfb.toString() ?? \"\",\n      timing?.transfer.toString() ?? \"\",\n    ].join(\",\");\n  });\n  return [headers.join(\",\"), ...csvRows].join(\"\\n\");\n}\n\nexport function downloadCSV(csv: string, filename: string): void {\n  const blob = new Blob([csv], { type: \"text/csv;charset=utf-8;\" });\n  const url = URL.createObjectURL(blob);\n  const link = document.createElement(\"a\");\n  link.href = url;\n  link.download = filename;\n  document.body.appendChild(link);\n  link.click();\n  document.body.removeChild(link);\n  URL.revokeObjectURL(url);\n  toast.success(\"CSV exported successfully\");\n}\n\nexport function handleExportCSV(\n  rows: CheckerRow[],\n  urlForFilename?: string,\n): void {\n  const csv = convertToCSV(rows);\n  const datePart = new Date().toISOString().split(\"T\")[0];\n\n  let filename: string;\n  if (urlForFilename) {\n    const sanitizedUrl = urlForFilename\n      .replace(/^https?:\\/\\//, \"\")\n      .replace(/[^a-zA-Z0-9.-]/g, \"_\");\n    filename = `checker-${sanitizedUrl}-${datePart}.csv`;\n  } else {\n    filename = `checker-results-${datePart}.csv`;\n  }\n\n  downloadCSV(csv, filename);\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/curl/client.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@openstatus/ui/components/ui/select\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { Fragment, useState } from \"react\";\n\ntype Values = {\n  url: string;\n  method: string;\n  body: string;\n  headers: { key: string; value: string }[];\n  verbose: boolean;\n  insecure: boolean;\n  json: boolean;\n};\n\nexport function Form() {\n  const [value, setValue] = useState<Values>({\n    url: \"\",\n    method: \"GET\",\n    body: \"\",\n    headers: [],\n    verbose: false,\n    insecure: false,\n    json: false,\n  });\n  const { copy, isCopied } = useCopyToClipboard();\n\n  return (\n    <form>\n      <div className=\"grid grid-cols-5 gap-4\">\n        <div className=\"col-span-1 space-y-1\">\n          <Label htmlFor=\"method\" className=\"text-base\">\n            Method\n          </Label>\n          <Select\n            name=\"method\"\n            defaultValue=\"GET\"\n            value={value.method}\n            onValueChange={(value) =>\n              setValue((v) => ({ ...v, method: value }))\n            }\n          >\n            <SelectTrigger\n              id=\"method\"\n              className=\"h-auto! w-full rounded-none p-4 text-base\"\n            >\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent className=\"rounded-none\">\n              {[\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"].map((method) => (\n                <SelectItem\n                  key={method}\n                  value={method}\n                  className=\"rounded-none px-2 py-3\"\n                >\n                  {method}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n        <div className=\"col-span-4 space-y-1\">\n          <Label htmlFor=\"url\" className=\"text-base\">\n            URL\n          </Label>\n          <Input\n            name=\"url\"\n            id=\"url\"\n            placeholder=\"https://openstatus.dev\"\n            className=\"h-auto! rounded-none p-4 text-base md:text-base\"\n            value={value.url}\n            onChange={(e) => setValue((v) => ({ ...v, url: e.target.value }))}\n          />\n        </div>\n        <div className=\"col-span-5 space-y-1\">\n          <Label className=\"text-base\">Headers</Label>\n          <div className=\"grid grid-cols-5 gap-2\">\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              className=\"col-span-2 h-auto! rounded-none p-4 text-base\"\n              onClick={() =>\n                setValue((v) => ({\n                  ...v,\n                  headers: [...v.headers, { key: \"\", value: \"\" }],\n                }))\n              }\n            >\n              Add Header\n            </Button>\n            <div className=\"col-span-3\" />\n            {value.headers.map((header, index) => (\n              <Fragment key={index}>\n                <Input\n                  placeholder=\"Key\"\n                  className=\"col-span-2 h-auto! rounded-none p-4 text-base md:text-base\"\n                  value={header.key}\n                  onChange={(e) =>\n                    setValue((v) => ({\n                      ...v,\n                      headers: v.headers.map((h, i) =>\n                        i === index ? { ...h, key: e.target.value } : h,\n                      ),\n                    }))\n                  }\n                />\n                <Input\n                  placeholder=\"Value\"\n                  className=\"col-span-2 h-auto! rounded-none p-4 text-base md:text-base\"\n                  value={header.value}\n                  onChange={(e) =>\n                    setValue((v) => ({\n                      ...v,\n                      headers: v.headers.map((h, i) =>\n                        i === index ? { ...h, value: e.target.value } : h,\n                      ),\n                    }))\n                  }\n                />\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  className=\"h-full w-full rounded-none p-4 text-base\"\n                  onClick={() =>\n                    setValue((v) => ({\n                      ...v,\n                      headers: v.headers.filter((_, i) => i !== index),\n                    }))\n                  }\n                >\n                  Remove\n                </Button>\n              </Fragment>\n            ))}\n          </div>\n        </div>\n        <div className=\"col-span-5 space-y-1\">\n          <Label htmlFor=\"body\" className=\"text-base\">\n            Body\n          </Label>\n          <Textarea\n            id=\"body\"\n            name=\"body\"\n            placeholder=\"\"\n            rows={6}\n            className=\"resize-none rounded-none p-4 text-base md:text-base\"\n            value={value.body}\n            onChange={(e) => setValue((v) => ({ ...v, body: e.target.value }))}\n          />\n        </div>\n        <div className=\"col-span-5 space-y-4\">\n          <div className=\"flex items-start space-x-2\">\n            <Checkbox\n              id=\"json-body\"\n              className=\"size-5 rounded-none\"\n              checked={value.json}\n              onCheckedChange={(checked) =>\n                setValue((v) => ({ ...v, json: Boolean(checked) }))\n              }\n            />\n            <Label\n              htmlFor=\"json-body\"\n              className=\"flex flex-col items-start gap-0 text-base\"\n            >\n              <span>JSON Content-Type</span>\n              <span className=\"text-muted-foreground\">\n                Set the Content-Type header to application/json.\n              </span>\n            </Label>\n          </div>\n          <div className=\"flex items-start space-x-2\">\n            <Checkbox\n              id=\"verbose\"\n              className=\"size-5 rounded-none\"\n              checked={value.verbose}\n              onCheckedChange={(checked) =>\n                setValue((v) => ({ ...v, verbose: Boolean(checked) }))\n              }\n            />\n            <Label\n              htmlFor=\"verbose\"\n              className=\"flex flex-col items-start gap-0 text-base\"\n            >\n              <span>Verbose</span>\n              <span className=\"text-muted-foreground\">\n                Make the operation more talkative.\n              </span>\n            </Label>\n          </div>\n          <div className=\"flex items-start space-x-2\">\n            <Checkbox\n              id=\"insecure\"\n              className=\"size-5 rounded-none\"\n              checked={value.insecure}\n              onCheckedChange={(checked) =>\n                setValue((v) => ({ ...v, insecure: Boolean(checked) }))\n              }\n            />\n            <Label\n              htmlFor=\"insecure\"\n              className=\"flex flex-col items-start gap-0 text-base\"\n            >\n              <span>Accept self-signed certificats</span>\n              <span className=\"text-muted-foreground\">\n                Allow insecure server connections.\n              </span>\n            </Label>\n          </div>\n        </div>\n        <div className=\"col-span-5\">\n          <Textarea\n            id=\"headers\"\n            name=\"headers\"\n            placeholder=\"\"\n            rows={3}\n            className=\"resize-none rounded-none p-4 text-base md:text-base\"\n            value={generateCurlCommand(value)}\n            readOnly\n          />\n        </div>\n        <div className=\"col-span-5\">\n          <Button\n            type=\"button\"\n            onClick={() => copy(generateCurlCommand(value), {})}\n            className=\"h-full w-full rounded-none p-4 text-base\"\n          >\n            {isCopied ? \"Copied\" : \"Copy to Clipboard\"}\n          </Button>\n        </div>\n      </div>\n    </form>\n  );\n}\n\nfunction generateCurlCommand(values?: Values) {\n  if (!values) return \"\";\n\n  const { method, url, body, verbose, insecure, json, headers } = values;\n\n  let curlCommand = \"curl\";\n\n  if (method) {\n    curlCommand += ` -X ${method}`;\n  }\n\n  if (url) {\n    curlCommand += ` \"${url}\" \\\\\\n`;\n  } else {\n    // force a new line if there is no URL\n    curlCommand += \" \\\\\\n\";\n  }\n\n  for (const header of headers) {\n    const { key, value } = header;\n    if (key && value) {\n      curlCommand += `  -H \"${key}: ${value}\" \\\\\\n`;\n    }\n  }\n\n  if (json) {\n    curlCommand += '  -H \"Content-Type: application/json\" \\\\\\n';\n  }\n\n  if (body?.trim()) {\n    curlCommand += `  -d '${body.trim()}' \\\\\\n`;\n  }\n\n  if (verbose) {\n    curlCommand += \"  -v \\\\\\n\";\n  }\n\n  if (insecure) {\n    curlCommand += \"  -k \\\\\\n\";\n  }\n\n  // Remove the trailing ` \\` at the end\n  return curlCommand.trim().slice(0, -2);\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/curl/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getToolsPage } from \"@/content/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBreadcrumbList,\n  getJsonLDFAQPage,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport { Form } from \"./client\";\n\nexport function generateMetadata(): Metadata {\n  const page = getToolsPage(\"curl\");\n  return getPageMetadata(page, \"play\");\n}\n\nexport default function Page() {\n  const page = getToolsPage(\"curl\");\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDWebPage(page),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Playground\", url: `${BASE_URL}/play` },\n      { name: page.metadata.title, url: `${BASE_URL}/play/curl` },\n    ]),\n    getJsonLDFAQPage(page),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{page.metadata.title}</h1>\n      <p className=\"text-lg\">{page.metadata.description}</p>\n      <Form />\n      <CustomMDX source={page.content} />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/page.tsx",
    "content": "import { components } from \"@/content/mdx\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport {\n  ContentBoxDescription,\n  ContentBoxLink,\n  ContentBoxTitle,\n  ContentBoxUrl,\n} from \"../content-box\";\n\nconst TITLE = \"Playground (Tools)\";\nconst DESCRIPTION = \"Playground for tools and services related to OpenStatus.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/play\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default function Page() {\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <h1>Playground (Tools)</h1>\n      <components.Grid cols={2}>\n        {PLAY.map((tool) => (\n          <ContentBoxLink key={tool.href} href={tool.href}>\n            <ContentBoxTitle>{tool.label}</ContentBoxTitle>\n            <ContentBoxDescription>{tool.description}</ContentBoxDescription>\n            <ContentBoxUrl url={tool.href} />\n          </ContentBoxLink>\n        ))}\n      </components.Grid>\n    </section>\n  );\n}\n\nconst PLAY = [\n  {\n    label: \"Global Speed Checker\",\n    description: \"Test the latency of your website worldwide\",\n    href: \"/play/checker\",\n  },\n  {\n    label: \"cURL Builder\",\n    description: \"Curl your website\",\n    href: \"/play/curl\",\n  },\n  {\n    label: \"Uptime SLA Calculator\",\n    description: \"Calculate downtime duration or uptime percentage\",\n    href: \"/play/uptime-sla\",\n  },\n  {\n    label: \"Theme Explorer\",\n    description: \"Explore themes for your status page\",\n    href: \"https://themes.openstatus.dev\",\n  },\n  {\n    label: \"Incident Severity Matrix Builder\",\n    description: \"Classify incidents with deterministic, auditable rules\",\n    href: \"/play/severity-matrix\",\n  },\n  {\n    label: \"All Status Codes\",\n    description: \"Explore all HTTP status codes\",\n    href: \"https://openstat.us\",\n  },\n  {\n    label: \"Vercel Edge Ping\",\n    description: \"Use edge functions to ping your website\",\n    href: \"https://light.openstatus.dev\",\n  },\n  {\n    label: \"React Data Table\",\n    description: \"Shadcn tanstack table with nuqs integration\",\n    href: \"https://logs.run\",\n  },\n  {\n    label: \"Shadcn Time Picker\",\n    description: \"Shadcn time picker with date-fns integration\",\n    href: \"https://time.openstatus.dev\",\n  },\n  {\n    label: \"Astro Status Page\",\n    description: \"Astro status page with openstatus integration\",\n    href: \"https://astro.openstat.us\",\n  },\n  {\n    label: \"Open-source templates\",\n    description: \"Template for dashboard, statuspage and marketing\",\n    href: \"https://template.openstatus.dev\",\n  },\n];\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/severity-matrix/client.tsx",
    "content": "\"use client\";\n\nimport { Details } from \"@/content/mdx-components/details\";\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Checkbox } from \"@openstatus/ui/components/ui/checkbox\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { Slider } from \"@openstatus/ui/components/ui/slider\";\nimport { useState } from \"react\";\n\ntype SeverityLevel = \"SEV0\" | \"SEV1\" | \"SEV2\" | \"SEV3\";\n\nconst SEV_ORDER = [\"SEV0\", \"SEV1\", \"SEV2\", \"SEV3\"] as const;\n\nconst SEVERITY_META = {\n  SEV0: {\n    label: \"Critical\",\n    emoji: \"🔴\",\n    responseTime: \"15 minutes\",\n    statusPageLabel: \"Major Outage\",\n    communication: \"Immediate public update + all-hands\",\n    updateCadence: \"Every 15 min\",\n    escalation: \"VP Engineering + on-call\",\n    postmortem: \"Required\",\n  },\n  SEV1: {\n    label: \"High\",\n    emoji: \"🟠\",\n    responseTime: \"30 minutes\",\n    statusPageLabel: \"Partial Outage\",\n    communication: \"Public update within 15 min\",\n    updateCadence: \"Every 30 min\",\n    escalation: \"Engineering lead\",\n    postmortem: \"Required\",\n  },\n  SEV2: {\n    label: \"Medium\",\n    emoji: \"🟡\",\n    responseTime: \"2 hours\",\n    statusPageLabel: \"Degraded Performance\",\n    communication: \"Status page update + ticket\",\n    updateCadence: \"Every 2 hours\",\n    escalation: \"Team lead\",\n    postmortem: \"Required (team)\",\n  },\n  SEV3: {\n    label: \"Low\",\n    emoji: \"🟢\",\n    responseTime: \"1 business day\",\n    statusPageLabel: \"Minor Issue\",\n    communication: \"Internal ticket only\",\n    updateCadence: \"Daily\",\n    escalation: \"Assigned engineer\",\n    postmortem: \"Optional\",\n  },\n} satisfies Record<\n  SeverityLevel,\n  {\n    label: string;\n    emoji: string;\n    responseTime: string;\n    statusPageLabel: string;\n    communication: string;\n    updateCadence: string;\n    escalation: string;\n    postmortem: string;\n  }\n>;\n\nconst SEVERITY_BORDER: Record<SeverityLevel, string> = {\n  SEV0: \"border-red-500\",\n  SEV1: \"border-orange-500\",\n  SEV2: \"border-yellow-500\",\n  SEV3: \"border-green-500\",\n};\n\nconst TEST_SCENARIOS = [\n  {\n    label: \"DB Failover\",\n    usersAffected: 60,\n    securityImpact: false,\n    slaBreach: false,\n  },\n  {\n    label: \"API Auth Breach\",\n    usersAffected: 5,\n    securityImpact: true,\n    slaBreach: false,\n  },\n  {\n    label: \"CDN Degradation\",\n    usersAffected: 15,\n    securityImpact: false,\n    slaBreach: false,\n  },\n  {\n    label: \"Payment Timeout\",\n    usersAffected: 90,\n    securityImpact: false,\n    slaBreach: true,\n  },\n  {\n    label: \"CSS Regression\",\n    usersAffected: 5,\n    securityImpact: false,\n    slaBreach: false,\n  },\n] as const;\n\nfunction classifySeverity(params: {\n  usersAffected: number;\n  securityImpact: boolean;\n  slaBreach: boolean;\n  thresholdCritical: number;\n  thresholdHigh: number;\n  thresholdMedium: number;\n}): SeverityLevel {\n  const {\n    usersAffected,\n    securityImpact,\n    slaBreach,\n    thresholdCritical,\n    thresholdHigh,\n    thresholdMedium,\n  } = params;\n\n  if (securityImpact) return \"SEV0\";\n\n  let sev: SeverityLevel;\n  if (usersAffected >= thresholdCritical) sev = \"SEV0\";\n  else if (usersAffected >= thresholdHigh) sev = \"SEV1\";\n  else if (usersAffected >= thresholdMedium) sev = \"SEV2\";\n  else sev = \"SEV3\";\n\n  // SLA modifier: upgrade one level if severity is below SEV1\n  if (slaBreach && (sev === \"SEV2\" || sev === \"SEV3\")) {\n    sev = SEV_ORDER[SEV_ORDER.indexOf(sev) - 1];\n  }\n\n  return sev;\n}\n\nfunction classifyReason(params: {\n  usersAffected: number;\n  securityImpact: boolean;\n  slaBreach: boolean;\n  thresholdCritical: number;\n  thresholdHigh: number;\n  thresholdMedium: number;\n}): string {\n  const {\n    usersAffected,\n    securityImpact,\n    slaBreach,\n    thresholdCritical,\n    thresholdHigh,\n    thresholdMedium,\n  } = params;\n\n  if (securityImpact) return \"Security impact override\";\n\n  if (usersAffected >= thresholdCritical)\n    return `≥${thresholdCritical}% users affected`;\n  if (usersAffected >= thresholdHigh) {\n    // SLA breach doesn't affect SEV1+ — only upgrades SEV2/SEV3\n    return `≥${thresholdHigh}% users affected`;\n  }\n  if (usersAffected >= thresholdMedium) {\n    if (slaBreach)\n      return `≥${thresholdMedium}% users affected + SLA breach (upgraded)`;\n    return `≥${thresholdMedium}% users affected`;\n  }\n  if (slaBreach)\n    return `<${thresholdMedium}% users affected + SLA breach (upgraded)`;\n  return `<${thresholdMedium}% users affected`;\n}\n\ntype Values = {\n  usersAffected: number;\n  securityImpact: boolean;\n  slaBreach: boolean;\n  thresholdCritical: number;\n  thresholdHigh: number;\n  thresholdMedium: number;\n};\n\nconst defaultValues: Values = {\n  usersAffected: 15,\n  securityImpact: false,\n  slaBreach: false,\n  thresholdCritical: 80,\n  thresholdHigh: 50,\n  thresholdMedium: 10,\n};\n\nexport function SeverityMatrixBuilder() {\n  const [value, setValue] = useState<Values>(defaultValues);\n\n  const severity = classifySeverity(value);\n  const reason = classifyReason(value);\n  const meta = SEVERITY_META[severity];\n  const thresholdsOrdered =\n    value.thresholdMedium < value.thresholdHigh &&\n    value.thresholdHigh < value.thresholdCritical;\n\n  return (\n    <div className=\"not-prose space-y-8\">\n      {/* Calculator */}\n      <div className=\"space-y-4\">\n        {/* Result */}\n        <div className={`space-y-3 border-2 p-4 ${SEVERITY_BORDER[severity]}`}>\n          <div className=\"space-y-1\">\n            <p className=\"mt-0! font-medium font-mono text-2xl\">\n              {meta.emoji} {severity} – {meta.label}\n            </p>\n            <p className=\"text-muted-foreground text-sm\">{reason}</p>\n          </div>\n          <dl className=\"grid grid-cols-2 gap-x-4 gap-y-2 text-sm sm:grid-cols-3\">\n            <div>\n              <dt className=\"text-muted-foreground\">Response time</dt>\n              <dd className=\"font-medium text-foreground\">\n                {meta.responseTime}\n              </dd>\n            </div>\n            <div>\n              <dt className=\"text-muted-foreground\">Update cadence</dt>\n              <dd className=\"font-medium text-foreground\">\n                {meta.updateCadence}\n              </dd>\n            </div>\n            <div>\n              <dt className=\"text-muted-foreground\">Postmortem</dt>\n              <dd className=\"font-medium text-foreground\">{meta.postmortem}</dd>\n            </div>\n            <div>\n              <dt className=\"text-muted-foreground\">Status page</dt>\n              <dd className=\"font-medium text-foreground\">\n                {meta.statusPageLabel}\n              </dd>\n            </div>\n            <div>\n              <dt className=\"text-muted-foreground\">Escalation</dt>\n              <dd className=\"font-medium text-foreground\">{meta.escalation}</dd>\n            </div>\n            <div>\n              <dt className=\"text-muted-foreground\">Communication</dt>\n              <dd className=\"font-medium text-foreground\">\n                {meta.communication}\n              </dd>\n            </div>\n          </dl>\n        </div>\n\n        <div className=\"space-y-3\">\n          <div className=\"flex items-center justify-between\">\n            <Label className=\"text-base\" htmlFor=\"users-affected\">\n              Users affected\n            </Label>\n            <span className=\"font-medium font-mono text-base text-foreground\">\n              {value.usersAffected}%\n            </span>\n          </div>\n          <Slider\n            id=\"users-affected\"\n            value={[value.usersAffected]}\n            onValueChange={(values) =>\n              setValue((v) => ({ ...v, usersAffected: values[0] }))\n            }\n            className=\"[&_[data-slot=slider-thumb]]:size-5 [&_[data-slot=slider-thumb]]:rounded-none [&_[data-slot=slider-track]]:rounded-none\"\n            min={0}\n            max={100}\n            step={5}\n          />\n        </div>\n        <div className=\"grid gap-4 sm:grid-cols-2\">\n          <div className=\"flex items-start space-x-3\">\n            <Checkbox\n              id=\"security-impact\"\n              checked={value.securityImpact}\n              onCheckedChange={(checked) =>\n                setValue((v) => ({ ...v, securityImpact: checked === true }))\n              }\n              className=\"size-5 rounded-none [&_svg]:size-4\"\n            />\n            <Label\n              htmlFor=\"security-impact\"\n              className=\"flex flex-col items-start gap-0 text-base\"\n            >\n              <span>Security impact</span>\n              <span className=\"text-muted-foreground\">\n                Confirmed security or data breach\n              </span>\n            </Label>\n          </div>\n          <div className=\"flex items-start space-x-3\">\n            <Checkbox\n              id=\"sla-breach\"\n              checked={value.slaBreach}\n              onCheckedChange={(checked) =>\n                setValue((v) => ({ ...v, slaBreach: checked === true }))\n              }\n              className=\"size-5 rounded-none [&_svg]:size-4\"\n            />\n            <Label\n              htmlFor=\"sla-breach\"\n              className=\"flex flex-col items-start gap-0 text-base\"\n            >\n              <span>SLA breach</span>\n              <span className=\"text-muted-foreground\">\n                Active contractual SLA at risk\n              </span>\n            </Label>\n          </div>\n        </div>\n      </div>\n\n      {/* Test Scenarios */}\n      <div className=\"space-y-2\">\n        <Label className=\"text-base\">Test scenarios</Label>\n        <div className=\"flex flex-wrap gap-2\">\n          {TEST_SCENARIOS.map((scenario) => {\n            const isActive =\n              scenario.usersAffected === value.usersAffected &&\n              scenario.securityImpact === value.securityImpact &&\n              scenario.slaBreach === value.slaBreach;\n            return (\n              <Button\n                key={scenario.label}\n                type=\"button\"\n                size=\"sm\"\n                variant={isActive ? \"default\" : \"outline\"}\n                className=\"rounded-none\"\n                onClick={() =>\n                  setValue((v) => ({\n                    ...v,\n                    usersAffected: scenario.usersAffected,\n                    securityImpact: scenario.securityImpact,\n                    slaBreach: scenario.slaBreach,\n                  }))\n                }\n              >\n                {scenario.label}\n              </Button>\n            );\n          })}\n        </div>\n      </div>\n\n      {/* Customize Thresholds */}\n      <Details summary=\"Customize thresholds\">\n        <div className=\"grid grid-cols-3 gap-4\">\n          <div className=\"space-y-1\">\n            <Label\n              htmlFor=\"threshold-critical\"\n              className=\"text-muted-foreground text-sm\"\n            >\n              SEV0 ≥\n            </Label>\n            <div className=\"relative\">\n              <Input\n                id=\"threshold-critical\"\n                type=\"number\"\n                min={0}\n                step={5}\n                max={100}\n                value={value.thresholdCritical}\n                onChange={(e) =>\n                  setValue((v) => ({\n                    ...v,\n                    thresholdCritical: Number(e.target.value),\n                  }))\n                }\n                className=\"h-auto! rounded-none p-4 pr-8 text-base md:text-base\"\n              />\n              <span className=\"-translate-y-1/2 pointer-events-none absolute top-1/2 right-4 text-muted-foreground\">\n                %\n              </span>\n            </div>\n          </div>\n          <div className=\"space-y-1\">\n            <Label\n              htmlFor=\"threshold-high\"\n              className=\"text-muted-foreground text-sm\"\n            >\n              SEV1 ≥\n            </Label>\n            <div className=\"relative\">\n              <Input\n                id=\"threshold-high\"\n                type=\"number\"\n                min={0}\n                step={5}\n                max={100}\n                value={value.thresholdHigh}\n                onChange={(e) =>\n                  setValue((v) => ({\n                    ...v,\n                    thresholdHigh: Number(e.target.value),\n                  }))\n                }\n                className=\"h-auto! rounded-none p-4 pr-8 text-base md:text-base\"\n              />\n              <span className=\"-translate-y-1/2 pointer-events-none absolute top-1/2 right-4 text-muted-foreground\">\n                %\n              </span>\n            </div>\n          </div>\n          <div className=\"space-y-1\">\n            <Label\n              htmlFor=\"threshold-medium\"\n              className=\"text-muted-foreground text-sm\"\n            >\n              SEV2 ≥\n            </Label>\n            <div className=\"relative\">\n              <Input\n                id=\"threshold-medium\"\n                type=\"number\"\n                min={0}\n                step={5}\n                max={100}\n                value={value.thresholdMedium}\n                onChange={(e) =>\n                  setValue((v) => ({\n                    ...v,\n                    thresholdMedium: Number(e.target.value),\n                  }))\n                }\n                className=\"h-auto! rounded-none p-4 pr-8 text-base md:text-base\"\n              />\n              <span className=\"-translate-y-1/2 pointer-events-none absolute top-1/2 right-4 text-muted-foreground\">\n                %\n              </span>\n            </div>\n          </div>\n        </div>\n        {!thresholdsOrdered && (\n          <p className=\"text-destructive text-sm\">\n            Thresholds must be in ascending order: SEV2 &lt; SEV1 &lt; SEV0.\n          </p>\n        )}\n      </Details>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/severity-matrix/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getToolsPage } from \"@/content/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBreadcrumbList,\n  getJsonLDFAQPage,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport { SeverityMatrixBuilder } from \"./client\";\n\nexport function generateMetadata(): Metadata {\n  const page = getToolsPage(\"severity-matrix\");\n  return getPageMetadata(page, \"play\");\n}\n\nexport default function Page() {\n  const page = getToolsPage(\"severity-matrix\");\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDWebPage(page),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Playground\", url: `${BASE_URL}/play` },\n      { name: page.metadata.title, url: `${BASE_URL}/play/severity-matrix` },\n    ]),\n    getJsonLDFAQPage(page),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none space-y-4\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{page.metadata.title}</h1>\n      <p className=\"text-lg\">{page.metadata.description}</p>\n      <SeverityMatrixBuilder />\n      <CustomMDX source={page.content} />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/uptime-sla/client.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport {\n  TabsContent,\n  TabsList,\n  Tabs as TabsPrimitive,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { useState } from \"react\";\n\nconst periods = [\"days\", \"weeks\", \"months\", \"years\"] as const;\n\nconst periodsInSeconds = {\n  days: 24 * 60 * 60,\n  weeks: 24 * 60 * 60 * 7,\n  months: 24 * 60 * 60 * 30,\n  years: 24 * 60 * 60 * 365,\n} satisfies Record<(typeof periods)[number], number>;\n\n// Parse downtime string like \"11h 30m 10s\" or \"1d 2h 30m\" to seconds\nfunction parseDowntimeToSeconds(downtime: string): number {\n  if (!downtime.trim()) return 0;\n\n  const regex = /(\\d+)\\s*(d|h|m|s)/g;\n  let totalSeconds = 0;\n  let match: RegExpExecArray | null = null;\n\n  // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>\n  while ((match = regex.exec(downtime)) !== null) {\n    const value = Number.parseInt(match[1], 10);\n    const unit = match[2];\n\n    switch (unit) {\n      case \"w\":\n        totalSeconds += value * 24 * 60 * 60 * 7; // weeks\n        break;\n      case \"d\":\n        totalSeconds += value * 24 * 60 * 60; // days\n        break;\n      case \"h\":\n        totalSeconds += value * 60 * 60; // hours\n        break;\n      case \"m\":\n        totalSeconds += value * 60; // minutes\n        break;\n      case \"s\":\n        totalSeconds += value; // seconds\n        break;\n    }\n  }\n\n  return totalSeconds;\n}\n\n// Calculate uptime percentage from downtime seconds and period\nfunction calculateUptimePercentage(\n  downtimeSeconds: number,\n  periodSeconds: number,\n): number {\n  if (downtimeSeconds >= periodSeconds) return 0;\n  const uptimeSeconds = periodSeconds - downtimeSeconds;\n  return (uptimeSeconds / periodSeconds) * 100;\n}\n\n// Format seconds into human-readable format (days, hours, minutes, seconds)\nfunction formatDuration(seconds: number): string {\n  if (seconds === 0) return \"0s\";\n\n  const days = Math.floor(seconds / (24 * 60 * 60));\n  const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60));\n  const minutes = Math.floor((seconds % (60 * 60)) / 60);\n  const remainingSeconds = Math.floor(seconds % 60);\n\n  const parts: string[] = [];\n\n  if (days > 0) parts.push(`${days}d`);\n  if (hours > 0) parts.push(`${hours}h`);\n  if (minutes > 0) parts.push(`${minutes}m`);\n  if (remainingSeconds > 0) parts.push(`${remainingSeconds}s`);\n\n  return parts.join(\" \");\n}\n\nconst ninesBadges = [\n  {\n    label: \"three nines\",\n    value: 99.9,\n  },\n  {\n    label: \"four nines\",\n    value: 99.99,\n  },\n  {\n    label: \"five nines\",\n    value: 99.999,\n  },\n  {\n    label: \"six nines\",\n    value: 99.9999,\n  },\n];\n\nconst durationBadges = [\"1m\", \"5m\", \"1h\", \"1h 30m\", \"1d\"];\n\nexport function Calculation() {\n  const [uptimePercentage, setUptimePercentage] = useState(99.99);\n  const [downtimeDuration, setDowntimeDuration] = useState(\"1h 30m\");\n  return (\n    <TabsPrimitive defaultValue=\"percentage\">\n      <TabsList className=\"h-auto w-full rounded-none\">\n        <TabsTrigger\n          value=\"percentage\"\n          className=\"h-auto w-full truncate rounded-none p-4\"\n        >\n          Uptime Percentage\n        </TabsTrigger>\n        <TabsTrigger\n          value=\"duration\"\n          className=\"h-auto w-full truncate rounded-none p-4\"\n        >\n          Downtime Duration\n        </TabsTrigger>\n      </TabsList>\n      <TabsContent value=\"percentage\" className=\"space-y-4 rounded-none\">\n        <div className=\"grid gap-4 sm:grid-cols-2\">\n          <Input\n            id=\"uptime-percentage\"\n            value={uptimePercentage}\n            onChange={(e) => setUptimePercentage(Number(e.target.value))}\n            type=\"number\"\n            placeholder=\"99.99\"\n            className=\"h-auto! rounded-none p-4 text-base md:text-base\"\n          />\n          <div className=\"flex flex-wrap gap-2\">\n            {ninesBadges.map((badge) => (\n              <Button\n                key={badge.value}\n                size=\"sm\"\n                variant={\n                  uptimePercentage === badge.value ? \"default\" : \"outline\"\n                }\n                onClick={() => setUptimePercentage(badge.value)}\n                className=\"rounded-none\"\n              >\n                {badge.label}\n              </Button>\n            ))}\n          </div>\n        </div>\n        <ul>\n          {periods.map((period) => {\n            const totalSeconds =\n              (uptimePercentage * periodsInSeconds[period]) / 100;\n            const allowedDowntimeSeconds =\n              periodsInSeconds[period] - totalSeconds;\n            return (\n              <li key={period}>\n                <span className=\"capitalize\">{period} reporting:</span>{\" \"}\n                <span className=\"font-medium text-foreground\">\n                  {formatDuration(Math.round(allowedDowntimeSeconds))}\n                </span>{\" \"}\n                <span>downtime</span>\n              </li>\n            );\n          })}\n        </ul>\n      </TabsContent>\n      <TabsContent value=\"duration\" className=\"space-y-4 rounded-none\">\n        <div className=\"grid gap-4 sm:grid-cols-2\">\n          <Input\n            id=\"duration-downtime\"\n            value={downtimeDuration}\n            placeholder=\"11h 30m 10s\"\n            onChange={(e) => setDowntimeDuration(e.target.value)}\n            className=\"h-auto! rounded-none p-4 text-base md:text-base\"\n          />\n          <div className=\"flex flex-wrap gap-2\">\n            {durationBadges.map((badge) => (\n              <Button\n                key={badge}\n                size=\"sm\"\n                variant={downtimeDuration === badge ? \"default\" : \"outline\"}\n                onClick={() => setDowntimeDuration(badge)}\n                className=\"rounded-none\"\n              >\n                {badge}\n              </Button>\n            ))}\n          </div>\n        </div>\n        <ul>\n          {periods.map((period) => {\n            const downtimeSeconds = parseDowntimeToSeconds(downtimeDuration);\n            const periodSeconds = periodsInSeconds[period];\n            const uptimePercentage = calculateUptimePercentage(\n              downtimeSeconds,\n              periodSeconds,\n            );\n\n            return (\n              <li key={period}>\n                <span className=\"capitalize\">{period} reporting:</span>{\" \"}\n                <span className=\"font-medium text-foreground\">\n                  {uptimePercentage.toFixed(5)}%\n                </span>{\" \"}\n                <span>uptime</span>\n              </li>\n            );\n          })}\n        </ul>\n      </TabsContent>\n    </TabsPrimitive>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/play/uptime-sla/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getToolsPage } from \"@/content/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBreadcrumbList,\n  getJsonLDFAQPage,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport { Calculation } from \"./client\";\n\nexport function generateMetadata(): Metadata {\n  const page = getToolsPage(\"uptime-sla\");\n  return getPageMetadata(page, \"play\");\n}\n\nexport default function Page() {\n  const page = getToolsPage(\"uptime-sla\");\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDWebPage(page),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Playground\", url: `${BASE_URL}/play` },\n      { name: page.metadata.title, url: `${BASE_URL}/play/uptime-sla` },\n    ]),\n    getJsonLDFAQPage(page),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none space-y-4\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{page.metadata.title}</h1>\n      <p className=\"text-lg\">{page.metadata.description}</p>\n      <Calculation />\n      <CustomMDX source={page.content} />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/status/page.tsx",
    "content": "import { components } from \"@/content/mdx\";\nimport { env } from \"@/env\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport {\n  ContentBoxDescription,\n  ContentBoxLink,\n  ContentBoxTitle,\n  ContentBoxUrl,\n} from \"../content-box\";\nimport { type AtlassianDescriptionEnum, externalStatusArray } from \"./utils\";\n\nexport const dynamic = \"force-dynamic\";\n\nconst TITLE = \"External Status\";\nconst DESCRIPTION =\n  \"Easily check if your external providers is working properly\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/status\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default async function Page() {\n  const res = await fetch(env.EXTERNAL_API_URL);\n  const data = await res.json();\n  const externalStatus = externalStatusArray.parse(data);\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <h1>External Status</h1>\n      <components.Grid cols={2}>\n        {externalStatus.map((status) => (\n          <ContentBoxLink\n            key={status.name}\n            href={status.url}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            <ContentBoxTitle>{status.name}</ContentBoxTitle>\n            <ContentBoxDescription\n              className={STATUS[status.status_description]}\n            >\n              {status.status_description}\n            </ContentBoxDescription>\n            <ContentBoxUrl url={status.url} />\n          </ContentBoxLink>\n        ))}\n      </components.Grid>\n    </section>\n  );\n}\n\nconst STATUS = {\n  \"All Systems Operational\": \"text-success\",\n  \"Major System Outage\": \"text-destructive\",\n  \"Partial System Outage\": \"text-warning\",\n  \"Minor Service Outage\": \"text-warning\",\n  \"Degraded System Service\": \"text-warning\",\n  \"Partially Degraded Service\": \"text-warning\",\n  \"Service Under Maintenance\": \"text-info\",\n} satisfies Record<AtlassianDescriptionEnum, string>;\n"
  },
  {
    "path": "apps/web/src/app/(landing)/status/utils.ts",
    "content": "import { z } from \"zod\";\n\nexport const atlassianDescriptionEnum = z.enum([\n  \"All Systems Operational\", // green\n  \"Major System Outage\", // red\n  \"Partial System Outage\", // orange\n  \"Minor Service Outage\", // yellow\n  \"Degraded System Service\", // yellow\n  \"Partially Degraded Service\", // yellow\n  \"Service Under Maintenance\", // blue\n]);\n\nexport const externalStatus = z.object({\n  id: z.number(),\n  name: z.string(),\n  url: z.string(),\n  external_id: z.string(),\n  last_updated_at: z.iso.datetime({ offset: true }),\n  time_zone: z.string(),\n  status_indicator: z.string(),\n  status_description: atlassianDescriptionEnum,\n  created_at: z.string(),\n  updated_at: z.iso.datetime(),\n});\n\nexport const externalStatusArray = z.array(externalStatus);\n\nexport type ExternalStatus = z.infer<typeof externalStatus>;\nexport type ExternalStatusArray = z.infer<typeof externalStatusArray>;\nexport type AtlassianDescriptionEnum = z.infer<typeof atlassianDescriptionEnum>;\n\n// ------------------------------\n\nexport function getClassname(status: ExternalStatus) {\n  switch (status.status_description) {\n    case \"All Systems Operational\":\n      return \"text-status-operational\";\n    case \"Major System Outage\":\n      return \"text-status-down\";\n    case \"Partial System Outage\":\n      return \"text-status-degraded\";\n    case \"Minor Service Outage\":\n      return \"text-status-degraded\";\n    case \"Degraded System Service\":\n      return \"text-status-degraded\";\n    case \"Partially Degraded Service\":\n      return \"text-status-degraded\";\n    case \"Service Under Maintenance\":\n      return \"text-status-monitoring\";\n    default:\n      return \"text-gray-500\";\n  }\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/use-case/[slug]/page.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getUseCasePages } from \"@/content/utils\";\nimport { BASE_URL, getPageMetadata } from \"@/lib/metadata/shared-metadata\";\nimport {\n  createJsonLDGraph,\n  getJsonLDBlogPosting,\n  getJsonLDBreadcrumbList,\n  getJsonLDFAQPage,\n  getJsonLDHowTo,\n  getJsonLDOrganization,\n  getJsonLDWebPage,\n} from \"@/lib/metadata/structured-data\";\nimport type { Metadata } from \"next\";\nimport Image from \"next/image\";\nimport { notFound } from \"next/navigation\";\nimport { ContentMetadata } from \"../../content-metadata\";\nimport { ContentPagination } from \"../../content-pagination\";\n\nexport const dynamicParams = false;\n\nexport async function generateStaticParams() {\n  const posts = getUseCasePages();\n\n  return posts.map((post) => ({\n    slug: post.slug,\n  }));\n}\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}): Promise<Metadata | undefined> {\n  const { slug } = await params;\n  const post = getUseCasePages().find((post) => post.slug === slug);\n  if (!post) {\n    return;\n  }\n\n  const metadata = getPageMetadata(post, \"use-case\");\n\n  return metadata;\n}\n\nexport default async function UseCase({\n  params,\n}: {\n  params: Promise<{ slug: string }>;\n}) {\n  const { slug } = await params;\n  const posts = getUseCasePages().sort(\n    (a, b) =>\n      b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(),\n  );\n  const postIndex = posts.findIndex((post) => post.slug === slug);\n  const post = posts[postIndex];\n  const previousPost = posts[postIndex - 1];\n  const nextPost = posts[postIndex + 1];\n\n  if (!post) {\n    notFound();\n  }\n\n  const jsonLDGraph = createJsonLDGraph([\n    getJsonLDOrganization(),\n    getJsonLDWebPage(post),\n    getJsonLDBlogPosting(post, \"use-case\"),\n    getJsonLDBreadcrumbList([\n      { name: \"Home\", url: BASE_URL },\n      { name: \"Use Cases\", url: `${BASE_URL}/use-case` },\n      { name: post.metadata.title, url: `${BASE_URL}/use-case/${slug}` },\n    ]),\n    getJsonLDHowTo(post),\n    getJsonLDFAQPage(post),\n  ]);\n\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <script\n        type=\"application/ld+json\"\n        suppressHydrationWarning\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify(jsonLDGraph).replace(/</g, \"\\\\u003c\"),\n        }}\n      />\n      <h1>{post.metadata.title}</h1>\n      <ContentMetadata data={post} />\n      {post.metadata.image ? (\n        <div className=\"relative aspect-video w-full overflow-hidden border border-border\">\n          <Image\n            src={post.metadata.image}\n            alt={post.metadata.title}\n            fill\n            className=\"object-contain\"\n          />\n        </div>\n      ) : null}\n      <CustomMDX source={post.content} />\n      <ContentPagination\n        previousPost={previousPost}\n        nextPost={nextPost}\n        prefix=\"/use-case\"\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/(landing)/use-case/page.tsx",
    "content": "import { getUseCasePages } from \"@/content/utils\";\nimport { defaultMetadata, ogMetadata } from \"@/lib/metadata/shared-metadata\";\nimport { twitterMetadata } from \"@/lib/metadata/shared-metadata\";\nimport type { Metadata } from \"next\";\nimport { ContentList } from \"../content-list\";\n\nconst TITLE = \"Use Cases\";\nconst DESCRIPTION =\n  \"Discover how teams use OpenStatus for compliance, open-source projects, crypto exchanges, and API infrastructure.\";\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  title: TITLE,\n  description: DESCRIPTION,\n  alternates: {\n    canonical: \"/use-case\",\n  },\n  openGraph: {\n    ...ogMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n  },\n  twitter: {\n    ...twitterMetadata,\n    title: TITLE,\n    description: DESCRIPTION,\n    images: [`/api/og?title=${TITLE}&description=${DESCRIPTION}`],\n  },\n};\n\nexport default function UseCaseListPage() {\n  const allUseCases = getUseCasePages();\n  return (\n    <div className=\"prose dark:prose-invert max-w-none\">\n      <h1>Use Cases</h1>\n      <ContentList data={allUseCases} prefix=\"/use-case\" withCategory />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/callback/pagerduty/route.ts",
    "content": "import { redirect } from \"next/navigation\";\n\nconst DASHBOARD_V2 = true;\n\nexport async function GET(request: Request) {\n  const { searchParams } = new URL(request.url);\n  const workspace = searchParams.get(\"workspace\");\n\n  const APP_URL = `${\n    process.env.NODE_ENV === \"development\" // FIXME: This sucks\n      ? \"http://localhost:3000\"\n      : \"https://app.openstatus.dev\"\n  }/notifications?${searchParams}&channel=pagerduty`;\n\n  const WWW_URL = `${\n    process.env.NODE_ENV === \"development\" // FIXME: This sucks\n      ? \"http://localhost:3000\"\n      : \"https://www.openstatus.dev\"\n  }/app/${workspace}/notifications/new/pagerduty?${searchParams}`;\n\n  redirect(DASHBOARD_V2 ? APP_URL : WWW_URL);\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/cron/10m/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\n\nimport { cron, isAuthorizedDomain } from \"../_cron\";\nimport { runSentryCron } from \"../_sentry\";\n\nexport const runtime = \"nodejs\";\n// export const preferredRegion = [\"auto\"];\nexport const dynamic = \"force-dynamic\";\nexport const maxDuration = 300;\nexport const revalidate = 0;\n\nexport async function GET(req: NextRequest) {\n  if (isAuthorizedDomain(req.url)) {\n    const { cronCompleted, cronFailed } = runSentryCron(\"10-m-cron\");\n    try {\n      await cron({ periodicity: \"10m\", req });\n      await cronCompleted();\n    } catch (_error) {\n      await cronFailed();\n    }\n  }\n  return NextResponse.json({ success: true });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/cron/1h/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\n\nimport { cron, isAuthorizedDomain } from \"../_cron\";\nimport { runSentryCron } from \"../_sentry\";\n\nexport const runtime = \"nodejs\";\n// export const preferredRegion = [\"auto\"];\nexport const dynamic = \"force-dynamic\";\nexport const maxDuration = 300;\nexport const revalidate = 0;\n\nexport async function GET(req: NextRequest) {\n  if (isAuthorizedDomain(req.url)) {\n    const { cronCompleted, cronFailed } = runSentryCron(\"1-h-cron\");\n    try {\n      await cron({ periodicity: \"1h\", req });\n      await cronCompleted();\n    } catch (_error) {\n      await cronFailed();\n    }\n  }\n  return NextResponse.json({ success: true });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/cron/1m/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\n\nimport { cron, isAuthorizedDomain } from \"../_cron\";\nimport { runSentryCron } from \"../_sentry\";\n\nexport const runtime = \"nodejs\";\n// export const preferredRegion = [\"auto\"];\nexport const dynamic = \"force-dynamic\";\nexport const maxDuration = 300;\nexport const revalidate = 0;\n\nexport async function GET(req: NextRequest) {\n  if (isAuthorizedDomain(req.url)) {\n    const { cronCompleted, cronFailed } = runSentryCron(\"1-m-cron\");\n    try {\n      await cron({ periodicity: \"1m\", req });\n      await cronCompleted();\n    } catch (_error) {\n      await cronFailed();\n    }\n  }\n  return NextResponse.json({ success: true });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/cron/30m/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\n\nimport { cron, isAuthorizedDomain } from \"../_cron\";\nimport { runSentryCron } from \"../_sentry\";\n\nexport const runtime = \"nodejs\";\n// export const preferredRegion = [\"auto\"];\nexport const dynamic = \"force-dynamic\";\nexport const maxDuration = 300;\nexport const revalidate = 0;\n\nexport async function GET(req: NextRequest) {\n  if (isAuthorizedDomain(req.url)) {\n    const { cronCompleted, cronFailed } = runSentryCron(\"30-m-cron\");\n    try {\n      await cron({ periodicity: \"30m\", req });\n      await cronCompleted();\n    } catch (_error) {\n      await cronFailed();\n    }\n  }\n  return NextResponse.json({ success: true });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/cron/30s/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\n\nimport { cron, isAuthorizedDomain } from \"../_cron\";\nimport { runSentryCron } from \"../_sentry\";\n\nexport const runtime = \"nodejs\";\n// export const preferredRegion = [\"auto\"];\nexport const dynamic = \"force-dynamic\";\nexport const maxDuration = 300;\nexport const revalidate = 0;\n\nexport async function GET(req: NextRequest) {\n  if (isAuthorizedDomain(req.url)) {\n    const { cronCompleted, cronFailed } = runSentryCron(\"30-s-cron\");\n    try {\n      await cron({ periodicity: \"30s\", req });\n      await cronCompleted();\n    } catch (_error) {\n      await cronFailed();\n    }\n  }\n  return NextResponse.json({ success: true });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/cron/5m/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\n\nimport { cron, isAuthorizedDomain } from \"../_cron\";\nimport { runSentryCron } from \"../_sentry\";\n\nexport const runtime = \"nodejs\";\n// export const preferredRegion = [\"auto\"];\nexport const dynamic = \"force-dynamic\";\nexport const maxDuration = 300;\nexport const revalidate = 0;\n\nexport async function GET(req: NextRequest) {\n  if (isAuthorizedDomain(req.url)) {\n    const { cronCompleted, cronFailed } = runSentryCron(\"5-m-cron\");\n    try {\n      await cron({ periodicity: \"5m\", req });\n      await cronCompleted();\n    } catch (_error) {\n      await cronFailed();\n    }\n  }\n  return NextResponse.json({ success: true });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/cron/_cron.ts",
    "content": "import { CloudTasksClient } from \"@google-cloud/tasks\";\nimport type { google } from \"@google-cloud/tasks/build/protos/protos\";\nimport type { NextRequest } from \"next/server\";\nimport { z } from \"zod\";\n\nimport { and, db, eq, gte, isNotNull, lte, notInArray } from \"@openstatus/db\";\nimport type { MonitorStatus } from \"@openstatus/db/src/schema\";\nimport {\n  maintenance,\n  monitor,\n  monitorStatusTable,\n  selectMonitorSchema,\n  selectMonitorStatusSchema,\n} from \"@openstatus/db/src/schema\";\nimport {\n  maintenancesToPageComponents,\n  pageComponent,\n} from \"@openstatus/db/src/schema/page_components\";\n\nimport { env } from \"@/env\";\nimport type { Region } from \"@openstatus/db/src/schema/constants\";\nimport { regionDict } from \"@openstatus/regions\";\nimport {\n  type httpPayloadSchema,\n  type tpcPayloadSchema,\n  transformHeaders,\n} from \"@openstatus/utils\";\n\nconst periodicityAvailable = selectMonitorSchema.pick({ periodicity: true });\n\n// FIXME: do coerce in zod instead\n\nconst DEFAULT_URL = process.env.VERCEL_URL\n  ? `https://${process.env.VERCEL_URL}`\n  : \"http://localhost:3000\";\n\n// We can't secure cron endpoint by vercel thus we should make sure they are called by the generated url\nexport const isAuthorizedDomain = (url: string) => {\n  return url.includes(DEFAULT_URL);\n};\n\nexport const cron = async ({\n  periodicity,\n  // biome-ignore lint/correctness/noUnusedVariables: <explanation>\n  req,\n}: z.infer<typeof periodicityAvailable> & { req: NextRequest }) => {\n  const client = new CloudTasksClient({\n    projectId: env.GCP_PROJECT_ID,\n    credentials: {\n      client_email: process.env.GCP_CLIENT_EMAIL,\n      private_key: env.GCP_PRIVATE_KEY.replaceAll(\"\\\\n\", \"\\n\"),\n    },\n  });\n  const parent = client.queuePath(\n    env.GCP_PROJECT_ID,\n    env.GCP_LOCATION,\n    periodicity,\n  );\n\n  const timestamp = Date.now();\n\n  const currentMaintenance = db\n    .select({ id: maintenance.id })\n    .from(maintenance)\n    .where(\n      and(lte(maintenance.from, new Date()), gte(maintenance.to, new Date())),\n    )\n    .as(\"currentMaintenance\");\n\n  const currentMaintenanceMonitors = db\n    .select({ id: pageComponent.monitorId })\n    .from(maintenancesToPageComponents)\n    .innerJoin(\n      currentMaintenance,\n      eq(maintenancesToPageComponents.maintenanceId, currentMaintenance.id),\n    )\n    .innerJoin(\n      pageComponent,\n      eq(maintenancesToPageComponents.pageComponentId, pageComponent.id),\n    )\n    .where(isNotNull(pageComponent.monitorId));\n\n  const result = await db\n    .select()\n    .from(monitor)\n    .where(\n      and(\n        eq(monitor.periodicity, periodicity),\n        eq(monitor.active, true),\n        notInArray(monitor.id, currentMaintenanceMonitors),\n      ),\n    )\n    .all();\n\n  console.log(`Start cron for ${periodicity}`);\n\n  const monitors = z.array(selectMonitorSchema).safeParse(result);\n  const allResult = [];\n  if (!monitors.success) {\n    console.error(\n      `Error while fetching the monitors ${monitors.error.issues.map((issue) => issue.message).join(\", \")}`,\n    );\n    throw new Error(\"Error while fetching the monitors\");\n  }\n\n  for (const row of monitors.data) {\n    // const selectedRegions = row.regions.length > 0 ? row.regions : [\"ams\"];\n\n    const result = await db\n      .select()\n      .from(monitorStatusTable)\n      .where(eq(monitorStatusTable.monitorId, row.id))\n      .all();\n    const monitorStatus = z.array(selectMonitorStatusSchema).safeParse(result);\n    if (!monitorStatus.success) {\n      console.error(\n        `Error while fetching the monitor status ${monitorStatus.error.issues.map((issue) => issue.message).join(\", \")}`,\n      );\n      continue;\n    }\n\n    for (const region of row.regions) {\n      const status =\n        monitorStatus.data.find((m) => region === m.region)?.status || \"active\";\n\n      const r = regionDict[region as keyof typeof regionDict];\n\n      if (!r) {\n        console.error(`Invalid region ${region}`);\n        continue;\n      }\n      if (r.deprecated) {\n        // Let's uncomment this when we are ready to remove deprecated regions\n        // We should not use deprecated regions anymore\n        console.error(`Deprecated region ${region}`);\n        continue;\n      }\n      const response = createCronTask({\n        row,\n        timestamp,\n        client,\n        parent,\n        status,\n        region,\n      });\n      allResult.push(response);\n      if (periodicity === \"30s\") {\n        // we schedule another task in 30s\n        const scheduledAt = timestamp + 30 * 1000;\n        const response = createCronTask({\n          row,\n          timestamp: scheduledAt,\n          client,\n          parent,\n          status,\n          region,\n        });\n        allResult.push(response);\n      }\n    }\n  }\n\n  const allRequests = await Promise.allSettled(allResult);\n\n  const success = allRequests.filter((r) => r.status === \"fulfilled\").length;\n  const failed = allRequests.filter((r) => r.status === \"rejected\").length;\n\n  console.log(\n    `End cron for ${periodicity} with ${allResult.length} jobs with ${success} success and ${failed} failed`,\n  );\n};\n// timestamp needs to be in ms\nconst createCronTask = async ({\n  row,\n  timestamp,\n  client,\n  parent,\n  status,\n  region,\n}: {\n  row: z.infer<typeof selectMonitorSchema>;\n  timestamp: number;\n  client: CloudTasksClient;\n  parent: string;\n  status: MonitorStatus;\n  region: Region;\n}) => {\n  let payload:\n    | z.infer<typeof httpPayloadSchema>\n    | z.infer<typeof tpcPayloadSchema>\n    | null = null;\n\n  //\n  if (row.jobType === \"http\") {\n    payload = {\n      workspaceId: String(row.workspaceId),\n      monitorId: String(row.id),\n      url: row.url,\n      method: row.method || \"GET\",\n      cronTimestamp: timestamp,\n      body: row.body,\n      headers: row.headers,\n      status: status,\n      assertions: row.assertions ? JSON.parse(row.assertions) : null,\n      degradedAfter: row.degradedAfter,\n      timeout: row.timeout,\n      trigger: \"cron\",\n      otelConfig: row.otelEndpoint\n        ? {\n            endpoint: row.otelEndpoint,\n            headers: transformHeaders(row.otelHeaders),\n          }\n        : undefined,\n      retry: row.retry || 3,\n      followRedirects: row.followRedirects || true,\n    };\n  }\n  if (row.jobType === \"tcp\") {\n    payload = {\n      workspaceId: String(row.workspaceId),\n      monitorId: String(row.id),\n      uri: row.url,\n      status: status,\n      assertions: row.assertions ? JSON.parse(row.assertions) : null,\n      cronTimestamp: timestamp,\n      degradedAfter: row.degradedAfter,\n      timeout: row.timeout,\n      trigger: \"cron\",\n      retry: row.retry || 3,\n      otelConfig: row.otelEndpoint\n        ? {\n            endpoint: row.otelEndpoint,\n            headers: transformHeaders(row.otelHeaders),\n          }\n        : undefined,\n    };\n  }\n\n  if (!payload) {\n    throw new Error(\"Invalid jobType\");\n  }\n  const regionInfo = regionDict[region];\n  let regionHeader = {};\n  if (regionInfo.provider === \"fly\") {\n    regionHeader = { \"fly-prefer-region\": region };\n  }\n  if (regionInfo.provider === \"koyeb\") {\n    regionHeader = { \"X-KOYEB-REGION-OVERRIDE\": region.replace(\"koyeb_\", \"\") };\n  }\n  if (regionInfo.provider === \"railway\") {\n    regionHeader = { \"railway-region\": region.replace(\"railway_\", \"\") };\n  }\n  const newTask: google.cloud.tasks.v2beta3.ITask = {\n    httpRequest: {\n      headers: {\n        \"Content-Type\": \"application/json\", // Set content type to ensure compatibility your application's request parsing\n        ...regionHeader,\n        Authorization: `Basic ${env.CRON_SECRET}`,\n      },\n      httpMethod: \"POST\",\n      url: generateUrl({ row, region }),\n      body: Buffer.from(JSON.stringify(payload)).toString(\"base64\"),\n    },\n    scheduleTime: {\n      seconds: timestamp / 1000,\n    },\n  };\n\n  const request = { parent: parent, task: newTask };\n  return client.createTask(request);\n};\n\nfunction generateUrl({\n  row,\n  region,\n}: {\n  row: z.infer<typeof selectMonitorSchema>;\n  region: Region;\n}) {\n  const regionInfo = regionDict[region];\n\n  switch (regionInfo.provider) {\n    case \"fly\":\n      return `https://openstatus-checker.fly.dev/checker/${row.jobType}?monitor_id=${row.id}`;\n    case \"koyeb\":\n      return `https://openstatus-checker.koyeb.app/checker/${row.jobType}?monitor_id=${row.id}`;\n    case \"railway\":\n      return `https://railway-proxy-production-9cb1.up.railway.app/checker/${row.jobType}?monitor_id=${row.id}`;\n\n    default:\n      throw new Error(\"Invalid jobType\");\n  }\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/cron/_sentry.ts",
    "content": "// Props: https://github.com/getsentry/sentry-javascript/issues/9335#issuecomment-1779057528\nimport { captureCheckIn, flush } from \"@sentry/nextjs\";\n\nexport function runSentryCron(monitorSlug: string) {\n  // 🟡 Notify Sentry your job is running:\n  const checkInId = captureCheckIn({\n    monitorSlug,\n    status: \"in_progress\",\n  });\n  return {\n    cronCompleted: async () => {\n      // 🟢 Notify Sentry your job has completed successfully:\n      captureCheckIn({\n        checkInId,\n        monitorSlug,\n        status: \"ok\",\n      });\n      return flush();\n    },\n    cronFailed: async () => {\n      // 🔴 Notify Sentry your job has failed:\n      captureCheckIn({\n        checkInId,\n        monitorSlug,\n        status: \"error\",\n      });\n      return flush();\n    },\n  };\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/test/http/route.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\n\nimport { monitorRegionSchema } from \"@openstatus/db/src/schema/constants\";\n\nimport { checkRegion } from \"@/lib/checker/utils\";\nimport { httpPayloadSchema } from \"@openstatus/utils\";\nimport { isAnInvalidTestUrl } from \"../../utils\";\n\nexport const runtime = \"edge\";\nexport const preferredRegion = \"auto\";\nexport const dynamic = \"force-dynamic\";\nexport const revalidate = 0;\n\nexport function GET() {\n  return NextResponse.json({ success: true });\n}\n\nexport async function POST(request: Request) {\n  try {\n    const json = await request.json();\n    const _valid = httpPayloadSchema\n      .pick({ url: true, method: true, headers: true, body: true })\n      .extend(z.object({ region: monitorRegionSchema.prefault(\"ams\") }).shape)\n      .safeParse(json);\n\n    if (!_valid.success) {\n      return NextResponse.json({ success: false }, { status: 400 });\n    }\n\n    const { url, region, method, headers, body } = _valid.data;\n    // 🧑‍💻 for the smart one who want to create a loop hole\n    if (isAnInvalidTestUrl(url)) {\n      return NextResponse.json({ success: true }, { status: 200 });\n    }\n\n    const res = await checkRegion({ url, region, method, headers, body });\n\n    return NextResponse.json(res);\n  } catch (e) {\n    console.error(e);\n    return NextResponse.json({ success: false }, { status: 400 });\n  }\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/test/tcp/route.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\n\nimport {\n  type Region,\n  monitorRegionSchema,\n} from \"@openstatus/db/src/schema/constants\";\n\nimport { TCPResponse, tcpPayload } from \"./schema\";\n\nexport const runtime = \"edge\";\nexport const preferredRegion = \"auto\";\nexport const dynamic = \"force-dynamic\";\nexport const revalidate = 0;\n\nexport function GET() {\n  return NextResponse.json({ success: true });\n}\n\nexport async function POST(request: Request) {\n  try {\n    const json = await request.json();\n    const _valid = tcpPayload\n      .pick({ url: true })\n      .extend(z.object({ region: monitorRegionSchema.prefault(\"ams\") }).shape)\n      .safeParse(json);\n\n    if (!_valid.success) {\n      return NextResponse.json({ success: false }, { status: 400 });\n    }\n\n    const { url, region } = _valid.data;\n\n    const res = await checkTCP(url, region);\n\n    return NextResponse.json(res);\n  } catch (e) {\n    console.error(e);\n    return NextResponse.json({ success: false }, { status: 400 });\n  }\n}\nasync function checkTCP(url: string, region: Region) {\n  //\n  const res = await fetch(`https://checker.openstatus.dev/tcp/${region}`, {\n    headers: {\n      Authorization: `Basic ${process.env.CRON_SECRET}`,\n      \"Content-Type\": \"application/json\",\n      \"fly-prefer-region\": region,\n    },\n    method: \"POST\",\n    body: JSON.stringify({\n      uri: url,\n    }),\n    next: { revalidate: 0 },\n  });\n\n  const json = await res.json();\n\n  const data = TCPResponse.safeParse(json);\n\n  if (!data.success) {\n    console.error(res);\n    console.error(JSON.stringify(json));\n    console.error(\n      `something went wrong with request to ${url} error ${data.error.message}`,\n    );\n    throw new Error(data.error.message);\n  }\n\n  return data.data;\n}\n"
  },
  {
    "path": "apps/web/src/app/api/checker/test/tcp/schema.ts",
    "content": "import { z } from \"zod\";\n\nimport { monitorRegionSchema } from \"@openstatus/db/src/schema/constants\";\n\nexport const tcpPayload = z.object({\n  workspaceId: z.string(),\n  monitorId: z.string(),\n  url: z.string(),\n  cronTimestamp: z.number(),\n  timeout: z.number().prefault(45000),\n  degradedAfter: z.number().nullable(),\n});\n\nexport const TCPResponse = z.object({\n  type: z.literal(\"tcp\").prefault(\"tcp\"),\n  requestId: z.number().optional(),\n  workspaceId: z.number().optional(),\n  monitorId: z.number().optional(),\n  timestamp: z.number(),\n  timing: z.object({\n    tcpStart: z.number(),\n    tcpDone: z.number(),\n  }),\n  error: z.string().optional(),\n  region: monitorRegionSchema,\n  latency: z.number().optional(),\n});\n\nexport const TCPResponseTest = TCPResponse.extend({\n  state: z.literal(\"success\").prefault(\"success\"),\n}).or(\n  z.object({\n    type: z.literal(\"tcp\").prefault(\"tcp\"),\n    state: z.literal(\"error\").prefault(\"error\"),\n  }),\n);\nexport type tcpPayload = z.infer<typeof tcpPayload>;\n"
  },
  {
    "path": "apps/web/src/app/api/checker/utils.ts",
    "content": "export const isAnInvalidTestUrl = (rawUrl: string) => {\n  const url = new URL(rawUrl);\n  const isSelfHostName = url.hostname\n    .split(\".\")\n    .slice(-2) // ex: any.sub.openstatus.dev\n    .join(\".\")\n    .includes(\"openstatus.dev\"); // ex: openstatus.dev:80\n\n  return isSelfHostName && url.pathname.startsWith(\"/api/checker/\");\n};\n"
  },
  {
    "path": "apps/web/src/app/api/internal/email/feedback/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\n\nimport { and, gte, lte } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db/src/db\";\nimport { user } from \"@openstatus/db/src/schema\";\nimport { FeedbackEmail, sendEmail } from \"@openstatus/emails\";\n\nexport async function GET(request: NextRequest) {\n  const authHeader = request.headers.get(\"authorization\");\n  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {\n    return new Response(\"Unauthorized\", { status: 401 });\n  }\n\n  const date1 = new Date();\n  date1.setDate(date1.getDate() - 15);\n  const date2 = new Date();\n  date2.setDate(date2.getDate() - 14);\n\n  const users = await db\n    .select({ email: user.email })\n    .from(user)\n    .where(and(gte(user.createdAt, date1), lte(user.createdAt, date2)))\n    .all();\n\n  let sent = 0;\n  for (const u of users) {\n    if (!u.email || u.email.trim() === \"\") continue;\n\n    await sendEmail({\n      from: \"Thibault from OpenStatus <thibault@openstatus.dev>\",\n      subject: \"One quick question\",\n      to: [u.email],\n      react: FeedbackEmail(),\n    });\n    sent++;\n  }\n\n  return Response.json({ success: true, sent });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/internal/email/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\n\nimport { and, eq, gte, inArray, lte } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db/src/db\";\nimport {\n  integration,\n  user,\n  usersToWorkspaces,\n} from \"@openstatus/db/src/schema\";\nimport {\n  FollowUpEmail,\n  SlackFeedbackEmail,\n  sendEmail,\n} from \"@openstatus/emails\";\n\nexport async function GET(request: NextRequest) {\n  const authHeader = request.headers.get(\"authorization\");\n  console.log(authHeader);\n  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {\n    return new Response(\"Unauthorized\", {\n      status: 401,\n    });\n  }\n\n  const date1 = new Date();\n  date1.setDate(date1.getDate() - 2);\n  const date2 = new Date();\n  date2.setDate(date2.getDate() - 1);\n\n  const users = await db\n    .select({\n      email: user.email,\n      workspaceId: usersToWorkspaces.workspaceId,\n    })\n    .from(user)\n    .innerJoin(usersToWorkspaces, eq(user.id, usersToWorkspaces.userId))\n    .where(and(gte(user.createdAt, date1), lte(user.createdAt, date2)))\n    .all();\n\n  const workspaceIds = [\n    ...new Set(users.map((u) => u.workspaceId).filter(Boolean)),\n  ];\n\n  const slackWorkspaceIds = new Set<number>();\n  if (workspaceIds.length > 0) {\n    const slackIntegrations = await db\n      .select({ workspaceId: integration.workspaceId })\n      .from(integration)\n      .where(\n        and(\n          eq(integration.name, \"slack-agent\"),\n          inArray(integration.workspaceId, workspaceIds),\n        ),\n      )\n      .all();\n    for (const row of slackIntegrations) {\n      if (row.workspaceId) {\n        slackWorkspaceIds.add(row.workspaceId);\n      }\n    }\n  }\n\n  for (const u of users) {\n    if (!u.email) continue;\n    const hasSlack = u.workspaceId\n      ? slackWorkspaceIds.has(u.workspaceId)\n      : false;\n\n    await sendEmail({\n      from: \"Thibault from OpenStatus <thibault@openstatus.dev>\",\n      subject: hasSlack\n        ? \"How's the Slack app working for you?\"\n        : \"Manage incidents from Slack\",\n      to: [u.email],\n      react: hasSlack ? SlackFeedbackEmail() : FollowUpEmail(),\n    });\n  }\n  return Response.json({ success: true });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/internal/email/team-invite/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\n\nimport { and, count, eq, gte, inArray, lte } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db/src/db\";\nimport { invitation, user, usersToWorkspaces } from \"@openstatus/db/src/schema\";\nimport { TeamInviteReminderEmail, sendEmail } from \"@openstatus/emails\";\n\nexport async function GET(request: NextRequest) {\n  const authHeader = request.headers.get(\"authorization\");\n  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {\n    return new Response(\"Unauthorized\", { status: 401 });\n  }\n\n  const date1 = new Date();\n  date1.setDate(date1.getDate() - 6);\n  const date2 = new Date();\n  date2.setDate(date2.getDate() - 5);\n\n  const users = await db\n    .select({\n      email: user.email,\n      workspaceId: usersToWorkspaces.workspaceId,\n    })\n    .from(user)\n    .innerJoin(usersToWorkspaces, eq(user.id, usersToWorkspaces.userId))\n    .where(and(gte(user.createdAt, date1), lte(user.createdAt, date2)))\n    .all();\n\n  const workspaceIds = [\n    ...new Set(users.map((u) => u.workspaceId).filter(Boolean)),\n  ];\n\n  if (workspaceIds.length === 0) {\n    return Response.json({ success: true, sent: 0 });\n  }\n\n  const workspaceMemberCounts = await db\n    .select({\n      workspaceId: usersToWorkspaces.workspaceId,\n      memberCount: count(usersToWorkspaces.userId),\n    })\n    .from(usersToWorkspaces)\n    .where(inArray(usersToWorkspaces.workspaceId, workspaceIds))\n    .groupBy(usersToWorkspaces.workspaceId)\n    .all();\n\n  const workspacesWithInvitations = await db\n    .select({ workspaceId: invitation.workspaceId })\n    .from(invitation)\n    .where(inArray(invitation.workspaceId, workspaceIds))\n    .groupBy(invitation.workspaceId)\n    .all();\n\n  const hasTeamActivity = new Set<number>();\n\n  for (const row of workspaceMemberCounts) {\n    if (row.memberCount > 1) {\n      hasTeamActivity.add(row.workspaceId);\n    }\n  }\n  for (const row of workspacesWithInvitations) {\n    hasTeamActivity.add(row.workspaceId);\n  }\n\n  let sent = 0;\n  for (const u of users) {\n    if (!u.email) continue;\n    if (u.workspaceId && hasTeamActivity.has(u.workspaceId)) continue;\n\n    await sendEmail({\n      from: \"Thibault from OpenStatus <thibault@openstatus.dev>\",\n      subject: \"Incidents are a team sport\",\n      to: [u.email],\n      react: TeamInviteReminderEmail(),\n    });\n    sent++;\n  }\n\n  return Response.json({ success: true, sent });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/markdown/[[...path]]/route.ts",
    "content": "import { convertMdxToMarkdown } from \"@/content/convert\";\nimport { resolveContent } from \"@/content/resolve\";\nimport { type NextRequest, NextResponse } from \"next/server\";\n\nexport const runtime = \"nodejs\"; // Need fs access for content loading\n\n/**\n * GET handler for markdown content negotiation\n * Serves clean markdown when Accept: text/markdown header is present\n */\nexport async function GET(\n  _request: NextRequest,\n  { params }: { params: Promise<{ path?: string[] }> },\n) {\n  try {\n    // Extract pathname from catch-all params\n    const { path: pathSegments } = await params;\n    const pathname = pathSegments ? `/${pathSegments.join(\"/\")}` : \"/\";\n\n    // Resolve content (MDX or listing)\n    const result = resolveContent(pathname);\n\n    if (!result) {\n      return new NextResponse(\"Not Found\", { status: 404 });\n    }\n\n    let markdown: string;\n    let contentSource: string;\n\n    // Handle based on content type\n    if (result.type === \"mdx\") {\n      // Convert MDX to markdown with metadata for frontmatter\n      markdown = convertMdxToMarkdown(result.data);\n      contentSource = \"mdx\";\n    } else {\n      // Use pre-generated listing\n      markdown = result.data;\n      contentSource = \"listing\";\n    }\n\n    // Return with appropriate headers\n    return new NextResponse(markdown, {\n      status: 200,\n      headers: {\n        \"Content-Type\": \"text/markdown; charset=utf-8\",\n        \"Cache-Control\":\n          \"public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400\",\n        \"X-Content-Source\": contentSource,\n      },\n    });\n  } catch (error) {\n    console.error(\"Error serving markdown:\", error);\n    return new NextResponse(\"Internal Server Error\", { status: 500 });\n  }\n}\n\n// Only allow GET requests\nexport async function POST() {\n  return new NextResponse(\"Method Not Allowed\", { status: 405 });\n}\n\nexport async function PUT() {\n  return new NextResponse(\"Method Not Allowed\", { status: 405 });\n}\n\nexport async function DELETE() {\n  return new NextResponse(\"Method Not Allowed\", { status: 405 });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/_components/background.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport function Background({\n  children,\n  tw,\n}: {\n  children: React.ReactNode;\n  tw?: string;\n}) {\n  return (\n    <div\n      tw={cn(\n        \"relative flex flex-col bg-white items-center justify-center w-full h-full\",\n        tw,\n      )}\n    >\n      <div\n        tw=\"flex w-full h-full absolute inset-0\"\n        // not every css variable is supported\n        style={{\n          backgroundImage: \"radial-gradient(#cbd5e1 10%, transparent 10%)\",\n          backgroundSize: \"16px 16px\",\n        }}\n      />\n      <div\n        tw=\"flex w-full h-full absolute inset-0 opacity-70\"\n        style={{\n          backgroundColor: \"white\",\n          backgroundImage:\n            \"radial-gradient(farthest-corner at 100px 100px, #cbd5e1, white 80%)\", // tbd: switch color position\n        }}\n      />\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/_components/basic-layout.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport { Background } from \"./background\";\n\nexport function BasicLayout({\n  title,\n  description,\n  children,\n  tw,\n}: {\n  title: string;\n  description?: string | null;\n  children?: React.ReactNode;\n  tw?: string;\n}) {\n  return (\n    <Background>\n      <div tw=\"flex flex-col h-full w-full px-24\">\n        <div tw=\"flex flex-col flex-1 justify-end\">\n          <div tw=\"flex flex-col px-12\">\n            <h3 style={{ fontFamily: \"Cal\" }} tw=\"text-5xl\">\n              {title}\n            </h3>\n            {description ? (\n              <p\n                tw=\"text-slate-600 text-3xl\"\n                style={{ lineClamp: 2, display: \"block\" }}\n              >\n                {description}\n              </p>\n            ) : null}\n          </div>\n        </div>\n        <div\n          tw={cn(\n            \"flex flex-col justify-center shadow-2xl mt-1 bg-white rounded-t-lg border-t-2 border-r-2 border-l-2 border-slate-200 px-12\",\n            tw,\n          )}\n        >\n          {children}\n        </div>\n      </div>\n    </Background>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/_components/status-check.tsx",
    "content": "import type { Tracker } from \"@openstatus/tracker\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport function StatusCheck({ tracker }: { tracker: Tracker }) {\n  const details = tracker.currentDetails;\n\n  // FIXME: move icons into @openstatus/tracker lib\n  function getVariant() {\n    switch (details.variant) {\n      case \"maintenance\":\n        return Hammer;\n      case \"down\":\n        return Minus;\n      case \"degraded\":\n        return Minus;\n      case \"incident\":\n        return Alert;\n      default:\n        return Check;\n    }\n  }\n\n  // REMINDER: we cannot use custom tailwind utility colors like `bg-status-operational/90` here\n  function getClassName() {\n    switch (details.variant) {\n      case \"maintenance\":\n        return \"bg-blue-500/90 border-blue-500\";\n      case \"down\":\n        return \"bg-rose-500/90 border-rose-500\";\n      case \"degraded\":\n        return \"bg-amber-500/90 border-amber-500\";\n      case \"incident\":\n        return \"bg-rose-500/90 border-rose-500\";\n      default:\n        return \"bg-green-500/90 border-green-500\";\n    }\n  }\n\n  const Icon = getVariant();\n\n  return (\n    <div tw=\"flex flex-col justify-center items-center w-full\">\n      <div\n        tw={cn(\n          \"flex text-white rounded-full p-3 border-2 mb-2\",\n          getClassName(),\n        )}\n      >\n        <Icon />\n      </div>\n      <p style={{ fontFamily: \"Cal\" }} tw=\"text-4xl\">\n        {details.long}\n      </p>\n    </div>\n  );\n}\n\nfunction Hammer() {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"40\"\n      height=\"40\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n    >\n      <path d=\"m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9\" />\n      <path d=\"m18 15 4-4\" />\n      <path d=\"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.202-1.756L9 2.96l.92.82A6.18 6.18 0 0 1 12 8.4V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5\" />\n    </svg>\n  );\n}\n\nfunction Check() {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"40\"\n      height=\"40\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n    >\n      <path d=\"M20 6 9 17l-5-5\" />\n    </svg>\n  );\n}\n\nfunction Minus() {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"40\"\n      height=\"40\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n    >\n      <path d=\"M5 12h14\" />\n    </svg>\n  );\n}\n\nfunction Alert() {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"40\"\n      height=\"40\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n    >\n      <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\" />\n      <path d=\"M12 9v4\" />\n      <path d=\"M12 17h.01\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/_components/tracker.tsx",
    "content": "import { Tracker as OSTracker, classNames } from \"@openstatus/tracker\";\n\nimport type { ResponseStatusTracker } from \"@/lib/tb\";\nimport { cn, formatDate } from \"@/lib/utils\";\n\ninterface TrackerProps {\n  data: ResponseStatusTracker[];\n}\n\nexport function Tracker({ data }: TrackerProps) {\n  const tracker = new OSTracker({ data });\n\n  return (\n    <div tw=\"flex flex-col w-full my-12\">\n      <div tw=\"flex flex-col mx-auto\">\n        <div tw=\"flex flex-row items-center justify-between -mb-1 text-black font-light\">\n          <p tw=\"font-medium\">{tracker.totalUptime}%</p>\n        </div>\n        {/* Empty State */}\n        <div tw=\"flex flex-row relative\">\n          {new Array(data.length).fill(null).map((_, i) => {\n            return <div key={i} tw=\"h-16 w-3 rounded-full mr-1 bg-black/20\" />;\n          })}\n          <div tw=\"flex flex-row-reverse absolute left-0\">\n            {tracker.days.map((item, i) => {\n              const isBlackListed = Boolean(item.blacklist);\n              if (isBlackListed) {\n                return (\n                  <div\n                    key={i}\n                    tw=\"h-16 w-3 rounded-full mr-1 bg-status-operational/90\"\n                  />\n                );\n              }\n              return (\n                <div\n                  key={i}\n                  tw={cn(\n                    \"h-16 w-3 rounded-full mr-1\",\n                    classNames[item.variant],\n                  )}\n                />\n              );\n            })}\n          </div>\n        </div>\n        <div tw=\"flex flex-row items-center justify-between -mt-3 text-slate-500 text-sm\">\n          <p tw=\"\">{data.length} days ago</p>\n          <p tw=\"mr-1\">{formatDate(new Date())}</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/checker/route.tsx",
    "content": "import { ImageResponse } from \"next/og\";\n\nimport {\n  getCheckerDataById,\n  regionFormatter,\n  timestampFormatter,\n} from \"@/lib/checker/utils\";\nimport { cn } from \"@/lib/utils\";\nimport { BasicLayout } from \"../_components/basic-layout\";\nimport {\n  SIZE,\n  calSemiBold,\n  interLight,\n  interMedium,\n  interRegular,\n} from \"../utils\";\n\nexport const runtime = \"edge\";\n\nexport async function GET(req: Request) {\n  const [interRegularData, interLightData, calSemiBoldData, interMediumData] =\n    await Promise.all([interRegular, interLight, calSemiBold, interMedium]);\n\n  const { searchParams } = new URL(req.url);\n\n  const id = searchParams.has(\"id\") ? searchParams.get(\"id\") : undefined;\n\n  const data = id ? await getCheckerDataById(id) : undefined;\n\n  function getMinMax() {\n    if (!data?.checks?.length) return;\n    let min = data.checks[0];\n    let max = data.checks[0];\n    for (const check of data.checks) {\n      if (check.latency < min.latency) min = check;\n      if (check.latency > max.latency) max = check;\n    }\n    return { min, max };\n  }\n\n  const { min, max } = getMinMax() || {};\n\n  function getStatusColor(statusCode: number) {\n    const green = String(statusCode).startsWith(\"2\");\n    if (green) return \"border-green-300 bg-green-50 text-green-700\";\n    const blue = String(statusCode).startsWith(\"3\");\n    if (blue) return \"border-blue-300 bg-blue-50 text-blue-700\";\n    const red =\n      String(statusCode).startsWith(\"4\") || String(statusCode).startsWith(\"5\");\n    if (red) return \"border-rose-300 bg-rose-50 text-rose-700\";\n    return \"border-gray-300 bg-gray-50 text-gray-700\";\n  }\n\n  return new ImageResponse(\n    <BasicLayout\n      title=\"Speed Checker\"\n      description=\"Experience the performance of your application from around the different continents.\"\n      tw=\"pt-4 pb-8\"\n    >\n      <h2\n        style={{\n          width: (SIZE.width * 3) / 4,\n          lineClamp: 2,\n          textOverflow: \"ellipsis\",\n          overflow: \"hidden\",\n          whiteSpace: \"nowrap\",\n        }}\n        tw=\"text-3xl text-left font-medium mb-0\"\n      >\n        {data?.url}\n      </h2>\n      {data && (\n        <p tw=\"text-slate-500 text-right\">\n          {timestampFormatter(data.timestamp)}\n        </p>\n      )}\n      <div tw=\"flex\">\n        <div tw=\"flex flex-col flex-1\">\n          <p tw=\"text-slate-600 mb-1\">Min. Request</p>\n        </div>\n        <div tw=\"flex flex-col flex-1\">\n          <p tw=\"text-slate-600 mb-1\">Max. Request</p>\n        </div>\n      </div>\n      <div tw=\"flex w-full h-px bg-slate-200\" />\n      <div tw=\"flex\">\n        <div tw=\"flex flex-col flex-1\">\n          <div tw=\"flex items-center\">\n            <p tw=\"text-slate-600 font-medium text-lg mr-2 w-24 mb-2\">Status</p>\n            {min?.status && (\n              <p\n                tw={cn(\n                  \"text-lg border rounded-full px-3 mb-2\",\n                  getStatusColor(min.status),\n                )}\n              >\n                {min?.status}\n              </p>\n            )}\n          </div>\n          <div tw=\"flex items-center\">\n            <p tw=\"text-slate-600 font-medium text-lg mr-2 w-24 mb-2\">Region</p>\n            {min?.region && (\n              <p tw=\"text-black text-xl mb-2\">{regionFormatter(min.region)}</p>\n            )}\n          </div>\n          <div tw=\"flex items-center\">\n            <p tw=\"text-slate-600 font-medium text-lg mr-2 w-24 mb-2\">\n              Latency\n            </p>\n            <p tw=\"text-black text-xl font-mono mb-2\">{min?.latency}ms</p>\n          </div>\n        </div>\n        <div tw=\"flex flex-col flex-1\">\n          <div tw=\"flex items-center\">\n            <p tw=\"text-slate-600 font-medium text-lg mr-2 w-24 mb-2\">Status</p>\n            {max?.status && (\n              <p\n                tw={cn(\n                  \"text-lg border rounded-full px-3 mb-2\",\n                  getStatusColor(max.status),\n                )}\n              >\n                {max?.status}\n              </p>\n            )}\n          </div>\n          <div tw=\"flex items-center\">\n            <p tw=\"text-slate-600 font-medium text-lg mr-2 w-24 mb-2\">Region</p>\n            {max?.region && (\n              <p tw=\"text-black text-xl mb-2\">{regionFormatter(max.region)}</p>\n            )}\n          </div>\n          <div tw=\"flex items-center\">\n            <p tw=\"text-slate-600 font-medium text-lg mr-2 w-24 mb-2\">\n              Latency\n            </p>\n            <p tw=\"text-black text-xl font-mono mb-2\">{max?.latency}ms</p>\n          </div>\n        </div>\n      </div>\n    </BasicLayout>,\n    {\n      ...SIZE,\n      fonts: [\n        {\n          name: \"Inter\",\n          data: interMediumData,\n          style: \"normal\",\n          weight: 500,\n        },\n        {\n          name: \"Inter\",\n          data: interRegularData,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"Inter\",\n          data: interLightData,\n          style: \"normal\",\n          weight: 300,\n        },\n        {\n          name: \"Cal\",\n          data: calSemiBoldData,\n          style: \"normal\",\n          weight: 600,\n        },\n      ],\n    },\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/monitor/route.tsx",
    "content": "import { ImageResponse } from \"next/og\";\n\nimport { OSTinybird } from \"@openstatus/tinybird\";\n\nimport { env } from \"@/env\";\nimport { DESCRIPTION, TITLE } from \"@/lib/metadata/shared-metadata\";\nimport { BasicLayout } from \"../_components/basic-layout\";\nimport { Tracker } from \"../_components/tracker\";\nimport { SIZE, calSemiBold, interLight, interRegular } from \"../utils\";\n\nconst tb = new OSTinybird(env.TINY_BIRD_API_KEY);\n\nexport const runtime = \"edge\";\n\nexport async function GET(req: Request) {\n  const [interRegularData, interLightData, calSemiBoldData] = await Promise.all(\n    [interRegular, interLight, calSemiBold],\n  );\n\n  const { searchParams } = new URL(req.url);\n\n  const title =\n    (searchParams.has(\"title\") && searchParams.get(\"title\")) || TITLE;\n\n  const description =\n    (searchParams.has(\"description\") && searchParams.get(\"description\")) ||\n    DESCRIPTION;\n\n  const monitorId =\n    (searchParams.has(\"id\") && searchParams.get(\"id\")) || undefined;\n\n  // TODO: we need to pass the monitor type here\n\n  const res = (monitorId &&\n    (await tb.legacy_httpStatus45d({\n      monitorId,\n    }))) || { data: [] };\n\n  return new ImageResponse(\n    <BasicLayout\n      title={title}\n      description={description}\n      tw={res.data.length === 0 ? \"mt-32\" : undefined}\n    >\n      {res.data.length ? <Tracker data={res.data} /> : null}\n    </BasicLayout>,\n    {\n      ...SIZE,\n      fonts: [\n        {\n          name: \"Inter\",\n          data: interRegularData,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"Inter\",\n          data: interLightData,\n          style: \"normal\",\n          weight: 300,\n        },\n        {\n          name: \"Cal\",\n          data: calSemiBoldData,\n          style: \"normal\",\n          weight: 600,\n        },\n      ],\n    },\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/page/route.tsx",
    "content": "import { ImageResponse } from \"next/og\";\n\nimport { Tracker } from \"@openstatus/tracker\";\n\nimport { DESCRIPTION, TITLE } from \"@/lib/metadata/shared-metadata\";\nimport { api } from \"@/trpc/server\";\nimport { BasicLayout } from \"../_components/basic-layout\";\nimport { StatusCheck } from \"../_components/status-check\";\nimport { SIZE, calSemiBold, interLight, interRegular } from \"../utils\";\n\nexport const runtime = \"edge\";\n\n// TODO: legacy Tracker - use api.statusPage.get.query instead\n\nexport async function GET(req: Request) {\n  const [interRegularData, interLightData, calSemiBoldData] = await Promise.all(\n    [interRegular, interLight, calSemiBold],\n  );\n\n  const { searchParams } = new URL(req.url);\n\n  const slug = searchParams.has(\"slug\") ? searchParams.get(\"slug\") : undefined;\n\n  const page = await api.statusPage.getLight.query({ slug: slug || \"\" });\n  const _protected = page?.accessType !== \"public\";\n  const title = page ? page.title : TITLE;\n  const description = page ? \"\" : DESCRIPTION;\n\n  // REMINDER: if password protected, we keep the status 'operational' by default, hiding the actual status\n  const tracker = new Tracker({\n    incidents: _protected ? undefined : page?.incidents,\n    statusReports: _protected ? undefined : page?.statusReports,\n    maintenances: _protected ? undefined : page?.maintenances,\n  });\n\n  return new ImageResponse(\n    <BasicLayout title={title} description={description} tw=\"py-24 px-24\">\n      <StatusCheck tracker={tracker} />\n    </BasicLayout>,\n    {\n      ...SIZE,\n      fonts: [\n        {\n          name: \"Inter\",\n          data: interRegularData,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"Inter\",\n          data: interLightData,\n          style: \"normal\",\n          weight: 300,\n        },\n        {\n          name: \"Cal\",\n          data: calSemiBoldData,\n          style: \"normal\",\n          weight: 600,\n        },\n      ],\n    },\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/post/route.tsx",
    "content": "/* eslint-disable @next/next/no-img-element */\nimport { ImageResponse } from \"next/og\";\n\nimport { TITLE } from \"@/lib/metadata/shared-metadata\";\nimport { BasicLayout } from \"../_components/basic-layout\";\nimport {\n  DEFAULT_URL,\n  SIZE,\n  calSemiBold,\n  commitMonoBold,\n  commitMonoRegular,\n  interLight,\n  interRegular,\n} from \"../utils\";\n\nexport const runtime = \"edge\";\n\nexport async function GET(req: Request) {\n  const [\n    interRegularData,\n    interLightData,\n    calSemiBoldData,\n    commitMonoRegularData,\n    commitMonoBoldData,\n  ] = await Promise.all([\n    interRegular,\n    interLight,\n    calSemiBold,\n    commitMonoRegular,\n    commitMonoBold,\n  ]);\n\n  const { searchParams } = new URL(req.url);\n\n  const title =\n    (searchParams.has(\"title\") && searchParams.get(\"title\")) || TITLE;\n  const description = searchParams.has(\"description\")\n    ? searchParams.get(\"description\")\n    : undefined;\n  const image = searchParams.has(\"image\")\n    ? searchParams.get(\"image\")\n    : undefined;\n\n  return new ImageResponse(\n    <BasicLayout title={title} description={description}>\n      {image ? (\n        <img\n          alt=\"\"\n          style={{ objectFit: \"cover\", height: 330 }} // h-80 = 320px\n          tw=\"flex w-full\"\n          src={new URL(image, DEFAULT_URL).toString()}\n        />\n      ) : null}\n    </BasicLayout>,\n    {\n      ...SIZE,\n      fonts: [\n        {\n          name: \"Inter\",\n          data: interRegularData,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"Inter\",\n          data: interLightData,\n          style: \"normal\",\n          weight: 300,\n        },\n        {\n          name: \"Cal\",\n          data: calSemiBoldData,\n          style: \"normal\",\n          weight: 600,\n        },\n        {\n          name: \"Commit Mono\",\n          data: commitMonoRegularData,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"Commit Mono\",\n          data: commitMonoBoldData,\n          style: \"normal\",\n          weight: 700,\n        },\n      ],\n    },\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/route.tsx",
    "content": "/* eslint-disable @next/next/no-img-element */\nimport { ImageResponse } from \"next/og\";\n\nimport { OG_DESCRIPTION, TITLE } from \"@/lib/metadata/shared-metadata\";\nimport { SIZE } from \"./utils\";\n\nexport const runtime = \"edge\";\n\nconst FOOTER = \"openstatus.dev\";\nconst CATEGORY = \"product\";\n\nexport async function GET(req: Request) {\n  const fontMonoRegular = await fetch(\n    new URL(\"../../../public/fonts/RobotoMono-Regular.ttf\", import.meta.url),\n  ).then((res) => res.arrayBuffer());\n  const fontMonoMedium = await fetch(\n    new URL(\"../../../public/fonts/RobotoMono-Medium.ttf\", import.meta.url),\n  ).then((res) => res.arrayBuffer());\n  const fontMonoBold = await fetch(\n    new URL(\"../../../public/fonts/RobotoMono-Bold.ttf\", import.meta.url),\n  ).then((res) => res.arrayBuffer());\n\n  const { searchParams } = new URL(req.url);\n\n  const title =\n    (searchParams.has(\"title\") && searchParams.get(\"title\")) || TITLE;\n\n  const description =\n    (searchParams.has(\"description\") && searchParams.get(\"description\")) ||\n    OG_DESCRIPTION;\n\n  const footer =\n    (searchParams.has(\"footer\") && searchParams.get(\"footer\")) || FOOTER;\n\n  const category =\n    (searchParams.has(\"category\") && searchParams.get(\"category\")) || CATEGORY;\n\n  return new ImageResponse(\n    <div tw=\"relative flex flex-col items-start justify-start w-full h-full bg-gray-100\">\n      <div\n        tw=\"flex flex-col h-full p-8 w-full\"\n        style={{ fontFamily: \"Font Mono\" }}\n      >\n        <div tw=\"flex flex-col justify-end flex-1 mb-8\">\n          <p tw=\"text-xl text-left\">[{category.toLowerCase()}]</p>\n          <h1\n            tw=\"text-6xl text-black text-left font-medium\"\n            style={{ lineClamp: 2, display: \"block\" }}\n          >\n            {title}\n          </h1>\n          <p\n            tw=\"text-4xl text-slate-700 text-left\"\n            style={{ lineClamp: 2, display: \"block\" }}\n          >\n            {description}\n          </p>\n        </div>\n        <p tw=\"font-medium text-xl text-slate-500 text-left\">{footer}</p>\n      </div>\n    </div>,\n    {\n      ...SIZE,\n      fonts: [\n        {\n          name: \"Font Mono\",\n          data: fontMonoRegular,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"Font Mono\",\n          data: fontMonoMedium,\n          style: \"normal\",\n          weight: 500,\n        },\n        {\n          name: \"Font Mono\",\n          data: fontMonoBold,\n          style: \"normal\",\n          weight: 700,\n        },\n      ],\n    },\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/status/route.tsx",
    "content": "import { ImageResponse } from \"next/og\";\n\nimport { DESCRIPTION, TITLE } from \"@/lib/metadata/shared-metadata\";\nimport { cn } from \"@/lib/utils\";\nimport { api } from \"@/trpc/server\";\nimport type { RouterOutputs } from \"@openstatus/api\";\nimport { format } from \"date-fns\";\nimport { SIZE } from \"../utils\";\n\nexport const runtime = \"edge\";\n\nconst FOOTER = \"openstatus.dev\";\n\ntype Page = NonNullable<RouterOutputs[\"statusPage\"][\"get\"]>;\n\nfunction getContent(page?: Page | null) {\n  switch (page?.status) {\n    case \"error\":\n      return {\n        label: \"Incident Ongoing\",\n        bg: \"bg-rose-500/90 border-rose-500\",\n        text: \"text-rose-500\",\n      };\n    case \"degraded\":\n      return {\n        label: \"Degraded Performance\",\n        bg: \"bg-orange-500/90 border-orange-500\",\n        text: \"text-amber-500\",\n      };\n    case \"info\":\n      return {\n        label: \"Maintenance\",\n        bg: \"bg-blue-500/90 border-blue-500\",\n        text: \"text-blue-500\",\n      };\n    case \"success\":\n      return {\n        label: \"All Systems Operational\",\n        bg: \"bg-green-500/90 border-green-500\",\n        text: \"text-green-500\",\n      };\n    default:\n      return {\n        label: \"Unknown\",\n        bg: \"bg-gray-500/90 border-gray-500\",\n        text: \"text-gray-500\",\n      };\n  }\n}\n\nexport async function GET(req: Request) {\n  const fontMonoRegular = await fetch(\n    new URL(\"../../../../public/fonts/RobotoMono-Regular.ttf\", import.meta.url),\n  ).then((res) => res.arrayBuffer());\n  const fontMonoMedium = await fetch(\n    new URL(\"../../../../public/fonts/RobotoMono-Medium.ttf\", import.meta.url),\n  ).then((res) => res.arrayBuffer());\n  const fontMonoBold = await fetch(\n    new URL(\"../../../../public/fonts/RobotoMono-Bold.ttf\", import.meta.url),\n  ).then((res) => res.arrayBuffer());\n\n  const { searchParams } = new URL(req.url);\n\n  const slug = searchParams.has(\"slug\") ? searchParams.get(\"slug\") : undefined;\n\n  const page = await api.statusPage.get.query({ slug: slug || \"\" });\n  const content = getContent(page);\n\n  const title = page ? page.title : TITLE;\n  const description = page ? page.description : DESCRIPTION;\n  const category = content?.label || \"unknown\";\n  const footer =\n    page?.customDomain || page ? `${page?.slug}.openstatus.dev` : FOOTER;\n\n  return new ImageResponse(\n    <div tw=\"relative flex flex-col items-start justify-start w-full h-full bg-gray-100\">\n      <div\n        tw=\"flex flex-col h-full p-8 w-full\"\n        style={{ fontFamily: \"Font Mono\" }}\n      >\n        <div tw=\"flex flex-col justify-end flex-1 mb-8\">\n          <div tw={cn(\"flex flex-row items-center text-2xl\", content?.text)}>\n            [<div tw={cn(\"rounded-full h-5 w-5 mr-2\", content?.bg)} />\n            <p>{category}</p>] | {format(new Date(), \"MMM d, yyyy HH:mm zzz\")}\n          </div>\n          <h1\n            tw=\"text-6xl text-black text-left font-medium\"\n            style={{ lineClamp: 2, display: \"block\" }}\n          >\n            {title}\n          </h1>\n          <p\n            tw=\"text-4xl text-slate-700 text-left\"\n            style={{ lineClamp: 2, display: \"block\" }}\n          >\n            {description}\n          </p>\n        </div>\n        <p tw=\"font-medium text-xl text-slate-500 text-left\">{footer}</p>\n      </div>\n    </div>,\n    {\n      ...SIZE,\n      fonts: [\n        {\n          name: \"Font Mono\",\n          data: fontMonoRegular,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"Font Mono\",\n          data: fontMonoMedium,\n          style: \"normal\",\n          weight: 500,\n        },\n        {\n          name: \"Font Mono\",\n          data: fontMonoBold,\n          style: \"normal\",\n          weight: 700,\n        },\n      ],\n    },\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/api/og/utils.ts",
    "content": "export const SIZE = {\n  width: 1200,\n  height: 630,\n};\n\nexport const DEFAULT_URL = process.env.VERCEL_URL\n  ? `https://${process.env.VERCEL_URL}`\n  : \"http://localhost:3000\";\n\nexport const interMedium = fetch(\n  new URL(\"../../../public/fonts/Inter-Medium.ttf\", import.meta.url),\n).then((res) => res.arrayBuffer());\n\nexport const interRegular = fetch(\n  new URL(\"../../../public/fonts/Inter-Regular.ttf\", import.meta.url),\n).then((res) => res.arrayBuffer());\n\nexport const interLight = fetch(\n  new URL(\"../../../public/fonts/Inter-Light.ttf\", import.meta.url),\n).then((res) => res.arrayBuffer());\n\nexport const calSemiBold = fetch(\n  new URL(\"../../../public/fonts/CalSans-SemiBold.ttf\", import.meta.url),\n).then((res) => res.arrayBuffer());\n\nexport const commitMonoRegular = fetch(\n  new URL(\"../../../public/fonts/CommitMono-400-Regular.otf\", import.meta.url),\n).then((res) => res.arrayBuffer());\n\nexport const commitMonoBold = fetch(\n  new URL(\"../../../public/fonts/CommitMono-700-Regular.otf\", import.meta.url),\n).then((res) => res.arrayBuffer());\n"
  },
  {
    "path": "apps/web/src/app/api/search/route.ts",
    "content": "import { slugify } from \"@/content/mdx\";\nimport {\n  type MDXData,\n  PAGE_TYPES,\n  getHomePage,\n  getPages,\n} from \"@/content/utils\";\nimport sanitizeHtml from \"sanitize-html\";\nimport { z } from \"zod\";\n\nconst SearchSchema = z.object({\n  p: z.enum(PAGE_TYPES).nullish(),\n  q: z.string().nullish(),\n});\n\nexport type SearchParams = z.infer<typeof SearchSchema>;\n\nexport async function GET(request: Request) {\n  const { searchParams } = new URL(request.url);\n  const query = searchParams.get(\"q\");\n  const page = searchParams.get(\"p\");\n\n  const params = SearchSchema.safeParse({\n    p: page,\n    q: query,\n  });\n\n  if (!params.success) {\n    console.error(params.error);\n    return new Response(JSON.stringify({ error: params.error.message }), {\n      status: 400,\n    });\n  }\n\n  if (!params.data.p) {\n    return new Response(JSON.stringify([]), {\n      status: 200,\n    });\n  }\n\n  const results = search(params.data).sort((a, b) => {\n    return b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime();\n  });\n\n  return new Response(JSON.stringify(results), {\n    status: 200,\n  });\n}\n\nfunction search(params: SearchParams) {\n  const { p, q } = params;\n  let results: MDXData[] = [];\n\n  if (p === \"tools\") {\n    results = getPages(\"tools\").filter((tool) => tool.slug !== \"checker-slug\");\n  } else if (p === \"product\") {\n    const home = getHomePage();\n    // NOTE: we override /home with / for the home.mdx file\n    home.href = \"/\";\n    home.metadata.title = \"Homepage\";\n    results = [home, ...getPages(\"product\")];\n  } else if (p === \"all\") {\n    const home = getHomePage();\n    // NOTE: we override /home with / for the home.mdx file\n    home.href = \"/\";\n    home.metadata.title = \"Homepage\";\n    results = [\n      ...getPages(\"blog\"),\n      ...getPages(\"changelog\"),\n      ...getPages(\"tools\").filter((tool) => tool.slug !== \"checker-slug\"),\n      ...getPages(\"compare\"),\n      ...getPages(\"product\"),\n      ...getPages(\"guides\"),\n      ...getPages(\"use-case\"),\n      ...getPages(\"unrelated\"),\n      home,\n    ];\n  } else {\n    if (p) results = getPages(p);\n  }\n\n  const searchMap = new Map<\n    string,\n    {\n      title: boolean;\n      content: boolean;\n    }\n  >();\n\n  results = results\n    .filter((result) => {\n      if (!q) return true;\n\n      const hasSearchTitle = result.metadata.title\n        .toLowerCase()\n        .includes(q.toLowerCase());\n      const hasSearchContent = result.content\n        .toLowerCase()\n        .includes(q.toLowerCase());\n\n      searchMap.set(result.slug, {\n        title: hasSearchTitle,\n        content: hasSearchContent,\n      });\n\n      return hasSearchTitle || hasSearchContent;\n    })\n    .map((result) => {\n      const search = searchMap.get(result.slug);\n\n      // Find the closest heading to the search match and add it as an anchor\n      let href = result.href;\n\n      // Add query parameter for highlighting\n      if (q) {\n        href = `${href}?q=${encodeURIComponent(q)}`;\n      }\n\n      if (q && search?.content) {\n        const headingSlug = findClosestHeading(result.content, q);\n        if (headingSlug) {\n          href = `${href}#${headingSlug}`;\n        }\n      }\n\n      const content =\n        search?.content || !search?.title\n          ? getContentSnippet(result.content, q)\n          : \"\";\n\n      return {\n        ...result,\n        content,\n        href,\n      };\n    });\n\n  return results;\n}\n\nconst WORKDS_BEFORE = 2;\nconst WORKDS_AFTER = 20;\n\nfunction getContentSnippet(\n  mdxContent: string,\n  searchQuery: string | null | undefined,\n): string {\n  if (!searchQuery) {\n    return `${mdxContent.slice(0, 100)}...`;\n  }\n\n  const content = sanitizeContent(mdxContent.toLowerCase());\n  const searchLower = searchQuery.toLowerCase();\n  const matchIndex = content.indexOf(searchLower);\n\n  if (matchIndex === -1) {\n    // No match found, return first 100 chars\n    return `${content.slice(0, 100)}...`;\n  }\n\n  // Find start of snippet (go back N words)\n  let start = matchIndex;\n  for (let i = 0; i < WORKDS_BEFORE && start > 0; i++) {\n    const prevSpace = content.lastIndexOf(\" \", start - 2);\n    if (prevSpace === -1) break;\n    start = prevSpace + 1;\n  }\n\n  // Find end of snippet (go forward N words)\n  let end = matchIndex + searchQuery.length;\n  for (let i = 0; i < WORKDS_AFTER && end < content.length; i++) {\n    const nextSpace = content.indexOf(\" \", end + 1);\n    if (nextSpace === -1) {\n      end = content.length;\n      break;\n    }\n    end = nextSpace;\n  }\n\n  // Extract snippet\n  let snippet = content.slice(start, end).trim();\n\n  if (!snippet) return snippet;\n\n  if (start > 0) snippet = `...${snippet}`;\n  if (end < content.length) snippet = `${snippet}...`;\n\n  return snippet;\n}\n\nexport function sanitizeContent(input: string) {\n  return sanitizeHtml(input)\n    .replace(/<[^>]+>/g, \"\") // strip JSX tags\n    .replace(/^#{1,6}\\s+/gm, \"\") // strip markdown heading symbols, keep text\n    .replace(/!\\[.*?\\]\\(.*?\\)/g, \"\") // strip images\n    .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\") // keep link text\n    .replace(/\\*\\*(.*?)\\*\\*/g, \"$1\") // strip bold\n    .replace(/__(.*?)__/g, \"$1\") // strip italic\n    .replace(/_(.*?)_/g, \"$1\") // strip underline\n    .replace(/[`*>~]/g, \"\") // strip most formatting\n    .replace(/\\s+/g, \" \") // collapse whitespace\n    .replace(/[<>]/g, (c) => (c === \"<\" ? \"&lt;\" : \"&gt;\")) // escape any remaining angle brackets\n    .trim();\n}\n\n/**\n * Find the closest heading before the search match and return its slug\n */\nfunction findClosestHeading(\n  mdxContent: string,\n  searchQuery: string | null | undefined,\n): string | null {\n  if (!searchQuery) return null;\n\n  const searchLower = searchQuery.toLowerCase();\n  const contentLower = mdxContent.toLowerCase();\n  const matchIndex = contentLower.indexOf(searchLower);\n\n  if (matchIndex === -1) return null;\n\n  // Look for headings before the match (## Heading, ### Heading, etc.)\n  const contentBeforeMatch = mdxContent.slice(0, matchIndex);\n  const headingRegex = /^#{1,6}\\s+(.+)$/gm;\n  const headings: { text: string; index: number }[] = [];\n\n  let match = headingRegex.exec(contentBeforeMatch);\n  while (match !== null) {\n    headings.push({\n      text: match[1].trim(),\n      index: match.index,\n    });\n    match = headingRegex.exec(contentBeforeMatch);\n  }\n\n  // Return the closest heading (last one before the match)\n  if (headings.length > 0) {\n    const closestHeading = headings[headings.length - 1];\n    return slugify(closestHeading.text);\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "apps/web/src/app/api/test/timeout/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport { wait } from \"@/lib/utils\";\n\nexport async function POST() {\n  await wait(10_000);\n  return NextResponse.json({ message: \"Hello, World!\" });\n}\n\nexport async function GET() {\n  await wait(10_000);\n  return NextResponse.json({ message: \"Hello, World!\" });\n}\n"
  },
  {
    "path": "apps/web/src/app/api/trpc/edge/[trpc]/route.ts",
    "content": "import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport type { NextRequest } from \"next/server\";\n\nimport { createTRPCContext } from \"@openstatus/api\";\nimport { edgeRouter } from \"@openstatus/api/src/edge\";\n\nexport const runtime = \"edge\";\n\nconst handler = (req: NextRequest) =>\n  fetchRequestHandler({\n    endpoint: \"/api/trpc/edge\",\n    router: edgeRouter,\n    req: req,\n    createContext: () => createTRPCContext({ req }),\n    onError: ({ error }) => {\n      console.log(\"Error in tRPC handler (edge)\");\n      console.error(error);\n    },\n  });\n\nexport { handler as GET, handler as POST };\n"
  },
  {
    "path": "apps/web/src/app/api/trpc/lambda/[trpc]/route.ts",
    "content": "import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport type { NextRequest } from \"next/server\";\n\nimport { createTRPCContext } from \"@openstatus/api\";\nimport { lambdaRouter } from \"@openstatus/api/src/lambda\";\n\n// Stripe is incompatible with Edge runtimes due to using Node.js events\n// export const runtime = \"edge\";\n\nconst handler = (req: NextRequest) =>\n  fetchRequestHandler({\n    endpoint: \"/api/trpc/lambda\",\n    router: lambdaRouter,\n    req: req,\n    createContext: () => createTRPCContext({ req }),\n    onError: ({ error }) => {\n      console.log(\"Error in tRPC handler (lambda)\");\n      console.error(error);\n    },\n  });\n\nexport { handler as GET, handler as POST };\n"
  },
  {
    "path": "apps/web/src/app/api/upload/route.ts",
    "content": "import { put } from \"@vercel/blob\";\nimport { NextResponse } from \"next/server\";\n\nexport async function POST(request: Request): Promise<NextResponse> {\n  const { searchParams } = new URL(request.url);\n  const filename = searchParams.get(\"filename\");\n\n  if (!filename || !request.body) {\n    return NextResponse.json(\n      { error: \"Internal Server Error\" },\n      { status: 500 },\n    );\n  }\n\n  const blob = await put(filename, request.body, {\n    access: \"public\",\n  });\n\n  return NextResponse.json(blob);\n}\n"
  },
  {
    "path": "apps/web/src/app/api/webhook/stripe/route.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { getHTTPStatusCodeFromError } from \"@trpc/server/http\";\nimport type { NextRequest } from \"next/server\";\n\nimport { createTRPCContext } from \"@openstatus/api\";\nimport { lambdaRouter, stripe } from \"@openstatus/api/src/lambda\";\n\nimport { env } from \"@/env\";\n\nexport async function POST(req: NextRequest) {\n  const payload = await req.text();\n  const signature = req.headers.get(\"Stripe-Signature\");\n  if (!signature) return new Response(\"No signature\", { status: 400 });\n\n  try {\n    const event = stripe.webhooks.constructEvent(\n      payload,\n      signature,\n      env.STRIPE_WEBHOOK_SECRET_KEY,\n    );\n\n    /**\n     * Forward to tRPC API to handle the webhook event\n     */\n    const ctx = await createTRPCContext({ req });\n    const caller = lambdaRouter.createCaller(ctx);\n\n    switch (event.type) {\n      case \"checkout.session.completed\":\n        await caller.stripeRouter.webhooks.sessionCompleted({ event });\n        break;\n      case \"customer.subscription.updated\":\n        await caller.stripeRouter.webhooks.customerSubscriptionUpdated({\n          event,\n        });\n        break;\n      case \"customer.subscription.deleted\":\n        await caller.stripeRouter.webhooks.customerSubscriptionDeleted({\n          event,\n        });\n        break;\n\n      default:\n        throw new Error(`Unhandled event type ${event.type}`);\n    }\n  } catch (error) {\n    if (error instanceof TRPCError) {\n      const errorCode = getHTTPStatusCodeFromError(error);\n      console.error(\"Error in tRPC webhook handler\", error);\n      return new Response(error.message, { status: errorCode });\n    }\n\n    const message = error instanceof Error ? error.message : \"Unknown error\";\n    return new Response(`Webhook Error: ${message}`, {\n      status: 400,\n    });\n  }\n\n  return new Response(null, { status: 200 });\n}\n"
  },
  {
    "path": "apps/web/src/app/global-error.tsx",
    "content": "\"use client\";\n\nimport * as Sentry from \"@sentry/nextjs\";\nimport NextError from \"next/error\";\nimport { useEffect } from \"react\";\n\nexport default function GlobalError({\n  error,\n}: {\n  error: Error & { digest?: string };\n}) {\n  useEffect(() => {\n    Sentry.captureException(error);\n  }, [error]);\n\n  return (\n    <html lang=\"en\">\n      <body>\n        {/* This is the default Next.js error component but it doesn't allow omitting the statusCode property yet. */}\n        {/* biome-ignore lint/suspicious/noExplicitAny: <explanation> */}\n        <NextError statusCode={undefined as any} />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/layout.tsx",
    "content": "import \"@/styles/globals.css\";\n\nimport { OpenPanelComponent } from \"@openpanel/nextjs\";\nimport type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport LocalFont from \"next/font/local\";\n\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { env } from \"@/env\";\nimport {\n  defaultMetadata,\n  ogMetadata,\n  twitterMetadata,\n} from \"@/lib/metadata/shared-metadata\";\nimport { TRPCReactQueryProvider } from \"@/trpc/rq-client\";\nimport { Toaster } from \"@openstatus/ui/components/ui/sonner\";\nimport PlausibleProvider from \"next-plausible\";\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\n\nconst inter = Inter({ subsets: [\"latin\"] });\n\nconst calSans = LocalFont({\n  src: \"../public/fonts/CalSans-SemiBold.ttf\",\n  variable: \"--font-cal\",\n});\n\nexport const metadata: Metadata = {\n  ...defaultMetadata,\n  twitter: {\n    ...twitterMetadata,\n  },\n  openGraph: {\n    ...ogMetadata,\n  },\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body\n        className={`${\n          inter.className\n          // biome-ignore lint/nursery/useSortedClasses: <explanation>\n        } ${calSans.variable}`}\n      >\n        <PlausibleProvider domain=\"openstatus.dev\">\n          <ThemeProvider attribute=\"class\" defaultTheme=\"light\" enableSystem>\n            <NuqsAdapter>\n              <TRPCReactQueryProvider>{children}</TRPCReactQueryProvider>\n            </NuqsAdapter>\n          </ThemeProvider>\n        </PlausibleProvider>\n        {env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID && (\n          <OpenPanelComponent\n            clientId={env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID}\n            trackScreenViews\n            trackOutgoingLinks\n            trackAttributes\n          />\n        )}\n        <Toaster\n          toastOptions={{\n            classNames: {\n              toast:\n                \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg rounded-none!\",\n              description: \"group-[.toast]:text-muted-foreground\",\n              actionButton:\n                \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground rounded-none!\",\n              cancelButton:\n                \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n              closeButton: \"group-[.toast]:text-muted-foreground\",\n            },\n          }}\n          icons={{\n            success: null,\n            error: null,\n            warning: null,\n            info: null,\n            loading: null,\n          }}\n          richColors\n        />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/not-found.tsx",
    "content": "import { CustomMDX } from \"@/content/mdx\";\nimport { getUnrelatedPage } from \"@/content/utils\";\n\nexport default function NotFound() {\n  const page = getUnrelatedPage(\"not-found\");\n  return (\n    <section className=\"prose dark:prose-invert max-w-none\">\n      <h1>{page.metadata.title}</h1>\n      <p className=\"text-lg\">{page.metadata.description}</p>\n      <CustomMDX source={page.content} />\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/app/robots.ts",
    "content": "import type { MetadataRoute } from \"next\";\n\nexport default function robots(): MetadataRoute.Robots {\n  return {\n    rules: {\n      userAgent: \"*\",\n      allow: \"/\",\n    },\n    sitemap: \"https://www.openstatus.dev/sitemap.xml\",\n  };\n}\n"
  },
  {
    "path": "apps/web/src/app/sitemap.ts",
    "content": "import {\n  getBlogPosts,\n  getChangelogPosts,\n  getComparePages,\n  getGuides,\n  getHomePage,\n  getProductPages,\n  getToolsPages,\n  getUnrelatedPages,\n  getUseCasePages,\n} from \"@/content/utils\";\nimport type { MetadataRoute } from \"next\";\n\nconst allPosts = getBlogPosts();\nconst allChangelogs = getChangelogPosts();\nconst allComparisons = getComparePages();\nconst allUnrelated = getUnrelatedPages().filter(\n  (page) => page.slug !== \"not-found\",\n);\nconst allProducts = getProductPages();\nconst allPlaygrounds = getToolsPages().filter(\n  (tool) => tool.slug !== \"checker-slug\",\n);\nconst allGuides = getGuides();\nconst allUseCases = getUseCasePages();\n\nexport default function sitemap(): MetadataRoute.Sitemap {\n  const blogs = allPosts.map((post) => ({\n    url: `https://www.openstatus.dev/blog/${post.slug}`,\n    lastModified: post.metadata.publishedAt, // date format should be YYYY-MM-DD\n    changeFrequency: \"monthly\" as const,\n    priority: 0.7,\n  }));\n\n  const changelogs = allChangelogs.map((post) => ({\n    url: `https://www.openstatus.dev/changelog/${post.slug}`,\n    lastModified: post.metadata.publishedAt, // date format should be YYYY-MM-DD\n    changeFrequency: \"weekly\" as const,\n    priority: 0.6,\n  }));\n\n  const comparisons = allComparisons.map((comparison) => ({\n    url: `https://www.openstatus.dev/compare/${comparison.slug}`,\n    lastModified: comparison.metadata.publishedAt,\n    changeFrequency: \"monthly\" as const,\n    priority: 0.8,\n  }));\n\n  const landings = allUnrelated.map((page) => ({\n    url: `https://www.openstatus.dev/${page.slug}`,\n    lastModified: page.metadata.publishedAt,\n    changeFrequency: \"monthly\" as const,\n    priority: 0.7,\n  }));\n\n  const products = allProducts.map((product) => ({\n    url: `https://www.openstatus.dev/${product.slug}`,\n    lastModified: product.metadata.publishedAt,\n    changeFrequency: \"weekly\" as const,\n    priority: 0.9,\n  }));\n\n  const playgrounds = allPlaygrounds.map((playground) => ({\n    url: `https://www.openstatus.dev/play/${playground.slug}`,\n    lastModified: playground.metadata.publishedAt,\n    changeFrequency: \"monthly\" as const,\n    priority: 0.6,\n  }));\n\n  const guides = allGuides.map((guide) => ({\n    url: `https://www.openstatus.dev/guides/${guide.slug}`,\n    lastModified: guide.metadata.publishedAt,\n    changeFrequency: \"monthly\" as const,\n    priority: 0.6,\n  }));\n\n  const home = [\n    {\n      url: \"https://www.openstatus.dev/\",\n      lastModified: getHomePage().metadata.publishedAt,\n      changeFrequency: \"daily\" as const,\n      priority: 1.0,\n    },\n  ];\n\n  const useCases = allUseCases.map((useCase) => ({\n    url: `https://www.openstatus.dev/use-case/${useCase.slug}`,\n    lastModified: useCase.metadata.publishedAt,\n    changeFrequency: \"monthly\" as const,\n    priority: 0.8,\n  }));\n\n  return [\n    ...home,\n    ...blogs,\n    ...changelogs,\n    ...comparisons,\n    ...landings,\n    ...products,\n    ...playgrounds,\n    ...guides,\n    ...useCases,\n  ];\n}\n"
  },
  {
    "path": "apps/web/src/components/dev-mode-container.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport default function DevModeContainer({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    <div\n      className={cn(\n        \"-m-2 relative rounded-lg border-2 border-destructive/80 p-2\",\n        className,\n      )}\n    >\n      <p className=\"-top-2 absolute left-3 bg-background px-1 font-medium text-destructive text-xs uppercase\">\n        dev mode\n      </p>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/components/icon-cloud-provider.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport { Fly, Koyeb, Railway } from \"@openstatus/icons\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { Globe } from \"lucide-react\";\n\nexport function IconCloudProvider({\n  provider,\n  className,\n}: React.ComponentProps<\"svg\"> & {\n  provider: string;\n}) {\n  switch (provider) {\n    case \"fly\":\n      return <Fly className={cn(\"size-4\", className)} />;\n    case \"koyeb\":\n      return <Koyeb className={cn(\"size-4\", className)} />;\n    case \"railway\":\n      return <Railway className={cn(\"size-4\", className)} />;\n    default:\n      return <Globe className={cn(\"size-4\", className)} />;\n  }\n}\n\nexport function IconCloudProviderTooltip(\n  props: React.ComponentProps<typeof IconCloudProvider>,\n) {\n  return (\n    <TooltipProvider>\n      <Tooltip delayDuration={0}>\n        <TooltipTrigger type=\"button\">\n          <IconCloudProvider {...props} />\n        </TooltipTrigger>\n        <TooltipContent className=\"capitalize\">{props.provider}</TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/components/icons.tsx",
    "content": "import {\n  Activity,\n  AlertTriangle,\n  ArrowLeftRight,\n  Bell,\n  Book,\n  BookOpenCheck,\n  Bot,\n  Calendar,\n  Camera,\n  CandlestickChart,\n  Check,\n  ChevronsLeftRightEllipsis,\n  Clock,\n  Cog,\n  Command,\n  Copy,\n  Cpu,\n  CreditCard,\n  Eye,\n  EyeOff,\n  File,\n  FileClock,\n  FileText,\n  Fingerprint,\n  Gauge,\n  Globe,\n  Hammer,\n  Hourglass,\n  Image,\n  Info,\n  KeyRound,\n  Laptop,\n  LayoutDashboard,\n  Library,\n  LineChart,\n  Link,\n  Linkedin,\n  Megaphone,\n  MessageCircle,\n  Minus,\n  Moon,\n  Network,\n  Newspaper,\n  Package,\n  Palette,\n  PanelTop,\n  Pencil,\n  Play,\n  Plug,\n  Puzzle,\n  Radar,\n  Ratio,\n  Search,\n  SearchCheck,\n  Server,\n  ShieldCheck,\n  Siren,\n  Sparkles,\n  SunMedium,\n  Table,\n  Tag,\n  Terminal,\n  Timer,\n  ToyBrick,\n  Trash,\n  TwitterIcon,\n  UserCircle,\n  Users,\n  Webhook,\n  Workflow,\n  Youtube,\n  Zap,\n} from \"lucide-react\";\nimport type { LucideIcon, LucideProps } from \"lucide-react\";\n\nexport type Icon = LucideIcon;\nexport type IconProps = LucideProps;\nexport type ValidIcon = keyof typeof Icons;\n\nexport const Icons = {\n  activity: Activity,\n  \"layout-dashboard\": LayoutDashboard,\n  link: Link,\n  siren: Siren,\n  \"panel-top\": PanelTop,\n  table: Table,\n  \"toy-brick\": ToyBrick,\n  gauge: Gauge,\n  package: Package,\n  library: Library,\n  cog: Cog,\n  cpu: Cpu,\n  hammer: Hammer,\n  search: Search,\n  \"search-check\": SearchCheck,\n  palette: Palette,\n  fingerprint: Fingerprint,\n  pencil: Pencil,\n  \"message-circle\": MessageCircle,\n  calendar: Calendar,\n  tag: Tag,\n  trash: Trash,\n  twitter: TwitterIcon,\n  terminal: Terminal,\n  globe: Globe,\n  compare: ArrowLeftRight,\n  plug: Plug,\n  copy: Copy,\n  check: Check,\n  play: Play,\n  bot: Bot,\n  puzzle: Puzzle,\n  image: Image,\n  bell: Bell,\n  zap: Zap,\n  eye: Eye,\n  file: File,\n  \"file-text\": FileText,\n  workflow: Workflow,\n  \"eye-off\": EyeOff,\n  network: Network,\n  users: Users,\n  key: KeyRound,\n  \"credit-card\": CreditCard,\n  \"alert-triangle\": AlertTriangle,\n  \"file-clock\": FileClock,\n  megaphone: Megaphone,\n  webhook: Webhook,\n  minus: Minus,\n  sun: SunMedium,\n  moon: Moon,\n  laptop: Laptop,\n  sparkles: Sparkles,\n  timer: Timer,\n  clock: Clock,\n  \"line-chart\": LineChart,\n  linkedin: Linkedin,\n  book: Book,\n  newspaper: Newspaper,\n  youtube: Youtube,\n  \"hour-glass\": Hourglass,\n  \"candlestick-chart\": CandlestickChart,\n  ratio: Ratio,\n  user: UserCircle,\n  camera: Camera,\n  \"book-open-check\": BookOpenCheck,\n  \"shield-check\": ShieldCheck,\n  info: Info,\n  server: Server,\n  command: Command,\n  radar: Radar,\n  \"chevron-left-right-ellipsis\": ChevronsLeftRightEllipsis,\n  discord: ({ ...props }: LucideProps) => (\n    <svg viewBox=\"0 0 640 512\" {...props}>\n      <path\n        fill=\"currentColor\"\n        d=\"M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z\"\n      />\n    </svg>\n  ),\n  google: (props: LucideProps) => (\n    <svg viewBox=\"0 0 24 24\" {...props}>\n      <path\n        fill=\"currentColor\"\n        d=\"M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z\"\n      />\n    </svg>\n  ),\n  github: (props: LucideProps) => (\n    <svg viewBox=\"0 0 438.549 438.549\" {...props}>\n      <path\n        fill=\"currentColor\"\n        d=\"M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z\"\n      />\n    </svg>\n  ),\n  bluesky: (props: LucideProps) => (\n    <svg role=\"img\" viewBox=\"0 0 24 24 \" {...props}>\n      <path\n        fill=\"currentColor\"\n        d=\"M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z\"\n      />\n    </svg>\n  ),\n} as const;\n"
  },
  {
    "path": "apps/web/src/components/loading-animation.tsx",
    "content": "import { cva } from \"class-variance-authority\";\nimport type { VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst loadingVariants = cva(\n  \"animate-pulse rounded-full direction-alternate duration-700\",\n  {\n    variants: {\n      variant: {\n        // we might want to inverse both styles\n        default: \"bg-primary-foreground\",\n        inverse: \"bg-primary\",\n      },\n      size: {\n        default: \"h-1 w-1\",\n        lg: \"h-1.5 w-1.5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\ninterface Props\n  extends React.ComponentProps<\"div\">,\n    VariantProps<typeof loadingVariants> {}\n\nexport function LoadingAnimation({\n  className,\n  variant,\n  size,\n  ...props\n}: Props) {\n  return (\n    <div\n      className={cn(\"flex items-center justify-center gap-1\", className)}\n      {...props}\n    >\n      <div className={cn(loadingVariants({ variant, size }))} />\n      <div className={cn(loadingVariants({ variant, size }), \"delay-150\")} />\n      <div className={cn(loadingVariants({ variant, size }), \"delay-300\")} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/components/tailwind-indicator.tsx",
    "content": "export function TailwindIndicator() {\n  if (process.env.NODE_ENV === \"production\") return null;\n\n  return (\n    <div className=\"fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-white text-xs\">\n      <div className=\"block sm:hidden\">xs</div>\n      <div className=\"hidden sm:block md:hidden\">sm</div>\n      <div className=\"hidden md:block lg:hidden\">md</div>\n      <div className=\"hidden lg:block xl:hidden\">lg</div>\n      <div className=\"hidden xl:block 2xl:hidden\">xl</div>\n      <div className=\"hidden 2xl:block\">2xl</div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport type { ThemeProviderProps } from \"next-themes\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "apps/web/src/config/socials.ts",
    "content": "import type { ValidIcon } from \"@/components/icons\";\n\nexport type Social = {\n  title: string;\n  href: string;\n  icon: ValidIcon;\n};\n\nexport const socialsConfig: Social[] = [\n  {\n    title: \"Discord\",\n    href: \"/discord\",\n    icon: \"discord\",\n  },\n  {\n    title: \"GitHub\",\n    href: \"/github\",\n    icon: \"github\",\n  },\n  {\n    title: \"Bluesky\",\n    href: \"https://bsky.app/profile/openstatus.dev\",\n    icon: \"bluesky\",\n  },\n  {\n    title: \"Twitter\",\n    href: \"/twitter\",\n    icon: \"twitter\",\n  },\n  {\n    title: \"LinkedIn\",\n    href: \"/linkedin\",\n    icon: \"linkedin\",\n  },\n];\n"
  },
  {
    "path": "apps/web/src/content/cmdk.tsx",
    "content": "\"use client\";\n\nimport type { MDXData } from \"@/content/utils\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandList,\n  CommandLoading,\n  CommandSeparator,\n  CommandShortcut,\n} from \"@openstatus/ui/components/ui/command\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n} from \"@openstatus/ui/components/ui/dialog\";\nimport { useDebounce } from \"@openstatus/ui/hooks/use-debounce\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Loader2, Search } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { useRouter } from \"next/navigation\";\nimport * as React from \"react\";\n\ntype ConfigItem = {\n  type: \"item\";\n  label: string;\n  href: string;\n  shortcut?: string;\n};\n\ntype ConfigGroup = {\n  type: \"group\";\n  label: string;\n  heading: string;\n  page: string;\n};\n\ntype ConfigSection = {\n  type: \"group\";\n  heading: string;\n  items: (ConfigItem | ConfigGroup)[];\n};\n\n// TODO: missing shortcuts\nconst CONFIG: ConfigSection[] = [\n  {\n    type: \"group\",\n    heading: \"Resources\",\n    items: [\n      {\n        type: \"group\",\n        label: \"Search in all pages...\",\n        heading: \"All pages\",\n        page: \"all\",\n      },\n      {\n        type: \"item\",\n        label: \"Go to Home\",\n        href: \"/\",\n      },\n      {\n        type: \"item\",\n        label: \"Go to Pricing\",\n        href: \"/pricing\",\n      },\n      {\n        type: \"item\",\n        label: \"Go to Docs\",\n        href: \"https://docs.openstatus.dev\",\n      },\n      {\n        type: \"item\",\n        label: \"Go to Global Speed Checker\",\n        href: \"/play/checker\",\n        shortcut: \"⌘G\",\n      },\n      {\n        type: \"group\",\n        label: \"Search in Products...\",\n        heading: \"Products\",\n        page: \"product\",\n      },\n      {\n        type: \"group\",\n        label: \"Search in Blog...\",\n        heading: \"Blog\",\n        page: \"blog\",\n      },\n      {\n        type: \"group\",\n        label: \"Search in Changelog...\",\n        heading: \"Changelog\",\n        page: \"changelog\",\n      },\n      {\n        type: \"group\",\n        label: \"Search in Tools...\",\n        heading: \"Tools\",\n        page: \"tools\",\n      },\n      {\n        type: \"group\",\n        label: \"Search in Compare...\",\n        heading: \"Compare\",\n        page: \"compare\",\n      },\n      {\n        type: \"group\",\n        label: \"Search in Guides...\",\n        heading: \"Guides\",\n        page: \"guides\",\n      },\n      {\n        type: \"group\",\n        label: \"Search in Use Cases...\",\n        heading: \"Use Cases\",\n        page: \"use-case\",\n      },\n      {\n        type: \"item\",\n        label: \"Go to About\",\n        href: \"/about\",\n      },\n      {\n        type: \"item\",\n        label: \"Book a call\",\n        href: \"/cal\",\n      },\n    ],\n  },\n\n  {\n    type: \"group\",\n    heading: \"Community\",\n    items: [\n      {\n        type: \"item\",\n        label: \"Discord\",\n        href: \"/discord\",\n      },\n      {\n        type: \"item\",\n        label: \"GitHub\",\n        href: \"/github\",\n      },\n      {\n        type: \"item\",\n        label: \"X\",\n        href: \"/twitter\",\n      },\n      {\n        type: \"item\",\n        label: \"BlueSky\",\n        href: \"/bsky\",\n      },\n      {\n        type: \"item\",\n        label: \"YouTube\",\n        href: \"/youtube\",\n      },\n      {\n        type: \"item\",\n        label: \"LinkedIn\",\n        href: \"/linkedin\",\n      },\n    ],\n  },\n];\n\nexport function CmdK() {\n  const [open, setOpen] = React.useState(false);\n  const inputRef = React.useRef<HTMLInputElement | null>(null);\n  const listRef = React.useRef<HTMLDivElement | null>(null);\n  const resetTimerRef = React.useRef<NodeJS.Timeout | undefined>(undefined);\n  const [search, setSearch] = React.useState(\"\");\n  const [pages, setPages] = React.useState<string[]>([]);\n  const debouncedSearch = useDebounce(search, 300);\n  const router = useRouter();\n\n  const page = pages.length > 0 ? pages[pages.length - 1] : null;\n\n  const {\n    data: items = [],\n    isLoading: loading,\n    isFetching: fetching,\n  } = useQuery({\n    queryKey: [\"search\", page, debouncedSearch],\n    queryFn: async () => {\n      if (!page) return [];\n      const searchParams = new URLSearchParams();\n      searchParams.set(\"p\", page);\n      if (debouncedSearch) searchParams.set(\"q\", debouncedSearch);\n      const promise = fetch(`/api/search?${searchParams.toString()}`);\n      // NOTE: artificial delay to avoid flickering\n      const delay = new Promise((r) => setTimeout(r, 300));\n      const [res, _] = await Promise.all([promise, delay]);\n      return res.json();\n    },\n    placeholderData: (previousData) => previousData,\n  });\n\n  const resetSearch = React.useCallback(() => setSearch(\"\"), []);\n\n  React.useEffect(() => {\n    const down = (e: KeyboardEvent) => {\n      if (e.key === \"k\" && (e.metaKey || e.ctrlKey)) {\n        e.preventDefault();\n        setOpen((open) => !open);\n      }\n\n      // Handle shortcuts when dialog is open\n      if (open && (e.metaKey || e.ctrlKey)) {\n        const key = e.key.toLowerCase();\n\n        // Find matching shortcut in CONFIG\n        for (const section of CONFIG) {\n          for (const item of section.items) {\n            if (item.type === \"item\" && item.shortcut) {\n              const shortcutKey = item.shortcut.replace(\"⌘\", \"\").toLowerCase();\n              if (key === shortcutKey) {\n                e.preventDefault();\n                router.push(item.href);\n                setOpen(false);\n                return;\n              }\n            }\n          }\n        }\n      }\n    };\n\n    document.addEventListener(\"keydown\", down);\n    return () => document.removeEventListener(\"keydown\", down);\n  }, [open, router]);\n\n  React.useEffect(() => {\n    inputRef.current?.focus();\n  }, []);\n\n  // NOTE: Reset search and pages after dialog closes (with delay for animation)\n  // - if within 1 second of closing, the dialog will not reset\n  React.useEffect(() => {\n    const DELAY = 1000;\n\n    if (!open && items.length > 0) {\n      if (resetTimerRef.current) {\n        clearTimeout(resetTimerRef.current);\n      }\n      resetTimerRef.current = setTimeout(() => {\n        setSearch(\"\");\n        setPages([]);\n      }, DELAY);\n    }\n\n    if (open && resetTimerRef.current) {\n      clearTimeout(resetTimerRef.current);\n      resetTimerRef.current = undefined;\n    }\n\n    return () => {\n      if (resetTimerRef.current) {\n        clearTimeout(resetTimerRef.current);\n      }\n    };\n  }, [open, items.length]);\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        className={cn(\n          \"flex w-full items-center text-left hover:bg-muted\",\n          open && \"bg-muted!\",\n        )}\n        onClick={() => setOpen(true)}\n      >\n        <span className=\"truncate text-muted-foreground\">\n          Search<span className=\"text-xs\">...</span>\n        </span>\n        <kbd className=\"pointer-events-none ml-auto inline-flex h-5 select-none items-center gap-1 border bg-muted px-1.5 font-medium font-mono text-[10px] text-muted-foreground opacity-100\">\n          <span className=\"text-xs\">⌘</span>K\n        </kbd>\n      </button>\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogContent className=\"top-[15%] translate-y-0 overflow-hidden rounded-none p-0 font-mono shadow-2xl\">\n          <DialogTitle className=\"sr-only\">Search</DialogTitle>\n          <Command\n            onKeyDown={(e) => {\n              // e.key === \"Escape\" ||\n              if (e.key === \"Backspace\" && !search) {\n                e.preventDefault();\n                setPages((pages) => pages.slice(0, -1));\n              }\n            }}\n            shouldFilter={!page}\n            className=\"rounded-none\"\n          >\n            <div\n              className=\"flex items-center border-b px-3\"\n              cmdk-input-wrapper=\"\"\n            >\n              {loading || fetching ? (\n                <Loader2 className=\"mr-2 h-4 w-4 shrink-0 animate-spin opacity-50\" />\n              ) : (\n                <Search className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n              )}\n              <CommandPrimitive.Input\n                className=\"flex h-11 w-full rounded-none bg-transparent py-3 text-sm outline-hidden placeholder:text-foreground-muted disabled:cursor-not-allowed disabled:opacity-50\"\n                placeholder=\"Type to search…\"\n                value={search}\n                onValueChange={setSearch}\n              />\n            </div>\n            <CommandList ref={listRef} className=\"[&_[cmdk-item]]:rounded-none\">\n              {(loading || fetching) && page && !items.length ? (\n                <CommandLoading>Searching...</CommandLoading>\n              ) : null}\n              {!(loading || fetching) ? (\n                <CommandEmpty>No results found.</CommandEmpty>\n              ) : null}\n              {!page ? (\n                <Home\n                  setPages={setPages}\n                  resetSearch={resetSearch}\n                  setOpen={setOpen}\n                />\n              ) : null}\n              {items.length > 0 ? (\n                <SearchResults\n                  items={items}\n                  search={search}\n                  setOpen={setOpen}\n                  page={page}\n                />\n              ) : null}\n            </CommandList>\n          </Command>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n\nfunction Home({\n  setPages,\n  resetSearch,\n  setOpen,\n}: {\n  setPages: React.Dispatch<React.SetStateAction<string[]>>;\n  resetSearch: () => void;\n  setOpen: React.Dispatch<React.SetStateAction<boolean>>;\n}) {\n  const router = useRouter();\n  const { resolvedTheme, setTheme } = useTheme();\n\n  return (\n    <>\n      {CONFIG.map((group, groupIndex) => (\n        <React.Fragment key={group.heading}>\n          {groupIndex > 0 && <CommandSeparator />}\n          <CommandGroup heading={group.heading}>\n            {group.items.map((item) => {\n              if (item.type === \"item\") {\n                return (\n                  <CommandItem\n                    key={item.label}\n                    onSelect={() => {\n                      router.push(item.href);\n                      setOpen(false);\n                    }}\n                  >\n                    <span>{item.label}</span>\n                    {item.shortcut && (\n                      <CommandShortcut>{item.shortcut}</CommandShortcut>\n                    )}\n                  </CommandItem>\n                );\n              }\n              if (item.type === \"group\") {\n                return (\n                  <CommandItem\n                    key={item.page}\n                    onSelect={() => {\n                      setPages((pages) => [...pages, item.page]);\n                      resetSearch();\n                    }}\n                  >\n                    <span>{item.label}</span>\n                  </CommandItem>\n                );\n              }\n              return null;\n            })}\n          </CommandGroup>\n        </React.Fragment>\n      ))}\n      <CommandSeparator />\n      <CommandGroup heading=\"Settings\">\n        <CommandItem\n          onSelect={() => setTheme(resolvedTheme === \"dark\" ? \"light\" : \"dark\")}\n        >\n          <span>\n            Switch to {resolvedTheme === \"dark\" ? \"light\" : \"dark\"} theme\n          </span>\n        </CommandItem>\n      </CommandGroup>\n    </>\n  );\n}\n\nfunction SearchResults({\n  items,\n  search,\n  setOpen,\n  page,\n}: {\n  items: MDXData[];\n  search: string;\n  setOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  page: string | null;\n}) {\n  const router = useRouter();\n\n  const _page = CONFIG[0].items.find(\n    (item) => item.type === \"group\" && item.page === page,\n  ) as ConfigGroup | undefined;\n\n  return (\n    <CommandGroup heading={_page?.heading ?? \"Search Results\"}>\n      {items.map((item) => {\n        // Highlight search term match in the title, case-insensitive\n        const title = item.metadata.title.replace(\n          new RegExp(search.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"i\"),\n          (match) => `<mark>${match}</mark>`,\n        );\n        const html = item.content.replace(\n          new RegExp(search.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"i\"),\n          (match) => `<mark>${match}</mark>`,\n        );\n\n        return (\n          <CommandItem\n            key={item.slug}\n            keywords={[item.metadata.title, item.content, search]}\n            onSelect={() => {\n              router.push(item.href);\n              setOpen(false);\n            }}\n          >\n            <div className=\"grid min-w-0\">\n              <span\n                className=\"block truncate\"\n                // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n                dangerouslySetInnerHTML={{ __html: title }}\n              />\n              {item.content && search ? (\n                <span\n                  className=\"block truncate text-muted-foreground text-xs\"\n                  // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n                  dangerouslySetInnerHTML={{ __html: html }}\n                />\n              ) : null}\n            </div>\n          </CommandItem>\n        );\n      })}\n    </CommandGroup>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/component-highlighter.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\n/**\n * Mapping of data-slot values to component names for display\n */\nconst DATA_SLOT_TO_COMPONENT: Record<string, string> = {\n  // Layout components\n  status: \"Status\",\n  \"status-header\": \"StatusHeader\",\n  \"status-title\": \"StatusTitle\",\n  \"status-description\": \"StatusDescription\",\n  \"status-content\": \"StatusContent\",\n\n  // Banner components\n  \"status-banner\": \"StatusBanner\",\n\n  // Monitor components\n  \"status-component\": \"StatusComponent\",\n  \"status-component-header\": \"StatusComponentHeader\",\n  \"status-component-header-left\": \"StatusComponentHeaderLeft\",\n  \"status-component-header-right\": \"StatusComponentHeaderRight\",\n  \"status-component-body\": \"StatusComponentBody\",\n  \"status-component-title\": \"StatusComponentTitle\",\n  \"status-component-uptime\": \"StatusComponentUptime\",\n  \"status-component-status\": \"StatusComponentStatus\",\n  \"status-component-footer\": \"StatusComponentFooter\",\n\n  // Group component\n  \"status-component-group\": \"StatusComponentGroup\",\n  \"status-component-group-trigger\": \"StatusComponentGroupTrigger\",\n  \"status-component-group-content\": \"StatusComponentGroupContent\",\n\n  // Feed component\n  \"status-feed\": \"StatusFeed\",\n\n  // Event components\n  \"status-event-group\": \"StatusEventGroup\",\n  \"status-event\": \"StatusEvent\",\n  \"status-event-content\": \"StatusEventContent\",\n  \"status-event-title\": \"StatusEventTitle\",\n  \"status-event-title-check\": \"StatusEventTitleCheck\",\n  \"status-event-affected\": \"StatusEventAffected\",\n  \"status-event-affected-badge\": \"StatusEventAffectedBadge\",\n  \"status-event-date\": \"StatusEventDate\",\n  \"status-event-date-badge\": \"StatusEventDateBadge\",\n  \"status-event-aside\": \"StatusEventAside\",\n  \"status-event-timeline-report\": \"StatusEventTimelineReport\",\n  \"status-event-timeline-report-update\": \"StatusEventTimelineReportUpdate\",\n  \"status-event-timeline-maintenance\": \"StatusEventTimelineMaintenance\",\n  \"status-event-timeline-title\": \"StatusEventTimelineTitle\",\n  \"status-event-timeline-message\": \"StatusEventTimelineMessage\",\n  \"status-event-timeline-dot\": \"StatusEventTimelineDot\",\n  \"status-event-timeline-separator\": \"StatusEventTimelineSeparator\",\n\n  // Bar components\n  \"status-bar\": \"StatusBar\",\n  \"status-bar-item\": \"StatusBarItem\",\n  \"status-bar-card\": \"StatusBarCard\",\n  \"status-bar-content\": \"StatusBarContent\",\n  \"status-bar-event\": \"StatusBarEvent\",\n\n  // Utility components\n  \"status-timestamp\": \"StatusTimestamp\",\n  \"status-timestamp-content\": \"StatusTimestampContent\",\n  \"status-timestamp-row\": \"StatusTimestampRow\",\n};\n\n/**\n * Find the nearest element with a data-slot attribute by traversing up the DOM tree\n */\nfunction findNearestSlot(target: HTMLElement, root: HTMLElement) {\n  let el: HTMLElement | null = target;\n  while (el && el !== root) {\n    const slot = el.dataset.slot;\n    if (slot) {\n      const componentName = DATA_SLOT_TO_COMPONENT[slot] || slot;\n      return { element: el, name: componentName };\n    }\n    el = el.parentElement;\n  }\n  return null;\n}\n\n/**\n * ComponentHighlighter\n *\n * Highlights OpenStatus components on hover by detecting their data-slot attributes.\n * Uses DOM inspection to find components, which works reliably at any nesting level.\n *\n * @param enabled - Control the highlighter state externally (optional)\n * @param children - The component tree to enable highlighting for\n *\n * @example\n * ```tsx\n * <ComponentHighlighter>\n *   <StatusPageExample />\n * </ComponentHighlighter>\n * ```\n */\nexport function ComponentHighlighter({\n  enabled = true,\n  children,\n}: {\n  enabled?: boolean;\n  shortcut?: boolean;\n  children: React.ReactNode;\n}) {\n  const rootRef = useRef<HTMLDivElement>(null);\n\n  const [hovered, setHovered] = useState<{\n    rect: DOMRect;\n    name: string;\n  } | null>(null);\n\n  // Find and highlight the nearest component with data-slot on mouse move\n  const handleMouseMove = useCallback(\n    (e: React.MouseEvent) => {\n      if (!enabled || !rootRef.current) return;\n      const match = findNearestSlot(e.target as HTMLElement, rootRef.current);\n      if (match) {\n        setHovered({\n          rect: match.element.getBoundingClientRect(),\n          name: match.name,\n        });\n      } else {\n        setHovered(null);\n      }\n    },\n    [enabled],\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      onMouseMove={enabled ? handleMouseMove : undefined}\n      onMouseLeave={enabled ? () => setHovered(null) : undefined}\n    >\n      {children}\n      {hovered && (\n        <div\n          className=\"pointer-events-none fixed z-[99999] bg-info/5 outline-2 outline-info transition-all duration-100 ease-in-out\"\n          style={{\n            top: hovered.rect.top,\n            left: hovered.rect.left,\n            width: hovered.rect.width,\n            height: hovered.rect.height,\n          }}\n        >\n          <span className=\"-left-px -top-px absolute whitespace-nowrap bg-info px-2 py-0.5 font-mono font-semibold text-[11px] text-white leading-tight\">\n            {hovered.name}\n          </span>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/convert.ts",
    "content": "import type { MDXData } from \"./utils\";\nimport { formatDate } from \"./utils\";\n\n/**\n * Converts MDX content to clean markdown format for AI tools\n * Handles YAML frontmatter generation and component serialization\n */\nexport function convertMdxToMarkdown(data: MDXData): string {\n  let output = \"\";\n\n  // Step 0: Generate YAML frontmatter from metadata\n  const { metadata, content } = data;\n\n  output += \"---\\n\";\n  output += `title: \"${metadata.title}\"\\n`;\n  if (metadata.publishedAt) {\n    output += `date: ${formatDate(metadata.publishedAt)}\\n`;\n  }\n  if (metadata.author) {\n    output += `author: \"${metadata.author}\"\\n`;\n  }\n  if (metadata.description) {\n    output += `description: \"${metadata.description}\"\\n`;\n  }\n  if (metadata.category) {\n    output += `category: \"${metadata.category}\"\\n`;\n  }\n  if (metadata.image) {\n    output += `image: \"${metadata.image}\"\\n`;\n  }\n  output += \"---\\n\\n\";\n\n  // Step 1: Strip import statements (confirmed: always at top of file)\n  let markdown = content.replace(/^import\\s+.*$/gm, \"\");\n\n  // Step 2: Convert <ButtonLink> to markdown links\n  markdown = markdown.replace(\n    /<ButtonLink\\s+href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/ButtonLink>/g,\n    (_match, href, children) => {\n      // Extract plain text from children (remove any nested tags)\n      const text = children.replace(/<[^>]*>/g, \"\").trim();\n      return `[${text}](${href})`;\n    },\n  );\n\n  // Step 3: Convert <Image> to markdown images\n  // Handle with alt attribute\n  markdown = markdown.replace(\n    /<Image\\s+(?:[^>]*\\s+)?src=\"([^\"]*)\"(?:[^>]*\\s+)?alt=\"([^\"]*)\"[^>]*\\/?>/g,\n    (_match, src, alt) => {\n      return `![${alt}](${src})`;\n    },\n  );\n  // Handle without alt attribute\n  markdown = markdown.replace(\n    /<Image\\s+(?:[^>]*\\s+)?src=\"([^\"]*)\"[^>]*\\/?>/g,\n    (_match, src) => {\n      return `![](${src})`;\n    },\n  );\n\n  // Step 4: Replace <Table> components with a note to use GFM tables\n  // TODO: Convert Table components in MDX source files to GFM tables\n  markdown = markdown.replace(/<Table\\s+data=\\{[\\s\\S]*?\\}\\s*\\/>/g, () => {\n    return \"<!-- Table: Please convert to GFM markdown table format for better AI tool support -->\";\n  });\n\n  // Step 5: Convert <Details> to native HTML <details>/<summary> (always closed)\n  markdown = markdown.replace(\n    /<Details\\s+summary=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/Details>/g,\n    (_match, summary, content) => {\n      return `<details>\\n  <summary>${summary}</summary>\\n  ${content.trim()}\\n</details>`;\n    },\n  );\n\n  // Step 6: Replace interactive components with HTML comments\n  markdown = markdown.replace(\n    /<Tweet[^>]*(?:\\/>|>[\\s\\S]*?<\\/Tweet>)/g,\n    \"<!-- Tweet omitted -->\",\n  );\n  markdown = markdown.replace(\n    /<StatusPageExample[^>]*(?:\\/>|>[\\s\\S]*?<\\/StatusPageExample>)/g,\n    \"<!-- StatusPageExample omitted -->\",\n  );\n  markdown = markdown.replace(\n    /<SimpleChart[^>]*(?:\\/>|>[\\s\\S]*?<\\/SimpleChart>)/g,\n    \"<!-- SimpleChart omitted -->\",\n  );\n\n  // Step 7: Extract text from <Grid> containers (remove Grid wrapper, keep content)\n  markdown = markdown.replace(\n    /<Grid[^>]*>([\\s\\S]*?)<\\/Grid>/g,\n    (_match, content) => {\n      return content;\n    },\n  );\n\n  // Step 8: Strip generic HTML containers but extract their text\n  // This handles div, span, section, article\n  markdown = markdown.replace(\n    /<(div|span|section|article)(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>/g,\n    (match) => {\n      // Extract text content (remove all tags)\n      return match.replace(/<[^>]*>/g, \" \").trim();\n    },\n  );\n\n  // Step 9: Strip remaining unknown JSX components\n  // Handle paired tags\n  markdown = markdown.replace(/<[A-Z]\\w*[^>]*>[\\s\\S]*?<\\/[A-Z]\\w*>/g, \"\");\n  // Handle self-closing tags\n  markdown = markdown.replace(/<[A-Z]\\w*\\s*\\/>/g, \"\");\n\n  // Clean up: Remove excessive blank lines (more than 2 consecutive)\n  markdown = markdown.replace(/\\n{3,}/g, \"\\n\\n\");\n\n  // Trim whitespace\n  markdown = markdown.trim();\n\n  output += markdown;\n  return output;\n}\n"
  },
  {
    "path": "apps/web/src/content/copy-button.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { ButtonGroup } from \"@openstatus/ui/components/ui/button-group\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { toast } from \"sonner\";\n\nexport function CopyButton({\n  className,\n  copyText,\n  buttonText = \"copy\",\n  copiedText = \"copied\",\n  ...props\n}: React.ComponentProps<typeof Button> & {\n  copyText: string;\n  buttonText?: string;\n  copiedText?: string;\n}) {\n  const { copy, isCopied } = useCopyToClipboard();\n\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"lg\"\n      className={cn(\"rounded-none p-4\", className)}\n      onClick={() => copy(copyText, { withToast: true })}\n      {...props}\n    >\n      {isCopied ? `[${copiedText}]` : `[${buttonText}]`}\n    </Button>\n  );\n}\n\nexport function CopyDropdownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof ButtonGroup>) {\n  const { copy, isCopied } = useCopyToClipboard();\n\n  const handleCopyLink = () => {\n    copy(window.location.href, {\n      successMessage: \"Link copied to clipboard\",\n      withToast: true,\n    });\n  };\n\n  const handleCopyMarkdown = async () => {\n    try {\n      const item = new ClipboardItem({\n        \"text/plain\": fetch(window.location.pathname, {\n          headers: { Accept: \"text/markdown\" },\n        })\n          .then((r) => {\n            if (!r.ok) throw new Error(\"Failed to fetch markdown\");\n            return r.text();\n          })\n          .then((text) => new Blob([text], { type: \"text/plain\" })),\n      });\n\n      await navigator.clipboard.write([item]);\n      toast.success(\"Markdown copied to clipboard\");\n    } catch (_error) {\n      toast.error(\"Failed to copy markdown\");\n    }\n  };\n\n  return (\n    <ButtonGroup\n      className={cn(\"rounded-none border-none p-4\", className)}\n      {...props}\n    >\n      <Button\n        variant=\"ghost\"\n        className=\"rounded-none p-4\"\n        onClick={handleCopyLink}\n      >\n        {isCopied ? \"[link copied]\" : \"[copy link]\"}\n      </Button>\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button\n            variant=\"ghost\"\n            className=\"group rounded-none\"\n            aria-label=\"Copy dropdown\"\n          >\n            <span\n              className=\"relative top-[1px] shrink-0 origin-center text-[10px] text-muted-foreground transition duration-300 group-hover:text-foreground group-data-[state=open]:rotate-180 group-data-[state=open]:text-foreground\"\n              aria-hidden=\"true\"\n            >\n              ▲\n            </span>\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent\n          align=\"end\"\n          className=\"min-w-[var(--radix-dropdown-menu-trigger-width)] rounded-none\"\n          alignOffset={0}\n          sideOffset={0}\n        >\n          <DropdownMenuGroup>\n            <DropdownMenuItem\n              className=\"rounded-none font-mono\"\n              onClick={handleCopyMarkdown}\n            >\n              [copy markdown]\n            </DropdownMenuItem>\n          </DropdownMenuGroup>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </ButtonGroup>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/footer-status.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { StatusResponse } from \"@openstatus/react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Link from \"next/link\";\n\nexport function FooterStatus() {\n  const { data } = useQuery({\n    queryKey: [\"footer-status\"],\n    queryFn: async () => {\n      const res = await fetch(\n        \"https://api.openstatus.dev/public/status/status\",\n      );\n      if (!res.ok) return { status: \"unknown\" as const };\n      return (await res.json()) as StatusResponse;\n    },\n    refetchInterval: 60000, // Refetch every 60 seconds\n    staleTime: 60000,\n  });\n\n  const status = data?.status ?? \"unknown\";\n\n  return (\n    <Link\n      href=\"https://status.openstatus.dev\"\n      className={cn(\n        \"flex w-full items-center gap-2 p-4 hover:bg-muted\",\n        STATUS[status].color,\n      )}\n    >\n      {STATUS[status].label}\n    </Link>\n  );\n}\n\nconst STATUS = {\n  operational: {\n    color: \"text-green-700 dark:text-green-400\",\n    label: \"Operational\",\n  },\n  degraded_performance: {\n    color: \"text-yellow-700 dark:text-yellow-400\",\n    label: \"Degraded Performance\",\n  },\n  partial_outage: {\n    color: \"text-yellow-700 dark:text-yellow-400\",\n    label: \"Partial Outage\",\n  },\n  major_outage: {\n    color: \"text-red-700 dark:text-red-400\",\n    label: \"Major Outage\",\n  },\n  under_maintenance: {\n    color: \"text-blue-700 dark:text-blue-400\",\n    label: \"Under Maintenance\",\n  },\n  unknown: { color: \"text-gray-700 dark:text-gray-400\", label: \"Unknown\" },\n  incident: {\n    color: \"text-yellow-700 dark:text-yellow-400\",\n    label: \"Incident\",\n  },\n} satisfies Record<StatusResponse[\"status\"], { color: string; label: string }>;\n"
  },
  {
    "path": "apps/web/src/content/footer.tsx",
    "content": "import { Link } from \"@/content/link\";\nimport { ThemeToggle } from \"@/content/theme-toggle\";\nimport { footerLinks } from \"@/data/content\";\nimport { FooterStatus } from \"./footer-status\";\n\nexport function Footer() {\n  return (\n    <footer>\n      <div className=\"grid grid-cols-1 gap-px border border-border bg-border sm:grid-cols-2 md:grid-cols-3 [&>div]:bg-background [&>div]:p-4\">\n        {footerLinks.map((section) => (\n          <div key={section.label}>\n            <p className=\"text-muted-foreground\">{section.label}</p>\n            <ul>\n              {section.items.map((item) => (\n                <li key={item.href}>\n                  <Link\n                    href={item.href}\n                    className=\"block w-full truncate hover:bg-muted\"\n                  >\n                    {item.label}\n                  </Link>\n                </li>\n              ))}\n            </ul>\n          </div>\n        ))}\n      </div>\n      <div className=\"grid gap-px border border-border border-t-0 bg-border sm:grid-cols-2 md:grid-cols-3 [&>*]:bg-background\">\n        <Link\n          href=\"/cal\"\n          className=\"flex w-full items-center gap-2 p-4 hover:bg-muted\"\n        >\n          Talk to the founders\n        </Link>\n        <div>\n          <FooterStatus />\n        </div>\n        <div className=\"sm:col-span-2 md:col-span-1\">\n          <ThemeToggle className=\"rounded-none\" />\n        </div>\n      </div>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/header.tsx",
    "content": "import { headerLinks } from \"@/data/content\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@openstatus/ui/components/ui/dropdown-menu\";\nimport Link from \"next/link\";\nimport { CmdK } from \"./cmdk\";\nimport { LogoWithContextMenu } from \"./logo-with-context-menu\";\n\nexport function Header() {\n  return (\n    <header className=\"grid grid-cols-3 gap-px border border-border bg-border lg:grid-cols-6 [&>*]:bg-background [&>*]:px-4 [&>*]:py-4 [&>*]:hover:bg-muted\">\n      <LogoWithContextMenu />\n      {headerLinks.map((section, _) => (\n        <DropdownMenu key={section.label}>\n          <DropdownMenuTrigger className=\"group flex items-center gap-1 data-[state=open]:bg-muted\">\n            <span className=\"w-full truncate text-left\">{section.label}</span>\n            <span\n              className=\"relative top-[1px] shrink-0 origin-center text-[10px] text-muted-foreground transition duration-300 group-hover:text-foreground group-data-[state=open]:rotate-180 group-data-[state=open]:text-foreground\"\n              aria-hidden=\"true\"\n            >\n              ▲\n            </span>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent\n            align=\"start\"\n            className=\"min-w-[var(--radix-dropdown-menu-trigger-width)] rounded-none\"\n            alignOffset={0}\n            sideOffset={0}\n          >\n            {section.items.map((item) => (\n              <DropdownMenuItem\n                key={item.href}\n                className=\"rounded-none px-2 py-3 font-mono\"\n                asChild\n              >\n                <Link href={item.href}>{item.label}</Link>\n              </DropdownMenuItem>\n            ))}\n          </DropdownMenuContent>\n        </DropdownMenu>\n      ))}\n      <Link href=\"/pricing\" className=\"truncate\">\n        Pricing\n      </Link>\n      <CmdK />\n      <Link\n        href=\"https://app.openstatus.dev/login\"\n        className=\"truncate text-blue-700 dark:text-blue-400\"\n      >\n        Dashboard\n      </Link>\n    </header>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/highlight-text.tsx",
    "content": "\"use client\";\n\nimport { useSearchParams } from \"next/navigation\";\nimport { useEffect, useRef } from \"react\";\n\nfunction highlight(root: HTMLElement, query: string) {\n  if (!query) return;\n\n  const regex = new RegExp(`(${query})`, \"gi\");\n  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);\n\n  const textNodes: Text[] = [];\n  let node = walker.nextNode();\n\n  while (node) {\n    if (node instanceof Text) {\n      textNodes.push(node);\n    }\n    node = walker.nextNode();\n  }\n\n  for (const textNode of textNodes) {\n    const nodeValue = textNode.nodeValue;\n    if (!nodeValue || !regex.test(nodeValue)) continue;\n\n    const span = document.createElement(\"span\");\n    span.innerHTML = nodeValue.replace(regex, \"<mark>$1</mark>\");\n\n    textNode.parentNode?.replaceChild(span, textNode);\n  }\n}\n\nexport function HighlightText({ children }: { children: React.ReactNode }) {\n  const ref = useRef<HTMLDivElement>(null);\n  const searchParams = useSearchParams();\n  const q = searchParams.get(\"q\");\n\n  useEffect(() => {\n    if (ref.current && q) {\n      highlight(ref.current, q);\n    }\n  }, [q]);\n\n  return <div ref={ref}>{children}</div>;\n}\n"
  },
  {
    "path": "apps/web/src/content/image-zoom.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useTheme } from \"next-themes\";\nimport Image from \"next/image\";\nimport type { ImageProps } from \"next/image\";\nimport { useEffect, useState } from \"react\";\nimport Zoom, {\n  type ControlledProps,\n  type UncontrolledProps,\n} from \"react-medium-image-zoom\";\n\nexport type ImageZoomProps = UncontrolledProps & {\n  isZoomed?: ControlledProps[\"isZoomed\"];\n  onZoomChange?: ControlledProps[\"onZoomChange\"];\n  className?: string;\n  backdropClassName?: string;\n  \"aria-hidden\"?: boolean;\n  inert?: true;\n};\n\nexport const ImageZoom = ({\n  className,\n  backdropClassName,\n  \"aria-hidden\": ariaHidden,\n  inert,\n  ...props\n}: ImageZoomProps) => (\n  <div\n    aria-hidden={ariaHidden}\n    inert={inert}\n    className={cn(\n      \"relative\",\n      \"[&_[data-rmiz-ghost]]:pointer-events-none [&_[data-rmiz-ghost]]:absolute\",\n      \"[&_[data-rmiz-btn-zoom]]:m-0 [&_[data-rmiz-btn-zoom]]:size-10 [&_[data-rmiz-btn-zoom]]:touch-manipulation [&_[data-rmiz-btn-zoom]]:appearance-none [&_[data-rmiz-btn-zoom]]:rounded-[50%] [&_[data-rmiz-btn-zoom]]:border-none [&_[data-rmiz-btn-zoom]]:bg-foreground/70 [&_[data-rmiz-btn-zoom]]:p-2 [&_[data-rmiz-btn-zoom]]:text-background [&_[data-rmiz-btn-zoom]]:outline-offset-2\",\n      \"[&_[data-rmiz-btn-unzoom]]:m-0 [&_[data-rmiz-btn-unzoom]]:size-10 [&_[data-rmiz-btn-unzoom]]:touch-manipulation [&_[data-rmiz-btn-unzoom]]:appearance-none [&_[data-rmiz-btn-unzoom]]:rounded-[50%] [&_[data-rmiz-btn-unzoom]]:border-none [&_[data-rmiz-btn-unzoom]]:bg-foreground/70 [&_[data-rmiz-btn-unzoom]]:p-2 [&_[data-rmiz-btn-unzoom]]:text-background [&_[data-rmiz-btn-unzoom]]:outline-offset-2\",\n      \"[&_[data-rmiz-btn-zoom]:not(:focus):not(:active)]:pointer-events-none [&_[data-rmiz-btn-zoom]:not(:focus):not(:active)]:absolute [&_[data-rmiz-btn-zoom]:not(:focus):not(:active)]:size-px [&_[data-rmiz-btn-zoom]:not(:focus):not(:active)]:overflow-hidden [&_[data-rmiz-btn-zoom]:not(:focus):not(:active)]:whitespace-nowrap [&_[data-rmiz-btn-zoom]:not(:focus):not(:active)]:[clip-path:inset(50%)] [&_[data-rmiz-btn-zoom]:not(:focus):not(:active)]:[clip:rect(0_0_0_0)]\",\n      \"[&_[data-rmiz-btn-zoom]]:absolute [&_[data-rmiz-btn-zoom]]:top-2.5 [&_[data-rmiz-btn-zoom]]:right-2.5 [&_[data-rmiz-btn-zoom]]:bottom-auto [&_[data-rmiz-btn-zoom]]:left-auto [&_[data-rmiz-btn-zoom]]:cursor-zoom-in\",\n      \"[&_[data-rmiz-btn-unzoom]]:absolute [&_[data-rmiz-btn-unzoom]]:top-5 [&_[data-rmiz-btn-unzoom]]:right-5 [&_[data-rmiz-btn-unzoom]]:bottom-auto [&_[data-rmiz-btn-unzoom]]:left-auto [&_[data-rmiz-btn-unzoom]]:z-[1] [&_[data-rmiz-btn-unzoom]]:cursor-zoom-out\",\n      '[&_[data-rmiz-content=\"found\"]_img]:cursor-zoom-in',\n      '[&_[data-rmiz-content=\"found\"]_svg]:cursor-zoom-in',\n      '[&_[data-rmiz-content=\"found\"]_[role=\"img\"]]:cursor-zoom-in',\n      '[&_[data-rmiz-content=\"found\"]_[data-zoom]]:cursor-zoom-in',\n      className,\n    )}\n  >\n    <Zoom\n      classDialog={cn(\n        \"[&::backdrop]:hidden\",\n        \"[&[open]]:fixed [&[open]]:m-0 [&[open]]:h-dvh [&[open]]:max-h-none [&[open]]:w-dvw [&[open]]:max-w-none [&[open]]:overflow-hidden [&[open]]:border-0 [&[open]]:bg-transparent [&[open]]:p-0\",\n        \"[&_[data-rmiz-modal-overlay]]:absolute [&_[data-rmiz-modal-overlay]]:inset-0 [&_[data-rmiz-modal-overlay]]:transition-all\",\n        '[&_[data-rmiz-modal-overlay=\"hidden\"]]:bg-transparent',\n        '[&_[data-rmiz-modal-overlay=\"visible\"]]:bg-background/80 [&_[data-rmiz-modal-overlay=\"visible\"]]:backdrop-blur-md',\n        \"[&_[data-rmiz-modal-content]]:relative [&_[data-rmiz-modal-content]]:size-full\",\n        \"[&_[data-rmiz-modal-img]]:absolute [&_[data-rmiz-modal-img]]:origin-top-left [&_[data-rmiz-modal-img]]:cursor-zoom-out [&_[data-rmiz-modal-img]]:transition-transform\",\n        \"motion-reduce:[&_[data-rmiz-modal-img]]:transition-none motion-reduce:[&_[data-rmiz-modal-overlay]]:transition-none\",\n        backdropClassName,\n      )}\n      {...props}\n    />\n  </div>\n);\n\ninterface ZoomableImageProps extends ImageProps {\n  darkSrc?: string;\n  imageWidth: number;\n  imageHeight: number;\n}\n\nexport function ZoomableImage({\n  src,\n  alt,\n  className,\n  darkSrc,\n  imageWidth,\n  imageHeight,\n  ...rest\n}: ZoomableImageProps) {\n  const { resolvedTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n  const isDark = resolvedTheme === \"dark\";\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  return (\n    <figure>\n      <ImageZoom\n        backdropClassName={cn(\n          '[&_[data-rmiz-modal-overlay=\"visible\"]]:bg-black/80',\n        )}\n        zoomMargin={16}\n        aria-hidden={mounted ? isDark : undefined}\n        inert={mounted && isDark ? true : undefined}\n        className=\"block dark:hidden\"\n      >\n        <Image\n          {...rest}\n          src={src}\n          alt={alt ?? \"\"}\n          width={imageWidth}\n          height={imageHeight}\n          sizes=\"100vw\"\n          style={{ width: \"100%\", height: \"auto\" }}\n          className={className}\n        />\n      </ImageZoom>\n      <ImageZoom\n        backdropClassName={cn(\n          '[&_[data-rmiz-modal-overlay=\"visible\"]]:bg-black/80',\n        )}\n        zoomMargin={16}\n        aria-hidden={mounted ? !isDark : undefined}\n        inert={mounted && !isDark ? true : undefined}\n        className=\"hidden dark:block\"\n      >\n        <Image\n          {...rest}\n          src={darkSrc ?? src}\n          alt={alt ?? \"\"}\n          width={imageWidth}\n          height={imageHeight}\n          sizes=\"100vw\"\n          style={{ width: \"100%\", height: \"auto\" }}\n          className={className}\n        />\n      </ImageZoom>\n      {alt && <figcaption>{alt}</figcaption>}\n    </figure>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/latency-chart-table.tsx",
    "content": "import fs from \"node:fs\";\nimport type { RegionMetricsChartTable } from \"@/data/content\";\nimport { type Region, regionDict } from \"@openstatus/regions\";\nimport { SimpleChart } from \"./simple-chart\";\n\nexport interface LatencyChartTableProps {\n  staticFile: string;\n  caption?: string;\n}\n\ninterface LatencyChartTableData {\n  regions: Region[];\n  data: {\n    regions: Region[];\n    data: (Partial<Record<Region, number>> & { timestamp: string })[];\n  };\n  metricsByRegion: RegionMetricsChartTable[];\n}\n\nexport function LatencyChartTable({\n  staticFile,\n  caption = \"A list of all the selected regions.\",\n}: LatencyChartTableProps) {\n  const data = JSON.parse(\n    fs.readFileSync(`${process.cwd()}/public${staticFile}`, \"utf8\"),\n  ) as LatencyChartTableData;\n  const { regions, data: chartData, metricsByRegion } = data;\n\n  return (\n    <div className=\"table-wrapper\">\n      <table>\n        <caption>{caption}</caption>\n        <thead>\n          <tr>\n            <th className=\"w-[100px]\">Region</th>\n            <th>Trend</th>\n            <th className=\"w-[50px]\">P75</th>\n            <th className=\"w-[50px]\">P95</th>\n            <th className=\"w-[50px]\">P99</th>\n          </tr>\n        </thead>\n        <tbody>\n          {regions\n            .filter((region) => regions.includes(region))\n            .map((region) => {\n              const regionConfig = regionDict[region as Region];\n              const flag = regionConfig.flag;\n              const code = regionConfig.code;\n              const metrics = metricsByRegion.find((m) => m.region === region);\n              return (\n                <tr key={region}>\n                  <td>\n                    {flag} {code}\n                  </td>\n                  <td>\n                    <SimpleChart\n                      data={chartData.data.map((d) => ({\n                        timestamp: d.timestamp,\n                        latency: d[region],\n                      }))}\n                    />\n                  </td>\n                  <td>\n                    {metrics?.p75Latency}\n                    ms\n                  </td>\n                  <td>\n                    {metrics?.p95Latency}\n                    ms\n                  </td>\n                  <td>\n                    {metrics?.p99Latency}\n                    ms\n                  </td>\n                </tr>\n              );\n            })}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/link.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport NextLink from \"next/link\";\n\n// TODO: we could add cva variants for the link\n\nexport function Link({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof NextLink>) {\n  return (\n    <NextLink\n      className={cn(\"font-medium text-foreground\", className)}\n      {...props}\n    >\n      {children}\n    </NextLink>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/listing.ts",
    "content": "import {\n  type MDXData,\n  formatDate,\n  getBlogPosts,\n  getChangelogPosts,\n  getComparePages,\n  getGuides,\n  getUseCasePages,\n} from \"./utils\";\n\n/**\n * Generates sitemap-style markdown listings for index and category pages\n * Used as fallback when MDX content is not found\n */\nexport function generateListingForPath(pathname: string): string | null {\n  const segments = pathname.split(\"/\").filter(Boolean);\n\n  // Root path → overview of all content\n  if (segments.length === 0) {\n    return generateRootListing();\n  }\n\n  const [category, subcategory, slug] = segments;\n\n  // Index pages: /blog, /changelog, etc.\n  if (!subcategory) {\n    switch (category) {\n      case \"blog\":\n        return generatePostsList(getBlogPosts(), \"Blog Posts\");\n      case \"changelog\":\n        return generatePostsList(getChangelogPosts(), \"Changelog\");\n      case \"compare\":\n        return generatePostsList(getComparePages(), \"Comparisons\");\n      case \"guides\":\n        return generatePostsList(getGuides(), \"Guides\");\n      case \"use-case\":\n        return generatePostsList(getUseCasePages(), \"Use Cases\");\n      default:\n        return null;\n    }\n  }\n\n  // Category pages: /blog/category/engineering\n  if (subcategory === \"category\" && slug) {\n    return generateCategoryList(category, slug);\n  }\n\n  return null;\n}\n\n/**\n * Generate a markdown list from posts\n */\nfunction generatePostsList(posts: MDXData[], title: string): string {\n  const sorted = posts.sort(\n    (a, b) =>\n      b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(),\n  );\n\n  const items = sorted\n    .map(\n      (post) =>\n        `- [${post.metadata.title}](${post.href}) - ${formatDate(post.metadata.publishedAt)}`,\n    )\n    .join(\"\\n\");\n\n  return `# ${title}\\n\\n${items}\\n`;\n}\n\n/**\n * Generate category-filtered listings\n */\nfunction generateCategoryList(type: string, category: string): string | null {\n  let posts: MDXData[];\n  let title: string;\n\n  switch (type) {\n    case \"blog\":\n      posts = getBlogPosts();\n      title = `Blog Posts - ${category}`;\n      break;\n    case \"changelog\":\n      posts = getChangelogPosts();\n      title = `Changelog - ${category}`;\n      break;\n    default:\n      return null;\n  }\n\n  const filtered = posts.filter((p) => p.metadata.category === category);\n\n  if (filtered.length === 0) {\n    return null;\n  }\n\n  return generatePostsList(filtered, title);\n}\n\n/**\n * Generate root listing with overview of all content sections\n */\nfunction generateRootListing(): string {\n  const sections = [\n    { title: \"Blog\", posts: getBlogPosts(), path: \"/blog\" },\n    { title: \"Changelog\", posts: getChangelogPosts(), path: \"/changelog\" },\n    { title: \"Comparisons\", posts: getComparePages(), path: \"/compare\" },\n    { title: \"Guides\", posts: getGuides(), path: \"/guides\" },\n    { title: \"Use Cases\", posts: getUseCasePages(), path: \"/use-case\" },\n  ];\n\n  const content = sections\n    .map((section) => {\n      const count = section.posts.length;\n      const sortedPosts = section.posts.sort(\n        (a, b) =>\n          b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(),\n      );\n\n      const topPosts = sortedPosts\n        .slice(0, 5)\n        .map((p) => `- [${p.metadata.title}](${p.href})`)\n        .join(\"\\n\");\n\n      const viewAll =\n        count > 5\n          ? `\\n- [View all ${count} ${section.title.toLowerCase()}...](${section.path})`\n          : \"\";\n\n      return `## [${section.title}](${section.path}) (${count})\\n\\n${topPosts}${viewAll}`;\n    })\n    .join(\"\\n\\n\");\n\n  return `# OpenStatus Content\\n\\n${content}\\n`;\n}\n"
  },
  {
    "path": "apps/web/src/content/logo-with-context-menu.tsx",
    "content": "\"use client\";\n\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuTrigger,\n} from \"@openstatus/ui/components/ui/context-menu\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\n\nexport function LogoWithContextMenu() {\n  return (\n    <ContextMenu>\n      <ContextMenuTrigger className=\"group\" asChild>\n        <Link href=\"/\" className=\"relative flex items-center gap-2\">\n          <Image\n            src=\"/assets/landing/openstatus-logo.svg\"\n            alt=\"openstatus logo\"\n            width={20}\n            height={20}\n            className=\"rounded-full border border-border dark:border-foreground\"\n          />\n          <span className=\"hidden sm:block\">openstatus</span>\n          <div className=\"absolute right-0.5 bottom-0 hidden group-hover:block\">\n            <span className=\"text-[10px] text-muted-foreground/50\">\n              [right click]\n            </span>\n          </div>\n        </Link>\n      </ContextMenuTrigger>\n      <ContextMenuContent className=\"min-w-[var(--radix-dropdown-menu-trigger-width)] rounded-none\">\n        <ContextMenuItem className=\"rounded-none px-2 py-3 font-mono\" asChild>\n          <a href=\"/assets/logos/OpenStatus.svg\" download=\"openstatus.svg\">\n            Download Name SVG\n          </a>\n        </ContextMenuItem>\n        <ContextMenuItem className=\"rounded-none px-2 py-3 font-mono\" asChild>\n          <a\n            href=\"/assets/logos/OpenStatus-Logo.svg\"\n            download=\"openstatus-logo.svg\"\n          >\n            Download Logo SVG\n          </a>\n        </ContextMenuItem>\n      </ContextMenuContent>\n    </ContextMenu>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/button-link.tsx",
    "content": "import { Button } from \"@openstatus/ui/components/ui/button\";\nimport type React from \"react\";\nimport { CustomLink } from \"./custom-link\";\n\nexport function ButtonLink(\n  props: React.ComponentProps<typeof Button> & { href: string },\n) {\n  return (\n    <Button\n      variant=\"outline\"\n      size=\"lg\"\n      className=\"no-underline! h-auto rounded-none px-4 py-4 text-base\"\n      asChild\n      {...props}\n    >\n      <CustomLink href={props.href}>{props.children}</CustomLink>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/code.tsx",
    "content": "import type React from \"react\";\nimport { highlight } from \"sugar-high\";\n\nexport function Code({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"code\">) {\n  // Only apply syntax highlighting if a language is specified (className contains \"language-\")\n  const hasLanguage = className?.includes(\"language-\");\n\n  if (hasLanguage) {\n    const codeHTML = highlight(children?.toString() ?? \"\");\n    return (\n      <code\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n        dangerouslySetInnerHTML={{ __html: codeHTML }}\n        className={className}\n        {...props}\n      />\n    );\n  }\n\n  // Plain code block without language - render as-is\n  return (\n    <code className={className} {...props}>\n      {children}\n    </code>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/custom-image.tsx",
    "content": "import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getImageDimensions } from \"@/lib/image-dimensions\";\nimport { cn } from \"@/lib/utils\";\nimport Image from \"next/image\";\nimport { ImageZoom, ZoomableImage } from \"../image-zoom\";\n\nexport function CustomImage({\n  className,\n  ...props\n}: React.ComponentProps<typeof Image>) {\n  const { src, alt, width, height, ...rest } = props;\n\n  if (!src || typeof src !== \"string\") {\n    return (\n      <figure>\n        <ImageZoom\n          backdropClassName={cn(\n            '[&_[data-rmiz-modal-overlay=\"visible\"]]:bg-background/80',\n          )}\n          zoomMargin={16}\n        >\n          <Image\n            className={className}\n            src={src}\n            alt={alt ?? \"image\"}\n            fill\n            sizes=\"100vw\"\n            style={{ objectFit: \"contain\" }}\n            {...rest}\n          />\n        </ImageZoom>\n        <figcaption>{alt}</figcaption>\n      </figure>\n    );\n  }\n\n  // Get actual image dimensions from filesystem\n  const dimensions = getImageDimensions(src);\n  const imageWidth = width || dimensions?.width || 1200;\n  const imageHeight = height || dimensions?.height || 630;\n\n  // Generate dark mode image path by adding .dark before extension\n  const getDarkImagePath = (path: string) => {\n    const match = path.match(/^(.+)(\\.[^.]+)$/);\n    if (match) {\n      return `${match[1]}.dark${match[2]}`;\n    }\n    return path;\n  };\n\n  // Check if dark image exists, fallback to light version if not\n  const checkDarkImageExists = (darkPath: string) => {\n    // If path starts with /, it's in the public directory\n    if (darkPath.startsWith(\"/\")) {\n      const publicPath = join(process.cwd(), \"public\", darkPath);\n      return existsSync(publicPath);\n    }\n    // For relative paths, check relative to public\n    const publicPath = join(process.cwd(), \"public\", darkPath);\n    return existsSync(publicPath);\n  };\n\n  const darkSrc = getDarkImagePath(src);\n  const useDarkImage = checkDarkImageExists(darkSrc);\n\n  return (\n    <ZoomableImage\n      {...rest}\n      src={src}\n      alt={alt}\n      className={className}\n      darkSrc={useDarkImage ? darkSrc : undefined}\n      imageWidth={imageWidth as number}\n      imageHeight={imageHeight as number}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/custom-link.tsx",
    "content": "import Link from \"next/link\";\nimport type React from \"react\";\n\nexport function CustomLink(props: React.ComponentProps<\"a\">) {\n  const href = props.href ?? \"\";\n\n  if (href.startsWith(\"/\")) {\n    return (\n      <Link href={href} {...props}>\n        {props.children}\n      </Link>\n    );\n  }\n\n  if (href.startsWith(\"#\")) {\n    return <a {...props} />;\n  }\n\n  return <a target=\"_blank\" rel=\"noopener noreferrer\" {...props} />;\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/details.tsx",
    "content": "import React from \"react\";\n\nexport function Details({\n  children,\n  summary,\n  open = false,\n}: {\n  children: React.ReactNode;\n  summary: string;\n  open?: boolean;\n}) {\n  return (\n    <details open={open}>\n      <summary>{summary}</summary>\n      {React.isValidElement(children)\n        ? // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n          React.cloneElement(children, { hidden: \"until-found\" } as any)\n        : children}\n    </details>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/grid.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type React from \"react\";\n\nexport function Grid({\n  cols = 2,\n  children,\n  className,\n}: {\n  cols?: 1 | 2 | 3 | 4 | 5;\n  children: React.ReactNode;\n  className?: string;\n}) {\n  const colsClass = {\n    1: \"md:grid-cols-1\",\n    2: \"md:grid-cols-2\",\n    3: \"md:grid-cols-3\",\n    4: \"md:grid-cols-4\",\n    5: \"md:grid-cols-5\",\n  };\n\n  // Remove top border from all except first row\n  const topBorderClass = {\n    1: \"[&>*]:border-t-0 [&>*:first-child]:border-t\",\n    2: \"[&>*]:border-t-0 [&>*:first-child]:border-t md:[&>*:nth-child(-n+2)]:border-t\",\n    3: \"[&>*]:border-t-0 [&>*:first-child]:border-t md:[&>*:nth-child(-n+3)]:border-t\",\n    4: \"[&>*]:border-t-0 [&>*:first-child]:border-t md:[&>*:nth-child(-n+4)]:border-t\",\n    5: \"[&>*]:border-t-0 [&>*:first-child]:border-t md:[&>*:nth-child(-n+5)]:border-t\",\n  };\n\n  // Remove left border from all except first column (only on md+ screens)\n  const leftBorderClass = {\n    1: \"\",\n    2: \"md:[&>*]:border-l-0 md:[&>*:nth-child(2n+1)]:border-l\",\n    3: \"md:[&>*]:border-l-0 md:[&>*:nth-child(3n+1)]:border-l\",\n    4: \"md:[&>*]:border-l-0 md:[&>*:nth-child(4n+1)]:border-l\",\n    5: \"md:[&>*]:border-l-0 md:[&>*:nth-child(5n+1)]:border-l\",\n  };\n\n  return (\n    <div\n      className={cn(\n        \"my-4 grid grid-cols-1\",\n        \"[&>*]:border [&>*]:border-border [&>*]:p-4\",\n        // NOTE: remove extra margin from prose grid cells of first and last element\n        \"[&>*>*:first-child]:!mt-0 [&>*>*:last-child]:!mb-0\",\n        colsClass[cols],\n        topBorderClass[cols],\n        leftBorderClass[cols],\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/heading.tsx",
    "content": "import React from \"react\";\n\nexport function slugify(str: string) {\n  return str\n    .toString()\n    .toLowerCase()\n    .trim() // Remove whitespace from both ends of a string\n    .replace(/\\s+/g, \"-\") // Replace spaces with -\n    .replace(/&/g, \"-and-\") // Replace & with 'and'\n    .replace(/[^\\w-]+/g, \"\") // Remove all non-word characters except for -\n    .replace(/--+/g, \"-\"); // Replace multiple - with single -\n}\n\nexport function createHeading(level: number) {\n  const Heading = ({ children }: { children: React.ReactNode }) => {\n    const slug = slugify(children?.toString() ?? \"\");\n    return React.createElement(\n      `h${level}`,\n      { id: slug },\n      [\n        React.createElement(\"a\", {\n          href: `#${slug}`,\n          key: `link-${slug}`,\n          className: \"anchor\",\n          \"aria-label\": `Link to section: ${children?.toString()}`,\n        }),\n      ],\n      children,\n    );\n  };\n\n  Heading.displayName = `Heading${level}`;\n\n  return Heading;\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/index.tsx",
    "content": "import { LatencyChartTable } from \"../latency-chart-table\";\nimport { ButtonLink } from \"./button-link\";\nimport { Code } from \"./code\";\nimport { CustomImage } from \"./custom-image\";\nimport { CustomLink } from \"./custom-link\";\nimport { Details } from \"./details\";\nimport { Grid } from \"./grid\";\nimport { createHeading } from \"./heading\";\nimport { Pre } from \"./pre\";\nimport { MDXStatusPageExample } from \"./status-page-example\";\nimport { Table } from \"./table\";\nimport { MDXTweet } from \"./tweet\";\n\nexport { slugify } from \"./heading\";\n\nexport const components = {\n  h1: createHeading(1),\n  h2: createHeading(2),\n  h3: createHeading(3),\n  h4: createHeading(4),\n  h5: createHeading(5),\n  h6: createHeading(6),\n  Image: CustomImage,\n  a: CustomLink,\n  ButtonLink: ButtonLink,\n  code: Code,\n  pre: Pre,\n  table: Table,\n  Grid,\n  Details, // Capital D for JSX usage with props\n  details: Details, // lowercase for HTML tag replacement\n  SimpleChart: LatencyChartTable,\n  Tweet: MDXTweet,\n  StatusPageExample: MDXStatusPageExample,\n};\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/pre.tsx",
    "content": "import React from \"react\";\nimport { CopyButton } from \"../copy-button\";\n\nfunction extractTextFromReactNode(node: React.ReactNode): string {\n  if (typeof node === \"string\") {\n    return node;\n  }\n  if (typeof node === \"number\") {\n    return String(node);\n  }\n  if (Array.isArray(node)) {\n    return node.map(extractTextFromReactNode).join(\"\");\n  }\n  if (React.isValidElement(node)) {\n    const props = node.props as { children?: React.ReactNode };\n    if (props.children) {\n      return extractTextFromReactNode(props.children);\n    }\n  }\n  return \"\";\n}\n\nexport function Pre({ children, ...props }: React.ComponentProps<\"pre\">) {\n  const textContent = extractTextFromReactNode(children);\n  return (\n    <div className=\"relative\">\n      <pre {...props}>{children}</pre>\n      <CopyButton\n        copyText={textContent}\n        className=\"absolute top-px right-px backdrop-blur-xs\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/status-page-example.tsx",
    "content": "import { ComponentHighlighter } from \"../component-highlighter\";\nimport { StatusPageExample } from \"../shadcn-registry-example\";\n\nexport function MDXStatusPageExample() {\n  return (\n    <ComponentHighlighter>\n      <StatusPageExample />\n    </ComponentHighlighter>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/table.tsx",
    "content": "import type React from \"react\";\n\nexport function Table(props: React.ComponentProps<\"table\">) {\n  return (\n    <div className=\"table-wrapper\">\n      <table {...props} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx-components/tweet.tsx",
    "content": "import { Tweet, type TweetProps } from \"react-tweet\";\n\nexport function MDXTweet(props: TweetProps) {\n  return (\n    <div data-theme=\"light\" className=\"not-prose [&>div]:mx-auto\">\n      <Tweet {...props} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/mdx.tsx",
    "content": "import { MDXRemote, type MDXRemoteProps } from \"next-mdx-remote/rsc\";\nimport React from \"react\";\nimport remarkGfm from \"remark-gfm\";\nimport { HighlightText } from \"./highlight-text\";\nimport { components } from \"./mdx-components\";\n\nexport { components, slugify } from \"./mdx-components\";\n\nfunction MDXContent(props: MDXRemoteProps) {\n  return (\n    <MDXRemote\n      {...props}\n      options={{\n        blockJS: false, // Allow JS expressions in trusted MDX content\n        blockDangerousJS: true, // Still block dangerous operations\n        mdxOptions: {\n          remarkPlugins: [remarkGfm],\n          ...props.options?.mdxOptions,\n        },\n        ...props.options,\n      }}\n      components={\n        {\n          ...components,\n          ...props.components,\n        } as MDXRemoteProps[\"components\"]\n      }\n    />\n  );\n}\n\nexport function CustomMDX(props: MDXRemoteProps) {\n  return (\n    <React.Suspense fallback={<MDXContent {...props} />}>\n      <HighlightText>\n        <MDXContent {...props} />\n      </HighlightText>\n    </React.Suspense>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/nav.tsx",
    "content": ""
  },
  {
    "path": "apps/web/src/content/pages/blog/2023-year-review.mdx",
    "content": "---\ntitle: \"Our 2023 year in review.\"\ndescription: \"Let's review our year in 2023, analyze our metrics, and discuss our future plans.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2023-12-29\"\nimage: \"/assets/posts/2023-year-review/title.png\"\ncategory: \"company\"\n---\n\nIt has been a wild six months for us at OpenStatus. In late June, we began\nwriting the first line of code for OpenStatus.\n\nWe started with a simple idea: make an open-source a status page, but we quickly\nrealized that we could do more. We could build a platform that would allow you\nto monitor your services and notify your users when something goes wrong.\n\nIn this article, I will share with you the challenges we encountered and how we\nsolved them. We will also be sharing our metrics and our plans for the future.\n\n# A quick recap of our journey\n\n## June 2023\n\n**Middle of month**\n\nI asked on Twitter if people would be interested in an open-source alternative\nfor a status page. Max replied to me, and we started to work on the project.\n\n**End of month**\n\nWe wrote the first line of code for OpenStatus.\n\n## July 2023\n\n**Middle of month**\n\nMy tweet got a lot of traction, and and it gave us the motivation to continue to\nbuild OpenStatus.\n\n**End of month**\n\nWe launched the initial version of OpenStatus, which was more than just a basic\nstatus page. We incorporated website monitoring features that would shape the\nfuture of OpenStatus.\n\nMax's Twitter thread went viral, gaining us a lot of attention on the platform.\n\n<Tweet id=\"1685666982786404352\" />\n\nAfter that OpenStatus trended on GitHub for a few days.\n\n## August 2023\n\n**Middle of month**\n\nWe launched our first paid plan, which allowed us to cover our costs. Couple of\nhours after the launch, we had our first paying customer. We were so happy! 🎉\n\n## September 2023\n\nIt was a month of reflection for us. We were a bit confused about the direction\nwe wanted to pursue at that time. Having just a status page as a side business\nis cool, but we also aspire to build something that could become a full-fledged\ncompany in the future.\n\n## October 2023\n\n**Beginning of month**\n\nWe launched on Hacker News, and we got a lot of feedback from the community. We\nwere so happy to see that people were interested in our project.\n\n<Image\n  alt=\"New users per week\"\n  src=\"/assets/posts/2023-year-review/show-hn.png\"\n  width={650}\n  height={575}\n/>\n\nWe were not expecting such a good traction from the hacker news community\n\n**Middle of month**\n\nFor some cost saving reasons, we decided to migrate our checker infrastucture\nfrom Vercel to Fly.io.\n\n**End of month**\n\nWe told our migration in a blog post, and it got a lot of traction, also on\nHacker News.\n\n<Image\n  alt=\"New users per week\"\n  src=\"/assets/posts/2023-year-review/blog-hn.png\"\n  width={650}\n  height={575}\n/>\n\n## November 2023\n\nWe have rewritten our checker infrastructure in Golang to enhance stability,\nresulting in cost savings on fly.io server.\n\n## December 2023\n\nWe have launched our deploy your own status page\n\n# Our metrics\n\n## Our product metrics\n\n<Image\n  alt=\"New users per week\"\n  src=\"/assets/posts/2023-year-review/users-per-week.png\"\n  width={650}\n  height={575}\n/>\n\n**Number of new users per week**\n\n<Image\n  alt=\"New monitors per week\"\n  src=\"/assets/posts/2023-year-review/monitors-per-week.png\"\n  width={650}\n  height={575}\n/>\n\n**Number of monitors created per week**\n\n<Image\n  alt=\"New Pages per week\"\n  src=\"/assets/posts/2023-year-review/pages-per-week.png\"\n  width={650}\n  height={575}\n/>\n\n**Number of page created per week**\n\n## Other metrics\n\n<Image\n  alt=\"GitHub stars\"\n  src=\"/assets/posts/2023-year-review/github-stars.png\"\n  width={650}\n  height={575}\n/>\n**Number of GitHub stars**\n\n<Image\n  alt=\"Visitors per month\"\n  src=\"/assets/posts/2023-year-review/visitors.png\"\n  width={650}\n  height={575}\n/>\n\n**Number of visitors per month**\n\n<Image\n  alt=\"Search console\"\n  src=\"/assets/posts/2023-year-review/search-console.png\"\n  width={650}\n  height={575}\n/>\n\n**Search console**\n\n## Financials\n\n<Image\n  alt=\"MRR\"\n  src=\"/assets/posts/2023-year-review/mrr.png\"\n  width={650}\n  height={575}\n/>\n\n**Monthly recuring revenue**\n\n# Let's talked about our 2023 year\n\n## What went well\n\nWe launched OpenStatus 🔥. We didn't waste too much time over-engineering it.\n\nWe had a good traction among the developer community. We were able to get a lot\nof feedback from the community, which helped us to improve our product.\n\nThe number of GitHub stars, users are a good indicator of the traction we had.\n\nWe both reach more than 1000 followers on Twitter.\n\n## What could have gone better\n\nWe can brag about our MRR on Twitter but we are still not profitable. We are\nstill not able to cover our costs despites a small MRR.\n\nWe could have listen more to our users. We could have done more to improve our\nproduct. We should have done more users surveys.\n\n## What went wrong\n\nWe got confused. We were not sure about the direction we wanted to pursue. We\nalso got defocused when VC started to reach out to us. They are paid to talk to\nyou but not us. We are not building anything when we are talking to them.\n\nThey also don't know more than us about what our customers want. They are not\nthe one we should listen to. We should listen to our users 😁\n\nWe still pay too much for Tinybird 😭.\n\n# Our plans for 2024\n\nFirst we want to reach profitability. 😁\n\nWe will allocate more time to the project 🚀\n\nWe will follow our intuition and not listen to VC. We will listen to our users\ninstead. 🤦\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/2026-roadmap.mdx",
    "content": "---\ntitle: \"Openstatus 2025 Year in Review: Lessons Learned and What’s Next\"\ndescription: \"As we close out 2025, it’s the perfect time to pause, reflect, and share where we are heading for 2026.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2025-12-01\"\nimage: \"/assets/posts/2026-roadmap/2026.png\"\ncategory: \"company\"\n---\n\nAs we close out 2025, it’s the perfect time to pause, reflect, and share where we are heading. It has been a year of shipping, learning, and making hard decisions about the future of OpenStatus.\n\nBefore we dive into our roadmap for 2026, let’s look at what we accomplished together this year.\n\n## 2025 Retrospective\n\n### A Year of Shipping\n\nIf you look back at our [2025 roadmap](/blog/vision-2025), our goal was clear: focus on synthetic monitoring, status pages, alerting, and self-hosting.\n\nHere are some of the [highlights](/changelog) from the past 12 months:\n- **DNS Monitor:** We added the ability to track DNS records giving you visibility into the fundamental plumbing of your connectivity.\n-  **Multi-Cloud Deployment:** We enabled monitoring from different cloud providers to help you visualize how network routing affects your latency globally.\n-  **Private location**:  We launched the ability to monitor internal services from within your own infrastructure.\n- **Theme Builder:**  We gave you complete control over the look and feel of your status pages to fully match your brand identity.\n- **New Status Page:** A completely redesigned, faster, and accessible public face for your uptime.\n- **New Dashboard:** A cleaner UI designed to visualize your data without the noise.\n- **New Marketing Page:** A fresh style and clearer positioning.\n- **Self-Hosting:** We released a [self-hosted version](https://docs.openstatus.dev/guides/self-hosting-openstatus/) of openstatus. \n\n<Image\n  alt=\"Linear issue created more than a year ago, splitting the apps into multiple apps\"\n  src=\"/assets/posts/2026-roadmap/linear.png\"\n  width={690}\n  height={558}\n/>\n\n**A Note on Architecture:** \nWe also spent significant time under the hood. We’ve decoupled our applications, making the platform faster to build and easier for the community to contribute to. This refactor was on our wishlist for over a year, and we are thrilled to see it finally come to life.\n\n\n### Staying bootstrap \n\nTransparency is one of our core values. In the spirit of that transparency: **In 2025, we went through an acquisition process.**\n\nThe offer was strong, and the acquiring team was fantastic. It would have been a \"good exit.\" However, as we moved through the due diligence, we realized something profound: **we aren't done yet**.\n\nWe felt that selling now would leave too much value on the table, not just financially, but in terms of the product vision we want to deliver to the OpenStatus community. We still have the strength, motivation, and resources to push OpenStatus further on our own terms.\n\nWe chose to stay independent. We chose to keep building.\n\n\n### The first openstatus retreat \n\n<Image\n  alt=\"Us in Kochel am See for the openstatus 2025 team retreat\"\n  src=\"/assets/posts/2026-roadmap/team-retreat.jpg\"\n  width={608}\n  height={456}\n/>\n\n2025 wasn't just about code; it was about us. We held our very first team retreat. We looked for a location that was equidistant for us, easy to access, and emphatically _not_ Paris.\n\nWe chose **Munich**.\n\nWe spent the week in Kochel am See, eating Kaiserschmarrn, riding e-bikes, and drinking Bavarian beer. It was a vital reminder that while we build software, the human connection is what keeps the momentum alive.\n\n\n## Lessons learned: Let's go \"Status Page First\" \n\nBuilding a SaaS is a constant loop of shipping and listening. In 2025, thanks to the help of Alex and Emily shaping our perspective, we learned two massive lessons about our market fit.\n\nWe used to try to be \"mild\"—trying to look approachable for everyone. But the reality is: **our users are engineers.** We need to own that niche.\n\n### The Enterprise Dilemma\n\nWe realized that trying to be an \"all-in-one\" uptime monitoring solution for Enterprise is the wrong play.\n- **The Reality:** Enterprises already have established monitoring stacks (Datadog, Prometheus, etc.).\n- **The Friction:** They generally _don't_ want a status page tightly coupled to raw monitoring data. There are internal politics involved in declaring incidents; they need a curated status report, not a raw feed of 500 errors.\n- **The Fit**: Paradoxically, we are often \"too cheap\" for Enterprise, and our complex multi-cloud monitoring is overkill for anyone not doing $1B in revenue.\n\n### The Solo-Dev/Hobbyist  Pattern\n\nOn the other end of the spectrum, solo developers love our bundled uptime monitoring, status page and the fact that we are open source. However, while usage in this segment is high, it rarely converts to sustainable revenue.\n\nWe have finally released our self-hosted version to better serve this community. We want hobbyists and solo devs to have access to reliable uptime monitoring without the pressure of a SaaS subscription.\n\n### The Aha! Moment \n\nBy analyzing our user journey, we found a distinct pattern: \n\n> Users search for a Status Page → They see it's Open Source → They stay for the bundled Uptime Monitoring.\n\nThe entry point isn't the monitoring; **it's the Status Page.**\n\n\n## The Plan for 2026: Doubling Down on the Status Page \n\nWe’ve spent the last year listening, learning, and watching how you handle downtime. If there is one universal truth we’ve uncovered, it’s this: \n\n**Context switching is the enemy of incident resolution.**\n\nWhen your database locks up or your API starts returning 500s, the last thing you want to do is tab out of your terminal, to log into a separate dashboard just to update a status page. It breaks your flow when you need it most.\n\n#### **Our Goal for 2026** \n\nBased on these learnings, we are setting an ambitious goal for 2026: **To build the absolute best tooling for Status Pages on the market.**\n\nBut how do we define \"best\"?\n\nTo us, the best tool is the one that gets out of your way. It’s a tool that lives inside your existing workflow. We believe you should be able to manage the communication of an incident from the exact same place you are fixing it.\n\n#### **The Philosophy: \"Stripe for Incidents\"** \n\nWe realized that every engineering team has a unique DNA. A three-person startup handles fires differently than a 50-person scale-up. Because of this, we are moving away from the \"one-size-fits-all\" approach.\n\nInstead of imposing a rigid workflow on you, we want to provide the **primitives** that allow you to build your own.\n\nWe aim to become the **\"Stripe for Incidents\"** an infrastructure platform that is so flexible and developer-friendly that you can build your ideal incident management process right on top of us.\n\n#### **The Roadmap: API-First and ChatOps** \n\nTo make this a reality, our 2026 roadmap is focused on meeting you where you work:\n\n- **Deep ChatOps Integration:** We are building robust **Slack and Discord apps**. You should be able to declare an incident, update a component, and notify customers without ever leaving your team's chat channel.\n    \n- **API-First Expansion:** We are drastically improving our API. We want you to be able to script updates, automate status changes via webhooks from your monitoring tools, and integrate us deeply into your CI/CD or observability stack.\n    \n\n**Focusing on the Builders** We believe our product currently shines brightest for **small teams and startups**. In 2026, we are doubling down on you. We want to give you the lean, powerful tools you need to maintain trust with your users without the enterprise bloat.\n\n**Looking Ahead: Moving Upstream** Status pages are just the beginning. Currently, we help you communicate _during_ the fire. As our tooling matures, we plan to move \"upstream\" in your workflow.\n\nOnce our status page tooling is perfected, we will expand toward full **Incident Management**. We want to bridge the gap between _detecting_ the problem and _communicating_ it, eventually helping you manage the entire lifecycle of an incident.\n\n\nLooking forward to next year. \n\nCheers, Thibault and Max\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/data-table-redesign.mdx",
    "content": "---\ntitle: \"The React data-table I always wanted\"\ndescription: \"Better design, new features, and performance improvements.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2025-02-02\"\nimage: \"/assets/posts/data-table-redesign/toolbox.png\"\ncategory: \"engineering\"\n---\n\nWe have release our react data-table based on [shadcn/ui](https://ui.shadcn.com/) a couple of months ago. While there's still in development, the PR [#11](https://github.com/openstatusHQ/data-table-filters/pull/11) marks an important second milestone after the initial release a few months ago. We now have a solid foundation to focus on the component API design and data fetching. Though you can create your own data table using only config files (for \"sheet,\" \"filters,\" and \"columns\"), you end up writing more code than you would like.\n\nIf you want to try out the demo right away [logs.run/i](https://logs.run/i) or go to the [data-table-filters](https://github.com/openstatusHQ/data-table-filters) GitHub repository - it's open source.\n\n### Design improvements\n\nWe've reworked the design. Adding table borders improves clarity and structure. We've replaced the `Check` icon with the rounded square already used in the Chart to maintain design consistency. We've also removed the \"green\" highlighting to emphasize bad requests instead.\n\nA quick look of the before/after changes:\n\n<Image\n  src=\"/assets/posts/data-table-redesign/before.png\"\n  alt=\"before\"\n/>\n\n<Image\n  src=\"/assets/posts/data-table-redesign/after.png\"\n  alt=\"after\"\n/>\n\nWe have been inspired by [Vercel](https://vercel.com/?ref=openstatus), [Datadog](https://datadoghq.com/?ref=openstatus) and [Axiom](https://axiom.co/?ref=openstatus) when it comes to design and features.\n\n### Features\n\nThis time, we prioritized keyboard navigation to make table access and navigation quickly. We've added:\n\n- \"Skip to content\" to jump straight to the table\n- \"Tab\" navigation (with potential Arrow key support coming) + \"Enter\" keypress on rows\n- Include filters in the details `<Sheet />`\n- Additional hotkeys to quickly:\n    - `⌘ Esc` reset focus (to the body, starting with \"Skip to content\")\n    - `⌘ .` reset filters\n    - `⌘ U` undo column states like order or visibility\n\nThe W3 WAI (Web Accessibility Initiative) has two concise patterns for grids: The [Grid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) and the [Treegrid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/). The Grid Pattern focuses on cell navigation via arrows and the Treegrid Pattern focuses on hierarchical data with sub-rows, also using arrows to navigate. We might want to take an even closer look to the Web Standards to respect them.\n\nThe log data table leverages a lot of [tanstack table](https://tanstack.com/table/latest/docs/introduction) core features and adds some additional customization to it.\n\nHere's what's included in our logs data table:\n\n1. column resizing\n2. column reordering\n3. column visibility\n4. column sorting\n5. custom filter functions\n6. array facets support\n7. custom row/cell/header styles via `meta` data\n8. …and much much more\n\n**Creating data tables via configs** might seem like over-engineering, especially given how unique and edge-case-heavy they can be. However, this approach can simplify many common use cases and serve as a valuable reference for building data tables with tanstack table.\n\n### Issues and hacks\n\nWe've encountered numerous issues along the way (unfortunately, I missed some of them). Here are the notable ones:\n\nThe biggest challenge was **browser compatibility** for `table` and `thead` HTML tags: making the `thead` sticky while horizontally scrollable with borders was surprisingly tricky. While it worked in Chrome, Safari wouldn't show the header border, despite using the same approach that worked for table cells. After some CSS exploration, we found a solution. Read about the issue [here](https://stackoverflow.com/questions/50361698/border-style-do-not-work-with-sticky-position-element/53559396#53559396).\n\nSeveral smaller hacks were necessary:\n\nTo **highlight the table rows**, we added negative outlines to fit within the container. This prevents the table overflow from cutting off the outline. We couldn't use the border attribute since the table's left-hand border serves as a separator between filter controls and main content (including cmd k, chart, and toggles).\n\nUsed tailwindcss classes: `focus-visible:outline-solid outline-1 -outline-offset-1 outline-primary focus-visible:bg-muted/50 data-[state=selected]:outline-solid`\n\n> In general, whenever there are outline issues due to `overflow-hidden`, I often tend to add negative margin with the same positive padding to the element `-m-* p-*`.\n\nTo **reset the active focus element** (returning to the first focusable element in the document), we found no web standard solution. While `document.activeElement.blur()` dismisses the current focus, it remembers the last focused position. Our solution: manually setting and removing a `tabindex` attribute on the `body`.\n\n```jsx\ndocument.body.setAttribute(\"tabindex\", \"0\");\ndocument.body.focus();\ndocument.body.removeAttribute(\"tabindex\");\n```\n\nWhile not an issue in this update, we've repeatedly found that when using `recharts`, the **date property can't be a Date()** for x-axis label reading and formatting. You must use it as `string` (toString) or `number` (getTime) – not the most intuitive approach.\n\nOne persistent issue is the flickering of default values on the `<Accordion defaultOpen={...} />;`. This occurs on the official [radix-ui](https://www.radix-ui.com/primitives/docs/components/accordion) during page refresh (not client-side navigation) in Safari/Firefox, but not Chrome. We have opened an issue [#12](https://github.com/openstatusHQ/data-table-filters/issues/12) if you have solved that problem before and want to contribute.\n\n> Whenever I encounter an issue, I try to leave a `REMINDER:` comment. That way, I can easily search within the files and have a good reminder to not remove the code untested. Also this helped me to write that blog section at the end without having to leave the code.\n\n### Performance\n\nPerformance is improving. We've moved most of the state into a dedicated context, so only components consuming it rerender. Previously, our entire `data-table-infinite.tsx` component would trigger rerenders for all child components on any property change.\n\nVery important: **we will stick with the shadcn defaults** and avoid additional libraries except [`nuqs`](https://nuqs.47ng.com/), an excellent type-safe search params state manager supporting major React frameworks. We'll keep using React Context for state management, letting you choose your preferred library (zustand, jotai, redux) when needed.\n\nWe've added `debounce` to all possible controls to prevent renders on every keystroke. This helps with input searches and slider value changes.\n\nA simple example of reducing the re-render is by using a dedicated `ControlsContext` that toggles the `data-expaneded` attribute. With css only, you can then hide or show containers based on the value. See the @taiwindcss v3/v4 example:\n\n```tsx\ninterface ControlsContextType {\n  open: boolean;\n  setOpen: React.Dispatch<React.SetStateAction<boolean>>;\n}\n\nexport const ControlsContext = React.createContext<ControlsContextType | null>(null);\n\nexport function ControlsProvider({ children }: { children: React.ReactNode }) {\n  const [open, setOpen] = React.useState(true);\n\n  return (\n    <ControlsContext.Provider value={{ open, setOpen }}>\n      <div\n        /**\n         * How to use the controls state without rerendering the children\n         * components that do not consume the context with tailwind:\n         * \"hidden group-data-[expanded=true]/controls:block\" (v3/v4)\n         * \"hidden group-data-expanded/controls:block\" (v4)\n         */\n        className=\"group/controls\"\n        data-expanded={open}\n      >\n        {children}\n      </div>\n    </ControlsContext.Provider>\n  );\n}\n\nexport function useControls() {\n  const context = React.useContext(ControlsContext);\n\n  if (!context) {\n    throw new Error(\"useControls must be used within a ControlsProvider\");\n  }\n\n  return context as ControlsContextType;\n}\n```\n\n> Don't sleep on css and basic html!\n\nThe new [React Compiler](https://react.dev/learn/react-compiler) reduces our need for memoization while delivering great out-of-the-box performance. We’ve enabled in our Nextjs project, and we plan to include it in our future Vite example. We still need to add virtualization for handling larger tables (rendering only visible portions of the list).\n\nIf you want to learn when your components rerender, I highly recommend the [react-scan](https://github.com/mxkaske/react-scan) library.\n\nWe can avoid one full table rerender on row selection, which happens due to the `rowSelection` key used for outlining selected rows while the `<Sheet />` is open. But hey, it's a nice visual touch to see which row is selected, so we're keeping it for now.\n\n### Feature Requests\n\nThe mobile navigation needs more love. Horizontal scrolling now gives access to previously hidden columns. Currently, we simply place filter controls at the screen's top. This should move into a [`Drawer`](https://ui.shadcn.com/docs/components/drawer) component for better touch screen UX [#13](https://github.com/openstatusHQ/data-table-filters/issues/13).\n\nTo make the data table more accessible for React users, we need to create a simple vitejs example [#14](https://github.com/openstatusHQ/data-table-filters/issues/14) that doesn't rely on Nextjs (except maybe for the `/api` endpoint to fetch data from any server)!\n\nA fun feature is support for natural language filters [#15](https://github.com/openstatusHQ/data-table-filters/issues/15), allowing to write the filter and let AI translate into the correct `filter:query` using AI and the config models!\n\nFeel free to open a GitHub issue with feature requests or encountered bugs, or contribute directly by opening a PR.\n\n### What's next?\n\nThe `<Component />` API designs including the different `config` objects and `/api` endpoint standardization will drive our next bigger improvements. While I'm unsure when I'll have more time to focus on this, the current state makes for a perfect break point.\n\nThanks for the read and see you in a while! And don’t forget to [leave a star](https://github.com/openstatusHQ/openstatus) if you enjoyed it!\n\nTry the demo [logs.run/i](https://logs.run/i) or checkout the [data-table-filters](https://github.com/openstatusHQ/data-table-filters) repository."
  },
  {
    "path": "apps/web/src/content/pages/blog/deploy-private-locations-raspberry-pi.mdx",
    "content": "---\ntitle: \"Deploy private locations on a Raspberry Pi\"\ndescription: \"An 8.5MB Docker image running uptime checks from home.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2025-10-17\"\nimage: \"/assets/posts/deploy-private-locations-raspberry-pi/raspberry-pi.png\"\ncategory: \"engineering\"\n---\n\nWe just pushed the first version of **Private Locations** at openstatus - and I had to test it on my old **Raspberry Pi 3**.\n\nSince **2016**, I've been using the Pi for **AirPlay** to connect to my receiver but other than that, it's been sitting around without any other use case.\n\nTime to revive it!\n\n<Image\n  src=\"/assets/posts/deploy-private-locations-raspberry-pi/raspberry-pi-receiver.jpg\"\n  alt=\"My bare metal Raspberry Pi on top of the receiver I use for AirPlay\"\n/>\n\n---\n\n## Deployment Guide\n\nObviously connecting to the Raspberry Pi via `ssh` - and if you're like me, the default password is secure enough:\n\n```shell\n$ ssh pi@raspberrypi.local\n> password: raspberry \n```\n\n### Hardware Specs\n\nI didn’t know exactly which Debian version it was running, but it was _very_ old - so I took the opportunity to clean everything up and upgrade to the latest available version.\n\nModel:\n\n```shell\n$ cat /proc/device-tree/model\n> Raspberry Pi 3 Model B Rev 1.2\n```\n\nLinux Kernel:\n\n```shell\n$ uname -a\n> Linux raspberrypi 6.12.47+rpt-rpi-v8 #1 SMP PREEMPT Debian 1:6.12.47-1+rpt1 (2025-09-16) aarch64 GNU/Linux\n```\n\nOperating System:\n\n```shell\n$ cat /etc/os-release\n> PRETTY_NAME=\"Debian GNU/Linux 13 (trixie)\"\n> NAME=\"Debian GNU/Linux\"\n> VERSION_ID=\"13\"\n> VERSION=\"13 (trixie)\"\n> VERSION_CODENAME=trixie\n> DEBIAN_VERSION_FULL=13.1\n> ID=debian\n> HOME_URL=\"https://www.debian.org/\"\n> SUPPORT_URL=\"https://www.debian.org/support\"\n> BUG_REPORT_URL=\"https://bugs.debian.org/\"\n```\n\nI'm running the OS on a **16GB micro SD** card. The Pi has **1GB of RAM**, which is perfectly enough for this use case.\n\n<Image\n  src=\"/assets/posts/deploy-private-locations-raspberry-pi/raspberry-htop.png\"\n  alt=\"Statistics from htop command\"\n/>\n\n\n### Install Docker Image\n\nLet's freshly install **Docker** using the official Debian repository instructions: \n\nhttps://docs.docker.com/engine/install/debian/#install-using-the-repository\n\nOnce installed, Docker should already be running.  \n\nYou can verify it with:\n\n```shell\n$ sudo systemctl status docker\n```\n\n### Pull and Run the OpenStatus Container\n\nLet's grab the image:\n\n```shell\n$ sudo docker pull ghcr.io/openstatushq/private-location:latest\n```\n\nIf you have access to Private Locations in the **Dashboard**, create one - it'll will provide you a `token` to use as an environment variables:\n\n<Image\n  src=\"/assets/posts/deploy-private-locations-raspberry-pi/dashboard-create-private-location.png\"\n  alt=\"Create a private location\"\n/>\n\n\nAdd the `token` to your shell profile:\n\n```bash\n$ echo 'export OPENSTATUS_KEY=<token>’ >> ~/.bashrc\n```\n\nReload the shell profile:\n\n```bash\n$ source ~/.bashrc\n```\n\nIf you are using `oh-my-zsh` you might be using the `~/.zshrc` file instead.\n\nCheck that it worked:\n\n```bash\n# print the env var\n$ echo $OPENSTATUS_KEY\n```\n\n### Run the Container\n\nNow let's launch it:\n\n```shell\n$ sudo docker run -d \\\n  --name openstatus-private-location \\\n  --restart=always \\\n  -e OPENSTATUS_KEY=$OPENSTATUS_KEY \\\n  ghcr.io/openstatushq/private-location:latest\n```\n\nWhat these flags do:\n- `-d`: run in background (detached mode)\n- `--restart`: auto-start on reboot\n- `--name`: give it a nice readable name\n- `-e`: pass your env var into the container\n\n> Don't sleep on `--restart=always`! How often I had to quickly switch position of and need to reboot the Raspberry, this is a game changer.\n\nTo see live logs:\n\n```shell\n$ sudo docker logs -f openstatus-private-location\n```\n\n\n\n---\n\n### What Happens Next\n\nOnce the container is up, openstatus automatically fetches the endpoints you've chosen for this private location and starts monitoring them.\n\nEvery few minutes (`last_seen_at`), the agent refreshes its internal database to stay up-to-date with any changes - like added monitors or updated headers.\n\nThe responses of the pings are sent to our ingest endpoint for storage and aggregation.\n\n\n<Image\n  src=\"/assets/posts/deploy-private-locations-raspberry-pi/dashboard-response-logs.png\"\n  alt=\"Access your logs within the openstatus Dashboard.\"\n/>\n\n\n### Light on Resources\n\nOur Docker image is just **8.5 MB** in size. Compare that to 1GB+ for some competitors (yes, they pack more features, but still 🤯).\n\nIt's wild that a tiny 1 GB-RAM device from 2016 can now handle this so easily.\n\n### Why Private Locations?\n\nRunning a private probe means your checks happen **from within your own network** - not just from public cloud servers.\n\nIt’s useful for:\n- Monitoring internal applications or databases\n- Testing network latency from real-world locations (like your office or home)\n- Running low-cost monitoring nodes on tiny devices (e.g. in agriculture or industrial setups)\n\n---\n\n## Closing Thoughts\n\nAnd that’s it - my **Raspberry Pi (2016)** is officially back in action. Running quietly next to my receiver, it’s now part of the openstatus network, checking endpoints from my home.\n\nNot bad for a 9-year-old piece of hardware.\n\nIf you'd like to try this out yourself, contact us ([discord](https://openstatus.dev/discord), [email](mailto:ping@openstatus.dev)) and head over to:\n\n[pkgs/container/private-location](https://github.com/openstatusHQ/openstatus/pkgs/container/private-location)\n\nPrivate locations are currently in **beta** and we're gathering feedback from early users. If you've got an old Raspberry Pi lying around - this is your excuse to bring it back to life.\n\n---\n\n**A few more useful Commands**\n\n```shell\n# stop running\n$ docker stop openstatus-private-location\n# remove the container, not the image or data:  \n$ docker rm openstatus-private-location\n# remove unused images (docker keeps old images even after pulling updates)\n$ docker image prune -f\n# check docker image statistics\n$ docker stats\n# check the CPU / RAM usage (each work)\n$ htop\n$ top\n# update docker image\n$ docker stop openstatus-private-location\n$ docker rm openstatus-private-location\n$ docker pull ghcr.io/openstatushq/private-location:latest\n$ docker run ...  # same command as before\n```"
  },
  {
    "path": "apps/web/src/content/pages/blog/dynamic-breadcrumb-nextjs.mdx",
    "content": "---\ntitle: \"Dynamic Breadcrumb in Next.js with Parallel Routes\"\ndescription: \"Learn how to build dynamic breadcrumbs in Next.js using parallel routes and server-side rendering for automatic navigation that updates based on the current page hierarchy.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2024-08-19\"\nimage: \"/assets/posts/dynamic-breadcrumb-nextjs/breadcrumb.png\"\ncategory: \"engineering\"\n---\n\nIn this post, we'll dive into the process of creating dynamic breadcrumbs in Next.js using parallel routes. Our goal is to build a breadcrumb component that automatically updates based on the current page and its hierarchy, all while leveraging server-side rendering for optimal performance.\n\n## Introduction\n\nBreadcrumbs are an essential navigation aid in web applications, helping users understand their current location within a site's hierarchy. With Next.js 13's introduction of parallel routes, we have new tools to create more dynamic and flexible navigation structures.\n\n\nYou can find the complete, working example for this tutorial on [GitHub](https://github.com/openstatusHQ/nextjs-dynamic-breadcrumb/). The project uses Next.js 15 and demonstrates the implementation of parallel routes alongside our dynamic breadcrumb.\n\n\n## The Challenge\n\nWe're building a website that showcases a catalog of dogs and cats. Our primary challenge is to create a breadcrumb that adapts to the user's current location within the site structure.\n\n### Project Structure\n\nOur repository is organized as follows:\n\n\n```\nsrc/\n    app/\n        about/\n            page.tsx\n        cats/\n            page.tsx\n            [id]/\n                page.tsx\n        dogs/\n            page.tsx\n            [id]/\n                page.tsx\n```\n\n\n### Desired Breadcrumb Behavior\n\n\n1. On the homepage:\n\n`Home`\n\n2. On pet pages (e.g., Dogs or Cats):\n\n`Home > Dogs` or `Home > Cats`\n\n3. On individual pet pages:\n\nHere we only have the pet id, so we need to fetch the pet name from the server side and display it in the breadcrumb.\n\n`Home > Dogs > Dog Name`\n\nThe key is to fetch the necessary data on the server side and dynamically update the breadcrumb based on the current route instead of displaying the pet id.\n\n\n\n### Parallel route 🚀\n\n[Parallel routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) in Next.js allow us to render multiple pages in the same layout simultaneously. This feature is particularly useful for our breadcrumb implementation as it allows us to maintain a consistent navigation structure across different page types.\n\n\n#### Homepage\n\nIn our root layout  we will use slot to render the breadcrumb component.\n\n```tsx\nexport default function RootLayout({\n\tbreadcrumb,\n\tchildren,\n}: Readonly<{\n\tbreadcrumb: React.ReactNode;\n\tchildren: React.ReactNode;\n}>) {\n\treturn (\n\t\t<html lang=\"en\">\n\t\t\t<body>\n\t\t\t\t{breadcrumb}\n\t\t\t\t{children}\n\t\t\t</body>\n\t\t</html>\n\t);\n}\n```\n\nWe need to create a new folder `@breadcrumb` in the `app` folder\n\nHere we create a new file `page.tsx` that will be responsible for rendering the breadcrumb.\n\n```tsx\nimport {\n\tBreadcrumb,\n\tBreadcrumbItem,\n\tBreadcrumbLink,\n\tBreadcrumbList,\n} from \"@/components/ui/breadcrumb\";\n\nexport default function BreadcrumbSlot() {\n\treturn (\n\t\t<Breadcrumb>\n\t\t\t<BreadcrumbList>\n\t\t\t\t<BreadcrumbItem>\n\t\t\t\t\t<BreadcrumbLink href=\"/\">Home</BreadcrumbLink>\n\t\t\t\t</BreadcrumbItem>\n\t\t\t</BreadcrumbList>\n\t\t</Breadcrumb>\n\t);\n}\n```\n\n#### Pet pages\n\nWe also want to create a catch all components for the dynamic routes in the `app` folder, to achieve this we need to create a new file `page.tsx` in the `@breadcrumb/[...all]` folder.\n\n```tsx\nimport {\n\tBreadcrumb,\n\tBreadcrumbItem,\n\tBreadcrumbLink,\n\tBreadcrumbList,\n\tBreadcrumbPage,\n\tBreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport React from \"react\";\nimport type { ReactElement } from \"react\";\n\nexport default function BreadcrumbSlot({\n\tparams,\n}: { params: { all: string[] } }) {\n\tconst breadcrumbItems: ReactElement[] = [];\n\tlet breadcrumbPage: ReactElement = <></>;\n\tfor (let i = 0; i < params.all.length; i++) {\n\t\tconst route = params.all[i];\n\t\tconst href = `/${params.all.at(0)}/${route}`;\n\t\tif (i === params.all.length - 1) {\n\t\t\tbreadcrumbPage = (\n\t\t\t\t<BreadcrumbItem>\n\t\t\t\t\t<BreadcrumbPage className=\"capitalize\">{route}</BreadcrumbPage>\n\t\t\t\t</BreadcrumbItem>\n\t\t\t);\n\t\t} else {\n\t\t\tbreadcrumbItems.push(\n\t\t\t\t<React.Fragment key={href}>\n\t\t\t\t\t<BreadcrumbItem>\n\t\t\t\t\t\t<BreadcrumbLink href={href} className=\"capitalize\">\n\t\t\t\t\t\t\t{route}\n\t\t\t\t\t\t</BreadcrumbLink>\n\t\t\t\t\t</BreadcrumbItem>\n\t\t\t\t</React.Fragment>,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn (\n\t\t<Breadcrumb>\n\t\t\t<BreadcrumbList>\n\t\t\t\t<BreadcrumbItem>\n\t\t\t\t\t<BreadcrumbLink href=\"/\">Home</BreadcrumbLink>\n\t\t\t\t</BreadcrumbItem>\n\t\t\t\t{breadcrumbItems}\n\t\t\t\t<BreadcrumbSeparator />\n\t\t\t\t{breadcrumbPage}\n\t\t\t</BreadcrumbList>\n\t\t</Breadcrumb>\n\t);\n}\n```\nThis code was taken from the [Jeremy's post](https://jeremykreutzbender.com/blog/app-router-dynamic-breadcrumbs) about dynamic breadcrumbs in Next.js.\n\n#### Dogs and Cats pages\n\nWe need to create a new file `page.tsx` in the `@breadcrumb/dogs/[id]` folder, we must follow the exact same structure as our specific pet pages.\nIn this component we will fetch the pet name from the server side and display it in the breadcrumb.\n\n```tsx\n\nimport {\n\tBreadcrumbItem,\n\tBreadcrumbLink,\n\tBreadcrumbList,\n\tBreadcrumbPage,\n\tBreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\n\nexport default async function BreadcrumbSlot({params}: {params: {id: string}}) {\n\t// Fetch our cat information from the database\n\tconst cat = await fetchCat({id: params.id});\n\n\treturn (\n\t\t<BreadcrumbList>\n\t\t\t<BreadcrumbItem>\n\t\t\t\t<BreadcrumbLink href=\"/\">Home</BreadcrumbLink>\n\t\t\t</BreadcrumbItem>\n\t\t\t<BreadcrumbSeparator />\n\t\t\t<BreadcrumbItem>\n\t\t\t\t<BreadcrumbLink href=\"/cats\">Cats</BreadcrumbLink>\n\t\t\t</BreadcrumbItem>\n\t\t\t<BreadcrumbSeparator />\n\t\t\t<BreadcrumbItem>\n\t\t\t\t<BreadcrumbPage className=\"capitalize\">{cat.name}</BreadcrumbPage>\n\t\t\t</BreadcrumbItem>\n\t\t</BreadcrumbList>\n\t);\n}\n```\n\n\n## Conclusion\n\nBy leveraging Next.js parallel routes and server-side rendering, we've created a dynamic breadcrumb component that updates based on the current route and fetches data efficiently on the server side. This approach provides a smooth user experience while maintaining good performance.\n\nRemember to check out the full working example on [GitHub](https://github.com/openstatusHQ/nextjs-dynamic-breadcrumb/) to see how all the pieces fit together in a Next.js 15 project.\n\nHappy coding!"
  },
  {
    "path": "apps/web/src/content/pages/blog/event-analytics-implementation.mdx",
    "content": "---\ntitle: \"How We Implemented Event Analytics with OpenPanel\"\ndescription: \"How we implemented event analytics at OpenStatus using OpenPanel with tRPC and Hono middlewares for clean, reusable tracking across our stack.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2024-12-27\"\nimage: \"/assets/posts/event-analytics-implementation/event-analytics-implementation.png\"\ncategory: \"engineering\"\n---\n\nWe had never really tracked events properly. We had some basic tracking in place, but it was not very useful. It is time to improve that with [OpenPanel](https://openpanel.dev?ref=openstatus).\n\nAfter some research, we finally settled on leveraging tRPC and Hono middlewares. Shoutout to [Midday](https://midday.ai?ref=openstatus) for the (tRPC) inspiration. They use a similar approach with [next-safe-action](https://next-safe-action.dev?ref=openstatus) for their server actions.\n\nThis post is not a step-by-step guide but instead presents the core concepts and ideas behind the implementation. Please refer to the [Hono](https://hono.dev?ref=openstatus) or [tRPC](https://trpc.io?ref=openstatus) documentation for more detailed information and our [GitHub](https://openstatus.dev/github) repository for the full implementation.\n\n---\n\nFirst, let's start with the basics. We need to define the events we want to track, like `page_created`, `user_created`, etc.\n\n```ts\n// packages/analytics/src/events.ts\nexport type EventProps = {\n  name: string;\n  channel: string;\n};\n\nexport const Events = {\n  CreatePage: {\n    name: \"page_created\",\n    channel: \"page\",\n  },\n  UpdatePage: {\n    name: \"page_upated\",\n    channel: \"page\",\n  },\n  // ... add more events\n} as const satisfies Record<string, EventProps>;\n```\n\nNext, we need to initialize OpenPanel (see [installation](https://openpanel.dev/docs/sdks/javascript?ref=openstatus)) and set up the analytics in our application.\n\n```ts\n// packages/analytics/src/index.ts\nimport {\n  OpenPanel,\n  type PostEventPayload,\n  type IdentifyPayload,\n} from \"@openpanel/sdk\";\nimport { type EventProps } from \"@openstatus/analytics\";\n\nconst op = new OpenPanel({\n  clientId: process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID,\n  clientSecret: process.env.OPENPANEL_CLIENT_SECRET,\n});\n\nexport async function setupAnalytics(props: Partial<IdentifyPayload>) {\n  if (props.profileId) {\n    await op.identify(props):\n  }\n\n  return {\n    track: (opts: EventProps & PostEventPayload[\"properties\"]) => {\n      const { name, ...rest } = opts;\n      return op.track(name, rest);\n    },\n  };\n}\n```\n\nNow that we have the basic setup in place, we can start implementing the tracking in our application. We will use the tRPC middleware and metadata to track events. Below, we define the `trackEvent` middleware that will track the event after the procedure has been executed. An `enforceUserSession` middleware can be added to include the user's ID for tracking.\n\n```ts {8,12,35} /trackEvent/\n// packages/trpc/src/index.ts\nimport { after } from \"next/server\";\nimport { initTRPC } from \"@trpc/server\";\nimport { setupAnalytics, type EventProps } from \"@openstatus/analytics\";\nimport type { User } from \"@openstatus/auth\";\n\ntype Context = { user?: User };\ntype Meta = { track?: EventProps };\n\nexport const t = initTRPC\n  .context<Context>()\n  .meta<Meta>()\n  .create({ /* ... */ });\n\n\nconst trackEvent = t.middleware(async opts => {\n  const result = await opts.next(opts.ctx);\n  \n  if (!result.ok) return result;\n\n  if (opts.meta.track) {\n    after(async () => {\n      const identify = opts.ctx.user ? { userId: opts.ctx.user.id } : {};\n      const analytics = await setupAnalytics(identify);\n      await analytics.track(opts.meta.track);\n    })\n  }\n  return result;\n});\n\nconst enforceUserSession = t.middleware(async opts => { \n  // ... set user to ctx\n});\n\nexport const protectedProcedure = t.procedure\n  .use(enforceUserSession)\n  .use(trackEvent);\n```\n\nThe `after` function (similar to `waitUntil`) will execute the tracking after the procedure has been executed and won't block the response.\n\nThe `next()` return value has an `ok` boolean property to check if the procedure was successful. If not, we don't want to track the event.\n\nHow will we use it in a procedure? Adding a `meta` property will allow us to track the event by defining the event we want to track.\n\n```ts {8}\n// packages/trpc/src/router/page.ts\nimport { Events } from '@openstatus/analytics';\nimport { insertPageSchema } from \"@openstatus/db\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const pageRouter = createTRPCRouter({\n  create: protectedProcedure\n    .meta({ track: Events.CreatePage })\n    .input(insertPageSchema)\n    .mutation(async (opts) => { /* ... */ })\n});\n```\n\nVoilà! Each time you want to add tracking to a new procedure, you only need to add the `meta` property with the event you want to track. The middleware handles the rest.\n\n---\n\nNow, how do we track the events within our API? Let's start by adding the `trackMiddleware` function and only track the event if the response has been finalized. \n\n```ts /trackMiddleware/\n// app/server/src/middleware.ts\nimport { setupAnalytics, type EventProps } from \"@openstatus/analytics\";\nimport type { Context, Next } from \"hono\";\nimport type { User } from \"@openstatus/auth\";\n\nexport function trackMiddleware(event: EventProps) {\n  return async (c: Context<{ Variables: { user?: User } }, \"/*\">, next: Next) => {\n    await next();\n    \n    const isValid = c.res.status.toString().startsWith(\"2\") && !c.error;\n\n    if (isValid) {\n      setTimeout(async () => {\n        const analytics = await setupAnalytics({\n          profileId: c.get(\"user\")?.id,\n        });\n        await analytics.track(event);\n      }, 0);\n    }\n  };\n}\n```\n\nDepending on where you are running the server, you might want to replace `setTimeout` with `waitUntil` (cf workers, Vercel) or other functions that extend the lifetime of the request without blocking the response.\n\nAnd again, we check if there was an `error` before tracking the event. We don't want to track unsuccessful events.\n\nThe [`@hono/zod-openapi`](https://github.com/honojs/middleware/tree/main/packages/zod-openapi?ref=openstatus) routes have a `middleware` property that allows you to add middleware to the route. This is where we will add the tracking middleware.\n\n```ts {11}\n// apps/web/src/pages/post.ts\nimport { createRoute } from \"@hono/zod-openapi\";\nimport { Events } from \"@openstatus/analytics\";\nimport { trackMiddleware } from \"../middleware\";\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"page\"],\n  description: \"Create a new Page\",\n  path: \"/\",\n  middleware: [trackMiddleware(Events.CreatePage)],\n  request: { /* ... */ },\n  responses: { /* ... */},\n});\n\n// ...\n```\n\n---\n\nAnd that's it. With minimal code changes and the help of middlewares, we have implemented event tracking in our application. You can swap [OpenPanel](https://openpanel.dev?ref=openstatus) with any other analytics provider like PostHog, but give it a try, it's an amazing tool!\n\nBy the way, this approach can be used for audit log tracking for example as well.\n\nCheck out our [GitHub](https://openstatus.dev/github) repository for the full implementation and don't forget to leave a star if you found this helpful."
  },
  {
    "path": "apps/web/src/content/pages/blog/global-latency-monitoring-benchmark-hono-hetzner.mdx",
    "content": "---\ntitle: \"Global Latency Monitoring: Benchmarking a Hono App on Hetzner with openstatus\"\ndescription: \"We monitored a Hono app on Hetzner Finland from 17 global probes across Fly, Koyeb, and Railway for 7 days. Result: providers differ by less than 10% — geographic distance is what matters most.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2025-10-23\"\nimage: \"/assets/posts/global-latency-monitoring-benchmark-hono-hetzner/global.png\"\ncategory: \"engineering\"\nfaq:\n  - question: \"Does the cloud provider affect latency to Hetzner?\"\n    answer: \"Barely. In our 7-day benchmark from 17 probes across Fly, Koyeb, and Railway, providers in the same region reported less than 10% latency difference. For example, Fly and Koyeb probes in Frankfurt both showed ~80ms P95 to Hetzner Finland. Geographic distance is the dominant factor.\"\n  - question: \"What is the latency from different regions to Hetzner Finland?\"\n    answer: \"Frankfurt probes averaged ~80ms P95. US East probes were higher due to transatlantic routing. Singapore probes from Fly, Koyeb, and Railway all reported similar latencies with less than 10% difference between providers, confirming distance is the primary factor.\"\n  - question: \"Should I pick Fly, Koyeb, or Railway based on network performance?\"\n    answer: \"Network performance alone should not drive your decision. Our benchmark showed minimal latency differences between Fly, Koyeb, and Railway when monitoring the same target from the same region. Pick the PaaS that fits your workflow, pricing, and deployment needs — the network differences are negligible.\"\n  - question: \"How do you benchmark global latency with openstatus?\"\n    answer: \"Deploy your app on any provider, then configure openstatus to monitor it from multiple regions across Fly, Koyeb, and Railway simultaneously. openstatus checks all selected regions in parallel every minute, giving you a true multi-provider, multi-region latency baseline without internal network bias.\"\n---\n\nAt openstatus, we offer **global monitoring** to give you an unbiased, external view of your service's performance. To truly test our monitoring capabilities and gather real-world latency data, we decided to benchmark a simple application deployed outside our internal network.\n\n\nAs a software developer, I know that **latency** is critical for user experience. When building an application, understanding how your app performs from different locations around the world is essential.\n\n## The Benchmark Setup\n\nTo prevent internal networking biases from skewing the results, I deployed a minimal test application a [**Hono server**](https://github.com/openstatusHQ/status-code/) on third-party cloud providers, independent from openstatus's infrastructure.\n\n### Why Hetzner?\n\nI chose **Hetzner** for this experiment primarily for its **affordability** and robust infrastructure. It’s a popular choice for developers, making it a relevant target for benchmarking.\n\nThe test app is a single instance deployed in their **Finland datacenter**.\n\n> ⚠️ **A Note on Deployment Experience:** The initial setup experience with Hetzner involved some friction, including KYC verification, which felt a bit dated. To manage the deployment, I opted to use **Coolify Cloud**, which helped bridge the gap, but the inner developer in me definitely missed the simplicity of a managed **PaaS** during the process.\n\n\n<Image\n  alt=\"hetzner kyc\"\n  src=\"/assets/posts/global-latency-monitoring-benchmark-hono-hetzner/hetzner.jpg\"\n  width={800}\n  height={800}\n/>\n\n\n##  Monitoring Strategy with openstatus\n\nThe core of this experiment is to see how different cloud providers' network paths affect the latency to our Finland-based Hetzner app.\n\nWe've set up openstatus probes across multiple continents and providers, specifically leveraging monitoring locations from:\n\n- **Fly.io**\n- **Koyeb**\n- **Railway**\n\nThe goal is to monitor latency from similar geographic regions across these different providers to observe any network path variations.\n\n\n### openstatus Probe Configuration\n\nOur monitoring strategy is focused on diverse, yet regionally comparable, deployment regions:\n\n| Location          | Providers           |\n| ----------------- | ------------------- |\n| Frankfurt         | Koyeb, Fly          |\n| Amsterdam         | Fly, Railway        |\n| Paris             | Koyeb, Fly          |\n| US East           | Koyeb, Fly, Railway |\n| US West (LAX/SFO) | Koyeb, Fly, Railway |\n| Singapore         | Koyeb, Fly, Railway |\n| Tokyo             | Koyeb, Fly          |\n\n\nI have defined our monitor with the openstatus cli, and this is the exact configuration used for it :\n\n\n```yaml\n\"hetzner-monitoring\":\n  active: true\n  assertions:\n  - compare: eq\n    kind: statusCode\n    target: 200\n  frequency: 1m\n  kind: http\n  name: Hetzner Monitoring\n  openTelemetry: {}\n  public: true\n  regions:\n  - koyeb_fra\n  - cdg\n  - ams\n  - railway_europe-west4-drams3a\n  - fra\n  - koyeb_par\n  - koyeb_sfo\n  - railway_us-west2\n  - lax\n  - iad\n  - koyeb_was\n  - railway_us-east4-eqdc4a\n  - koyeb_sin\n  - railway_asia-southeast1-eqsg3a\n  - koyeb_tyo\n  - sin\n  - nrt\n  request:\n    method: GET\n    url: https://hetzner.openstat.us/\n  retry: 3\n```\n\nNote that non prefixed regions are Fly regions.\n\n### Analyzing the Results\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/global-latency-monitoring-benchmark-hono-hetzner/hetzner.json\"\n    caption=\"Hetzner data center global latency benchmark from multiple providers between 16 Oct and 23 Oct 2025 aggregated in a 1h window.\"\n  />\n</div>\n\n#### 1. Regional Consistency Across Providers\n\nIn regions geographically close to the Hetzner Finland datacenter, probes deployed on different cloud providers showed remarkably similar performance.\n\nFor instance, from **Frankfurt, Germany**:\n\n- The latency from the **Koyeb** probe and the **Fly** probe had an almost identical **P95 latency of ≈80ms**.\n\nThis indicates that in proximate locations, the difference in the cloud provider's network path to the target server is minimal; the dominant factor is the geographical distance and core internet routing.\n\n#### 2. Distance is the Primary Factor\n\nThe most significant finding reinforces a basic network principle: **the further you are from the server location, the longer the latency is.**\n\n#### 3. Consistency Even at Extreme Distance\n\nEven across vast distances, the latency differences between providers remained small.\n\nIn **Singapore**, for example, the probes from **Fly**, **Koyeb**, and **Railway** all reported very similar latencies, often with **less than a 10% difference** between their average results. This suggests that while all are far, none of the providers offer a vastly superior or inferior route for transcontinental communication in this case.\n\n\n## Conclusion\n\nThrough this global setup, I gained valuable insights into external network performance. I confirmed that the core challenge remains **geographic distance**, but the multi-provider approach helped us establish a **reliable baseline** for external connections. Developers can use this methodology to move past internal network bias and truly understand their app's latency profile from various points on the globe.\n\nMy 2cts, if you want to pick a PaaS to deploy your next project, pick the one that fits your needs the best, because in the end, the network differences are minimal!\n\nIf you want to try monitoring our app from multiple global locations, you can try our [global speed checker](/play/checker) for free.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/hono-vercel-fluid-compute.mdx",
    "content": "---\ntitle: \"Hono on Vercel: A Performance Deep Dive into Fluid Compute\"\ndescription: \"Deploying a Hono server on Vercel and benchmarking Fluid Compute performance — comparing cold start vs warm server latency with OpenStatus monitoring.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2025-08-27\"\nimage: \"/assets/posts/hono-vercel-fluid-compute/hono.png\"\ncategory: \"engineering\"\n---\n\n\n\nThis article details how to deploy a new Hono server on Vercel and monitor it using OpenStatus, with a focus on observing the impact of Vercel's Fluid Compute.\nWe'll compare the performance of a \"warm\" server, which is regularly pinged, against a \"cold\" server that remains idle.\n\n\n## Our Setup\n\n#### Our Setup\n\nFirst, we set up our Hono server using Vercel's [zero-configuration deployment](https://hono.dev/docs/getting-started/vercel):\n\n1. We created a new Hono project: `pnpm create hono@latest`.`\n2. We navigated into the new directory: `cd new-directory`\n3. We followed Vercel's zero-configuration deployment instructions for Hono backends.\n4. We deployed the application using `vc deploy`.\n\nWe repeated this process to create two identical servers. One server is designated as \"warm,\" receiving a request every minute to prevent it from going idle. The other is \"cold,\" and we only send a request to it once per hour to observe the impact of cold starts. Both servers were hosted in the `IAD1` region.\n\n\nNext, we configured monitoring with openstatus. We created a new monitor with the following YAML configuration.\n\nThis is the configuration for the \"cold\" server:\n\n```yaml\n# yaml-language-server: $schema=https://www.openstatus.dev/schema.json\n\n\"hono-cold\":\n  active: true\n  assertions:\n  - compare: eq\n    kind: statusCode\n    target: 200\n  description: Monitoring Hono App on Vercel\n  frequency: 1h\n  kind: http\n  name: Hono Vercel Cold\n  public: true\n  regions:\n  - arn\n  - ams\n  - atl\n  - bog\n  - bom\n  - bos\n  - cdg\n  - den\n  - dfw\n  - ewr\n  - eze\n  - fra\n  - gdl\n  - gig\n  - gru\n  - hkg\n  - iad\n  - jnb\n  - lax\n  - lhr\n  - mad\n  - mia\n  - nrt\n  - ord\n  - otp\n  - phx\n  - scl\n  - sea\n  - sin\n  - sjc\n  - syd\n  - yul\n  - yyz\n  request:\n    headers:\n      User-Agent: OpenStatus\n    method: GET\n    url: https://hono-cold.vercel.app/\n  retry: 3\n```\nWe have deployed it using the [openstatus cli](https://docs.openstatus.dev/tutorial/get-started-with-openstatus-cli/).\n\n```bash\nopenstatus monitors apply\n```\n\n\n### Our metrics\n\nThese are our metrics for both cold and warm deployments from the last 24 hours.\n\n#### Warm\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILING\n\n</div>\n<div>\n\n**47,520**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**171**ms P50\n\n</div>\n<div>\n\n**275**ms P75\n\n</div>\n<div>\n\n**343**ms P90\n\n</div>\n<div>\n\n**417**ms P95\n\n</div>\n<div>\n\n**524**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/hono-vercel-fluid-compute/hono-warm.json\"\n    caption=\"hono warm p50 latency between 18. Augh and 25. Aug 2025 aggregated in a 1h window.\"\n  />\n</div>\n\n#### Cold\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILING\n\n</div>\n<div>\n\n**792**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**212**ms P50\n\n</div>\n<div>\n\n**333**ms P75\n\n</div>\n<div>\n\n**439**ms P90\n\n</div>\n<div>\n\n**529**ms P95\n\n</div>\n<div>\n\n**639**ms P99\n\n</div>\n</Grid>\n\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/hono-vercel-fluid-compute/hono-cold.json\"\n    caption=\"Vercel edge p50 latency between 18. Augh and 25. Aug 2025 aggregated in a 1h window.\"\n  />\n</div>\n\n\n### Analysis and Discussion\n\nWhen we compare our results, the warm server's performance is significantly faster, as expected. Its p99 latency is 524ms, while the cold server's p99 latency is 639ms. This 115ms difference highlights the overhead of a cold start. However, when we compare this to a similar test we ran with the previous Node.js runtime, the performance is notably better.\n\n> Read our blog post: [Monitoring latency: Vercel Serverless Function vs Vercel Edge Function](/blog/monitoring-latency-vercel-edge-vs-serverless)\n\n### The Good\n\n- **Excellent Developer Experience (DX):** Deploying a Hono server on Vercel is incredibly simple, requiring just a couple of commands. The zero-configuration setup is a major plus for developers.\n- **Performance Improvements:** Fluid Compute provides a tangible improvement over the previous Vercel Node.js runtime. It reduces the impact of cold starts and makes the serverless experience more efficient.\n\n\n### The Bad\n\n- **Deprecation of Edge Functions:** Vercel has deprecated its dedicated Edge Functions in favor of a unified Vercel Functions infrastructure that uses Fluid Compute. While this unifies the platform, it might force a transition for existing projects.\n- **Cost Considerations:** While Fluid Compute aims for efficiency, the \"warm\" server, which is active for roughly 8 minutes and 20 seconds per day, translates to over 400 minutes of usage per month. This could exceed the free tier's limits, depending on the specific CPU time and memory usage, requiring a paid plan.\n- **Complexity:** The new pricing model, which combines active CPU time, provisioned memory, and invocations, can be more complex to track and predict than the simpler invocation-based pricing of the past.\n\n\n\n### Conclusion\n\nIn conclusion, deploying a Hono server on Vercel offers excellent developer experience. However, the deprecation of Edge Functions and the complexity of the new pricing model are potential drawbacks.\n\n\n\n_Create an account on [OpenStatus](/app/sign-up) to\nmonitor your API and get notified when your latency increases._\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/how-we-build-our-github-action.mdx",
    "content": "---\ntitle: \"How We Built our own GitHub Action\"\ndescription: \"How we built a custom Docker-based GitHub Action to run OpenStatus synthetic tests on every push — a speedrun guide to building your own GitHub Action.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2025-02-26\"\nimage: \"/assets/posts/how-we-build-our-github-action/GitHub.png\"\ncategory: \"engineering\"\n---\n\nA couple of weeks ago, when we released our CLI, we wanted to use it to run our own Synthetics Tests in a GitHub Action. We aimed to have a simple way to run our tests on every push to our main branch or on a schedule.\n\nThere are three ways to build a GitHub Action:\n\n- Composite actions\n- Javascript actions\n- Docker container actions\n\n\nOur CLI is built in Golang, and we publish every new version as a binary.  It made sense to go with the Docker container actions.\nThis way we could use our CLI to run the tests.\n\nWe needed to have a way to pass the API key and the configuration file to the action.\n\n\nLet's start our speedrun on building our own custom Docker GitHub Action.\n\n### Create a Dockerfile\n\nOur Docker file is pretty simple. We use the alpine image and install curl to download the CLI. We then extract the CLI and set the entrypoint to the entrypoint.sh file.\n\n\n```Dockerfile\nFROM alpine:3.21.3\n\nRUN apk --no-cache add curl\n\nWORKDIR /home/openstatus\n\nCOPY entrypoint.sh .\n\nRUN  curl -o cli.tar.gz -L  https://github.com/openstatusHQ/cli/releases/latest/download/cli_Linux_x86_64.tar.gz\n\nRUN tar -xf ./cli.tar.gz\n\nENTRYPOINT [\"/home/openstatus/entrypoint.sh\"]\n```\n\nIt results in a small image of around 25 MB, which is perfect for a GitHub Action.\n\nTo download the latest version of the CLI we use the following URL.\n\n```\nhttps://github.com/openstatusHQ/cli/releases/latest/download/cli_Linux_x86_64.tar.gz\n```\n\n\nOur entrypoint.sh file is also quite simple. We just run the CLI with the API key and the configuration file.\n\n```bash\n#!/bin/sh\n\nif [ -z \"$INPUT_CONFIG_PATH\" ]; then\n    /home/openstatus/openstatus run --access-token $INPUT_API_KEY\nelse\n    /home/openstatus/openstatus run --access-token $INPUT_API_KEY --config $INPUT_CONFIG_PATH\nfi\n\nif [ $? -eq 0 ]\nthen\n    echo \"OpenStatus run successfully\"\n    exit 0\nelse\n    echo \"OpenStatus run failed\"\n    exit 1\nfi\n```\n\n### Create the action.yml file\n\nOur action.yml file\n\n```yaml\nname: 'OpenStatus Synthetics CI'\ndescription: 'Run your OpenStatus synthetics checks as part of your GitHub Actions workflow.'\nauthor: 'OpenStatus'\nbranding:\n  icon: 'zap'\n  color: gray-dark\n\ninputs:\n  api_key:\n    description: 'OpenStatus API key'\n    required: true\n  config_path:\n    description: 'Path to the OpenStatus configuration file'\n    required: false\n\nruns:\n  using: docker\n  image: docker://ghcr.io/openstatushq/action:latest\n  args:\n    - ${{ inputs.api_key }}\n    - ${{ inputs.config_path }}\n```\n\nThat's all you need to build your own Docker custom GitHub Action.\n\n\n\nI struggle a bit with the GitHub action because the docker image was being built on the fly.\nI had to push the image to the GitHub Container Registry to be able to use it in the action.  You can use any other container registry but GitHub Container Registry is free for public repositories.\n\nThe solution was to change the image in the action.yml file to the image in the GitHub Container Registry.\n\nFrom:\n\n```yaml\nimage: Dockerfile\n```\nTo:\n```yaml\nimage: docker://ghcr.io/openstatushq/action:latest\n```\n\n### Publish it to the GitHub marketplace\n\nAdd branding to your action and your action is ready. You can publish it to the GitHub Marketplace.\n\n\n### Conclusion\n\nBuilding a GitHub Action is pretty simple. We choose to use a Docker container action because we wanted to use our CLI to run the tests.\n\n\n\nIf you want to start using our action you can find it in the GitHub Marketplace.\n\n[https://github.com/marketplace/actions/openstatus-synthetics-ci](https://github.com/marketplace/actions/openstatus-synthetics-ci\n)\n\n\nIf you want to see a GitHub repository with the action you can find it here.\n\n[https://github.com/openstatusHQ/github-action-tester/actions](https://github.com/openstatusHQ/github-action-tester/actions\n)\n\n\n_Create an account on [OpenStatus](/app/sign-in?ref=blog-github-action) and start running your own Synthetics Tests in your GitHub Actions._"
  },
  {
    "path": "apps/web/src/content/pages/blog/introducing-goatstack.mdx",
    "content": "---\ntitle: \"Introducing the GoaT stack\"\ndescription: \"Introducing the GoaT stack — a full-stack app template with Golang, SQLite, ConnectRPC, React, and TanStack Router for type-safe, schema-first development.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2025-03-24\"\nimage: \"/assets/posts/introducing-goatstack/goat.png\"\ncategory: \"engineering\"\n---\n\n## What is the GoaT stack?\n\nThe GoaT stack is a [full-stack app template](https://github.com/openstatusHQ/goat-stack) featuring a Golang server and a Vite + React SPA front end.\n\nWe want the GoaT stack to have a great DX.\n\n### What is included in the GoaT stack?\n\n**[SQLite](https://www.sqlite.org/)**: We have chosen SQLite as the database because we love it. It’s just a file.\n**[LiteStream](https://litestream.io/)**: To backup our database\n\n\n**[Protocol Buffer with ConnecTRPC](https://connectrpc.com/)**: For their http based protocol that works with Protocol Buffer.\n\n**[Chi](https://go-chi.io)**: For our Golang router\\\n**[connectrpc.com/connect](https://connectrpc.com/connect)**: To handle http based request\n\n\n**[React](https://react.dev/)**: Our frontend framework of choice.\\\n**[TanStack Query + Connect-Query](https://github.com/connectrpc/connect-query-es)**: to generate our typesafe query.\\\n**[TanStack Router](https://tanstack.com/router/latest)**: The typed router is a joy to work with.\n\n\n\n### API: Schema First Approach\n\nUsing Protocol Buffer we can design our API in `proto` file\n\n```proto\nsyntax = \"proto3\";\n\npackage goat.v1;\n\nenum Vote {\n  YES = 0;\n  NO = 1;\n}\n\nmessage VoteRequest {\n    Vote Vote = 1;\n}\nmessage VoteResponse {\n  bool Success = 1;\n}\n```\n\n### Why not going full stack TypeScript\n\n\n<a href=\"https://bsky.app/profile/did:plc:eu6cezqsf5yocjsyc7mgkued/post/3lkeag64a2s2x\"><Image\n  alt=\"New users per week\"\n  src=\"/assets/posts/introducing-goatstack/bsky-post.png\"\n  width={650}\n  height={575}\n/></a>\n\n\nWe have worked in a large monorepo, we have often been frustrated by the number of times we had to restart our TypeScript LSP.\n\n\n\n\nWe love Golang. We heavily rely on it for OpenStatus.\n\n## How to get started with the Goat Stack\n\n\n### Requirements\n\nTo get started you need to have these  installed on your computer\n\n- [Just](https://just.systems)\n- [Golang](https://go.dev/)\n- [pnpm](https://pnpm.io)\n- [Node](https://nodejs.org/en)\n\n\n\n### Get Started\n1. First clone our repository : [https://github.com/openstatusHQ/goat-stack](https://github.com/openstatusHQ/goat-stack)\n\n2. Run\n```bash\njust init\n```\n\nIt will download all the dependancies.\n\n3. Open your IDE and update `packages/proto/goat/v1/goat.proto`\nAdd new procedure in the GoatService\n\n```proto\nservice GoatService {\n    rpc GetVotes(GetVotesRequest) returns (GetVotesResponse) {}\n    rpc Vote(VoteRequest) returns (VoteResponse) {}\n}\n\nmessage GetVotesRequest {}\n\nmessage GetVotesResponse {\n  int64 Yes = 1;\n  int64 No = 2;\n}\n\n```\n\n4. Run `just buf`\n\n5. Implement the handler in the server  `apps/server/internal/goat/handler.go`\n\n```go\nfunc (h *goatHandler) Vote(ctx context.Context, req *connect.Request[goatv1.VoteRequest]) (*connect.Response[goatv1.VoteResponse], error) {\n\ttx := h.db.MustBegin()\n\tvar value string\n\tswitch req.Msg.Vote {\n\tcase goatv1.Vote_YES:\n\t\tvalue = \"yes\"\n\t\tbreak\n\tcase goatv1.Vote_NO:\n\t\tvalue = \"no\"\n\t\tbreak\n\tdefault:\n\t\tbreak\n\t}\n\tr := tx.MustExec(\"INSERT INTO vote (timestamp, vote) VALUES ($1, $2)\", time.Now().Unix(), value)\n\ttx.Commit()\n\tres := connect.NewResponse(&goatv1.VoteResponse{\n\t\tSuccess: true,\n\t})\n\treturn res, nil\n}\n```\n\n6. Start calling it in your React App with the newly generated query\n\n```tsx\n// Our wrapper around tanstack query\nimport { useMutation } from \"@connectrpc/connect-query\";\nimport { createFileRoute, Link, useRouter } from \"@tanstack/react-router\";\n// Our generated query\nimport { vote } from \"../gen/proto/goat/v1/goat-GoatService_connectquery\";\n// Our generated types\nimport { Vote } from \"../gen/proto/goat/v1/goat_pb\";\nimport { Button } from \"@goat/ui/components/button\";\n\nexport const Route = createFileRoute(\"/\")({\n  component: App,\n});\n\nfunction App() {\n  // Use the mutation hook with our generated query\n  const v = useMutation(vote);\n  const { navigate } = useRouter();\n  return (\n    <div>\n      <div >\n        <p>Is this the 🐐 stack?</p>\n        <div>\n          <Button\n            variant={\"outline\"}\n            disabled={v.isPending}\n            onClick={async () => {\n              await v.mutateAsync({\n                Vote: Vote.YES,\n              });\n              navigate({ to: \"/results\" });\n            }}\n          >\n            Yes\n          </Button>\n          <Button\n            variant={\"outline\"}\n            disabled={v.isPending}\n            onClick={async () => {\n              await v.mutateAsync({\n                Vote: Vote.NO,\n              });\n              navigate({ to: \"/results\" });\n            }}\n          >\n            No\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n````\n\n\n### How to deploy it.\n\nWe provide 2 docker files to deploy the dashboard and the server where you want. For example our server for [GoatStack.dev](https://GoatStack.dev) is hosted on [Koyeb](https://www.koyeb.com/) and our dashboard on [Cloudflare](https://cloudflare.com/)\n\nBefore deploying the server you need to update `apps/server/etc/litestream.yml` with your S3 compatible bucket key to backup your database.\n\n```yaml\ndbs:\n  - path: /data/db\n    replicas:\n      - type: s3\n        endpoint: https://${CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com/\n        bucket: goat-stack\n        access-key-id: ${CLOUDFLARE_R2_ACCESS_KEY_ID}\n        secret-access-key: ${CLOUDFLARE_R2_SECRET_ACCESS_KEY}\n```\n\n\n### Conclusion\n\nWe hope you will enjoy using the GoaT stack as much as we do. We are looking forward to seeing what you will build with it. Feel free to reach out to us on [ping@openstatus](mailto:ping@openstatus.dev?subject=GoatStack) if you have any questions or feedback.\n\nAnd if you want to contribute to the GoaT stack, we would be more than happy to welcome you to our community.\n\nAnd create a free [OpenStatus account](/app/login) to monitor your server and get notified if something goes wrong.\n\nHappy coding! 🐐\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/introducing-openstatus-cli.mdx",
    "content": "---\ntitle: \"Level Up Your Monitoring: Introducing the Openstatus CLI\"\ndescription: \"Introducing the OpenStatus CLI — manage monitors as code, run synthetic tests from your terminal, and bring GitOps to your uptime monitoring workflow.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2025-08-02\"\nimage: \"/assets/posts/introducing-openstatus-cli/CLI.png\"\ncategory: \"engineering\"\n---\n\nAs developers, we live in the terminal, sometimes we are even stuck trying to quit NeoVim. At openstatus, we understand that deeply. That's why we're thrilled to announce the release of the openstatus CLI, a powerful new tool designed to bring your monitoring workflow directly into the environment you love.\n\n## From ClickOps to GitOps: A New Approach to Monitoring\n\nRemember the early days of openstatus? It was all about ClickOps – navigating dashboards, clicking buttons, and managing your monitors through the web application. While effective, we knew there was a better way for those who spend their days in their terminal. We wanted a more integrated experience, one that felt as natural and efficient as everything else you do in your terminal.\n\nThat vision led us to develop a command-line interface (CLI) that allows you to interact with openstatus directly from your terminal. We wanted to empower you to quickly grab essential information about your monitors and trigger tests.\n\n## Under the Hood: Why Go?\n\nWhen it came to building our CLI, the choice of Golang was clear, we are already using it for our probe. Its robust standard library, excellent concurrency primitives, and strong type system make it an ideal language for building fast, reliable, and portable command-line tools. Go's ability to compile into a single binary simplifies deployment and ensures a smooth experience across different operating systems. This focus on performance and ease of use perfectly aligns with our goal of providing a seamless terminal experience.\n\n## The Power of the Terminal: Meeting Developer Needs\n\nAs we were building openstatus, we listened closely to our users. A recurring theme emerged: while dashboards are great for an overview, more and more developers want to move beyond pure ClickOps. They want to integrate their monitoring directly into their development workflows, automate tasks, and manage their infrastructure as code.\n\nWe already have a [Terraform](https://registry.terraform.io/providers/openstatusHQ/openstatus/latest) provider but we wanted to use the same cli to trigger and modify monitors. Also we think YAML is more readable than HCL.\n\nThe openstatus CLI directly addresses these needs. Imagine being able to:\n\n\n### Export and manage monitors as code\n\nWe've made it possible to import your existing monitors from your openstatus dashboard.\nJust run `openstatus monitors import`\n\nThis means you can now manage your monitoring configurations as version-controlled files. Define your monitors in a YAML. To apply your change run `openstatus monitors apply`\n\nThis isn't just about convenience; it's about empowerment. The openstatus CLI allows you to treat your monitoring setup like any other part of your codebase, bringing the benefits of version control, automation, and a developer-centric workflow to your uptime and performance checks.\n\n\n\n### Trigger checks directly from your GitHub Actions\n\nIntegrate monitoring into your CI/CD pipeline, ensuring that every deployment is thoroughly tested before it goes live.\n\nHere's our custom GitHub action : [https://github.com/marketplace/actions/openstatus-synthetics-ci](https://github.com/marketplace/actions/openstatus-synthetics-ci)\n\nAnd if you want to install here's our GitHub Action worklow\n\n```yaml\nname: Run OpenStatus Synthetics CI\n\non:\n  workflow_run:\n      workflows: ['Fly Deploy']\n      types: [completed]\n      branches:\n        - main\n  repository_dispatch:\n      types:\n        - 'vercel.deployment.success'\n      branches:\n        - main\n\njobs:\n  synthetic_ci:\n    runs-on: ubuntu-latest\n    name: Run OpenStatus Synthetics CI\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Run OpenStatus Synthetics CI\n        uses: openstatushq/openstatus-github-action@v1\n        with:\n          api_key: ${{ secrets.OPENSTATUS_API_KEY }}\n```\n\nIf you want to see the action in action  we are using it in our main repository and running test after every deployment:\n\nhttps://github.com/openstatusHQ/openstatus/actions/workflows/synthetic.yml\n\n\n## Get Started Today!\n\nWe're incredibly excited about the openstatus CLI and believe it will fundamentally change how you interact with your monitoring. It's a significant step towards a more integrated, efficient, and developer-friendly monitoring experience.\n\nReady to take your monitoring to the next level? Head over to our [documentation](https://docs.openstatus.dev/tutorial/get-started-with-openstatus-cli/) to learn how to install and start using the openstatus CLI today.\n\nWe can't wait to hear what you think of it.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/introducing-status-page-theme-explorer.mdx",
    "content": "---\ntitle: \"Introducing: The Status Page Theme Explorer\"\ndescription: \"Customize and build your own status page experience.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2025-10-28\"\nimage: \"/assets/posts/introducing-status-page-theme-explorer/palette.png\"\ncategory: \"education\"\n---\n\nWho uses status pages the most? _Mainly Developers_. And to match their nerdiness, we’re adding more font-mono into the game. We’ve collaborated with [@aliszu](https://x.com/aliszu) to rebuilt our status page experience from the ground up and are providing you with more flexibility and control.\n\nA status page should feel at home with your brand - without requiring constant maintenance. It should also let you decide exactly how much data you want to share and at what level of detail.\nThe status-page is now a standalone app, fully separated from our main web project - continuing the great split after the [dashboard extraction](/blog/new-dashboard-we-are-so-back).\n\n**TL;DR**: create your own theme on [themes.openstatus.dev](https://themes.openstatus.dev/?t=supabase&b=true) + more page configuration\n\n---\n\n### Theme Explorer\n\nFrom day one, we’ve built with [shadcn/ui](https://ui.shadcn.com), and we’re sticking to it: same `CSS` variables, same philosophy. And luckily, it makes it easy to change themes!\n\n<Image\n  src=\"/assets/posts/introducing-status-page-theme-explorer/theme-explorer-supabase.png\"\n  alt=\"Supabase Theme\"\n/>\n\nWe’ve now extended those variables with custom ones for states like _maintenance_, _success_, and _degraded_, among others.\n\nWe’re starting with four themes:\n- our new default openstatus (Squared) theme,\n- a legacy rounded theme close to the current one,\n- a [Supabase](https://supabase.com) theme, and\n- a GitHub high-contrast theme.\n\nAnd here’s the fun part: **you can contribute your own themes**. \n\nGo to [themes.openstatus.dev](https://themes.openstatus.dev), open the **Theme Builder** by toggling the sidebar (e.g. `cmd + b`) and play around with different styles.\n\nOnce you’re happy with your setup:\n- Copy the configuration to your clipboard.\n- Create a new theme file with that config.\n- Append its ID to the `THEMES_LIST`.\n- Open a PR to contribute it!\n\n\nWe have a comprehensive [README](https://github.com/openstatusHQ/openstatus/tree/main/packages/theme-store) with more context.\n\nWe’ve also updated the local seed setup — making it easier to get started and preview your theme right away at http://localhost:3000/status.\n\n> Note, that we won’t accept every submission. We want to keep themes high-quality and recognizable - no random or off-brand experiments (sorry Santa 🎅).\n\nTo try the new experience on your own status page, **opt in to the redesign (beta)** ([Tutorial > How to configure a status page](https://docs.openstatus.dev/tutorial/how-to-configure-status-page/)) and select your preferred theme. Newly created pages will automatically opt in to the new theme.\n\n---\n\n### Shared Values Configuration\n\nThere’s more configuration. \n\nWe’ve heard from teams who wanted to **manually update their status page without connecting any synthetic monitors**. While we still believe _monitoring + transparency go hand in hand_, we don’t want to exclude users who just want to share updates directly.\n\n<Image\n  src=\"/assets/posts/introducing-status-page-theme-explorer/dashboard-status-page-redesign.png\"\n  alt=\"Status Page Dashboard Settings\"\n/>\n\nWith the new _“manual”_ type, you can only share status reports you are creating, regardless of the derived synthetic monitoring values.\n\nAll metrics, including uptime, will refer to your shared (manual) data.\nTo understand the underlying concept, check out our docs:\n[Concepts > Status Values and Uptime Calculation](https://docs.openstatus.dev/concept/uptime-calculation-and-values/) - where we explain how these values are derived, including details about the default _“absolute”_ type (which takes duration and request values to aggregate the uptime data).\n\n---\n\nWe’re not done yet. Next up: _grouped monitors_, _white-labeling support_, and _private themes_ - all coming to the new status page configuration soon.\n\n> Expect all status pages to be fully migrated to the new version early next year.\n\nIf you’re already using openstatus - or planning to - and feel a status page feature is missing, we’d love to hear from you. Contact us via [ping@openstatus.dev](mailto:ping@openstatus.dev) or by joining [Discord](https://openstatus.dev/discord)."
  },
  {
    "path": "apps/web/src/content/pages/blog/live-mode-infinite-query.mdx",
    "content": "---\ntitle: \"Live Mode\"\ndescription: \"How we leverage TanStack Infinite Query for implementing live mode\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2025-03-16\"\nimage: \"/assets/posts/live-mode-infinite-query/tanstack.png\"\ncategory: \"engineering\"\n---\n\nThis article is part of the [logs.run](https://logs.run) series.\n\nYou can enable the live mode right away via [logs.run/i?live=true](https://logs.run/i?live=true). \n\nNote that it's a demo. The data is mocked and not persisted. Live mode might take a while to load new data.\n\nWhile TanStack provides excellent documentation on [Infinite Queries](https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries), this article offers an additional practical example focusing on implementing a live data update.\n\n## Basic Concept\n\nInfinite queries work with \"pages\" of data. Each time you load new data, a new \"page\" is either appended (_load more_ older data) or prepended (_live mode_ newer data) to the `data.pages` array defined by the `useInfiniteQuery` hook. In the documentation, you'll read `lastPage` and `firstPage` to refer to the last and first page respectively. \n\nEach query to our API endpoint requires two key parameters:\n\n1. A `cursor` - a pointer indicating a position in the dataset\n2. A `direction` - specifying whether to fetch data before or after the cursor (\"prev\" or \"next\")\n\nA timeline sketch of the infinite query behavior:\n\n![Timeline with live mode and load more behavior](/assets/posts/live-mode-infinite-query/infinite-query.png)\n\n- The `nextCursor` is the timestamp of the last item of the page. The \"next\" page will only fetch items that are older than the `nextCursor`.\n- The `prevCursor` is the timestamp of the first item of the page (or the current timestamp). The \"prev\" page will only fetch items that are newer than the `prevCursor`.\n\n## API Endpoint\n\nYour API endpoint should return at minimum:\n\n```ts\ntype ReturnType<T> = {\n  // The single \"page\" data to be rendered\n  data: T[];\n  // The timestamp to be used for the next page on _load more_\n  nextCursor?: number | null;\n  // The timestamp to be used for the previous page on _live mode_\n  prevCursor?: number | null;\n};\n```\n\nWhen fetching older pages (\"next\" direction), we set a `LIMIT` clause (e.g., 40 items). However, when fetching newer data (\"prev\" direction), we return all data between the `prevCursor` and `Date.now()`.\n\nLet's take a look at an example implementation of the API endpoint:\n\n```ts\n// import ...\n\ntype TData = {\n  id: string;\n  timestamp: number;\n  // ...\n};\n\nexport async function GET(req: NextRequest) {\n  const searchParams = request.nextUrl.searchParams;\n  const cursor = searchParams.get(\"cursor\");\n  const direction = searchParams.get(\"direction\");\n\n  // Live mode\n  if (direction === \"prev\") {\n    const prevCursor = Date.now();\n    const data = await sql`\n      SELECT * FROM table\n      WHERE timestamp > ${cursor} AND timestamp <= ${prevCursor}\n      ORDER BY timestamp DESC\n    `;\n    const res: ReturnType<TData> = { data, prevCursor, nextCursor: null };\n    return Response.json(res);\n    // Load more\n  } else {\n    const data = await sql`\n      SELECT * FROM table\n      WHERE timestamp < ${cursor}\n      ORDER BY timestamp DESC\n      LIMIT 40\n    `;\n    const nextCursor = data.length > 0 ? data[data.length - 1].timestamp : null;\n    const res: ReturnType<TData> = { data, nextCursor, prevCursor: null };\n    return Response.json(res);\n  }\n}\n```\n\nKey points:\n\n- _Live mode_ (\"prev\" direction): Returns all new data between `Date.now()` and the `cursor` from the first page\n- _Load more_ (\"next\" direction): Returns 40 items before the `cursor` of the last page and updates `nextCursor`\n\n> **Important**: Be careful with timestamp boundaries. If items share the same timestamp, you might miss data because of the `>` comparison. To prevent data loss, include all items sharing the same timestamp as the last item in your query.\n\n### Avoid Using OFFSET with Frequent Data Updates in Non-Live Mode\n\nWhile it might be tempting to use the cursor as an `OFFSET` for pagination (e.g. `?cursor=1`, `?cursor=2`, ...), the following approach can cause problems when data is frequently prepended:\n\n```ts\nconst limit = 40;\nconst offset = limit * cursor;\n\nconst data = await sql`\n  SELECT * FROM table \n  ORDER BY timestamp DESC \n  LIMIT ${limit} \n  OFFSET ${offset}\n`;\n```\n\nWhen new items are prepended, they shift the offset values, causing duplicate items in subsequent queries.\n\n![Offset caveat example](/assets/posts/live-mode-infinite-query/offset-caveat.png)\n\n## Client Implementation\n\nLet's call our API endpoint from the client and use the dedicated infinite query functions that are added to the `useQuery` hook.\n\n```tsx\n\"use client\";\n\nimport React from \"react\";\nimport { useInfiniteQuery } from \"@tanstack/react-query\";\n\nconst dataOptions = {\n  queryKey: [\n    \"my-key\",\n    // any other keys, e.g. for search params filters\n  ],\n  queryFn: async ({ pageParam }) => {\n    const { cursor, direction } = pageParam;\n    const res = await fetch(\n      `/api/get/data?cursor=${cursor}&direction=${direction}`,\n    );\n    const json = await res.json();\n    // For direction \"next\": { data: [...], nextCursor: 1741526294, prevCursor: null }\n    // For direction \"prev\": { data: [...], nextCursor: null, prevCursor: 1741526295 }\n    return json as ReturnType;\n  },\n  // Initialize with current timestamp and get the most recent data in the past\n  initialPageParam: { cursor: new Date().getTime(), direction: \"next\" },\n\t// Function to fetch newer data\n  getPreviousPageParam: (firstPage, allPages) => {\n    if (!firstPage.prevCursor) return null;\n    return { cursor: firstPage.prevCursor, direction: \"prev\" };\n  },\n\t// Function to fetch older data\n  getNextPageParam: (lastPage, allPages) => {\n    if (!lastPage.nextCursor) return null;\n    return { cursor: lastPage.nextCursor, direction: \"next\" };\n  },\n};\n\nexport function Component() {\n  const { data, fetchNextPage, fetchPreviousPage } = useInfiniteQuery(dataOptions);\n\n  const flatData = React.useMemo(\n    () => data?.pages?.flatMap((page) => page.data ?? []) ?? [],\n    [data?.pages],\n  );\n\n  return <div>{flatData.map((item) => {/* render item */})}</div>;\n}\n```\n\nThe `getPreviousPageParam` and `getNextPageParam` functions receive the first and last pages respectively as their first parameter. This allows us to access the return values from the API, `prevCursor` and `nextCursor` and to track our position of the `cursor` in the dataset.\n\nTanStack provides helpful states like `isFetchingNextPage` and `isFetchingPreviousPage` for loading indicators, as well as `hasNextPage` and `hasPreviousPage` to check for available pages - especially useful for as we can hit the end of the load more values. Check out the [`useInfiniteQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery) docs for more details.\n\nBoth `fetchNextPage` and `fetchPreviousPage` can run independently and in parallel. TanStack Query manages appending and prepending pages to the `data.pages` array accordingly.\n\n## Implementing Auto-Refresh\n\nWhile TanStack Query provides a `refetchInterval` option, it would refetch all pages, growing increasingly expensive as more pages are loaded. Additionally, it doesn't reflect the purpose of live mode as instead of refreshing the data, we want to fetch newer data.\n\nTherefore, we implement a custom refresh mechanism for fetching only new data that you can add to any client component. Here's an simple example implementation of the `LiveModeButton`:\n\n```tsx\n\"use client\";\n\nimport * as React from \"react\";\nimport type { FetchPreviousPageOptions } from \"@tanstack/react-query\";\n\nconst REFRESH_INTERVAL = 5_000; // 5 seconds\n\ninterface LiveModeButtonProps {\n\tfetchPreviousPage?: (\n\t\toptions?: FetchPreviousPageOptions | undefined,\n\t) => Promise<unknown>;\n}\n\nexport function LiveModeButton({ fetchPreviousPage }: LiveModeButtonProps) {\n\t// or nuqs [isLive, setIsLive] = useQueryState(\"live\", parseAsBoolean)\n\tconst [isLive, setIsLive] = React.useState(false);\n\n\tReact.useEffect(() => {\n\t\tlet timeoutId: NodeJS.Timeout;\n\n\t\tasync function fetchData() {\n\t\t\tif (isLive) {\n\t\t\t\tawait fetchPreviousPage();\n\t\t\t\t// schedule the next fetch after REFRESH_INTERVAL \n\t\t\t\t// once the current fetch completes\n\t\t\t\ttimeoutId = setTimeout(fetchData, REFRESH_INTERVAL);\n\t\t\t} else {\n\t\t\t\tclearTimeout(timeoutId);\n\t\t\t}\n\t\t}\n\t\tfetchData();\n\n\t\treturn () => clearTimeout(timeoutId);\n\t}, [isLive, fetchPreviousPage]);\n\n\treturn <button onClick={() => setIsLive(!isLive)}>\n\t\t{isLive ? \"Stop live mode\" : \"Start live mode\"}\n\t</button>\n}\n```\n\nWe use `setTimeout` with recursion rather than `setInterval` to ensure each refresh only starts after the previous one completes. This prevents multiple simultaneous fetches when network latency exceeds the refresh interval and is a better UX.\n\n---\n\nGo check it out on [logs.run/i?live=true](https://logs.run/i?live=true).\n\nFor more details about our data table implementation, check out [The React data-table I always wanted](/blog/data-table-redesign) blog post.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/migrating-from-zod-openapi-to-connectrpc.mdx",
    "content": "---\ntitle: \"One API to Rule Them All: Migrating from zod-openapi to ConnectRPC\"\ndescription: \"We're unifying our internal tRPC API and public REST API into a single ConnectRPC service. Here's how we're doing it—and how AI is accelerating the migration.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2026-02-04\"\nimage: \"/assets/posts/migrating-from-zod-openapi-to-connectrpc/API.png\"\ncategory: \"engineering\"\n---\n\n**TL;DR:** Two APIs. Double the bugs. Double the work. We maintain tRPC for ourselves and REST for everyone else. We're unifying them with ConnectRPC—a schema-first API that's type-safe *and* curl-able. Here's how we're doing it, and how AI is accelerating the migration.\n\n---\n\n## The Problem: Two APIs, Double the Work\n\nHere's a confession: we weren't dogfooding our own public API.\n\nInternally, we use **tRPC** for its end-to-end type safety. For our public API, we built a separate REST layer with **zod-openapi**.\n\nThe result? The \"Split Stack\" problem:\n* **Two codebases.** Every endpoint defined twice.\n* **Two sets of types.** Internal types drifted from public DTOs.\n* **Half the velocity.** Every feature shipped twice. Every bug fix risked inconsistency.\n\nThis debt compounds. We needed to merge them—but we didn't want to sacrifice tRPC's DX or REST's accessibility.\n\nThe answer wasn't REST. It wasn't GraphQL. It was **ConnectRPC**.\n\n---\n\n## Why ConnectRPC Won\n\nWe evaluated several options. ConnectRPC stood out because it bridges strict contracts with developer ease.\n\n### 1. Better Client Generation Tooling\n\nOpenAPI can generate clients for multiple languages, but the tooling is hit-or-miss. For TypeScript, [Hey API](https://heyapi.dev/) is solid. For Go or Rust? You'll spend hours fixing the output. We learned this the hard way with our CLI.\n\nThe Buf ecosystem solves this. **Generated clients actually work.** No manual fixes, no weird edge cases. The proto schema compiles to idiomatic code in any language.\n\nHere's what our Monitor API looks like in proto:\n\n```protobuf\nsyntax = \"proto3\";\npackage openstatus.monitor.v1;\n\nimport \"buf/validate/validate.proto\";\n\nservice MonitorService {\n  rpc CreateHTTPMonitor(CreateHTTPMonitorRequest) returns (CreateHTTPMonitorResponse);\n  rpc UpdateHTTPMonitor(UpdateHTTPMonitorRequest) returns (UpdateHTTPMonitorResponse);\n  rpc DeleteMonitor(DeleteMonitorRequest) returns (DeleteMonitorResponse);\n  rpc ListMonitors(ListMonitorsRequest) returns (ListMonitorsResponse);\n  rpc GetMonitor(GetMonitorRequest) returns (GetMonitorResponse);\n  rpc TriggerMonitor(TriggerMonitorRequest) returns (TriggerMonitorResponse);\n}\n\nmessage HTTPMonitor {\n  string id = 1;\n  string name = 2 [(buf.validate.field).string = {\n    min_len: 1\n    max_len: 256\n  }];\n  string url = 3 [(buf.validate.field).string = {\n    min_len: 1\n    max_len: 2048\n    uri: true\n  }];\n  Periodicity periodicity = 4 [(buf.validate.field).enum = {\n    not_in: [0]  // Require a value, reject UNSPECIFIED\n  }];\n  repeated Region regions = 5 [(buf.validate.field).repeated.max_items = 28];\n  int64 timeout = 6;\n  HTTPMethod method = 7;\n  optional string body = 8;\n  repeated Headers headers = 9;\n  repeated StatusCodeAssertion statusCodeAssertions = 10;\n  repeated BodyAssertion bodyAssertions = 11;\n  repeated HeaderAssertion headerAssertions = 12;\n  bool followRedirects = 13;\n}\n\nmessage ListMonitorsRequest {\n  optional int32 limit = 1 [(buf.validate.field).int32 = {\n    gte: 1\n    lte: 100\n  }];\n  optional int32 offset = 2 [(buf.validate.field).int32.gte = 0];\n}\n```\n\nNotice the `buf.validate` constraints inline. This isn't documentation—it's executable validation that runs at runtime via protovalidate.\n\n### 2. Schema Linting and Breaking Change Detection\n\nBeyond client generation, Buf gives us guardrails we never had with OpenAPI.\n\nOur `buf.yaml`:\n\n```yaml\nversion: v2\n\nmodules:\n  - path: ./api/\n    name: buf.build/openstatus/api\n\ndeps:\n  - buf.build/bufbuild/protovalidate\n\nlint:\n  use:\n    - STANDARD\n  except:\n    - PACKAGE_VERSION_SUFFIX\n\nbreaking:\n  use:\n    - FILE\n```\n\nWhat this gives us:\n* **`buf lint`**: Catches style issues and naming conventions before PRs merge.\n* **`buf breaking`**: Detects backward-incompatible changes in CI. No more accidentally shipping a breaking SDK change.\n\n### 3. Still Curl-able (No gRPC Client Required)\n\nConnectRPC isn't \"gRPC-or-nothing.\" It speaks HTTP/JSON natively:\n\n```bash\n# Create a monitor\ncurl -X POST https://api.openstatus.dev/rpc/openstatus.monitor.v1.MonitorService/CreateHTTPMonitor \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-openstatus-key: YOUR_API_KEY\" \\\n  -d '{\n    \"monitor\": {\n      \"name\": \"My Website\",\n      \"url\": \"https://example.com\",\n      \"periodicity\": \"PERIODICITY_1M\",\n      \"regions\": [\"REGION_AMS\", \"REGION_IAD\"],\n      \"method\": \"HTTP_METHOD_GET\"\n    }\n  }'\n\n# List monitors\ncurl -X POST https://api.openstatus.dev/rpc/openstatus.monitor.v1.MonitorService/ListMonitors \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-openstatus-key: YOUR_API_KEY\" \\\n  -d '{\"limit\": 10}'\n```\n\nDebuggable in Chrome DevTools. No special tooling required.\n\n### 4. Ship Types, Not Docs\n\nDocumentation gets stale. Types don't compile if they're wrong.\n\nWith OpenAPI, your docs and implementation can drift apart silently. With Protobuf, the schema *is* the implementation contract. If the types don't match, the build fails.\n\nThis matters even more now that AI agents are consuming APIs. They work better with structured schemas than with prose documentation. For more on this idea, see Boris Tane's [Ship types, not docs](https://shiptypes.com?ref=openstatus.dev).\n\n\n\n---\n\n## How We Built It: Hono + ConnectRPC\n\nOur API runs on **Hono**. Here's the integration:\n\n### Router Setup\n\n```typescript\n// apps/server/src/routes/rpc/router.ts\nimport { createConnectRouter } from \"@connectrpc/connect\";\nimport { MonitorService } from \"@openstatus/proto/monitor/v1\";\nimport { NotificationService } from \"@openstatus/proto/notification/v1\";\nimport { StatusPageService } from \"@openstatus/proto/status_page/v1\";\n\nimport {\n  authInterceptor,\n  errorInterceptor,\n  loggingInterceptor,\n  validationInterceptor,\n} from \"./interceptors\";\n\n/**\n * Interceptors run in order (outermost to innermost):\n * 1. errorInterceptor - Catches all errors, maps to ConnectError\n * 2. loggingInterceptor - Logs requests/responses (wide events pattern)\n * 3. authInterceptor - Validates API key, sets workspace context\n * 4. validationInterceptor - Validates request messages via protovalidate\n */\nexport const routes = createConnectRouter({\n  interceptors: [\n    errorInterceptor(),\n    loggingInterceptor(),\n    authInterceptor(),\n    validationInterceptor(),\n  ],\n})\n  .service(MonitorService, monitorServiceImpl)\n  .service(NotificationService, notificationServiceImpl)\n  .service(StatusPageService, statusPageServiceImpl);\n```\n\n### Service Implementation: Just Business Logic\n\nHere's a real excerpt.\n\n```typescript\n// apps/server/src/routes/rpc/services/monitor/index.ts\nimport type { ServiceImpl } from \"@connectrpc/connect\";\nimport type { MonitorService } from \"@openstatus/proto/monitor/v1\";\n\nexport const monitorServiceImpl: ServiceImpl<typeof MonitorService> = {\n  async createHTTPMonitor(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n    const limits = rpcCtx.workspace.limits;\n\n    // Validation is already done by the interceptor via protovalidate\n    const mon = req.monitor!;\n\n    // Check workspace limits\n    await checkMonitorLimits(workspaceId, limits, mon.periodicity, mon.regions);\n\n    // Insert into database\n    const newMonitor = await db\n      .insert(monitor)\n      .values({\n        workspaceId,\n        jobType: \"http\",\n        url: mon.url,\n        method: httpMethodToString(mon.method),\n        body: mon.body || undefined,\n        headers: headersToDbJson(mon.headers),\n        assertions: httpAssertionsToDbJson(\n          mon.statusCodeAssertions,\n          mon.bodyAssertions,\n          mon.headerAssertions,\n        ),\n        ...getCommonDbValues(mon),\n      })\n      .returning()\n      .get();\n\n    return {\n      monitor: dbMonitorToHttpProto(newMonitor),\n    };\n  },\n\n  async listMonitors(req, ctx) {\n    const rpcCtx = getRpcContext(ctx);\n    const workspaceId = rpcCtx.workspace.id;\n\n    const limit = Math.min(Math.max(req.limit ?? 50, 1), 100);\n    const offset = req.offset ?? 0;\n\n    const monitors = await db\n      .select()\n      .from(monitor)\n      .where(and(\n        eq(monitor.workspaceId, workspaceId),\n        isNull(monitor.deletedAt)\n      ))\n      .limit(limit)\n      .offset(offset)\n      .all();\n\n    // Group by type\n    const httpMonitors: HTTPMonitor[] = [];\n    const tcpMonitors: TCPMonitor[] = [];\n    const dnsMonitors: DNSMonitor[] = [];\n\n    for (const m of monitors) {\n      switch (m.jobType) {\n        case \"http\": httpMonitors.push(dbMonitorToHttpProto(m)); break;\n        case \"tcp\":  tcpMonitors.push(dbMonitorToTcpProto(m));   break;\n        case \"dns\":  dnsMonitors.push(dbMonitorToDnsProto(m));   break;\n      }\n    }\n\n    return { httpMonitors, tcpMonitors, dnsMonitors, totalSize: monitors.length };\n  },\n};\n```\n\n### The Interceptor Pattern\n\nSimilar to Hono middleware, but with typed context via `createContextKey`:\n\n```typescript\n// apps/server/src/routes/rpc/interceptors/auth.ts\nimport { Code, ConnectError, type Interceptor, createContextKey } from \"@connectrpc/connect\";\n\nexport const RPC_CONTEXT_KEY = createContextKey<RpcContext | undefined>(undefined);\n\nexport function authInterceptor(): Interceptor {\n  return (next) => async (req) => {\n    const apiKey = req.header.get(\"x-openstatus-key\");\n\n    if (!apiKey) {\n      throw new ConnectError(\"Missing 'x-openstatus-key' header\", Code.Unauthenticated);\n    }\n\n    const { error, result } = await validateKey(apiKey);\n    if (error || !result.valid) {\n      throw new ConnectError(\"Invalid API Key\", Code.Unauthenticated);\n    }\n\n    const workspace = await lookupWorkspace(Number(result.ownerId));\n    req.contextValues.set(RPC_CONTEXT_KEY, { workspace, requestId: nanoid() });\n\n    return next(req);\n  };\n}\n```\n\nFor validation, we use `@connectrpc/validate` which runs protovalidate constraints automatically. See the full implementation in [our repo](https://github.com/openstatushq/openstatus/tree/main/apps/server/src/routes/rpc).\n\n---\n\n## Before and After: Validation Moves to the Schema\n\n**Before (zod-openapi):** Validation defined in TypeScript, separate from the OpenAPI spec.\n\n```typescript\nimport { createRoute, z } from \"@hono/zod-openapi\";\n\nconst HTTPMonitorSchema = z.object({\n  name: z.string().min(1).max(256),\n  url: z.string().url().max(2048),\n  frequency: z.enum([\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\"]),\n  regions: z.array(z.enum(AVAILABLE_REGIONS)),\n  headers: z.array(headerSchema).optional(),\n  method: z.enum([\"GET\", \"POST\", \"PUT\", \"DELETE\"]).default(\"GET\"),\n});\n\nconst postRoute = createRoute({\n  method: \"post\",\n  tags: [\"monitor\"],\n  path: \"/monitors/http\",\n  request: {\n    body: {\n      content: { \"application/json\": { schema: HTTPMonitorSchema } },\n    },\n  },\n  responses: {\n    200: {\n      content: { \"application/json\": { schema: MonitorSchema } },\n      description: \"Monitor created\",\n    },\n    ...openApiErrorResponses,\n  },\n});\n\nexport function registerPostMonitorHTTP(api: typeof monitorsApi) {\n  return api.openapi(postRoute, async (c) => {\n    const workspaceId = c.get(\"workspace\").id;\n    const input = c.req.valid(\"json\");\n    // ... 50 more lines of validation and DB logic\n  });\n}\n```\n\n**After (ConnectRPC):** Schema defines validation. Handler is pure business logic.\n\n```protobuf\n// Proto defines the contract\nmessage HTTPMonitor {\n  string name = 2 [(buf.validate.field).string = { min_len: 1, max_len: 256 }];\n  string url = 3 [(buf.validate.field).string = { min_len: 1, max_len: 2048, uri: true }];\n  Periodicity periodicity = 4 [(buf.validate.field).enum = { not_in: [0] }];\n  repeated Region regions = 5;\n  HTTPMethod method = 7;\n}\n```\n\n```typescript\n// Service - just business logic\nasync createHTTPMonitor(req, ctx) {\n  const rpcCtx = getRpcContext(ctx);  // Auth handled by interceptor\n  const mon = req.monitor!;            // Validation handled by interceptor\n\n  await checkMonitorLimits(rpcCtx.workspace.id, rpcCtx.workspace.limits, mon.periodicity, mon.regions);\n\n  const newMonitor = await db.insert(monitor).values({...}).returning().get();\n  return { monitor: dbMonitorToHttpProto(newMonitor) };\n}\n```\n\nThe difference: validation constraints live in the proto schema and execute at runtime via protovalidate.\n\n---\n\n## The Payoff: Auto-Generated Hooks with Full Type Inference\n\nWith `@connectrpc/connect-query`, we get TanStack Query hooks generated from the proto schema:\n\n**React Component:**\n\n```typescript\n\"use client\";\n\nimport { useQuery, useMutation } from \"@connectrpc/connect-query\";\nimport { listMonitors, createHTTPMonitor } from \"@openstatus/proto/monitor/v1-MonitorService_connectquery\";\n\nexport function MonitorList() {\n  const { data, isLoading } = useQuery(listMonitors, { limit: 50 });\n  const createMutation = useMutation(createHTTPMonitor);\n\n  if (isLoading) return <Loading />;\n\n  return (\n    <div>\n      <ul>\n        {data?.httpMonitors.map(m => (\n          <li key={m.id}>{m.name} - {m.url}</li>\n        ))}\n      </ul>\n      <button onClick={() => createMutation.mutate({\n        monitor: {\n          name: \"New Monitor\",\n          url: \"https://example.com\",\n          periodicity: Periodicity.PERIODICITY_1M,\n          regions: [Region.REGION_AMS],\n          method: HTTPMethod.HTTP_METHOD_GET,\n        }\n      })}>\n        Add Monitor\n      </button>\n    </div>\n  );\n}\n```\n\n**Server Component (Next.js):**\n\n```typescript\nimport { getRpcClient } from \"@/lib/rpc/server\";\nimport { MonitorService } from \"@openstatus/proto/monitor/v1\";\n\nexport default async function MonitorsPage() {\n  const client = await getRpcClient(MonitorService);\n  const { httpMonitors } = await client.listMonitors({ limit: 50 });\n\n  return (\n    <ul>\n      {httpMonitors.map(m => <li key={m.id}>{m.name}</li>)}\n    </ul>\n  );\n}\n```\n\nFull type safety. Auto-generated hooks. The same API your users consume.\n\n---\n\n## How AI Helped Us Ship Faster\n\nIn early 2026, we treated this migration as a test case for AI-assisted development.\n\nThe result? **AI is exceptionally good at schema work.**\n\n`.proto` files are declarative, highly structured, and pattern-heavy—perfect for LLMs. We used Claude to:\n\n1. Convert our Zod schemas into idiomatic Protobuf definitions\n2. Generate service implementations from existing REST handlers\n3. Flag inconsistencies between our internal and external APIs\n4. Write the interceptor patterns\n\nWe shifted from \"writing boilerplate\" to \"reviewing code.\" What we estimated at months is now tracking to finish in weeks.\n\n---\n\n## Testing: Just HTTP\n\nNo special test setup. ConnectRPC endpoints are plain HTTP:\n\n```typescript\nimport { describe, expect, test } from \"bun:test\";\nimport { app } from \"@/index\";\n\nasync function connectRequest(\n  method: string,\n  body: Record<string, unknown> = {},\n  headers: Record<string, string> = {},\n) {\n  return app.request(`/rpc/openstatus.monitor.v1.MonitorService/${method}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\", ...headers },\n    body: JSON.stringify(body),\n  });\n}\n\ndescribe(\"MonitorService\", () => {\n  test(\"ListMonitors returns monitors\", async () => {\n    const res = await connectRequest(\"ListMonitors\", { limit: 10 });\n    expect(res.status).toBe(200);\n    const data = await res.json();\n    expect(Array.isArray(data.httpMonitors)).toBe(true);\n  });\n\n  test(\"CreateHTTPMonitor validates input\", async () => {\n    const res = await connectRequest(\"CreateHTTPMonitor\", {\n      monitor: { name: \"\", url: \"not-a-url\" }  // Invalid\n    });\n    expect(res.status).toBe(400);  // InvalidArgument\n  });\n});\n```\n\n---\n\n## What's Next\n\nTwo things:\n\n1. **Dogfooding.** We're migrating our dashboard to the public ConnectRPC API. Same DX we had with tRPC, but now we use exactly what our users use.\n\n2. **The SDK is live.** Build on our monitoring platform with type-safe, generated clients:\n\n```bash\nnpx jsr add @openstatus/sdk\n```\n\n```typescript\nimport { createOpenStatusClient } from \"@openstatus/sdk-node\";\n\nconst client = createOpenStatusClient({\n  apiKey: process.env.OPENSTATUS_API_KEY,\n});\n\nconst monitors = await client.monitor.v1.MonitorService.listMonitors({});\nconsole.log(monitors);\n```\n\nOnce the migration is complete, no more maintaining two APIs. We just build.\n\n---\n\n**Ready to try it?**\n\n**[Get the SDK](https://jsr.io/@openstatus/sdk-node)** | **[View the Proto definitions](https://github.com/openstatushq/openstatus/tree/main/packages/proto)**\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/migration-auth-clerk-to-next-auth.mdx",
    "content": "---\ntitle: \"Why we migrated from Clerk to NextAuth (Auth.js).\"\ndescription: \"Why we switched from Clerk to NextAuth (Auth.js) — improving the open-source contributor experience with simpler local auth setup and no external dependencies.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2024-05-15\"\nimage: \"/assets/posts/migration-auth-clerk-to-next-auth/authjs.png\"\ncategory: \"engineering\"\n---\n\nWe recently switched from [Clerk](https://clerk.com) to NextAuth\n([Auth.js](https://authjs.dev)) for our authentication. Now, let's explore the\nreasons for this decision.\n\n## Why did we migrate from Clerk to NextAuth (Auth.js)? 🤔\n\nFirst, Clerk is an amazing product. But we are building an open-source project\nand creating an account on Clerk, setting up social login, and routing the\nwebhook to localhost was a pain for new contributors as it required a tunnel to\n`localhost:3000` via e.g. [ngrok](https://ngrok.com).\n\nOur goal is to improve the contributor experience (CX) for OpenStatus by\nspeeding up the time to first line of code. _#TTFLOC_\n\nOur efforts have already simplified the process significantly. With SQLite and\nnow NextAuth, we can seed the DB and login with an email (magic link gets\nprinted in the console) within less than a minute.\n\n> Not gonna lie, we still have some more improvements to make, like adding a\n> better Tinybird setup, but saving it for later! ✍️\n\n## Why did we start with Clerk? 🧑‍💻\n\nWhen we started OpenStatus, we needed a way to authenticate users. We wanted to\nmake it easy for users to sign up for our platform. Clerk was a good choice for\nus because it provided a simple way to authenticate users.\n\nThe documentation is clear, the setup is straightforward, and it is working\nflawlessly with Next.js App Router.\n\nAt the same time, NextAuth was starting a transition to Auth.js and while we\nwanted to use the latest tech and have chosen\n[drizzle](https://orm.drizzle.team) over [prisma](https://www.prisma.io), there\nwas no proper adapter for it.\n\nBack then, Clerk was the perfect fit.\n\n## Why did we pick NextAuth (Auth.js) and not Lucia? 🔑\n\nFirst, when we thought about migrating from Clerk, we considered\n[Lucia](https://lucia-auth.com/). Lucia is an amazing authentication library\nthat is gaining popularity built by [Pilcrow](https://pilcrowonpaper.com/).\n\nWe were scared about the stability and direction of the project. When you\nimplement authentication, you want to do it once and forget about it after.\n\n<Tweet id=\"1785701672012107841\" />\n\nWe decided to go with NextAuth because it was more mature, and it seemed more\nstable to us. Additionally, we both had some experience with NextAuth. It was a\ngood choice for us as we did not have to learn a new library.\n\n## How did we migrate from Clerk to NextAuth (Auth.js)? 🚀\n\nWe were scared of the migration at first. We thought it would be long and hard.\nBut the migration was quite simple, as we were already storing most of the user\ndata in our database.\n\n### What we already had in place\n\nWhenever a user signed up with Clerk, we stored the `user`'s data in our\ndatabase. We had to store the `tenantId` from Clerk to be able to fetch map the\nuser, and we additionally stored `email`, `name`, `photoUrl`, and SQLite created\nan additional primary key `id`.\n\nAll the other information of the user's social account (Google, GitHub OAuth)\nwas stored in Clerk.\n\n### Starting with NextAuth\n\nWhenever you start with NextAuth, you choose your DB/ORM adapter and create the\ntables you need:\n\n- `users`\n- `sessions`\n- `accounts`\n- `verificationTokens`\n\nWith drizzle as our ORM, we followed the following steps from\n[here](https://authjs.dev/getting-started/adapters/drizzle), including using\nSQLite as schema.\n\nNow because we already had the `users` table, we extended it with the necessary\nfields or mappings in our\n[extended adapter](https://github.com/openstatusHQ/openstatus/blob/main/apps/web/src/lib/auth/adapter.ts)\n(e.g., we have kept the `photoUrl` field and did not use the `image` field from\nNextAuth to store the social image of the user). NextAuth allows you to override\nthe adapter functions and that's what we did: we overrode the `getUser` and\n`createUser` functions to match our current user db schema.\n\nThe other tables `sessions`, `accounts`, and `verificationTokens` were created\nas is.\n\nNow you might wonder: **How did we migrate the data?** We simply did not.\n\nWhile Clerk has all the information for the social `accounts` of every user, we\nare connecting the user to the account when they log in the next time with.\nNextAuth provides a\n[`allowDangerousEmailAccountLinking`](https://authjs.dev/reference/core/providers#allowdangerousemailaccountlinking)\nproperty that re-links the `user` to the `account` when they log in with the\nsame email.\n\n> We still have some issue with types and NextAuth. We have an open issue\n> [#812](https://github.com/openstatusHQ/openstatus/issues/812) if you want to\n> help us 🤗.\n\n### Customizing the UI\n\nWe have been using Clerk's UI for a long time and swapped it with custom\ncomponents. But luckily our website is powered by\n[shadcn UI](https://ui.shadcn.com) and we could easily create the components.\n\n## Conclusion\n\nIf you are building an open-source project, we recommend NextAuth. But if it's a\nclosed-source SaaS project, Clerk is a good choice.\n\nWe are happy with our decision to migrate from Clerk to NextAuth (Auth.js).\nWhile we will miss some Clerk features (analytics), we look forward to exploring\nwhat NextAuth offers us.\n\nIf you have any questions about our migration, feel free to send us an email at\n[ping@openstatus.dev](mailto:ping@openstatus.dev).\n\nOur main PR for the migration is\n[#801](https://github.com/openstatusHQ/openstatus/pull/801) while we implemented\nthe magic link for dev mode in\n[#806](https://github.com/openstatusHQ/openstatus/pull/806) and improved the\ntypes in [#807](https://github.com/openstatusHQ/openstatus/pull/807).\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/migration-backend-from-vercel-to-fly.mdx",
    "content": "---\ntitle: \"Why we migrated our backend from Vercel to Fly.io.\"\ndescription: \"Why we migrated OpenStatus' backend from Vercel to Fly.io using Hono and Bun — the challenges, performance gains, and lessons from the migration.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2023-10-29\"\nimage: \"/assets/posts/migration-backend-from-vercel-to-fly/fly.png\"\ncategory: \"company\"\n---\n\nIn this article, we are going to see the reasons that made us change our backend\nto Fly.io and the challenges we had during the migrations.\n\nWe chose [Hono](https://hono.dev/) as our API server with [Bun](https://bun.sh/)\nas the runtime and picked Fly.io as our hosting service.\n\n## 🤔 Why did we want to move our backend from Vercel?\n\n### ⚡ A lightweight Server\n\nWe required a lightweight server with a simple REST API for our monitoring\nendpoint. Deploying a simple Express server is possible, but it is not\nspecifically designed for this purpose.\n\n`It's possible to deploy an Express.js application as a single Serverless Function, but it comes with drawbacks and should only be used as a migration path. Instead, use Next.js or embrace multiple Serverless Functions as you incrementally migrate to the Vercel platform.`\n\n[Source Vercel Doc](https://vercel.com/guides/using-express-with-vercel)\n\nAlso launching a clean and new Next.js server takes 2.5 seconds on my MacBook\nPro M1 and takes 110mb of RAM, and it includes unnecessary extra features from\nNext.js. Our prod Next.js app takes about 5 seconds to launch on my computer\n(contentlayer).\n\n<Image\n  alt=\"Next server\"\n  src=\"/assets/posts/migration-backend-from-vercel-to-fly/next-server.png\"\n  width={650}\n  height={200}\n/>\n\nFor comparaison launching our current server takes 0.19ms and only takes 91mb.\nOur current server stack is Hono + Bun.\n\n<Image\n  alt=\"Hono server\"\n  src=\"/assets/posts/migration-backend-from-vercel-to-fly/hono-server.png\"\n  width={650}\n  height={575}\n/>\n\n### 💸 Pricing\n\nWe initially aimed to provide multi-region monitoring for all users while\nmaintaining a free tier. On Vercel, if you want a multi-region function, you\nneed to opt for Edge Functions. Edge functions are cost-effective as you only\npay for the actual CPU execution. This means that you won't be billed for idle\ntimes when fetching data.\n\nIt's still affordable, but we are a bootstrap business and it difficult for us\nto predict our monthly expenses. If we experience an increase in new users, our\ncosts will also increase accordingly.\n\nHere's the math for the cost of one monitor for a user:\n\n```\n6 (10 min monitors) * 24 * 30 * 3 (average execution unit per monitor) * 6 (number of regions) = 77,760 executions units per month\n\n77,600 * (2/1,000,000) = 0.15c per monitor monthly\n```\n\nWe have over 1000 monitors, and the monthly cost to run them would be $150.\n\nWhile on fly we only have 6 servers with 2vcpu/512Mb It cost us $23.34 monthly\n($3.89\\*6).\n\n## 🤯 What challenges did we face during our migrations?\n\n### Docker + monorepo = 🪨\n\nWe are deploying to fly.io. We have to setup our app as a Docker image. Our apps\nis in a monorepo. Our initial image was over 2 GB in size, which was excessively\nlarge for a basic server.\n\nOur image included everything, which was inefficient. After optimizing, our\nimage now occupies only 700MB. Although it is still somewhat large, it is a\nsignificant improvement over our initial version.\n\nIt was something we never had to manage with Vercel deployment.\n\n### ⏳ Fly deployment timed\n\nOur Fly deployments have been experiencing frequent timeouts without any\nspecific reason. The only solution we have discovered is to increase the timeout\nduration.\n\n```\nflyctl deploy --wait-timeout=500\n```\n\nBased on our experience, Fly deployments are generally less reliable compared\n(more timed out) to Vercel deployments. Additionally, we have not discovered a\nquick method to rollback to the previous version.\n\n### 🐛 The Bun bug\n\nWhen we migrated to Fly, we decided to use Bun as our runtime. However, in the\nfirst few hours after the migration, we observed an unexplained increase in\nrequest failures.\n\nAfter digging into the Bun GitHub we found a solution: We needed to set the\n`keepalive` parameter to `false`. This is necessary because closed connections\nare not automatically removed and remain in the ﻿`CLOSE_WAIT` state.\n\nHere's the GitHub issue:\n\nhttps://github.com/oven-sh/bun/issues/3327\n\nI wish it had been documented elsewhere.\n\n## Our conclusion\n\nWe are pleased with our migration to Fly.io, although it was accompanied by a\nchallenging weekend. And we still love Vercel, they offer a super product, it\nremoves a lot of pain for the developers. However, if you require a hosting an\napplication other than Next.js, it may not be the best option.\n\nWe are still using it for our frontend.\n\nCreate an account on [OpenStatus](/app/login?ref=blog3)\nto start monitoring our website and managing your incidents for free.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/migration-planetscale-to-turso.mdx",
    "content": "---\ntitle: \"Why we migrated from PlanetScale to Turso.\"\nimage: \"/assets/posts/migration-planetscale-to-turso/turso.png\"\ndescription: \"Why we migrated from PlanetScale to Turso for our edge database — comparing features, performance, and developer experience with Drizzle ORM.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2023-08-20\"\ncategory: \"company\"\n---\n\n## What are we building ? 🏗️\n\nWe are building an open source status uptime monitoring with a beautiful status\npage. Our goal is to provide a fast service to our users.\n\nTo achieve this, we are hosting it on the Vercel Edge Runtime, which requires an\nEdge Database to fully benefit from it.\n\nWe also aim to simplify contributions to our project by eliminating the\nrequirement for an account on an external service.\n\nOur current data tech stack is:\n\n- [Next 13](https://nextjs.org/)\n- [Drizzle ORM](https://orm.drizzle.team/)\n- [Tinybird](https://www.tinybird.co/)\n- [Turso](https://turso.tech/)\n\n### What is PlanetScale ? 🗃️\n\n[PlanetScale](https://planetscale.com/) is a cloud-native database platform.\n\nIt is built on top of the open-source database system Vitess. PlanetScale offers\na managed database service that allows developers to deploy, manage, and scale\ndatabases easily. In my opinion, the standout feature of PlanetScale is its\nmigration management system.\n\n### What is Turso ? 🌍\n\n[Turso](https://turso.tech/) is a new database provider.\n\nTurso is built on top of LibSQL a fork of SQLite. They provide an edge database\nthat enhances the developer experience of SQLite for your users. It enables\nextensive replication, allowing you to distribute your data to numerous\nlocations where your users are located.\n\n### History of the project 📜\n\nWhen we began the project, we opted for PlanetScale due to its simplicity and\ncost-free database option. They also don't have any cold start which is super\nnice for us. Since we wanted to build fast, it was a good choice when we started\nthe project. The database was just a URL. Their migration tools, ensuring zero\ndowntime, were also valuable to us during rapid prototyping and frequent\nmodifications.\n\nI came across Turso on X (formely known as Twitter) and became interested in\ntrying it out. It seems promising as their value proposition is an edge\ndatabase, which is exactly what we need. We plan to deploy the status page to\nthe edge (Vercel Edge Runtime). Calling a database in a specific region would\nnegate the benefits of Vercel Edge Runtime.\n\n## What drove our changes ? 🤔\n\n### Performance 🏎️\n\nNot all of our users are in `us-east-1`. Users who are not located in\n`us-east-1` should receive the same level of performance as those in that\nregion.\n\nI was playing with this [Vercel tool](https://edge-data-latency.vercel.app/) to\ndetermine the most suitable database for our users. Among the options, Turso\nproved to be the fastest.\n\n<Image\n  alt=\"Turso latency\"\n  src=\"/assets/posts/migration-planetscale-to-turso/turso-latency.png\"\n  width={650}\n  height={575}\n/>\n\nTurso is around `60ms` for 5 serial queries globally.\n\n<Image\n  alt=\"PlanetScale latency\"\n  src=\"/assets/posts/migration-planetscale-to-turso/planetscale-latency.png\"\n  width={650}\n  height={575}\n/>\n\nWhile PlanetScale is around `450ms` for 5 serial queries in a different region.\n\nWe considered using read replicas from PlanetScale, but I was also interested in\ntrying out Turso.\n\nWith Turso, you only need a single URL for all your replicas, eliminating the\nneed to add this logic in your codebase. Additionally, Turso's scaler plan is\nmore affordable, offering 6 replicas for $29, whereas PlanetScale only allows 3\nreplicas.\n\nTurso offers 26 regions, while PlanetScale only offers 11 regions (their AWS\nregions).\n\n### Better DX for contributors 🧑‍💻\n\nWe are building an open source status page, and we want to make it easy for\neveryone to contribute to it.\n\nAsking our users to create an account on a new service is a bit awkward. We\ncould have asked for our user to use Docker and create a MySQL8 database while\ndeveloping locally. However I don’t like Docker, it consumes too much memory on\nmy laptop 😁.\n\nOr we use a file and SQLite, as it is the best option for everyone. And it’s\nwhat we can achieve with Turso: seamless local development.\n\n## How the migration went. 🚀\n\n### SQLite has a less powerful type system than MySQL. 📦\n\nThe migration process from MySQL to SQLite requires some changes. SQLite has\nfewer data types compared to MySQL. For example, DateTime doesn't exist in\nSQLite, so dates should be treated as integers.\n\nThere are only five types in SQLite:\n\n- `INT`\n- `INTEGER`\n- `REAL`\n- `TEXT`\n- `BLOB`\n\nWe use Drizzle for type handling and migration in our codebase, so we weren't\nsignificantly affected by this. Additionally, our experience with SQLite has\ntaught us the importance of implementing additional checks in our code, rather\nthan solely relying on the database engine.\n\n### Drizzle ORM Migration 😱\n\nWe had to write additional SQL for migration when transitioning from Planetscale\nand/or Prisma. Initially, it was a bit frustrating because Drizzle could not\ngenerate all the necessary migration code.\n\n```\n/*\n SQLite does not support \"Drop not null from column\" out of the box,\n we do not generate automatic migration for that, so it has to be done manually\n Please refer to: <https://www.techonthenet.com/sqlite/tables/alter_table.php>\n                  <https://www.sqlite.org/lang_altertable.html>\n                  <https://stackoverflow.com/questions/2083543/modify-a-columns-type-in-sqlite3>\n\n Due to that we don't generate migration automatically\n and it has to be done manually\n*/\n\n```\n\nAfter migrating a couple of times, you realize that they are mostly the same,\nand you need to follow these steps.\n\n1. Create new table\n2. Copy data into new table\n3. Drop old table\n4. Rename new table\n\nAs it can been seen in this\n[migration](https://github.com/openstatusHQ/openstatus/blob/9f610d7cafb892774ceda533b3f21aaa4a4b3d5f/packages/db/drizzle/0003_glamorous_living_mummy.sql)\nwhen we released the incident:\n\n```sql\nALTER TABLE `incident` RENAME TO `incident_old`;--> statement-breakpoint\nALTER TABLE `incident_update` RENAME TO `incident_update_old`;--> statement-breakpoint\n\nDROP TABLE `incident_old`;--> statement-breakpoint\nDROP TABLE `incident_update_old`;--> statement-breakpoint\n\nCREATE TABLE `incident` (\n`id` integer PRIMARY KEY NOT NULL,\n`status` text(4) NOT NULL,\n`title` text(256) NOT NULL,\n`created_at` integer DEFAULT (strftime('%s', 'now')),\n`updated_at` integer DEFAULT (strftime('%s', 'now')),\n`workspace_id` integer NOT NULL,\nFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\n\nCREATE TABLE `incident_update` (\n`id` integer PRIMARY KEY NOT NULL,\n`status` text(4) NOT NULL,\n`date` integer NOT NULL,\n`message` text NOT NULL,\n`created_at` integer DEFAULT (strftime('%s', 'now')),\n`updated_at` integer DEFAULT (strftime('%s', 'now')),\n`incident_id` integer NOT NULL,\nFOREIGN KEY (`incident_id`) REFERENCES `incident`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\n\n```\n\n## The Turso Doc 📚\n\nThe Turso Documentation has been the major challenge during the migration. We\nfaced difficulties finding the specific information. We needed to make it work\nboth locally and on Turso.\n\nHowever, we have DMed [Glauber Costa](https://twitter.com/glcst), the CEO of\nTurso, and they are aware of this issue. Hopefully, they will update the\ndocumentation soon. We believe that with comprehensive documentation and plenty\nof examples, Turso will be an even greater product.\n\n## The verdict 🍾\n\nWe are pleased with the change, regardless of whether our users are interested\nin the stack we use. We genuinely enjoy using this stack. Turso is still an\nearly product, but we are confident that it will improve over time. I strongly\nbelieve in its future.\n\nP.S. We are building OpenStatus in public, follow\n[me](https://www.twitter.com/thibaultleouay) or\n[Max](https://www.twitter.com/mxkaske) on Twitter/X if you want sneak peaks of\nthe upcoming features - it's fun to watch! 🍿\n\nP.P.S: OpenStatus is open-source, you can browse our code on\n[GitHub](https://github.com/openstatusHQ/openstatus) and give us a star ⭐️\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/monitoring-latency-cf-workers-fly-koyeb-raylway-render.mdx",
    "content": "---\ntitle: \"Monitoring latency: Cloudflare Workers vs Fly vs Koyeb vs Railway vs Render\"\ndescription: \"We benchmarked latency across 5 cloud providers from 6 global locations over 2 weeks. Cloudflare Workers averaged 182ms, Fly.io 61ms (warm), Railway 381ms, Koyeb 539ms, and Render 451ms. Full timing breakdowns included.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2024-02-19\"\nimage: \"/assets/posts/monitoring-latency/all-hosting-providers.png\"\ncategory: \"education\"\nfaq:\n  - question: \"Which cloud provider has the lowest latency?\"\n    answer: \"In our benchmark, Cloudflare Workers had the lowest average latency at 182ms across 6 global regions, with a P75 of 138ms. Fly.io in production (with min_machines_running=1) averaged just 61ms, but with cold starts it averaged 1,471ms.\"\n  - question: \"Does Fly.io have cold start issues?\"\n    answer: \"Yes. With auto_stop_machines enabled and min_machines_running=0, Fly.io averaged 1,471ms due to cold starts (~1.5s machine boot time). Setting min_machines_running=1 eliminates cold starts and brings the average down to 61ms.\"\n  - question: \"How does Cloudflare Workers latency compare to Railway and Render?\"\n    answer: \"Cloudflare Workers averaged 182ms with 100% uptime. Railway averaged 381ms with 99.991% uptime (1 failure). Render averaged 451ms with 99.89% uptime (12 failures). Cloudflare Workers deploy globally to 275+ locations, while Railway and Render run from a single region.\"\n  - question: \"Which cloud provider had the most downtime in the benchmark?\"\n    answer: \"Render had the most failures with 12 failed checks and 99.89% uptime over 2 weeks. Railway had 1 failure (99.991% uptime). Cloudflare Workers, Fly.io, and Koyeb all had 0 failures and 100% uptime.\"\n---\n\n> ⚠️ We are using the default settings for each provider and conducting\n> datacenter to datacenter requests. A real-world application's results are\n> going to be different. ⚠️\n\nYou want to know which cloud providers offer the lowest latency?\n\nIn this post, I compare the latency of\n[Cloudflare Workers](#cloudflare-workers), [Fly](#flyio), [Koyeb](#koyeb),\n[Railway](#railway) and [Render](#render) using\n[OpenStatus](https://www.openstatus.dev).\n\nI deployed the application on the cheapest or free tier offered by each\nprovider.\n\nFor this test, I used a basic [Hono](https://hono.dev) server that returns a\nsimple text response.\n\n```js\nconst app = new Hono();\napp.use(\"*\", logger());\n\napp.use(\"*\", poweredBy());\n\napp.get(\"/\", (c) => {\n  return c.text(\n    \"Just return the desired http status code, e.g. /404 🤯 \\nhttps://www.openstatus.dev\",\n  );\n});\n```\n\nYou can find the code in the [`status-code` repository](https://github.com/openstatusHQ/status-code), it’s\nopen source 😉.\n\nOpenStatus monitored our endpoint every **10 minutes** from **6 locations**\nlocated in Amsterdam, Ashburn, Hong Kong, Johannesburg, Sao Paulo and Sydney.\n\nIt's a good way to test our own product and improve it.\n\nLet's analyze the data from the past two weeks.\n\n## Cloudflare workers\n\nCloudflare Workers is a serverless platform by Cloudflare. It lets you build new\napplications using JavaScript/Typescript. You can deploy up to 100 worker\nscripts for free, running on more than 275 network locations.\n\n### Latency metrics\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILS\n\n</div>\n<div>\n\n**10,956**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**182**ms AVG\n\n</div>\n<div>\n\n**138**ms P75\n\n</div>\n<div>\n\n**695**ms P90\n\n</div>\n<div>\n\n**778**ms P95\n\n</div>\n<div>\n\n**991**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/monitoring-latency/cloudflare.json\"\n    caption=\"Cloudflare avg. latency between 04. Feb and 18. Feb 2024 aggregated in a 1h window.\"\n  />\n</div>\n\n### Timing metrics\n\n| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) |\n| --- | --- | --- | --- | --- | --- |\n| AMS | 17 | 2 | 17 | 27 | 0 |\n| GRU | 38 | 2 | 13 | 28 | 0 |\n| HKG | 19 | 2 | 13 | 29 | 0 |\n| IAD | 24 | 1 | 14 | 30 | 0 |\n| JNB | 123 | 168 | 182 | 185 | 0 |\n| SYD | 51 | 1 | 11 | 25 | 0 |\n\nI can notice that Johannesburg's latency is about ten times higher than that of\nthe other monitors.\n\n### Headers\n\nFrom the Cloudflare request I can get the location of the workers that handle\nthe request, with `Cf-ray` in the headers response.\n\n| Checker region | Workers region | number of request |\n| --- | --- | --- |\n| HKG | HKG | 1831 |\n| SYD | SYD | 1831 |\n| AMS | AMS | 1831 |\n| IAD | IAD | 1831 |\n| GRU | GRU | 1791 |\n| GRU | GIG | 40 |\n| JNB | AMS | 741 |\n| JNB | MUC | 4 |\n| JNB | HKG | 5 |\n| JNB | SIN | 6 |\n| JNB | NRT | 8 |\n| JNB | EWR | 10 |\n| JNB | CDG | 82 |\n| JNB | FRA | 276 |\n| JNB | LHR | 699 |\n| JNB | AMS | 741 |\n\nI can see all the request from JNB is never routed to a nearby data-center.\n\nApart from the strange routing error in Johannesburg, Cloudflare workers are\nfast worldwide.\n\nI have not experienced any cold start issues.\n\n## Fly.io\n\nFly.io simplifies deploying and running server-side applications globally.\nDevelopers can deploy their applications near users worldwide for low latency\nand high performance. It uses a lightweight Firecracker VM to easily deploy\nDocker images.\n\n### Latency metrics\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILS\n\n</div>\n<div>\n\n**10,952**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**1,471**ms AVG\n\n</div>\n<div>\n\n**1,514**ms P75\n\n</div>\n<div>\n\n**1,555**ms P90\n\n</div>\n<div>\n\n**1,626**ms P95\n\n</div>\n<div>\n\n**2,547**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/monitoring-latency/fly.json\"\n    caption=\"Fly avg. latency between 04. Feb and 18. Feb 2024 aggregated in a 1h window.\"\n  />\n</div>\n\n### Timing metrics\n\n| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) |\n| --- | --- | --- | --- | --- | --- |\n| AMS | 6 | 1 | 8 | 1469 | 0 |\n| GRU | 5 | 0 | 4 | 1431 | 0 |\n| HKG | 4 | 0 | 5 | 1473 | 0 |\n| IAD | 3 | 0 | 5 | 1470 | 0 |\n| JNB | 24 | 0 | 5 | 1423 | 0 |\n| SYD | 3 | 0 | 3 | 1489 | 0 |\n\nThe DNS is fast, our checker is attempting to connect to a region in the same\ndata center, but our machine's cold start is slowing us down, leading to the\nhigh TTFB.\n\nHere’s our config for Fly.io:\n\n```toml\napp = 'statuscode'\nprimary_region = 'ams'\n\n[build]\n  dockerfile = \"./Dockerfile\"\n\n[http_service]\n  internal_port = 3000\n  force_https = true\n  auto_stop_machines = true\n  auto_start_machines = true\n  min_machines_running = 0\n  processes = ['app']\n\n[[vm]]\n  cpu_kind = 'shared'\n  cpus = 1\n  memory_mb = 256\n```\n\nThe primary region of our server is Amsterdam, and the fly instances is getting\npaused after a period of inactivity.\n\nThe machine starts slowly, as indicated by the logs showing a start time of\n`1.513643778s.`\n\n```\n2024-02-14T11:24:16.107 proxy[286560ea703108] ams [info] Starting machine\n\n2024-02-14T11:24:16.322 app[286560ea703108] ams [info] [ 0.035736] PCI: Fatal: No config space access function found\n\n2024-02-14T11:24:16.533 app[286560ea703108] ams [info] INFO Starting init (commit: bfa79be)...\n\n2024-02-14T11:24:16.546 app[286560ea703108] ams [info] INFO Preparing to run: `/usr/local/bin/docker-entrypoint.sh bun start` as root\n\n2024-02-14T11:24:16.558 app[286560ea703108] ams [info] INFO [fly api proxy] listening at /.fly/api\n\n2024-02-14T11:24:16.565 app[286560ea703108] ams [info] 2024/02/14 11:24:16 listening on [fdaa:3:2ef:a7b:10c:3c9a:5b4:2]:22 (DNS: [fdaa::3]:53)\n\n2024-02-14T11:24:16.611 app[286560ea703108] ams [info] $ bun src/index.ts\n\n2024-02-14T11:24:16.618 runner[286560ea703108] ams [info] Machine started in 460ms\n\n2024-02-14T11:24:17.621 proxy[286560ea703108] ams [info] machine started in 1.513643778s\n\n2024-02-14T11:24:17.628 proxy[286560ea703108] ams [info] machine became reachable in 7.03669ms\n```\n\n#### OpenStatus Prod metrics\n\nIf you update your fly.toml file to include the following, you can get the zero\ncold start and achieve a better latency.\n\n```\n  min_machines_running = 1\n```\n\nThis is our data for our production server deploy on Fly.io.\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILS\n\n</div>\n<div>\n\n**12,076**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**61**ms AVG\n\n</div>\n<div>\n\n**67**ms P75\n\n</div>\n<div>\n\n**164**ms P90\n\n</div>\n<div>\n\n**198**ms P95\n\n</div>\n<div>\n\n**327**ms P99\n\n</div>\n</Grid>\n\n> We use Fly.io in production, and the machine never sleeps, yielding much\n> better results.\n\n## Koyeb\n\nKoyeb is a developer-friendly serverless platform that allows for global app\ndeployment without the need for operations, servers, or infrastructure\nmanagement. Koyeb offers a free Starter plan that includes one Web Service, one\nDatabase service. The platform focuses on ease of deployment and scalability for\ndevelopers\n\n### Latency metrics\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILS\n\n</div>\n<div>\n\n**10,955**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**539**ms AVG\n\n</div>\n<div>\n\n**738**ms P75\n\n</div>\n<div>\n\n**881**ms P90\n\n</div>\n<div>\n\n**1,013**ms P95\n\n</div>\n<div>\n\n**1,525**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/monitoring-latency/koyeb.json\"\n    caption=\"Koyeb avg. latency between 04. Feb and 18. Feb 2024.\"\n  />\n</div>\n\n### Timing metrics\n\n| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) |\n| --- | --- | --- | --- | --- | --- |\n| AMS | 50 | 2 | 17 | 107 | 0 |\n| GRU | 139 | 65 | 75 | 407 | 0 |\n| HKG | 48 | 2 | 13 | 321 | 0 |\n| IAD | 35 | 1 | 12 | 129 | 0 |\n| JNB | 298 | 1 | 11 | 720 | 0 |\n| SYD | 97 | 1 | 10 | 711 | 0 |\n\n### Headers\n\nThe request headers show that none of our requests are cached. They contain\n`cf-cache-status: dynamic`. Cloudflare handles the Koyeb edge layer.\nhttps://www.koyeb.com/blog/building-a-multi-region-service-mesh-with-kuma-envoy-anycast-bgp-and-mtls\n\nOur requests follow this route:\n\n```\nCf workers -> koyeb Global load balancer -> koyeb backend\n```\n\nLet's see where did we hit the cf workers\n\n| Checker region | Workers region | number of request |\n| --- | --- | --- |\n| AMS | AMS | 1866 |\n| GRU | GRU | 504 |\n| GRU | IAD | 38 |\n| GRU | MIA | 688 |\n| GRU | EWR | 337 |\n| GRU | CIG | 299 |\n| HKG | HKG | 1866 |\n| IAD | IAD | 1866 |\n| JNB | JNB | 1861 |\n| JNB | AMS | 1 |\n| SYD | SYD | 1866 |\n\nKoyeb Global Load Balancer region we hit:\n\n| Checker region | Koyeb Global Load Balancer | number of request |\n| --- | --- | --- |\n| AMS | FRA1 | 1866 |\n| GRU | WAS1 | 1866 |\n| HKG | SIN1 | 1866 |\n| IAD | WAS1 | 1866 |\n| JNB | PAR1 | 4 |\n| JNB | SIN1 | 1864 |\n| JNB | FRA1 | 1 |\n| JNB | SIN1 | 1866 |\n\nI have deployed our app in the Frankfurt data-center.\n\n## Railway\n\nRailway is a cloud platform designed for building, shipping, and monitoring\napplications without the need for Platform Engineers. It simplifies the\napplication development process by offering seamless deployment and monitoring\ncapabilities.\n\n### Latency metrics\n\n<Grid cols={5}>\n<div>\n\n**99.991**% UPTIME\n\n</div>\n<div>\n\n**1**# FAILS\n\n</div>\n<div>\n\n**10,955**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**381**ms AVG\n\n</div>\n<div>\n\n**469**ms P75\n\n</div>\n<div>\n\n**653**ms P90\n\n</div>\n<div>\n\n**661**ms P95\n\n</div>\n<div>\n\n**850**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/monitoring-latency/railway.json\"\n    caption=\"Railway avg. latency between 04. Feb and 18. Feb 2024 aggregated in a 1h window.\"\n  />\n</div>\n\n### Timing metrics\n\n| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) |\n| --- | --- | --- | --- | --- | --- |\n| AMS | 9 | 21 | 18 | 158 | 0 |\n| GRU | 14 | 115 | 127 | 178 | 0 |\n| HKG | 8 | 45 | 54 | 225 | 0 |\n| IAD | 7 | 2 | 14 | 65 | 0 |\n| JNB | 18 | 193 | 178 | 319 | 0 |\n| SYD | 21 | 108 | 105 | 280 | 0 |\n\n### Headers\n\nThe headers don't provide any information.\n\nRailway is using Google Cloud Platform. It’s the only service that does not\nallow us to pick a specific region on the free plan. Our test app will be\nlocated to `us-west1` Portland, Oregon. We can see that the latency is the\nlowest in IAD.\n\nBy default our app did not scale down to 0. It was always running. We don't have\nany cold start.\n\n## Render\n\nRender is a platform that simplifies deploying and scaling web applications and\nservices. It offers features like automated SSL, automatic scaling, native\nsupport for popular frameworks, and one-click deployments from Git. The platform\nfocuses on simplicity and developer productivity.\n\n### Latency metrics\n\n<Grid cols={5}>\n<div>\n\n**99.89**% UPTIME\n\n</div>\n<div>\n\n**12**# FAILS\n\n</div>\n<div>\n\n**10,946**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**451**ms AVG\n\n</div>\n<div>\n\n**447**ms P75\n\n</div>\n<div>\n\n**591**ms P90\n\n</div>\n<div>\n\n**707**ms P95\n\n</div>\n<div>\n\n**902**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/monitoring-latency/render.json\"\n    caption=\"Render avg. latency between 04. Feb and 18. Feb 2024 aggregated in a 1h window.\"\n  />\n</div>\n\n### Timing metrics\n\n| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) |\n| --- | --- | --- | --- | --- | --- |\n| AMS | 20 | 2 | 7 | 107 | 0 |\n| GRU | 61 | 2 | 6 | 407 | 0 |\n| HKG | 76 | 2 | 6 | 321 | 0 |\n| IAD | 15 | 1 | 5 | 129 | 0 |\n| JNB | 36 | 161 | 167 | 720 | 0 |\n| SYD | 103 | 1 | 4 | 711 | 0 |\n\n### Headers\n\nThe headers don't provide any information.\n\nI have deployed our app in the Frankfurt data-center.\n\nAccording to the Render docs, the free tier will shut down the service after 15\nminutes of inactivity. However, our app is being accessed by a monitor every 10\nminutes. We should never scale down to 0.\n\n```\nRender spins down a Free web service that goes 15 minutes without receiving inbound traffic. Render spins the service back up whenever it next receives a request to process.\n```\n\nI think the failures are due to the cold start of our app. We have a default\ntimeout of 30s and the render app takes up to 50s to start.We might have hit an\ninflection point between cold and warm.\n\n## Conclusion\n\nHere are the results of our test:\n\n| Provider | Uptime | Fails Ping | Total Pings | AVG latency (ms) | P75 (ms) | P90 (ms) | P95 (ms) | P99 (ms) |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| CF Workers | 100 | 0 | 10 | 956 | 182 | 138 | 690 | 778 |\n| Fly.io | 100 | 0 | 10 | 952 | 1 | 471 | 1 | 514 |\n| Koyeb | 100 | 0 | 10 | 955 | 536 | 738 | 881 | 1 |\n| Railway | 99.991 | 1 | 10 | 955 | 381 | 469 | 653 | 661 |\n| Render | 99.89 | 12 | 10 | 946 | 451 | 447 | 591 | 707 |\n\nIf you value low latency, Cloudflare Workers are the best option for fast global\nperformance without cold start issues. They deploy your app worldwide\nefficiently.\n\nFor multi-region deployment, check out Koyeb and Fly.io.\n\nFor specific region deployment, Railway and Render are good choices.\n\nChoosing a cloud provider involves considering not just latency but also user\nexperience and pricing.\n\nWe use Fly.io in production and are satisfied with it.\n\n#### Vercel\n\nI haven't included Vercel in this test. But we have a blog post comparing [Vercel Serverless vs Edge vs Serverless](/blog/monitoring-latency-vercel-edge-vs-serverless).\n\nIf you want to monitor your API or website, create an account on\n[OpenStatus](/app/sign-up?ref=blog-monitoring).\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/monitoring-latency-vercel-edge-vs-serverless.mdx",
    "content": "---\ntitle: \"Monitoring latency: Vercel Serverless Function vs Vercel Edge Function\"\ndescription: \"We benchmarked Vercel Edge vs Serverless functions from 6 global regions. Edge averaged 106ms (P50), Serverless warm 246ms, and Serverless cold 859ms. Edge is 9x faster on cold starts and 2x faster when warm.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2024-03-14\"\nimage: \"/assets/posts/monitoring-vercel/serverless-vs-edge.png\"\ncategory: \"education\"\nfaq:\n  - question: \"Is Vercel Edge faster than Vercel Serverless?\"\n    answer: \"Yes. In our benchmark from 6 global regions, Edge functions averaged 106ms (P50) vs 246ms for warm Serverless and 859ms for cold Serverless. Edge is about 9x faster during cold starts and 2x faster when warm.\"\n  - question: \"What is the cold start latency of Vercel Serverless functions?\"\n    answer: \"In our test, Vercel Serverless cold starts averaged 859ms (P50), with P95 at 1,046ms and P99 at 1,156ms. Functions were pinged every 30 minutes to ensure they scaled down between requests.\"\n  - question: \"Do Vercel Edge functions have cold starts?\"\n    answer: \"Vercel Edge functions have negligible cold starts. In our benchmark, Edge functions maintained a consistent P50 of 106ms and P99 of 328ms regardless of request frequency, compared to Serverless which jumped from 246ms (warm) to 859ms (cold).\"\n  - question: \"Where are Vercel Serverless functions deployed?\"\n    answer: \"Vercel Serverless functions are deployed in a single region (iad1 — Washington, D.C. by default). All requests are routed through a nearby data center before being forwarded to the function's region. Edge functions are deployed globally and execute in the datacenter closest to the user.\"\n---\n\nIn our previous\n[article](/blog/monitoring-latency-cf-workers-fly-koyeb-raylway-render),\nwe compared the latency of various cloud providers but did not include Vercel.\nThis article will compare the latency of Vercel Serverless Function with Vercel\nEdge Function.\n\nWe will test a basic Next.js application with the app router. Below is the code\nfor the routes:\n\n```ts\nimport { NextResponse } from \"next/server\";\n\nexport const dynamic = \"force-dynamic\";\n\nexport const maxDuration = 25; // to trick and not using the same function as the other ping route\n\nexport async function GET() {\n  return NextResponse.json({ ping: \"pong\" }, { status: 200 });\n}\n\nexport async function POST(req: Request) {\n  const body = await req.json();\n  return NextResponse.json({ ping: body }, { status: 200 });\n}\n```\n\nWe have 4 routes, 3 using the NodeJS runtime and one is using Edge runtime.\n\n- `/api/ping` is using the NodeJS runtime\n- `/api/ping/warm` is using the NodeJS runtime\n- `/api/ping/cold` is using the NodeJS runtime\n- `/api/ping/edge` is using the Edge runtime\n\nEach route have a different `maxDuration`, it's a trick to avoid bundling the\nfunctions in the same physical functions.\n\nHere is the repository of the\n[application](https://github.com/openstatusHQ/openstatus-next-latency).\n\n## Vercel Serverless Function - NodeJS runtime\n\nThey are using the NodeJS 18 runtime. We have access to all the nodejs API. Our\nfunction are deployed in a single location: iad1 - Washington, D.C., USA.\n\nUpgrading to Node.js 20 could enhance cold start performance, but it's still in\nbeta.\n\nWe analyzed the header of each request and observe that all requests are\nprocessed in a data center near our location before being routed to our\nserverless location.\n\n- `ams` -> `fra1` -> `iad1`\n- `gru` -> `gru1` -> `iad1`\n- `hkg` -> `hkg1` -> `iad1`\n- `iad` -> `iad1` -> `iad1`\n- `jnb` -> `cpt1` -> `iad1`\n- `syd` -> `syd1` -> `iad1`\n\nWe never encountered a request routed to a different data center, and we never\nhit the Vercel cache.\n\n### Warm - `/api/ping/warm`\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILS\n\n</div>\n<div>\n\n**12,090**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**246**ms P50\n\n</div>\n<div>\n\n**305**ms P75\n\n</div>\n<div>\n\n**442**ms P90\n\n</div>\n<div>\n\n**563**ms P95\n\n</div>\n<div>\n\n**855**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/monitoring-vercel/vercel-warm.json\"\n    caption=\"Vercel warm p50 latency between 10. Mar and 13. Mar 2024 aggregated in a 1h window.\"\n  />\n</div>\n\nWe are pinging this functions every 5 minutes to keep it warm.\n\n### Cold - `/api/ping/cold`\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILS\n\n</div>\n<div>\n\n**2,010**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**859**ms P50\n\n</div>\n<div>\n\n**933**ms P75\n\n</div>\n<div>\n\n**1,004**ms P90\n\n</div>\n<div>\n\n**1,046**ms P95\n\n</div>\n<div>\n\n**1,156**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/monitoring-vercel/vercel-cold.json\"\n    caption=\"Vercel cold p50 latency between 10. Mar and 13. Mar 2024 aggregated in a 1h window.\"\n  />\n</div>\n\nWe are pinging this functions every 30 minutes to ensure the functions will be\nscaled down.\n\n### Cold Roulette - `/api/ping`\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILS\n\n</div>\n<div>\n\n**6,036**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**305**ms P50\n\n</div>\n<div>\n\n**791**ms P75\n\n</div>\n<div>\n\n**914**ms P90\n\n</div>\n<div>\n\n**972**ms P95\n\n</div>\n<div>\n\n**1,086**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/monitoring-vercel/vercel-roulette.json\"\n    caption=\"Vercel roulette p50 latency between 10. Mar and 13. Mar 2024 aggregated in a 1h window.\"\n  />\n</div>\n\nWe are pinging this functions every 10 minutes. It's an inflection point where\nwe never know if the function will be warm or cold.\n\n## Vercel Edge Function\n\nVercel Edge Functions is using the Edge Runtime. They are deployed globally and\nexecuted in a datacenter close to the user.\n\nThey have limitations compared to the NodeJs runtime, but they have a faster\ncold start.\n\nWe analyzed the request header and found that the `X-Vercel-Id` header indicates\nthe request is processed in a datacenter near the user.\n\n- `ams` -> `fra1`\n- `gru` -> `gru1`\n- `hkg` -> `hkg1`\n- `iad` -> `iad1`\n- `jnb` -> `cpt1`\n- `syd` -> `syd1`\n\n### Edge - `/api/ping/edge`\n\n<Grid cols={5}>\n<div>\n\n**100**% UPTIME\n\n</div>\n<div>\n\n**0**# FAILS\n\n</div>\n<div>\n\n**6,042**# PINGS\n\n</div>\n<div className=\"hidden md:block\"></div>\n<div className=\"hidden md:block\"></div>\n<div>\n\n**106**ms P50\n\n</div>\n<div>\n\n**124**ms P75\n\n</div>\n<div>\n\n**152**ms P90\n\n</div>\n<div>\n\n**178**ms P95\n\n</div>\n<div>\n\n**328**ms P99\n\n</div>\n</Grid>\n\n<div className=\"mt-4\">\n  <SimpleChart\n    staticFile=\"/assets/posts/monitoring-vercel/vercel-edge.json\"\n    caption=\"Vercel edge p50 latency between 10. Mar and 13. Mar 2024 aggregated in a 1h window.\"\n  />\n</div>\n\nWe are pinging this functions every 10 minutes.\n\n## Conclusion\n\n| Runtime | p50 | p95 | p99 |\n| --- | --- | --- | --- |\n| Serverless Cold Start | 859ms | 1 | 046ms |\n| Serverless Warm | 246ms | 563ms | 855ms |\n| Edge | 106ms | 178ms | 328ms |\n\nGlobablly Edge functions are approximately 9 times faster than Serverless\nfunctions during cold starts, but only 2 times faster when the function is warm.\n\nEdge functions have similar latency regardless of the user's location. If you\nvalue your users and have a worldwide audience, you should consider Edge\nFunctions.\n\nCreate an account on [OpenStatus](/app/sign-up) to\nmonitor your API and get notified when your latency increases.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/new-dashboard-we-are-so-back.mdx",
    "content": "---\ntitle: \"We Are So Back\"\ndescription: \"New dashboard - rebuilt for clarity, speed and insights.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2025-08-03\"\nimage: \"/assets/posts/new-dashboard-we-are-so-back/we-are-so-back.png\"\ncategory: \"company\"\n---\n\nWith a few days delay exactly two years ago (30.07.2023), we launched openstatus with Thibault. Today marks the day we sit together for the second time in real life and are launching the new version of the dashboard. _What a blast!_\n\n<Image\n  alt=\"Us celebratring the launch of the dashboard\"\n  src=\"/assets/posts/new-dashboard-we-are-so-back/us.jpg\"\n  width={650}\n  height={575}\n/>\n\nIt didn’t start from nowhere. Over the last few months, we’ve collected users feedback, worked on our product strategy and listened to our core heart: monitoring and status pages.\n\nBefore diving in, if you are new - create an account - or just login with your existing one!\n\nhttps://app.openstatus.dev\n\n## New Dashboard - What has changed?\n\nLet's **compare** right away: old (1) vs new (2) dashboard!\n\n<figure>\n  <Image\n    alt=\"View of the old dashboard\"\n    src=\"/assets/posts/new-dashboard-we-are-so-back/dashboard-before.png\"\n    width={650}\n    height={575}\n  />\n  <figcaption>Old Dashboard with top tab navigation</figcaption>\n</figure>\n\n<figure>\n  <Image\n    alt=\"View of the new dashboard\"\n    src=\"/assets/posts/new-dashboard-we-are-so-back/dashboard-after.png\"\n    width={650}\n    height={575}\n  />\n  <figcaption>New Dashboard with left sidebar navigation</figcaption>\n</figure>\n\n\n### Different Layout\n\nThe new design will look different to what you know from openstatus and might _look more familiar - maybe boring_. From day one, we used shadcn as our design system - back when it had just started as well. It's been in our DNA. We're keeping everything you already know, but with different color templates and the removal of the dotted background. We hope you won't miss it too much.\n\nObviously, the biggest change on first sight is the layout: we are embracing the **sidebar navigation** to share more informations. Every individual status page or monitor will be shown there, making it easier to navigate across entries and create new ones on the fly. They also include the status of the current entry (e.g. _active_, _error_, _degraded_) - heavily inspired by Tinybird's dashboard sidebar.\n\nSince we moved the main navigation to the sidebar, the top header now shows **breadcrumb navigation** and lets you quickly switch between subpages via a select option.\n\nWe’ve also added a **right-hand sidebar** with additional information about the page you’re viewing. It’s hidden by default to avoid overwhelming you, but can be revealed when you need it – inspired by Axiom.\n\n> The products you use, inspire the products you build.\n\nWe removed the top-level '_Incidents_' navigation. It took too much visual space and had little impact on how users interacted - which is normal, since we hadn’t invested much in it. We don’t want to solve everything related to uptime monitoring. **What we do want to solve, we want to solve well.** That requires focus - otherwise you start over-bloating the dashboard.\n\nInstead, each incident now lives within its corresponding monitor’s subpage. We are not competing with on-call or incident response tools. Instead, we’ll build integrations so you can pick your favorite tool. Contact us if you’re missing an integration.\n\n### More Elements\n\nWe have been collecting data that we never displayed. Sometimes you design something internally but realize users could benefit from it too. That was the case with our Tinybird `audit_logs` table (btw, proud of the implementation on [GitHub](https://github.com/openstatusHQ/openstatus/tree/main/packages/tinybird/src/audit-log)). We track:\n- From which region your monitor fails\n- When it’s degraded\n- When and to which channel we send notifications\n\nUntil now, this was invisible in the dashboard. We’re adding the **Timeline Table** so you can keep track of internal state changes.\n\nBut there’s more - three new visual components to help you understand your monitors at a glance:\n\n- **Timing Phase Area Chart**: Shows the full breakdown of request timing phases (DNS, transfer, TTFB,...) across all selected regions, making it easy to spot where delays are happening.\n- **Uptime Bar Chart**: Provides a more granular view of uptime, along with the number of pings we’ve sent to your endpoint and the request status (_success_, _degraded_ or _error_)\n- **Degraded Overview Card**: Highlights requests that didn’t fail outright but were still degraded, helping you catch subtle performance issues before they escalate.\n\nWe’ve also reworked the **Response Logs Table**. You can now filter by custom date ranges within the last 14 days.\n\n## New @shadcn template\n\nWe are dropping a new dashboard template free to use! The [`openstatus-template`](https://github.com/OpenStatusHQ/openstatus-template) is a **full shadcn x Nextjs SPA (so BYO router) starter kit** with some dashboard components ready to use via shadcn CLI.\n\nhttps://template.openstatus.dev\n\nEverybody's dashboard requires some high-level components for sections, forms, action cards. That's what makes the component part of react so simple. We've been embracing the CSS pattern to name every component with a `data-slot=\"name\"` so that the siblings, children or parent component change if the component exists. Same belongs to the `data-variant=\"name\"` data states where you can switch between different styles. Once it clicks, you're not going back.\n\nFor example is our `FormCardContent` using the following tailwind class:\n\n```tsx\nconst formCardContent = \"px-4 group-has-data-[slot=card-upgrade]:opacity-50 group-has-data-[slot=card-upgrade]:pointer-events-none\"\n// you should still add <input disable={LOCKED} /> within the form\n```\n\nThat way, whenever we add the `FormCardUpgrade` component into the mix, it will automatically change the behavior of the content.\n\n>Tailwind + shadcn is faster to design as dev than starting with Figma.\n\nWe are providing them to you via shadcn cli, like:\n\n```bash\npnpm dlx shadcn@latest add https://template.openstatus.dev/r/form-card.json\n```\n\nThe following components are ready to be quick-installed:\n- `form-card` - inspired by Vercel’s settings cards with clear section separators\n- `metric-card` - inspired by Checkly's overview cards\n- `action-card`\n- `section`\n- `empty-state`\n\nRefer to the [README](https://github.com/openstatusHQ/openstatus-template) for more informations.\n\n**Use it, star it, share it!**\n\n## New Dashboard - What was our approach?\n\n### Phase 1: Vision - finding our value\n\nWe had to first understand our value in this wide ecosystem and what we want to focus on. We can't do everything our users ask for - otherwise we will loose sight of our true goal: empower devs to easily monitor APIs and Websites and connect to their users if something unexpected happened. Should be simple right?\n\nRead more about how we worked on our [product strategy](https://openstatus.dev/blog/product-strategy-a-reality-check) with Emily Omier.\n\n### Phase 2: Frontend - experimenting around\n\nEverything starts with some sketches: Reassemble all picked components from different platforms.\n\n> How big can the canvas be? Yes\n\n<figure>\n  <Image\n    alt=\"Excalidraw sketches from the dashboard\"\n    src=\"/assets/posts/new-dashboard-we-are-so-back/excalidraw.png\"\n    width={650}\n    height={575}\n  />\n  <figcaption>Excalidraw screenshot of the dashboard brainstorming.</figcaption>\n</figure>\n\n\nWe started by only the frontend as full decoupled template, where we've added dummy data to not bother about the rest and fully embrace the UI/UX and iterate quicky on a lightweight nextjs project. It frees your mind from the overbloated (some call it design debt, which just naturally happens over time) vision and is much faster to run. The benefit for you? You have one more template to have fun with!\n\n>\"If your product evolves fast, you should be paying this design debt every 2-3 years.\" - [linear.app](https://linear.app/blog/a-design-reset)\n\nWe didn’t wait to update the marketing page – time is limited and waiting until “everything” is finished means it will never ship.\n\n### Phase 3: Backend - connecting the dots\n\nWe've taken a new approach, inspired by Midday to prefetch trpc queries on the server and hydrate them on the client. We've also optimized the number of procedure calls and DB calls to get data which makes it overall quicker to navigate.\n\nAs we had most of the dummy data structure similar to our DB schema, it was easy to update/create trpc calls.\n\n---\n\nBig thanks to all early testers that provided incredible feedback and helped us shape the new dashboard. Starting from the template itself to once the dashboard was ready to be used. We really appreciate you.\n\n>Ask! Don't be scared to ask - people in the OSS space and beyond are increadibly helpfull and will let you know how they see your product.\n\n## Better CLI\n\nYou can now `import` your current monitors from the GUI into a YML file and `apply` changes to settings - or `trigger` checks when needed. Devs don't wanna switch context and want to keep track of changes within a version control. Let's enable them!\n\nRead more about the latest changes in our newly published [blog post](/blog/introducing-openstatus-cli).\n\nTo push **monitoring as code**, we are not only referring to our CLI docs but also create a dedicated `/cli` page with the most important commands, templates and links.\n\nWe'd love to here from what your use cases are and how you handle infra/monitoring as code.\n\n## Same Pricing. More Monitors.\n\nTo better align with the new changes, we've updated our pricing plans. [Read more](/blog/same-pricing-more-monitors).\n\n---\n\nThat's a wrap! We are back on track and more focused than ever.\n\nBut first, we’ll enjoy some summer vibes and then come back stronger - with improvements to the status page.\n\nCheers\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/nobody-should-hand-code-a-data-table.mdx",
    "content": "---\ntitle: \"Nobody should hand-code a data table in 2026\"\ndescription: \"We have rebuilt the data-table-filters with a single schema, state management adapters, shadcn registry distribution, and an AI agent skill.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2026-03-16\"\nimage: \"/assets/posts/nobody-should-hand-code-a-data-table/nobody-should-hand-code-a-data-table.png\"\ncategory: \"engineering\"\n---\n\nThe [data-table-filters](https://data-table.openstatus.dev?referrer=openstatus) project has been live for 1.5+ years. Filters, sorting, infinite scroll — the whole thing. It worked. People starred it. Bigger companies like [Supabase](https://supabase.com?referrer=openstatus) took inspiration for their logs dashboard.\n\nBut adopting it in another project wasn't straightforward. You'd have to clone the repo, wire together multiple files just to add a column — columns in one file, filter config in another, sheet fields in a third, Zod schema somewhere else. Change one, forget the others, wonder why nothing works.\n\n> It was a good idea. It was a bad DX.\n\nThen the **[shadcn](https://ui.shadcn.com?referrer=openstatus)** components registry and **agent skills** landed — and suddenly the pieces fit. We can ship code people own and an AI that knows how to set it up. No CLI installer needed. No npm package needed. And for AI coding tools, importing files via CLI keeps the context window lean — no need to overbloat it by copy-pasting entire repos worth of code.\n\nWe rebuilt the entire developer experience — refactoring code, improving composability, and shipping modern distribution — without compromising the UI taste. Here's what we shipped.\n\n---\n\n<Image src=\"/assets/posts/nobody-should-hand-code-a-data-table/data-table-filters-homepage.png\" alt=\"The data-table-filters homepage.\" />\n\n---\n\n## State management adapters\n\nBefore, the table was married to [nuqs](https://nuqs.dev?referrer=openstatus) (URL search params). If you didn't want URL state, tough luck.\n\nWe built a BYOS (Bring Your Own Store) architecture with three adapters that all implement the same interface:\n\n- **Memory** — state lives in React refs. No URL, no external store. The default when getting started.\n- **[nuqs](https://nuqs.dev?referrer=openstatus)** — state syncs to URL search params. Shareable links, back button works, SSR-friendly. The original behavior, now pluggable.\n- **[zustand](https://zustand.docs.pmnd.rs?referrer=openstatus)** — state lives in a zustand store via a `createFilterSlice()` helper. Drops into existing zustand setups.\n\nAll three support pause/resume (for live-streaming mode) and work with React 18's `useSyncExternalStore`. Swapping adapters is one line — the table doesn't know or care which one you're using.\n\nAdding a new store to the mix is straightforward too. Just implement the `StoreAdapter<T>` interface and you're good to go.\n\n## Single source of truth: the table schema\n\nThis was the big refactor. Before, defining a table meant keeping five separate files in sync:\n\n- `columns.tsx` — TanStack column definitions, cell renderers, sizing\n- `constants.tsx` — filter field configs, sheet field configs, UI properties\n- `schema.ts` — Zod validation for data rows AND a separate BYOS filter schema with serialization delimiters\n- `search-params.ts` — nuqs parser derived from the filter schema\n- `store.ts` — zustand slice derived from the filter schema\n\nAdding a column? Edit all five. Renaming a field? All five. Changing a filter type from checkbox to slider? Update the schema, the constants, and the columns file — and hope you didn't miss the filterFn.\n\nThe solution: define a column once, derive everything else.\n\n```tsx\nexport const tableSchema = createTableSchema({\n  level: col\n    .enum(LEVELS)\n    .label(\"Level\")\n    .filterable(\"checkbox\", {\n      options: LEVELS.map((l) => ({ label: l, value: l })),\n    })\n    .defaultOpen()\n    .size(27),\n\n  date: col\n    .timestamp()\n    .label(\"Date\")\n    .display(\"timestamp\")\n    .sortable()\n    .defaultOpen(),\n\n  latency: col\n    .number()\n    .label(\"Latency\")\n    .display(\"number\", { unit: \"ms\" })\n    .filterable(\"slider\"),\n});\n```\n\nOne definition. Four generators derive columns, filter fields, filter schema, and sheet fields automatically. Five files collapsed into one `table-schema.tsx`.\n\nThis became the foundation for everything that followed — the builder, the Drizzle integration, the registry all depend on it.\n\nIt was also only possible to build because of all the extra work done before. It's fine to copy-paste things and not extract too early — until you see the pattern and can actually optimize for it.\n\n## The schema builder\n\nIf the schema can be generated from a definition, why not generate it from raw data?\n\nA [builder](https://data-table.openstatus.dev/builder?referrer=openstatus) where you paste JSON (or upload a CSV) and instantly get a working, filterable table. No column definitions. No config. Just data in, table out.\n\n<Image src=\"/assets/posts/nobody-should-hand-code-a-data-table/builder-data-table.png\" alt=\"The schema builder: JSON on the left, live filterable table on the right.\" />\n\nThe components aren't fully customizable in the builder (serialization constraints), but it helps you get started and understand how schema changes affect the data table.\n\nThe inference engine detects types and applies domain heuristics — keys with \"latency\" get millisecond units, \"id\" columns get monospace display, trace IDs are auto-hidden. We ship presets for common patterns like timestamps, durations, and log levels. Contributions welcome — the more opinionated, well-built presets we have, the better the out-of-the-box experience gets for specific use cases.\n\nThe preview uses the same infinite-scroll architecture as the real thing — what you see in the builder is exactly what you'll ship.\n\n## Drizzle ORM: a real database with real data\n\nA client-side demo only takes you so far. People need to see this working with a real database, real data, growing over time.\n\nWe built a full [Drizzle ORM](https://orm.drizzle.team?referrer=openstatus) integration connected to a [Supabase](https://supabase.com?referrer=openstatus) PostgreSQL database.\n\nA [Vercel](https://vercel.com?referrer=openstatus) cron job runs every 10 minutes, generating realistic HTTP request logs — randomized timing metrics, status codes, multiple regions with latency multipliers. The data accumulates over time, so the demo always has fresh, realistic data to filter through — and it supports live mode (just time it right, the cron runs every 10 minutes).\n\nThe [`/drizzle`](https://data-table.openstatus.dev/drizzle?referrer=openstatus) route shows it all working together: infinite scroll with cursor-based pagination, faceted search with live mode, nuqs URL state so filters are shareable, and time-bucketed charts via PostgreSQL's `date_bin()`.\n\n<Image src=\"/assets/posts/nobody-should-hand-code-a-data-table/drizzle-data-table.png\" alt=\"The /drizzle route with live data, faceted filters, and time-bucketed charts.\" />\n\nThis is the kind of example that actually helps people adopt a library. Not \"here's a static demo\" — here's a production-like setup with a real database, real data pipeline, and all the pieces wired together.\n\n## Tests. Lots of tests.\n\n> Writing good tests is cheap nowadays. Just do it. Thank me later.\n\n39 test files covering the table schema, store adapters, builder, Drizzle ORM (including a dedicated SQL injection suite), and utilities. Everything from column builder validation to `'; DROP TABLE logs; --`.\n\nCI runs against a real PostgreSQL container — migrations, seed data, then tests. No mocks for the database layer. We want this production-ready, so making sure test coverage is solid and tests are green is non-negotiable.\n\n## Distribution killer: shadcn registry + agent skill\n\nA GitHub issue ([#39](https://github.com/openstatushq/data-table-filters/issues/39?referrer=openstatus)) put it plainly: \"Can you please provide it as a package, so that it could easily be installed and managed?\" Another ([#14](https://github.com/openstatushq/data-table-filters/issues/14?referrer=openstatus)) asked for a Vite example, which would've meant restructuring into a monorepo.\n\nThe **[shadcn registry](https://ui.shadcn.com/docs/registry?referrer=openstatus)** solves the distribution problem. Adopting it required migrating to Tailwind v4 first (which was long overdue!) — the registry blocks declare CSS variables using `@theme inline` syntax, so v4 was a prerequisite. Then we created a `registry.json` with 9 installable blocks:\n\n```bash\nnpx shadcn@latest add https://data-table.openstatus.dev/r/data-table.json\n```\n\nOne command. Dependencies resolved. CSS variables injected. Path aliases rewritten. Install just the core, or add Drizzle helpers, command palette, zustand adapter — whatever you need.\n\nThen there's the **agent skill**.\n\nThink about what a CLI installer does: it asks you questions (\"TypeScript?\", \"Which state manager?\", \"Do you use Drizzle?\"), then generates files based on your answers. It's a decision tree pretending to be a conversation.\n\nAn agent skill is an actual conversation. Install it with:\n\n```bash\nnpx skills add https://github.com/openstatushq/data-table-filters --skill data-table-filters\n```\n\nWhen someone opens their AI coding tool and says \"add a filterable data table\", the skill activates.\n\n<Image src=\"/assets/posts/nobody-should-hand-code-a-data-table/agent-data-table.png\" alt=\"asking 'add a data-table example' - no extra guidance (after setting up the skill).\" />\n\nIt understands the project, installs the right blocks, generates a schema from the data model, wires up the state adapter, and configures the database integration — if you ask for it. Start minimal and expand to your use case.\n\nWe also rewrote all the [docs](https://data-table.openstatus.dev/docs?referrer=openstatus) from scratch. Not glamorous work — but when an AI reads your docs to install your library, doc quality directly affects agent quality. **Better docs, better agent.**\n\n## Where this is going\n\nWe keep improving data-table-filters to make it the fastest way to spin up large, production-ready dataset views for logs and beyond.\n\n> It's not a library. It's a playbook.\n\nThe combination of shadcn registries and agent skills is a really good distribution model for frontend libraries. You don't publish a package with a fixed API. You ship composable code blocks, matching your design system, that an AI understands how to assemble.\n\nNot \"install this package and read the docs.\" More like \"tell your AI what you need and it builds it from well-structured, composable pieces.\"\n\n> Nobody should hand-code a data table anymore.\n\nThe full project is open source at [data-table-filters](https://github.com/openstatushq/data-table-filters?referrer=openstatus). Try the builder at [data-table.openstatus.dev/builder](https://data-table.openstatus.dev/builder?referrer=openstatus). Or just open your favorite chat interface, install the agent skill, and say \"add a filterable data table\" — the skill will take it from there.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/openstatus-infra.mdx",
    "content": "---\ntitle: \"Building OpenStatus: A Deep Dive into Our Infrastructure Architecture\"\ndescription: \"A deep dive into OpenStatus' infrastructure architecture — how we use Vercel, Fly.io, Turso, and Tinybird to build a resilient synthetic monitoring platform.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2024-12-29\"\ncategory: \"engineering\"\nimage: \"/assets/posts/infra-openstatus/tech-infra.png\"\n---\n\n## Infrastructure Overview\n\nOpenStatus is a synthetic monitoring platform designed with resilience, scalability, and efficiency in mind.\nOur users rely on us to provide real-time insights into their service health, making it essential to maintain a robust and performant infrastructure.\n\nIn this post, we'll take a deep dive into our infrastructure architecture, exploring the key components, managed services, and design principles that power OpenStatus.\n\n## Application Landscape\n\nOur platform consists of several interconnected applications, each designed for a specific purpose:\n\n1. **Frontend Ecosystem**:\n    - A NextJS application that powers our marketing site, user dashboard, and status page hosted on [Vercel](https://vercel.com/).\n    - An Astro + Starlight-powered documentation application hosted on [Cloudflare Pages](https://pages.cloudflare.com/).\n\nWe chose Vercel for the Next.js application because it performs exceptionally well there, the DX is great. And we selected Cloudflare Pages for the documentation since it is a static site and it's super cheap.\n\n2. **Backend Infrastructure** All our backend services are hosted on Fly.io.\n    - API server: Our public API and our alerting engine\n    - Probes/Checker: a golang app deployed globally to monitor your service\n    - Screenshot app: a service that takes screenshot of your website when we detect an downtime (Playwright)\n    - Workflow engine: a server that handles the workflow of alerting,  and our internal workflows (email automation).\n\nWe chose Fly.io for our backend services because it's a great platform for deploying globally distributed services. It's also very easy to deploy and manage.\nWe are planning to add more providers (e.g. Koyeb) to our probes to have a more resilient system.\n\n<Image\n  alt=\"Hosting providers\"\n  src=\"/assets/posts/infra-openstatus/hosting.png\"\n  width={650}\n  height={575}\n/>\n\n\n## Managed Services\n\nWe also rely heavily on managed services to avoid handling it ourselves. Here are the services we use:\n\n### Scheduling\n\nRecognizing the critical nature of monitoring, we've heavily rely on CRON  to ensure timely checks:\n\n- **Cron Jobs**: Currently using Vercel Cron, with plans to migrate to Google Cron for an enhanced user experience (better UI e.g. we can see when the cron ran, retry policy).\n\n\n### Queue Architecture\n\nDue to the critical nature of checks, we are using a queue to handle task processing and retry logic:\n\nEvery check is pushed to a queue and processed by our probes. If the probe fails to process the check, it is retried 3 times before being marked as failed.\n\n- **Job Queue**: Google Task Queues provide our distributed task management, with strategically segmented queues for different check frequencies\n\nWe've implemented a granular queue system to ensure efficient task processing, each queue is dedicated to a specific check frequency (e.g. every minute, every 10 minutes).\n\n\n<Image\n  alt=\"Queue providers\"\n  src=\"/assets/posts/infra-openstatus/queue.png\"\n  width={650}\n  height={575}\n/>\n\n\n### Data Infrastructure\n\nWe also don't want to handle the data infrastructure by ourselves. We rely on managed services for that:\n\n- **Primary Database**: [Turso](https://turso.tech?ref=openstatus.dev), providing a cost efficient data storage solution. We love the fact that's it's hosted SQLite database. It's just a file we can embedded in our services and sync it periodically.\n- **Analytics Database**: [Tinybird](https://www.tinybird.co?ref=openstatus.dev), enabling complex analytical queries and insights.\n\n## Design Philosophy\n\nOur infrastructure design is driven by several key principles:\n\n- **Resilience**: Ensuring high availability and fault tolerance\n- **Scalability**: Architectural choices that allow seamless growth\n- **Cost-Efficiency**: Leveraging managed services and cloud credits\n- **Performance**: Optimizing each component for maximum efficiency.\n\n\n## How much does it cost us?\n\nOur current monthly cost is around $328. This includes:\n\n- Vercel: $40, we are two members in the team, so we had to upgrade to the team plan.\n- Fly.io: $154   36*4 (all our probes at $4 average, not all regions cost the same) + $10 (for the api server)\n- Google Cloud Platform: $0 (We are still using the free credits, but we expect to pay around $50 for the queue)\n- Tinybird: $100\n- Turso: $29\n- Cloudfare: $5\n\n\n\n# Conclusion\n\nBuilding a resilient synthetic monitoring platform is hard. It's not just a $5 VPS that you can deploy and forget. It requires a more complex infrastructure to be able to provide a reliable service.\n\nThe drawback of this approach is the complexity of providing an easy self hostable services. Which is annoying because we are an open-source project and we want to provide a self-hostable version of OpenStatus. But we are working on community edition that will be easier to deploy.\n\n*Want to start monitoring your services with OpenStatus? [Sign up for free](/app/login?ref=blogpost-infra) and get started today!*"
  },
  {
    "path": "apps/web/src/content/pages/blog/openstatus-light-viewer.mdx",
    "content": "---\ntitle: \"OpenStatus Light Viewer\"\ndescription: \"The perfect companion for the vercel-edge-ping project\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2025-03-30\"\nimage: \"/assets/posts/openstatus-light-viewer/light-viewer.png\"\ncategory: \"engineering\"\n---\n\n› [logs.run/light](https://logs.run/light)\n\nA few months ago, we released [light.openstatus.dev](https://light.openstatus.dev) (`vercel-edge-ping`) - a lightweight community edition of OpenStatus designed to monitor your `HTTP` endpoints via Vercel's edge regions and notify you if `>50%` of the requests go down. No dependencies, no UI, just the essentials. For more details, check out our [README](https://github.com/openstatusHQ/vercel-edge-ping).\n\nNow, we’re excited to announce support for an extensive dashboard. If you're already using Tinybird to store your data, simply redeploy the latest `vercel-edge-ping` project and include the updated [pipes](https://github.com/openstatusHQ/vercel-edge-ping/tree/main/tb/pipes) in Tinybird. You can then access your values via [logs.run/light](https://logs.run/light) by updating the base URL of the API endpoint (use the floating button in the bottom left corner). By default, it will display demo values from [light.openstatus.dev](https://light.openstatus.dev).\n\n> Own your data. Build your dashboard.\n\nGet your own lightweight OpenStatus in two steps:\n\n1. One-click deploy [`vercel-edge-ping`](https://github.com/openstatusHQ/vercel-edge-ping)\n2. Change base URL of the API endpoint\n\n![OpenStatus Light Viewer](/assets/posts/openstatus-light-viewer/data-table-popover.png)\n\n### Build your dashboard\n\nFor everyone who wants to build their own dashboard with the [`data-table-filters`](https://github.com/openstatusHQ/data-table-filters) project, here's a snapshot of the current **Folder Structure** that's been used:\n\n```json\n/src/app/light\n├── api\n│   └── route.ts\n├── client.tsx\n├── columns.tsx\n├── constants.tsx\n├── layout.tsx\n├── page.tsx\n├── query-options.ts\n└── search-params.ts\n```\n\n- **`route.ts`**: API endpoint to fetch Tinybird values.\n- **`client.tsx`**: Tanstack query client to fetch data from `route.ts`.\n- **`columns.tsx`**: TanStack column array configuration.\n- **`constants.tsx`**: Filter and sheet fields configuration.\n- **`layout.tsx`**: Simple layout component.\n- **`page.tsx`**: Server component to cache `nuqs` search parameters.\n- **`query-options.ts`**: Infinite query options used in `client.tsx`.\n- **`search-params.ts`**: `nuqs` query parameters configuration.\n\nWe explore most of these files in the [Guide (WIP)](https://logs.run/guide) to support you in building your own data-table.\n\nWe're continuously refining the setup. While some configurations may appear duplicated, our ultimate goal is to consolidate everything into a single configuration file, making it easier to build an infinite logs data table from front to back.\n\nWe're taking you along on this journey.\n\n---\n\n\n### What is the `vercel-edge-ping` project?\n\nThe [`vercel-edge-ping`](https://github.com/openstatusHQ/vercel-edge-ping) project is a lightweight, community edition of OpenStatus with the following basic features:\n\n- Notification Channels (Slack, Discord, etc.)\n- Cron Job (via Vercel or GitHub Actions)\n- Storage (Tinybird)\n\n### Do I need to self-host the `data-table-filters` project to access my data?\n\nSelf-hosting is not required. If you have `vercel-edge-ping` ingesting data into Tinybird and are using the default pipes, you can open the API endpoint configuration. Click the floating button in the bottom left corner or press <kbd>⌘ + J</kbd>. Enter the base URL, and the `tb_endpoint` cookie will be set automatically. Delete the cookie manually if needed.\n\nUse the [data-table-filters](https://github.com/openstatusHQ/data-table-filters) to build your own logs tables. Check out [logs.run](https://logs.run) for a guide and more examples.\n\n### Is an authentication system provided?\n\nNo. The project is auth-agnostic, allowing you to implement your preferred authentication solution if needed. Be aware that your data will be publicly accessible via the API endpoint. We might add basic authentication in the future.\n\n---\n\n### What's next?\n\nWe are looking for more use cases. If you don't want to use Vercel, we are planning to add more cloud provider in the future.\n\nPlease reach out to [ping@openstatus.dev](mailto:ping@openstatus.dev) if you have specific use cases you'd like to see in action. Your feedback will help us refine the dashboard configuration and integrate more cloud providers.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/openstatus-slack-agent.mdx",
    "content": "---\ntitle: \"How We Built Our Slack Agent\"\ndescription: \"Manage incidents from Slack with natural language — no slash commands, no context switching.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2026-02-27\"\ncategory: \"engineering\"\nimage: \"/assets/posts/openstatus-slack-agent/slack-agent.png\"\n---\n\n\n# How We Built Our Slack Agent\n\nIt's 2 AM. Your API is returning 500s. You're in a Slack thread with your team, looking at logs, sharing stack traces, trying to figure out what's going on. The last thing you want to do is switch to a dashboard, find the right status page, fill out a form, and publish a status report.\n\nWe know switching tools during an incident is a DX-killer. We wanted to be where our users already are — in Slack, in the thread, in the conversation. So we built a Slack agent.\n\n`@openstatus` in a thread, describe what's happening, approve the draft, done. Your status page is updated without ever leaving Slack.\n\n## No Slash Commands\n\n> The only slash command I remember is the gif one — me, 2026\n\nWe decided not to include any slash commands, because in 2026 they don't make any sense. LLMs have gotten really good at understanding intent from natural language.\n\nCompare these two:\n\n```\n/openstatus create --title \"API Outage\" --status investigating --page 1 --message \"We are investigating elevated 500 error rates on our REST API.\"\n```\n\nvs.\n\n```\n@OpenStatus our API is returning 500s, can you create an incident?\n```\n\nSame result. One requires you to remember flags and IDs. The other is just talking to your team.\n\nThe agent understands context from the conversation. Say _\"we found the root cause\"_ and it knows to set the status to `identified`. Say _\"it's fixed\"_ and it drafts a resolution. You don't need to think about the status page schema — the agent handles that.\n\n## How It Works\n\nHere's what a full incident lifecycle looks like in Slack:\n\n### 1. Create an incident\n\nSomeone @mentions the bot in a channel or thread:\n\n> **@openstatus** our API is returning 500 errors for about 10% of requests. Can you create an incident on the status page?\n\nThe agent reads the message, looks up your status pages and components, and drafts a status report. Instead of publishing it directly, it posts a confirmation card:\n\n<Image\n  alt=\"openstatus Slack Agent confirmation card\"\n  src=\"/assets/posts/openstatus-slack-agent/thread.png\"\n  width={810}\n  height={1864}\n/>\n\nThe card shows the drafted title, status, target page, and message — so you can review before anything goes public.\n\nYou get three options:\n- **Approve** — publishes the status report.\n- **Approve & Notify** — publishes and sends a notification to all your subscribers (email, SMS, etc.).\n- **Cancel** — discards the draft.\n\n### 2. Post an update\n\n30 minutes later, you've identified the root cause. In the same thread:\n\n> **@openstatus** we found the cause — it's a connection pool exhaustion on the primary database. We're scaling up the pool now.\n\n<Image\n  alt=\"openstatus Slack Agent confirmation card\"\n  src=\"/assets/posts/openstatus-slack-agent/card.png\"\n  width={810}\n  height={1864}\n/>\n\nThe agent drafts an update with status `identified` and a professional message for the status page. Same confirmation flow — review, approve, done.\n\n### 3. Resolve\n\nOnce the fix is deployed:\n\n> **@openstatus** it's fixed, we deployed a patch to increase the connection pool size.\n\nThe agent drafts a resolution. Approve it and your status page shows the incident as resolved. If you choose \"Approve & Notify\", your subscribers get the all-clear.\n\nThe entire incident — from creation to resolution — happened in a single Slack thread. No tab switching, no forms, no context loss.\n\n## The Tech Behind It\n\n### Hono + Slack Events API\n\nWe integrated the Slack bot directly into our existing Hono server. No separate process, no Bolt framework. It's just a few new routes:\n\n- `POST /slack/events` — receives messages and mentions from Slack.\n- `POST /slack/interactions` — receives button clicks (approve, cancel).\n- `GET /slack/install` — kicks off the OAuth install flow.\n- `GET /slack/oauth/callback` — completes the OAuth handshake.\n\nEvery incoming POST is verified with Slack's HMAC-SHA256 signature. We wrote a Hono middleware for that — it checks the `x-slack-signature` header against the request body and a shared signing secret, and rejects anything older than 5 minutes.\n\n### The AI Agent\n\nWe use the [Vercel AI SDK](https://sdk.vercel.ai/) with Claude Sonnet. The agent has 6 tools:\n\n**Read tools** (execute immediately):\n- `listStatusPages` — returns all status pages and their components for the workspace.\n- `listStatusReports` — returns active (or all) reports with their latest update.\n\n**Mutation tools** (require human confirmation):\n- `createStatusReport`\n- `addStatusReportUpdate`\n- `updateStatusReport`\n- `resolveStatusReport`\n\nHere's the key design decision: mutation tools don't actually mutate anything. They return `{ needsConfirmation: true, params }` and the handler shows the confirmation card in Slack.\n\n```ts\nexport function createCreateStatusReportTool() {\n  return tool({\n    description: \"Create a new status report...\",\n    inputSchema: z.object({\n      title: z.string(),\n      status: z.enum([\"investigating\", \"identified\", \"monitoring\", \"resolved\"]),\n      message: z.string(),\n      pageId: z.number(),\n    }),\n    execute: async (input) => {\n      return { needsConfirmation: true as const, params: input };\n    },\n  });\n}\n```\n\nThe actual database writes only happen when the user clicks \"Approve\" in Slack. This means the AI can never publish something without explicit human approval.\n\n### Thread-Aware Context\n\nWhen the bot is mentioned in a thread, it reads up to 100 replies and converts them into a conversation history for the LLM. So when you say _\"can you resolve it?\"_ in a thread that already has an incident, the agent knows which report you're talking about.\n\nIf you refine your request in the same thread (say you change the wording before approving), the pending action is replaced — you don't get duplicate confirmation cards piling up.\n\n### Human-in-the-Loop Confirmation\n\nWe store pending actions in Redis with a 5-minute TTL. When you click a button:\n\n1. The handler does an atomic `getdel` on the Redis key — this means if you double-click or two people click at the same time, only one execution goes through.\n2. It checks that the person clicking is the same person who initiated the action. Anyone else gets an ephemeral _\"Only the person who initiated this action can approve or cancel it.\"_\n3. If approved, it runs the database transaction and updates the Slack message with a confirmation link to the status page.\n\n## Building for Slack: The DX Pain Points\n\nWhen building a Slack bot you can choose between Socket mode and HTTP mode.\n\nMaybe it's a skill issue, but we went with HTTP mode because we wanted to integrate it into our existing Hono server without requiring a separate process. The tradeoff: you need a public URL for Slack to deliver events to, which means setting up ngrok (or similar) to tunnel requests to your local machine during development.\n\nThe Slack developer UI is also clunky — configuring event subscriptions, OAuth scopes, and interactivity URLs across multiple tabs is not a great experience.\n\nA couple of things we learned the hard way:\n\n**Respond fast, process later.** Slack expects a response within 3 seconds. If you don't respond in time, Slack retries the event — and now you're processing the same incident twice. We respond `200 OK` immediately and process the event asynchronously in the background.\n\n**Deduplicate events.** Even with fast responses, Slack sometimes sends the same event multiple times. We keep an in-memory map of processed event IDs (with a 5-minute expiry) and skip any duplicates. Simple, but saves you from a lot of head-scratching.\n\n## What's Next\n\nRight now the agent handles status reports — creating, updating, and resolving incidents. But we're planning to expand it:\n\n- **Reminders** — set reminders to post update about your on-going incidents.\n- **Incident summaries** — ask the bot for a quick overview of all active incidents.\n\nWe want the Slack agent to become the primary interface for teams who live in Slack.\n\n## Try It\n\nThe Slack agent is available today on paid plans. Install it from your dashboard under **Settings > Integrations**, @mention it in any channel, and manage your next incident without leaving Slack.\n\n_Install the Slack agent from your [openstatus dashboard](https://app.openstatus.dev?ref=blog-slack-agent) and manage your next incident without leaving Slack._\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/our-new-pricing-explained.mdx",
    "content": "---\ntitle: \"Our new pricing explained, and why pricing is hard\"\ndescription: \"Why we overhauled OpenStatus pricing, dropped per-seat limits, and introduced Starter, Growth, and Pro plans. Lessons learned on SaaS pricing strategy.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2024-01-15\"\nimage: \"/assets/posts/our-new-pricing-explained/pricing-hard.png\"\ncategory: \"company\"\n---\n\nWe began the new year with two goals: to increase profitability and attract more\npaid users. One way to accomplish this was to overhaul our pricing.\n\nHowever, this was a challenging task due to the complexity of pricing.\n\nWe have introduced two new plans and renamed our current plan as follows:\n\n- Starter: 29e (previously known as the Pro plan)\n- Growth: 79e\n- Pro: 149e\n\nWe went through multiple iterations before selecting them.\n\n## Per Seat pricing sucks 🗑️\n\nWhen I initially shared the draft of our new [pricing](/pricing), we had a per\nseat limitation. It was shamelessly copied from our competitors, who all use per\nseat pricing, and many other SaaS businesses.\n\nWe believed it was the simplest way for our users to upgrade to a higher tier.\nHowever, the first reaction from\n[@tibotiber](https://www.twitter.com/@tibotiber),\n[@notrab](https://www.twitter.com/notrab), and\n[@chronark\\_](https://www.twitter.com/@chronark_) was to eliminate the per seat\npricing.\n\nI agreed with them, as per seat pricing doesn't add any value in our case.\n\nI remembered that I was frustrated when I was using a product with per seat\npricing. And what I did was to set up a team account with a catch-all email and\nshare the credentials with the team.\n\nWe dropped the per seat pricing.\n\n## Don’t be cheap 🤑\n\nBy the end of December, I have been asked for a €9 plan. Initially, when we\nrevised our pricing plan, we added this as our first option.\n\nWe believed that offering a plan at such a low price would attract more\ncustomers, as €9 is affordable for most companies and it would show their\nsupport for us.\n\nAfter couple of days, we realized that it was a mistake. We should not be cheap.\nWe are a fully bootstrapped, we can't compete on cheaper pricing, but we can\ncompete with a better product, though.\n\nIf we have a cheap plan, our customers will be biased towards using our product.\nWe won't feel it's worth the same thing as our competitor, even if our product\noffers a better alternative to them.\n\nAvoiding underpricing was the most challenging for us.\n\n## How we priced our plans 📈\n\nWe have wanted to only to price our product based on the value it provides to\nour users.\n\nThus we are only charging for:\n\n- The number of monitors\n- The number of alerts\n\nWhy only these two metrics?\n\n- For the monitors, we have a higher request frequency and therefore more data\n  to process on Tinybird. We also have a longer data retention period.\n- For the alerts we are paying our SMS provider.\n\nWe also wanted to keep it simple and avoid a pay-as-you-go plan. We don't want\nsome of our users to end up with a huge bill at the end of the month. 💸\n\n## Why did we keep the free plan? 🤔\n\nSome bootstrappers (like\n[marc](https://marclou.beehiiv.com/p/ditch-your-free-plan)) will tell you to\nremove the free plan. It's a waste of time.\n\nWe wanted to keep it within reasonable limitations. The number of monitors\nshould be enough for a small side project. For us, it's a way to attract more\nusers to use our product.\n\nAnd if more users are using our hosted status page, we will have more SEO\nbacklinks, and it's a win for us.\n\n<Image\n  alt=\"New users per week\"\n  src=\"/assets/posts/our-new-pricing-explained/ahref.png\"\n  width={650}\n  height={575}\n/>\n\n## Conclusion 🎉\n\nIt's hard to price your product. But our pricing is not static. If it's not\nworking, we will change it.\n\nWant to support us?\n[Sign-up](/app/sign-up?ref=pricing-blog) and pick the\nplan that rights for you.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/our-producthunt-launch-brutal-reality.mdx",
    "content": "---\ntitle: \"Our Product Hunt Launch: The Brutal Reality\"\ndescription: \"The honest story behind OpenStatus' unexpected Product Hunt launch — how we ended up as the #2 product of the day without any preparation.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2025-10-22\"\nimage: \"/assets/posts/producthunt-launch-brutal-reality/producthunt-launch.png\"\ncategory: \"company\"\n---\n\n\nHere’s the story of our Product Hunt launch, where we ended up as the second product of the day, along with our thoughts a couple of days later.\n\n## The Launch\n\nOn Saturday, October 18th, [Flo Merian](https://bsky.app/profile/fmerian.com) unexpectedly launched our product on [Product Hunt](https://www.producthunt.com/products/openstatus-2) without our knowledge.\n\nAt 9 AM, the hour of the launch, I received an email congratulating me on the launch and my product.\n\nInitially, I thought it was a broken AI outreach. We had never planned to launch OpenStatus on Product Hunt, but there we were. Unsure of how to react, we decided to embrace the situation and see what happened. Just doing what we do best: YOLO.\n\nSoon after, my LinkedIn inbox filled with outreach messages trying to sell me upvotes.\n\n<Image\n  alt=\"screenshot of linkedin inbox full of spam\"\n  src=\"/assets/posts/producthunt-launch-brutal-reality/linkedin-inbox.png\"\n  width={350}\n  height={575}\n/>\n\n\nI read a lot of  blog posts about preparing for Product Hunt day and how to achieve a high ranking. However, we followed none of this advice and continued with our day as we have planned it. Though I replied to every comments on the Product Hunt page. Also\n\nMax and I only posted once on our social media about the launch. My post promoting the launch was on super random. But people from our communities were super kind to upvote and comment and shared it. Thanks the OSS community!\n\n<Image\n  alt=\"My linkedin post about the producthunt launch\"\n  src=\"/assets/posts/producthunt-launch-brutal-reality/thibault-linkedinpost.png\"\n  width={800}\n  height={800}\n/>\n\nBy the end of the day, we finished second, behind Claude’s Skills by Anthropic.\n\n## The Metrics\n\nAs of October 22nd:\n-  Upvotes: 416\n- LinkedIn spam: a lot\n- GitHub stars: negligible\n- Users: approximately 10\n- Paying customers: 0\n- Visits: 687\n\n<Image\n  alt=\"screenshot of linkedin inbox full of spam\"\n  src=\"/assets/posts/producthunt-launch-brutal-reality/plausible.png\"\n  width={800}\n  height={800}\n/>\n\n\n## Why Didn’t We Launch Earlier?\n\nWhen we started OpenStatus, we considered launching for a long time. I told Max, “Let’s do it; it would be good.” However, he was hesitant. We knew we needed to prepare, and we feared disappointment if it didn’t work out. So, we never launched. Also probably knowing our perfect users is\n\n## My Opinion: Product Hunt is Dead, Prove me Wrong!\n\nGiven the spam I received and the lack of sales, I genuinely don’t believe the ratings I see there. Between the VC-backed tool upvoted by all their friends and investors, and the paid upvotes, the platform seems to be dead.\n\nIf you want to buy 100 upvotes, it’s only 40 USD.\n\nMaybe it’s just not the place to launch a dev tool; it’s only an AI tool nowadays, but Vercel launched their products a couple of years ago, and it’s really fun to see their old [launch](https://www.producthunt.com/products/vercel/launches/now-api).\n\nI don’t believe spending time on a Product Hunt launch is worthwhile, and it should not be taken seriously as part of your marketing strategy. I'm not even sure we will showcase the ProductHunt badge on our website.\n\nAlso, we launched on a Saturday; we did not end up in the newsletter, which could have helped us reach more people.\n\n\nProduct Hunt is Dead, Prove me Wrong!\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/pricing-update-july-2024.mdx",
    "content": "---\ntitle: \"Our latest pricing update: more value, cleaner numbers.\"\ndescription: \"We have update our pricing again, let's deep dive into it.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2024-07-24\"\nimage: \"/assets/posts/pricing-update-july-2024/pricing.png\"\ncategory: \"company\"\n---\n\n\nIt's been six months since our last pricing update, and a lot has changed. We've shipped a lot of new features  since January significantly improving our product, e.g :\n- [35 regions available](/changelog/more-regions),\n- [Password protected status page](/changelog/password-protected-status-page),\n- [PagerDuty integrations](/changelog/pagerduty-integration)\n\n\nIn light of these improvements, we realized our pricing needed a refresh to accurately reflect the current value we're providing to our users.\n\nLet's dive into the details of our update and why it matters.\n\n**We have grandfathered all our existing customers into the previous pricing, so this update only affects new customers. For the free users if you want to keep your old limit just [book](https://cal.com/team/openstatus/15min) a call with us.**\n\n## 🫳 9 We Dropped the 9: Embracing Clean Pricing\n\nYou may notice something unusual in our new pricing structure: the absence of prices ending in 9 This isn't a random choice, but a deliberate step towards more transparent and honest pricing.\n\nThe psychology behind pricing is fascinating. Research by [Paddle](https://www.paddle.com/blog/end-prices-in-9s) reveals that while prices ending in 9 can boost sales, it's a common practice across all industry. They're often associated with discount brands or perceived as a marketing ploy. By embracing round numbers, we're making a statement about our product's quality and value. We're not trying to create an illusion of a bargain; instead, we're confidently presenting our true worth.\n\n## 🌍💰More Regions, Lower Price\n\nWe're proud to say that even with our updated pricing, we remain more affordable than our competitors while offering more regions. Here's a quick comparison:\n\nWe currently support `35 regions`, and plans to expand to `41 regions` soon.\n\nOur main competitor, Datadog, only covers `29 regions`. They also charge per request `$7.20 per 10k requests`, which can quickly add up if you're monitoring multiple services.\n\nWe took a completly different approach we are charging per monitors instead of requests. You can still run some checks via our [API](https://api.openstatus.dev/v1#tag/monitor/POST/monitor/:id/trigger) if you need to.\n\nThis means you can set a fixed number of monitors without worrying about the number of requests our price is fixed.\n\n\n## 🏠💸 The Cost of Self-Hosting.\n\nSome of you might be wondering, \"Could I save money by hosting this myself?\" Let's break it down.\n\nOur checker is built in Go, making it incredibly lightweight and resource-efficient.\nUnlike most of our competitors who use Node-based checkers and/or Playwright, our solution can handle millions of requests on small machines.\nPlus, Golang's networking package is unparalleled for handling various network checks (with DNS, TCP, and ICMP support coming soon).\n\nIf you were to host our checker yourself across the same number of regions we cover, here's a rough cost estimate:\n\n```\n35 (Number of Fly Regions) * $3 (Average price per Fly Machine) = $105 per month\n```\n\n\nAnd that's just for hosting.\n\nIt doesn't account for the time and expertise required to manage and maintain the system across all these regions.\n\nBut you can get it for $30 with our `Starter` plan.\n\n## 🔄💰 Why Regular Pricing Updates Matter\n\nUpdating pricing regularly is crucial for any startup. It's a way to gauge how users value the product and ensure the business model remains sustainable as the product evolves.\n\nValuing a product correctly is one of the most challenging tasks for any team.\nIt requires a delicate balance between providing value to users and ensuring the company can continue to innovate and grow.\n\n## Conclusion\n\n\nOur pricing update reflects the significant improvements we've made to our product over the past six months.\n\nThis pricing allows us to continue providing top-notch service across more regions than our competitors, all while keeping costs manageable for our users.\n\nWe believe in transparency, which is why we've shared these insights into our pricing strategy.\n\nOur goal is to continue providing you with the best monitoring service.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/product-strategy-a-reality-check.mdx",
    "content": "---\ntitle: \"Product Strategy - A Reality Check\"\ndescription: \"Emily Omier challenged us to think deeply about our product strategy.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2025-07-31\"\nimage: \"/assets/posts/product-strategy-a-reality-check/emily-omier.png\"\ncategory: \"company\"\n---\n\nWe worked closely with [Emily Omier](https://emilyomier.com/) to understand our place in the open-source ecosystem and to challenge ourselves. Over the course of a month, every week, Emily hit us with tough questions and then questioned our answers again. All async - so everyone could work on it when they had the capacity.\n\nSpoiler alert: it’s been hard - really hard.\n\nThe high-level questions/topics were:\n- What is our unique value in the ecosystem?\n- Who is our target customer?\n- Open-source vs hosted product\n- What pain point do we solve better than others?\n- What is our opinion on the ecosystem?\n- Positioning Canvas\n\n> Let us know if you're interested in a closer look at the questions she asked.\n\nIt helped us reflect deeply on our product - what we actually solve, and how we differ from other solutions out there.\n\nOur core strengths are uptime/synthetic monitoring and status pages. We also looked closely at some nearby competitors in the space.\n\nLet's break some topics down.\n\n## Unique value in the ecosystem\n\nWe didn’t feel particularly unique. And that’s one of the hardest things we’re struggling with. If you know what makes you special, you can lean into it.\nWithout naming any competitors - most of them offer status pages and monitoring. So why would anyone choose an open-source, bootstrapped company?\n\nHere’s why:\n\nWe build in the open, share what we learn, and our work inspires others. At the start of our journey, we were lucky to make it onto the oss-friends list. We speak the same language as developers (and never like corporate brands). That’s just not who we are. We focus on crafting beautiful DX, UX, and UI - and that gets people talking.\n\nTechnically, we offer parallel scheduling and multi-region monitoring by default.\nBut most of our users primarily care about one thing: uptime. Parallel pings are resource-heavy, and if we want to offer more monitors at a lower cost, we should consider round-robin scheduling to reduce resource consumption and pass those savings to users.\n\nLots of competitors use a pay-as-you-go approach. We try to keep reasonable limits within a fixed pricing. Know in advance what you'll pay!\n\nWith the new dashboard and CLI (soon more), we’re moving toward an opinionated but clean DX/UX (inspired by the [Linear method](https://linear.app/method)) and focusing entirely on monitoring + status pages. We’re also updating our pricing very soon (if we haven't already) to better reflect what our users need.\n\n{/* ~~REMINDER: add post once published~~ */}\n\nWith tools like `shadcn` emerging and the increasing influence of AI-powered tools, we want to embrace this wave and provide even more value to the ecosystem.\n\nWe're a small, bootstrapped, and transparent team, we can move fast in terms of changes but it also implies moving slower due to pivots. So we have to be aware of our values and stay true to ourselves.\n\nAnd if you have an issue, you’ll hear from the Thibault or me directly.\n\n> Get in touch: via [cal.com](https://openstatus.dev/cal), [Discord](https://openstatus.dev/discord), or simply by [email](mailto:ping@openstatus.dev) - choose your channel.\n\n## Open-source vs hosted product\n\nWe’re devs and we think like devs. That’s why we build for devs. But devs don’t like to pay. They want to self-host.\n\nBut we’re not great at maintaining our self-hosted product. We’ve published a few repositories (like [`vercel-edge-ping`](https://github.com/openstatusHQ/vercel-edge-ping), [`astro-status-page`](https://github.com/openstatusHQ/astro-status-page)), but they don’t really help anyone self-host our full product.\n\nOver time, we’ve tried to reduce friction - like migrating from Clerk to NextAuth - but we’re not fully there yet. Thankfully, [Tinybird published a Docker version](https://www.tinybird.co/blog-posts/tinybird-local-docker-container), which removed one of our biggest blockers. And our usage of Google Queue can easily be replaced.\n\nOur resources are limited. Until there's a clear and urgent need to invest in self-hosting, we have to stay focused. That’s also why we’re not building an on-call system or branching out into features that would broaden our audience too much. Doing so would mess up our purpose: monitoring and status pages. Instead, we provide solid integrations with tools like [OpsGenie](https://docs.openstatus.dev/reference/notification/#opsgenie) and [PagerDuty](https://docs.openstatus.dev/reference/notification/#pagerduty).\n\nAlso: running OpenStatus yourself, with all features enabled, is often **more expensive** than paying for a hosted plan.\n\n> If you’re a larger company and want to support OpenStatus and open-source, we’d be happy to discuss funding this effort.\n\nWe have a [GitHub issue open](https://github.com/openstatusHQ/openstatus/issues/1209) to track this. We believe that we are much better than other open-source solutions out there (but they are easier to self-host), so we really want to make it happen. Let us know if you're interested in helping us out.\n\n## Pain points we solve better than others\n\nWe can’t (and won’t) say: “With OpenStatus, users recover X times faster.”  Mainly because users usually start with us to begin monitoring. Also, we’re not fans of bragging based on self-created numbers.\n\nBut we now provide a simple way for uptime monitoring as code: you don't need language stuff - just a simple yaml file. Run it in your CI/CD pipes and add additional health checks after deployments via our [GitHub integration](https://docs.openstatus.dev/guides/how-to-run-synthetic-test-github-action/).\n\n> We know that our marketing page doesn’t clearly explain the problems we solve.\n> It lists features (not even very well), but it should focus on the _problems_ we address - and how OpenStatus helps solve them. It will take time to rework our pages and fill it with words.\n\n## Our opinions in the ecosystem\n\n- Synthetic monitoring is broken. Every provider defines it differently, which makes adoption harder.\n- Synthetic monitoring is too expensive.\n- We need standardization, something like OpenTelemetry, for defining synthetic tests, monitors, and status pages:\n\t- Codegen\n\t- CLI\n\t- Import/export\n- We need better Infrastructure-as-Code support: for both monitors _and_ status pages.\n\n---\n\nWe were lucky to work closely with Emily - and lucky that she challenged us. If you are in the OSS space, we can only recommend reaching out.\nEven now, I can already picture her questioning our decisions as we move forward. She gave us timeless input, tough questions, and a method we’ll come back to whenever we find ourselves stuck.\n\nEverything takes time and we have reduced the number of new features shipped over the last months - on purpose. Changes are coming - and we’re excited to start this next chapter with you.\n\nAs always, we’re incredibly grateful for every feedback and support we are getting.\n\nWe’re learning as we go. Together!\n\nopen-source ftw\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/q1-2024-update.mdx",
    "content": "---\ntitle: \"OpenStatus Q1 2024 Update\"\ndescription: \"Let's review our first quarter of 2024.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2024-04-29\"\nimage: \"/assets/posts/q1-2024-update/q1-2024-update.png\"\ncategory: \"company\"\n---\n\nA lot has happened in the first quarter of 2024. We have been working hard to\nimprove OpenStatus and make it the best open source platform for monitoring the\nperformance of your services.\n\n## Metrics 📈\n\nOur MRR has increased by 108% compared to the last quarter of 2023 and our new\n[pricing](/blog/our-new-pricing-explained).\n\n<Image\n  alt=\"New users per week\"\n  src=\"/assets/posts/q1-2024-update/chart.png\"\n  width={650}\n  height={575}\n/>\n\nWe grew our user base by 38% and now have over 3000 users on the platform.\n\n## Product Updates 🚀\n\nWe have shipped a lot of new features in the first quarter of 2024, just to name\na few:\n\n- Reworked our dashboard\n- Terraform Providers\n- Request Assertion\n- Monitor Tags\n- Auto Resolved Incidents\n\nBut you should check out our [changelog](/changelog) for a complete list of updates.\n\n## Team Update 👥\n\nThibault went full time on OpenStatus in March 2024 and got father.\n\n## What to expect for the next quarter? 🤔\n\nWe are slowly rolling out Real User Monitoring (RUM) to all users. If you want\nto be opted in in the beta program, send an email to\n[ping@openstatus.dev](mailto:ping@openstatus.dev).\n\nWe are also working hard on improving our synthetic monitoring amd making it\nmore reliable.\n\nWe are also planing on making the platform more easily self hosted and remove\nmost of the external services we rely on. We want to switch from Clerk to either\nNextAuth V5 or Lucia.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/reflecting-1-year-building-openstatus.mdx",
    "content": "---\ntitle: \"Our Journey Building OpenStatus: From Idea to Reality\"\ndescription: \"Reflecting on our first year of building OpenStatus, an open source synthetic monitoring platform that has grown to more than 6,000 GitHub stars and 5,000 users.\"\nauthor: \"Thibault Le Ouay\"\npublishedAt: \"2024-09-26\"\nimage: \"/assets/posts/reflecting-1-year-building-openstatus/first-year.png\"\ncategory: \"company\"\n---\n\nIn one year, we've grown our open-source synthetic monitoring platform to more than 6,000 GitHub stars and 5,000 users. This article will reflect on our first year of building OpenStatus.\n\nWe've written about OpenStatus's journey before:\n\n- [Celebrated 48 hours of OpenStatus](/blog/the-first-48-hours?ref=blog-1year)\n- [Our 2023 year in review](/blog/2023-year-review?ref=blog-1year)\n\n## The Beginning: A Twitter Connection 🤝\n\nIt all started with a simple connection on Twitter. As members of the same developer community, we exchanged a couple of messages over the past few months.\nOur shared experiences and complementary skills led us to a decision: why not build something together?\n\n<Image\n  alt=\"Twitter\"\n  src=\"/assets/posts/reflecting-1-year-building-openstatus/twitter.png\"\n  width={650}\n  height={575}\n/>\n\n\n\n### Our Initial Goals\n\n1. Launch a viable product collaboratively\n2. Create a sustainable business.\n3. Build an open-source product\n4. Focus on developer tools\n\n## The Evolution of our Idea 💡\n\nOur journey began with a simple concept: a beautiful status page built with cutting-edge technology. We would populate this page with our own monitoring checks.\n\nThis initial idea served as the seed from which our entire project would grow.\n\n As we progressed, we talked to a lot of users, VC. We explored various directions, sometimes feeling a bit lost in the sea of possibilities:\n\n1. **Serverless monitoring**: Given that most of our users were Vercel customers, we considered specializing in this area.\n2. **Incident management**: Inspired by products like [incident.io](https://incident.io/), we contemplated starting with status pages and expanding into incident management. However, we realized this would target larger, Series B+ startups, involving longer sales processes and diverging from our core audience of small teams from bootstrapped businesses or Series A startups.\n3. **AI-enhanced alerting systems**: We briefly considered focusing on our product's alerting capabilities and incorporating AI for smart alerts. In retrospect, this seemed more like an attempt to capitalize on the AI trend rather than a strategic decision.\n4. **Real User Monitoring (RUM)**: We implemented this feature, but after discussions, we recognized the need to focus on our core strength: synthetic monitoring.\n\nUltimately, these explorations helped us refine our focus and understand our unique value proposition in the market.\n\n**We want to build the best open-source synthetic monitoring platform.**\n\nIn today's market, you'll find either closed-source products like Checkly and Datadog, or open-source solutions with poor user experience (such as Prometheus Blackbox Exporter).\n\n## Understanding our Market 🏇\n\nPrior to launching OpenStatus, we had experience with tools like:\n\n- Atlassian Statuspage\n- BetterStack\n- Datadog\n\nHowever, we weren't power users of these platforms. Our understanding of the ecosystem was broad but not deeply ingrained. This experience taught us a valuable lesson:\n\n**You don't need to be an expert in a field before starting. The key is to begin building—you'll learn along the way.**\n\n## Our Marketing Strategy 📈\n\n### Riding the OSS train\n\nWe started at the right time where lots of Open-Source Software companies like our friends at [papermark.io](https://papermark.io?ref=openstatus.dev), [uninbox.com](https://uninbox.com?ref=openstatus.dev) or [documenso.com](https://documenso.com?ref=openstatus.dev) flourished at the same time and made it into the oss-friends list by [formbricks.com](https://formbricks.com?ref=openstatus.dev).\n\n**Sometimes, you have to be lucky! We were fortunate to be in the right place at the right time, gaining the visibility that helped set us apart.**\n\n### Leveraging Cutting-Edge Technology\n\nBy utilizing the latest tech, we caught the attention of theirs developers, who shared our work on twitter bringing us more visibility.\n\nWe were early adopters of Turso, Next.js server actions, Bun, and Hono.\n\nBeing early adopters meant we encountered some bugs, but we could also benefit from the hype surrounding other products.\n\n### Open-Source as a Marketing Tool\n\nWe embraced open-source not just as a development philosophy but as a powerful marketing strategy.\n\nWe were multiple times in the github trending repository both global and for typescript.\n\nTo be in these lists you don’t only need to ship feature but you have to engage with your community on github.\n\n### Content Creation\n\nWe lacked a formal SEO strategy, but some of our blog posts performed well:\n\n- [Monitoring Serverless Providers](/blog/monitoring-latency-cf-workers-fly-koyeb-raylway-render)\n- [Migrating our backend from Vercel to Fly](/blog/migration-backend-from-vercel-to-fly)\n\nOur content approach was straightforward:\n\n> Write articles we'd love to read ourselves and not targeting keyword.\n\n### Organic Growth\n\nOur status pages served as an excellent marketing tool. Each time a user published one on their website, it created a valuable backlink for us\n\n### Tools and Components\n\nAdditionally, creating tools and components people love is a great way to draw attention and leverage SEO: [time.openstatus.dev](https://time.openstatus.dev) - a shadcn time picker drives 3k visitors per month organically. Same goal is with [data-table.openstatus.dev](https://data-table.openstatus.dev) - an extensive data-table for your realtime data.\n\n> We want to show users we are here to build with and for you. And of course in the open.\n\n## Our Initial Approach: YOLO 🤘\n\nIn our early days , we adopted what we call a \"YOLO\" mindset.\n\nOur approach was characterized by:\n\n1. **Rapid iteration**: We moved swiftly from one idea to another, exploring various possibilities without getting bogged down in extensive planning.\n2. **Flexibility**: Being a small team of two without external constraints allowed us to pivot quickly and adapt to new insights or market demands.\n3. **Learning through action**: Instead of extensive market research, we learned by doing, gaining valuable insights through direct experience.\n\nWhile this approach was exciting and allowed for quick exploration, it also had its challenges:\n\n- **Lack of structure**: Our development process was often messy and lacked clear direction.\n- **Inconsistent focus**: We sometimes struggled to maintain sustained effort on a single initiative.\n\nPerfect example would be the RUM idea: after having that idea, the time and the will we build the MVP of that feature. But what happens next is that you have an additional infrastructure to maintain and suddenly a new product to market to sell, next to synthetic monitoring.\n\nWhile it was fun, we don’t want to spread out too much and want stay in one domain and improve and make our users happy in that one, especially for a small team like us.\n\nThis yolo chaotic approach was sustainable while we had a limited user base.\n\nHowever, now that we've grown to more than 5,000 users, we recognize the need for a more structured and strategic approach to continue scaling effectively and meeting our users' needs.\n\n> Our customers rely on us; we can’t be flaky anymore, even though it was okay when we launched.\n\n### Project Management\n\nWe experimented with GitHub Projects for our workflow, aiming to make everything open-source—including our ideas and tickets. However, this approach didn't quite fit our needs and we couldn’t dump everything into it.\n\nNow, we've re-initialized Linear and are documenting our upcoming features more thoroughly.\n\nIt's our platform for brainstorming and iterating on ideas.\n\n## Personal Journeys 🧳\n\n### Max\n\n- Maintained a full-time job throughout 2023/2024 and still is at the time of writing\n- Plans to transition to OpenStatus at the end of the year\n\n> After working evenings and nights on OpenStatus for over a year, I had to make a personal decision to cut back on my 9-to-5 job. I've been neglecting other pillars in my life that I want to nurture again. While losing the income security, I'll free up mental space and open myself to new opportunities.\n>\n\n### Thibault\n\n- Left full-time employment in late February 2024\n- Welcomed a daughter in early March 2024\n\nDespite working remotely for most of the OpenStatus history, we finally met in mid-August. Max visited me, and we spent two great days:\n\n- Cooking together 🥘\n- Enjoying craft beers 🍻\n- Family time (with our partners and the lovely baby 👶)\n\n<Image\n  alt=\"Us\"\n  src=\"/assets/posts/reflecting-1-year-building-openstatus/us.png\"\n  width={650}\n  height={575}\n/>\n\n\n\nThis time reinforced our commitment to OpenStatus and our shared vision for its future. We didn't write a single line of code during that time—proving that you don't need to grind 24/7 to make progress.\n\n## The future 🚀\n\nWe all know it: You have created a dedicated roadmap that you’re excited working on but finally you start building everything else. And that’s fine (at a certain degree). The most important thing is to build, to learn and enjoy it - so that the inner drive keeps moving. We have been doing a lot of stuff that came up in the moment and build it (SDD - Self Driven Development) that does include bug fixes, user requests, community support on a higher priority.\n\nAs we reflect on our journey, we're excited about the road ahead. So how does our roadmap looks like (and we already know that it will not fully reflect the future - it’s a helpful path that provides us a North Star):\n\n- More checker types like TCP, UDP, ICMP,…\n- Improve the Single Checker experience via dashboard UI, config file, better use cases,…\n- Create an Alert Management Engine and improve the notification channels\n- Overall solidify the source code structure via tests, code splitting and staging environment\n- Being more integrated into your o11y stack\n\n> If you have ideas, requests or wanna chat feel free to contact us via [ping@openstatus.dev](mailto:ping@openstatus.dev) or join our Discord channel [/discord](https://openstatus.dev/discord)\n>\n\nOpenStatus has grown from a simple idea into a promising open-source project with a dedicated user base. We're committed to continuing our innovation, engaging with our community, and building tools that developers love.\n\nStay tuned for more updates as we continue to improve OpenStatus!\n\n\n<Image\n  alt=\"To infinity and beyond\"\n  src=\"/assets/posts/reflecting-1-year-building-openstatus/image.png\"\n  width={650}\n  height={575}\n/>\n\nTo infinity and beyond  🚀\n\nWant to join us on this journey? [Star us on GitHub](https://www.github.com/openstatusHQ/openstatus)! 🌟\nOr create an account on [Openstatus](/app/login) and start monitoring your services today!\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/rss-app-slack-feed.mdx",
    "content": "---\ntitle: \"RSS feed in your Slack channel\"\ndescription: \"Learn how to subscribe to RSS feeds in Slack and stay up-to-date with blog posts, changelogs, and status page updates.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2025-09-29\"\nimage: \"/assets/posts/rss-app-slack-feed/rss-app-slack-feed.png\"\ncategory: \"education\"\n---\n\nUsing RSS (**Really Simple Syndication**) to stay up to date is definitely underrated. Every site should provide an RSS feed - whether it’s for blog posts, changelogs, or status page updates like incidents, reports, and maintenances. RSS makes it simple to follow the latest news without scraping websites or doom-scrolling through social media.\n\nWe’ve supported RSS feeds for a long time, but only recently did I realize how powerful they really are for team communication.\n\nMost of our collaboration happens in Slack, so we've set up an `#rss` channel to subscribe to different feeds. Slack offers a native [RSS app](https://openstatus.slack.com/services/B09H29513HC) that lets you add RSS feeds directly into Slack channels. The app fetches updates every few minutes. It’s not truly real-time (since it polls periodically), so if you need instant notifications it’s not the best fit. But for most use cases, a short delay is perfectly fine - and even most paid Slack RSS integrations rely on the same periodic fetching model.\n\n---\n\n### How to add RSS feeds to Slack\n\nIt’s stupid simple to set up the Slack RSS app.  \n\nJust enter the feed URL and the channel where you want the updates to appear. Done.  \n\n<Image\n  alt=\"Slack RSS app settings - Subscribe to feed\"\n  src=\"/assets/posts/rss-app-slack-feed/rss-app-slack-feed-subscribe.png\"\n  width={650}\n  height={575}\n/>\n\nOnce set up, you can manage and view all your subscriptions in one list.  \n\n<Image\n  alt=\"Slack RSS app settings - List of subscribed feeds\"\n  src=\"/assets/posts/rss-app-slack-feed/slack-feed-settings.png\"\n  width={650}\n  height={575}\n/>\n\nRead more in the [official Slack instructions for adding RSS feeds](https://slack.com/help/articles/218688467-Add-RSS-feeds-to-Slack).\n\n---\n\n### Status page RSS feed support  \n\nWhile we don’t yet provide a direct Slack integration for OpenStatus updates, you can already subscribe to our **status page RSS feeds** (available on every plan), as well as feeds from our **blog** and **changelog**. This way, you can keep all important updates - whether outages, incidents, or maintenance - flowing into your Slack workspace automatically.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/same-pricing-more-monitors.mdx",
    "content": "---\ntitle: \"Same Pricing. More Monitors.\"\ndescription: \"We are updating our plans.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2025-08-02\"\nimage: \"/assets/posts/same-pricing-more-monitors/same-pricing-more-monitors.png\"\ncategory: \"company\"\n---\n\nWe’re updating our plans alongside the launch of the new [dashboard](https://openstatus.dev/blog/new-dashboard-we-are-so-back).\n\nA lot of users rely on openstatus for uptime checks and don’t really care about monitoring from _all_ 35 regions at once.\n\nUntil now, our **Starter** plan included just **5 monitors**, each checked from all 35 regions in parallel. Going forward, you’ll get **20 monitors**, with the option to choose **up to 6 regions per monitor** - still from the full list of 35.\n\nThe **Pro** plan is getting a big upgrade too: we’re going from **15 monitors** to **50(!)** - and you can still monitor from all **35 regions** in parallel if you want.\n\nThe free **Hobby** users aren’t affected by this change: you’ll still get **1 monitor**, with the ability to choose from 6 regions.\n\nOne thing is moving behind the paywall: **Response Logs**. We’ve added new high-level info like uptime charts and timeline tables that free users can access, but full response details will now be limited to paid plans - we’re hoping this encourages some upgrades. Paid users? No restrictions.\n\nGot questions, relying on response logs as a Hobby user or have been using parallel scheduling on all regions within the Starter plan? Reach out at [ping@openstatus.dev](mailto:ping@openstatus.dev).\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/secure-api-with-unkey.mdx",
    "content": "---\ntitle: \"How to secure your API with Unkey and Hono.js Middleware\"\ndescription: \"The simplest way to secure your API routes within seconds.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2023-10-01\"\nimage: \"/assets/posts/secure-api-with-unkey/unkey.png\"\ncategory: \"engineering\"\n---\n\n## Introduction\n\nWhy do we need to secure our APIs? Well, there are many reasons for that. The\nmost important one is that we want to protect your data from unauthorized\naccess.\n\nWe will learn how to secure our [Hono.js](https://hono.dev) API server with\n[Unkey](https://unkey.dev).\n\nUnkey is a service that allows you to create and manage API keys. It includes\nuseful features like:\n\n- **Rate limiting**: avoid getting DDoSed\n- **Temporary keys**: when working on free trials\n- **Key limitation**: limit the number of max. requests\n\n**The best part**: We have no additional database migration or setup to do. Just\nuse the Unkey API and/or SDK to create, revoke and verify keys.\n\nHono is similar to express just hipper and runs on the edge.\n\n### How does OpenStatus use Unkey?\n\nWhenever OpenStatus creates an API key, we will send a request to the Unkey API\nusing their [Typescript SDK](https://docs.unkey.dev/libraries/js/overview) with\nthe a specific `ownerId` which will be the `workspaceId` in our case. The user\nwill get an API key back which they can use to access their content via our API\nroute. Unkey will match the API key to the `ownerId` and we will be able to\nvalidate that the request is the owner of the `workspaceId`.\n\nAs an example, here the Next.js `server action` (see\n[GitHub](<https://github.com/openstatusHQ/openstatus/blob/main/apps/web/src/app/app/(dashboard)/%5BworkspaceSlug%5D/settings/_components/api-keys/actions.ts>))\nto allow users to create and revoke their own API keys:\n\n```ts title=\"actions.ts\"\n\"use server\";\n\nimport { Unkey } from \"@unkey/api\";\n\nconst unkey = new Unkey({ token: process.env.UNKEY_TOKEN });\n\nexport async function create(ownerId: number) {\n  const key = await unkey.keys.create({\n    apiId: process.env.UNKEY_API_ID,\n    ownerId: String(ownerId), // workspaceId\n    prefix: \"os\", // os_1234567890\n    // include more options like 'ratelimit', 'expires', 'remaining'\n  });\n  return key;\n}\n\nexport async function revoke(keyId: string) {\n  const res = await unkey.keys.revoke({ keyId });\n  return res;\n}\n```\n\nTo test key creation, you can simply go to the\n[Unkey Dashboard](https://unkey.dev/app) and create an API key manually instead\nof using the SDK. The SDK is useful once you want your users to create API keys\nprogrammatically.\n\n## Getting started\n\nCheckout [hono.dev](https://hono.dev) if you want to set up a new project or\nfollow along if you already have a Hono.js project.\n\nWe will pinpoint the most important parts of the setup. You can find the full\ncode source on\n[GitHub](https://github.com/openstatusHQ/openstatus/tree/main/apps/server).\n\n### Create the base path\n\nThat's as simple as it looks. Create a `new Hono()` instance and define the\nroutes (`route`) and middlewares (`use`).\n\nFor the sake of this example, we only consider the `/api/v1/monitor` route.\n\n```ts title=\"index.ts\"\nimport { middleware } from \"./middleware\";\nimport { monitorApi } from \"./monitor\";\n\nexport type Variables = { workspaceId: string }; // Context\n\nconst api = new Hono<{ Variables: Variables }>().basePath(\"/api/v1\");\n\napi.use(\"/*\", middleware);\napi.route(\"/monitor\", monitorApi);\n\nexport default app;\n```\n\n### Create the middleware\n\nThe middleware will automatically be applied to all routes that match the path\n`/api/v1/*`. We will use the `x-openstatus-key` request header to append the API\nkey and verify it on our server.\n\nThe Hono [Context](https://hono.dev/api/context) will be used to store the\n`workspaceId` we are retrieving from Unkey and sharing it across the\napplication.\n\nHere, we are verifying the API key via the\n[`@unkey/api`](https://docs.unkey.dev/libraries/js/overview) package. It returns\neither an `error` or the `result.valid` whether or not to grant access to the\nuser.\n\n```ts title=\"middleware.ts\"\nimport { verifyKey } from \"@unkey/api\";\nimport type { Context, Next } from \"hono\";\n\nimport type { Variables } from \"./index\";\n\nexport async function middleware(\n  c: Context<{ Variables: Variables }, \"/api/v1/*\">,\n  next: Next,\n) {\n  const key = c.req.header(\"x-openstatus-key\");\n\n  if (!key) return c.text(\"Unauthorized\", 401);\n\n  const { error, result } = await verifyKey(key);\n\n  // up to you if you want to pass the actual message to your users\n  // or simply return \"Internal Server Error\"\n  if (error) return c.text(error.message, 500);\n  if (!result.valid) return c.text(\"Unauthorized\", 401);\n\n  c.set(\"workspaceId\", result.ownerId);\n\n  await next();\n}\n```\n\n### Create the route\n\nEvery route, here `monitorApi`, will have access to the `workspaceId` via the\nContext and therefore can query the database for the workspace.\n\n```ts title=\"monitor.ts\"\nimport type { Variables } from \"./index\";\n\nexport const monitorApi = new Hono<{ Variables: Variables }>();\n\nmonitorApi.get(\"/:id\", async (c) => {\n  const workspaceId = c.get(\"workspaceId\");\n  const { id } = c.req.valid(\"param\");\n\n  // ...fetch data from your database [e.g. via Drizzle ORM]\n  const monitor = await db\n    .select()\n    .from(monitor)\n    .where(\n      and(\n        eq(monitor.id, Number(id)),\n        eq(monitor.workspaceId, Number(workspaceId)),\n      ),\n    )\n    .get();\n\n  return c.json(monitor);\n});\n```\n\nRead more about the Hono path parameter `\":id\"` in their\n[docs](https://hono.dev/api/routing#path-parameter).\n\n### Test it\n\nOnce your project is running, you can test your implementation with the\nfollowing `curl` command to access your monitor with the id `1`:\n\n```bash\ncurl --location 'http://localhost:3000/api/v1/monitor/1' \\\n--header 'x-openstatus-key: os_1234567890'\n```\n\nFor OpenStatus, we are running our Hono.js server on [fly.io](https://fly.io)\nwith [bun](https://bun.sh) via `bun run src/index.ts`.\n\n> We have included the\n> [`@hono/zod-openapi`](https://github.com/honojs/middleware/tree/main/packages/zod-openapi)\n> plugin to generate an OpenAPI spec out of the box. Read more about the\n> supported endpoints in our\n> [docs](https://api.openstatus.dev/v1).\n\n## Conclusion\n\nEt voilà. We have secured our API with Unkey and the Hono.js middleware, only\nallowing authorized users to access their data.\n\nUnkey increases our velocity and **helps us focus on what's relevant** to the\nuser, not the infrastructure behind it. We also get **key verification\ninsights** out of the box and can target specific users based on their usage.\n\n[@chronark\\_](https://twitter.com/chronark_) has recently published an\n[`@unkey/hono`](https://docs.unkey.dev/libraries/ts/hono) package that uses a\nsimilar implementation under the hood, reducing some boilerplate code for you.\nHighly recommend checking it out if you are using Hono.js.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/shadcn-component-registry.mdx",
    "content": "---\ntitle: \"How We Built Our shadcn Component Registry\"\ndescription: \"From duplicated components across three apps to a clean registry - our journey to building a shadcn/ui - compatible component library.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2026-02-15\"\nimage: \"/assets/posts/shadcn-component-registry/shadcn-component-registry.png\"\ncategory: \"engineering\"\n---\n\nAfter splitting our main web project into three distinct apps - `dashboard`, `status-page`, and `web` (our marketing site) - we'd duplicated the [shadcn/ui](https://ui.shadcn.com) components across each of them. Quick and easy, but not sustainable. It was time to consolidate our UI components into `packages/ui`.\n\n**The advantage:** maintain components in one place.\n\n**The disadvantage:** every change needs validation across all three apps (especially CSS).\n\nWhile refactoring our component library, I realized we could take this further: what if we turned our custom \"blocks\" into proper shadcn registry components and distributed them via the shadcn CLI? Better yet, what if we could use the shadcn CLI itself to add and update components within our `packages/ui`?\n\nThe challenge: we wanted to use the exact same components in our codebase - no build steps, no bundler, no webpack configuration - while making them easily distributable through shadcn's standard conventions.\n\nOur monorepo structure looks like this:\n\n```\nopenstatus/\n├── apps/\n│   ├── dashboard/        # Main dashboard app\n│   ├── status-page/      # Public status pages\n│   ├── web/              # Marketing site\n│   └── ...\n├── packages/\n│   ├── ui/               # Shared UI components\n│   │   ├── src/\n│   │   │   ├── components/\n│   │   │   │   ├── ui/       # shadcn components\n│   │   │   │   └── blocks/   # Custom blocks\n│   │   │   ├── lib/\n│   │   │   └── hooks/\n│   │   ├── components.json\n│   │   ├── registry.json\n│   │   └── package.json\n│   ├── db/\n│   ├── api/\n│   └── ...\n└── pnpm-workspace.yaml\n```\n\n## First Approach: Following the shadcn Defaults\n\nThe most obvious approach: just follow shadcn's standard setup. Every shadcn project uses a `@/` alias, so why not do the same in `packages/ui`?\n\nHere's the thing - TypeScript won't complain about this setup within the package itself. Everything type-checks perfectly. The problem only surfaces when you actually try to use these components in your apps.\n\nThe default shadcn approach uses a simple `@/` alias in `packages/ui/tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  }\n}\n```\n\nAnd write components using this alias in `packages/ui/src/components/ui/button.tsx`:\n\n```tsx\n// ✅ The shadcn/ui way\nimport { cn } from \"@/lib/utils\";\nimport { Slot } from \"@radix-ui/react-slot\";\n\nexport function Button({ className, ...props }) {\n  return <button className={cn(\"...\", className)} {...props} />;\n}\n```\n\nThe problem? When you import this button in your app:\n\n```tsx\n// apps/dashboard/src/app/page.tsx\nimport { Button } from \"@openstatus/ui/components/ui/button\";\n// ❌ Error: Cannot find module '@/lib/utils'\n// The @/ alias doesn't exist in the app's context!\n```\n\n## Second Approach: Bundle It Up\n\nIf the imports are the problem, what if we bundle the package and distribute compiled output? Tools like tsup can handle this cleanly, and many modern component libraries go this route.\n\nHere's what this would look like with tsup:\n\n```ts\n// packages/ui/tsup.config.ts\nimport { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  external: [\"react\", \"react-dom\"],\n  banner: {\n    js: '\"use client\";',\n  },\n});\n```\n\nAnd adding the build step to the package:\n\n```json\n// packages/ui/package.json\n{\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\"\n}\n```\n\nThe downsides:\n- Every change requires a rebuild (yes, even with `--watch` mode, there's still a delay)\n- Adds complexity to the development workflow\n- Build output needs to be managed and debugged\n- Still doesn't help with shadcn CLI integration for the registry\n\n## Third Approach: Configure Webpack\n\nWhat about webpack aliases? Next.js lets you customize the webpack config, so theoretically you could tell it how to resolve the imports. Here's what a working config would look like:\n\n```js\n// apps/dashboard/next.config.js\nconst path = require(\"path\");\n\nconst nextConfig = {\n  webpack: (config) => {\n    config.resolve.alias = {\n      ...config.resolve.alias,\n      // Resolve @openstatus/ui to source\n      \"@openstatus/ui\": path.resolve(__dirname, \"../../packages/ui/src\"),\n      // ALSO resolve @/ to the ui package - this is critical!\n      \"@\": path.resolve(__dirname, \"../../packages/ui/src\"),\n    };\n    return config;\n  },\n  transpilePackages: [\"@openstatus/ui\"],\n};\n```\n\nThe issue? You need **both** aliases. Just pointing `@openstatus/ui` to the source isn't enough - you also need to tell the app how to resolve the `@/` imports that appear _inside_ the package's components.\n\nBut now you have a **new problem**: what if your app also wants to use `@/` for its own imports? You've just created a conflict! The `@/` alias now points to the UI package, not your app's source directory. You'd need to choose between:\n\n1. Never using `@/` in your app (use a different alias like `@app/`)\n2. Using a different alias in the UI package (but then it's not compatible with standard shadcn components)\n3. Some complex webpack resolver that checks the importing file's location to decide what `@/` means (maintainability nightmare)\n\nThe problems:\n- Fragile configuration that breaks with Next.js updates\n- Needs to be duplicated in every app\n- Turbopack doesn't support custom webpack configs\n- **Most importantly:** Creates alias conflicts between app and package\n- Forces you to abandon standard `@/` convention in either the app or package\n\n## Fourth Approach: The Elegant Solution\n\nAfter three dead ends, we stepped back and thought differently. What if we stopped fighting against the monorepo structure and embraced it instead?\n\nThe insight: `@openstatus/ui` is already a valid import in every app - it's right there in their `package.json`. What if we used that same alias **within** `packages/ui` itself?\n\nThis means components would import from each other using `@openstatus/ui/...` internally. Then, when building the registry for distribution, we simply replace `@openstatus/ui` with `@`. That's it.\n\nNo bundler. No webpack config. No alias conflicts. Just a clever use of TypeScript path mapping and a simple find-replace at build time.\n\nHere's the setup in `packages/ui/tsconfig.json`:\n\n```json\n{\n  \"extends\": \"@openstatus/tsconfig/react-library.json\",\n  \"compilerOptions\": {\n    \"paths\": {\n      \"@openstatus/ui/*\": [\"./src/*\"]\n    }\n  }\n}\n```\n\nAnd the key part in `packages/ui/components.json`:\n\n```json\n{\n  \"aliases\": {\n    \"components\": \"@openstatus/ui/components\",\n    \"utils\": \"@openstatus/ui/lib/utils\",\n    \"ui\": \"@openstatus/ui/components/ui\",\n    \"lib\": \"@openstatus/ui/lib\",\n    \"hooks\": \"@openstatus/ui/hooks\"\n  }\n}\n```\n\n### The Bonus: shadcn CLI Integration\n\nThis setup unlocks a powerful benefit: **we can use the shadcn CLI directly in our package!**\n\nWhen shadcn releases new components or updates, we simply run:\n\n```bash\ncd packages/ui\nnpx shadcn add button      # Add new components\nnpx shadcn diff button     # Check for updates\n```\n\nThe CLI reads our `components.json` and automatically uses the `@openstatus/ui` aliases. Newly added components work immediately in both the package and all consuming apps - no manual import rewrites, no configuration updates. It just works.\n\n### How It Looks in Practice\n\nComponents within the package import from each other using `@openstatus/ui`:\n\n```tsx\n// packages/ui/src/components/blocks/status-banner.tsx\nimport { Tabs, TabsContent } from \"@openstatus/ui/components/ui/tabs\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport type { StatusType } from \"@openstatus/ui/components/blocks/status.types\";\n```\n\nApps import using the exact same pattern:\n\n```tsx\n// apps/dashboard/src/app/layout.tsx\nimport { Toaster } from \"@openstatus/ui/components/ui/sonner\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n```\n\nNotice something? **The imports are identical.** Whether you're inside the package or inside an app, the import path is the same. This consistency eliminates a whole class of path-resolution bugs.\n\n### The Magic: Import Transformation\n\nWhen building the registry for public distribution, we transform the imports with a simple regex (`packages/ui/scripts/transform-imports.mjs`):\n\n```js\n// Transform: @openstatus/ui/components/ui/button → @/components/ui/button\ncontent = content.replace(\n  /@openstatus\\/ui\\/(components|lib|hooks|types)/g,\n  \"@/$1\"\n);\n```\n\nThis converts our package-specific imports into the standard shadcn `@/` convention that everyone expects.\n\n### Publishing the Registry\n\nThe last piece of the puzzle is getting our components into a public registry that others can install. The build pipeline is a three-step process defined in `packages/ui/package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"registry:build\": \"node scripts/transform-imports.mjs && cd dist && shadcn build && cd .. && node scripts/copy-to-web.mjs\"\n  }\n}\n```\n\nHere's what each step does:\n\n1. **Transform imports** (`transform-imports.mjs`): Copies source files to `dist/` and rewrites all `@openstatus/ui` imports to `@` for standard shadcn compatibility\n2. **Build registry** (`shadcn build`): Generates the registry JSON files that describe each component and its dependencies\n3. **Copy to web** (`copy-to-web.mjs`): Moves the generated registry files to our Next.js app's public directory for hosting\n\nThe `copy-to-web.mjs` script is simple:\n\n```js\nconst registryDir = join(ROOT_DIR, \"dist/public/r\");\nconst targetDir = join(WEB_APP_PUBLIC_DIR, \"r\");\ncpSync(registryDir, targetDir, { recursive: true, force: true });\n```\n\nNow when users run:\n\n```bash\nnpx shadcn@latest add https://openstatus.dev/r/status-banner.json\n```\n\nThey're pulling from `apps/web/public/r/status-banner.json`, which was generated from the same components we use internally.\n\n## The Result\n\nWhat started as a refactoring task turned into a clean solution for both our internal needs and public distribution:\n\n- **One source of truth**: All three apps import from `@openstatus/ui`\n- **Zero build overhead**: Components work directly in development with no compilation\n- **Standard shadcn workflow**: `npx shadcn add/diff` works seamlessly in the package\n- **Public registry**: Anyone can install our components via the shadcn CLI\n\nThe key insight was embracing the monorepo structure instead of fighting it. By using the package name as the import alias, we avoided all the pitfalls of other approaches: no bundler complexity, no webpack configuration fragility, and no alias conflicts.\n\nIf you're running a multi-app monorepo with shared components, this pattern might save you considerable time and headaches. And if you need battle-tested status page components, check out our [component registry](/registry) - they're ready to drop into your project.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/status-page-addons.mdx",
    "content": "---\ntitle: \"Status Page Add-ons\"\ndescription: \"Enhance your status pages with email authentication, white label branding, and additional status pages.\"\nimage: \"/assets/posts/status-page-addons/status-page-addons.png\"\npublishedAt: \"2026-01-04\"\ncategory: \"company\"\nauthor: \"Maximilian Kaske\"\n---\n\nToday, we're introducing status page add-ons, bringing new features like white-labeling and email authentication to the plan.\n\n## Why Add-ons?\n\nWe're committed to keeping our core product accessible while offering advanced capabilities for teams with specific needs. These add-ons help you:\n\n- **Scale**: Add capacity as your infrastructure grows\n- **Control access**: Restrict who can view your status information\n- **Build trust**: Present a fully branded experience to your customers\n\n## Add-ons\n\n### Email Authentication\n\nProtect your status pages with domain-based access control. Configure allowed email domains to restrict visibility to specific users or organizations. Visitors must verify their email address (via magic link) before accessing your status page.\n\nPerfect for:\n- Internal status pages shared only with your team\n- Customer-facing pages restricted to specific organizations\n- Meeting compliance requirements around information disclosure\n- Limiting sensitive incident details to authorized stakeholders\n\n**Pricing**: $100/mo., applies to all status pages in your workspace\n\n### White Label\n\nRemove the \"powered by openstatus.dev\" branding from your status pages. Present a completely branded experience that aligns with your company's identity. Your customers will see only your brand, maintaining consistency across all touchpoints.\n\nTogether with the [custom themes](https://themes.openstatus.dev) we provide, you can create a fully branded status page experience.\n\n**Pricing**: $300/mo., applies to all status pages in your workspace\n\n### Additional Status Pages\n\nScale beyond your plan's included status pages with flexible per-page pricing. Whether you're managing multiple products, regional deployments, or client-specific status pages, grow your monitoring infrastructure without upgrading your entire plan.\n\nPerfect for:\n- Multi-product companies with separate status pages\n- Agencies managing client status pages\n- Regional deployments with localized status communication\n- Separate pages for testing and staging environments\n\n**Pricing**: $20/mo. per additional status page\n\n---\n\nOur add-on model gives you control over your investment. Start with the features you need today and add capabilities as your requirements evolve. All add-ons are available across all workspace plans, so you can access these features regardless of your current tier.\n\n<Image src=\"/assets/posts/status-page-addons/billing-addons.png\" alt=\"Workspace billing settings form container\" />\n\nVisit our [pricing](/pricing) page to learn more and get started.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/status-page-components.mdx",
    "content": "---\ntitle: \"Introducing Status Page Components\"\ndescription: \"A major architectural refactor that decouples status pages from monitors, enabling flexible status reporting and better self-hosting support.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2026-01-30\"\nimage: \"/assets/posts/status-page-components/status-page-components.png\"\ncategory: \"company\"\n---\n\nStatus pages started simple for us: a page showed the uptime of a monitor. That assumption shaped our entire data model - and eventually became our biggest limitation.\n\nWe embraced this tight coupling early on because it let us move fast. The downside? It made us move slow when requirements evolved.\n\nOver the year, we've received requests from users wanting to share status reports without any uptime monitoring attached. We tried addressing this with the `page.configuration` object - you could choose between bar type, card value, and showing uptime - but that was us duct-taping a more fundamental problem: status page components were tightly coupled to monitors.\n\n## The Solution\n\nFrom the ground up, we've reworked the relationships between status reports, maintenances, pages, and monitors. The result: **page components** - a flexible new architecture that decouples status pages from monitors.\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│                 BEFORE (monitors_to_pages)                   │\n└──────────────────────────────────────────────────────────────┘\n\n  incidents ───▶ monitors ◀─── maintenances\n                    │     ◀─── status_reports\n                    │\n                    ▼\n            monitors_to_pages (m-to-m)\n                    │\n                    ▼\n                  pages\n\n\n┌──────────────────────────────────────────────────────────────┐\n│                  AFTER (page_components)                     │\n└──────────────────────────────────────────────────────────────┘\n\n  incidents ───▶ monitors\n                    │\n                    │ (optional)\n                    ▼\n            page_components ◀─── maintenances\n                    │       ◀─── status_reports\n                    ▼\n                  pages\n```\n\n## Component Types\n\nA **page component** has a `type` that determines its behavior:\n\n- **static**: zero dependency on any uptime data—simply shows values based on connected status reports or maintenances\n- **monitor**: acts just like before where uptime data _can be_ tightly linked to our monitoring system (see `page.configuration` for more)\n\n### Future Types\n\nThese types can be extended. We see **third-party** as a big third pillar where status updates from third-party services can be displayed in near real-time. Feel free to [reach out to us](mailto:ping@openstatus.dev) if you're interested in those components.\n\n<Image src=\"/assets/posts/status-page-components/form-components.png\" alt=\"Page components form, grouped by regions\" />\n\n## Key Benefits\n\nThis architectural change brings several advantages:\n\n- **Flexibility**: Status pages are no longer limited to monitored services—share status updates for any component, monitored or not\n- **Simplified self-hosting**: Self-hosters can create status pages without setting up a full monitoring infrastructure\n- **Future extensibility**: The component-based architecture makes it easy to add new types like third-party integrations\n- **Better separation of concerns**: Monitoring and status reporting are now properly decoupled, allowing each to evolve independently\n\n## Pricing and Plan Limits\n\nPage components are counted across your workspace based on your plan tier:\n\n- **Hobby (free)**: 3 components\n- **Starter**: 20 components\n- **Pro**: 50 components\n\nThe flexibility of **static components** means you can now maximize your plan limits more effectively. Previously, every component required a monitor (which counts against your monitor limit). Now you can:\n\n- Use monitor-type components for services you want to actively check\n- Use static components for third-party dependencies, managed services, or components you update manually\n- Mix both types to get full visibility without needing a monitor for everything\n\n## Migration Approach\n\nOur strategy was to sync the legacy database while incrementally migrating every part of our stack:\n\n- database migration + setup syncing between `monitors_to_pages` and `page_components` tables\n- status page\n- status reports and maintenances\n- dashboard form\n- status report emails\n- ...\n\nThis allowed us to step by step test and merge changes to production without having to pause the migration.\n\n## API Compatibility\n\nFor existing integrations, the **v1 API maintains backward compatibility**. Status page responses continue to include the legacy `monitorIds` and `monitors` fields to prevent breaking changes. However, these fields now only return page components of type `monitor` - the `static` components are excluded from these legacy fields.\n\n**Looking ahead:** Future API versions will use the `pageComponents` structure as the primary interface. Thibault is already cooking our v2 API version including an SDK to improve the DX around openstatus!"
  },
  {
    "path": "apps/web/src/content/pages/blog/status-pages-is-politics.mdx",
    "content": "---\ntitle: \"Status Pages Are Politics\"\ndescription: \"An always-green status page isn't a sign of reliability — it's a sign of opacity. Why the industry rewards dishonesty and what buyers should do about it.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2026-03-12\"\nimage: \"/assets/posts/status-page-is-politics/status-page-politics.png\"\ncategory: \"education\"\nfaq:\n  - question: \"Why is an always-green status page a red flag?\"\n    answer: \"Every system has incidents. A status page that has been green for months likely means the vendor isn't reporting incidents, not that they aren't having them. Without standardized reporting, a clean status page is indistinguishable from an opaque one.\"\n  - question: \"How should buyers evaluate a vendor's status page?\"\n    answer: \"Instead of comparing which page looks cleaner, ask vendors to show their incident history for the past 6 months. If they claim zero incidents, press harder. Treat transparency as a trust signal, not a liability.\"\n  - question: \"Why do transparent companies lose deals to opaque competitors?\"\n    answer: \"When there's no standard for what gets reported, buyers rationally pick the vendor with fewer visible incidents. The signal is inverted: transparency looks like unreliability, and opacity looks like stability. This rewards the wrong behavior.\"\n  - question: \"Why do large companies use internal status pages?\"\n    answer: \"Big organizations know public status pages are political documents. They run private, internal status pages so that teams across silos can coordinate on the actual operational truth — not the sanitized version crafted for public consumption.\"\n---\n\nA founder posted on [r/SaaS](https://www.reddit.com/r/SaaS/comments/1rgbhtt/we_lost_a_deal_because_of_our_status_page_history/) about losing a deal because their status page was too honest. Three minor incidents over two months. Quick resolutions, full [post-mortems](/guides/public-postmortem-underrated-marketing), [99.9% uptime](/guides/why-uptime-percentage-is-misleading). Didn't matter. The competitor's page showed zero incidents — not because they were more reliable, but because they never reported anything. The deal went to the green page.\n\nThis isn't a one-off sales story. It's the entire industry's dirty secret.\n\n## An Always-Green Status Page Is a Red Flag\n\nLet's be blunt: if a vendor's status page has been green for 18 months straight, they're not running a reliable service. They're running a PR campaign. Every system has incidents. The only question is whether a company reports them or buries them.\n\nThere's no shared definition of what counts as an \"incident.\" No auditing mechanism. No reporting standard. One vendor calls a 30-minute partial outage an incident, another calls it a blip and never reports it. When \"incident\" means whatever each company wants it to mean, a clean status page is unfalsifiable — you can't tell the difference between a reliable vendor and an opaque one.\n\n## The Game Is Rigged Against Honesty\n\nHere's what makes this infuriating: the buyer in that Reddit story wasn't stupid. They were being rational. When one page shows incidents and another doesn't, and there's no standard for what gets reported, the only safe bet is to pick the clean page. The signal is completely inverted — transparency looks like unreliability, and opacity looks like stability.\n\nEvery choice on a status page is political. What counts as an incident. When to open one. Whether \"degraded performance\" means 10% of users are affected or the whole system is down. How long \"investigating\" can run before becoming \"identified.\" These aren't technical decisions. They're PR decisions, made under pressure from legal, sales, and whoever has the most anxiety about appearances.\n\nIn big corporations, status page updates don't even come from the engineers handling the incident. They go through a review process — legal checks the wording, comms rewrites it, a manager signs off. By the time something gets published, it's been sanitized through three layers of organizational anxiety. The update you're reading isn't what happened. It's what the company decided was safe to say.\n\n## Big Orgs Already Know This\n\nLarge organizations figured this out a long time ago. That's why they don't rely on public status pages internally. When you have dozens of teams across silos — infrastructure, platform, product, SRE — you can't coordinate incident response on a page designed for external PR. You need the actual truth: real-time metrics, auto-detected incidents, granular component status, no political filtering.\n\nThat's exactly why [internal status pages](/guides/public-vs-private-status-pages) exist. The public page is for customers — measured, reviewed, political. The private page is for the people who actually need to fix things. The fact that big companies run two separate systems tells you everything about how much trust they place in public status pages: none.\n\n## Stop Rewarding the Liars\n\nThe founder's workaround — proactively framing transparency in sales conversations — is smart. But it's a band-aid on a broken system.\n\nWhat would actually fix this:\n\n- **Buyers should treat an empty status page as suspicious**, not reassuring. Ask vendors: \"Show me your incident history for the last 6 months.\" If the answer is \"we had none,\" press harder.\n- **Standardized [incident severity definitions](/guides/incident-severity-matrix)** so that \"incident\" means the same thing across vendors. Right now it's completely arbitrary.\n- **Third-party uptime verification** that buyers can actually trust, backed by real [SLIs, not just SLA promises](/guides/sla-vs-slo-vs-sli) — instead of self-reported theater.\n\nUntil then, every vendor who publishes honest incident reports is subsidizing competitors who don't. That's the real cost of transparency — and it's paid by the people doing the right thing.\n\n---\n\nThe founder who lost that deal made the harder choice. They could have scrubbed their page, played the game, and closed the deal. They didn't. That takes conviction. The least the rest of us can do is stop pretending that a green status page means anything at all.\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/telegram-group-qr-integration.mdx",
    "content": "---\ntitle: \"Connecting Telegram Groups Without a Webhook\"\ndescription: \"How we implemented a two-phase QR code flow to securely link Telegram groups to OpenStatus using Redis, polling, and stale-update filtering — no webhooks needed.\"\nauthor: \"Moulik Aggarwal\"\npublishedAt: \"2026-02-21\"\nimage: \"/assets/posts/telegram-group-qr-integration/telegram.png\"\ncategory: \"engineering\"\n---\n\nMost Telegram bot integrations follow the same playbook: spin up a webhook endpoint, expose it to the internet, and handle updates as they arrive. It's well-documented, straightforward, and used by just about every platform that offers a \"Connect your Telegram\" button.\n\nWe didn't do that.\n\nThis post explains the two-phase QR code flow we built to connect Telegram groups to OpenStatus — and why the approach we took is more reliable, simpler to deploy, and doesn't require us to maintain a live webhook endpoint at all.\n\n---\n\n## The Problem with Webhooks\n\nWebhooks are powerful, but they come with operational overhead that can exceed the complexity of a one-time setup flow:\n\n- **Infrastructure requirements**: You need a publicly accessible HTTPS endpoint that stays online 24/7\n- **Request handling**: You must implement retry logic, ensure request ordering, and handle deduplication\n- **Security validation**: Every incoming request needs verification against a Telegram secret token\n- **State consistency**: Mismatches between the webhook receiver and your database become a real problem at scale\n\nFor our use case — a one-time group connection during initial setup — this felt disproportionate. We weren't building a real-time chat pipeline. We were building a configuration UI.\n\nThe alternative? Telegram's `getUpdates` long-polling API. It's the simpler sibling of webhooks, designed for bots that pull updates on demand rather than receive them pushed. We already had the infrastructure: a working API, Redis, and a frontend capable of polling. So we leaned in.\n\n---\n\n## The Two-Phase Flow\n\nThe core challenge with Telegram group notifications is that **you can't directly obtain a group chat ID from a user**. Unlike a \"Log in with Telegram\" flow, groups don't have a built-in sign-in mechanism. The user can't just authenticate and grant group access.\n\nThe solution is a two-step handshake:\n\n**Phase 1 — Identity Verification (Private Chat)**\n\nThe user scans a QR code that opens a direct message with the bot, pre-filled with `/start <token>`. When they send it, the bot receives a private message containing the token. We verify the token matches what we stored in Redis for that workspace, then return the user's private `chatId` to the frontend.\n\n**Phase 2 — Group Detection**\n\nNow that we know *who* initiated the flow (via their private `chatId`), we wait for that same user to add our bot to a group. The bot receives a `new_chat_members` update. We verify:\n- The bot was actually added to the group\n- The bot was added *by the same user from Phase 1*\n- The event happened *after* the session started\n\nOnce both phases complete, we have the group's `chatId` — which is what gets stored and used for alert delivery.\n\n---\n\n## Token Management: One Key per Workspace\n\nDuring Phase 1, we need to issue a short-lived, single-use token that links a QR scan back to a specific OpenStatus workspace.\n\nOur strategy is simple: **one Redis key per workspace**.\n\n```\ntelegram:workspace_token:${workspaceId}  →  \"a3kXp9bN1qRt\"  (30 min TTL)\n```\n\nThe token is a 12-character random ID generated with `nanoid(12)`. This is short enough to fit comfortably in a Telegram deep-link URL parameter (Telegram limits `start` parameters to 64 characters, giving us plenty of headroom), yet random enough to be effectively unguessable.\n\nKey properties of this design:\n\n- **One active token per workspace** — generating a new QR code automatically invalidates the previous one. There's no accumulation of stale tokens floating around.\n- **30-minute hard expiry** — set at the Redis level using TTL, so cleanup is free and automatic.\n- **Single-use deletion** — once Phase 1 succeeds, we immediately delete the key. Even if a user somehow replays the same QR code, it won't match anymore.\n\n```ts\n// Phase 1 server-side verification\nconst tokenKey = `telegram:workspace_token:${workspaceId}`;\nconst storedToken = await redis.get<string>(tokenKey);\n\nif (storedToken && receivedToken === storedToken) {\n  await redis.del(tokenKey); // one-time use\n  return { chatId: String(message.chat.id), user: message.from };\n}\n```\n\nThis approach scales horizontally: adding new workspaces requires no coordination between API instances, since each one can independently manage its own Redis keys.\n\n---\n\n## Stale Update Prevention: Timestamp Filtering\n\nHere's the subtle problem with `getUpdates`: **Telegram's API returns updates from the last 24 hours by default**. If a user previously added the bot to a group (even accidentally), that event would still be in the update feed.\n\nWithout filtering, Phase 2 would instantly succeed with stale data — connecting the user to the wrong group.\n\nOur fix: **session start time**.\n\nWhen the user clicks \"Connect with QR\", the frontend records a Unix timestamp (`sessionStartTime = Math.floor(Date.now() / 1000)`). This value is passed as `since` with every polling request. The backend skips any update older than this threshold.\n\n```ts\nconst recentUpdates = since\n  ? updates.filter((u) => u.message && u.message.date >= since)\n  : updates;\n```\n\nThis also cleanly handles the **reset flow**. If a user accidentally adds the bot to the wrong group, they can click \"Reset Group ID\". We clear the stored `chatId`, generate a new `sessionStartTime`, and resume polling. The previous group-add event is now older than the new session start — so it gets filtered out automatically, with no server-side cleanup needed.\n\nThe beauty of this approach: **the client drives the filtering logic**, and the server is just a passive validator. No state needs to be stored on the backend beyond the 30-minute token.\n\n---\n\n## Ownership Verification in Phase 2\n\nDetecting that the bot was added to a group is straightforward. But we need to make sure *the right user* did it. Otherwise, any user who scans the QR could add the bot to an arbitrary group that doesn't belong to them.\n\nThe verification chain in Phase 2:\n\n1. The update must be a `group` or `supergroup` event (not a private chat)\n2. The `new_chat_members` array must include our bot's username\n3. The `message.from.id` must match the `privateChatId` returned from Phase 1\n4. The `message.date` must be `>= since` (the session start time)\n\n\n```ts\nfunction extractGroupBotAddition(update, privateChatId, botUsername) {\n  const { message } = update;\n  if (!message || ![\"group\", \"supergroup\"].includes(message.chat.type)) return null;\n  if (String(message.from.id) !== privateChatId) return null;\n\n  // Telegram uses all three field names inconsistently across versions\n  const isBotAdded =\n    message.new_chat_participant?.username === botUsername ||\n    message.new_chat_member?.username === botUsername ||\n    message.new_chat_members?.some((m) => m.username === botUsername);\n\n  if (!isBotAdded) return null;\n\n  return { chatId: String(message.chat.id), chatTitle: message.chat.title };\n}\n```\n\nAll three Telegram field variants (`new_chat_participant`, `new_chat_member`, `new_chat_members`) are checked because the field name varies across bot API versions and group types. It's defensive, but necessary — we've learned that Telegram's API isn't always consistent across versions and group configurations.\n\n---\n\n## Frontend State: A Reducer-Driven Flow\n\nManaging the two-phase flow on the client required careful state handling. We used `useReducer` to model the flow explicitly, treating the entire QR connection process as a state machine.\n\nThe state machine looks like this:\n\n```\nflowStep: \"private\"  →  (Phase 1 success)  →  flowStep: \"group\"  →  (Phase 2 success)\n                                               ↓\n                                         (Phase 2 success)\n                                               ↓\n                                         chatId written to form\n```\n\nThe reducer handles five actions:\n\n| Action | Effect |\n|--------|--------|\n| `SET_SESSION_START_TIME` | Captures Unix timestamp when QR mode is entered |\n| `SET_PRIVATE_CONNECTION_DATA` | Stores `privateChatId`, advances flow to `\"group\"` step |\n| `SET_GROUP_CONNECTION_DATA` | Stores `groupTitle`, triggers form update with final `chatId` |\n| `RESET_GROUP_CONNECTION` | Clears group data, generates new `sessionStartTime`, keeps `privateChatId` |\n| `RESET_STATE` | Full reset on unmount or user discard |\n\nThe polling query is driven by this state:\n\n```ts\nconst { data: updates } = useQuery({\n  ...trpc.notification.getTelegramUpdates.queryOptions({\n    privateChatId: state.flowStep === \"group\" ? state.privateChatId : undefined,\n    since: state.sessionStartTime ?? undefined,\n  }),\n  enabled: !!tokenData?.token && !form.watch(\"data.chatId\") && mode === \"qr\",\n  refetchInterval: 5000,\n});\n```\n\nPolling automatically stops once `chatId` is set in the form — there's no manual cleanup needed. The `enabled` flag handles it reactively. This means the frontend naturally stops polling once the connection succeeds, freeing up resources without explicit teardown logic.\n\n---\n\n## Why This Architecture Scales\n\nThe thing that makes this design hold up at scale is that **the server stays stateless between polls**.\n\n- No WebSocket connections to manage or monitor for leaks\n- No long-lived processes that could accumulate state\n- No event listeners that need memory cleanup\n- No webhook endpoint that requires uptime SLAs\n\nEach poll is a short-lived tRPC call that:\n1. Fetches updates from Telegram\n2. Runs a few comparisons against a single Redis key\n3. Returns the result\n\nRedis handles token TTL and single-use deletion atomically. The frontend manages all UX state locally via the reducer. There's no coordination needed between API instances.\n\nAdding a new workspace doesn't change anything server-side. The Redis key pattern `telegram:workspace_token:${workspaceId}` scales horizontally with zero coordination between instances. If you double your API servers, the system just works.\n\n---\n\n## The User Experience\n\nFrom a user's perspective, the flow is straightforward:\n\n1. Open the \"Connect Telegram\" panel in OpenStatus\n2. Scan the QR code with your phone\n3. Telegram opens a DM with the bot — you hit send on the pre-filled `/start` message\n4. The dashboard immediately detects the connection and shows your Telegram username\n5. You add the bot to your Telegram group\n6. The dashboard detects the group and auto-fills the notification channel\n7. Alerts now flow directly to that group\n\nNo webhooks. No manual token copying. No server-side secrets exposed in URLs. No stale data from leftover `getUpdates` history. The setup takes about 10 seconds.\n\n---\n\n## Key Takeaways\n\nThis pattern works well when:\n\n- **Setup is one-time**: You're not streaming real-time events; you're wiring up a configuration\n- **Latency tolerance exists**: A 5-second poll interval is acceptable (vs. instant webhook delivery)\n- **Operational simplicity matters**: You want to avoid running a public endpoint with its attendant security and availability requirements\n\nIf you need true real-time bidirectional communication with Telegram (a chat application, for instance), webhooks are the right choice. But for one-shot integrations like \"connect your group for alerts,\" this polling approach is simpler, more reliable, and far easier to deploy.\n\n---\n\n_Set up Telegram alerting on [Openstatus](/app/login) and get notified the moment a monitor fails._"
  },
  {
    "path": "apps/web/src/content/pages/blog/the-first-48-hours.mdx",
    "content": "---\ntitle: \"48 hours of public OpenStatus\"\nimage: \"/assets/posts/the-first-48-hours/48.png\"\ndescription: \"The numbers, limits we faced and consequences we have taken.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2023-08-02\"\ncategory: \"company\"\n---\n\n## 48 hours of Rollercoaster 🎢\n\nThe past two days following the launch have been incredibly hectic. The level of\ninterest we received exceeded our expectations. We were even trending on GitHub.\nThe number of users and created monitors far surpassed what we anticipated. We\nsincerely appreciate your support, as it further motivates us. The raw\nstatistics are astonishing:\n\n- Monitors: 315\n- Users: 418\n- Status pages: 174\n\nWith a large user base comes great responsibility. Thanks to the high volume of\nusers, we discovered several flaws in our system. We promptly addressed the\nissues and took action.\n\n## Limits we encountered 😱\n\nWe conducted a DDOS attack on Vercel's firewalls from Upstash servers because we\nwhere pining every single vercel region for every monitor (18 \\* 315 = 5.670)\nmore or less at once. Before considering the removal of\n[QStash](https://upstash.com/qstash?ref=openstatus), we implemented a hotfix to\nintroduce random delays of 0 to 180 seconds between each ping. This prevented us\nfrom overwhelming Vercel with simultaneous requests, significantly reducing the\nnumber of retries on QStash. As a result of these measures, we now have a\nrandomized check interval of 0 to 90 seconds.\n\nFor the time being, we will keep it to ensure that every check is successfully\nprocessed.\n\n<Image\n  alt=\"Qstash Usage\"\n  src=\"/assets/posts/the-first-48-hours/qstash-usage.png\"\n  width={650}\n  height={168}\n/>\n\nMoreover, our actions caused the `/monitor/[id]/data` data-table to break. We\nwere storing the metadata `res.text()` in Tinybird and retrieving it with the\nquery `SELECT * from monitors WHERE ....` This means that if the text is in HTML\nformat, we store the HTML content of the page, which is acceptable. However,\nthis led to quickly exceeding the 100MB limit for the result length when\naccessing monitor data. Now, we are only querying the necessary data from\ntinybird: `SELECT latency, timestamp, url, ... from monitors WHERE ...`. Despite\nthis, we continue to ingest the data and will only request it in the future when\nyou click on the \"View metadata\" action.\n\nAnd of course no need to mention all the little hotfixed we had to make here and\nthere.\n\n### What would it cost us? 💸\n\nLet's break the current numbers a bit more down if we would have kept pinging\nall the regions:\n\n- Number of checks every 10min: 315 \\* 18 regions = 5.670\n- Number of checks every 1h: 5.670 \\* 6 = 34.020\n- Number of checks every day: 34.020 \\* 24 = 816.480\n\n<Image\n  alt=\"Vercel Usage\"\n  src=\"/assets/posts/the-first-48-hours/vercel-usage.png\"\n  width={650}\n  height={263}\n/>\n\n> We have resolved an issue with regex invocation in the edge middleware.\n> Previously, it was being called whenever an API endpoint was called.\n\nUnder Vercel's Pro plan, we have 1 million edge function executions included,\nand for each additional 1 million, there is a\n$2 charge. Because the numbers are constant, we would have payed\n1$/100k Qstash\nmessages which would lead us to a approx. **10$/day** for the current free\ntiers.\n\n### Actions taken 👷\n\nBut due to the high cost of our current infrastructure, **we have downgraded all\nmonitors to a single region** (`/regions/auto`, a which will take a random\nvercel region) in order to stay within a reasonable budget, by dividing it\nby 18. You can edit the region for each monitor. We apologize for any\ninconvenience caused and will update you on the new conditions once everything\nis normalized.\n\nA big appreciation to [@chronark\\_](https://twitter.com/chronark_) who joined us\non a call to discuss potential solutions and for sharing his knowledge about his\nown service, [planetfall.io](https://planetfall.io).\n\nAlso, Guilherme had a very good comment:\n\n<Tweet id=\"1686482013685940224\" />\n\nAs a result of this incident, we will be reevaluating the prices and plans\ndisplayed on our homepage.\n\nJoin [Discord](https://openstatus.dev/discord) if you want to learn more or just\nhave a chat!\n\nAgain, thank you for all your support and your understanding. 🙏\n\n---\n\nP.S. We are generating your workspace-slug by merging two random names. Create\nan account on [openstatus](https://openstatus.dev) and share yours on\n[Discord](https://openstatus.dev/discord) - it's fun to watch! 🍿\n\n<Image\n  alt=\"wet-grandmother slug\"\n  src=\"/assets/posts/the-first-48-hours/wet-grandmother-workspace-slug.png\"\n  width={400}\n  height={100}\n/>\n\n<Image\n  alt=\"enough-gas slug\"\n  src=\"/assets/posts/the-first-48-hours/enough-gas-workspace-slug.png\"\n  width={400}\n  height={100}\n/>\n"
  },
  {
    "path": "apps/web/src/content/pages/blog/vision-2025.mdx",
    "content": "---\ntitle: \"Our plans in 2025\"\ndescription: \"Embrace OpenTelemetry, improve developer experience, and support community editions.\"\nauthor: \"Maximilian Kaske\"\npublishedAt: \"2024-12-31\"\nimage: \"/assets/posts/vision-2025/vision-2025.png\"\ncategory: \"company\"\n---\n\nThis post has two purposes: (a) *sharing our vision with you*  and (b) *holding ourselves accountable*.\n\nWe know that startup cycles are unpredictable and move fast. This post will serve as a checkpoint—allowing us to reflect on what we’ve achieved and where we may have lost sight of our initial plan.\n\nAfter all the work, we’ve finally distilled our focus into three main pillars:\n\n1. **Synthetic Monitoring**\n2. **Status Pages**\n3. **Alerting**\n\nAdditionally, **Community Editions** are something we’d like to invest in, enabling faster access to self-hosted solutions.\n\nLet’s explore these pillars and uncover our key drivers for 2025.\n\n---\n\n### 1. Synthetic Monitoring\n\nThis is where our heart lies.  \n\nWe are committed to providing the best tools to monitor your APIs, websites and server globally. So far, we’ve built a highly performant checker that you can deploy as your own probe. \n\nMoving forward, we want to embrace standards **OpenTelemetry.** \n\nWe believe the current synthetic monitoring ecosystem  is broken you can not build a observability tool isolated from the rest of the observability stack \n\nWhat does this mean? By leveraging OpenTelemetry, we can export data from our probes via metrics to your observability stacks.\n\nThis flexibility allows you to choose how you handle the data—leaving alerts, visualizations, and storage to your stack while enabling custom integrations. \n\nBesides we also think at a new kind of monitor which is neither TCP or HTTP based but based on metrics from your system.  You define a PromQL query and configure custom assertions,  if an assertion fails, you can inform your users via a *headless* status page and receive alerts instantly.\n\nBut there’s more. As developers, we live in our IDEs and terminals. That’s why we really want improve our **openstatus CLI** and `config.openstatus.yaml` to make developer experience neat (DX). Allowing you to run your synthetic check in you CI workflow, or from your terminal.\n\nThinking of DX, if interest exists, we’d start building SDKs for your favorite language.\n\nSome of you also love beautiful craft UI this is why  we want to make the dashboard feel *alive*  Dashboard—showing real-time pings as they await responses.\n\nLooking ahead, we want to expand our probes to more cloud providers like AWS, GCP, Koyeb, Render or more if requested.\n\n---\n\n### 2. Status Pages\n\nStatus pages go hand in hand with synthetic monitoring. \n\nWe aim to make it effortless to communicate incidents to your users while ensuring the page feels uniquely *yours*. **Customization is key**: supporting brand colors, layouts, and enabling custom `script` injections (e.g., for tools like Plausible to learn who your users are and where they’re coming from).\n\nAs we work on our otel monitor integrating non http/tcp monitor will be possible, allowing you to show any part of your stack without the need to define a health endpoint.\n\n---\n\n### 3. Alerting\n\nLet’s clarify what *our* Alerting is. It’s **not on-call rotation**—instead, it’s about sending notifications to your preferred channels when something unexpected happens. Whether it’s failures, degraded performance, or endpoint recoveries, you’ll always be informed and ready to act.\n\nSo far, our alerting configurations have been basic. We plan to make them more customizable by **reworking our Alert Engine**. Features we’re considering include:\n\n- Retry policies\n- Delays (e.g., “wait X minutes before sending a notification”)\n- Recovery policies (e.g. “wait for X successful pings”)\n\nNaturally, we’ll integrate with more notification channels and third-party tools for on-call rotations and incident management. While we’ll provide the basics, our focus will remain on flexibility and core alerting—not full-blown incident management. We’ll leave that part to others.\n\n---\n\n### Community Editions\n\nLast but not least, we know many of you want easy-to-host solutions to monitor your services. However, hosting our full application currently involves third-party dependencies and some setup complexity. Instead, we’ll focus on:\n\n(a) *Supporting the community* as they build on top of OpenStatus core infra.\n\n(b) *Building smaller, easily self-hostable solutions* (e.g., [vercel-edge-ping](https://github.com/openstatusHQ/vercel-edge-ping)).\n\nYou should think of our community editions as Uptime Kuma on steroids with powerful opentelemetry support based our on work for our main product.\n\nWe aren't making the full app difficult to host on purpose. Instead, we're focusing on keeping things simple while meeting community needs. As enterprises request specific features like dedicated databases and queues, we'll evolve toward a more modular architecture.\n\n---\n\n### Recap\n\nHere are the key focus areas for each pillar in 2025:\n\n- **Synthetic Monitoring**: OTel adoption and developer experience (CLI, SDK, real-time UX).\n- **Status Pages**: Customization (styles, scripts) and third-party integrations.\n- **Alerting**: Advanced configurations and more integrations.\n- **Community Editions**: Smaller, self-hostable projects.\n\nIf there’s something crucial you’re looking for, we’d love to hear from you. Join our [Discord](https://openstatus.dev/discord) or drop us a [message](mailto:ping@openstatus.dev)!\n\nThanks for supporting us on this journey. ❤️\n\n**Happy New Year,**\n\nThibault and Max"
  },
  {
    "path": "apps/web/src/content/pages/changelog/auto-resolved-incidents.mdx",
    "content": "---\ntitle: \"Auto Resolved Incidents\"\ndescription: \"Automatically resolve incidents when your endpoint is back.\"\nimage: \"/assets/changelog/auto-resolved-incidents.png\"\npublishedAt: \"2024-02-19\"\nauthor: \"openstatus\"\ncategory: \"incidents\"\n---\n\nWe have added auto-resolved incidents to the incident management feature. Now,\nonce your endpoint is back up and running i.e. _more than 50% of the regions are\nnot failing_, the incident will be automatically resolved. The time between the\ncreation and resolution of the incident will be marked as downtime on the status\npage.\n\nAdditionally, the calculated `p95` latency over a day has been removed. It is\ncostly to calculate and was not providing much value.\n\nInstead, we are thinking of adding a chart that shows the latency of the last X\ndays/hours. If you have ideas, [let us know](/discord)!\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/binary-payload.mdx",
    "content": "---\ntitle: \"Binary payload\"\ndescription: \"Send binary payloads with your HTTP synthetic check requests.\"\nimage: \"/assets/changelog/content-type.png\"\npublishedAt: \"2024-09-09\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nYou can now include binary payloads in your HTTP synthetic check requests, which is handy for sending files or other binary data in the request body.\n\nAdditionally, you have the flexibility to set the Content-Type to your preference, such as `application/octet-stream` or `application/edn`.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/check-run-api.mdx",
    "content": "---\ntitle: \"Run Check API\"\ndescription: \"Launch a check with our API to test your endpoint's latency.\"\nimage: \"/assets/changelog/check-api.png\"\npublishedAt: \"2024-06-18\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nWe have added a new API to launch a check with OpenStatus. You can now test your endpoint's latency programmatically.\n\nIt's useful if you want to integrate OpenStatus with your CI/CD pipeline.\n\nYou can read the [API documentation](https://api.openstatus.dev/v1#tag/monitor/POST/monitor/:id/trigger).\n\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/checker-playground.mdx",
    "content": "---\ntitle: \"Checker Playground\"\ndescription: \"Experience the performance of your application from different regions.\"\nimage: \"/assets/changelog/checker-playground.png\"\npublishedAt: \"2024-01-20\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nYou can now test your API or website from multiple regions around the world and\nexperience the performance of your application from the different continents.\n\nLearn more about `dns`, `tls`, `ttfb` and more metrics that are collected by the\nchecker.\n\nGo to the **[Playground](/play/checker)** to test your endpoint.\n\nBut there is more: use the `https://checker.openstatus.dev/ping` API endpoint to\nget the metrics by yourself.\n\nFor example:\n\n```bash\ncurl --request POST \\\n  --url https://checker.openstatus.dev/ping/ams \\\n  --header 'Content-Type: application/json' \\\n  --header 'x-openstatus-key: <YOUR_API_KEY>' \\\n  --header 'prefer-fly-region: ams' \\\n  --data '{\n  \"url\": \"https://api.openstatus.dev/ping\",\n  \"method\": \"GET\"\n}'\n```\n\nRead more about the\n[Checker API](https://api.openstatus.dev/v1#tag/monitor/POST/monitor/:id/run) in our docs.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/cli-import-apply.mdx",
    "content": "---\ntitle: \"CLI: Import and manage monitors\"\ndescription: \"Import your openstatus monitors, modify them and apply change with the CLI\"\nimage: \"/assets/changelog/cli-apply.png\"\npublishedAt: \"2025-08-02\"\nauthor: \"openstatus\"\ncategory: \"cli\"\n---\n\nWe have released a new version of our CLI.\n\nIt comes with 2 new commands:\n\n- `openstatus monitors import`: It will import your monitors from your openstatus workspace to a YAML file\n- `openstatus monitors apply`: Create, update and delete your monitors via your `openstatus.yaml` file.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/cli-improvement.mdx",
    "content": "---\ntitle: \"New CLI commands\"\ndescription: \"We have updated our CLI to provide more features and improve the user experience.\"\nimage: \"/assets/changelog/cli-update.png\"\npublishedAt: \"2025-06-16\"\nauthor: \"openstatus\"\ncategory: \"cli\"\n---\n\nUpgrade your CLI to the latest version for enhanced functionality and a better user experience.\n\n## New Features\n\n- Create monitors: Create your monitors defined in the configuration file.\n- Export monitors: Export all your workspace monitors to a YAML file.\n\n\nCheck out our [CLI documentation](https://docs.openstatus.dev/reference/cli-reference/) for more information.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/cli-monitor-template.mdx",
    "content": "---\ntitle: \"CLI Monitor configuration template\"\ndescription: \"We have published a new repository with CLI Monitor configuration template.\"\nimage: \"/assets/changelog/cli-template.png\"\npublishedAt: \"2025-06-30\"\nauthor: \"openstatus\"\ncategory: \"cli\"\n---\n\nIn our [previous changelog](/changelog/cli-improvement), we have introduced a new command to create monitors defined in a YAML configuration file.\n\nWe have now published a new repository with monitor configuration template:\n\n[https://github.com/openstatusHQ/cli-template](https://github.com/openstatusHQ/cli-template)\n\nMaking it easy to manage your openstatus monitors via GitOps.\n\nWe are going to add more examples over time to the repository.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/clone-monitor.mdx",
    "content": "---\ntitle: \"Clone Monitor\"\ndescription: \"No more work. Just a click.\"\nimage: \"/assets/changelog/clone-monitor.png\"\npublishedAt: \"2024-07-14\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\nSometimes you need to create a copy of your monitor with the same settings quickly and easily.\n\nYou can now clone a monitor directly from the action menu in the monitor table.\n\nWe have added this feature to the monitor settings.\n\nShout-out to [@Ipriyankrajai](https://x.com/Ipriyankrajai) for his contribution!\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/curl-builder-playground.mdx",
    "content": "---\ntitle: \"cURL Builder\"\ndescription: \"An online curl command line builder. Generate curl commands to test your API endpoints.\"\nimage: \"/assets/changelog/curl-builder-playground.png\"\npublishedAt: \"2024-11-14\"\nauthor: \"openstatus\"\ncategory: \"tools\"\n---\n\nAn online curl command line builder. Generate curl commands to test your API endpoints.\n\nGo to the **[Playground](/play/curl)** and test it."
  },
  {
    "path": "apps/web/src/content/pages/changelog/dark-theme-support.mdx",
    "content": "---\ntitle: \"Dark mode support\"\ndescription: \"Choose your favorite theme. Dark or light.\"\nimage: \"/assets/changelog/dark-theme-support.png\"\npublishedAt: \"2023-11-27\"\nauthor: \"openstatus\"\ncategory: \"company\"\n---\n\nYou can now use dark mode on the website, after a long awaited pull request\n[#73](https://github.com/openstatusHQ/openstatus/pull/73) has been merged.\n\nThe **default** theme will still be **light** mode. You can change the theme in\nthe footer of the website.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/dashboard-metrics-card.mdx",
    "content": "---\ntitle: \"Dashboard Metrics Card\"\ndescription: \"Get more insights into your services.\"\nimage: \"/assets/changelog/dashboard-metrics-card.png\"\npublishedAt: \"2024-02-09\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nYou can now see more insights into your services with the new metrics card on\nthe monitor's overview.\n\nChoose a period and see the: **uptime** percentage, number of **errors**, number\nof **total requests**, **average**, **p99**, **p95**, **p90**, and **p75**\nresponse times - and compare the metrics with the previous period.\n\nYou'll also notice that you can now filter the charts by **region** and combine\nor split the regions separately for simpler comparison.\n\nAdditionally, we've updated the monitor settings section to improve the UI/UX.\n\nSo go to your [dashboard](/app) to check it out!\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/dns-monitoring.mdx",
    "content": "---\ntitle: \"DNS Monitoring\"\ndescription: \"Monitor your DNS records with openstatus.\"\nimage: \"/assets/changelog/dns-monitoring.png\"\npublishedAt: \"2025-11-24\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nWe're excited to announce that DNS monitoring is now available in openstatus! This new addition expands our monitoring capabilities, allowing you to keep track of all your DNS records.\n\n\n####  Get Started \nTo start monitoring your DNS records, simply add a new monitor and select \"DNS\" as your monitor type.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/docker-checker.mdx",
    "content": "---\ntitle: \"Docker Image Checker\"\ndescription: \"Our checker has been published to a container registry.\"\nimage: \"/assets/changelog/docker-checker.png\"\npublishedAt: \"2024-07-24\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nYou can deploy our checker within your infrastructure, any cloud providers or coolify by pulling the image from the container registry.\n\nThe image is available at `ghcr.io/openstatushq/checker:latest`.\n\nIt opens up a lot of possibilities to run the checker in your own environment, on your own infrastructure, or on any cloud provider. We will be able to add private locations soon.\n\nWe have wrote a [guide](https://docs.openstatus.dev/guides/how-to-deploy-probes-cloudflare-containers) to help you deploy our checker on [Koyeb](https://www.koyeb.com/).\n\nThanks to [Depot](https://depot.dev/?utm_source=Opource=OpenStatus) for the fast build and the easy integration with GitHub Actions."
  },
  {
    "path": "apps/web/src/content/pages/changelog/follow-redirect.mdx",
    "content": "---\ntitle: \"Follow Redirect\"\ndescription: \"Choose if you want to follow redirects or not.\"\nimage: \"/assets/changelog/follow-redirect.png\"\npublishedAt: \"2025-09-28\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nYou can now choose if you want to follow redirects for your requests. By default, redirects are followed.\n\n\nThanks to [Anonymus2000](https://github.com/Nil2000) for the contribution.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/github-action.mdx",
    "content": "---\ntitle: \"OpenStatus GitHub Actions\"\ndescription: \"Run your OpenStatus synthetic checks as part of your GitHub Workflow.\"\nimage: \"/assets/changelog/github-action.png\"\npublishedAt: \"2025-02-18\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nWe have published our GitHub Actions, It lets your run your OpenStatus synthetic checks as part of your GitHub Workflow.\n\nThe action is available on [GitHub Marketplace](https://github.com/marketplace/actions/openstatus-synthetics-ci)\n\nHere's a simple example of how to use it:\n\n```yaml\n- name: Run OpenStatus Synthetics CI\n    uses: openstatushq/openstatus-github-action@v1\n    with:\n        api_key: ${{ secrets.OPENSTATUS_API_KEY }}\n```\n\nHere's a GitHub repo with the action in use: [openstatushq/openstatus-github-action-example](https://github.com/openstatusHQ/github-action-tester)"
  },
  {
    "path": "apps/web/src/content/pages/changelog/global-speed-checker-skills.mdx",
    "content": "---\ntitle: \"Global Speed Checker Skill\"\ndescription: \"Test your API performance from 28 global regions with your AI agents.\"\nimage: \"/assets/changelog/global-speed-checker-skills.png\"\npublishedAt: \"2026-02-01\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nEnable AI agents like Claude Code to check website performance from multiple global locations directly in your terminal.\n\nInstall it via npx skills:\n\n```bash\nnpx skills add openstatushq/skills\n```\n\nOr with the Claude Code plugin system:\n\n```bash\n/plugin marketplace add openstatushq/skills\n/plugin install openstatus-skills\n```\n\n**Repository:** [@openstatushq/skills](https://github.com/openstatusHQ/skills)"
  },
  {
    "path": "apps/web/src/content/pages/changelog/golang-monitor-checker.mdx",
    "content": "---\ntitle: \"Golang monitor checker\"\ndescription: \"Stability improvement for the endpoint health checker.\"\nimage: \"/assets/changelog/golang-monitor-checker.png\"\npublishedAt: \"2023-12-02\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nThe endpoint health checker has been improved to be more stable and reliable\nwith [Go](https://go.dev). In the past, we have been experiencing missing data\nendpoints and longer request times for some `regions`.\n\nIn addition to that, we have renamed the **Incidents** to **Status Reports** to\neasily distinguish between internal incidents and external reports.\n\n> The API endpoint has also moved from `/v1/incidents` to `/v1/status_reports`.\n> Read more in the [docs](https://docs.openstatus.dev/reference/incident).\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/google-chat-notifications.mdx",
    "content": "---\ntitle: \"Google Chat Notifications\"\ndescription: \"OpenStatus now supports Google Chat notifications. Receive uptime monitoring alerts directly in your Google Chat spaces.\"\nimage: \"/assets/changelog/google-chat.png\"\npublishedAt: \"2025-12-18\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\n\nYou can now receive your uptime notifications via Google Chat.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/grafana-oncall-integration.mdx",
    "content": "---\ntitle: \"Grafana OnCall Notifications\"\ndescription: \"OpenStatus now supports Grafana OnCall notifications. Integrate uptime alerts with your existing incident management workflows in Grafana OnCall.\"\nimage: \"/assets/changelog/grafana-oncall.png\"\npublishedAt: \"2026-01-31\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\n\nYou can now receive your uptime notifications via Grafana OnCall. This allows you to integrate OpenStatus with your existing incident management workflows in Grafana OnCall.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/grouped-monitors.mdx",
    "content": "---\ntitle: \"Grouped Monitors\"\ndescription: \"Organize monitors by region or service for a cleaner overview.\"\nimage: \"/assets/changelog/grouped-monitors.png\"\npublishedAt: \"2025-11-06\"\nauthor: \"openstatus\"\ncategory: \"statuspage\"\n---\n\nYou can now group monitors on your status page - for example, by region (EU, US, APAC) or service (API, Dashboard, Auth). This makes it easier for visitors to understand which parts of your system are affected."
  },
  {
    "path": "apps/web/src/content/pages/changelog/individual-status-report-page.mdx",
    "content": "---\ntitle: \"Invidivual Status Report Page\"\ndescription: \"Every created status report now has its own page.\"\npublishedAt: \"2023-12-22\"\nimage: \"/assets/changelog/individual-status-report-page.png\"\nauthor: \"openstatus\"\ncategory: \"statuspage\"\n---\n\nEvery created status report now has its own page. That will make it easier to\nshare the status report with your team and across your users.\n\nAdditionally, we've addded a neutral blue color to the tracker whenever a status\nreport has been published.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/json-status-page.mdx",
    "content": "---\ntitle: JSON Status Page \ndescription: Get your status page data as JSON.\nimage: /assets/changelog/json-status-page.png\npublishedAt: 2025-11-27\nauthor: \"openstatus\"\ncategory: \"statuspage\"\n---\nWe're excited to announce that you can now access your status page data in JSON format! This feature allows you to create exciting tool/integration using your status page data.\n\nThis features is available for all status pages. To access your status page data in JSON format, simply append `/feed/json` to your status page URL.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/latency-quantiles.mdx",
    "content": "---\ntitle: \"Latency quantiles\"\ndescription: \"Refine response time analysis with precision using quantiles.\"\nimage: \"/assets/changelog/latency-quantiles.png\"\npublishedAt: \"2024-01-04\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nYou can now refine your latency data more effectively with quantiles: `p99`,\n`p95`, `p90`, and `p75`. The average (`avg`) has been retained.\n\nThe `<Tracker />` data on your status pages now displays the `p95` quantile\ninstead of the average.\n\nFurthermore, the `/monitors` layout has been updated. The data table has its\ndedicated tab to prevent loading excessive data at once, along with the edit\nform. The pause button is also now more quickly accessible.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/maintenance-status.mdx",
    "content": "---\ntitle: \"Maintenance status\"\ndescription: \"Show your users that you're performing maintenance.\"\nimage: \"/assets/changelog/maintenance-status.png\"\npublishedAt: \"2024-06-09\"\nauthor: \"openstatus\"\ncategory: \"statuspage\"\n---\n\nYou can now set your status to _Under Maintenance_ to inform your users that you're performing maintenance. You'll be able to set the start and end times for the maintenance period. During this time, we will pause the connected monitors and suppress any notifications.\n\nTo create a maintenance, go to the corresponding status page and click the 'Maintenance' sub navigation. From there, you can create a new maintenance status, including `title` and `description`.\n\nThe users will be able to see all maintenances under a new tab on the status page."
  },
  {
    "path": "apps/web/src/content/pages/changelog/monitor-external-name.mdx",
    "content": "---\ntitle: \"Monitor External Name\"\ndescription: \"Differentiate between internal and external naming for status page\"\nimage: \"/assets/changelog/monitor-external-name.png\"\npublishedAt: \"2025-12-19\"\nauthor: \"openstatus\"\ncategory: \"statuspage\"\n---\n\nYou can now customize the external name for monitors, which will be displayed on status pages, in reports, and to email subscribers. This allows you to differentiate between internal monitor names and the names shown to your users."
  },
  {
    "path": "apps/web/src/content/pages/changelog/monitor-tags.mdx",
    "content": "---\ntitle: \"Monitor Tags\"\ndescription: \"Easily categorize your monitors with tags.\"\nimage: \"/assets/changelog/monitor-tags.png\"\npublishedAt: \"2024-03-20\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nYou can now create tags for your monitors to easily categorize them. Tags can be\nused to filter monitors on the dashboard and in the monitor list.\n\nCreate, update or delete a tag via your monitor settings.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/monitor-threshold.mdx",
    "content": "---\ntitle: \"Monitor Threshold\"\ndescription: \"Set custom request timeouts and degradation timing.\"\nimage: \"/assets/changelog/monitor-threshold.png\"\npublishedAt: \"2024-07-01\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\nSometimes you need to get notified if your monitor is taking too long to respond or if it's degrading.\n\nYou can set custom request timeouts and degradation timing for your monitors.\n\nWe have added this feature to the monitor settings.\n\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/more-regions.mdx",
    "content": "---\ntitle: \"More regions available 🌐\"\ndescription: \"You can now monitor your endpoint from 35 regions.\"\nimage: \"/assets/changelog/more-regions.png\"\npublishedAt: \"2024-06-19\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\nWe have added more regions to OpenStatus.\n\nYou can now monitor your endpoint from 35 regions.\n\nHere is the list of the all our supported regions:\n\n**Africa**\n- Johannesburg, South Africa  🇿🇦\n\n**Asia**\n- Hong Kong, Hong Kong 🇭🇰\n- Mumbai, India 🇮🇳\n- Singapore, Singapore 🇸🇬\n- Tokyo, Japan 🇯🇵\n\n**Europe**\n- Amsterdam, Netherlands 🇳🇱\n- Bucharest, Romania 🇷🇴\n- Frankfurt, Germany 🇩🇪\n- London, United Kingdom 🇬🇧\n- Madrid, Spain 🇪🇸\n- Paris, France 🇫🇷\n- Stockholm, Sweden 🇸🇪\n- Warsaw, Poland 🇵🇱\n\n\n**North America**\n- Ashburn, Virginia, USA 🇺🇸\n- Atlanta, Georgia, USA 🇺🇸\n- Boston, Massachusetts, USA 🇺🇸\n- Chicago, Illinois, USA 🇺🇸\n- Dallas, Texas, USA 🇺🇸\n- Denver, Colorado, USA 🇺🇸\n- Guadalajara, Mexico 🇲🇽\n- Los Angeles, California, USA 🇺🇸\n- Miami, Florida, USA 🇺🇸\n- Montreal, Canada 🇨🇦\n- Phoenix, Arizona, USA 🇺🇸\n- Queretaro, Mexico 🇲🇽\n- Seattle, Washington, USA 🇺🇸\n- San Jose, California, USA 🇺🇸\n- Toronto, Canada 🇨🇦\n\n**South America**\n- Bogota, Colombia 🇨🇴\n- Buenos Aires, Argentina 🇦🇷\n- Rio de Janeiro, Brazil 🇧🇷\n- Sao Paulo, Brazil 🇧🇷\n- Santiago, Chile 🇨🇱\n\n**Oceania**\n- Sydney, Australia 🇦🇺\n\nLet's make synthetic monitoring more global! 🌐\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/multi-cloud-fly-railway-koyeb.mdx",
    "content": "---\ntitle: \"Multi-Cloud is here\"\ndescription: \"Supporting Koyeb and Railway regions to extend Fly.io coverage\"\nimage: \"/assets/changelog/multi-cloud-fly-railway-koyeb.png\"\npublishedAt: \"2025-10-06\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nWe’re expanding our global coverage, supporting now multiple cloud providers!\nIn addition to Fly.io, you can now run checks from Koyeb and Railway regions, giving you more flexibility and reliability worldwide.\n\nYou can pick from the 6 Koyeb regions:\n\n- Frankfurt, Germany 🇩🇪\n- Paris, France 🇫🇷\n- San Franciso, USA 🇺🇸\n- Singapore, Singapore 🇸🇬\n- Tokyo, Japan 🇯🇵\n- Washington, USA 🇺🇸\n\nAnd the 4 Railway regions:\n\n- California, USA 🇺🇸\n- Virginia, USA 🇺🇸\n- Amsterdam, Netherlands 🇳🇱\n- Singapore, Singapore 🇸🇬\n\nLet's make synthetic monitoring multi-cloud! 🌐\n\nTry it out: [Global Speed Checker](https://openstatus.dev/play/checker)\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/multi-region-monitoring.mdx",
    "content": "---\ntitle: \"Multi regions monitoring by default\"\ndescription: \"Your monitors are now being called from around the world.\"\npublishedAt: \"2023-10-25\"\nimage: \"/assets/changelog/multi-region-monitoring.png\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nWe have migrated our backend to [fly.io](https://fly.io) and we have enabled\nmulti regions monitoring by default for every monitor at no extra costs.\n\nHere are the list of regions we are monitoring from:\n\n- `ams` - Amsterdam, Netherlands - 🇳🇱\n- `iad` - Ashburn, Virginia, USA - 🇺🇸\n- `jnb` - Johannesburg, South Africa - 🇿🇦\n- `hkg` - Hong Kong, Honkg Kong - 🇭🇰\n- `gru` - Sao Paulo, Brazil - 🇧🇷\n- `syd` - Sydney, Australia - 🇦🇺\n\nHappy monitoring!\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/ntfy-sh-integration.mdx",
    "content": "---\ntitle: \"ntfy.sh Integration\"\ndescription: \"Get notified via ntfy.sh if we detect an Incident.\"\nimage: \"/assets/changelog/ntfy.png\"\npublishedAt: \"2025-03-17\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\nYou can now receive notifications via ntfy.sh when we detect an incident.\n\n[ntfy.sh](https://ntfy.sh/) is an open-source servicer  that allows you to receive push notifications on your phone.\n\nWe love open source, so we've added this integration to our free plan.\n\nOpen source FTW 🚀"
  },
  {
    "path": "apps/web/src/content/pages/changelog/openstatus-cli.mdx",
    "content": "---\ntitle: \"OpenStatus CLI\"\ndescription: \"Manage triggers for your monitors from your terminal.\"\nimage: \"/assets/changelog/CLI.png\"\npublishedAt: \"2024-11-07\"\nauthor: \"openstatus\"\ncategory: \"cli\"\n---\n\nOur CLI is now available 🎉\n\n#### OpenStatus CLI ⌨\n\nThe OpenStatus CLI gives you the power to manage your monitoring setup directly from the command line. With this new tool, you can:\n\n- List all your monitors: View details on all your configured monitoring checks.\n- Triggers check on-demand: Run any of your configured checks immediately, without waiting for a scheduled execution.\n\n\n####  Get Started 🚀\n\nTo start using the CLI, simply install it using brew:\n\n```bash\nbrew tap openstatusHQ/cli\n```\n\n```bash\nbrew install openstatus\n```\n\nOnce installed, you can run `openstatus` to see all available commands.\n\n### GitHub Actions Integration Coming Soon 🔜\n\nOur CLI is the first step to have a better CI integration. Stay tuned for updates!\nWe will release our GitHub Action soon.\n\n\nWe're excited to continue improving the OpenStatus platform to make monitoring your applications and infrastructure even easier. As always, please let us know if you have any feedback or suggestions!"
  },
  {
    "path": "apps/web/src/content/pages/changelog/openstatus-sdk.mdx",
    "content": "---\ntitle: \"openstatus SDK\"\ndescription: \"Announcing the openstatus SDK for seamless integration and automation.\"\nimage: \"/assets/changelog/openstatus-sdk.png\"\npublishedAt: \"2026-02-04\"\nauthor: \"openstatus\"\ncategory: \"integrations\"\n---\n\nWe have just released our SDK. It allows you to interact with openstatus programmatically, making it easier to automate tasks and integrate with other systems.\n\nGet started with the SDK by installing it via npm:\n\n```bash\nnpm i @openstatus/sdk-node\n```\n\nor via jsr: \n\n```\ndeno add jsr:@openstatus/sdk-node\n```\n\nYou can find the documentation and examples on our [GitHub repository](https://github.com/openstatusHQ/sdk-node). If you have any questions or need assistance, feel free to reach out to us out.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/opentelemetry.mdx",
    "content": "---\ntitle: \"OpenTelemetry support\"\ndescription: \"Export your OpenStatus synthetics checks as OpenTelemetry metrics.\"\nimage: \"/assets/changelog/Otel.png\"\npublishedAt: \"2025-02-24\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nWe have added support for exporting your OpenStatus synthetic checks as OpenTelemetry metrics.\n\nCheck our [documentation](https://docs.openstatus.dev/guides/how-to-export-metrics-to-otlp-endpoint/) for more information on how to set it up.\n\nHere's an example of us exporting our synthetic checks to Grafana.\n\n[Grafana X OpenStatus](https://openstatus.grafana.net/public-dashboards/a85806536e9243f7ab9e0d458eb2ad32)\n\nYou just need an OTLP Endpoint to get started\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/pagerduty-integration.mdx",
    "content": "---\ntitle: \"PagerDuty Integration\"\ndescription: \"Get notified via PagerDuty if we detect an incident.\"\nimage: \"/assets/changelog/pagerduty-integration.png\"\npublishedAt: \"2024-06-25\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\n\nWe've added a PagerDuty integration to the notification feature. This allows you to receive incident alerts through PagerDuty."
  },
  {
    "path": "apps/web/src/content/pages/changelog/password-protected-status-page.mdx",
    "content": "---\ntitle: \"Password-protected status page\"\ndescription: \"Hide your page from the public.\"\nimage: \"/assets/changelog/password-protected-status-page.png\"\npublishedAt: \"2024-05-03\"\nauthor: \"openstatus\"\ncategory: \"statuspage\"\n---\n\nYou can now protect your status page with a password. This will hide the page\nfrom the public and only allow access to users with the password.\n\nYou can enable the password protection from the status page within the\n_Visibility_ settings. You will be able to:\n\n- Enable the feature with a simple checkbox to easily turn it on and off\n- Set your password\n- Share the protected page with a `?authorize=your-password` query parameter to\n  auto-complete the password field\n\n> This feature is available on the Starter plan and above.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/play-checker-improvements.mdx",
    "content": "---\ntitle: \"Speed Checker\"\ndescription: \"Improving the speed checker playground for a better experience.\"\nimage: \"/assets/changelog/play-checker-improvements.png\"\npublishedAt: \"2024-08-27\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nWe've refreshed the Playground for a better experience. Now, access real-time data without leaving the page.\n\nCheck out the [Speed Checker](/play/checker)."
  },
  {
    "path": "apps/web/src/content/pages/changelog/private-location.mdx",
    "content": "---\ntitle: \"Private Location is here (beta)\"\ndescription: \"Monitor your services from your own infrastructure\"\nimage: \"/assets/changelog/private-location.png\"\npublishedAt: \"2025-10-28\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nWe’re excited to introduce Private Location, a beta feature that allows you to monitor your services from your own infrastructure!\n\nThis means you can set up monitoring agents within your own network, giving you more control and insight into your service performance.\n\nAs an example we have deployed a Private Location on [Max's Raspberry Pi](/blog/deploy-private-locations-raspberry-pi) at home, and on [Cloudflare containers](https://docs.openstatus.dev/guides/how-to-deploy-probes-cloudflare-containers/)\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/public-monitors.mdx",
    "content": "---\ntitle: \"Public Monitors\"\ndescription: \"Make your monitors public and share more metrics with your users.\"\nimage: \"/assets/changelog/public-monitors.png\"\npublishedAt: \"2024-04-14\"\nauthor: \"openstatus\"\ncategory: \"statuspage\"\n---\n\nYou can now change the monitors visibility to public. This will allow you to\nshare the monitor's metrics _(the overview page)_ with your users. The period is\nrestricted to **1d** and **7d** for now.\n\nThe monitor can be accessed within a **status page**.\n\n- Status Page URL:\n  [status.openstatus.dev/monitors/1](https://status.openstatus.dev/monitors/1)\n\nYou can enable public mode from the monitor _Danger_ section setting.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/raycast-integration.mdx",
    "content": "---\ntitle: \"Raycast Extension\"\ndescription: \"Use Raycast to manage your monitors, status page and status updates.\"\nimage: \"/assets/changelog/raycast-integration.png\"\npublishedAt: \"2025-01-15\"\nauthor: \"openstatus\"\ncategory: \"integrations\"\n---\n\nWe have published our Raycast integration. You can now manage your monitors, status page and status updates directly from Raycast.\n\nHere are the commands you can use:\n\n- Show Monitors\n- Create Status Report\n- Create Status Report Update\n- Show Status Page\n\nGo to our [Raycast extension](https://www.raycast.com/thibaultleouay/openstatus) page to install it.\n\n```\n\n```\n\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/request-assertions.mdx",
    "content": "---\ntitle: \"Request assertions\"\ndescription: \"Not all requests should return a 200 status code. You can now assert on the status code, headers of your requests.\"\npublishedAt: \"2024-04-01\"\nimage: \"/assets/changelog/request-assertions.png\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nNot all requests should return a 200 status code to be successful. Sometimes you\nwant to assert on the status code of a request. For example, you might want to\ncheck that a request returns a 404 status code when a resource is not found.\n\nYou can now do this with OpenStatus when you configure your monitor.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/response-time-charts.mdx",
    "content": "---\ntitle: \"Response Time Charts\"\ndescription: \"You have now access to new charts for better visualization.\"\npublishedAt: \"2023-10-08\"\nimage: \"/assets/changelog/response-time-charts.png\"\nauthor: \"openstatus\"\ncategory: \"monitoring\"\n---\n\nYou can now assess your monitor's response time using the new charts and get a\nbetter overview of your monitor's performance.\n\nPowered by [tremor.so](https://tremor.so).\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/screenshot-incident.mdx",
    "content": "---\ntitle: \"Website screenshots for incidents\"\ndescription: \"When creating an incident, we capture a website screenshot for added context.\"\nimage: \"/assets/changelog/screenshot-incident.png\"\npublishedAt: \"2024-04-05\"\nauthor: \"openstatus\"\ncategory: \"incidents\"\n---\n\nWe want to provide you with as much context as possible when an incident occurs.\nThat's why we've added website screenshots to incidents.\n\nWhen creating an incident, we capture a website screenshot for added context. We\nalso take a screenshot upon resolving the incident to see the website\npost-resolution.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/slack-agent.mdx",
    "content": "---\ntitle: \"Slack Agent\"\ndescription: \"Manage incidents directly from Slack with the OpenStatus agent.\"\npublishedAt: \"2026-02-27\"\nimage: \"/assets/changelog/slack-agent.png\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\n\nMeet the OpenStatus Slack agent — manage your incidents without leaving Slack.\n\nMention `@openstatus` in any channel to create, update, and resolve incidents or schedule maintenance windows. Your status page and subscribers stay in sync automatically.\n\nHere's what you can do:\n\n- **Create incidents** — `@openstatus create an incident for the payment API – high latency detected.`\n- **Update incidents** — `@openstatus keep the status page updated that we are still monitoring the issue.`\n- **Resolve incidents** — `@openstatus resolve the ongoing incident on my API status page.`\n- **Schedule maintenance** — `@openstatus schedule a maintenance window for my database next Friday from 2–3 PM.`\n\nHead to **Settings > Integrations** in your dashboard to install the Slack agent.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/slack-discord-improvements.mdx",
    "content": "---\ntitle: \"Slack and Discord Message Improvements\"\ndescription: \"Enhanced notification experience for Slack and Discord channels.\"\npublishedAt: \"2026-01-23\"\nimage: \"/assets/changelog/slack-discord-improvements.png\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\n\nWe've improved the Slack and Discord notification experience with better message formatting and are including the downtime duration of an incident.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/slack-discord-notification.mdx",
    "content": "---\ntitle: \"Slack and Discord notifications\"\ndescription: \"You can now send notifications to your favorite channels.\"\npublishedAt: \"2023-11-01\"\nimage: \"/assets/changelog/slack-discord-notification.png\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\n\nWe have just released a new feature that allows you to receive notifications in\nSlack or Discord when your monitor is down.\n\nThanks [Kelvin](https://github.com/AmoabaKelvin) for the help 🔥\n\nExpect more integration to come soon!\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/sms-notification.mdx",
    "content": "---\ntitle: \"Get alerted on your cell phone\"\ndescription: \"Receive alerts on your cell phone via SMS when your monitor is down.\"\npublishedAt: \"2023-11-13\"\nimage: \"/assets/changelog/sms-notification.png\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\n\nYou can now receive alerts on your cell phone via SMS when your monitor is done.\nIt's a great way to get notified when your monitor is done, even if you don't\nhave access to your email.\n\nIt's a paid feature, so you'll need to upgrade your account to use it.\n\nHappy monitoring!\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-badge-v2.mdx",
    "content": "---\ntitle: \"Status Page Badge (v2)\"\ndescription: \"Easily integrate the status of your service everywhere.\"\nimage: \"/assets/changelog/status-page-badge-v2.png\"\npublishedAt: \"2025-08-16\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nUntil now, the badge was a PNG image. Now we provide an SVG instead to make it crystal clear. We are reducing the default style to a minimum.\n\nJust copy the following code and paste it into your website or GitHub README:\n\n```html\n<img src=\"https://YOUR-SLUG.openstatus.dev/badge/v2\" className=\"border rounded-md overflow-hidden\" />\n```\n\nAnd it will look like this:\n\n<a href=\"https://status.openstatus.dev\">\n  <img src=\"https://status.openstatus.dev/badge/v2\" />\n</a>\n\n\nRead more in our [documentation](https://docs.openstatus.dev/guides/how-to-add-svg-status-badge/).\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-badge.mdx",
    "content": "---\ntitle: \"Status Page Badge\"\ndescription: \"Easily integrate the status of your service everywhere.\"\nimage: \"/assets/changelog/status-page-badge.jpeg\"\npublishedAt: \"2024-05-02\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nWe have added a new feature to embed the status of your service everywhere.\n\nJust copy the following code and paste it into your website or GitHub README:\n\n```html\n<img src=\"https://YOUR-SLUG.openstatus.dev/badge\" />\n```\n\nAnd it will look like this:\n\n<a href=\"https://status.openstatus.dev\">\n  <img src=\"https://status.openstatus.dev/badge\" />\n</a>\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-colors-and-more.mdx",
    "content": "---\ntitle: \"Status Page rework\"\ndescription: \"New status specific colors, improved navigation and more.\"\npublishedAt: \"2024-07-21\"\nimage: \"/assets/changelog/status-page-colors-and-more.png\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nWe have reworked our status page!\n\n- **less flashy colors** for _operational_, _degraded_, _downtime_ and _maintenance_\n- **new sticky navigation** bar, merging _maintenances_ and _incidents_ into an _events_ page (your old links will still work)\n- redesign of **status reports**"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-component-subscription.mdx",
    "content": "---\ntitle: \"Component Subscriptions\"\ndescription: \"Subscribe to specific components on a status page to only get notified about what matters to you.\"\npublishedAt: \"2026-03-01\"\nimage: \"/assets/changelog/status-page-component-subscription.png\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nSubscribers can now choose exactly which components they want to receive notifications for on a status page.\n\nInstead of getting every update, subscribers can opt into specific components - like only the API, or only the EU region. When a status report or maintenance affects a component they care about, they get notified. Everything else stays quiet.\n\n- **Per-component subscriptions** - select individual components or entire groups\n- **Manage anytime** - update your subscription scope from the status page via your unique link\n- **Backwards compatible** - existing subscribers continue to receive all updates unless they customize their scope\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-email-authentification.mdx",
    "content": "---\ntitle: \"Status Page Email Authentication\"\ndescription: \"Restrict access to your status pages by email domain for enhanced privacy and security.\"\nimage: \"/assets/changelog/status-page-email-authentification.png\"\npublishedAt: \"2026-01-04\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nYou can now protect your status pages with email domain restrictions. Configure allowed email domains to limit access to specific users or organizations. Visitors will need to verify their email address before viewing your status page.\n\nThis feature is perfect for internal status pages or when you want to share updates with a specific audience while keeping them private from the public.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-feed.mdx",
    "content": "---\ntitle: \"Status Page Feeds\"\ndescription: \"Subscribe to status page updates via RSS and Atom feed.\"\nimage: \"/assets/changelog/status-page-feed.png\"\npublishedAt: \"2025-01-04\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nYou can now subscribe via RSS and Atom feed on any status page! \n\nS/o to [@washingtonserip](https://x.com/washingtonserip) for this contribution."
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-monitor-order.mdx",
    "content": "---\ntitle: \"Status page monitor order\"\ndescription: \"Sort your monitors by importance on your status page.\"\nimage: \"/assets/changelog/status-page-monitor-order.png\"\npublishedAt: \"2024-05-20\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nYou can now sort your monitors by importance on your status page. This allows\nyou to prioritize the most important monitors and show them at the top of your\nstatus page.\n\nPowered by [sortable.sadmn.com](https://sortable.sadmn.com).\n\n{/* The API endpoint for the pages also supports the new `order` property. Read more */}\n{/* in the [docs](https://docs.openstatus.dev/api-reference/page/post-page). */}\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-monitor-values-visibility.mdx",
    "content": "---\ntitle: \"Monitor values visibility\"\ndescription: \"Hide the uptime and number of request on your status pages.\"\npublishedAt: \"2024-10-20\"\nimage: \"/assets/changelog/status-page-monitor-values-visibility.png\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\n\nYou can now hide your request values.\n\nWe have added a checkbox within your Status Page settings to hide/show the number of total and failed requests incl. the average percentage for each monitor connected to the page."
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-redesign-beta.mdx",
    "content": "---\ntitle: \"Status page redesign (beta)\"\ndescription: \"A whole new experience with more control\"\nimage: \"/assets/changelog/status-page-redesign-beta.png\"\npublishedAt: \"2025-10-03\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nYou can now toggle the new status page version. We provide more flexibility and options to include:\n\n- more tracker options\n- community themes\n- support button to website / email\n- homepage link on logo\n\nRead more within our [documentation](http://localhost:4321/tutorial/how-to-configure-status-page/) and see how your page will look like by going to `[slug].stpg.dev` (e.g. [status.stpg.dev](https://status.stpg.dev)).\n\nWe will enable the new status page globally beginning of next year.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-slack-feed-subscribe.mdx",
    "content": "---\ntitle: \"Slack feed subscribe on status page\"\ndescription: \"Link the RSS feed to any Slack channel.\"\nimage: \"/assets/changelog/status-page-slack-feed-subscribe.png\"\npublishedAt: \"2025-12-03\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nLink the RSS feed to any Slack channel."
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-subscribers.mdx",
    "content": "---\ntitle: \"Status Page Subscribers\"\ndescription: \"You can now get the list of your status page subscribers\"\npublishedAt: \"2024-02-23\"\nimage: \"/assets/changelog/status-page-subscribers.png\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nWhen visiting your status page section, you will find a list displaying all\nindividuals who have subscribed to receive updates from your status page.\n\nThis feature enables you to easily identify and manage the subscribers who are\nfollowing your page for notifications and alerts regarding updates on your\nservices.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-unsubscribe.mdx",
    "content": "---\ntitle: \"Status Page Unsubscribe\"\ndescription: \"Subscribers can now easily unsubscribe from status page updates.\"\npublishedAt: \"2026-01-15\"\nimage: \"/assets/changelog/status-page-unsubscribe.png\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nLong overdue, subscribers can now manually unsubscribe from status page email notifications at any time."
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-page-white-label.mdx",
    "content": "---\ntitle: \"Status Page White Label\"\ndescription: \"Remove openstatus branding from your status pages for a fully customized experience.\"\nimage: \"/assets/changelog/status-page-white-label.png\"\npublishedAt: \"2026-01-04\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nYou can now remove the \"powered by openstatus.dev\" branding from your status pages with our new White Label add-on.\n\nPresent a fully branded experience to your users without any external attribution."
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-report-location-change.mdx",
    "content": "---\ntitle: \"Status Report location change\"\ndescription: \"Each report is now saved within a Status Page.\"\npublishedAt: \"2024-07-14\"\nimage: \"/assets/changelog/status-report-location-change.png\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nThe location of your reports has changed. The can be found within the connected status page. No need to connect manually to a status page.\n\nIf you encounter any issues. Please [contact us](mailto:ping@openstatus.dev)."
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-update-subscriber.mdx",
    "content": "---\ntitle: \"Status Update Subscriber\"\ndescription: \"You can now subscribe to pro plan status updates via email.\"\npublishedAt: \"2023-12-11\"\nimage: \"/assets/changelog/status-update-subscriber.png\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nYou can now subscribe to pro plan status updates via email.\n\nAs a subscriber, we require you to verify your email address to avoid spam. You\ncan do this by clicking the link in the email we send you after you subscribe.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/status-widget.mdx",
    "content": "---\ntitle: \"Status Widget\"\ndescription: \"You can now access the status of your status page via a public api.\"\npublishedAt: \"2023-10-05\"\nimage: \"/assets/changelog/status-widget.png\"\ncategory: \"statuspage\"\nauthor: \"openstatus\"\n---\n\nYou can now access the status of your status page via a public api endpoint.\nThis allows you to build your own custom status widget with your unique `:slug`.\n\n```\n$ curl https://api.openstatus.dev/public/status/:slug\n```\n\nYou can find the documentation\n[here](https://docs.openstatus.dev/guides/how-to-use-react-widget).\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/tcp-monitoring.mdx",
    "content": "---\ntitle: \"TCP Monitoring\"\ndescription: \"Monitor your TCP endpoints with OpenStatus.\"\nimage: \"/assets/changelog/tcp-request.png\"\npublishedAt: \"2024-11-05\"\ncategory: \"monitoring\"\nauthor: \"openstatus\"\n---\n\nWe're excited to announce that TCP monitoring is now available in OpenStatus! This new addition expands our monitoring capabilities, allowing you to keep track of all your TCP endpoints.\n\n\n####  Get Started 🚀\nTo start monitoring your TCP endpoints, simply add a new monitor and select \"TCP\" as your monitor type.\n\n\n#### Coming Soon 🔜\nWe're actively working on expanding our monitoring capabilities further with ICMP monitoring support. Stay tuned for updates!"
  },
  {
    "path": "apps/web/src/content/pages/changelog/team-invites.mdx",
    "content": "---\ntitle: \"Team invites\"\ndescription: \"Invite your team members to your workspace\"\nimage: \"/assets/changelog/team-invites.png\"\npublishedAt: \"2023-12-08\"\ncategory: \"company\"\nauthor: \"openstatus\"\n---\n\nAs a Pro plan user, you can now invite you favorite team members to your\nworkspace and collaborate on your project together.\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/telegram-bot-integration.mdx",
    "content": "---\ntitle: \"Telegram Bot Integration\"\ndescription: \"OpenStatus now supports Telegram notifications. Connect your uptime monitoring alerts to the @openstatushq_bot Telegram bot.\"\nimage: \"/assets/changelog/telegram-bot-integration.png\"\npublishedAt: \"2025-12-09\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\n\nYou can connect your uptime notifications to the `@openstatushq_bot` telegram bot."
  },
  {
    "path": "apps/web/src/content/pages/changelog/terraform-provider.mdx",
    "content": "---\ntitle: \"Terraform Provider\"\ndescription: \"Manage your monitors with the Terraform provider\"\nimage: \"/assets/changelog/terraform.png\"\npublishedAt: \"2024-02-07\"\ncategory: \"monitoring\"\nauthor: \"openstatus\"\n---\n\nWe are excited to announce the release of the Terraform provider for OpenStatus.\nThis provider allows you to manage your Grafana Cloud resources using Terraform,\nenabling you to define and manage your OpenStatus resources as code.\n\nHere's the link to the:\n[OpenStats Terraform Provider](https://registry.terraform.io/providers/openstatus/openstatus/latest/docs)\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/webhook-integration.mdx",
    "content": "---\ntitle: \"Webhook Integration\"\ndescription: \"Get notified via webhook if we detect an Incident..\"\nimage: \"/assets/changelog/webhook.png\"\npublishedAt: \"2025-04-08\"\ncategory: \"integrations\"\nauthor: \"openstatus\"\n---\n\nWhen your monitor's status changes, you can receive notifications through a webhook.\n\nThis feature is included in every plan.\n\nFor more information, check our [documentation](https://docs.openstatus.dev/reference/notification/#webhook).\n"
  },
  {
    "path": "apps/web/src/content/pages/changelog/whatsapp-notifications.mdx",
    "content": "---\ntitle: \"WhatsApp Notifications\"\ndescription: \"OpenStatus now supports WhatsApp notifications. Receive uptime monitoring alerts via WhatsApp — more reliable than SMS and works worldwide.\"\nimage: \"/assets/changelog/whatsapp.png\"\npublishedAt: \"2025-12-16\"\nauthor: \"openstatus\"\ncategory: \"notifications\"\n---\n\nYou can now receive your uptime notifications via WhatsApp. It more reliable than SMS and works worldwide.\n"
  },
  {
    "path": "apps/web/src/content/pages/compare/atlassian-statuspage.mdx",
    "content": "---\ntitle: \"Atlassian Statuspage vs openstatus\"\npublishedAt: \"2026-02-21\"\nauthor: \"openstatus\"\ndescription: \"Open-source uptime monitoring and status pages. Learn how openstatus compares to Atlassian Statuspage.\"\ncategory: \"company\"\nfaq:\n  - question: \"Is openstatus a good Atlassian Statuspage alternative?\"\n    answer: \"Yes. Openstatus includes built-in uptime monitoring from 28 global regions — something Atlassian Statuspage does not offer at all. Openstatus pricing is flat ($30/mo) and does not scale with subscriber count. Atlassian Statuspage charges per subscriber tier, meaning your bill grows as your audience does. Openstatus is also open-source and self-hostable.\"\n  - question: \"Does openstatus include uptime monitoring unlike Atlassian Statuspage?\"\n    answer: \"Yes. Atlassian Statuspage has no built-in monitoring. You must connect a separate tool (Datadog, Pingdom, etc.) to detect incidents. Openstatus monitors your endpoints from 28 regions simultaneously and can update your status page automatically based on check results.\"\n  - question: \"How does openstatus pricing compare to Atlassian Statuspage?\"\n    answer: \"Atlassian Statuspage starts at $29/month for one page and 100 subscribers, jumping to $99/month for three pages and $399/month for custom HTML/CSS. Subscriber-count tiers add cost as your audience grows. Openstatus starts at $30/month with flat pricing, unlimited subscribers, and monitoring included.\"\n  - question: \"What does the OpsGenie shutdown mean for Statuspage users?\"\n    answer: \"OpsGenie, Atlassian's incident management tool often used alongside Statuspage, is shutting down in April 2027. Teams using the Statuspage + OpsGenie combination will need to find replacements for both. Openstatus covers the monitoring and status page layer in one product.\"\n  - question: \"Can I self-host openstatus?\"\n    answer: \"Yes. Openstatus is open-source (AGPL-3.0) and fully self-hostable. Atlassian Statuspage is a closed-source SaaS with no self-hosting option.\"\n---\n\n## Looking for an Atlassian Statuspage alternative?\n\n**TL;DR:** Atlassian Statuspage has no built-in monitoring, charges per subscriber tier, and gates custom styling behind a $399/month plan. Openstatus includes monitoring from 28 regions, flat pricing with unlimited subscribers, and is fully open-source. With OpsGenie shutting down in 2027, now is a good time to simplify your stack.\n\nIf you're paying for Statuspage and a separate monitoring tool on top of it, openstatus replaces both. It includes uptime monitoring from 28 global regions and a native status page in one product — with flat pricing that doesn't scale with your subscriber count. Plus, with OpsGenie shutting down in 2027, now is a good time to simplify your stack.\n\nAtlassian Statuspage is the longest-established name in status pages, but it comes with significant trade-offs: **no built-in monitoring**, pricing that scales with your subscriber count, and customization gated behind its most expensive tier. Openstatus covers monitoring and status pages in one product at a fraction of the cost, with flat pricing and an open-source codebase.\n\nThe most important structural difference: Atlassian Statuspage requires you to connect a separate monitoring tool (Datadog, Pingdom, New Relic) to detect incidents. Openstatus monitors your endpoints directly from 28 regions and updates your status page automatically — no second tool, no manual webhook wiring.\n\nAdditionally, OpsGenie — Atlassian's incident management product often used alongside Statuspage — is shutting down in April 2027.\n\n## Feature Comparison\n\n| Feature                    | openstatus       | Atlassian Statuspage    |\n| -------------------------- | ---------------- | ----------------------- |\n| Open-source                | Yes              | No                      |\n| Self-hosted                | Yes              | No                      |\n| Built-in uptime monitoring | Yes              | No                      |\n| Multi-region               | 28 regions       | Not applicable          |\n| Monitoring as code         | Yes              | No                      |\n| OpenTelemetry export       | Yes              | No                      |\n| Subscriber-count billing   | Flat (unlimited) | Scales with subscribers |\n| Custom HTML/CSS            | Themes included  | $399/mo plan only       |\n| Team members               | Unlimited        | Restricted by plan      |\n\n## Pricing Comparison\n\n|                     | openstatus       | Atlassian Statuspage        |\n| ------------------- | ---------------- | --------------------------- |\n| Starting price      | $30/mo           | $29/mo                      |\n| Monitoring included | Yes (28 regions) | No (requires separate tool) |\n| Status pages        | 1 (+$20/mo each) | 1                           |\n| Subscribers         | Unlimited        | 100 (scales with cost)      |\n| Custom Themes       | Included         | $399/mo plan                |\n| Team members        | Unlimited        | Restricted                  |\n| 3 status pages      | $70/mo           | $99/mo                      |\n\nThe starting prices look similar, but Atlassian Statuspage's $29/month plan only includes 100 subscribers and no monitoring. To get custom styling, you need the $399/month plan. And you still need to pay for a separate monitoring tool on top of that. Openstatus includes monitoring, unlimited subscribers, and theming from $30/month.\n\n## When to Choose openstatus\n\n- You want **monitoring + status page** in a single product without wiring up a second tool\n- You need **flat, predictable pricing** that does not scale with subscriber count\n- You want **open-source** software or need to self-host\n- You are not locked into the Atlassian ecosystem (Jira, Confluence)\n- You need **monitoring-as-code**, OpenTelemetry export, or CI/CD integration\n\n## When to Choose Atlassian Statuspage\n\n- Your organization is deeply embedded in the **Atlassian ecosystem** and requires native Jira integration\n- You need enterprise **subscriber management at very large scale** with Atlassian's compliance certifications\n- Your contracts or procurement processes are already tied to **Atlassian Enterprise agreements**\n\n## Switching from Atlassian Statuspage to openstatus\n\nAtlassian Statuspage is one of the most common platforms teams switch from. Here's the migration path:\n\n1. **Sign up** for a free openstatus account — no credit card required\n2. **Recreate your components** — set up the same service components you had on Statuspage\n3. **Create monitors** — add HTTP, TCP, or DNS monitors for each component. Unlike Statuspage, openstatus detects incidents automatically from 28 regions — no need for a separate Datadog or Pingdom integration\n4. **Set up your status page** — configure custom domain, branding, maintenance windows, and subscriber notifications\n5. **Configure alerts** — openstatus supports Slack, Discord, Email, PagerDuty, OpsGenie, Grafana OnCall, and more\n6. **Update your DNS** — point your custom status page domain to openstatus\n7. **Notify subscribers** — let your status page subscribers know about any URL changes\n8. **Cancel Statuspage + monitoring tool** — since openstatus includes monitoring, you can cancel both your Statuspage subscription and the separate monitoring service\n\nWith OpsGenie shutting down in April 2027, this is also a good time to consolidate your incident detection and communication into a single tool.\n\n---\n\nStart monitoring from 28 regions today\n\n<ButtonLink href=\"https://app.openstatus.dev?ref=compare-atlassianstatuspage\">\n  Get Started Free\n</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/compare/betterstack.mdx",
    "content": "---\ntitle: \"BetterStack vs openstatus\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"openstatus vs BetterStack compared side-by-side. 28 regions (parallel) vs 4 (round-robin), open-source vs closed-source, and transparent pricing with no add-on fees for status pages or subscribers.\"\ncategory: \"company\"\nfaq:\n  - question: \"Is openstatus a good BetterStack alternative?\"\n    answer: \"Yes. openstatus offers uptime monitoring from 28 regions (vs. BetterStack's 4), an open-source codebase, parallel check scheduling, and unlimited status page subscribers — all included in the base price. BetterStack charges extra for private status pages ($42/mo.) and subscriber tiers ($42/mo. per 1000). openstatus is also bootstrapped and self-hostable.\"\n  - question: \"How does openstatus pricing compare to BetterStack?\"\n    answer: \"openstatus starts free (Hobby plan) and paid plans begin at $30/month (Starter) and $100/month (Pro). Private status pages and unlimited subscribers are included. BetterStack's equivalent features require paid add-ons on top of their base price.\"\n  - question: \"Does openstatus support more monitoring regions than BetterStack?\"\n    answer: \"openstatus monitors from 28 regions worldwide across 3 cloud providers. BetterStack uses approximately 4 regions. openstatus also uses a parallel scheduling strategy — all selected regions fire simultaneously — whereas BetterStack uses round-robin scheduling, cycling through regions one at a time.\"\n  - question: \"Can I self-host openstatus instead of using the cloud service?\"\n    answer: \"Yes. openstatus is open-source (AGPL-3.0 license) and fully self-hostable with Docker. BetterStack is a closed-source SaaS product with no self-hosting option.\"\n  - question: \"What is the difference between parallel and round-robin monitoring?\"\n    answer: \"Parallel monitoring (openstatus) checks all selected regions simultaneously at each interval, giving you a true global snapshot of availability. Round-robin monitoring (BetterStack) cycles through regions one at a time, so each check only tests from a single location. Parallel monitoring detects regional outages faster.\"\n---\n\n## Looking for a BetterStack alternative?\n\n**TL;DR:** Openstatus checks from 28 regions in parallel for a true global snapshot. BetterStack checks from 4 regions in round-robin (one at a time). Openstatus is open-source with transparent pricing — no add-on fees for status pages or subscribers.\n\nIf you want more monitoring regions, transparent pricing, and no add-on fees, openstatus is the BetterStack alternative to consider. It checks from 28 regions in parallel (vs. BetterStack's 4 in round-robin), is fully open-source, and includes status pages and unlimited subscribers in the base price — no surprise charges.\n\nOpenstatus and BetterStack are both uptime monitoring platforms with status pages. Openstatus is open-source, bootstrapped, and built for teams that want global coverage and transparent pricing. BetterStack is a VC-backed, closed-source SaaS with a broader product suite including log management.\n\nThe most significant technical difference is scheduling strategy: openstatus checks all selected regions **simultaneously** (parallel), giving you a true global picture of availability at each check interval. BetterStack uses **round-robin** scheduling, cycling through regions one at a time.\n\n## Feature Comparison\n\n| Feature                        | openstatus             | BetterStack                 |\n| ------------------------------ | ---------------------- | --------------------------- |\n| Open-source                    | Yes                    | No                          |\n| Multi-region                   | 28 regions             | ~4 regions                  |\n| Scheduling strategy            | Parallel (all at once) | Round-robin (one at a time) |\n| Incident escalation            | No                     | Yes                         |\n| OpenTelemetry exporter         | Yes                    | No                          |\n| GitHub Action                  | Yes                    | No                          |\n| CLI to trigger checks          | Yes                    | No                          |\n| Team members                   | Unlimited              | Restricted                  |\n| Password-protected status page | Included in team plan  | $42/mo add-on               |\n| Self-hostable                  | Yes                    | No                          |\n\n## Pricing Comparison\n\n|                     | openstatus              | BetterStack                 |\n| ------------------- | ----------------------- | --------------------------- |\n| Free plan           | 1 monitor, 6 regions    | 5 monitors, limited regions |\n| Starter/paid        | $30/mo (20 monitors)    | $25/mo                      |\n| Pro                 | $100/mo (50 monitors)   | Custom pricing              |\n| Private status page | Included                | $42/mo add-on               |\n| Team members        | Unlimited on paid plans | Restricted                  |\n| Log management      | Not included            | Available (separate cost)   |\n\nBetterStack's base price looks competitive, but add-ons for private status pages and subscriber tiers increase the effective cost significantly. Openstatus pricing is flat — no surprise charges.\n\n## When to Choose openstatus\n\n- You want monitoring from **28 regions** with parallel checks\n- You prefer **open-source** software or need to self-host\n- You want **predictable pricing** without subscriber or feature add-ons\n- You need **OpenTelemetry export** or CI/CD integration via GitHub Actions\n\n## When to Choose BetterStack\n\n- You need **incident escalation** workflows\n- You want a single platform for uptime monitoring **and** log management\n- You prefer a mature, closed-source SaaS with dedicated support tiers\n\n## Switching from BetterStack to openstatus\n\n1. **Sign up** for a free openstatus account — no credit card required\n2. **Recreate your monitors** — use the dashboard or monitoring-as-code (YAML + CLI) to define them programmatically\n3. **Set up your status page** — openstatus includes branded status pages with custom domain support, password protection, and unlimited subscribers on paid plans\n4. **Configure alerts** — openstatus supports Slack, Discord, PagerDuty, OpsGenie, and more\n5. **Export telemetry** — if you use BetterStack for log management, openstatus supports OpenTelemetry export to push check results into your existing observability stack\n\n---\n\nStart monitoring from 28 regions today\n\n## <ButtonLink href=\"https://app.openstatus.dev?ref=compare-betterstack\">Get Started Free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/compare/checkly.mdx",
    "content": "---\ntitle: \"Checkly vs openstatus\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"Open-source uptime monitoring. Learn how openstatus compares to Checkly.\"\ncategory: \"company\"\nfaq:\n  - question: \"Is openstatus a good Checkly alternative?\"\n    answer: \"It depends on your use case. Openstatus is focused on uptime monitoring and status pages — it excels at HTTP, TCP, and DNS checks from 28 global regions. Checkly is focused on synthetic monitoring with browser-based checks using Playwright. If you need uptime monitoring with a public status page, openstatus is the stronger choice. If you need end-to-end browser testing as monitoring, Checkly is more suitable.\"\n  - question: \"Does openstatus have more monitoring regions than Checkly?\"\n    answer: \"Yes. Openstatus monitors from 28 regions worldwide. Checkly uses approximately 19 regions. Openstatus also checks all regions simultaneously (parallel scheduling) rather than cycling through them.\"\n  - question: \"Does openstatus include a status page?\"\n    answer: \"Yes. Openstatus includes branded status pages with custom domains, maintenance windows, and subscriber notifications on all plans. Checkly does not offer a built-in public status page product.\"\n  - question: \"Is openstatus open-source?\"\n    answer: \"Yes. Openstatus is AGPL-3.0-licensed and fully self-hostable. Checkly is a closed-source SaaS product.\"\n---\n\n## Looking for a Checkly alternative?\n\n**TL;DR:** Openstatus is built for uptime monitoring with built-in status pages. Checkly is built for browser-based synthetic monitoring with Playwright. If you need to know when your API is down, choose openstatus. If you need to test complex user flows, choose Checkly.\n\nIf your main need is uptime monitoring with a public status page rather than browser-based synthetic tests, openstatus is the better fit. It monitors from 28 regions with parallel scheduling, is fully open-source, and includes status pages with every plan — no Playwright scripts needed to know if your API is down.\n\nOpenstatus and Checkly take different approaches to monitoring. Openstatus focuses on **uptime monitoring** — checking whether your endpoints are reachable and performant from 28 global regions — and pairs this with **status pages** for incident communication. Checkly focuses on **synthetic monitoring** using Playwright and Puppeteer to run full browser-based end-to-end checks.\n\nIf your primary need is knowing when your API or website goes down and communicating that to users, openstatus is the more direct fit. If you need to verify complex user flows (login, checkout, search) are working correctly, Checkly's browser testing approach is more powerful.\n\n## Feature Comparison\n\n| Feature                     | openstatus       | Checkly          |\n| --------------------------- | ---------------- | ---------------- |\n| Multi-region                | 28 regions       | ~19 regions      |\n| Status Page                 | Yes (built-in)   | No               |\n| Open-source                 | Yes              | No               |\n| Monitoring as code          | Yes (YAML + CLI) | Yes (JavaScript) |\n| Browser checks (Playwright) | No               | Yes              |\n| OpenTelemetry exporter      | Yes              | Yes              |\n| GitHub Action               | Yes              | Yes              |\n| Team members                | Unlimited        | Restricted       |\n| Self-hostable               | Yes              | No               |\n\n## Pricing Comparison\n\n|                | openstatus              | Checkly                    |\n| -------------- | ----------------------- | -------------------------- |\n| Free plan      | 1 monitor, 6 regions    | 5 browser checks           |\n| Starter/paid   | $30/mo                  | Usage-based (from ~$30/mo) |\n| Status page    | Included                | Not available              |\n| Team members   | Unlimited on paid plans | Per-seat pricing           |\n| Browser checks | Not available           | Core feature               |\n\nCheckly's pricing is usage-based and scales with the number and frequency of browser checks. Openstatus offers flat, predictable pricing — what you see is what you pay.\n\n## When to Choose openstatus\n\n- You need uptime monitoring **with a status page** for incident communication\n- You want **28 regions** with parallel scheduling\n- You prefer **open-source** software or need to self-host\n- You want HTTP/TCP/DNS monitoring with **assertions and thresholds**\n- You need **predictable, low-cost pricing** for a small team\n\n## When to Choose Checkly\n\n- You need **browser-based synthetic monitoring** with Playwright\n- You want to monitor **complex user flows** (checkout, authentication, search)\n- You already use JavaScript-based monitoring-as-code workflows\n\n## Switching from Checkly to openstatus\n\nIf your Checkly usage is primarily HTTP API checks (not browser-based Playwright tests), the switch is simple:\n\n1. **Sign up** for a free openstatus account\n2. **Migrate your API checks** — recreate HTTP monitors in the dashboard or define them via YAML and the openstatus CLI\n3. **Set up your status page** — something Checkly doesn't offer natively\n4. **Configure alerts** — openstatus supports Slack, Discord, PagerDuty, OpsGenie, and more\n\nIf you rely heavily on Playwright browser checks, openstatus is not a direct replacement for that capability. Consider using openstatus for uptime monitoring and status pages alongside a browser testing tool.\n\n---\n\nStart monitoring from 28 regions today\n\n## <ButtonLink href=\"https://app.openstatus.dev?ref=compare-checkly\">Get Started Free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/compare/incidentio.mdx",
    "content": "---\ntitle: \"Incident.io vs openstatus\"\npublishedAt: \"2026-02-21\"\nauthor: \"openstatus\"\ndescription: \"Open-source uptime monitoring and status pages. Learn how openstatus compares to incident.io.\"\ncategory: \"company\"\nfaq:\n  - question: \"Is openstatus a good incident.io alternative?\"\n    answer: \"It depends on what you need. Incident.io is an incident management platform — on-call scheduling, Slack-based workflows, AI post-mortems. Openstatus is a monitoring and status page platform. If your primary need is detecting outages and communicating them to users, openstatus is the more direct fit. If you already have monitoring and need sophisticated incident coordination across a large on-call team, incident.io is built for that.\"\n  - question: \"Does incident.io include uptime monitoring?\"\n    answer: \"No. Incident.io does not monitor your services. It receives alerts from external tools like Datadog, PagerDuty, or Prometheus and routes them through its incident workflow. You still need a separate monitoring tool. Openstatus includes uptime monitoring from 28 regions as part of the same product.\"\n  - question: \"Does openstatus have incident management features?\"\n    answer: \"Openstatus covers the detect-and-communicate part of incident response: monitors detect issues and your status page communicates them to users. It does not have on-call scheduling, escalation policies, or Slack-based incident coordination. For teams that need those workflows, incident.io is the stronger choice.\"\n  - question: \"How does openstatus pricing compare to incident.io?\"\n    answer: \"Openstatus starts at $30/month with unlimited team members. Incident.io uses per-seat pricing on paid plans and targets mid-to-large engineering teams. For small teams that only need monitoring and a status page, openstatus is significantly cheaper.\"\n---\n\n## Looking for an incident.io alternative?\n\n**TL;DR:** Openstatus detects outages and communicates them to users via status pages. Incident.io coordinates the human response through Slack workflows and on-call scheduling. They solve adjacent problems — openstatus is for monitoring and status pages, incident.io is for incident coordination.\n\nIf you need to detect outages and communicate them to users — not just coordinate the human response — openstatus is the simpler, more affordable choice. It combines uptime monitoring from 28 global regions with native status pages in a single open-source product, no separate monitoring tool required.\n\nOpenstatus and incident.io solve adjacent problems. Incident.io is an **incident management** platform — it coordinates the human response to an outage through Slack workflows, on-call scheduling, and post-mortems. Openstatus is a **monitoring and status page** platform — it detects outages automatically and communicates them to end users.\n\nThe key distinction: incident.io has no uptime monitoring. It relies on external alerting sources (Datadog, PagerDuty, Prometheus) to trigger its workflows. If you want a single tool that monitors your services and updates your status page without manual intervention, openstatus covers that loop directly. If you have monitoring in place and need sophisticated incident coordination across a large on-call team, incident.io is purpose-built for that.\n\n## Feature Comparison\n\n| Feature                    | openstatus | incident.io      |\n| -------------------------- | ---------- | ---------------- |\n| Open-source                | Yes        | No               |\n| Built-in uptime monitoring | Yes        | No               |\n| Multi-region               | 28 regions | Not applicable   |\n| Monitoring as code         | Yes        | No               |\n| OpenTelemetry export       | Yes        | No               |\n| Status page included       | Yes        | Yes              |\n| Incident management        | No         | Yes              |\n| On-call scheduling         | No         | Yes              |\n| Post-mortems               | No         | Yes (AI-powered) |\n| Team members               | Unlimited  | Per-seat pricing |\n\n## Pricing Comparison\n\n|                 | openstatus                        | incident.io                                 |\n| --------------- | --------------------------------- | ------------------------------------------- |\n| Free plan       | 1 monitor, 1 status page          | Limited free tier                           |\n| Starter/paid    | $30/mo                            | Per-seat pricing (contact sales)            |\n| What's included | Monitoring + status page + alerts | Incident workflows + on-call + post-mortems |\n| Team members    | Unlimited on paid plans           | Per-seat                                    |\n| Monitoring      | Built-in (28 regions)             | Requires external tool                      |\n\nFor small-to-mid teams that need monitoring and a status page, openstatus is significantly cheaper. Incident.io's per-seat pricing targets larger engineering organizations with complex on-call rotations.\n\n## When to Choose openstatus\n\n- You need **uptime monitoring + status page** without integrating two separate products\n- You want **open-source** software or need to self-host\n- You need **monitoring-as-code** or OpenTelemetry export\n- You want **predictable pricing** without per-seat charges\n\n## When to Choose incident.io\n\n- Your team already has monitoring (Datadog, PagerDuty) and needs **Slack-based incident coordination**\n- You need **on-call scheduling, escalation policies**, and rotation management\n- You want **AI-powered post-mortems** and structured incident timelines\n- You manage a large on-call rotation across multiple teams or services\n\n## Using openstatus alongside incident.io\n\nThese products are complementary rather than direct replacements. A common setup:\n\n- **Openstatus** detects outages from 28 regions and updates your public status page automatically\n- **Incident.io** receives alerts (from openstatus or other tools) and coordinates the internal response via Slack\n- Your **users** see the status page. Your **engineers** see the incident channel.\n\nIf you only need the detection and communication layer, openstatus covers it alone. If you need the full incident coordination layer too, both tools work well together.\n\n---\n\nStart monitoring from 28 regions today\n\n<ButtonLink href=\"https://app.openstatus.dev?ref=compare-incidentio\">Get Started Free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/compare/instatus.mdx",
    "content": "---\ntitle: \"Instatus vs openstatus\"\npublishedAt: \"2026-02-21\"\nauthor: \"openstatus\"\ndescription: \"Open-source uptime monitoring and status pages. Learn how openstatus compares to Instatus.\"\ncategory: \"company\"\nfaq:\n  - question: \"Is openstatus a good Instatus alternative?\"\n    answer: \"Yes, especially if you need real uptime monitoring alongside your status page. Instatus is a status-page-first product with basic HTTP monitoring added later. Openstatus was built around monitoring first — checking from 28 global regions simultaneously — with status pages as a native part of the product, not a bolt-on.\"\n  - question: \"Does openstatus include a status page like Instatus?\"\n    answer: \"Yes. Openstatus includes branded status pages with custom domains, maintenance windows, and subscriber notifications on all paid plans. The status page is tightly coupled to your monitors, so incidents and response times reflect real check results.\"\n  - question: \"How does openstatus pricing compare to Instatus?\"\n    answer: \"Instatus starts at $20/month for one custom-domain status page. Openstatus starts at $30/month and includes uptime monitoring from 28 regions, unlimited team members, and monitoring-as-code tooling. If you need both monitoring and a status page, openstatus covers both in one plan.\"\n  - question: \"Can I self-host openstatus?\"\n    answer: \"Yes. Openstatus is open-source (AGPL-3.0) and fully self-hostable. Instatus is a closed-source SaaS with no self-hosting option.\"\n---\n\n## Looking for an Instatus alternative?\n\n**TL;DR:** Instatus is a status-page-first product with basic monitoring bolted on. Openstatus was built around monitoring — 28 regions, parallel checks — with status pages as a native part of the product. If you want your status page backed by real monitoring data, choose openstatus.\n\nIf you want your status page backed by real monitoring — not just a pretty page with bolted-on HTTP pings — openstatus is the stronger choice. It monitors from 28 regions simultaneously and surfaces results directly on your status page, so incidents are detected and communicated automatically.\n\nOpenstatus and Instatus are both alternatives to Atlassian Statuspage, but they cover different ground. Instatus is focused purely on status pages — fast, CDN-delivered, with basic monitoring bolted on as a secondary feature. Openstatus treats **monitoring and status pages as equal parts of the same product**: checks run from 28 regions simultaneously, and your status page reflects those results in real time.\n\nThe practical difference: with Instatus, your status page and your monitoring live in separate worlds. With openstatus, an incident detected by a monitor can surface directly on your status page — no manual update, no webhook wiring between tools.\n\n## Feature Comparison\n\n| Feature               | openstatus                     | Instatus   |\n| --------------------- | ------------------------------ | ---------- |\n| Open-source           | Yes                            | No         |\n| Self-hosted           | Yes                            | No         |\n| Scheduling strategy   | Parallel (all regions at once) | Sequential |\n| Multi-region          | 28 regions                     | ~4 regions |\n| Monitoring as code    | Yes                            | No         |\n| OpenTelemetry export  | Yes                            | No         |\n| GitHub Action         | Yes                            | No         |\n| Status page           | Yes                            | Yes        |\n| Unlimited subscribers | Yes                            | Yes        |\n| Team members          | Unlimited                      | Unlimited  |\n\n## Pricing Comparison\n\n|                         | openstatus                                                      | Instatus                                       |\n| ----------------------- | --------------------------------------------------------------- | ---------------------------------------------- |\n| Free plan               | 1 monitor, 1 status page                                        | 1 status page (no custom domain)               |\n| Starter/paid            | $30/mo                                                          | $20/mo                                         |\n| What's included         | 20 monitors + 28 regions + status page + unlimited team members | 1 custom-domain status page + basic monitoring |\n| Additional status pages | $20/mo each                                                     | Included in higher plans                       |\n\nInstatus is $10/month cheaper on the base plan, but it does not include the depth of monitoring openstatus offers. If you're already paying for a separate monitoring tool alongside Instatus, openstatus replaces both at a lower combined cost.\n\n## When to Choose openstatus\n\n- You need **uptime monitoring** tightly integrated with your status page\n- You want **28-region parallel checks** rather than basic single-location HTTP pings\n- You need **monitoring-as-code** via YAML, CLI, Terraform, or GitHub Actions\n- You prefer **open-source** software or need to self-host\n- You want **OpenTelemetry export** to push check results into your existing observability stack\n\n## When to Choose Instatus\n\n- You want a **pure status page** with no monitoring requirements\n- You need a **static, CDN-delivered** status page with fast global load times\n- You are migrating from Atlassian Statuspage and monitoring is handled by a separate tool\n- You want the **lowest possible price** for a standalone status page\n\n## Switching from Instatus to openstatus\n\n1. **Sign up** for a free openstatus account — no credit card required\n2. **Set up your status page** — configure your custom domain, branding, and components to match your Instatus setup\n3. **Create monitors** — add HTTP, TCP, or DNS monitors for each component on your status page, with 28-region coverage\n4. **Configure alerts** — openstatus supports Slack, Discord, Email, PagerDuty, OpsGenie, and more\n5. **Update your DNS** — point your custom status page domain to openstatus\n6. **Notify subscribers** — let your status page subscribers know about the new URL if it changes\n\n---\n\nStart monitoring from 28 regions today\n\n## <ButtonLink href=\"https://app.openstatus.dev?ref=compare-instatus\">Get Started Free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/compare/statusio.mdx",
    "content": "---\ntitle: \"Status.io vs openstatus\"\npublishedAt: \"2026-02-21\"\nauthor: \"openstatus\"\ndescription: \"Open-source uptime monitoring and status pages. Learn how openstatus compares to Status.io.\"\ncategory: \"company\"\nfaq:\n  - question: \"Is openstatus a good Status.io alternative?\"\n    answer: \"Yes. Openstatus includes built-in uptime monitoring from 28 global regions, which Status.io does not offer. Openstatus is open-source, self-hostable, and starts at $30/month with unlimited team members. Status.io is a closed-source SaaS starting around $79/month with no monitoring capabilities.\"\n  - question: \"Does openstatus include uptime monitoring unlike Status.io?\"\n    answer: \"Yes. Status.io is a hosted status page platform with no built-in monitoring. You still need a separate tool to detect incidents. Openstatus monitors your endpoints from 28 regions simultaneously and can reflect real check results on your status page.\"\n  - question: \"How does openstatus pricing compare to Status.io?\"\n    answer: \"Status.io starts around $79/month. Openstatus starts at $30/month and includes uptime monitoring, unlimited team members, and developer tooling (CLI, Terraform, GitHub Actions) that Status.io does not offer.\"\n  - question: \"Can I self-host openstatus like I cannot with Status.io?\"\n    answer: \"Yes. Openstatus is open-source (AGPL-3.0) and fully self-hostable. Status.io is a closed-source SaaS with no self-hosting option.\"\n---\n\n## Looking for a Status.io alternative?\n\n**TL;DR:** Status.io costs $79+/month for a status page with no monitoring included. Openstatus starts at $30/month and includes uptime monitoring from 28 regions, unlimited team members, and developer tooling — all in one open-source product.\n\nIf you're paying $79+/month for a status page that doesn't even include monitoring, openstatus is the alternative that covers both. Get uptime monitoring from 28 global regions and a native status page in one open-source product starting at $30/month with unlimited team members.\n\nOpenstatus and Status.io are both hosted status page platforms, but they differ significantly in scope and price. Status.io is an older, enterprise-oriented status page tool with no built-in monitoring — you need a separate service to detect incidents. Openstatus monitors your endpoints from **28 regions simultaneously**, surfaces results on a native status page, and provides developer tooling (CLI, Terraform, GitHub Actions) that Status.io does not.\n\nFor teams that want monitoring and status pages without stitching together two products, openstatus covers both at a lower price point with an open-source codebase.\n\n## Feature Comparison\n\n| Feature                    | openstatus | Status.io      |\n| -------------------------- | ---------- | -------------- |\n| Open-source                | Yes        | No             |\n| Self-hosted                | Yes        | No             |\n| Built-in uptime monitoring | Yes        | No             |\n| Multi-region               | 28 regions | Not applicable |\n| Monitoring as code         | Yes        | No             |\n| OpenTelemetry export       | Yes        | No             |\n| GitHub Action              | Yes        | No             |\n| Status page included       | Yes        | Yes            |\n| Private status pages       | Yes        | Yes            |\n| Team members               | Unlimited  | Restricted     |\n\n## Pricing Comparison\n\n|                     | openstatus                        | Status.io              |\n| ------------------- | --------------------------------- | ---------------------- |\n| Starting price      | $30/mo                            | ~$79/mo                |\n| What's included     | Monitoring + status page + alerts | Status page only       |\n| Monitoring          | Built-in (28 regions)             | Requires separate tool |\n| Team members        | Unlimited on paid plans           | Limited by plan        |\n| Developer tooling   | CLI, Terraform, GitHub Actions    | No                     |\n| Self-hosting option | Yes (free)                        | No                     |\n\nStatus.io costs nearly 3x more than openstatus's Starter plan — and doesn't include any monitoring. When you factor in the cost of a separate monitoring tool, the total cost difference grows even larger.\n\n## When to Choose openstatus\n\n- You need **uptime monitoring + status page** without a separate monitoring tool\n- You want **open-source** software and self-hosting flexibility\n- You need **monitoring-as-code** via YAML, CLI, or Terraform\n- You want **lower, predictable pricing** with unlimited team members\n- You need **OpenTelemetry export** or GitHub Actions integration\n\n## When to Choose Status.io\n\n- Your organization has **existing vendor agreements** with Status.io\n- You need Status.io's specific **enterprise compliance certifications**\n- You require Status.io's **private internal status page** access control model\n- You have a large-scale deployment with **dedicated account management** needs\n\n## Switching from Status.io to openstatus\n\n1. **Sign up** for a free openstatus account — no credit card required\n2. **Set up your status page** — configure components, custom domain, branding, and subscriber notifications to match your Status.io setup\n3. **Create monitors** — add HTTP, TCP, or DNS monitors for each service component. Unlike Status.io, openstatus detects incidents automatically from 28 regions\n4. **Configure alerts** — openstatus supports Slack, Discord, Email, PagerDuty, OpsGenie, and more\n5. **Update your DNS** — point your custom status page domain to openstatus\n6. **Cancel your monitoring tool** — since openstatus includes monitoring, you may be able to cancel the separate monitoring service you were using with Status.io\n\n---\n\nStart monitoring from 28 regions today\n\n<ButtonLink href=\"https://app.openstatus.dev?ref=compare-statusio\">Get Started Free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/compare/uptime-kuma.mdx",
    "content": "---\ntitle: \"Uptime Kuma vs openstatus\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"openstatus vs Uptime Kuma compared side-by-side. Both open-source, but openstatus offers managed SaaS + 28-region global monitoring. Uptime Kuma is self-hosted only from 1 location.\"\ncategory: \"company\"\nfaq:\n  - question: \"Is openstatus a good Uptime Kuma alternative?\"\n    answer: \"Yes, especially if you want managed cloud hosting or global multi-region monitoring. Both are open-source, but openstatus is available as a SaaS (no server to maintain) and monitors from 28 regions worldwide. Uptime Kuma is self-hosted only and checks from a single server location.\"\n  - question: \"What is the main difference between openstatus and Uptime Kuma?\"\n    answer: \"The main difference is hosting model and monitoring coverage. Uptime Kuma is self-hosted only — you run it on your own server and it monitors from that single location. openstatus is available as a managed SaaS or self-hosted, and checks from 28 regions across multiple cloud providers simultaneously.\"\n  - question: \"Is openstatus free like Uptime Kuma?\"\n    answer: \"openstatus has a free Hobby plan (1 monitor, 6 regions, 1 status page) with no credit card required. Uptime Kuma is fully free and open-source but requires you to provision, host, and maintain your own server.\"\n  - question: \"Does openstatus support self-hosting like Uptime Kuma?\"\n    answer: \"Yes. openstatus is AGPL-3.0 licensed and can be self-hosted with Docker. You also get the option to use the managed cloud service without managing any infrastructure.\"\n  - question: \"Can openstatus monitor from multiple regions unlike Uptime Kuma?\"\n    answer: \"Yes. openstatus monitors from 28 regions across 3 cloud providers (AWS, GCP, Fly.io) simultaneously. Uptime Kuma only checks from the single server where it is installed, which means it cannot detect regional outages.\"\n---\n\n## Looking for an Uptime Kuma alternative?\n\n**TL;DR:** Both are open-source. Openstatus offers managed SaaS hosting and monitors from 28 global regions simultaneously. Uptime Kuma is self-hosted only and checks from one server location. Choose openstatus for global coverage without infrastructure, Uptime Kuma for fully free self-managed monitoring.\n\nIf you love the open-source ethos of Uptime Kuma but want managed hosting and global coverage, openstatus gives you both. Monitor from 28 regions across 3 cloud providers without maintaining your own server — or self-host it if you prefer. Either way, you get multi-region checks that Uptime Kuma's single-server architecture can't provide.\n\nOpenstatus and Uptime Kuma are both open-source uptime monitoring tools, making this a comparison between two projects with shared values but different architectures. The fundamental difference is the **hosting model**: Uptime Kuma is self-hosted only — you run it on your own server and it monitors from that single location. Openstatus is available both as a **managed SaaS** and for self-hosting, and checks from **28 regions worldwide**.\n\nIf you want zero infrastructure responsibility and global multi-region checks, openstatus is the natural choice. If you want a completely free, self-managed tool with full control and no external dependencies, Uptime Kuma is a solid option.\n\n## Feature Comparison\n\n| Feature             | openstatus          | Uptime Kuma              |\n| ------------------- | ------------------- | ------------------------ |\n| Open-source         | Yes                 | Yes                      |\n| Hosting model       | SaaS or self-hosted | Self-hosted only         |\n| Multi-cloud         | 3 cloud providers   | Single server            |\n| Multi-region        | 28 regions          | 1 (your server location) |\n| OTel Export         | Yes                 | No                       |\n| GitHub Action       | Yes                 | No                       |\n| Team members        | Unlimited           | Unlimited                |\n| Managed SaaS option | Yes                 | No                       |\n| Monitoring as code  | Yes                 | No                       |\n\n## Pricing Comparison\n\n|                     | openstatus                                    | Uptime Kuma                           |\n| ------------------- | --------------------------------------------- | ------------------------------------- |\n| Software cost       | Free (Hobby), $30/mo (Starter), $100/mo (Pro) | Free                                  |\n| Infrastructure cost | $0 (managed SaaS)                             | Your server cost ($5-20+/mo VPS)      |\n| Maintenance         | None (managed)                                | You manage updates, backups, uptime   |\n| Multi-region        | Included (28 regions)                         | Requires additional server per region |\n| Status page         | Included                                      | Included                              |\n\nUptime Kuma is free software, but running it requires a server. A basic VPS costs $5-20/month, and you're responsible for updates, backups, and keeping the monitoring server itself online. Openstatus's managed SaaS eliminates that overhead. To get multi-region monitoring with Uptime Kuma, you'd need to run separate instances in each region.\n\n## When to Choose openstatus\n\n- You want **managed SaaS** with no infrastructure to maintain\n- You need **28-region global monitoring** from multiple cloud providers\n- You want **monitoring as code** via YAML and GitHub Actions\n- You need **OpenTelemetry export** or a tightly integrated status page\n- You want to **talk to the founders** directly (bootstrapped, small team)\n\n## When to Choose Uptime Kuma\n\n- You want **completely free** monitoring with no usage limits\n- You are comfortable **running your own server**\n- You need monitoring **behind a firewall** with no external SaaS dependency\n- You prefer a **single-location** check from your own infrastructure\n\n## Switching from Uptime Kuma to openstatus\n\n1. **Sign up** for a free openstatus account — no credit card required\n2. **Recreate your monitors** — use the dashboard or monitoring-as-code (YAML + CLI) to define your HTTP, TCP, and DNS checks\n3. **Set up your status page** — openstatus includes a branded status page with custom domain support\n4. **Configure alerts** — openstatus supports Slack, Discord, Email, PagerDuty, OpsGenie, and more\n5. **Decommission your server** — once your monitors are running on openstatus, you can shut down your Uptime Kuma instance and stop paying for the VPS\n\nIf you prefer to self-host openstatus instead, check the [GitHub repository](https://github.com/openstatusHQ/openstatus) for Docker setup instructions.\n\n---\n\nStart monitoring from 28 regions today\n\n<ButtonLink href=\"https://app.openstatus.dev?ref=compare-uptimekuma\">Get Started Free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/compare/uptime-robot.mdx",
    "content": "---\ntitle: \"UptimeRobot vs openstatus\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"Open-source uptime monitoring. Learn how openstatus compares to UptimeRobot.\"\ncategory: \"company\"\nfaq:\n  - question: \"Is openstatus a good UptimeRobot alternative?\"\n    answer: \"Yes. Openstatus monitors from 28 global regions simultaneously (UptimeRobot checks from a single location at a time), is open-source and self-hostable, includes unlimited team members on paid plans, and supports OpenTelemetry export and CI/CD integration via GitHub Actions — none of which UptimeRobot offers.\"\n  - question: \"How does openstatus pricing compare to UptimeRobot?\"\n    answer: \"Openstatus starts free and paid plans begin at $30/month with unlimited team members. UptimeRobot charges an additional $19 per seat for team members, which adds up quickly for larger teams.\"\n  - question: \"Does openstatus monitor from more regions than UptimeRobot?\"\n    answer: \"Yes. Openstatus checks from 28 regions worldwide across multiple cloud providers. UptimeRobot does not offer meaningful multi-region monitoring.\"\n  - question: \"Can I self-host openstatus?\"\n    answer: \"Yes. Openstatus is open-source (AGPL-3.0 license) and self-hostable. UptimeRobot is a closed-source SaaS with no self-hosting option.\"\n---\n\n## Looking for an UptimeRobot alternative?\n\n**TL;DR:** Openstatus monitors from 28 regions simultaneously with open-source code and unlimited team members. UptimeRobot checks from one location at a time and charges $19/seat for team access.\n\nIf you're tired of single-location checks and per-seat pricing, openstatus is the UptimeRobot alternative worth considering. It's open-source, monitors from 28 regions simultaneously, includes unlimited team members on paid plans, and gives you a built-in status page — all things UptimeRobot doesn't offer.\n\nOpenstatus and UptimeRobot are both uptime monitoring tools, but they differ significantly in architecture and capabilities. Openstatus is open-source, monitors from 28 regions **simultaneously**, and includes status pages and developer tooling. UptimeRobot is a long-established closed-source SaaS that checks from a single location at a time with a simpler feature set.\n\nFor teams that need global monitoring coverage, CI/CD integration, or a public status page, openstatus is the stronger choice. UptimeRobot remains a simple, well-known option for basic single-location availability checks.\n\n## Feature Comparison\n\n| Feature            | openstatus       | UptimeRobot     |\n| ------------------ | ---------------- | --------------- |\n| Open-source        | Yes              | No              |\n| Multi-cloud        | 3 providers      | No              |\n| Multi-region       | 28 regions       | Single location |\n| OTel Export        | Yes              | No              |\n| GitHub Action      | Yes              | No              |\n| Team members       | Unlimited (paid) | $19/seat add-on |\n| Status Page        | Yes              | Basic           |\n| Monitoring as code | Yes              | No              |\n| Self-hostable      | Yes              | No              |\n\n## Pricing Comparison\n\n|              | openstatus              | UptimeRobot           |\n| ------------ | ----------------------- | --------------------- |\n| Free plan    | 1 monitor, 6 regions    | 50 monitors, 1 region |\n| Starter/paid | $30/mo (20 monitors)    | $8/mo per 10 monitors |\n| Team members | Unlimited on paid plans | $19/seat add-on       |\n| Status page  | Included                | Included (basic)      |\n| Multi-region | Included (28 regions)   | Not available         |\n\nUptimeRobot's free plan includes more monitors, but openstatus's free plan already includes multi-region monitoring. On paid plans, UptimeRobot's per-seat pricing adds up quickly for teams — a 5-person team adds $95/month on top of the monitoring cost.\n\n## When to Choose openstatus\n\n- You need **multi-region monitoring** from 28 global locations\n- You want a **status page** tightly integrated with your monitors\n- You need **team collaboration** without per-seat charges\n- You want **open-source** software or CI/CD integration\n- You need **OpenTelemetry export** or monitoring as code\n\n## When to Choose UptimeRobot\n\n- You want a **familiar, well-established** tool with a long track record\n- Single-location monitoring is **sufficient for your use case**\n- You need **many monitors on the free tier** — UptimeRobot's free plan includes up to 50 monitors\n\n## Switching from UptimeRobot to openstatus\n\nMoving from UptimeRobot to openstatus is straightforward:\n\n1. **Sign up** for a free openstatus account — no credit card required\n2. **Recreate your monitors** in the openstatus dashboard or use monitoring-as-code (YAML + CLI) to define them programmatically\n3. **Set up your status page** — openstatus includes a branded status page with custom domain support on paid plans\n4. **Configure alerts** — openstatus supports Slack, Discord, Email, PagerDuty, OpsGenie, and more\n\nMost teams complete the switch in under an hour. If you need help, reach out at [ping@openstatus.dev](mailto:ping@openstatus.dev).\n\n---\n\nStart monitoring from 28 regions today\n\n## <ButtonLink href=\"https://app.openstatus.dev?ref=compare-uptimerobot\">Get Started Free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/api-service-disruption.mdx",
    "content": "---\ntitle: \"API Service Disruption Template\"\ndescription: \"Professional template for communicating API outages and third-party service failures to your users.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-01-19\"\ncategory: \"template\"\nfaq:\n  - question: \"How often should I update users during an API disruption?\"\n    answer: \"Provide updates every 30-60 minutes during active incidents, even if there's no significant change. Users appreciate knowing you're still working on the issue. Once resolved, a final summary is essential.\"\n  - question: \"Should I name the third-party provider causing the issue?\"\n    answer: \"Yes, if you can. Being specific helps users understand the scope and that you're transparent about dependencies. Companies like Vercel, Stripe, and GitHub regularly name their providers during incidents. Just remain professional and factual.\"\n  - question: \"What metrics should I include in API disruption updates?\"\n    answer: \"Include error rates (e.g., '5% of requests failing'), affected endpoints, and timeframes when you have them. Avoid oversharing internal metrics that might confuse users. Focus on impact-oriented data that helps them assess how they're affected.\"\n  - question: \"When should I use this template instead of other incident templates?\"\n    answer: \"Use this template specifically for API-related issues, third-party provider failures, or integration connectivity problems. Use Database Performance for database-specific issues, Deployment Rollback for deployment failures, or Network Connectivity for regional/CDN issues.\"\n---\n\nUse this template when your API or third-party service provider is experiencing issues. Based on real-world examples from companies like Vercel, Stripe, and GitHub.\n\n## When to Use This Template\n\n- Third-party API failures\n- Elevated API error rates\n- Service provider outages\n- Integration connectivity issues\n\n## Template Messages\n\n### Investigating\n\nWe are currently experiencing issues with our third-party API provider. Our team is actively investigating and working with the provider to restore normal service.\n\nWe apologize for the inconvenience and will provide updates as we learn more.\n\n### Identified\n\nWe have confirmed the issue is with our third-party provider's infrastructure. They are working on a fix and we are monitoring their progress.\n\n### Monitoring\n\nA fix has been deployed. We are monitoring service health and will confirm full restoration shortly.\n\n### Resolved\n\nThe service has been fully restored. All API operations are back to normal. We apologize for the disruption.\n\n## Real-World Examples\n\n### Vercel: \"Elevated error rates across new deployments and Vercel API\"\n\n**Context**: Build failures and deployment issues affecting multiple systems\n**Duration**: ~32 minutes\n**Impact**: New deployments and API calls\n\nThis incident title demonstrates clear scope communication - users immediately understood both the severity (\"elevated error rates\") and which systems were affected.\n\n### GitHub: \"Configuration error during a model update\"\n\n**Context**: Copilot service degradation\n**Duration**: ~1.5 hours\n**Impact**: 18% average error rate, peaking at 100%\n\nGitHub's approach included specific metrics and root cause transparency, helping technical users understand the scope and nature of the issue.\n\n### Stripe: Payment Method Maintenance\n\n**Context**: Scheduled maintenance for payment providers\n**Approach**: Proactive communication with disclaimers about third-party information\n\nStripe consistently provides context about the source of information and sets clear expectations about accuracy.\n\n## Tips for Using This Template\n\n1. **Be specific about the provider** if you can share that information\n2. **Include timeframes** when you have them (e.g., \"Expected resolution: 30 minutes\")\n3. **Add metrics** if you're comfortable sharing them (e.g., \"affecting 5% of requests\")\n4. **Acknowledge impact** - users appreciate transparency about what's affected\n5. **Follow up after resolution** with any lessons learned or preventative measures\n\n## Customization Ideas\n\n- Add your specific API names (e.g., \"Payment API\" instead of generic \"API\")\n- Include workaround instructions if available\n- Link to status dashboard or real-time monitoring\n- Add contact information for urgent support needs\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/best-opensource-status-page-2026.mdx",
    "content": "---\ntitle: \"Best Open Source Status Page Tools in 2026\"\ndescription: \"Compare the top open-source status page tools in 2026. We review openstatus, Cachet, Vigil, Statping-ng, and Upptime to help you pick the right one for your team.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-01-19\"\ncategory: \"alternative\"\nfaq:\n  - question: \"What is the best open-source status page tool in 2026?\"\n    answer: \"openstatus is the top pick for 2026. It is actively maintained, offers both cloud-hosted and self-hosted deployments, includes built-in uptime monitoring, and integrates with Slack out of the box.\"\n  - question: \"What should I look for when choosing an open-source status page?\"\n    answer: \"Focus on active maintenance, deployment flexibility (hosted vs. self-hosted), built-in monitoring, notification integrations (Slack, email, webhooks), and how easy it is for your end users to understand the current status at a glance.\"\n  - question: \"Can I self-host an open-source status page for free?\"\n    answer: \"Yes. Tools like openstatus, Vigil, Cachet, and Statping-ng can all be self-hosted at no licensing cost. Keep in mind you will still need to provision and maintain your own infrastructure, so factor in server and operational costs.\"\n  - question: \"Is Upptime still a good choice in 2026?\"\n    answer: \"Upptime pioneered a clever GitOps approach using GitHub Actions and Pages, but its last major release was in 2020. Because it is no longer actively maintained, we recommend choosing an actively developed alternative like OpenStatus for production use.\"\n---\n\n# The State of Open-Source Status Pages in 2026: What Are Your Best Options?\n\nIf there’s one thing we’ve learned in DevRel and platform engineering, it’s that **downtime is inevitable, but poor communication is a choice.** When your API goes down or latency spikes, your users shouldn't have to guess what's happening. A reliable, transparent status page is your frontline for maintaining developer trust.\n\nBut building a status page from scratch in 2026 is usually a waste of valuable engineering cycles. Instead, the open-source community has provided several excellent tools to do the heavy lifting for us.\n\nwe’ve reviewed the current landscape of open-source status pages. Here is a breakdown of the top contenders this year, where they shine, and where they fall short.\n\n---\n\n### 1. Openstatus: The Modern MVP\n\n**Best for:** Teams looking for a modern, actively maintained, and feature-rich solution.\n\nIf you want a tool that feels like it belongs in a modern 2026 stack, **openstatus** is currently leading the pack. It is fully open-source and provides the flexibility of both cloud-hosted and self-hosted deployments.\n\nThe maintainers are highly active, and it goes beyond just displaying a static page. It includes built-in uptime monitoring and a very handy Slack agent, meaning your on-call team and your users stay in sync automatically.\n\n- **Pros:** Actively maintained, hosted/self-hosted flexibility, built-in monitoring, seamless Slack integration.\n- **Cons:** None that immediately stand out for standard use cases.\n\n### 2. Cachet: The Sleeping Giant\n\n**Best for:** Existing enterprise users willing to wait for the next generation.\n\nCachet has historically been a massive name in the open-source status page space. It remains actively maintained by a dedicated core team, but it is currently in a transitional phase.\n\nThe last official release was back in 2023, which might raise eyebrows for new adopters. However, this is because the team is heads-down rebuilding the platform from the ground up for **Version 3.0**. If you are looking for a mature tool with a rich history, keep an eye on Cachet—but you might want to hold off on a fresh deployment until 3.0 officially drops.\n\n- **Pros:** Proven track record, great community, highly anticipated v3.0 on the horizon.\n- **Cons:** Stale current release (last updated 2023).\n\n### 3. Vigil: The Lightweight Rust Option\n\n**Best for:** Infrastructure teams managing microservices who love self-hosting.\n\nWritten entirely in Rust, **Vigil** is a specialized, high-performance status page designed specifically for microservice architectures.\n\nIt handles its own monitoring and is incredibly lightweight, but it comes with a catch: it is strictly self-hosted. There is no managed cloud option here. If you have the infrastructure chops and want a blazingly fast, standalone binary monitoring your nodes, Vigil is a fantastic choice.\n\n- **Pros:** Written in Rust (fast, memory-safe), built for microservices, includes monitoring.\n- **Cons:** Self-hosted only, requires infrastructure overhead to manage.\n\n### 4. Statping-ng: Function Over Form\n\n**Best for:** Hackers and sysadmins who just need it to work.\n\n**Statping.ng** is an all-in-one solution that provides both underlying monitoring and a public-facing status page. It is robust and gets the job done without much fuss.\n\nHowever, in a world where developer experience (DX) and user interfaces matter more than ever, Statping-ng shows its age. The UI feels noticeably outdated compared to newer players like openstatus. If you don't care about aesthetics and just want raw functionality, it’s a viable pick.\n\n- **Pros:** All-in-one monitoring and status reporting, reliable.\n- **Cons:** The UI/UX feels stuck in the past.\n\n### 5. Upptime: The GitOps Pioneer\n\n**Best for:** Legacy projects looking for a zero-server setup (with caveats).\n\nUpptime brought a brilliant concept to the table: using GitHub Actions and GitHub Pages to run an entirely free, serverless status page.\n\nUnfortunately, as of 2026, the project seems to have stalled. With its last major release way back in October 2020, it is no longer actively maintained. While it’s a cool showcase of GitOps, relying on unmaintained software for your incident communication is a risky move I can't confidently recommend for new production environments.\n\n- **Pros:** Completely free, brilliant use of GitHub Actions/Pages.\n- **Cons:** Abandoned (no updates since 2020), not actively maintained.\n\n---\n\n### Summary Comparison\n\n| Tool            | Active in 2026? | Deployment           | Built-in Monitoring | Standout Feature                  |\n| :-------------- | :-------------- | :------------------- | :------------------ | :-------------------------------- |\n| **Openstatus**  | ✅ Yes          | Hosted & Self-Hosted | ✅ Yes              | Slack Agent integration           |\n| **Vigil**       | ✅ Yes          | Self-Hosted Only     | ✅ Yes              | Rust-based microservice focus     |\n| **Cachet**      | ⚠️ Rebuilding   | Self-Hosted          | ❌ No               | Massive community (awaiting v3.0) |\n| **Statping.ng** | ✅ Yes          | Self-Hosted          | ✅ Yes              | All-in-one, but outdated UI       |\n| **Upptime**     | ❌ No           | GitHub Actions       | ✅ Yes              | Zero-server GitOps                |\n\n### The Verdict\n\nIf you are starting a new project or migrating an old status page today, **openstatus** is the clear winner. Its modern architecture, active maintenance, and built-in integrations make it the easiest way to keep your users informed while your engineering team focuses on fixing the actual outages.\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/boring-is-better-for-status-pages.mdx",
    "content": "---\ntitle: \"Status Pages Should Be Boring\"\ndescription: \"Your status page has one job: tell users if your service is down. Learn why the best status pages prioritize speed, reliability, and simplicity over flashy design and modern frameworks.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-02-07\"\ncategory: \"education\"\nfaq:\n  - question: \"Why should status pages be static instead of using JavaScript frameworks?\"\n    answer: \"JavaScript frameworks add load, parse, and execution time before rendering meaningful content. During an incident when users' connectivity might be degraded, this delay destroys trust. Static HTML renders instantly and works even when JavaScript fails, which is exactly what you need during a crisis.\"\n  - question: \"What's wrong with using animated status indicators?\"\n    answer: \"Animated status indicators that require JavaScript to show if you're down create a critical failure mode - if the script fails, the page shows nothing, which is indistinguishable from being completely offline. Status information should be visible immediately in plain HTML.\"\n  - question: \"Should I A/B test my status page?\"\n    answer: \"No. During an incident, different users seeing different versions of your status page creates confusion and erodes trust. When customers compare notes and realize they're seeing conflicting information, you've turned an operational problem into a credibility crisis.\"\n  - question: \"Why avoid external dependencies on status pages?\"\n    answer: \"External dependencies like graphing services or CDNs create additional failure modes. When those services fail - and they will - your status page loses functionality at the exact moment it matters most. Plain HTML and CSS have decades of battle-testing with no unexpected edge cases.\"\n  - question: \"What makes a status page accessible during incidents?\"\n    answer: \"A good status page works in grayscale for colorblind users, supports keyboard navigation and screen readers, loads instantly, provides RSS/Atom feeds for automated monitoring, and displays text-based updates that remain readable even if CSS fails. Accessibility isn't optional during high-stress moments.\"\n---\n\nYour status page is not a portfolio piece. It's not the place to showcase your design skills or demonstrate your mastery of modern web frameworks. It has one job: tell users if your service is down.\n\nWhen your API returns 500s at 2 AM and customers are frantically checking if the problem is on their end, they don't care about your animated gradients. They want an answer, and they want it fast.\n\nA status page that looks impressive but loads slowly doesn't just fail - it actively makes things worse. If users can't load your status page during an incident, they assume everything is broken, including your ability to communicate. Trust evaporates.\n\nThe best status pages are boring. Static. Fast. Reliable. That's not a compromise - that's the entire point.\n\n## Every Millisecond Counts\n\nUsers check status pages during incidents. Their own connectivity might be degraded. Their patience is thin. Every moment waiting for JavaScript to bootstrap feels like an eternity when production is on fire.\n\nStatic HTML renders instantly. JavaScript frameworks - no matter how elegant your component architecture - add delay. They need to load, parse, and execute before rendering anything meaningful. During a crisis, that delay destroys trust.\n\nYour status page should feel instant. Use zero dependencies beyond basic HTML and CSS. If your status page needs its own deployment pipeline, its own monitoring, or requires emergency debugging during outages, you've fundamentally misunderstood what status infrastructure is supposed to do.\n\nYour status page should be the one thing that works when everything else is broken.\n\n## Show, Don't Hide\n\nStressed users have zero cognitive bandwidth. They're troubleshooting, fielding messages from their own customers, and trying to figure out if they need to wake up their team. The status needs to be obvious: red, yellow, or green.\n\nHover states and tooltips hide information. Every extra click or hover is friction. Show everything upfront. If you want to use hover states, use them to add context - like timezone conversion - not to gate essential information behind interactions.\n\nSupport timezones so users can correlate your incident timeline with their own logs. Provide shareable links to specific incidents so support teams can send a direct reference instead of saying \"check the status page.\" Make text copy-pasteable so they can quote your updates in their own communications.\n\nMarkdown is fine for emphasis or linking to third-party status pages when upstream issues affect you. It's not an excuse for fancy formatting that adds visual complexity without adding clarity.\n\n## Don't Create New Failure Modes\n\nIf your status page goes down when your service goes down, you've made the situation exponentially worse. Customers can't confirm there's an incident, so they flood support channels, blast social media, and lose faith in your operational maturity.\n\nPlain HTML, CSS, and HTTP have decades of battle-testing. No edge cases you haven't discovered yet. No subtle breaking changes in patch releases. They just work.\n\nStatic sites on separate infrastructure are nearly indestructible. They don't share resources with your main app. They don't fail when your database crashes or your CDN has an outage. They're there when you need them most - which is when everything else isn't.\n\n## Good Status Pages Are Predictable\n\nClear color coding works in grayscale because colorblind users exist and clarity shouldn't depend on perfect color perception.\n\nAccessibility isn't a checkbox - keyboard navigation, screen reader support, semantic HTML. Your status page needs to work for everyone, especially during high-stress moments when people are least equipped to deal with bad UX.\n\nLoad times that feel instant. RSS or Atom feeds so customers can integrate status into their own systems - meaning their monitoring can automatically detect your incidents before their users notice. Historical uptime in simple tables, not interactive charts that fail when the graphing service is unreachable.\n\nText-based incident updates that load instantly and stay readable even if CSS fails.\n\n## What Not to Do\n\nAnimated status indicators that require JavaScript to show if you're down. If the script fails, the page shows nothing - indistinguishable from being completely offline.\n\nComplex modals and overlays hiding incident details. Users need to read, copy, and share that information. Every layer of interaction is a barrier.\n\nFancy graphs dependent on external services. When those fail - and they will - your status page loses functionality at the exact moment it matters most.\n\nAB testing your status page. During an incident, different users seeing different versions of your status page creates confusion and erodes trust. When customers compare notes and realize they're seeing conflicting information, you've turned an operational problem into a credibility crisis. Your status page has one job - communicate clearly. There's no conversion rate to optimize here. \n\n## Boring Wins\n\nBoring is reliable. Reliable builds trust. Trust is what you need during an outage.\n\nThe best status page works when everything else is broken. It loads instantly, communicates clearly, and never becomes part of the problem it's meant to solve.\n\nSimple. Fast. Functional.\n\nThat's not settling for less. That's understanding what matters.\n\n---\n\n**Openstatus gets this.** We build status infrastructure that's bulletproof, not flashy - because when your service goes down, the last thing you need is your status page joining it.\n\nMigrating from your current status page? [Contact us](mailto:ping@openstatus.dev), we'll help you move over.\n\n---\n\nStart free. No credit card required. Set up your first status page in under 5 minutes.\n\n<ButtonLink href=\"https://app.openstatus.dev\">Try openstatus free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/database-performance-degradation.mdx",
    "content": "---\ntitle: \"Database Performance Degradation Template\"\ndescription: \"Technical template for communicating database latency and performance issues to your engineering-focused audience.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-01-19\"\ncategory: \"template\"\nfaq:\n  - question: \"How do I decide between technical and user-friendly messaging?\"\n    answer: \"Match your communication to your audience. For developer-focused products, include technical details like p95 latency, query times, and specific database components. For general users, focus on impact: 'slower response times' instead of 'elevated connection pool exhaustion.'\"\n  - question: \"Should I share the root cause of database performance issues?\"\n    answer: \"Yes, once resolved. Technical audiences appreciate transparency and can learn from your incidents. Share root cause, mitigation steps taken, and preventative measures. This builds trust and demonstrates engineering maturity.\"\n  - question: \"When should I trigger a database performance incident notification?\"\n    answer: \"Trigger notifications when p95 latency exceeds 2x normal, error rates exceed 1%, connection timeouts occur, or when user reports indicate widespread slowness. Set up automated monitoring alerts to catch these thresholds early.\"\n---\n\nUse this template when experiencing database performance issues, elevated latency, or query slowdowns. Ideal for engineering teams and technical stakeholders.\n\n## When to Use This Template\n\n- Elevated database latency\n- Slow query performance\n- Connection pool exhaustion\n- Database infrastructure issues\n\n## Template Messages\n\n### Investigating\n\nWe are experiencing elevated database latency affecting some features. Users may experience slower response times.\n\nOur database team is actively investigating and working to restore normal performance levels.\n\n### Identified\n\nWe have identified the root cause. Our team is implementing a fix.\n\n### Monitoring\n\nDatabase performance improvements have been deployed. Latency is returning to normal levels. We are continuing to monitor.\n\n### Resolved\n\nDatabase performance has been fully restored. All queries are executing at normal speeds.\n\n## Real-World Examples\n\n### GitHub: \"Infrastructure update to data stores\"\n\n**Context**: Data store infrastructure changes\n**Duration**: ~1.5 hours\n**Impact**: 1.8% combined failure rate, peaking at 10%\n\n**What they did well**:\n- Quantified impact with specific percentages\n- Identified multiple affected services\n- Committed to post-incident RCA (Root Cause Analysis)\n- Provided precise UTC timestamps\n\n**Sample messaging**: \"Infrastructure update to data stores caused 1.8% combined failure rate, peaking at 10% across multiple services Jan 15, 16:40-18:20 UTC.\"\n\n### Fly.io: \"Network instability in IAD region\"\n\n**Context**: Regional infrastructure issue\n**Approach**: Geographic specificity\n\n**What they did well**:\n- Identified specific region (IAD = Ashburn, Virginia)\n- Focused on infrastructure layer\n- Technical but clear naming\n\nThis demonstrates the value of being specific about location when database issues are region-specific.\n\n## Tips for Technical Communication\n\n1. **Include specific metrics** when you have them (p95 latency, error rates, query times)\n2. **Name the layer** - application, database, network, storage\n3. **Be technical if your audience is technical** - don't oversimplify for engineers\n4. **Quantify impact** - \"affecting 2% of queries\" is better than \"some users\"\n5. **Share root cause** when resolved - technical teams appreciate learning\n\n## Customization Ideas\n\nFor **developer audiences**:\n```\nWe are experiencing elevated p95 latency (2.5s vs normal 150ms)\non write operations to our primary PostgreSQL cluster.\n\nRoot cause: Connection pool exhaustion due to long-running\ntransactions from the reporting service.\n\nMitigation: Killed long-running queries, increased pool size\nfrom 100 to 150 connections, added query timeouts.\n```\n\nFor **general users**:\n```\nWe are experiencing slower than normal response times for some\nfeatures. Our team is working to resolve this quickly.\n```\n\n## Warning Signs to Monitor\n\nWatch for these indicators that might trigger using this template:\n- P95 latency > 2x normal\n- Error rate > 1%\n- Connection timeouts\n- User reports of slowness\n- Database monitoring alerts\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/deployment-rollback.mdx",
    "content": "---\ntitle: \"Deployment Rollback Template\"\ndescription: \"Template for communicating deployment failures and rollback procedures during service disruptions.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-01-19\"\ncategory: \"template\"\nhowto:\n  totalTime: \"PT1H\"\n  steps:\n    - name: \"Detect and acknowledge the deployment issue\"\n      text: \"Identify that a recent deployment is causing problems through monitoring alerts or user reports. Immediately acknowledge the issue and announce rollback decision to users.\"\n      url: \"#investigating\"\n    - name: \"Execute the rollback\"\n      text: \"Roll back to the previous stable version while providing status updates. Monitor error rates and system health during the rollback process.\"\n      url: \"#identified\"\n    - name: \"Monitor post-rollback stability\"\n      text: \"Verify that the rollback completed successfully and all services have returned to normal operation. Continue monitoring for any lingering issues.\"\n      url: \"#monitoring\"\n    - name: \"Confirm resolution and communicate lessons\"\n      text: \"Send final confirmation that services are restored. Schedule post-mortem to identify root cause and document preventative measures for future deployments.\"\n      url: \"#resolved\"\nfaq:\n  - question: \"How quickly should I decide to roll back a deployment?\"\n    answer: \"If error rates exceed 5% or critical functionality is broken, initiate rollback immediately. Don't wait to debug in production. Roll back first, then investigate the issue in a safe environment. Time is critical - every minute of impact affects user trust.\"\n  - question: \"Should I complete the rollback before communicating, or communicate during?\"\n    answer: \"Communicate the rollback decision immediately, then provide updates during the process. Users appreciate knowing you're taking action. Example: 'We've initiated a rollback and expect completion in 10 minutes' is better than waiting until it's done.\"\n  - question: \"What if the rollback doesn't fix the issue?\"\n    answer: \"This suggests the problem isn't with the recent deployment. Pivot your communication to general incident response, investigate the root cause, and consider whether you need to roll back further or take other remediation steps. Update users with revised information.\"\n  - question: \"Do I need a post-mortem after every rollback?\"\n    answer: \"Yes. Even quick rollbacks deserve analysis. Document what went wrong, why it wasn't caught in testing, and what process changes will prevent recurrence. Share key findings with users if appropriate to show continuous improvement.\"\n---\n\nUse this template when a deployment causes issues and requires rolling back to a previous version. Communicates the issue, mitigation, and resolution clearly.\n\n## When to Use This Template\n\n- Deployment causes service errors\n- New release breaking existing functionality\n- Configuration changes causing issues\n- Need to roll back to previous version\n\n## Template Messages\n\n### Investigating\n\nWe encountered an issue during a scheduled deployment and have initiated a rollback to restore service stability.\n\nWe apologize for the disruption and are working to complete the rollback as quickly as possible.\n\n### Identified\n\nThe deployment issue has been identified. We are completing the rollback to the previous stable version.\n\n### Monitoring\n\nThe rollback has been completed successfully. We are monitoring system stability to ensure all services are functioning normally.\n\n### Resolved\n\nAll services have been restored to normal operation. The deployment issue has been resolved and we will investigate the root cause.\n\n## Real-World Examples\n\n### Vercel: \"Elevated error rates across new deployments\"\n\n**Context**: Build system changes affecting deployments\n**Duration**: ~32 minutes\n**Impact**: New deployments and builds failing\n\n**Communication approach**:\n- Clear about what was affected (\"new deployments\")\n- Specific about the problem (\"elevated error rates\")\n- Quick resolution time communicated\n\n### GitHub: \"Configuration error during a model update\"\n\n**Context**: Copilot model deployment issue\n**Duration**: ~1.5 hours\n**Impact**: 18% error rate on average, spiking to 100%\n\n**What worked**:\n- Transparent about the cause (\"configuration error\")\n- Quantified impact (error rates)\n- Showed progression: investigation → rollback → recovery monitoring\n- Committed to improvement\n\n**Sample update progression**:\n1. \"We're investigating reports of Copilot failures\"\n2. \"Configuration error identified during model update. Rolling back.\"\n3. \"Rollback complete. Monitoring for full recovery.\"\n4. \"Fully resolved. Error rates back to normal. RCA in progress.\"\n\n## Best Practices\n\n### During Rollback\n\n**Do**:\n- ✅ Announce the rollback immediately\n- ✅ Set expectations on timeline if known\n- ✅ Acknowledge the user impact\n- ✅ Monitor closely during rollback\n\n**Don't**:\n- ❌ Wait until rollback is complete to communicate\n- ❌ Over-promise on fix timing\n- ❌ Skip the post-incident analysis\n\n### After Resolution\n\nAlways include:\n1. Confirmation that rollback succeeded\n2. Current system status\n3. Next steps (investigation, new deployment plan)\n4. Preventative measures (if ready to share)\n\n## Communication Patterns\n\n### For User-Facing Services\n\n```\nWe've rolled back a deployment that was causing issues with [feature].\nService is now restored. We're investigating to prevent this in the future.\n```\n\n### For API Services\n\n```\nA recent deployment caused elevated error rates in our API.\nWe've rolled back to the previous version and error rates\nhave returned to normal. All endpoints are functioning correctly.\n```\n\n### For Internal Tools\n\n```\nDeployment to production caused issues with [system]. Rolled back\nto previous version. Services restored. Post-mortem scheduled for\ntomorrow to identify root cause and improve deployment process.\n```\n\n## Preventative Context\n\nAfter resolution, consider adding context about prevention:\n\n> \"We're improving our deployment process to catch issues like this\n> before they reach production. This includes enhanced pre-deployment\n> testing and gradual rollout procedures.\"\n\n## Timing Guidance\n\n- **0-5 minutes**: Initial notification of issue and rollback decision\n- **5-20 minutes**: Rollback in progress updates\n- **20-30 minutes**: Rollback complete, monitoring\n- **30-60 minutes**: Confirmed resolution\n- **24 hours later**: Root cause analysis summary (optional)\n\n## Related Templates\n\n- Use **API Service Disruption** if rollback affects external integrations\n- Use **Database Performance** if rollback impacts database operations\n- Use **Security Incident** if deployment exposed security issues\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/feature-degradation.mdx",
    "content": "---\ntitle: \"Feature Degradation Template\"\ndescription: \"Template for communicating partial service degradation where some features are impacted but the service remains available.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-01-19\"\ncategory: \"template\"\nfaq:\n  - question: \"When is something 'degraded' vs a full outage?\"\n    answer: \"Use degraded when the feature still works partially (e.g., 50% success rate, slower than normal, or intermittent failures). Use outage when the feature is completely unavailable (100% failure rate). Clear distinction helps users set expectations.\"\n  - question: \"Should I list all working features or just the broken ones?\"\n    answer: \"For major features, list both. Users need to know what still works so they can continue their workflow. For minor features, focus on what's broken and state 'all other features operating normally.' The feature impact matrix format works well for complex degradations.\"\n  - question: \"How do I communicate partial degradation percentages to users?\"\n    answer: \"Be specific: '50% success rate - retry usually works' or '10% error rate - most requests succeeding.' Avoid vague terms like 'some' or 'many.' Percentages help users assess their likelihood of being affected and whether retrying makes sense.\"\n  - question: \"What if I don't know which feature is causing issues?\"\n    answer: \"Start with what you know: 'We're investigating reports of errors. Some users may be affected.' Then update as you learn more: 'We've confirmed the issue is with [specific feature].' Narrow the scope as quickly as possible to reduce user uncertainty.\"\n---\n\nUse this template when specific features are degraded or unavailable, but the core service remains operational. Helps users understand what's working and what isn't.\n\n## When to Use This Template\n\n- Specific feature experiencing errors\n- Elevated latency for particular operations\n- Partial functionality unavailable\n- Non-critical systems impaired\n\n## Template Messages\n\n### Investigating\n\nWe are experiencing issues with [Feature Name]. The core service remains available, but this specific feature may not work as expected.\n\nOur team is actively investigating and working to restore full functionality.\n\n### Identified\n\nWe have identified the cause of the [Feature Name] degradation. Our team is implementing a fix.\n\n### Monitoring\n\n[Feature Name] has been restored. We are monitoring to ensure stable operation.\n\n### Resolved\n\n[Feature Name] is now fully operational. All features are working normally.\n\n## Real-World Examples\n\n### Notion: \"Operations such as duplicate a page or move a block\"\n\n**Context**: Specific operations failing while rest of service worked\n**Duration**: ~4 hours\n**Impact**: Users could read/edit but not duplicate or move content\n\n**What worked**:\n- Specific about which operations were affected\n- Clear that core functionality (reading, editing) still worked\n- Precise timestamps in user's timezone (PST)\n\n**Sample progression**:\n```\n12:10pm PST - Investigating\n\"We are experiencing an incident that affects operations such as\nduplicate a page or move a block.\"\n\n2:30pm PST - Identified\n\"We've identified the issue and are implementing a fix. Most\noperations continue to work normally.\"\n\n4:09pm PST - Resolved\n\"All operations have been restored. The issue has been fully resolved.\"\n```\n\n### Notion: \"Exporting pages remains in a loading state\"\n\n**Context**: Export feature stuck, but rest of service worked\n**Approach**: Clear about single feature impact\n\n**Why it worked**:\n- Users knew exactly what was broken\n- Clear that it wasn't a full outage\n- No operational downtime reported for core features\n\n### GitHub: \"Some models missing in Copilot\"\n\n**Context**: Specific AI models unavailable\n**Duration**: ~2.5 hours\n**Impact**: Claude Opus 4.5 and GPT-5.2 inaccessible; other models worked\n\n**Communication breakdown**:\n```\nInvestigating:\n\"Some users unable to access premium AI models in GitHub Copilot\"\n\nIdentified:\n\"Misconfiguration marking Claude Opus 4.5 and GPT-5.2 as inaccessible.\nOther models functioning normally. Deploying configuration fix.\"\n\nResolved:\n\"All models now accessible. Configuration has been corrected and\nvalidated across all regions.\"\n```\n\n**Why it worked**:\n- Specific model names (users could check if they were affected)\n- Clear that other models still worked\n- Technical but not overly complex\n\n## Clear Scope Communication\n\n### ✅ Good Examples\n\n**Specific and clear**:\n```\nThe file upload feature is currently unavailable. All other features,\nincluding viewing, editing, and sharing, are working normally.\n```\n\n**With workaround**:\n```\nExport functionality is temporarily degraded. Exports may take 2-3x\nlonger than usual. Viewing and editing are unaffected.\n\nWorkaround: For faster exports, try exporting smaller sections\nindividually.\n```\n\n**With context**:\n```\nSearch is experiencing elevated latency (5-10 seconds vs normal <1s).\nWe're working on a fix. All other features operating normally.\n```\n\n### ❌ Vague Examples\n\n```\n\"Some features might not work\"\n\"Experiencing issues\"\n\"Partial service degradation\"\n\"Some users affected\"\n```\n\n## Feature Impact Matrix\n\nHelp users quickly understand what works and what doesn't:\n\n### Example 1: Export Feature Down\n```\nCurrent Status:\n\nWorking:\n✓ View documents\n✓ Edit documents\n✓ Share with team\n✓ Comments and collaboration\n✓ Search\n\nNot Working:\n✗ PDF export\n✗ CSV download\n✗ Bulk export\n\nWe're working to restore export functionality.\n```\n\n### Example 2: Upload Feature Degraded\n```\nCurrent Status:\n\nWorking Normally:\n✓ View files\n✓ Download files\n✓ Share links\n✓ Delete files\n\nDegraded:\n⚠️ File uploads (50% success rate, retrying usually works)\n⚠️ Drag-and-drop (use upload button instead)\n\nWe're investigating the upload issues.\n```\n\n## Communication by Feature Type\n\n### Core Features Degraded\n\nWhen important features fail, be very clear about impact:\n\n```\n🔴 Payment Processing Degraded\n\nImpact: High - payments may fail or experience delays\n\nStatus: We're experiencing elevated error rates (10%) on payment\nprocessing. Most transactions are succeeding, but some may fail.\n\nAction: If your payment fails, please wait 5 minutes and retry.\nWe're working on a fix.\n\nOther features: Account management, viewing history, and settings\nall working normally.\n```\n\n### Secondary Features Down\n\nWhen nice-to-have features fail:\n\n```\n🟡 Analytics Dashboard Unavailable\n\nImpact: Low - core product features unaffected\n\nStatus: The analytics dashboard is temporarily unavailable due to\na database issue. All product features continue working normally.\n\nYour data is safe and being collected. Analytics will be updated\nonce the dashboard is restored.\n```\n\n### Batch/Background Features\n\nWhen background processes are affected:\n\n```\n⚠️ Report Generation Delayed\n\nImpact: Reports scheduled in the last hour will be delayed.\n\nStatus: The background job processor is experiencing issues. Reports\nwill still generate, but may take 2-4 hours instead of the usual\n15 minutes.\n\nIf you need a report urgently, please contact support@company.com\nand we can prioritize it.\n```\n\n## User Action Guidance\n\n### No Action Needed\n```\nThe issue is on our end. No action is required from you. We'll\nupdate this status when resolved.\n```\n\n### Retry Recommended\n```\nIf you encounter an error, please wait 30 seconds and try again.\nMost operations succeed on the second attempt.\n```\n\n### Workaround Available\n```\nWhile we work on a fix, you can use this workaround:\n1. Go to Settings > Export\n2. Select \"Legacy Export Mode\"\n3. Your export should complete successfully\n\nWe'll notify you when the standard export is restored.\n```\n\n### Avoid Feature Temporarily\n```\nWe recommend avoiding [feature] until this is resolved to prevent\ndata sync issues. We'll send an all-clear when it's safe to use again.\n```\n\n## Timing and SLA Context\n\n### When degradation exceeds SLA:\n\n```\nUpdate: This degradation has exceeded our 99.9% uptime SLA for the\nmonth. Affected customers will receive automatic service credits as\nper our SLA terms. No action needed from you.\n```\n\n### When approaching SLA:\n\n```\nWe're aware this degradation is approaching our SLA threshold. We're\ntreating this as high priority and have additional engineers engaged.\n```\n\n## Progressive Updates\n\n### Initial (Minute 0)\n```\nWe're investigating reports of [feature] not working correctly.\nSome users may experience errors. Core functionality unaffected.\n```\n\n### Update 1 (Minute 15)\n```\nWe've confirmed [feature] is experiencing issues. Approximately\n20% of requests are failing. We're investigating the cause.\n```\n\n### Update 2 (Minute 30)\n```\nRoot cause identified: database connection timeout on [specific service].\nImplementing a fix now. Estimated resolution: 15 minutes.\n```\n\n### Update 3 (Minute 45)\n```\nFix deployed. [Feature] error rate has dropped from 20% to 2%.\nMonitoring for full recovery.\n```\n\n### Final (Minute 60)\n```\n[Feature] is fully restored. Error rates back to normal (< 0.1%).\nNo user action required. Thank you for your patience.\n```\n\n## Partial vs. Complete Degradation\n\n### Partial (50% working)\n```\n[Feature] is partially degraded. About half of requests are succeeding.\nIf you receive an error, try again in a few seconds - you'll likely\nsucceed on the second or third attempt.\n```\n\n### Severe (90% failing)\n```\n[Feature] is severely degraded with most requests failing. We recommend\nwaiting until this is resolved rather than retrying repeatedly.\n\nETA for fix: 30 minutes\n```\n\n### Complete (100% down)\n```\n[Feature] is currently unavailable. We've identified the issue and\nare working on a fix.\n\nEstimated restoration: 15 minutes\nWe'll notify you as soon as it's back.\n```\n\n## Post-Resolution Analysis\n\nAfter resolving, explain what happened:\n\n```\n[Feature] has been fully restored.\n\nWhat happened: A configuration change caused connection timeouts\nto the [service] backend.\n\nResolution: We rolled back the configuration change and implemented\nadditional connection monitoring.\n\nPrevention: We're adding pre-deployment validation for configuration\nchanges to catch issues like this before they reach production.\n\nThank you for your patience while we resolved this issue.\n```\n\n## Related Templates\n\n- Use **API Service Disruption** if the degraded feature is API-related\n- Use **Database Performance** if degradation is caused by database issues\n- Use **Network Connectivity** if some regions can't access the feature\n- Use **Deployment Rollback** if a recent deployment caused the degradation\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/http-headers.mdx",
    "content": "---\ntitle: \"HTTP Headers Every Developer Should Know\"\ndescription: \"A practical reference for common HTTP request and response headers, security headers, and the cloud provider-specific headers you'll encounter when monitoring APIs.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-02-22\"\ncategory: \"education\"\nfaq:\n  - question: \"What are HTTP headers?\"\n    answer: \"HTTP headers are metadata key-value pairs sent alongside HTTP requests and responses. They define content types, authentication, caching behavior, security policies, and more.\"\n  - question: \"What is Cache-Control?\"\n    answer: \"Cache-Control is an HTTP response header that tells browsers and CDNs how long to cache a response. Directives like max-age, no-cache, and no-store control caching behavior.\"\n  - question: \"What's the difference between X-Forwarded-For and X-Real-IP?\"\n    answer: \"X-Forwarded-For contains a chain of IPs added by each proxy. X-Real-IP is set by the first proxy and reflects just the original client IP.\"\n  - question: \"What is a CF-Ray header?\"\n    answer: \"CF-Ray is a Cloudflare-specific header that uniquely identifies a request. It's the first thing Cloudflare support will ask for when debugging an issue.\"\n---\n\nHTTP headers are key-value pairs sent alongside requests and responses. They carry metadata about authentication, content format, caching, and security—and are the first place to look when something behaves unexpectedly.\n\n```\nHeader-Name: header-value\n```\n\nHeader names are case-insensitive; values are not.\n\n---\n\n## Request Headers\n\n**`Accept`**\nWhich content types the client can handle. The `q` parameter sets preference weight.\n```\nAccept: application/json, text/html;q=0.9\n```\n\n**`Accept-Encoding`**\nCompression algorithms the client supports. Servers compress responses accordingly.\n```\nAccept-Encoding: gzip, br, deflate\n```\n\n**`Accept-Language`**\nThe client's preferred language for response content.\n```\nAccept-Language: en-US,en;q=0.9\n```\n\n**`Authorization`**\nCredentials for accessing protected resources. Common schemes: `Bearer`, `Basic`, `ApiKey`.\n```\nAuthorization: Bearer <token>\n```\n\n**`Content-Type`**\nThe format of the request body. Required for `POST`/`PUT` requests with a body.\n```\nContent-Type: application/json\n```\n\n**`Cookie`**\nPreviously stored cookies sent back to the server for session or user identification.\n```\nCookie: sessionId=abc123; theme=dark\n```\n\n**`If-Modified-Since`**\nConditional request—returns the resource only if it changed after the given date. Used with `Last-Modified` for cache revalidation.\n```\nIf-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT\n```\n\n**`Referer`**\nThe URL of the page that linked to the current resource. Note the historical misspelling.\n```\nReferer: https://example.com/previous-page\n```\n\n**`User-Agent`**\nIdentifies the client—browser, SDK, or monitoring tool.\n```\nUser-Agent: OpenStatus-Monitor/1.0\n```\n\n---\n\n## Response Headers\n\n**`Cache-Control`**\nHow and how long responses can be cached. Key directives:\n- `max-age=3600` — cache for 1 hour\n- `s-maxage=60` — CDN-specific override\n- `no-cache` — revalidate before serving cached content\n- `no-store` — never cache\n\n**`Content-Length`**\nThe response body size in bytes.\n\n**`ETag`**\nA fingerprint for a specific version of a resource. Clients echo it back in `If-None-Match`; unchanged resources return `304 Not Modified` with no body.\n\n**`Location`**\nTarget URL for redirect responses (`3xx`).\n```\nLocation: https://api.example.com/users/123\n```\n\n**`Server`**\nThe server software that generated the response. Often worth stripping in production to reduce information exposure.\n```\nServer: nginx/1.21.0\n```\n\n**`Set-Cookie`**\nInstructs the client to store a cookie with given attributes.\n```\nSet-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict\n```\n\n---\n\n## Security Headers\n\n**`Content-Security-Policy`**\nControls which resources the browser can load. Primary defense against XSS.\n```\nContent-Security-Policy: default-src 'self'; script-src 'self'\n```\n\n**`Strict-Transport-Security`**\nForces HTTPS for future requests. `includeSubDomains` extends coverage.\n```\nStrict-Transport-Security: max-age=31536000; includeSubDomains\n```\n\n**`X-Content-Type-Options`**\nPrevents browsers from MIME-sniffing the declared content type.\n```\nX-Content-Type-Options: nosniff\n```\n\n**`X-Frame-Options`**\nProtects against clickjacking by controlling iframe embedding.\n```\nX-Frame-Options: DENY\n```\n\n---\n\n## Proxy and Infrastructure Headers\n\nSet by reverse proxies, load balancers, and CDNs—not your origin server.\n\n| Header | Purpose |\n|---|---|\n| `X-Forwarded-For` | Chain of IPs through each proxy |\n| `X-Forwarded-Host` | Original `Host` before proxy rewrites |\n| `X-Forwarded-Proto` | Original protocol (`http` or `https`) |\n| `X-Real-IP` | Original client IP, set by the first proxy |\n| `X-Request-ID` | Unique identifier for tracing a request across services |\n| `X-RateLimit-Limit` | Maximum requests allowed in a window |\n| `X-RateLimit-Remaining` | Requests remaining in the current window |\n| `X-RateLimit-Reset` | Unix timestamp when the rate limit resets |\n\n---\n\n## Cloud Provider Headers\n\nEach provider injects its own headers. Recognizing them tells you which layer handled the request—and where to look when something fails.\n\n### Cloudflare\n\n| Header | Purpose |\n|---|---|\n| `CF-Ray` | Unique request ID—always include this in Cloudflare support tickets |\n| `CF-Cache-Status` | `HIT`, `MISS`, `EXPIRED`, or `BYPASS` |\n| `CF-Connecting-IP` | Original client IP—prefer this over `X-Forwarded-For` behind Cloudflare |\n| `CF-IPCountry` | Two-letter country code of the client |\n\n### Vercel\n\n| Header | Purpose |\n|---|---|\n| `X-Vercel-Id` | Unique request ID, includes region prefix (e.g. `cdg1::`) |\n| `X-Vercel-Cache` | `HIT`, `MISS`, or `STALE` from Vercel's edge cache |\n\n### Fly.io\n\n| Header | Purpose |\n|---|---|\n| `Fly-Request-Id` | Unique request ID for tracing |\n| `Fly-Region` | The region that handled the request (e.g. `ams`, `lax`) |\n\n### Railway / Koyeb\n\nBoth proxy requests through standard infrastructure. Expect `X-Forwarded-*` headers; request IDs may appear as `X-Request-Id` or `X-Correlation-Id` depending on your app framework.\n\n---\n\n## Why This Matters for Monitoring\n\nResponse headers reveal more than status codes alone:\n\n- `CF-Cache-Status: HIT` means you're hitting the cache, not your origin\n- `X-Vercel-Id` with an unexpected region prefix can reveal routing issues\n- Missing `Strict-Transport-Security` on production is worth alerting on\n- `Cache-Control: no-store` on a high-traffic route means zero caching is helping you\n\n---\n\n<ButtonLink href=\"https://app.openstatus.dev\">Start monitoring your API responses</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/incident-severity-matrix.mdx",
    "content": "---\ntitle: \"Incident Severity Matrix Template\"\ndescription: \"Ready-to-use templates for classifying and communicating incidents by severity level. Covers status page message templates, postmortem requirements, and real-world examples for SEV0 through SEV3.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-02-26\"\ncategory: \"template\"\nhowto:\n  totalTime: \"PT30M\"\n  steps:\n    - name: \"Define your severity thresholds\"\n      text: \"Set measurable thresholds for each severity level based on percentage of users affected. Use the interactive builder to customize thresholds to your team's operational reality.\"\n    - name: \"Classify the incident\"\n      text: \"Apply the classification rules in order: security impact always triggers SEV0, then check user percentage against thresholds, then apply the SLA modifier if active.\"\n      url: \"#severity-matrix\"\n    - name: \"Post to your status page\"\n      text: \"Use the per-severity message templates to post a consistent public update. Commit to the update cadence for your severity level and never go more than one hour without an update on any active SEV0 or SEV1.\"\n      url: \"#status-page-message-templates\"\n    - name: \"Escalate and run the postmortem\"\n      text: \"Follow the escalation path for the classified severity level. SEV0 and SEV1 always require a postmortem. Close the loop by publishing findings and action items.\"\n      url: \"#postmortem-requirements\"\nfaq:\n  - question: \"What does SEV0 mean?\"\n    answer: \"SEV0 indicates a critical incident — typically a complete service outage or confirmed security breach that requires immediate response from senior engineering leadership. It's the highest severity level and triggers the most aggressive communication and escalation protocols.\"\n  - question: \"How many severity levels should we use?\"\n    answer: \"Most teams use 3 or 4 levels. Four levels (SEV0 through SEV3) provide enough granularity to distinguish between a full outage and a minor cosmetic bug without overcomplicating triage during a live incident.\"\n  - question: \"What is the difference between severity and priority?\"\n    answer: \"Severity measures the impact of an incident — how many users are affected and how badly. Priority reflects business urgency and resource allocation. A typo on your pricing page might be low severity but high priority if it's costing you conversions. Your severity matrix should classify based on impact alone; priority is a triage decision.\"\n  - question: \"Should security incidents always be SEV0?\"\n    answer: \"In most cases, yes. Security incidents carry outsized risk even when few users are immediately affected — the blast radius can expand quickly and the reputational impact is disproportionate. Treating all confirmed security incidents as SEV0 ensures you mobilize the right resources immediately.\"\n  - question: \"How often should we review our severity matrix?\"\n    answer: \"Review it quarterly, or after any major incident where the classification felt wrong. If your team consistently debates whether something is a SEV1 or SEV2, your thresholds probably need adjustment.\"\n  - question: \"When is a postmortem required?\"\n    answer: \"SEV0 and SEV1 incidents always require a postmortem. SEV2 requires a team-level postmortem. SEV3 postmortems are optional. The postmortem closes the loop by documenting root cause, timeline, and action items to prevent recurrence.\"\n---\n\nUse these templates when classifying and communicating production incidents. Based on real-world patterns from GitHub, Stripe, and Vercel status pages. Use the [interactive builder](/play/severity-matrix) to classify incidents and customize thresholds for your team.\n\n## When to Use This Guide\n\n- An incident has just been detected and you need to classify it fast\n- You're writing a status page update and want consistent, professional language\n- You're setting up your team's incident response process for the first time\n- A postmortem revealed your classification or communication was inconsistent\n\n## Severity Matrix\n\nCopy these tables into your runbook, wiki, or Notion page.\n\nEach row maps a severity level to its user impact threshold, required response time, and communication protocol. Classification should be deterministic — given the same inputs, every engineer on your team should reach the same row.\n\n```markdown\n| Severity | Users Affected | Security | Response Time | Status Page Label | Communication | Postmortem |\n|----------|---------------|----------|---------------|-------------------|---------------|------------|\n| 🔴 SEV0 – Critical | ≥80% OR security incident | Yes | 15 minutes | Major Outage | Immediate public update + all-hands | Required |\n| 🟠 SEV1 – High | ≥50% | No | 30 minutes | Partial Outage | Public update within 15 min | Required |\n| 🟡 SEV2 – Medium | ≥10% | No | 2 hours | Degraded Performance | Status page update + ticket | Required (team) |\n| 🟢 SEV3 – Low | <10% | No | 1 business day | Minor Issue | Internal ticket only | Optional |\n```\n\nOnce the severity is set, this table tells every engineer exactly who owns it and when to escalate. If an incident isn't resolved within the auto-escalate window, re-classify upward immediately — don't wait.\n\n```markdown\n| Severity | First Response | Update Cadence | Escalation Path | Auto-Escalate If |\n|----------|---------------|----------------|-----------------|-----------------|\n| SEV0 | 15 min | Every 15 min | VP Engineering + on-call | — |\n| SEV1 | 30 min | Every 30 min | Engineering lead | Not resolved in 2h → SEV0 review |\n| SEV2 | 2 hours | Every 2 hours | Team lead | Not resolved in 4h → SEV1 |\n| SEV3 | 1 business day | Daily | Assigned engineer | Spreads to more systems → re-classify |\n```\n\n## Status Page Message Templates\n\nNever go more than one hour without a public update on any active SEV0 or SEV1. Even if there's no new information, a \"we're still investigating\" update is better than silence.\n\n### SEV0 – Critical\n\n**Investigating**\n\n```\nWe are investigating a critical incident affecting [description of impact]. Our on-call team has been paged and we are actively working to identify the root cause. Next update in 15 minutes.\n```\n\n**Identified**\n\n```\nWe have identified the root cause as [brief description]. A fix is being deployed. We will continue to provide updates every 15 minutes until service is fully restored.\n```\n\n**Monitoring**\n\n```\nA fix has been deployed. We are monitoring for full recovery and are seeing signs of improvement. Next update in 15 minutes.\n```\n\n**Resolved**\n\n```\nThis incident has been resolved. All systems are operating normally. We apologize for the disruption. A postmortem will be published within 48 hours.\n```\n\n---\n\n### SEV1 – High\n\n**Investigating**\n\n```\nWe are investigating degraded performance affecting [description of impact]. Our team is actively working on this. Next update in 30 minutes.\n```\n\n**Identified**\n\n```\nWe have identified the cause of the degradation. A fix is in progress. Next update in 30 minutes.\n```\n\n**Monitoring**\n\n```\nA fix has been deployed and we are monitoring for full recovery. Next update in 30 minutes.\n```\n\n**Resolved**\n\n```\nService has been fully restored. We apologize for the disruption. Our engineering team will publish a postmortem with root cause and action items.\n```\n\n---\n\n### SEV2 – Medium\n\n**Investigating**\n\n```\nWe are investigating reports of degraded performance affecting a subset of users. Core functionality remains available. We will provide an update within 2 hours.\n```\n\n**Identified**\n\n```\nWe have identified the root cause. A fix is being prepared and we expect resolution within [timeframe].\n```\n\n**Monitoring**\n\n```\nA fix has been deployed. We are monitoring for full resolution.\n```\n\n**Resolved**\n\n```\nThis issue has been resolved. All systems are operating normally.\n```\n\n---\n\n### SEV3 – Low\n\nSEV3 incidents typically do not require a public status page update. If you choose to communicate externally, use these templates.\n\n**Investigating**\n\n```\nWe are aware of a minor issue affecting a small number of users. Core functionality is not impacted. No immediate action is required on your end.\n```\n\n**Identified**\n\n```\nWe have identified the cause. A fix will be deployed in the normal course of work.\n```\n\n**Monitoring**\n\n```\nA fix has been deployed and we are monitoring for full resolution.\n```\n\n**Resolved**\n\n```\nThis minor issue has been resolved.\n```\n\n## Postmortem Requirements\n\nClosing the incident loop with a postmortem prevents recurrence and builds team knowledge.\n\n| Severity | Postmortem | Who attends | Timeline |\n|----------|-----------|-------------|---------|\n| SEV0 | Required | Full engineering leadership | Within 48 hours |\n| SEV1 | Required | Engineering leads + on-call | Within 72 hours |\n| SEV2 | Required (team) | Relevant engineering team | Within 1 week |\n| SEV3 | Optional | Assigned engineer | As needed |\n\nWhen you post the \"Resolved\" update for a SEV0 or SEV1, commit to the postmortem publicly: \"A postmortem will be published at [link] within 48 hours.\" This sets expectations and holds the team accountable.\n\n## Severity vs Priority\n\nSeverity and priority are often conflated, and that conflict tends to surface during live incidents at exactly the wrong time.\n\n**Severity** is objective — it measures blast radius: how many users are affected and how badly. It does not change based on who's asking.\n\n**Priority** is contextual — it reflects how urgently the team should act given business context. The same severity level can warrant different priorities.\n\nPriority levels run P0 (drop everything) through P3 (low urgency), independent of severity:\n\n| Scenario | Severity | Priority | Why |\n|----------|----------|----------|-----|\n| API down for 90% of users | SEV0 | P0 | Total outage + business impact |\n| Button misaligned on pricing page | SEV3 | P1 | Low impact but costs conversions |\n| Slow dashboard for 5% of users | SEV2 | P2 | Limited impact, no SLA risk |\n| Auth bug during enterprise demo | SEV2 | P0 | Low blast radius, high business risk |\n\nClassify severity based on impact. Decide priority separately during triage. A higher severity level also grants broader authority to take riskier recovery actions — a SEV0 may justify taking a service down entirely to restore stability.\n\n## Roles During a Severity Incident\n\nAssign an **Incident Commander** at the start of every SEV0 or SEV1. One person owns:\n\n- The current severity classification\n- All public status page updates\n- The escalation decision\n\nThis prevents contradictory messages on your status page. If two engineers post updates independently, users see conflicting information at the worst possible time. The IC is the single source of truth for external communication until the incident is resolved.\n\nFor SEV2 and below, the assigned engineer handles communication directly without a dedicated IC.\n\n## Real-World Examples\n\n### Database cluster failover\n\n**Scenario**: Primary database fails over to replica. 60% of users experience 2 minutes of downtime. No data loss.\n**Classification**: 🟠 SEV1 – High (60% users affected)\n\nFull communication arc:\n\n**Investigating**\n```\nWe are investigating degraded performance affecting the majority of users. Our team is actively working to restore service. Next update in 30 minutes.\n```\n\n**Identified**\n```\nWe have identified the cause as a database failover. Service is being restored and we are monitoring recovery. Next update in 30 minutes.\n```\n\n**Monitoring**\n```\nThe failover has completed and service is recovering. We are monitoring to confirm full stability. Next update in 30 minutes.\n```\n\n**Resolved**\n```\nService has been fully restored. We apologize for the disruption. Our engineering team will publish a postmortem with root cause and action items.\n```\n\n---\n\n### API authentication breach\n\n**Scenario**: Unauthorized access detected on API keys. Only 5% of users affected, but security is compromised.\n**Classification**: 🔴 SEV0 – Critical (security override)\n\n```\nWe are investigating a security incident affecting API authentication. Impacted API keys have been revoked as a precaution. Our security team is actively investigating. Next update in 15 minutes.\n```\n\n---\n\n### CDN edge node degradation\n\n**Scenario**: One CDN region serving stale assets. 15% of users see outdated content. Workaround: hard refresh.\n**Classification**: 🟡 SEV2 – Medium (15% users affected)\n\n```\nWe are investigating reports of stale content being served to users in [region]. A workaround is available: clear your browser cache or perform a hard refresh. We will provide an update within 2 hours.\n```\n\n---\n\n### Payment processor timeout\n\n**Scenario**: Stripe webhook failures causing 90% checkout failures. SLA breach triggered.\n**Classification**: 🔴 SEV0 – Critical (90% users affected)\n\n```\nWe are investigating a critical issue affecting checkout. The majority of payment attempts are currently failing. Our team is working urgently with our payment provider to restore service. Next update in 15 minutes.\n```\n\n---\n\n### CSS regression on settings page\n\n**Scenario**: Button misaligned on settings page. 3% of users affected. Functional workaround exists.\n**Classification**: 🟢 SEV3 – Low (3% users affected)\n**Status page message**: No public update needed. Internal ticket created and assigned.\n\n## Tips for Using the Severity Matrix\n\n1. **During a live incident, err high.** Over-escalating one incident costs less than under-escalating and letting user impact compound. Calibrate downward in the postmortem if you overshot.\n2. **Watch for severity inflation.** If your team regularly classifies 30%+ of incidents as SEV1, you've lost the signal. Measurable thresholds prevent this from drifting over time.\n3. **Auto-escalate on duration.** If a SEV2 is not resolved within 4 hours, trigger a SEV1 review. Unresolved incidents spreading to new systems always warrant re-classification.\n4. **Use UTC in all public updates.** For distributed teams and global users, UTC timestamps remove timezone ambiguity and prevent confusion during long-running incidents.\n5. **Pin the matrix where it will be found.** In your incident Slack channel, your runbook, and your on-call documentation. A severity matrix only works if engineers can find it in the first 30 seconds of an incident.\n6. **Review thresholds quarterly.** Or after any incident where the classification was debated. If your team is consistently arguing about SEV1 vs SEV2, adjust the threshold.\n\n---\n\nUse the [Incident Severity Matrix Builder](/play/severity-matrix) to classify incidents interactively, test your thresholds against real scenarios, and customize the matrix for your team.\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/network-connectivity-issues.mdx",
    "content": "---\ntitle: \"Network Connectivity Issues Template\"\ndescription: \"Template for communicating network outages, CDN problems, and regional connectivity issues to affected users.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-01-19\"\ncategory: \"template\"\nfaq:\n  - question: \"How do I identify which region or users are affected?\"\n    answer: \"Use monitoring data from multiple geographic locations, check CDN provider dashboards, and review user reports by location. Tools like uptime monitors with multi-region checks can quickly identify geographic patterns in outages.\"\n  - question: \"Should I provide a workaround like a direct IP address?\"\n    answer: \"Yes, if safe and practical. Direct IPs, alternate endpoints, or backup URLs can help users during CDN or DNS issues. Just ensure the workaround is secure and won't create additional problems once the primary issue is resolved.\"\n  - question: \"What if my CDN provider is having issues but hasn't acknowledged it?\"\n    answer: \"You can still communicate transparently without waiting for your provider. Say 'We're experiencing connectivity issues that appear related to our CDN provider. We're investigating with them and will update you shortly.' Users care about your transparency, not your provider's timeline.\"\n---\n\nUse this template when experiencing network-related issues affecting user connectivity, including CDN problems, regional outages, or ISP issues.\n\n## When to Use This Template\n\n- Regional network outages\n- CDN degradation or failures\n- DNS resolution problems\n- ISP-specific connectivity issues\n- DDoS mitigation impacts\n\n## Template Messages\n\n### Investigating\n\nWe are aware of connectivity issues affecting some users. The issue appears to be network-related and we are investigating the root cause.\n\nOur team is working to restore connectivity as quickly as possible.\n\n### Identified\n\nWe have identified the cause of the connectivity issues. Our team is working with our network provider to implement a fix.\n\n### Monitoring\n\nNetwork connectivity has been restored. We are monitoring the situation to ensure stability across all regions.\n\n### Resolved\n\nThe connectivity issues have been fully resolved. All users should now have normal access to our services.\n\n## Real-World Examples\n\n### Fly.io: \"Network instability in IAD region\"\n\n**Context**: Regional infrastructure problem\n**Location**: IAD (Ashburn, Virginia)\n**Approach**: Geographic specificity\n\n**What they did well**:\n- Named the specific region affected\n- Used technical but clear naming (\"network instability\")\n- Focused on infrastructure layer vs vague \"connectivity problems\"\n\n**Why it worked**: Users in that region immediately knew they were affected, while users elsewhere knew they weren't experiencing issues.\n\n### GitHub: Infrastructure with Specific Impact Areas\n\n**Context**: Multi-service infrastructure issues\n**Approach**: Service-by-service breakdown\n\n**Sample structure**:\n```\nWe're experiencing connectivity issues affecting:\n- Issues and Pull Requests: 10% error rate\n- GitHub Actions: 5% job failures\n- API: Elevated latency\n\nOther services operating normally.\n```\n\n**Why it worked**: Users could quickly assess if their workflow was affected.\n\n## Geographic Specificity Best Practices\n\n### Be Specific About Location\n\n✅ Good:\n- \"Users in Europe experiencing connectivity issues\"\n- \"Network problems in AWS us-east-1 region\"\n- \"CDN degradation affecting Asia-Pacific\"\n\n❌ Vague:\n- \"Some users having problems\"\n- \"Network issues\"\n- \"Connectivity degraded\"\n\n### Example: Regional Communication\n\n**For global outage**:\n```\nWe're experiencing network connectivity issues affecting users globally.\nOur network provider is investigating. We'll provide updates every 30 minutes.\n```\n\n**For regional outage**:\n```\nWe're aware of connectivity issues affecting users in the EU region.\nThis appears to be related to our CDN provider. US and Asia-Pacific\nregions are operating normally.\n```\n\n**For ISP-specific**:\n```\nWe're receiving reports of connectivity issues from users on [ISP Name].\nWe're working with the ISP to resolve. Users on other networks should\nnot be affected.\n```\n\n## CDN-Specific Communication\n\nWhen CDN issues affect your service:\n\n### During Investigation\n```\nWe're experiencing degraded performance due to issues with our CDN provider.\nUsers may see slower load times or intermittent connectivity. We're working\nwith [CDN Provider] to resolve this quickly.\n```\n\n### When Switching to Backup\n```\nWe've temporarily switched traffic to our backup CDN to restore performance.\nYou may experience brief disruptions during this transition.\n```\n\n### After Resolution\n```\nCDN performance has been fully restored. All traffic has been returned to\nour primary provider, and we're monitoring closely for any recurring issues.\n```\n\n## DNS Issues Communication\n\n### Initial Report\n```\nWe're aware of DNS resolution failures affecting access to [service].\nSome users may receive \"site cannot be reached\" errors. We're working\nwith our DNS provider to resolve this immediately.\n\nWorkaround: If you have our service's IP address cached, you can\nstill access via direct IP.\n```\n\n### Resolution\n```\nDNS resolution has been restored. Changes may take 5-15 minutes to\npropagate globally due to DNS caching. If you still can't access\nthe service, try clearing your DNS cache or waiting a few minutes.\n```\n\n## Providing Workarounds\n\nAlways include workarounds when possible:\n\n### Example 1: CDN Issues\n```\nWhile we work on the connectivity issue, you can access the service\ndirectly via: https://direct.yourservice.com (bypasses CDN)\n```\n\n### Example 2: Regional Issues\n```\nUsers in affected regions can temporarily use our US endpoint:\nhttps://us.yourservice.com\n```\n\n### Example 3: API Connectivity\n```\nIf experiencing timeouts, try reducing request rate or adding retry\nlogic with exponential backoff. We'll send an all-clear when issues\nare fully resolved.\n```\n\n## DDoS Mitigation Communication\n\n### During Active Mitigation\n```\nWe're currently mitigating a distributed denial-of-service (DDoS) attack.\nSome users may experience:\n- Slower response times\n- CAPTCHA challenges\n- Temporary access blocks\n\nThese are protective measures. Your data and account remain secure.\n```\n\n### Post-Mitigation\n```\nThe DDoS attack has been successfully mitigated. All protective measures\nremain in place. Service performance has returned to normal.\n```\n\n## Timing Expectations\n\nSet clear expectations about updates:\n\n```\nWe're investigating network connectivity issues in the APAC region.\nWe'll provide updates every 15 minutes until resolved.\n\nNext update: 10:15 UTC\n```\n\nThen honor that commitment:\n\n```\n[10:15 UTC Update] Still investigating. Network provider has identified\nthe issue and is implementing a fix. We expect resolution within 30 minutes.\n\nNext update: 10:30 UTC or sooner if resolved.\n```\n\n## Multi-Region Status Example\n\nFor services with multiple regions:\n\n```\nCurrent Status:\n- 🔴 EU-West: Major connectivity issues\n- 🟡 US-East: Minor latency increases\n- 🟢 US-West: Operating normally\n- 🟢 Asia-Pacific: Operating normally\n\nWe're prioritizing restoration of EU-West and monitoring other regions closely.\n```\n\n## Third-Party Provider Communication\n\nWhen the issue is with a third-party:\n\n✅ **Be transparent but professional**:\n```\nWe're experiencing connectivity issues due to problems with our CDN\nprovider, Cloudflare. They are working on a fix. We're monitoring\ntheir status page and will update you as we learn more.\n```\n\n❌ **Don't blame or criticize**:\n```\nCloudflare is down again! Not our fault! 🤷\n```\n\n## Follow-Up After Resolution\n\n### Short-Term (Same Day)\n```\nConnectivity has been fully restored across all regions. We're continuing\nto monitor for any recurring issues.\n```\n\n### Medium-Term (Next Day)\n```\nYesterday's connectivity issues were caused by [root cause]. We're working\nwith our network provider to implement additional safeguards to prevent\nrecurrence.\n```\n\n## Related Templates\n\n- Use **API Service Disruption** if network issues primarily affect API calls\n- Use **Database Performance** if network latency affects database connections\n- Use **Security Incident** if connectivity issues are attack-related\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/public-postmortem-underrated-marketing.mdx",
    "content": "---\ntitle: \"Public Postmortems are Underrated Marketing\"\ndescription: \"Public incident postmortems build more trust than marketing campaigns. Learn why companies share detailed root cause analyses and how radical transparency during outages proves engineering maturity.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-02-11\"\ncategory: \"education\"\nfaq:\n  - question: \"Why should I publish public postmortems instead of hiding incidents?\"\n    answer: \"Public postmortems demonstrate engineering depth, operational maturity, and build trust that no marketing can replicate. When GitLab live-streamed their database recovery, they gained massive respect from the developer community instead of losing customers. Transparency proves you understand your systems and take incidents seriously.\"\n  - question: \"What should I include in an incident postmortem?\"\n    answer: \"Include five key elements: a clear timeline with exact timestamps, deep root cause analysis (not just symptoms), blameless language focused on systems not individuals, concrete action items with owners and deadlines, and leadership visibility showing founders take responsibility.\"\n  - question: \"When should I start communicating about an incident?\"\n    answer: \"Start immediately when the incident begins - don't wait for the postmortem. Share real-time updates on your status page, Twitter/X, and Slack communities as you respond. The postmortem comes later as thorough analysis, but transparency starts the moment something breaks.\"\n  - question: \"What's the difference between a status page update and a postmortem?\"\n    answer: \"Status page updates are real-time communications during active incidents telling users what's happening now. Postmortems are detailed analyses published after resolution, explaining root causes, systemic failures, and prevention plans. Great companies do both - immediate transparency plus thorough follow-up.\"\n  - question: \"Won't admitting failures publicly make my company look incompetent?\"\n    answer: \"The opposite is true. Shallow or hidden responses to incidents signal shallow engineering culture. Detailed postmortems with root cause analysis and systemic thinking prove your team understands their systems. Technical buyers evaluating your infrastructure find this reassuring, not concerning.\"\n---\n\n**TL;DR**: Public incident postmortems build more trust than any marketing campaign. Companies like GitLab, Cloudflare, and Vercel have proven that radical transparency during outages - sharing real-time updates, detailed root cause analysis, and concrete action plans - turns potential PR disasters into proof of engineering maturity. Build in public, incidents included.\n\n---\n\nIncidents in public. That's the move most companies are too afraid to make.\n\nYou ship fast, something breaks, users get angry, and your first instinct is to go quiet. Minimize. Craft a vague statement about \"intermittent issues\" and hope everyone moves on. But the companies that have earned the deepest trust from developers do the exact opposite. They open the door, show you the fire, and walk you through how they put it out.\n\nPublic postmortems aren't designed to be marketing - but radical transparency when things break builds the kind of trust that no landing page ever will.\n\n## Your Status Page Is Not Enough\n\nWhen something goes wrong, users don't just want to know _that_ things are broken. They want to know _why_ - not _who_ to blame, but what actually failed. They want to know you understand the problem, that you're not just restarting servers and hoping for the best.\n\nA status page that says \"Degraded Performance - Investigating\" tells your users nothing. What actually builds trust is going further: sharing a timeline of events as they unfold, explaining what component failed and why, and giving users enough context to make their own decisions.\n\nThe best companies start communicating before the postmortem is even written. They share updates in real time - on their status page, on Twitter/X, in Slack communities - so users aren't left guessing. The postmortem comes later as a thorough analysis, but transparency starts the moment something breaks.\n\nCompanies like [Cloudflare](https://blog.cloudflare.com/tag/post-mortem/) and [Vercel](https://vercel.com/blog/postmortem-on-next-js-middleware-bypass) have turned this into an art form. When incidents hit, their CEOs publish detailed technical breakdowns with minute-by-minute timelines, root cause analysis, and specific systemic changes. No sanitized corporate apologies. No \"we take reliability seriously.\" Just: here's exactly what broke, here's exactly why, and here's what we're doing about it.\n\n## GitLab: The Postmortem That Became a Legend\n\nPerhaps the most famous example in tech history: in January 2017, a GitLab engineer accidentally deleted 300GB of production data from the primary database instead of a secondary replica. To make it worse, they discovered that five different backup systems had all been silently failing.\n\nWhat GitLab did next was unprecedented. They opened a public Google Doc and updated it in real time as they worked through the recovery. They live-streamed the entire recovery process on [YouTube](https://www.youtube.com/watch?v=v0TRHLvYGE0). CEO Sid Sijbrandij was visible throughout, and senior GitLab engineers showed up on Hacker News to answer questions and own the mistake.\n\nThe [postmortem they published](https://about.gitlab.com/blog/postmortem-of-database-outage-of-january-31/) was brutally honest. It named specific systems that failed, explained exactly why each backup mechanism was broken, and laid out a concrete plan for fixing every gap. The general reaction across the industry wasn't ridicule - it was respect. GitLab actually gained goodwill through the worst outage in their history because of how they handled it.\n\n## What Makes a Postmortem Actually Build Trust\n\nGreat postmortems share five characteristics:\n\n**A clear, honest timeline.** Minute-by-minute breakdowns of what happened, when it was detected, and how the team responded. No vague language. No \"approximately\" when you know the exact timestamp.\n\n**Root cause analysis that goes deep.** Not \"a configuration change caused the outage\" but _why_ that configuration change was possible, what safeguards should have caught it, and why they didn't. Use the \"Five Whys\" technique (asking \"why\" repeatedly to dig past symptoms) to uncover systemic issues.\n\n**Blameless language, not nameless language.** Focus on systems and processes, not individuals. Say \"a network engineer\" not \"John from the ops team.\" Create an environment where people share what actually happened instead of the sanitized version.\n\n**Concrete action items with owners.** Not \"we will improve our monitoring\" but \"we are adding automated validation for configuration files before deployment, owned by the platform team, shipping by Q2.\"\n\n**Leadership visibility.** When the CEO or CTO puts their name on the postmortem, it signals the company treats this seriously. These aren't engineers being told to write a blog post - these are founders saying \"this happened on my watch and here's what we're doing about it.\"\n\n### What to Avoid in Incident Postmortems\n\nJust as important as what to include: skip the corporate speak, avoid deflecting blame to vendors, and never publish vague action items like \"we'll monitor this more closely.\" These undermine the very trust you're trying to build.\n\n## Why Admitting Failure Is Better Marketing Than Hiding It\n\nMost companies think admitting failure makes them look weak. The opposite is true.\n\nWhen GitLab published their database deletion postmortem, the Hacker News thread hit the front page and stayed there for days. Instead of a mass exodus, they gained massive respect from the developer community. Their transparency turned what could have been a reputation crisis into proof of operational maturity.\n\nA thorough postmortem demonstrates things that matter to technical buyers:\n\n**Engineering depth.** A shallow postmortem reveals shallow engineering culture. A deep one - with detailed root cause analysis and systemic thinking - shows your team actually understands their own systems. That's reassuring if someone is evaluating whether to trust you with their infrastructure.\n\n**Operational maturity.** Companies that respond to incidents quickly, communicate transparently, and execute on remediation are companies that have their act together. The postmortem is proof.\n\n**Industry authority.** When your incident reports help other engineers avoid the same mistakes, you become a trusted voice in the community - the kind that makes engineers choose you over competitors with bigger marketing budgets.\n\n**Content that performs.** Public postmortems attract backlinks, get shared on Hacker News and Reddit, and become canonical references for specific failure modes. They're some of the highest-performing technical content you can publish.\n\n## Build in Public - Incidents Included\n\nThe \"build in public\" movement has taught us that sharing your journey - wins and losses - builds trust with your audience. Public postmortems are the natural extension of that philosophy. If you're willing to share your roadmap, your metrics, and your product decisions, why hide the moments when things break?\n\nEvery company has incidents. The ones that earn long-term trust own them publicly, explain what happened clearly, and show their work on prevention. Your next outage isn't just a crisis to manage - it's an opportunity to show your users exactly who you are when things are on fire.\n\n## Start Building Trust Through Transparency\n\nMost companies will keep hiding behind vague status updates. That's your competitive advantage.\n\nThe question isn't whether you'll have an incident - it's whether you'll have the courage to be honest about it. Start with your status page: make it transparent, update it in real-time during incidents, and follow up with detailed postmortems that help others learn from your mistakes.\n\nWhen your database catches fire and 5,000 people watch you put it out, that's not a disaster - that's the kind of trust you can't buy with any marketing budget.\n\n---\n\nStart free. No credit card required. Set up your first status page in under 5 minutes.\n\n<ButtonLink href=\"https://app.openstatus.dev\">Try openstatus free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/public-vs-private-status-pages.mdx",
    "content": "---\ntitle: \"Public vs Private Status Pages\"\ndescription: \"Learn the key differences between public and private status pages, when to use each, and how to match transparency to your audience - customers, teams, and partners.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-02-05\"\ncategory: \"education\"\nfaq:\n  - question: \"What's the main difference between public and private status pages?\"\n    answer: \"Public status pages are open to anyone and designed for customer-facing communication with reviewed, confirmed updates. Private status pages are access-controlled and designed for internal teams, partners, or enterprise clients with real-time metrics, auto-incidents, and detailed operational data.\"\n  - question: \"When should I create a public status page?\"\n    answer: \"Create a public status page when you want to proactively communicate service status to customers, reduce support tickets, and build trust through transparency. It's especially important once you have paying customers or when your service reliability directly impacts user workflows.\"\n  - question: \"When do I need a private status page?\"\n    answer: \"You need a private status page when you have more than 10 team members who need real-time operational visibility, when you have enterprise customers with contractual SLA requirements, or when you have partners integrating with your platform who need self-service status information.\"\n  - question: \"Can I use auto-incidents on a private status page?\"\n    answer: \"Yes, but it depends on your audience. For internal teams, auto-incidents are highly valuable - they close the gap between detection and awareness. For partner-facing pages, skip auto-incidents and use reviewed status reports instead, as brief internal hiccups don't need to appear on your partner's dashboard.\"\n  - question: \"What authentication methods are available for private status pages?\"\n    answer: \"Common authentication methods include email domain protection (for known organizations), simple password protection (for broader access or RSS support), SSO integration (for enterprise customers), and IP restriction (for locked-down network environments). Choose based on your audience and security requirements.\"\n  - question: \"Should auto-incidents be enabled for customer-facing pages?\"\n    answer: \"No. Public status pages should not use auto-incidents. Every update should be reviewed before publication to ensure accuracy and appropriate messaging. A brief monitoring anomaly or false alarm shouldn't create unnecessary customer concern.\"\n---\n\n**TL;DR** - Public status pages face your customers and build trust through transparency. Private status pages face your team and partners, giving them deeper context behind authentication. Once you have more than a handful of engineers or paying enterprise customers, you'll need both.\n\n|  | Public | Private |\n| --- | --- | --- |\n| **Audience** | Customers | end users |\n| **Access control** | None - open to anyone | Password |\n| **Transparency** | Measured - share what's confirmed | High - share everything you can |\n| **Communication** | Reviewed status reports | Real-time metrics |\n| **Search engines** | Indexed and discoverable | Hidden from crawlers |\n\n## What Is a Public Status Page?\n\nA public status page is an unprotected, internet-facing page that communicates the operational state of your services to anyone who visits. It lives at a predictable URL-typically `status.yourcompany.com` - so customers can find it the moment something feels off, before frustration turns into a support ticket.\n\nBefore they open a support ticket, before they tweet \"@yourcompany is down??\" to their 10,000 followers, they Google \"[your product] status.\" If your page is there with a clear answer, you've just turned potential frustration into reassurance - and kept their trust intact.\n\n## When to Use a Public Status Page\n\nA public status page signals that you take reliability seriously enough to be open about it. But transparency doesn't mean immediacy - you shouldn't broadcast every anomaly the second it appears. A brief spike in latency might be a monitoring false alarm. A brief error spike might resolve in seconds.\n\nThis is where a review process matters. Many teams gate public updates behind an internal check: is this real or a hiccup? What do we actually know? The delay between detection and public communication isn't a bug - it's responsible communication.\n\n**How you communicate is as important as what you communicate.** A status page that says \"We are investigating an issue\" with no follow-up for three hours doesn't build trust-it destroys it. Silence feels like negligence. Frequent, honest updates - even if the update is \"we're still investigating\" - maintain trust during incidents.\n\nAnd remember: your public page is visible to journalists, competitors, and investors too. That's a feature when you handle communication well, and a liability when you don't.\n\n## What Is a Private Status Page?\n\nA private status page is an access-controlled page that communicates service health to a restricted audience - your internal teams, selected partners, or enterprise customers. It sits behind authentication and is invisible to search engines, which means you can be radically transparent without worrying about public perception or competitor intel.\n\nIt shows the real-time operational data your public page doesn't-metrics, detailed diagnostics, and unfiltered incident details.\n\n## When to Use a Private Status Page\n\nPrivate status pages serve two distinct contexts with different approaches.\n\n### For Internal Teams\n\nInternally, the goal is maximum context. Share as much as possible - don't be shy.\n\nMetrics and monitor charts are the backbone-latency by region, uptime check results, error rates-which means teams can assess severity themselves instead of waiting for someone to write a status report. An engineer sees degraded latency in `eu-west-1`. A support lead sees failing uptime pings. No one waits for a Slack message.\n\nAuto-incidents - where monitoring automatically creates an incident when a threshold is breached - close the gap between detection and awareness. The result: no \"is X down?\" questions flooding Slack at 2 AM. No support team scrambling for answers they don't have. Everyone sees the same incident data at the same time-so coordination happens instead of chaos.\n\n### For Partners and Enterprise Customers\n\nPartner-facing pages require more curation. You're behind authentication, but communicating with an audience that has contractual expectations.\n\n**Skip auto-incidents.** A brief internal hiccup that resolves in 30 seconds doesn't need to appear on your partner's dashboard. Use dedicated status reports instead, with context tailored to what your SLA allows you to share.\n\n**Do include metrics and monitor charts.** Partners benefit from self-assessing whether the API they depend on is healthy without emailing your support team. This is especially valuable for enterprise customers tracking uptime against custom SLA thresholds.\n\nConsider exposing staging or sandbox environment status too - partners integrating with your platform care about non-production almost as much as production.\n\n### Access Control Options\n\nThe right authentication depends on your audience. **Email domain protection** works when you know which organizations should have access - set the allowed domains and authenticate via magic link. **Simple password protection** is better for broader audiences or when you need RSS support. **SSO** fits enterprise customers who expect integration with their identity provider. **IP restriction** suits locked-down network environments.\n\nChoose wrong and you'll either lock out legitimate users (too restrictive) or expose sensitive operational data to the wrong audience (too open).\n\n---\n\n## You Probably Need Both\n\nTeams with more than 10 people typically aren't choosing between public and private - they're choosing what goes where. The public page gets curated, reviewed updates. The private page gets raw metrics, auto-incidents, and detailed context.\n\nMatch the transparency to the audience. Customers need confidence. Teams need context. Partners need something in between.\n\n**Openstatus gives you both.** Create public pages for customer transparency and private pages with full metrics for your team-all from one platform.\n\n---\n\nStart free. No credit card required. Set up your first status page in under 5 minutes.\n\n<ButtonLink href=\"https://app.openstatus.dev\">Try openstatus free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/scheduled-maintenance.mdx",
    "content": "---\ntitle: \"Scheduled Maintenance Template\"\ndescription: \"Proactive template for communicating planned maintenance windows, upgrades, and scheduled downtime to users.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-01-19\"\ncategory: \"template\"\nhowto:\n  totalTime: \"PT2H\"\n  steps:\n    - name: \"Plan and schedule maintenance\"\n      text: \"Choose a low-traffic time window and schedule maintenance at least 1 week in advance. Consider timezone differences for global audiences.\"\n      url: \"#advance-notice-best-practices\"\n    - name: \"Send advance notifications\"\n      text: \"Notify users 1 week before, 24 hours before, and 1 hour before maintenance begins. Include exact times, duration, and expected impact.\"\n      url: \"#1-week-before\"\n    - name: \"Execute maintenance with updates\"\n      text: \"Begin maintenance at scheduled time, provide status updates every hour, and extend timeline if needed with clear communication.\"\n      url: \"#at-start\"\n    - name: \"Complete and verify\"\n      text: \"Test all systems after completion, send completion notice to all users, and communicate any improvements or benefits delivered.\"\n      url: \"#at-completion\"\nfaq:\n  - question: \"How much advance notice should I give for maintenance?\"\n    answer: \"Minimum 48 hours for minor maintenance under 30 minutes with no downtime. 1 week notice for standard maintenance 1-4 hours with read-only mode. 2+ weeks notice for major maintenance over 4 hours with full downtime.\"\n  - question: \"What if maintenance takes longer than expected?\"\n    answer: \"Provide updates every hour and notify users immediately of any time extensions. Include the new estimated completion time and reason for the extension.\"\n  - question: \"Should I include timezone information?\"\n    answer: \"Yes, always include multiple timezones for global audiences or use UTC time with a timezone converter link. This prevents confusion about when maintenance will occur.\"\n  - question: \"What information must be included in maintenance notices?\"\n    answer: \"Always include exact date and time with timezone, expected duration, impact description, what users can and cannot do during maintenance, and who to contact with questions.\"\n---\n\nUse this template for planned maintenance, infrastructure upgrades, or scheduled downtime. Proactive communication builds trust and reduces support burden.\n\n## When to Use This Template\n\n- Database upgrades\n- Infrastructure migrations\n- Security patch deployments\n- Third-party provider maintenance\n- Performance optimization work\n\n## Template Messages\n\n### Scheduled (Advance Notice)\n\nWe have scheduled maintenance for [System Name] on [Date] at [Time] [Timezone].\n\n**Expected Duration**: [Duration]\n**Expected Impact**: [Impact Description]\n\nWe will provide updates throughout the maintenance window.\n\n### In Progress\n\nScheduled maintenance is currently in progress. We expect to complete within the planned window.\n\n**Current Status**: [Brief status]\n**Expected Completion**: [Time]\n\n### Completed\n\nThe scheduled maintenance has been completed successfully. All systems are operating normally.\n\nThank you for your patience during this maintenance window.\n\n## Real-World Examples\n\n### Stripe: Payment Method Maintenance\n\n**Context**: Scheduled maintenance for TWINT payment method\n**Approach**: Clear scheduling with transparency disclaimers\n\n**Sample messaging**:\n```\nTWINT has an upcoming scheduled maintenance.\n\nNote: Stripe retrieves payment method planned maintenance information\nfrom payment method partners and the information may not be complete,\naccurate, or up to date.\n```\n\n**What worked**:\n- Clear about the service affected (TWINT)\n- Set expectations about information accuracy\n- Consistent disclaimer across all maintenance notices\n\n**Multi-stage updates**:\n1. **Scheduled**: \"TWINT has upcoming scheduled maintenance on [date]\"\n2. **In Progress**: \"Maintenance is currently in progress\"\n3. **Completed**: \"The scheduled maintenance has been completed\"\n\n### Notion: Planned Maintenance Communication\n\n**Approach**: Clear timing with minimal service impact\n\n**Sample structure**:\n```\nScheduled Maintenance\nTime: Jan 20, 2026 | 2:00 AM - 4:00 AM PST\nImpact: Brief connectivity interruptions expected\nStatus: Scheduled\n\nWe'll be performing database maintenance during this window.\nMost features will remain available, but you may experience\nbrief disruptions.\n```\n\n## Planning Your Maintenance Communication\n\n### 1 Week Before\n```\n📅 Scheduled Maintenance Notice\n\nWe'll be performing infrastructure upgrades on [date] at [time].\n\nWhat: Database migration to improve performance\nWhen: January 25, 2026 at 02:00 UTC\nDuration: Approximately 2 hours\nImpact: Read-only mode during migration\n\nAdd to calendar: [iCal link]\n```\n\n### 24 Hours Before\n```\n⏰ Reminder: Maintenance Tomorrow\n\nThis is a reminder that we'll be performing scheduled maintenance\ntomorrow, January 25 at 02:00 UTC (approximately 2 hours).\n\nDuring this time, the service will be in read-only mode. You'll be\nable to view data but won't be able to make changes.\n\nWe recommend completing any urgent work before maintenance begins.\n```\n\n### 1 Hour Before\n```\n🔧 Maintenance Starting Soon\n\nScheduled maintenance begins in 1 hour (02:00 UTC).\n\nQuick reminders:\n- Service will be in read-only mode\n- Expected duration: 2 hours\n- We'll provide hourly updates\n\nIf you have any questions, please contact support before maintenance begins.\n```\n\n### At Start\n```\n🚧 Maintenance In Progress\n\nScheduled maintenance has started. The service is now in read-only mode.\n\nProgress: Database backup completed ✓\nNext: Begin migration\nExpected completion: 04:00 UTC\n```\n\n### Hourly Updates\n```\n📊 Maintenance Progress Update (Hour 1)\n\nMigration is progressing as planned.\n\nCompleted:\n✓ Database backup\n✓ Schema updates\n✓ Data migration (65%)\n\nIn progress:\n⏳ Data migration (ongoing)\n\nStill on track for 04:00 UTC completion.\n```\n\n### At Completion\n```\n✅ Maintenance Complete\n\nScheduled maintenance has been completed successfully!\n\nThe service is now fully operational and out of read-only mode.\nAll features are available.\n\nThank you for your patience. You should notice improved performance\nwith our upgraded infrastructure.\n```\n\n### If Extended\n```\n⚠️ Maintenance Extended\n\nWe're making excellent progress, but the maintenance will take longer\nthan expected.\n\nNew estimated completion: 05:00 UTC (1 hour extension)\nReason: Data migration taking longer due to dataset size\n\nThe service remains in read-only mode. We'll provide another update\nin 30 minutes.\n```\n\n## Advance Notice Best Practices\n\n### Minimum Notice Periods\n\n- **Minor maintenance** (< 30 min, no downtime): 48 hours notice\n- **Standard maintenance** (1-4 hours, read-only): 1 week notice\n- **Major maintenance** (> 4 hours, full downtime): 2+ weeks notice\n\n### What to Include\n\n✅ **Always include**:\n- Exact date and time (with timezone)\n- Expected duration\n- Impact description\n- What users can/can't do\n- Who to contact with questions\n\n✅ **Nice to have**:\n- Reason for maintenance\n- Expected benefits\n- Calendar file (.ics)\n- Status page link\n- Estimated completion time\n\n❌ **Avoid**:\n- Vague timing (\"sometime next week\")\n- Unclear impact (\"may affect some users\")\n- No duration estimate\n- Technical jargon without explanation\n\n## Impact Level Communication\n\n### No Downtime\n```\nMaintenance Window: January 25, 02:00-04:00 UTC\nImpact: None - We'll be performing rolling updates with no service interruption\nAction Required: None\n```\n\n### Read-Only Mode\n```\nMaintenance Window: January 25, 02:00-04:00 UTC\nImpact: Service will be in read-only mode\nWhat you CAN do: View all data, run reports, access historical information\nWhat you CAN'T do: Create, update, or delete records\nAction Required: Complete any urgent changes before maintenance\n```\n\n### Full Downtime\n```\nMaintenance Window: January 25, 02:00-04:00 UTC\nImpact: Service will be completely unavailable\nWhat to expect: You won't be able to access the service at all\nAction Required: Plan accordingly, download any needed data beforehand\nAlternative: [If you have a status page or alternative access method]\n```\n\n### Partial Service Impact\n```\nMaintenance Window: January 25, 02:00-04:00 UTC\nImpact: Partial service degradation\n\nAvailable:\n✓ Login and authentication\n✓ Data viewing\n✓ API read endpoints\n\nUnavailable:\n✗ File uploads\n✗ Report generation\n✗ Data exports\n✗ Webhook delivery\n```\n\n## Timezone Considerations\n\nAlways include multiple timezones for global audiences:\n\n```\nMaintenance scheduled for:\n- 02:00 UTC\n- 6:00 PM PST (Jan 24)\n- 9:00 PM EST (Jan 24)\n- 11:00 AM JST (Jan 25)\n\nCheck your timezone: [worldtimebuddy.com link]\n```\n\nOr use relative time:\n```\nMaintenance begins in 24 hours.\nCheck your local time: January 25, 02:00 UTC\n```\n\n## Third-Party Maintenance\n\nWhen a provider schedules maintenance:\n\n```\nOur payment provider, Stripe, has scheduled maintenance for [date].\n\nImpact: Payment processing may be temporarily unavailable\nDuration: Approximately 30 minutes\nWorkaround: We'll queue any failed payments and automatically retry\n\nThis maintenance is being performed by our payment provider. For more\ninformation, visit: [provider status page]\n```\n\n## Emergency Maintenance\n\nSometimes maintenance can't wait:\n\n```\n🚨 Emergency Maintenance Required\n\nWe've identified a critical issue that requires immediate maintenance.\n\nStarting: Now (15:00 UTC)\nExpected duration: 30 minutes\nImpact: Service will be unavailable\n\nWe apologize for the short notice. This maintenance is necessary to\nprevent potential data issues.\n\nUpdates every 10 minutes.\n```\n\n## Benefits Communication\n\nHelp users understand the \"why\":\n\n```\nThis maintenance will deliver:\n✓ 3x faster query performance\n✓ 99.99% uptime SLA (up from 99.9%)\n✓ Support for 10x more concurrent users\n✓ Reduced latency for API calls\n\nWe appreciate your patience as we improve the service.\n```\n\n## FAQ to Include\n\nConsider adding to your maintenance notice:\n\n**Q: Can I use the service during maintenance?**\nA: The service will be in read-only mode. You can view data but not make changes.\n\n**Q: What if I'm in the middle of something?**\nA: We recommend saving your work before maintenance begins. Any unsaved changes may be lost.\n\n**Q: Will my data be safe?**\nA: Yes, we perform full backups before all maintenance. Your data is secure.\n\n**Q: What if maintenance takes longer?**\nA: We'll provide updates every hour and notify you of any time extensions.\n\n**Q: Can you reschedule?**\nA: This timing was chosen for minimal impact. Please contact support if you have urgent concerns.\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/security-incident-response.mdx",
    "content": "---\ntitle: \"Security Incident Response Template\"\ndescription: \"Formal template for communicating security issues with professional, reassuring language that maintains user trust.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-01-19\"\ncategory: \"template\"\nfaq:\n  - question: \"Do I need to notify users immediately for every security concern?\"\n    answer: \"No. Only communicate publicly when there's a confirmed incident with user impact or when you're taking visible security measures (like requiring password resets). Internal security reviews and patched vulnerabilities without exploitation typically don't require public disclosure.\"\n  - question: \"What legal obligations do I have for security incident disclosure?\"\n    answer: \"This varies by jurisdiction and industry. GDPR requires notification within 72 hours for personal data breaches. CCPA has requirements for California residents. Healthcare (HIPAA) and payment systems (PCI DSS) have specific rules. Always consult with legal counsel before publishing security incident communications.\"\n  - question: \"How much technical detail should I share about a security vulnerability?\"\n    answer: \"Share enough to demonstrate competence and transparency, but avoid tactical details that could help attackers. Good: 'unauthorized access attempts to our admin panel.' Bad: 'SQL injection on /admin/login using parameter X.' Wait until the vulnerability is fully patched and no longer exploitable before sharing technical details.\"\n  - question: \"Should I tell users to change their passwords?\"\n    answer: \"Only if there's evidence of credential compromise or unauthorized access to authentication systems. Don't create alarm fatigue by requesting password changes for unrelated security work. Be specific about why you're asking and which accounts are affected.\"\n---\n\nUse this template when investigating or responding to security-related incidents. Maintains professional tone while reassuring users about security measures.\n\n## When to Use This Template\n\n- Potential security vulnerabilities discovered\n- Unauthorized access attempts detected\n- Data exposure concerns\n- Security patch deployments\n- Compliance-related issues\n\n## Template Messages\n\n### Investigating\n\nWe are investigating a potential security issue. We take security very seriously and are working with our security team to assess the situation.\n\nWe will provide updates as we learn more. For security questions, please contact security@yourcompany.com.\n\n### Identified\n\nWe have identified the nature of the security issue and are implementing remediation measures.\n\n### Monitoring\n\nSecurity measures have been implemented. We are monitoring systems to ensure the issue is fully resolved.\n\n### Resolved\n\nThe security issue has been resolved. We have taken steps to prevent similar issues in the future.\n\n## Key Principles for Security Communication\n\n### 1. **Be Transparent, But Not Reckless**\n\nShare what you know, but don't provide details that could help attackers:\n\n✅ Good: \"We identified unauthorized access attempts to our admin panel\"\n❌ Avoid: \"Attackers tried SQL injection on /admin/login using parameter X\"\n\n### 2. **Reassure Without Minimizing**\n\n✅ Good: \"No user data was accessed. We take security seriously and have implemented additional safeguards.\"\n❌ Avoid: \"It's not a big deal, don't worry about it.\"\n\n### 3. **Provide Clear Next Steps**\n\nTell users what (if anything) they should do:\n- Change passwords?\n- Review account activity?\n- Update client software?\n- No action needed?\n\n### 4. **Show Competence**\n\nDemonstrate you're handling it professionally:\n- \"Working with our security team\"\n- \"Engaged external security experts\"\n- \"Following our incident response procedures\"\n\n## Real-World Security Communication Examples\n\n### Example 1: Data Access Concern\n\n**Investigating**:\n```\nWe are investigating reports of unusual account activity. As a precaution,\nwe've temporarily disabled affected accounts and are conducting a thorough\nsecurity audit.\n\nIf your account was affected, we will contact you directly via email.\n```\n\n**Resolved**:\n```\nOur investigation is complete. No user data was compromised. The unusual\nactivity was caused by automated testing scripts that were misconfigured.\n\nWe've implemented additional monitoring to prevent similar false positives\nand improved our alerting to distinguish between legitimate and suspicious\nactivity.\n```\n\n### Example 2: Vulnerability Patch\n\n**Investigating**:\n```\nWe've been notified of a potential security vulnerability in a third-party\nlibrary we use. We are investigating the impact and preparing a security\npatch.\n\nThis appears to be a low-severity issue with no evidence of exploitation,\nbut we're treating it with high priority.\n```\n\n**Resolved**:\n```\nWe've deployed a security patch addressing the vulnerability. No user data\nwas at risk, and we saw no signs of exploitation. All systems have been\nupdated and are operating normally.\n```\n\n## Tone Guidelines\n\n### Professional & Calm\nSecurity incidents can be stressful. Your communication should be:\n- **Measured**: Not panicked or overly casual\n- **Clear**: No jargon or ambiguity\n- **Authoritative**: Demonstrates control and competence\n- **Empathetic**: Acknowledges user concerns\n\n### Example Tone Comparisons\n\n❌ **Too casual**: \"Oops, we had a little security hiccup, but it's all good now!\"\n\n❌ **Too alarming**: \"URGENT: Your data may be compromised! Immediate action required!\"\n\n✅ **Just right**: \"We've identified and resolved a security issue. No user data was compromised. We've implemented additional safeguards to prevent similar issues.\"\n\n## Legal & Compliance Considerations\n\n### Include if Required\n\n- **GDPR**: Data breach notifications within 72 hours if personal data affected\n- **CCPA**: Notification if California residents' data compromised\n- **SOC 2**: Incident must be logged and reported per agreements\n- **Industry-specific**: HIPAA (healthcare), PCI DSS (payments), etc.\n\n### Standard Disclaimers\n\nConsider adding:\n```\nThis incident has been reported to relevant authorities as required by\n[regulation name]. Affected users will be notified directly via email\nas required by law.\n```\n\n## Security Incident Checklist\n\nBefore publishing updates:\n\n- [ ] Verified facts with security team\n- [ ] Removed any tactical details that could help attackers\n- [ ] Confirmed legal/compliance requirements met\n- [ ] Prepared answers to likely follow-up questions\n- [ ] Coordinated with PR/communications team if needed\n- [ ] Set up dedicated security@company.com contact\n- [ ] Drafted FAQ for support team\n\n## Follow-Up Communication\n\n### Within 24 Hours\nInitial resolution and immediate actions taken\n\n### Within 7 Days\nPreliminary findings and preventative measures\n\n### Within 30 Days (Optional)\nFull post-mortem with technical details (if appropriate)\n\n## Example: Complete Incident Progression\n\n**00:00 - Detection**\n```\nWe're investigating reports of unusual login activity. As a precaution,\nwe've temporarily increased authentication requirements and are reviewing\naccount access logs.\n```\n\n**00:45 - Identification**\n```\nWe've identified the cause as compromised API keys from a third-party\nintegration. We've revoked the affected keys and are auditing all\naccess during the exposure window. No evidence of data access.\n```\n\n**02:00 - Monitoring**\n```\nAll compromised keys have been revoked and replaced. We're monitoring\nfor any related activity. Additional security measures have been\ndeployed to prevent similar issues.\n```\n\n**04:00 - Resolution**\n```\nIncident resolved. Our investigation confirms no user data was accessed.\nWe've strengthened our API key rotation procedures and added real-time\nmonitoring for unusual API activity.\n\nUsers with affected integrations will receive direct communication\nabout key rotation requirements.\n```\n\n## When NOT to Use This Template\n\n- System outages unrelated to security → Use appropriate infrastructure template\n- Planned security patches with no active threat → Use maintenance template\n- Minor configuration issues → Use general service disruption template\n\nOnly use security incident language when there's an actual security concern to avoid alarm fatigue.\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/sla-vs-slo-vs-sli.mdx",
    "content": "---\ntitle: \"SLA vs SLO vs SLI Explained\"\ndescription: \"Learn the critical differences between SLAs, SLOs, and SLIs. Understand how to measure service reliability, set internal targets, and make customer promises without over-committing or under-delivering.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-02-09\"\ncategory: \"education\"\nfaq:\n  - question: \"What's the difference between SLI, SLO, and SLA?\"\n    answer: \"An SLI is a specific metric you measure (like API response time). An SLO is your internal target for that metric (stricter than customer promises). An SLA is your public promise to customers with consequences if you fail. They stack: SLI → SLO → SLA.\"\n  - question: \"Why should my SLO be stricter than my SLA?\"\n    answer: \"The gap between your SLO and SLA is your error budget - the buffer that lets you deploy features, run experiments, and handle incidents without immediately violating customer agreements. If they're the same number, one bad deploy breaks your promises.\"\n  - question: \"What makes a good SLI?\"\n    answer: \"A good SLI is specific, measurable, and directly correlates with user experience. If the metric degrades and users don't notice, it's not an SLI. Examples: P95 API response time under 200ms, 99.9% uptime, error rate below 0.1%.\"\n  - question: \"How many SLIs should I track?\"\n    answer: \"Limit yourself to 3-5 SLIs. Pick metrics that directly impact user experience. Tracking too many creates alert fatigue and buries real issues under noise. Focus on what matters: availability, latency, and error rates.\"\n  - question: \"Do startups need SLAs?\"\n    answer: \"No. Early-stage startups should skip SLAs entirely until they have the infrastructure to measure and maintain them. Focus on tracking 2-3 SLIs, being transparent about uptime, and communicating clearly during incidents. Formalize SLAs when enterprise customers start requesting them.\"\n---\n\n# SLA vs SLO vs SLI Explained\n\nEveryone throws these acronyms around at architecture reviews and planning meetings, but most teams confuse them. You'll hear \"our SLA is 99.9%\" when they mean SLO. Or \"we need better SLIs\" when they haven't defined what they're actually measuring.\n\nGetting this wrong has real consequences. Either you over-promise to customers and violate agreements you can't afford to break, or you set vague internal targets that don't guide any actual engineering decisions.\n\nHere's what they actually mean, how they relate, and how to use them without screwing up.\n\n---\n\n## What They Actually Are\n\n### SLI (Service Level Indicator)\n\nAn SLI is a specific metric you can measure. Not a feeling. Not a goal. A measurement.\n\n**Examples:**\n- API response time: 95th percentile under 200ms\n- Uptime percentage: 99.9% availability\n- Error rate: less than 0.1% of requests fail\n\n\"The site should feel fast\" is not an SLI. \"P95 page load time under 2 seconds\" is an SLI. The difference is whether you can measure it objectively and build alerts around it.\n\n### SLO (Service Level Objective)\n\nAn SLO is your internal target for an SLI. It's what you promise yourself, not what you promise customers.\n\n**Examples:**\n- \"API requests should return in under 200ms (P95) 99.5% of the time\"\n- \"Homepage should be available 99.95% of the time per month\"\n\nThis is your goal. The bar you hold yourself to. It should be stricter than your customer-facing SLA because you need room to miss your internal target without breaking external promises.\n\n### SLA (Service Level Agreement)\n\nAn SLA is your public promise to customers, with real consequences if you fail.\n\n**Examples:**\n- \"We guarantee 99.9% uptime. If we fall below, you get 10% credit\"\n- \"API latency will be under 500ms (P95) or we refund 25% of your monthly bill\"\n\nThis is a legal commitment. It has teeth. Miss it and you're writing refund checks or issuing service credits. That's the point—it's supposed to cost you when you fail customers.\n\n---\n\n## The Hierarchy\n\nThese aren't interchangeable terms. They stack:\n\n```\nSLI → What you measure\nSLO → Internal target for that measurement (stricter)\nSLA → External promise to customers (has buffer from SLO)\n```\n\n**Concrete example:**\n- **SLI:** API uptime percentage\n- **SLO:** 99.95% uptime (internal target)\n- **SLA:** 99.9% uptime (customer promise)\n\nThe gap between your SLO (99.95%) and your SLA (99.9%) is your **error budget**. That's the buffer that lets you deploy new features, run experiments, and handle incidents without immediately violating customer agreements.\n\nIf your SLO and SLA are the same number, one bad deploy breaks your promises. You've eliminated the margin that makes continuous deployment possible.\n\n---\n\n## Common Mistakes Teams Make\n\n### 1. SLA Without SLOs\n\nYou promise customers 99.9% uptime but never actually track it internally. You have no alerts for when you're approaching a violation. You find out you've broken your SLA when angry customers email asking for refunds.\n\n**The fix:** Set stricter internal SLOs before you promise anything externally. Alert when you're at risk of missing the SLO—before you're at risk of breaking the SLA.\n\n### 2. SLIs That Don't Matter to Users\n\nYou're tracking server CPU usage and disk I/O, but not API response time. Your monitoring dashboard is green. Your servers are happy. But users are experiencing 5-second page loads and your support inbox is filling up.\n\nThe test for a real SLI: \"If this metric degrades, do users notice?\" If the answer is no, it's not an SLI—it's just a metric.\n\n### 3. No Error Budget\n\nYour internal target is the same as your customer promise. Both are 99.9%. That means you have zero room for error. One bad deploy that drops availability to 99.85% for an hour violates your SLA immediately.\n\n**The fix:** Build in buffer. If your SLA is 99.9%, set your internal SLO at 99.95%. That 0.05% is your error budget—the space to operate, deploy, and handle incidents without breaking promises.\n\n### 4. Too Many SLIs\n\nYou're tracking 47 different metrics and calling them all \"SLIs.\" Everything is critical. Nothing is prioritized. Alert fatigue sets in. When a real issue happens, it's buried under noise.\n\n**The fix:** Limit yourself to 3-5 SLIs. Pick the metrics that directly correlate with user experience. If users don't notice when it degrades, stop calling it an SLI.\n\n### 5. Unmeasurable SLIs\n\nYour documented SLI is \"the site should feel fast.\" How do you measure feelings? How do you alert on \"feels slow\"? You can't.\n\n**The fix:** Define specific, measurable thresholds. \"P95 page load time under 2 seconds\" is measurable. \"Feels fast\" is not.\n\n---\n\n## Real-World Examples\n\n### Stripe (Doing It Right)\n\n- **SLI:** API success rate\n- **SLO:** 99.99% successful API calls (internal target)\n- **SLA:** 99.9% uptime with tiered service credits for violations\n\nThe 0.09% gap between their internal target and customer promise is their error budget. It gives them room to deploy code, handle incidents, and operate without immediately breaching customer agreements. They track the SLI obsessively and have alerts long before they're at risk of SLA violations.\n\n### Fast Startup Inc (Doing It Wrong)\n\n- **SLA:** \"99.99% uptime guaranteed!\"\n- **SLO:** Not defined\n- **SLI:** Not systematically tracked\n\nThey marketed an aggressive SLA to win enterprise deals but never built the infrastructure to measure or maintain it. They discover violations when customers complain. By then, it's too late—trust is gone and refund requests are piling up.\n\nThe lesson: don't promise what you can't measure. And don't measure without setting realistic internal targets first.\n\n---\n\n## How Status Pages Fit In\n\n### Public Status Page (Display SLAs)\n\nYour public page shows the commitments you've made to customers. Historical uptime against SLA targets. Incident timelines showing when you came close to—but didn't breach—agreements.\n\nThis is customer-facing transparency. It's not real-time operations data. It's curated, reviewed communication.\n\n### Private Status Page (Track SLOs)\n\nYour internal or partner page shows real-time SLO compliance. Dashboards tracking how much error budget you have left this month. Alerts when you're approaching SLO thresholds—before you're at risk of breaking the SLA.\n\nThis is operational data for teams making deployment and incident response decisions.\n\n### Always Monitor SLIs\n\nWhether public or private, everything flows from the SLIs. If you can't measure it, you can't set targets for it. And if you can't set targets, you definitely can't promise it to customers.\n\nYour entire reliability strategy—internal targets, customer promises, incident response priorities—depends on accurate, continuous SLI measurement.\n\n---\n\n## \"But We're Just Three People\"\n\nIf you're a small startup with 5 engineers and 200 customers, implementing a full SLI/SLO/SLA framework might feel like bringing a spreadsheet to a knife fight. You're not wrong.\n\nHere's the pragmatic path for early-stage teams:\n\n**Skip SLAs entirely.** Don't promise contractual uptime guarantees until you have the infrastructure and team to measure and maintain them. Your early customers chose you for the product, not the SLA. One missed SLA violation could cost you more in refunds than you made that month.\n\n**Track 2-3 SLIs, max.** Pick the metrics users actually notice: API availability, response time, error rate. Set up basic monitoring. That's it. Don't build a complex observability stack before you have product-market fit.\n\n**Use implicit SLOs.** You don't need documented internal targets tracked in spreadsheets. You need to know: \"Is the site up? Is it fast?\" If your monitoring shows 99.5% uptime and users aren't complaining, that's your implicit SLO. Formalize it later when you have the bandwidth.\n\n**Focus on transparency, not promises.** Put up a simple status page that shows current uptime. When things break, communicate clearly and quickly. Trust comes from honesty during incidents, not from contractual guarantees you may not hit.\n\nThe three-tier framework (SLI → SLO → SLA) is the end state, not the starting point. Early on, just measure what matters and be honest when it breaks. That's reliability engineering for startups.\n\nYou'll know when you need the full framework: when enterprise customers start asking for SLAs in contracts, when your team is big enough that \"just check if it's up\" stops scaling, or when you're mature enough that formal error budgets would actually guide deployment decisions.\n\nUntil then? Keep it simple. Measure. Communicate. Don't over-promise.\n\n---\n\n## The Bottom Line\n\nYou can't have reliable SLAs without disciplined SLOs. You can't have meaningful SLOs without accurate SLIs.\n\nStart from the bottom and work up:\n1. **Measure** (define SLIs that matter to users)\n2. **Set internal targets** (create SLOs with room for error)\n3. **Promise externally** (offer SLAs only after proving you can consistently hit SLOs)\n\nThe gap between your SLO and SLA isn't waste—it's your safety margin. It's what makes continuous deployment possible. It's what lets you handle incidents without immediately breaking promises.\n\nIf you can't measure it, you can't manage it. And you definitely shouldn't promise it.\n\n---\n\n**OpenStatus tracks all three layers.** Monitor your SLIs, set alerts for SLO thresholds, and display SLA compliance on public and private status pages—all from one platform.\n\n<ButtonLink href=\"/play/uptime-sla\">Try out our SLA calculator</ButtonLink>\n\n---\n\nStart free. No credit card required. Set up your first status page in under 5 minutes.\n\n<ButtonLink href=\"https://app.openstatus.dev\">Try openstatus free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/top-five-atlassian-statuspage-alternatives.mdx",
    "content": "---\ntitle: \"Top Five Atlassian Statuspage Alternatives\"\nimage: \"/assets/posts/top-five-atlassian-statuspage-alternatives/hero.png\"\ndescription: \"Explore the best alternatives to Atlassian Statuspage including OpenStatus, Status-io, Datadog Status Page, Instatus, and Betterstack.\"\nauthor: \"Thibault Le Ouay Ducasse\"\npublishedAt: \"2025-11-05\"\ncategory: \"alternative\"\nfaq:\n  - question: \"How do I migrate from Atlassian Statuspage to an alternative?\"\n    answer: \"Most alternatives offer import tools or APIs. Start by exporting your component list, subscriber data, and historical incidents. Set up your new status page in parallel, then coordinate a cutover date. Update all integrations and notify subscribers of the new status page URL before making the switch.\"\n  - question: \"Which alternative is most cost-effective for small teams?\"\n    answer: \"OpenStatus and Betterstack offer the most generous free tiers. OpenStatus provides unlimited team members at $30/month with monitoring included, while Betterstack has a powerful free tier for solo developers. Both are significantly more affordable than Atlassian Statuspage for small teams.\"\n  - question: \"Can I use these alternatives without built-in monitoring?\"\n    answer: \"Yes. Status.io focuses purely on status communication without monitoring. Other platforms like OpenStatus, Instatus, and Betterstack include monitoring but also allow manual incident updates, so you can use your existing monitoring tools if preferred.\"\n  - question: \"Do any alternatives support self-hosting?\"\n    answer: \"OpenStatus is the only open-source option on this list that supports self-hosting, giving you complete control over your data and infrastructure. The other alternatives are cloud-hosted SaaS solutions only.\"\n---\n\nAtlassian Statuspage has long been a go-to choice for organizations needing to communicate their service status. It's a robust, mature platform. However, the service reliability and incident communication landscape is evolving rapidly.\n\nNewer platforms have emerged, often with a different focus, tighter monitoring integration, developer-first workflows, or open-source flexibility.\n\nIf you're evaluating your options, you're in the right place. In this article, we'll break down the top five alternatives to Atlassian Statuspage, comparing them on key features like monitoring, customization, and developer experience.\n\nThe alternatives we'll cover:\n- [OpenStatus](https://openstatus.dev)\n- [Status.io](https://status.io)\n- [Datadog Status Page](https://www.datadoghq.com/product/status-page/)\n- [Instatus](https://instatus.com)\n- [Betterstack](https://betterstack.com/status-pages/)\n\n## Why Look for an Atlassian Statuspage Alternative?\n\nWhile Statuspage is a powerful tool, several factors might lead your team to seek an alternative:\n\n- **Cost and Pricing Models**: Atlassian's pricing can be significant, especially as your team, subscriber list, and number of components grow. Some alternatives offer more predictable pricing, generous free tiers, or unlimited team members.\n- **Tighter Monitoring Integration**: Many modern platforms bundle robust monitoring with the status page. This avoids the \"bolt-on\" feel, allowing you to automatically and instantly drive status updates directly from your monitoring data, rather than relying on manual updates or complex integrations.\n- **Developer-First Features**: The rise of DevOps and SRE has created demand for features like Monitoring as Code (e.g., configuring checks via YAML or Terraform), native OpenTelemetry support, and APIs for everything.\n- **Customization and Branding**: While Statuspage offers customization, other tools may provide more modern templates, a \"theme store,\" or simpler CSS/HTML access to get the exact look you want.\n- **Open-Source and Self-Hosting**: For teams with strict data privacy requirements or a desire for ultimate control, open-source solutions provide a level of transparency and flexibility (including self-hosting) that a closed-source SaaS can't.\n\n\n## Atlassian Statuspage Comparison\n\nHere's a high-level look at how these five alternatives stack up on key features.\n\n| Features | openstatus | status.io | Datadog Status Page | Instatus | Betterstack |\n| --- | --- | --- | --- | --- | --- |\n| **Status Page** |  |  |  |  |  |\n| Private Status Page | $30/m | $349/m | ✅ | $50/m | Add on $42/m |\n| Team Members | Unlimited | 50 for $349/ | $42/seat | 25 for the $50/m plan | $29/seat |\n| Custom Style | Theme Store | ✅ for $349 | ✅ | ✅ | $12 per page per month |\n| **Monitoring** |  |  |  |  |  |\n| Monitoring | ✅ | ❌ | ✅ | ✅ | ✅ |\n| Monitoring As Code | ✅ Yaml based | ❌ | ✅ Terraform | ❌ | ✅ Terraform |\n| OpenTelemetry | ✅ | ❌ | ✅ | ❌ | ✅ |\n| Private Location | ✅ | ❌ | ✅ | ❌ | ❌ |\n\n\n\n## Top Five Alternatives to Atlassian Statuspage\n\n### 1. OpenStatus\n\n<Image\n  alt=\"openstatus\"\n  src=\"/assets/posts/top-five-atlassian-statuspage-alternatives/openstatus.png\"\n  width={650}\n  height={575}\n/>\n\n[Openstatus](https://openstatus.dev) is a modern, open-source platform that tightly integrates status pages with powerful monitoring. It's built with a developer-first mindset, offering a cloud-hosted version.\n\n- **Key Features**:\n  - **Open-Source**: Provides full transparency.\n  - **Monitoring as Code**: Define all your monitoring checks in a single config.yaml file, perfect for version control.\n  - **Native OpenTelemetry**: First-class support for exporting your monitoring data using the OTel standard.\n  - **Private Locations**: Deploy lightweight agents within your own infrastructure to monitor internal services that aren't public-facing.\n\n- **Best for**: Teams who want a single, open-source, developer-centric solution that unifies uptime monitoring and status reporting.\n\n\n\n### 2. Status.io\n\n<Image\n  alt=\"status.io\"\n  src=\"/assets/posts/top-five-atlassian-statuspage-alternatives/statusio.png\"\n  width={650}\n  height={575}\n/>\n\n[Status.io](https://status.io) s a long-standing, cloud-based status page provider used by major companies like Docker. It focuses purely on status communication and incident management, positioning itself as a dedicated communication hub rather than a monitoring tool.\n\n- **Key Features**:\n  - **Incident Management**: Robust tools for managing and communicating incidents to subscribers.\n  - **Subscriber Notifications**: A wide range of notification options (Email, SMS, Webhooks, Slack).\n\n- **Best for**: Organizations seeking a straightforward status page service without integrated monitoring capabilities.\n\n### 3. Datadog Status Page\n\n<Image\n  alt=\"Datadog Status Page\"\n  src=\"/assets/posts/top-five-atlassian-statuspage-alternatives/datadog.png\"\n  width={650}\n  height={575}\n/>\n\nFor teams already embedded in the Datadog ecosystem, their [Status Page](https://www.datadoghq.com/blog/status-pages/) product is a natural extension. It leverages your existing monitoring data, dashboards, and incident management workflows.\n\n- **Key Features**:\n  - **Deep Ecosystem Integration**: Automatically link status page incidents to Datadog monitors, logs, and traces.\n  - **Unified Platform**: No need for a separate tool or integration if you're already a heavy Datadog user.\n\n- **Best for**: Organizations already using Datadog for monitoring who want to consolidate their tooling and leverage existing data.\n\n\n### 4. Instatus\n\n<Image\n  alt=\"Instatus\"\n  src=\"/assets/posts/top-five-atlassian-statuspage-alternatives/instatus.png\"\n  width={650}\n  height={575}\n/>\n\n[Instatus](https://instatus.com) has strong focus on integrating with your existing tools. It's a popular choice for modern tech companies that want an automated status page.\n\n- **Key Features**:\n  - **Third-Party Integrations**: Its primary strength is connecting to external monitoring tools (like UptimeRobot, Pingdom, DataDog) to automate status updates.\n  - **Emerging Monitoring**: No need for a separate tool or integration if you're already a heavy Datadog user.\n\n- **Best for**: Teams looking to leverage their existing monitoring to update their status page.\n\n\n\n\n### 5. Betterstack\n\n<Image\n  alt=\"Betterstack\"\n  src=\"/assets/posts/top-five-atlassian-statuspage-alternatives/betterstack.png\"\n  width={650}\n  height={575}\n/>\n\n[Betterstack](https://betterstack.com/status-page/) bundles uptime monitoring, incident management, and status pages into a single, cohesive platform. It's well-regarded for its generous free tier and all-in-one approach.\n\n- **Key Features**:\n  - **All-in-One Solution**:A single platform for monitoring, on-call scheduling, incident management, and status pages.\n  - **Generous Free Tier**: The free monitoring tier is a great starting point for solo developers.\n\n- **Best for**: solo developers who can benefit from its powerful free tier.\n\n## Conclusion\n\nWhile Atlassian Statuspage pioneered the status page market, several alternatives now offer compelling features and benefits. Your choice depends on your specific needs:\n\n- **OpenStatus** is ideal for those seeking an open-source solution with monitoring capabilities.\n- **Status.io** is suitable for organizations looking for a straightforward status page service.\n- **Datadog Status Page** is perfect for existing Datadog users wanting seamless integration.\n- **Instatus** if you love your current monitoring tools but want a status page.\n- **Betterstack** is a great option for those looking for an all-in-one monitoring and status page solution.\n\n\n## Need Help or Have Questions?\n\nIf you need help along the way, feel free to join our [Discord community](https://www.openstatus.dev/discord), check our [documentation](https://docs.openstatus.dev) for more information or reach out to us via [email](mailto:ping@openstatus.dev)\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/why-every-saas-needs-a-status-page.mdx",
    "content": "---\ntitle: \"Why Every SaaS Needs a Status Page\"\ndescription: \"Status pages aren't optional anymore. Learn why transparent service communication reduces support load, retains customers, and builds trust that marketing campaigns can't replicate.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-02-15\"\ncategory: \"education\"\nfaq:\n  - question: \"Do I really need a status page if my service rarely goes down?\"\n    answer: \"Yes. Users expect transparency from every SaaS product now. Not having a status page signals you're either hiding problems or haven't thought about reliability. Both destroy trust with technical buyers and enterprise customers.\"\n  - question: \"Won't a status page make us look unreliable?\"\n    answer: \"The opposite. Hiding incidents makes you look unreliable. A status page with honest incident history proves you take reliability seriously and communicate transparently. Companies like Stripe and Vercel publish detailed postmortems - it builds trust, not doubt.\"\n  - question: \"We're a small startup - shouldn't we wait until we're bigger?\"\n    answer: \"No. Start simple now. A basic status page takes 5 minutes to set up and establishes good habits early. Waiting until after your first major incident means angry customers watching you scramble to set up infrastructure you should have had from day one.\"\n  - question: \"How does a status page reduce support load?\"\n    answer: \"During incidents, hundreds of customers ask 'is it down?' Instead of flooding support with tickets, they check your status page and get an immediate answer. Your support team can focus on helping users affected by the incident rather than repeating the same status update.\"\n  - question: \"What's the ROI of a status page?\"\n    answer: \"Direct ROI: massive reduction in support tickets during incidents, lower customer churn from bad experiences, faster enterprise sales cycles. Indirect ROI: SEO traffic from '[your-product] status' searches, trust building that leads to word-of-mouth, and competitive differentiation against companies that hide their incidents.\"\n  - question: \"Should we build our own status page?\"\n    answer: \"No. When your main site goes down, your homegrown status page hosted on the same infrastructure goes down with it. Building reliable status infrastructure means separate hosting, static generation, minimal dependencies, and monitoring for the status page itself. Use a dedicated service or open-source solution designed to stay up when everything else is broken.\"\n---\n\n**TL;DR** - Status pages aren't a nice-to-have for SaaS products anymore. They're table stakes. Proactive communication reduces support load, retains customers during incidents, and builds trust that no marketing campaign can replicate. Not having one actively hurts your business.\n\n---\n\n## You Will Have an Outage\n\nEvery SaaS has incidents. Your database will fail. Your API will return 500s. Your CDN provider will have an outage. AWS will go down and take half the internet with it.\n\nThe question isn't *if* you'll have downtime - it's *how you'll communicate when it happens*.\n\nWhen your service breaks at 2 PM on a Tuesday and users can't complete their workflows, they have two options: check your status page or flood your support channels asking \"is it down?\"\n\nIf you don't have a status page, they choose Twitter. Reddit. Your support email. Your founder's DMs. Now you're managing an incident *and* a communication crisis simultaneously.\n\n**You lose control of the narrative.** Users speculate. Competitors watch. Journalists screenshot the chaos. By the time you post an explanation, the story is already written - and it's not the one you wanted.\n\nA status page turns chaos into clarity. One source of truth. Real-time updates. No speculation.\n\n## It's Expected Now - Especially for B2B\n\nEnterprise customers don't evaluate whether you *should* have a status page. They assume you do. Security questionnaires explicitly ask for your status page URL. Compliance frameworks like SOC 2 expect documented incident communication processes. Procurement teams check during vendor evaluation. Regulators in financial services and healthcare require documented incident communication.\n\nThe absence of a status page is a red flag that signals operational immaturity. It's the reliability equivalent of not having an SSL certificate.\n\nTry closing an enterprise contract without being able to point to:\n- Historical uptime data\n- Transparent incident communication\n- Documented [SLAs backed by real monitoring](/guides/sla-vs-slo-vs-sli)\n- Incident response process with public accountability\n\nYou'll lose deals to competitors who can. Enterprise buyers actively reward vendors who demonstrate transparency through status pages and [public postmortems](/guides/public-postmortem-underrated-marketing).\n\nFor B2C products, the stakes are different but just as real. When your app feels slow, users Google \"[your product] status\" before they even refresh the page. If third-party status checkers dominate those search results instead of *your* official page, you've lost control of the narrative.\n\n## Reduces Support Load (Immediately)\n\nDuring an incident, your support team gets bombarded with the same question hundreds of times: \"Is it down?\"\n\nWithout a status page, they spend hours sending the same canned response. With one, they send a link. The status page answers:\n- Yes, we're aware\n- Here's what's affected\n- Here's what we're doing about it\n- Here's when we expect resolution\n\nDuring a single hour-long outage, that can deflect 200-500 support tickets—letting your team focus on recovery instead of communication. Faster resolution for users who *actually* need help beyond \"yes, it's down.\" And a support team that stays focused instead of drowning in duplicates.\n\n**Your support team's morale matters too.** Answering \"is it down?\" for the 200th time at 3 AM destroys morale. A status page that deflects those questions automatically means less stress, less burnout, and a team that can focus on actually helping users recover.\n\n## Trust and Transparency Beat Silence\n\nSilence during outages destroys credibility. Users assume the worst: you don't know what's wrong, you're scrambling, you're hiding something.\n\n**One bad incident response can drive customers to competitors.** In a study of SaaS churn, poor communication during incidents was cited more often than the technical failure itself. The outage might be forgivable - the silence isn't.\n\nBeing upfront - even when the news is bad - shows professionalism. [Public postmortems](/guides/public-postmortem-underrated-marketing) prove you understand your systems and take incidents seriously. Companies like GitLab and Vercel have turned potential PR disasters into proof of engineering maturity by being radically transparent.\n\n**Your status page is part of that transparency.** It signals: we measure our uptime, we communicate during incidents, and we don't hide from problems.\n\nHere's the behavioral psychology: **transparency builds trust immediately; silence erodes it with every passing minute**. Customers are more forgiving when informed. A 30-minute outage with real-time updates feels manageable. A 30-minute outage with zero communication feels like negligence.\n\nPublish updates immediately when incidents happen. Don't wait for root cause analysis. Don't wait until you have all the answers. **Timing beats perfection.** \"We're investigating an issue affecting login\" posted 2 minutes after detection builds more trust than a perfect explanation posted 30 minutes later.\n\n## SEO and Discovery (Free Marketing)\n\nYour status page ranks for high-intent searches:\n- \"[your product] status\"\n- \"is [your product] down\"\n- \"[your product] uptime\"\n\nThese searches spike during incidents - exactly when you *need* to control the message. If you don't have a status page, third-party status checkers and Reddit threads dominate those results. Now users are reading speculation instead of facts.\n\nA well-maintained status page:\n- Captures that search traffic\n- Establishes you as authoritative about your own service\n- Becomes a backlink magnet (other sites reference your status updates)\n- Shows up in competitor research (\"why does [competitor] have a status page and we don't?\")\n\nThat's organic visibility you don't pay for. It compounds over time.\n\n## Proactive Notifications Turn Incidents Into Experiences\n\nA status page isn't just a destination users visit. It's a notification system.\n\nWhen users subscribe to your status page (via email, SMS, Slack, RSS), you turn a reactive \"is it down?\" experience into proactive communication. They get incident notifications before they even notice the problem. That transforms the narrative from \"your service failed me\" to \"your team is on top of it.\"\n\n**This matters for stakeholders too.** Your customer success team needs to know about incidents before their enterprise customers escalate. Your sales team needs to know before prospects ask \"is this happening often?\" Your leadership needs real-time visibility without pestering engineering.\n\nOne status page. Multiple subscriber channels. Everyone gets updates simultaneously. No information asymmetry. No \"why didn't anyone tell me?\"\n\n## Internal Accountability (That Makes Your Product Better)\n\nBuilding a status page forces uncomfortable but necessary questions about reliability:\n- What uptime are we actually achieving? (Not what we think—what we can prove.)\n- What [SLAs can we realistically offer](/guides/sla-vs-slo-vs-sli)? (And can we afford to miss them?)\n- How quickly do we detect and respond to incidents? (Minutes? Hours? Do we even know?)\n- What does our incident management process look like? (Or do we just scramble?)\n\nThese aren't philosophical questions once you have paying customers depending on you. A status page makes them impossible to ignore.\n\nIt also creates historical data for post-mortems. When did the incident start? How long did it take to detect? How long to resolve? This data turns vague \"we need to be more reliable\" conversations into concrete \"we need to cut detection time from 12 minutes to 2 minutes\" action items.\n\n## Common Objections (Answered)\n\n### \"We don't have outages\"\n\nYou will. And when you do, scrambling to set up a status page *during* an incident looks worse than not having one at all. Build it now, while everything is calm.\n\n### \"It's too expensive\"\n\nIt's not. Open-source options exist. Free tiers exist. The cost of *not* having one - lost deals, support overhead, customer churn - is higher.\n\n### \"It makes us look unreliable\"\n\nHiding problems makes you look unreliable. A status page with honest incident history proves you're transparent and mature about operations. [Boring, fast, and honest](/guides/boring-is-better-for-status-pages) beats flashy and evasive.\n\n### \"We're too small\"\n\nYou're the perfect size to build good habits. Startups who wait until they're \"big enough\" end up implementing it during a crisis instead of proactively. Start simple. Grow into it.\n\n### \"We'll build one when we need it\"\n\nYou need it *before* the incident. Customers checking your status page during downtime and finding nothing is worse than finding transparent updates.\n\n### \"We'll build our own\"\n\nBad idea. When your main site goes down, your homegrown status page - hosted on the same infrastructure, using the same database, deployed to the same servers - goes down with it.\n\nBuilding status infrastructure that actually stays up during outages means:\n- Separate hosting (different provider, different region)\n- Static generation that doesn't depend on your database\n- Minimal dependencies that won't fail when everything else does\n- Monitoring and alerting for the status page itself\n\nThat's 40-80 hours of engineering time up front, plus ongoing maintenance for infrastructure that isn't your product. Use a dedicated service or open-source solution designed to stay up when everything else is broken.\n\n## What Makes a Good Status Page\n\nNot all status pages are created equal. Here's what actually matters:\n\n**[Public by default](/guides/public-vs-private-status-pages)** - unless you have a specific reason to restrict access (internal teams, enterprise customers), make it public. Transparency builds trust.\n\n**[Boring and fast](/guides/boring-is-better-for-status-pages)** - no animations, no JavaScript dependencies that fail during incidents. Static HTML that loads instantly when everything else is broken.\n\n**Real data, not theater** - show actual uptime. Don't round numbers to make them look better. [Uptime percentages alone are misleading](/guides/why-uptime-percentage-is-misleading) (99.9% could mean three 8-hour outages), but they're better than nothing.\n\n**Incident history** - don't hide past outages. Show resolved incidents with timelines. It proves you've handled incidents before and will again.\n\n**Readable updates** - during active incidents, write for stressed users who need clarity. No jargon. No corporate speak. Just: what broke, what we're doing, when we expect resolution.\n\n## Getting Started\n\nYou don't need perfection. You need *something*. **Ship a basic status page today.**\n\nStart with:\n1. A simple page showing current status (operational, degraded, down)\n2. Automated monitoring that updates status in real-time\n3. Subscriber notifications (email at minimum)\n4. A process for posting incident updates immediately - not after root cause analysis\n5. Historical uptime data\n\nThat's it. You can add metrics, regional breakdowns, and advanced features later. The important part is having *a page* before you need it.\n\n**Open-source options exist** - OpenStatus, Uptime Kuma, Cachet. **Hosted platforms exist.** Pick one and ship it this week. Waiting for the \"right\" solution means you won't have one when it matters.\n\n## The Bottom Line\n\nEvery SaaS will have incidents. The difference between companies that retain trust and companies that lose it comes down to communication.\n\nA status page:\n- **Reduces support load by 60-80%** during incidents\n- **Prevents customer churn** - don't let silence be the bad experience that drives users away\n- **Controls the narrative** before speculation takes over social media\n- Meets enterprise and regulatory expectations\n- **Enables proactive notifications** so stakeholders learn about incidents from you, not from angry users\n- Captures SEO traffic and builds long-term authority\n- Forces internal accountability and incident response discipline\n\nNot having one is actively hurting your business. You're losing enterprise deals, frustrating customers, burning support team morale, and letting competitors control your narrative during incidents.\n\nUse a dedicated service or open-source solution designed to stay up when your infrastructure fails—building your own status page on the same infrastructure as your product defeats the entire purpose.\n\nStart today. Not after your first major incident.\n\n---\n\n**Openstatus makes this easy.** Set up a public or private status page in under 5 minutes. Automated monitoring. Real-time updates. Subscriber notifications. Open-source and transparent - so you can trust it won't fail when everything else does.\n\nStart free. No credit card required. Cancel anytime.\n\n<ButtonLink href=\"https://app.openstatus.dev\">Try openstatus free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/guides/why-uptime-percentage-is-misleading.mdx",
    "content": "---\ntitle: \"Why Uptime Percentage Alone is Misleading\"\ndescription: \"99.9% uptime sounds impressive until you realize it tells you nothing about user experience. Learn why availability percentages hide the real story and what to measure instead.\"\nauthor: \"openstatus\"\npublishedAt: \"2026-02-13\"\ncategory: \"education\"\n---\n\n99.9% uptime sounds great in sales decks. It makes executives nod approvingly and looks impressive on your status page.\n\nBut here's the truth: it's a vanity metric that hides more than it reveals.\n\nYou can have excellent uptime percentages and terrible user experience. Teams spend months optimizing for an incomplete metric while their users suffer. Let's talk about why.\n\n## The Math Hides Context\n\n99.9% uptime means 43 minutes of downtime per month. Simple math, right?\n\nBut **when** matters:\n\n- **Scenario A:** 43 separate 1-minute blips spread throughout the month. Users barely notice. They refresh and move on.\n- **Scenario B:** A single 43-minute outage at 2 PM on a Tuesday. Your CEO's phone explodes. Angry customers flood support. Social media erupts.\n\nBoth scenarios give you 99.9% uptime. The impact? Radically different.\n\n**What** broke matters too:\n\nYour homepage returns 200 OK. Monitoring shows green across the board. You're patting yourself on the back for that 99.9% availability.\n\nMeanwhile, your payment API throws 503s. Your EU region is down. Your CDN times out. Aggregate those numbers and you still hit \"99.9% available.\"\n\nAnd **who** was affected matters most:\n\nA 99.95% success rate looks stellar. But dig deeper: those 50 failures were all from one enterprise customer. They experienced 0% availability while your dashboard bragged about five nines.\n\nClustering disappears in averages.\n\n---\n\nWe've built an uptime SLA calculator to help you understand the real impact.\n\n<ButtonLink href=\"/play/uptime-sla\">Uptime SLA Calculator</ButtonLink>\n\n---\n\n\n## \"Available\" ≠ \"Usable\"\n\nYour health check endpoint returns 200 OK in 50ms. Monitoring says \"up.\" Dashboard is green. Perfect.\n\nYour users make actual requests. They get 200 OK responses... in 8 seconds. To them, your service is broken.\n\nYou can have 99.9% uptime with P99 latency at 30 seconds. Technically available. Completely unusable.\n\n**Availability measures if it responds. Reliability measures if it works.**\n\n## The Five Nines Trap\n\nTeams obsess over pushing 99.9% to 99.99%. The cost grows exponentially in engineering time, infrastructure, and complexity.\n\nMeanwhile: 10-second P99 latencies. 2% payment failure rates. Broken features left unfixed.\n\nYou hit your uptime target. Users churn anyway.\n\n**Optimizing the wrong metric is worse than not measuring at all.**\n\n## What to Measure Instead\n\nStop obsessing over a single percentage. Start measuring what users actually experience:\n\n**Latency percentiles:** P50, P95, P99. How long do real requests take? A P99 of 10 seconds means 1 in 100 users have a terrible experience, even with perfect uptime.\n\n**Error rates:** Break them down by endpoint, status code, and region. A 0.1% global error rate could be a 5% error rate for your checkout endpoint.\n\n**User journey success:** Can users actually complete the things that matter? Login, checkout, file upload. Track the whole flow, not just individual endpoints.\n\n**Regional availability:** Don't aggregate global uptime into one number. Your service down in Asia won't show up if North America is fine.\n\n**Error budget burn rate:** Are you on track to blow your SLO? This metric is actionable. A percentage alone tells you nothing about trajectory.\n\n## Bottom Line\n\nUptime percentage is useful for SLA tracking and executive dashboards. It's not useless—but it's insufficient for understanding user experience.\n\nMeasure what users feel: latency, errors by feature, regional failures. If your monitoring shows green while users are frustrated, you're measuring the wrong things.\n\nStop optimizing for a number that looks good in reports. Start optimizing for the experience your users actually have.\n\n---\n\nStart free. No credit card required. Set up your first status page in under 5 minutes.\n\n<ButtonLink href=\"https://app.openstatus.dev\">Try openstatus free</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/home.mdx",
    "content": "---\ntitle: \"Ship your status page before your SOC 2 auditor asks for it\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"The open-source status page trusted by growing teams. Communicate incidents, prove compliance readiness, and monitor uptime from 28 global regions.\"\ncategory: \"product\"\nfaq:\n  - question: \"What is openstatus?\"\n    answer: \"Openstatus gives you a branded status page and uptime monitoring that's audit-ready out of the box. Set up status.yourcompany.com, connect your monitors, and start communicating incidents — in minutes. It's open-source, self-hostable, and used by teams like Cal.com, WhiteBIT, and Documenso.\"\n  - question: \"Do I need a status page for SOC 2?\"\n    answer: \"SOC 2's CC2.3 criteria requires you to demonstrate incident communication with external parties — but it doesn't prescribe a specific tool. That said, a status page is the fastest, most auditor-friendly way to satisfy that requirement. Every status report on openstatus is timestamped and documented automatically, giving you an audit-ready trail of how you communicated during incidents. Most teams set it up in under 10 minutes.\"\n  - question: \"How does openstatus help with SOC 2 compliance?\"\n    answer: \"Openstatus gives you a branded status page with incident history, subscriber notifications, and maintenance windows — all the evidence an auditor needs to verify your incident communication process. Every status report and update is timestamped and documented automatically.\"\n  - question: \"What does the free plan include?\"\n    answer: \"The free plan includes one monitor, one status page with three page components, and a minimum check interval of 10 minutes. No credit card is required, and you can upgrade or cancel at any time.\"\n  - question: \"Who is behind openstatus?\"\n    answer: \"Openstatus is built by Thibault and Max, a bootstrapped two-person team. We're profitable and self-funded — we'll be here when your next audit comes around.\"\n  - question: \"What regions does openstatus monitor from?\"\n    answer: \"Openstatus monitors from 28 regions worldwide: Europe (Amsterdam, Stockholm, Paris, Frankfurt, London), North America (Dallas, New Jersey, Los Angeles, San Jose, Chicago, Toronto), South America (São Paulo), Asia (Mumbai, Tokyo, Singapore), Africa (Johannesburg), and Oceania (Sydney).\"\n  - question: \"Can I self-host openstatus?\"\n    answer: \"Yes. Openstatus is fully open source and can be self-hosted using its 8.5MB Docker image. You can also deploy private monitoring locations behind your firewall for internal services. The source code is available on GitHub.\"\n---\n\n<div className=\"space-x-2\">\n  <ButtonLink href=\"https://app.openstatus.dev\" children=\"Dashboard\" />\n  <ButtonLink\n    variant=\"ghost\"\n    href=\"https://openstatus.dev/github\"\n    children=\"GitHub 8k+\"\n  />\n</div>\n\n<Image src=\"/assets/landing/statuspage-meow.png\" alt=\"statuspage\" priority />\n\n## Trusted by teams who ship transparency\n\n<Grid cols={4}>\n  <a href=\"https://status.cal.com\">Cal.com</a>\n  <a href=\"https://status.documenso.com\">Documenso</a>\n  <a href=\"https://status.whitebit.com\">WhiteBIT</a>\n  <a href=\"https://status.midday.ai\">Midday</a>\n  <a href=\"https://status.openpanel.dev\">OpenPanel</a>\n  <a href=\"https://probostatus.com\">Probo</a>\n  <a href=\"https://status.hanko.io\">Hanko</a>\n  <a href=\"https://status.superwall.com\">Superwall</a>\n  <a href=\"https://status.streamelements.com/\">StreamElements</a>\n  <a href=\"https://status.smplrspace.com\">Smplrspace</a>\n  <a href=\"https://passboltuptime.com\">Passbolt</a>\n  <a href=\"https://twenty-status.com\">TwentyCRM</a>\n</Grid>\n\n## Beautiful, open-source status pages\n\nA status page helps you communicate incidents more effectively. It adds **transparency**, so users aren't left guessing. It enables **proactive communication**, giving updates without users needing to ask. And it shows **reliability**, not just in uptime but in how you handle downtime and keep people informed.\n\nMake it yours with [themes from our Theme Store](https://themes.openstatus.dev), custom domains, and branding. Share publicly or password protect for internal teams. Keep everyone in the loop with status reports, maintenance windows, and subscriptions.\n\n- **Customization** with our Theme Store\n- Public or **password protected** pages\n- **Custom domains**\n- **Status reports** and **maintenance windows**\n- **Subscription channels**: email, RSS/Atom, SSH\n\nRead more [about status pages](/status-page).\n\n## Monitor from 28 regions — know before your customers do\n\nMonitor your endpoints from 28 regions across multiple clouds. Get alerted on Slack, Discord, PagerDuty, or email the moment something breaks. Your status page updates automatically — no manual work during incidents.\n\n- **28 regions**, 3 cloud providers — no blind spots\n- Monitor any **HTTP endpoint**, REST or GraphQL\n- Version your monitors with **YAML** and **CI/CD**\n- Monitor behind firewalls with a single **Docker container**\n- Alerts on **Slack**, **Discord**, **PagerDuty**, email, webhooks, ...\n\nRead more about [uptime monitoring](/uptime-monitoring).\n\n---\n\nCheck your website's latency\n\n<ButtonLink href=\"/play/checker\">Global Speed Checker</ButtonLink>\n\n---\n\n<Grid>\n<div>\n\n**Status Page**\n\n<Image src=\"/assets/landing/statuspage.png\" alt=\"statuspage home\" />\n\n[Visit our Theme Explorer](https://themes.openstatus.dev)\n\n</div>\n<div>\n\n**Dashboard**\n\n<Image src=\"/assets/landing/dashboard-logs.png\" alt=\"dashboard monitor response logs\" />\n\n[Go to Dashboard](https://app.openstatus.dev)\n\n</div>\n\n</Grid>\n\n## Frequently asked questions\n\n<Details summary=\"What is openstatus?\">\n\nOpenstatus gives you a branded status page and uptime monitoring that's audit-ready out of the box. Set up status.yourcompany.com, connect your monitors, and start communicating incidents — in minutes.\n\nIt's open-source, self-hostable, and used by teams like [Cal.com](https://status.cal.com), [WhiteBIT](https://status.whitebit.com), and [Documenso](https://status.documenso.com). Available as a [managed SaaS](https://app.openstatus.dev) or for [self-hosting](https://github.com/openstatushq/openstatus).\n\n</Details>\n\n<Details summary=\"Do I need a status page for SOC 2?\">\n\nSOC 2's CC2.3 criteria requires you to demonstrate incident communication with external parties — but it doesn't prescribe a specific tool. That said, a status page is the **fastest, most auditor-friendly** way to satisfy that requirement.\n\nEvery status report on openstatus is **timestamped** and documented automatically, giving you an audit-ready trail of how you communicated during incidents. Most teams set it up in under 2 minutes.\n\n</Details>\n\n<Details summary=\"How does openstatus help with SOC 2 compliance?\">\n\nOpenstatus gives you everything an auditor needs to verify your incident communication process:\n\n- **Branded status page** with custom domain\n- **Incident history** with timestamped status reports\n- **Subscriber notifications** so stakeholders are proactively informed\n- **Maintenance windows** for planned changes\n- **Password protection** for internal or client-specific pages\n\nYou can be SOC 2-ready in minutes, not weeks.\n\n</Details>\n\n<Details summary=\"What does the free plan include?\">\n\nThe free plan includes **one monitor**, **one status page** (with three page components), and a minimum check interval of `10m`. Check the pricing table for a full comparison.\n\nNo credit card required — upgrade or cancel at any time.\n\n</Details>\n\n<Details summary=\"Who is behind openstatus?\">\n\nOpenstatus is built by [Thibault](https://bsky.app/profile/thibaultleouay.dev) and [Max](https://x.com/mxkaske) — a bootstrapped two-person team building in public.\n\nWe're profitable and self-funded — we'll be here when your next audit comes around.\n\nRead more on [our about page](/about).\n\n<Grid>\n\n<Image src=\"/assets/posts/2026-roadmap/team-retreat.jpg\" alt=\"@ Kochel am See - November 2025\" />\n\n</Grid>\n\n</Details>\n\n<Details summary=\"What regions does openstatus monitor from?\">\n\nOpenstatus monitors from **28 regions worldwide** across all continents:\n\n**Europe**\n\nAmsterdam 🇳🇱 | Stockholm 🇸🇪 | Paris 🇫🇷 | Frankfurt 🇩🇪 | London 🇬🇧\n\n**North America**\n\nDallas 🇺🇸 | New Jersey 🇺🇸 | Los Angeles 🇺🇸 | San Jose 🇺🇸 | Chicago 🇺🇸 | Toronto 🇨🇦\n\n**South America**\n\nSão Paulo 🇧🇷\n\n**Asia**\n\nMumbai 🇮🇳 | Tokyo 🇯🇵 | Singapore 🇸🇬\n\n**Africa**\n\nJohannesburg 🇿🇦\n\n**Oceania**\n\nSydney 🇦🇺\n\n*Need a specific region?* Feel free to [contact us](mailto:ping@openstatus.dev) or join our [Discord](https://discord.gg/openstatus) — we're always looking to expand our coverage!\n\n</Details>\n\n<Details summary=\"Can I self-host openstatus?\">\n\nYes. Openstatus is fully open source and can be self-hosted using its **8.5MB Docker image**. You can also deploy **private monitoring locations** behind your firewall to check internal services not exposed to the internet.\n\n- [Self-hosting the full openstatus stack](https://docs.openstatus.dev/guides/self-hosting-openstatus/)\n- [Self-hosting the status page only](https://docs.openstatus.dev/guides/self-host-status-page-only/)\n\nThe source code is available on [GitHub](https://openstatus.dev/github).\n\n</Details>\n"
  },
  {
    "path": "apps/web/src/content/pages/product/status-page.mdx",
    "content": "---\ntitle: \"Status Page\"\npublishedAt: \"2025-11-10\"\nauthor: \"Maximilian Kaske\"\ndescription: \"Turn transparency into trust with a dedicated status page. Keep users informed during downtime, so you can focus on the fix.\"\ncategory: \"Product\"\nfaq:\n  - question: \"When should I use a public vs private status page?\"\n    answer: \"Use public status pages for customer-facing services where transparency builds trust. Use private status pages (password-protected or magic link) for internal tools, client-specific deployments, or when you need to control who receives status updates.\"\n  - question: \"What's the difference between monitors and external services in page components?\"\n    answer: \"Monitors are automatically synced with your OpenStatus uptime monitoring data and update in real-time. External services are manually managed components for third-party dependencies or systems you don't directly monitor but want to report status for.\"\n  - question: \"Can I use my own domain for the status page?\"\n    answer: \"Yes, you can configure custom domains to host your status page on your own domain (e.g., status.yourcompany.com) instead of the default OpenStatus subdomain. This keeps the experience consistent with your brand.\"\n  - question: \"How do status page subscriptions work?\"\n    answer: \"Users can subscribe to receive updates when you post status reports or maintenance notices. We support email notifications, RSS/Atom feeds for feed readers, and JSON feeds for programmatic consumption. Subscribers are automatically notified when you publish updates.\"\n  - question: \"Can I customize the appearance of my status page?\"\n    answer: \"Yes, use the Theme Store to apply community themes or create your own. Themes control colors, fonts, and layout. For private custom themes, contact us. You can also define which data to share (uptime percentages, response times, or manual reports only).\"\n  - question: \"What is the Slack agent and what can it do?\"\n    answer: \"The Slack agent lets you manage your status page directly from Slack using natural language. @mention @openstatus in any channel or thread to create incidents, post updates, and resolve reports — without leaving Slack. No slash commands required.\"\n  - question: \"Does the Slack agent publish status reports automatically?\"\n    answer: \"No. The agent always shows a confirmation card before publishing anything. You can review the drafted title, status, and message, then choose to Approve, Approve & Notify (sends notifications to all subscribers), or Cancel. Nothing goes public without your explicit approval.\"\n  - question: \"Which plan includes the Slack agent?\"\n    answer: \"The Slack agent is available on paid plans. Install it from your dashboard under Settings > Integrations.\"\n---\n\n## Why do you need a status page?\n\nA status page is a dedicated webpage that provides real-time information about the operational status of your services. It serves as a communication tool to inform your users about any ongoing incidents, maintenance activities, or service disruptions.\n\n<Grid>\n<div>\n\n**Uptime Status Page**\n\n<Image src=\"/assets/landing/statuspage.png\" alt=\"status page overview\" />\n\n</div>\n<div>\n\n**Events Page**\n\n<Image src=\"/assets/landing/statuspage-events.png\" alt=\"status page events list\" />\n\n</div>\n</Grid>\n\n## Why choose openstatus for your status pages?\n\nWe provide you with a good **mix of customization and opinionated options**.\n\n### Page Components\n\nYou can attach specific monitors to a status page. You can either populate the data from the **aggregated uptime** data **or manually** manage them. Read more [about uptime monitoring](/uptime-monitoring).\n\nPage components provide a **flexible structure** that supports both:\n- **Monitors**: Automatically synced with your uptime monitoring data\n- **External Services**: Manually managed components for third-party services or systems you don't directly monitor\n\nYou can **group page components** by their services, locations, or any logical grouping, and they will be collapsible for better organization.\n\n### Customization\n\nMatch your brand **appearance** by contributing your own theme to the community. We have build a **[Theme Store](https://themes.openstatus.dev)** that helps you create and use custom themes on your status page. Contribute your own theme. If you a private custom theme let us know via [email](mailto:ping@openstatus.dev?subject=Private%20Theme).\n\nYou can **define the values** you want to share with your users. If can decide between duration values/uptime ping values or purely based on manual status report updates.\n\nYou can create **custom domains** to keep the domain your users are used to.\n\n### Links\n\nAdd a **Get in touch** in touch button and add a specific **website** link or a **`mailto:`** address.\n\nInclude a **homepage** link to redirect the user to your page on clicking on the left-hand nav icon.\n\n### Subscriptions\n\nAllow your users **subscribe** to your status page, to automatically receive **updates** whenever you add a status report or maintenance to your status page.\n\nWe support following communication channels:\n\n- **Emails**\n- **RSS/Atom feeds**\n- **JSON**\n\n[Contact us](mailto:ping@openstatus.dev) if you are looking for specific a channel.\n\n### Audience\n\nBy default, your status page is public. For internal or client-specific pages, you can protect access using **password protection** or **magic link** authentication to control who can view your updates.\n\n\n### Slack Agent\n\nManage incidents without leaving Slack. Install the **@openstatus** Slack agent and @mention it in any channel or thread to create, update, and resolve status reports using plain language - no slash commands, no tab switching.\n\n> **@openstatus** our API is returning 500 errors for about 10% of requests. Can you create an incident on the status page?\n\nThe agent reads the conversation, looks up your status pages and components, and drafts a status report. Before anything goes public it posts a confirmation card so you can review the title, status, and message. Choose to **Approve**, **Approve & Notify** (sends notifications to all subscribers), or **Cancel**.\n\nThe agent is **thread-aware**: when you follow up in the same thread (_\"we found the root cause\"_ or _\"it's fixed\"_), it picks up the context and drafts the next update automatically. The entire incident lifecycle - from creation to resolution - can happen in a single Slack thread."
  },
  {
    "path": "apps/web/src/content/pages/product/uptime-monitoring.mdx",
    "content": "---\ntitle: \"Uptime Monitoring\"\npublishedAt: \"2025-11-10\"\nauthor: \"Thibault Le Ouay Ducasse\"\ndescription: \"Monitor your websites and APIs from 28 regions across multiple cloud providers. Get instant alerts via Slack, Discord, email, and more. Open source with a free plan available.\"\ncategory: \"Product\"\nfaq:\n  - question: \"How many regions should I monitor from?\"\n    answer: \"Start with 3-5 regions covering your main user geographies. More regions provide better global coverage but use more check quota. For critical services, monitor from all major regions (North America, Europe, Asia) to catch regional issues quickly.\"\n  - question: \"Why monitor from multiple cloud providers instead of just one?\"\n    answer: \"If your service runs on AWS and your monitoring also runs on AWS, you won't detect AWS-wide outages or network issues affecting AWS connectivity. Using Fly.io, Koyeb, and Railway ensures monitoring independence from your infrastructure provider.\"\n  - question: \"What's the difference between frequency settings?\"\n    answer: \"Frequency determines how often we check your service (e.g., every 30 seconds, 1 minute, 5 minutes, 10 minutes). Higher frequency (30s) catches issues faster but uses more checks. Lower frequency (10m) is sufficient for non-critical services and conserves quota.\"\n  - question: \"Can uptime monitoring trigger my status page automatically?\"\n    answer: \"Yes, you can configure monitors to automatically update your status page based on monitoring results. When assertions fail or thresholds are exceeded, the status page can reflect degraded or down status without manual intervention.\"\n  - question: \"How do thresholds and assertions work together?\"\n    answer: \"Assertions validate response correctness (status code, headers, body content) while thresholds define performance boundaries (degraded latency, timeout). Both can trigger alerts - assertions catch functional failures, thresholds catch performance degradation.\"\n  - question: \"What types of API endpoints can I monitor?\"\n    answer: \"You can monitor any HTTP/HTTPS endpoint including REST APIs, GraphQL APIs, webhooks, and third-party service endpoints. Openstatus supports all HTTP methods (GET, POST, PUT, DELETE, etc.) and custom headers for authentication.\"\n  - question: \"Should I use YAML with CLI or Terraform for monitoring as code?\"\n    answer: \"Use YAML + CLI for simplicity and if you're not already using Terraform. It's lightweight and easy to get started. Choose Terraform if you're managing infrastructure as code and want to integrate monitoring into your existing Terraform workflows for unified state management.\"\n  - question: \"Can I run private monitoring locations in different networks?\"\n    answer: \"Yes, you can deploy as many private location probes as needed across different networks, VPCs, or regions. Each gets its own API key and appears as a separate monitoring region in your dashboard. The Docker image is only 8.5MB and supports ARM64 and AMD64.\"\n---\n\n```\n+----------------+\n| Service to be  |\n| Monitored      |\n+----------------+\n      ▲\n      |\n      |   (Network Latency/Failures)\n      |\n+-----+-------+   +-----+-------+   +-----+-------+\n| Monitoring  |   | Monitoring  |   | Monitoring  |\n| Node (USA)  |   | Node (EU)   |   | Node (Asia) |\n+-----+-------+   +-----+-------+   +-----+-------+\n      |               |                 |\n      |---------------|-----------------|\n      ▼               ▼                 ▼\n+------------------------------------------------+\n|                 openstatus                     |\n|                                                |\n| - Sends automated requests (e.g., pings or     |\n|   HTTP checks) from all nodes at set intervals |\n| - Records response time and success/failure    |\n| - Compares results from different nodes        |\n| - If a failure or a slow response is detected, |\n|   it triggers an alert.                        |\n+------------------------------------------------+\n      |\n      | (Alerts: Email, SMS, Slack, etc.) 🔔\n      |\n+-----+-----+\n| Your Team |\n+-----------+\n```\n\n\n## Why uptime monitoring is important?\n\nUptime monitoring is your first line of defense to ensure your service is available for your customers. By monitoring your service from multiple regions around the world, you can be sure that your customers are able to reach your service.\n\n\n<Grid>\n<div>\n\n**Uptime Dashboard**\n\n<Image src=\"/assets/landing/dashboard.png\" alt=\"dashboard monitor charts\" />\n\n</div>\n<div>\n\n**Response Logs**\n\n<Image src=\"/assets/landing/dashboard-logs.png\" alt=\"dashboard monitor logs\" />\n\n</div>\n</Grid>\n\n## Why choose openstatus for uptime monitoring?\n\n### Global Monitoring\n\nYou can monitor your services from multiple regions around the world. We currently have our probes deployed in **28 regions**.\n\nCheck all regions in the [global speed checker](/play/checker).\n\n### Multi Cloud\n\nYou should avoid monitoring your services from the same cloud providers you use for those services. That's why our probes are deployed across three different cloud providers:\n\n- **Fly.io**\n- **Koyeb**\n- **Railway**\n\n### API Monitoring\n\nMonitor any HTTP/HTTPS endpoint including REST APIs, GraphQL APIs, webhooks, and third-party service endpoints. Openstatus supports all HTTP methods and custom headers for authentication.\n\n#### Assertions\n\nValidating your response can be important. You can do **status code**, **header** or **body text** assertions. Read more [in the docs](https://docs.openstatus.dev/reference/http-monitor/#assertions).\n\n#### Thresholds\n\nYou can set a **degraded at** and **timeout** threshold whenever required. We will send you a notification whenever the responses exceed the thresholds.\n\n#### Open Telemetry\n\nWhen needed, you can export to your **OTLP endpoint** the metrics for every request we do. Access the data and set yourself an **alert channel** in Grafana, Honeycomb etc. Read more [in our guide](https://docs.openstatus.dev/guides/how-to-export-metrics-to-otlp-endpoint/).\n\n### Notification channels\n\nSet different notification channels and **get notified** whenever a not satisfying your assertions or are exceeding the thresholds.\n\nWe support:\n\n- Socials: Slack, Discord, Telegram Bot\n- Direct: Email, SMS\n- Incident Management: OpsGenie, PagerDuty\n- Custom: Webhook, Ntfy\n\n### Status Pages\n\nAutomatically update your status page based on your uptime monitoring results. Push reports and communicate with your users during incidents. You can choose to **share response details** on your status page. Read more [about status pages](/status-page).\n\n### Monitoring as Code\n\nThe best way to **version** your monitor changes is via the openstatus CLI and its **`YAML` config**. Additionally, we provide a [Terraform Provider](https://registry.terraform.io/providers/openstatusHQ/openstatus/latest).\n\n```yaml\n# yaml-language-server: $schema=https://www.openstatus.dev/schema.json\nuptime-monitor:\n  name: \"Graphql\"\n  description: \"GitHub GraphQL API\"\n  frequency: \"10m\"\n  active: true\n  regions:\n    - iad\n    - ams\n    - syd\n    - jnb\n    - gru\n  retry: 3\n  kind: http\n  request:\n    url: https://api.github.com/graphql\n    method: POST\n    headers:\n      User-Agent: OpenStatus\n      Authorization: Bearer YOUR_TOKEN_HERE\n    body: |\n      {\n        \"query\": \"query { viewer { login }}\"\n      }\n```\n\nOnce set, apply your changes via [CLI](https://docs.openstatus.dev/tutorial/get-started-with-openstatus-cli/):\n\n```bash\nopenstatus monitors apply\n```\n\nCheck our [GitHub repository](https://github.com/openstatusHQ/cli-template) to see the `YAML` templates of how to monitor **MCP**, **GraphQL**,... endpoints. We also provide a simple [GitHub Action](https://github.com/marketplace/actions/openstatus-synthetics-ci) to run it in your **CI/CD**.\n\n### Private Locations\n\nSometimes the services you want to monitor are not accessible from the outside. Simply **deploy our probes** to your favorite **cloud provider**, your own **server** or on your own **virtual private cloud** (VPC).\n\nThe docker image is only **8.5MB** small and should easily fit on your infrastructure whether ARM64 or AMD64.\n\n```bash\ndocker pull ghcr.io/openstatushq/private-location:latest\n```\n\n```bash\ndocker run -d \\\n  --name openstatus-private-location \\\n  --restart=always \\\n  -e OPENSTATUS_KEY=$OPENSTATUS_KEY \\\n  ghcr.io/openstatushq/private-location:latest\n```\n\nYou can read more here:\n\n- [Cloudflare Containers guide](https://docs.openstatus.dev/guides/how-to-deploy-probes-cloudflare-containers/)\n- [Raspberry Pi deployment](/blog/deploy-private-locations-raspberry-pi)\n- [Private location docs](https://docs.openstatus.dev/tutorial/how-to-create-private-location/)\n\n<Grid>\n<div>\n\n**Status Page Monitors**\n\n<Image src=\"/assets/landing/statuspage-monitor.png\" alt=\"status page monitor details\" />\n\n</div>\n</Grid>\n\n---\n\nCheck your website's latency\n\n<ButtonLink href=\"/play/checker\">Global Speed Checker</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/tools/checker-slug.mdx",
    "content": "---\ntitle: \"Global Speed Checker\"\npublishedAt: \"2025-11-10\"\nauthor: \"Thibault Le Ouay Ducasse\"\ndescription: \"Check the speed of your website from around the world.\"\ncategory: \"Product\"\nfaq:\n  - question: \"How long is the data stored?\"\n    answer: \"The data is stored for 7 days. If you want to keep it longer, consider creating an account at https://app.openstatus.dev and use the cloud solution.\"\n---\n\nThe data is getting stored for **7 days**. If you want to keep it longer, consider [creating an account](https://app.openstatus.dev) and use our cloud solution.\n\n---\n\n> **We have reworked the checker experience ([go back to v1](https://v1.openstatus.dev/play/checker))**. Please let us know if you are missing a feature from the older version. Contact us directly or send us a message to [ping@openstatus.dev](mailto:ping@openstatus.dev). Happy to bring stuff back!\n"
  },
  {
    "path": "apps/web/src/content/pages/tools/checker.mdx",
    "content": "---\ntitle: \"Global Speed Checker\"\npublishedAt: \"2025-11-10\"\nauthor: \"Thibault Le Ouay Ducasse\"\ndescription: \"Check the speed of your website from around the world.\"\ncategory: \"Product\"\nfaq:\n  - question: \"What Is a Website Speed Checker?\"\n    answer: \"A Website Speed Checker is an online tool that measures how fast your website or API responds when someone visits it. It analyzes various website performance metrics including client-side performance (FCP, LCP, CLS) and server-side performance (DNS lookup, TCP connection, TLS handshake, server response time).\"\n  - question: \"What Is a Global Speed Checker?\"\n    answer: \"A Global Speed Checker measures your website or API's latency and response time from multiple locations around the world. OpenStatus runs checks from 28 global regions across 3 cloud providers, giving you a complete picture of your site's real-world performance.\"\n  - question: \"What can I do with openstatus Global Speed Checker?\"\n    answer: \"You can test how fast your API or website responds worldwide, compare latency across different regions, identify network bottlenecks, and monitor uptime and availability in real time from distributed locations across Europe, Asia, North America, and beyond.\"\n---\n\n> **We support the Global Speed Checker as agentic skill! [See changelog](/changelog/global-speed-checker-skills).**\n\n## Start monitoring your services\n\n<Grid cols={3}>\n  <div>\n\n**API & Website Speed**\n\nTest Enter your URL and get a website speed check. Get insights on page load, header details and timing phases (DNS, Connect, TLS, TTFB, Transfer) of the response.\n\n  </div>\n  <div>\n  \n**Global Speed Test**\n\nMonitor latency performance in different regions to ensure quick load times for users across 28 regions worldwide.\n\n  </div>\n  <div>\n\n**Share the Results**\n\nQuickly share the results of your website speed test with your team or clients. The results expire after 7 days, so you can easily collaborate on performance.\n\n  </div>\n</Grid>\n\n### What Is a Website Speed Checker?\n\nA Website Speed Checker is an online tool that measures how fast your website or API responds when someone visits it. It analyzes various website performance metrics to help you understand which elements slow down your page load time.\nSpeed checkers can focus on two aspects of performance:\n\n- **Client-side performance**, which includes metrics like First Contentful Paint (FCP), Largest Contentful Paint (LCP), and Cumulative Layout Shift (CLS) — all indicators of how quickly your site becomes visible and usable to visitors.\n- **Server-side performance (or network performance)**, which looks at the technical steps of a request such as DNS lookup, TCP connection, TLS handshake, and server response time.\n\nUnderstanding both sides helps you identify whether slowdowns are caused by your frontend assets or your backend infrastructure.\n\n### What Is a Global Speed Checker?\n\nA Global Speed Checker measures your website or API's latency and response time from multiple locations around the world. Instead of testing from just one data center, it runs checks from 28 global regions across 3 cloud providers, giving you a complete picture of your site's real-world performance.\n\n---\n\nWith OpenStatus, you can:\n\n- Test how fast your API or website responds worldwide.\n- Compare latency across different regions.\n- Identify network bottlenecks.\n- Monitor uptime and availability in real time.\n\nWhether you want to test your website speed from Europe, Asia, North America, or beyond, our Global Speed Checker gives accurate, consistent data from distributed locations.\n\nIf you'd like to request additional test regions or providers, feel free to contact us at [ping@openstatus.dev](mailto:ping@openstatus.dev).\n"
  },
  {
    "path": "apps/web/src/content/pages/tools/curl.mdx",
    "content": "---\ntitle: \"cURL Builder\"\npublishedAt: \"2025-11-10\"\nauthor: \"Maximilian Kaske\"\ndescription: \"An online curl command line builder. Generate curl commands to test your API endpoints.\"\ncategory: \"Product\"\nfaq:\n  - question: \"What is cURL?\"\n    answer: \"cURL (Client URL) is a command-line tool and library for transferring data with URLs. It supports various protocols like HTTP, HTTPS, FTP, and more, making it a versatile choice for testing APIs, downloading files, or performing network tasks. It's available on most operating systems, including Linux, macOS, and Windows.\"\n---\n\n## What is cURL?\n\ncURL (Client URL) is a command-line tool and library for transferring data with URLs. It supports various protocols like HTTP, HTTPS, FTP, and more, making it a versatile choice for testing APIs, downloading files, or performing network tasks. Its simplicity and power come from the ability to execute complex operations through straightforward commands.\n\ncURL is available on most operating systems, including Linux, macOS, and Windows.\n"
  },
  {
    "path": "apps/web/src/content/pages/tools/severity-matrix.mdx",
    "content": "---\ntitle: \"Incident Severity Matrix Builder\"\npublishedAt: \"2026-02-26\"\nauthor: \"Maximilian Kaske\"\ndescription: \"Classify incidents with deterministic, auditable rules. Customize thresholds, test scenarios, and export a ready-to-use template for your runbook.\"\ncategory: \"Product\"\nfaq:\n  - question: \"What does SEV0 mean?\"\n    answer: \"SEV0 indicates a critical incident — typically a complete service outage or confirmed security breach that requires immediate response from senior engineering leadership. It's the highest severity level and triggers the most aggressive communication and escalation protocols.\"\n  - question: \"How many severity levels should we use?\"\n    answer: \"Most teams use 3 or 4 levels. Four levels (SEV0 through SEV3) provide enough granularity to distinguish between a full outage and a minor cosmetic bug without overcomplicating triage during a live incident. If you're a small team, 3 levels can work fine — you can always add granularity later.\"\n  - question: \"What is the difference between severity and priority?\"\n    answer: \"Severity measures the impact of an incident — how many users are affected and how badly. Priority reflects business urgency and resource allocation. A typo on your pricing page might be low severity but high priority if it's costing you conversions. Your severity matrix should classify based on impact alone; priority is a triage decision.\"\n  - question: \"Should security incidents always be SEV0?\"\n    answer: \"In most cases, yes. Security incidents carry outsized risk even when few users are immediately affected — the blast radius can expand quickly and the reputational impact is disproportionate. Treating all confirmed security incidents as SEV0 ensures you mobilize the right resources immediately.\"\n  - question: \"How often should we review our severity matrix?\"\n    answer: \"Review it quarterly, or after any major incident where the classification felt wrong. If your team consistently debates whether something is a SEV1 or SEV2, your thresholds probably need adjustment. The builder lets you customize thresholds so your matrix reflects your team's actual operational patterns.\"\n  - question: \"Is a severity matrix required for SOC 2?\"\n    answer: \"Not by name, but functionally yes. SOC 2 criterion CC7.4 explicitly requires understanding the 'nature and severity' of an incident to determine the appropriate response time frame. Auditors doing a Type II audit will sample real incidents and check whether your severity classification was applied consistently. Without a documented matrix, that evidence trail doesn't exist. ISO 27001 Annex A 5.25 is even more explicit — it directly mandates categorisation and prioritisation of security events.\"\n  - question: \"How does this relate to SLA compliance?\"\n    answer: \"Your severity levels determine how quickly you respond to incidents, which directly affects your cumulative downtime. If your SLA promises 99.9% uptime, you have roughly 8 hours and 46 minutes of allowed downtime per year. A single misclassified SEV0 treated as SEV2 could burn through that budget.\"\n---\n\n## Severity Matrix Template\n\nCopy these tables into your runbook, wiki, or Notion page.\n\n```markdown\n| Severity | Users Affected | Security | Response Time | Status Page Label | Communication | Postmortem |\n|----------|---------------|----------|---------------|-------------------|---------------|------------|\n| 🔴 SEV0 – Critical | ≥80% OR security incident | Yes | 15 minutes | Major Outage | Immediate public update + all-hands | Required |\n| 🟠 SEV1 – High | ≥50% | No | 30 minutes | Partial Outage | Public update within 15 min | Required |\n| 🟡 SEV2 – Medium | ≥10% | No | 2 hours | Degraded Performance | Status page update + ticket | Required (team) |\n| 🟢 SEV3 – Low | <10% | No | 1 business day | Minor Issue | Internal ticket only | Optional |\n```\n\nOnce you have your severity levels defined, pair them with explicit response expectations. This table tells every engineer exactly how fast to act and when to escalate — no guessing during an active incident.\n\n### Response Expectations Template\n\n```markdown\n| Severity | First Response | Update Cadence | Escalation Path | Auto-Escalate If |\n|----------|---------------|----------------|-----------------|-----------------|\n| SEV0 | 15 min | Every 15 min | VP Engineering + on-call | — |\n| SEV1 | 30 min | Every 30 min | Engineering lead | Not resolved in 2h → SEV0 review |\n| SEV2 | 2 hours | Every 2 hours | Team lead | Not resolved in 4h → SEV1 |\n| SEV3 | 1 business day | Daily | Assigned engineer | Spreads to more systems → re-classify |\n```\n\n## What Is an Incident Severity Matrix?\n\nAn incident severity matrix is a structured framework that classifies production incidents based on measurable impact: how many users are affected, whether security is compromised, and whether SLA commitments are at risk.\n\nWithout one, incident classification becomes subjective. Different engineers escalate differently. Status page updates are inconsistent. Response times vary depending on who's on call.\n\nA well-defined severity matrix solves this by making classification deterministic. Given the same inputs, every engineer arrives at the same severity level, the same response time expectation, and the same communication protocol.\n\n### Is it required for compliance?\n\nLargely, yes. SOC 2's CC7.4 criterion explicitly states that an organization must obtain \"an understanding of the nature and **severity** of the security incident\" to determine \"the appropriate containment strategy, including a determination of the appropriate response time frame.\" Without a documented severity framework, you cannot demonstrate consistent, auditable compliance during a Type II audit — where auditors sample real incidents and verify the classification was applied.\n\nISO 27001 goes further. Annex A control 5.25 directly mandates \"effective categorisation and prioritisation of information security events\" using impact, urgency, and priority as criteria. It is one of the most explicit requirements across any compliance framework for building exactly this kind of matrix.\n\nNIST SP 800-61, PCI DSS 12.10, and HIPAA's breach determination process all lean on the same concept: you cannot respond proportionally to something you haven't classified.\n\n## SEV0 vs SEV1 vs SEV2 vs SEV3\n\n**SEV0 – Critical**\nComplete outage or security breach. No workaround available. Requires immediate response from senior engineering leadership. Public status page should show \"Major Outage\" and updates should go out every 15 minutes. A postmortem is always required.\n\n**SEV1 – High**\nMajor degradation affecting most users. Significant business or revenue impact. Requires urgent response from engineering leads. Status page shows \"Partial Outage\" with 30-minute update cadence. A postmortem is required.\n\n**SEV2 – Medium**\nPartial degradation with limited user impact. Standard incident process applies. Status page shows \"Degraded Performance\" and updates go out every 2 hours. A team-level postmortem is required.\n\n**SEV3 – Low**\nMinor bug or cosmetic issue affecting a small percentage of users. Non-urgent resolution on a 1 business day timeline. Typically no public status page update is needed. Postmortem is optional.\n\nSee the [Incident Severity Matrix Template](/guides/incident-severity-matrix) for per-severity status page message templates, postmortem requirements, real-world examples, and tips.\n"
  },
  {
    "path": "apps/web/src/content/pages/tools/uptime-sla.mdx",
    "content": "---\ntitle: \"Uptime SLA Calculator - Convert Uptime % to Downtime\"\npublishedAt: \"2025-11-10\"\nauthor: \"Thibault Le Ouay Ducasse\"\ndescription: \"Free uptime SLA calculator. See how much downtime 99.9%, 99.99%, or 99.999% uptime allows per day, week, month, and year. Convert any uptime percentage to downtime instantly.\"\ncategory: \"Product\"\nfaq:\n  - question: \"What is an Uptime SLA?\"\n    answer: \"An Uptime SLA (Service Level Agreement) is a commitment between a service provider and a customer that guarantees a specific percentage of uptime over a given period. For example, a 99.9% SLA means the service can be down for no more than 8 hours and 45 minutes per year.\"\n  - question: \"What are common SLA tiers?\"\n    answer: \"Common SLA tiers include 99.9% (three nines) allowing 8h 45m downtime/year, 99.99% (four nines) allowing 52m 35s/year, and 99.999% (five nines) allowing only 5m 15s/year. Most cloud providers offer between 99.9% and 99.99% uptime SLAs.\"\n  - question: \"How much downtime does 99.9% uptime allow?\"\n    answer: \"99.9% uptime (three nines) allows approximately 8 hours and 45 minutes of downtime per year, 43 minutes per month, 10 minutes per week, or 1 minute and 26 seconds per day.\"\n  - question: \"What is the difference between 99.9% and 99.99% uptime?\"\n    answer: \"The difference is significant: 99.9% uptime allows about 8 hours 45 minutes of downtime per year, while 99.99% allows only about 52 minutes per year. That extra 9 reduces your allowed downtime by roughly 10x and typically requires redundant infrastructure and automated failover.\"\n  - question: \"How do I calculate uptime from downtime?\"\n    answer: \"To calculate uptime percentage from downtime, use this formula: Uptime % = ((Total time - Downtime) / Total time) × 100. For example, if your service was down for 1 hour in a 30-day month (720 hours), your uptime is ((720 - 1) / 720) × 100 = 99.86%.\"\n---\n\n_All calculations assume continuous 24/7 availability requirements._\n\n## Understanding Uptime SLA\n\nService Level Agreements (SLAs) define the expected performance and availability of your services. Understanding uptime percentages and their corresponding downtime allowances is crucial for maintaining customer trust and meeting compliance requirements.\n\n\nCommon SLA tiers include 99.9% (three nines), 99.99% (four nines), and 99.999% (five nines), each representing different levels of reliability. For example, 99.9% uptime allows for 8.77 hours of downtime per year, while 99.99% allows only 52.6 minutes annually.\n\n\nThis calculator helps you understand the real-world impact of your SLA commitments and plan for capacity, incident response, and stakeholder expectations.\n"
  },
  {
    "path": "apps/web/src/content/pages/unrelated/about.mdx",
    "content": "---\ntitle: \"About openstatus\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"Openstatus is built by Thibault and Max — a bootstrapped, profitable two-person team building the open-source status page for teams who ship transparency.\"\ncategory: \"company\"\nfaq:\n  - question: \"What is openstatus?\"\n    answer: \"Openstatus is an open-source status page and uptime monitoring platform founded in 2023 by Thibault Le Ouay Ducasse and Maximilian Kaske. It monitors websites, APIs, and services from 28 regions globally across multiple cloud providers. Openstatus is bootstrapped, profitable, and available both as a managed SaaS and for self-hosting.\"\n  - question: \"Why is openstatus bootstrapped?\"\n    answer: \"We're self-funded because it keeps us aligned with our customers, not investors. No VC pressure means we build features that matter to you, not features that look good on a pitch deck. We'll be here when your next SOC 2 audit comes around.\"\n---\n\n<Grid cols={3}>\n\n<div>\n\n<Image\n  className=\"aspect-square!\"\n  src=\"/assets/authors/thibault.jpeg\"\n  alt=\"Thibault Le Ouay Ducasse\"\n/>\n\n[BlueSky](https://bsky.app/profile/thibaultleouay.dev) | [LinkedIn](https://www.linkedin.com/in/thibault-le-ouay-ducasse/) | [GitHub](https://github.com/thibaultleouay/)\n\n</div>\n\n<div>\n\n<Image\n  className=\"aspect-square!\"\n  src=\"/assets/authors/max.png\"\n  alt=\"Maximilian Kaske\"\n/>\n\n[X](https://x.com/mxkaske) | [LinkedIn](https://www.linkedin.com/in/mxkaske/) | [GitHub](https://github.com/mxkaske/)\n\n</div>\n\n</Grid>\n\n## Our story\n\nWe met on Twitter in 2023 and started building openstatus together. What began as an **open-source** side project driven by curiosity grew into a real business with real impact.\n\nAlong the way, we've learned that **boring, reliable tech** beats chasing trends every time. We're committed to building openstatus for the long term.\n\n## Built to last, not to flip\n\nWe're **profitable and self-funded**. No VC pressure, no growth-at-all-costs. This matters for you because:\n\n- **We build for customers, not investors.** Every feature ships because it solves a real problem, not because it looks good on a pitch deck.\n- **We're not going anywhere.** When your SOC 2 audit comes around next year, we'll still be here. And the year after that.\n- **We stay lean and fast.** Two people, no bureaucracy. You can talk to the people who write the code.\n\n## Our mission\n\nWe're building the best open-source status page — connecting monitoring, incident communication, and compliance into a single platform. **Transparent by default**, for teams who believe their users deserve to know what's happening.\n"
  },
  {
    "path": "apps/web/src/content/pages/unrelated/not-found.mdx",
    "content": "---\ntitle: \"404 - Not Found\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"The page you are looking for does not exist.\"\ncategory: \"company\"\n---\n\n---\n\nAre you looking for something specific?\n\n- [API Reference](https://api.openstatus.dev/v1)\n- [Documentation](https://docs.openstatus.dev)\n- [Dashboard](https://app.openstatus.dev)\n- [Global Speed Checker](/play/checker)\n\nOr [Return Home](/)\n"
  },
  {
    "path": "apps/web/src/content/pages/unrelated/pricing.mdx",
    "content": "---\ntitle: \"Pricing\"\npublishedAt: \"2025-11-10\"\nauthor: \"Maximilian Kaske\"\ndescription: \"Start free with uptime monitoring and a status page. Upgrade to Starter ($30/mo) or Pro ($100/mo) for more monitors, team collaboration, and advanced features. No credit card required.\"\ncategory: \"company\"\n---\n\n| Features comparison | $0/month - Hobby |$30/month - Starter | $100/month - Pro |\n| --- | --- | --- | --- |\n| [Status Pages](/status-page) |  |  |  |\n| Number of status pages | 1 | 1 +$20/mo./each | 5 +$20/mo./each |\n| Number of components | 3 | 20 | 50 |\n| Maintenance status | + | + | + |\n| Toggle numbers visibility | + | + | + |\n| Subscribers |  | + | + |\n| Custom domain |  | + | + |\n| Slack Agent |  | + | + |\n| White Label |  | $300/mo. | $300/mo. |\n| **Audience** |  |  |  |\n| Password Protection |  | + | + |\n| Email Authentification |  | $100/mo. | $100/mo. |\n| [Monitors](/uptime-monitoring) |  |  |  |\n| Check Interval | 10m | 1m | 30s |\n| Number of monitors | 1 | 20 | 50 |\n| Multi-region monitoring | + | + | + |\n| Total regions | 6 | 28 | 28 |\n| Regions per monitor | 6 | 6 | 28 |\n| Data retention | 14 days | 3 months | 12 months |\n| Response logs |  | + | + |\n| Private locations |  |  | + |\n| OTel Exporter |  |  | + |\n| Number of on-demand checks | 30/mo. | 100/mo. | 300/mo. |\n| **Alerts** |  |  |  |\n| Slack, Discord, Email, Webhook | + | + | + |\n| WhatsApp |  | + | + |\n| SMS |  | + | + |\n| PagerDuty |  | + | + |\n| OpsGenie |  | + | + |\n| Grafana OnCall |  | + | + |\n| Number of notification channels | 1 | 10 | 20 |\n| **Collaboration** |  |  |  |\n| Team members | 1 | Unlimited | Unlimited |\n\nWe provide pricing support for **EUR**/**USD**/**INR** as currency. Contact us at [ping@openstatus.dev](mailto:ping@openstatus.dev) or [book a call](https://openstatus.dev/cal) if you have questions.\n\n**Add-ons** are workspace settings. Once enabled, you can use these configurations for every status page, monitor, etc. in your workspace.\n\n---\n\nGo to the dashboard and get started\n\n<ButtonLink href=\"https://app.openstatus.dev\">Create Account</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/unrelated/privacy.mdx",
    "content": "---\ntitle: \"Privacy\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"The privacy conduct for openstatus\"\ncategory: \"company\"\n---\n\nWelcome to [openstatus.dev](/) (the \"Site\"), hosted by OpenStatus (\"OpenStatus\",\n\"we\", \"us\", and/or \"our\"). OpenStatus provides a platform for creating and\nmanaging short links (the \"Services\")​1​. We value your privacy and are\ndedicated to protecting your personal data. This Privacy Policy covers how we\ncollect, handle, and disclose personal data on our Platform.\n\nIf you have any questions, comments, or concerns regarding this Privacy Policy,\nour data practices, or would like to exercise your rights, do not hesitate to\ncontact us.\n\n## To Whom Does This Policy Apply\n\nThis Privacy Policy applies to customers and site visitors. Each customer is\nresponsible for posting its own terms, conditions, and privacy policies, and\nensuring compliance with all applicable laws and regulations.\n\n## Changes To This Privacy Policy\n\nThis Privacy Policy may change from time to time, as our Platform and our\nbusiness may change. Your continued use of the Platform after any changes to\nthis Privacy Policy indicates your agreement with the terms of the revised\nPrivacy Policy.\n\n## What Information Do We Collect\n\nWe collect information directly from you when you provide it to us explicitly on\nour Site. We do not use third-party cookies on our Site.\n\n## What We Use Your Information For\n\nWe use your information to provide our Services, to improve our Platform, to\nunderstand how you use our Platform, and to communicate with you.\n\n## How To Contact Us\n\nFor privacy-related questions, please contact us at privacy@openstatus.dev.\n"
  },
  {
    "path": "apps/web/src/content/pages/unrelated/registry.mdx",
    "content": "---\ntitle: \"Component Registry\"\npublishedAt: \"2026-02-08\"\nauthor: \"openstatus\"\ndescription: \"Install beautiful, accessible React components for status pages. Pre-built, customizable components compatible with shadcn/ui.\"\ncategory: \"Documentation\"\n---\n\nBuild your status page in minutes, not hours. The openstatus registry provides production-ready React components specifically designed for status pages. Built on top of [shadcn/ui](https://ui.shadcn.com), these components are accessible, customizable, and battle-tested in production.\n\n## What is the Registry?\n\nThe openstatus registry is a collection of shadcn/ui-compatible, pre-built components for common status page patterns. Instead of building status indicators, event timelines, and monitoring displays from scratch, install and customize these components directly into your project.\n\nAll components are:\n- **Accessible** - Built with ARIA patterns and keyboard navigation\n- **Customizable** - Fully themeable with Tailwind CSS\n- **Type-safe** - Written in TypeScript with complete type definitions\n- **Production-ready** - Battle-tested in production at openstatus\n\n## Example\n\nHover over the example below to see which component is being used.\n\n<Grid cols={1}>\n\n<div>\n\n<StatusPageExample />\n\n</div>\n\n</Grid>\n\n> We are working on aligning component `Props` with [`@openstatus/sdk-node`](https://jsr.io/@openstatus/sdk-node) return values, enabling you to use our SDK to build your own custom status page.\n\nView the [GitHub source code](https://github.com/openstatusHQ/openstatus/tree/main/apps/web/src/content/shadcn-registry-example.tsx) for examples and usage patterns.\n\n## Installation\n\nInstall any component with a single [shadcn CLI](https://ui.shadcn.com/docs/cli) command:\n\n```bash\nnpx shadcn@latest add https://openstatus.dev/r/status-complete\n```\n\nOr install the registry first:\n\n```bash\npnpm dlx shadcn@latest registry add @openstatus\n```\n \n And install the component via:\n\n```bash\npnpm dlx shadcn@latest add @openstatus/status-complete\n```\n\n### Available Components\n\nComponents are organized by functionality:\n\n- **Collections:** `status-complete`, `status-essentials` - Full status page bundles\n- **Building Blocks:** `status-banner`, `status-bar`, `status-feed`, `status-events`, `status-component`, `status-icon`, `status-layout`, `status-timestamp` - Individual components\n\nExplore the full registry at [openstatus.dev/r/registry.json](https://openstatus.dev/r/registry.json)\n\n---\n\nWant to customize your theme?\n\n<ButtonLink href=\"https://themes.openstatus.dev\" children=\"Go to Themes Explorer\" />\n\n---\n\n## Learn More\n\n- [Source Code Example](https://github.com/openstatusHQ/openstatus/tree/main/apps/web/src/content/shadcn-registry-example.tsx) - See all components in action\n- [Blog Post](/blog/shadcn-component-registry) - How We Build Our shadcn Component Registry\n- [Openstatus SDK](https://jsr.io/@openstatus/sdk-node) - Build on top of openstatus with our SDK\n- [shadcn/ui Documentation](https://ui.shadcn.com) - Learn about the underlying component system\n- [Openstatus GitHub](https://github.com/openstatusHQ/openstatus) - Star the project and contribute\n- [Theme Explorer](https://themes.openstatus.dev) - Explore community themes\n\n## Support\n\nIf you encounter issues, have questions, or want to share feedback:\n\n- [Open an issue](https://github.com/openstatusHQ/openstatus/issues)\n- [Join our Discord](https://openstatus.dev/discord)\n- [Check the documentation](https://docs.openstatus.dev)\n- [Send an email](mailto:ping@openstatus.dev)\n"
  },
  {
    "path": "apps/web/src/content/pages/unrelated/subprocessors.mdx",
    "content": "---\ntitle: \"Subprocessors\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"The list of subprocessors for openstatus\"\ncategory: \"company\"\n---\n\n## List of Subprocessors\n\nVercel:\n\n- Purpose: Server hosting\n- Location: Global\n\nFly:\n\n- Purpose: Server hosting\n- Location: Global\n\nKoyeb:\n\n- Purpose: Server hosting\n- Location: Global\n\nRailway:\n\n- Purpose: Server hosting\n- Location: Global\n\nGCP:\n\n- Purpose: Queue\n- Location: EU\n\nTurso:\n\n- Purpose: Relational database (SQLite)\n- Location: Global\n\nTinybird:\n\n- Purpose: Analytical database (Clickhouse)\n- Location: EU\n\nStripe:\n\n- Purpose: payment processing\n- Location: US\n\nResend:\n\n- Purpose: Email sending\n- Location: EU\n\nOpenpanel:\n\n- Purpose: Product analytics\n- Location: EU\n"
  },
  {
    "path": "apps/web/src/content/pages/unrelated/terms.mdx",
    "content": "---\ntitle: \"Terms of Service\"\npublishedAt: \"2025-11-10\"\nauthor: \"openstatus\"\ndescription: \"The terms and conditions for openstatus\"\ncategory: \"company\"\n---\n\nSubject to these Terms of Service (this \"Agreement\"), [openstatus.dev](/)\n(\"OpenStatus\", \"we\", \"us\" and/or \"our\") provides access to OpenStatus's cloud\nplatform as a service (collectively, the \"Services\"). By using or accessing the\nServices, you acknowledge that you have read, understand, and agree to be bound\nby this Agreement.\n\nIf you are entering into this Agreement on behalf of a company, business or\nother legal entity, you represent that you have the authority to bind such\nentity to this Agreement, in which case the term \"you\" shall refer to such\nentity. If you do not have such authority, or if you do not agree with this\nAgreement, you must not accept this Agreement and may not use the Services.\n\n## 1. Acceptance of Terms\n\nBy signing up and using the services provided by OpenStatus (referred to as the\n\"Service\"), you are agreeing to be bound by the following terms and conditions\n(\"Terms of Service\"). The Service is owned and operated by OpenStatus (\"Us\",\n\"We\", or \"Our\").\n\n## 2. Description of Service\n\nOpenStatus provides an open source monitoring and status page tool. (\"the\nProduct\"). The Product is accessible at openstatus.dev and other domains and\nsubdomains controlled by Us (collectively, \"the Website\").\n\n## 3. Fair Use\n\nYou are responsible for your use of the Service and for any content that you\npost or transmit through the Service. You may not use the Service for any\npurpose that is illegal or infringes upon the rights of others.\n\nWe reserve the right to suspend or terminate your access to the Service if we\ndetermine, in our sole discretion, that you have violated these Terms of\nService, including but not limited to, adding phishing links, spam links, scam\nlinks, or other inappropriate or illegal content.\n\n## 4. Intellectual Property Rights\n\nYou acknowledge and agree that the Service and its entire contents, features,\nand functionality, including but not limited to all information, software, code,\ntext, displays, graphics, photographs, video, audio, design, presentation,\nselection, and arrangement, are owned by Us, our licensors, or other providers\nof such material and are protected by United States and international copyright,\ntrademark, patent, trade secret, and other intellectual property or proprietary\nrights laws.\n\n## 5. Changes to these Terms\n\nWe reserve the right to revise and update these Terms of Service from time to\ntime in our sole discretion. All changes are effective immediately when we post\nthem, and apply to all access to and use of the Website thereafter. Your\ncontinued use of the Website following the posting of revised Terms of Service\nmeans that you accept and agree to the changes.\n\n## 6. Contact Information\n\nQuestions or comments about the Website or these Terms of Service may be\ndirected to our support team at support@openstatus.dev.\n\n## 7. Disclaimer of Warranties\n\nTHE SERVICE AND ITS CONTENT ARE PROVIDED ON AN \"AS IS\" AND \"AS AVAILABLE\" BASIS\nWITHOUT ANY WARRANTIES OF ANY KIND. WE DISCLAIM ALL WARRANTIES, INCLUDING, BUT\nNOT LIMITED TO, THE WARRANTY OF TITLE, MERCHANTABILITY, NON-INFRINGEMENT OF\nTHIRD PARTIES’ RIGHTS, AND FITNESS FOR PARTICULAR PURPOSE.\n\n## 8. Limitation of Liability\n\nIN NO EVENT WILL WE, OUR AFFILIATES OR THEIR LICENSORS, SERVICE PROVIDERS,\nEMPLOYEES, AGENTS, OFFICERS OR DIRECTORS BE LIABLE FOR DAMAGES OF ANY KIND,\nUNDER ANY LEGAL THEORY, ARISING OUT OF OR IN CONNECTION WITH YOUR USE, OR\nINABILITY TO USE, THE WEBSITE, THE SERVICE, ANY WEBSITES LINKED TO IT, ANY\nCONTENT ON THE WEBSITE OR SUCH OTHER WEBSITES, INCLUDING ANY DIRECT, INDIRECT,\nSPECIAL, INCIDENTAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES.\n\n## 9. Governing Law and Jurisdiction\n\nThese Terms of Service and any dispute or claim arising out of or related to\nthem, their subject matter or their formation (in each case, including\nnon-contractual disputes or claims) shall be governed by and construed in\naccordance with the internal laws of France without giving effect to any choice\nor conflict of law provision or rule. Any legal suit, action, or proceeding\narising out of, or related to, these Terms of Service or the Website shall be\ninstituted exclusively in the federal courts of France.\n\n---\n\nBy using OpenStatus , you acknowledge that you have read these Terms of Service,\nunderstood them, and agree to be bound by them. If you do not agree to these\nTerms of Service, you are not authorized to use the Service. We reserve the\nright to change these Terms of Service at any time, so please review them\nfrequently.\n\nThank you for using OpenStatus!\n"
  },
  {
    "path": "apps/web/src/content/pages/use-case/api-providers.mdx",
    "content": "---\ntitle: \"Status Pages for API Infrastructure\"\npublishedAt: \"2026-03-13\"\nauthor: \"openstatus\"\ndescription: \"Your API consumers expect transparency. Give them a branded status page with real-time uptime data, incident history, and subscription channels — powered by monitoring from 28 regions.\"\ncategory: \"API Infrastructure\"\nfaq:\n  - question: \"Why do API providers need a public status page?\"\n    answer: \"Your API consumers build their products on top of your infrastructure. When your API is down, their products are down. A public status page reduces support tickets, builds trust, and shows enterprise customers you take reliability seriously.\"\n  - question: \"Can I show per-endpoint status?\"\n    answer: \"Yes. You can create separate page components for each API endpoint or service. Group them by product area, region, or any logical structure. Each component shows its own uptime data independently.\"\n  - question: \"Can I use monitoring as code to manage monitors?\"\n    answer: \"Yes. Define your monitors as YAML configuration and manage them with the openstatus CLI or Terraform provider. Version control your monitoring setup alongside your API code.\"\n  - question: \"How do API consumers subscribe to updates?\"\n    answer: \"Your consumers can subscribe via email, RSS/Atom feeds, or JSON feeds. Many API providers embed the JSON feed into their own dashboards to show upstream status to their users.\"\n---\n\n## Why API providers need a status page\n\nYour API consumers build their products on top of your infrastructure. When your API degrades, their products degrade. They need to know — **immediately**.\n\nA status page is the standard way API providers communicate reliability. If you don't have one, enterprise customers will ask why.\n\n## What API infrastructure teams need\n\n### Per-endpoint visibility\n\nCreate separate **page components** for each API endpoint or service. Group by product area, region, or environment. Your consumers see exactly which services are affected.\n\n### Real-time monitoring data\n\nAttach your monitors directly to status page components. Uptime data updates automatically — no manual status changes needed for detected incidents.\n\n### Monitoring as Code\n\nDefine monitors as **YAML config** and manage them with the CLI or **Terraform provider**. Version control your monitoring alongside your API code. Deploy changes through CI/CD.\n\n```yaml\nuptime-monitor:\n  name: \"REST API\"\n  frequency: \"1m\"\n  regions: [iad, ams, sin, syd]\n  request:\n    url: https://api.yourservice.com/health\n    method: GET\n```\n\n### Assertions and thresholds\n\nValidate responses beyond status codes. Check **headers**, **body content**, and **response times**. Set **degraded** and **timeout** thresholds to catch performance issues before they become outages.\n\n### Subscriber channels\n\nLet your API consumers subscribe via **email**, **RSS/Atom**, or **JSON** feeds. The JSON feed is particularly useful for consumers who want to embed your status into their own dashboards.\n\n### Custom domain\n\nHost on `status.yourapi.com`. Your consumers expect it.\n\n---\n\nGive your API consumers the transparency they expect\n\n<ButtonLink href=\"https://app.openstatus.dev\">Create Your Status Page</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/use-case/compliance.mdx",
    "content": "---\ntitle: \"SOC 2-Ready Status Page in 2 Minutes\"\npublishedAt: \"2026-03-13\"\nauthor: \"openstatus\"\ndescription: \"SOC 2 auditors expect documented incident communication. Openstatus gives you a branded status page with incident history, subscriber notifications, and audit-ready evidence — set up in minutes.\"\ncategory: \"Compliance\"\nfaq:\n  - question: \"What SOC 2 criteria relate to incident communication?\"\n    answer: \"SOC 2's CC2.3 (Communication with external parties) requires you to demonstrate incident communication processes with external users — a mechanism to report failures, open communication channels, and documentation of how incidents are communicated. A status page with timestamped incident reports and subscriber notifications is the fastest way to satisfy this.\"\n  - question: \"Do I need a status page specifically for SOC 2?\"\n    answer: \"No — SOC 2 CC2.3 requires you to demonstrate incident communication with external parties, but it doesn't prescribe a specific tool. You could use email notifications, a support portal, or other channels. That said, a status page is the fastest, most auditor-friendly way to satisfy the requirement and is increasingly considered standard practice.\"\n  - question: \"What evidence does openstatus provide for auditors?\"\n    answer: \"Every status report, update, and resolution is timestamped and stored. You get a full incident history showing when issues were detected, communicated, and resolved. Subscriber notification logs show you proactively informed stakeholders. This creates an auditable trail without manual documentation.\"\n  - question: \"Can I use openstatus alongside Vanta or Drata?\"\n    answer: \"Yes. openstatus handles the incident communication side of compliance while Vanta or Drata manage the broader audit automation. Your status page URL and incident history can be referenced in your compliance platform as evidence of your communication controls.\"\n  - question: \"How quickly can I be compliant?\"\n    answer: \"You can have a branded status page with custom domain, incident history, and subscriber notifications live in under 10 minutes. Every paid plan includes everything you need for SOC 2 incident communication compliance.\"\n---\n\n## Why SOC 2 auditors care about incident communication\n\nSOC 2's CC2.3 criteria (Communication with external parties) requires you to demonstrate incident communication processes — a mechanism for external users to report failures, open communication channels, and documentation of how incidents are communicated. Your auditor will ask: _\"How do you notify stakeholders when something goes wrong?\"_\n\nSOC 2 doesn't prescribe a specific tool — you could use email, a support portal, or other channels. But a status page is the **fastest, most auditor-friendly** answer. It provides **timestamped, documented evidence** that you proactively inform users about outages, maintenance, and degraded performance.\n\n## What auditors look for\n\nWhen reviewing your incident communication controls, SOC 2 auditors typically verify:\n\n- **Proactive notification**: Do you inform stakeholders before they have to ask?\n- **Documented timeline**: Can you show when an incident was detected, communicated, and resolved?\n- **Subscriber management**: Do affected parties have a way to receive updates?\n- **Consistent process**: Is your incident communication repeatable and reliable?\n\nOpenstatus checks every box automatically.\n\n## How openstatus helps\n\n### Incident history as audit evidence\n\nEvery status report you publish — from initial detection to resolution — is **timestamped and stored**. Your auditor gets a complete trail of how you communicated each incident without you maintaining separate documentation.\n\n### Subscriber notifications\n\nStakeholders can subscribe via **email**, **RSS/Atom**, or **JSON feeds**. When you post an update, subscribers are notified automatically. This proves you proactively communicate — exactly what auditors want to see.\n\n### Maintenance windows\n\nPlanned maintenance shows auditors you communicate **proactively**, not just reactively. Schedule maintenance windows and notify subscribers before any planned downtime.\n\n### Branded custom domain\n\nHost your status page on your own domain (e.g., `status.yourcompany.com`). This keeps the experience professional and consistent with your brand — important when auditors or enterprise customers visit.\n\n### Password protection\n\nFor internal services or client-specific deployments, protect your status page with **password protection** or **magic link authentication**. Control who sees what without maintaining separate systems.\n\n## Get SOC 2-ready in minutes\n\n1. [Create your account](https://app.openstatus.dev) — free to start\n2. Set up your status page with your brand and custom domain\n3. Add your monitors or external service components\n4. Enable subscriber notifications\n5. You're audit-ready\n\nEvery paid plan includes custom domain, incident history, subscriber notifications, and password protection — everything you need to satisfy SOC 2's incident communication requirements.\n\n---\n\nReady to check the compliance box?\n\n<ButtonLink href=\"https://app.openstatus.dev\">Create Your Status Page</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/use-case/crypto.mdx",
    "content": "---\ntitle: \"Status Pages for Crypto Exchanges and DeFi Protocols\"\npublishedAt: \"2026-03-13\"\nauthor: \"openstatus\"\ndescription: \"Keep your traders and users informed during outages. Monitor exchange APIs, wallet services, and blockchain nodes from 28 global regions with a branded, trustworthy status page.\"\ncategory: \"Crypto\"\nfaq:\n  - question: \"Why do crypto platforms need a status page?\"\n    answer: \"Downtime in crypto means lost trades and lost trust. A status page gives your users a single source of truth during incidents — reducing support tickets, preventing panic, and demonstrating operational maturity to institutional partners.\"\n  - question: \"Can I monitor blockchain RPC endpoints?\"\n    answer: \"Yes. Openstatus monitors any HTTP/HTTPS endpoint. You can monitor RPC nodes, REST APIs, WebSocket endpoints, and more with custom assertions to validate response correctness.\"\n  - question: \"Do you support custom domains?\"\n    answer: \"Yes. Host your status page on your own domain (e.g., status.exchange.com) to maintain brand trust. Custom domains are available on paid plans.\"\n  - question: \"Can I protect my status page for institutional clients?\"\n    answer: \"Yes. Use password protection or magic link authentication to create private status pages for institutional partners or internal teams.\"\n---\n\n## Why crypto platforms need a status page\n\nWhen your exchange goes down, users don't wait — they panic. A status page is the difference between **\"we're handling it\"** and **\"are they exit scamming?\"**\n\nFor institutional partners, a status page signals operational maturity. For retail users, it builds trust.\n\n## Built for always-on platforms\n\n### 28 global regions\n\nMonitor your APIs from regions that match your user base. Detect regional outages before your users report them.\n\n### API monitoring with assertions\n\nValidate that your endpoints return correct data — not just that they respond. Check status codes, headers, and response bodies to catch silent failures.\n\n### Instant alerts\n\nGet notified via **Slack**, **Discord**, **PagerDuty**, **OpsGenie**, **SMS**, or **webhook** the moment something degrades. Route alerts to your on-call team.\n\n### Branded status page\n\nHost on your own domain. Customize with your brand. Show your users you take reliability seriously.\n\n### Subscriber notifications\n\nLet users subscribe for email updates. When you push a status report, they're informed automatically — reducing support load during incidents.\n\n### Password-protected pages\n\nCreate private status pages for institutional partners or internal operations teams with **password protection** or **magic link authentication**.\n\n---\n\nKeep your users informed, not panicking\n\n<ButtonLink href=\"https://app.openstatus.dev\">Create Your Status Page</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/pages/use-case/open-source.mdx",
    "content": "---\ntitle: \"Status Pages for Open-Source Projects\"\npublishedAt: \"2026-03-13\"\nauthor: \"openstatus\"\ndescription: \"Give your open-source community a transparent view of service health. Free status page with uptime monitoring, incident history, and community-friendly subscriptions.\"\ncategory: \"Open Source\"\nfaq:\n  - question: \"Is openstatus free for open-source projects?\"\n    answer: \"Yes. The free plan includes one monitor, one status page with three components, and monitoring from up to 6 regions. For larger projects, paid plans start at $30/month with 20 monitors and custom domains.\"\n  - question: \"Can I self-host openstatus?\"\n    answer: \"Yes. Openstatus is fully open source (MIT license) and can be self-hosted. The checker runs as an 8.5MB Docker image. You can also use the managed SaaS and keep the monitoring infrastructure off your plate.\"\n  - question: \"How do open-source projects use openstatus?\"\n    answer: \"Projects like Cal.com, Documenso, Hanko, and OpenPanel use openstatus to give their communities transparent uptime data. They monitor APIs and services, publish incidents, and let contributors and users subscribe for updates.\"\n  - question: \"Can contributors subscribe to status updates?\"\n    answer: \"Yes. Your community can subscribe via email, RSS/Atom feeds, or JSON feeds. When you publish a status report or schedule maintenance, subscribers are notified automatically.\"\n---\n\n## Why open-source projects need a status page\n\nYour users and contributors rely on your services — APIs, documentation sites, package registries, demo instances. When something goes down, they need to know **what's happening** and **when it'll be back**.\n\nA status page turns \"is it down for everyone or just me?\" into a clear answer.\n\n## Trusted by the open-source community\n\nTeams like **Cal.com**, **Documenso**, **Hanko**, **OpenPanel**, and **Probo** use openstatus to keep their communities informed.\n\n## Why openstatus fits open-source\n\n### Open source itself\n\nOpenstatus is fully open source. You can inspect the code, contribute, self-host, or use the managed SaaS. No vendor lock-in.\n\n### Free tier that works\n\nThe free plan gives you a real status page with uptime monitoring — not a trial. One monitor, one status page, 6 regions. Enough for most projects to get started.\n\n### Transparent by default\n\nPublic status pages with uptime data, incident history, and response times. Your community sees exactly what's happening — matching the transparency ethos of open source.\n\n### Community subscriptions\n\nLet your users subscribe via **email**, **RSS/Atom**, or **JSON**. When you push a status report, they know. No manual pinging in Discord.\n\n### Theme Store\n\nMake your status page match your project's brand with [community themes](https://themes.openstatus.dev). Contribute your own theme back to the store.\n\n---\n\nGive your community the transparency they deserve\n\n<ButtonLink href=\"https://app.openstatus.dev\">Create Your Status Page</ButtonLink>\n\n---\n"
  },
  {
    "path": "apps/web/src/content/resolve.ts",
    "content": "import { generateListingForPath } from \"./listing\";\nimport type { MDXData } from \"./utils\";\nimport {\n  getBlogPosts,\n  getChangelogPosts,\n  getComparePages,\n  getGuides,\n  getHomePage,\n  getProductPages,\n  getToolsPages,\n  getUnrelatedPages,\n  getUseCasePages,\n} from \"./utils\";\n\n/**\n * Content resolution result - either MDX content or a generated listing\n */\nexport type ContentResult =\n  | { type: \"mdx\"; data: MDXData }\n  | { type: \"listing\"; data: string };\n\n/**\n * Resolves pathname to content using two-tier fallback:\n * 1. Try to find MDX content (blog posts, pages, etc.)\n * 2. Fallback to generating sitemap-style listings\n */\nexport function resolveContent(pathname: string): ContentResult | null {\n  // Normalize pathname: remove trailing slash, decode URI\n  const normalizedPath = decodeURIComponent(pathname).replace(/\\/$/, \"\");\n\n  // TIER 1: Try to find MDX content first\n  const mdxContent = resolveMdxContent(normalizedPath);\n  if (mdxContent) {\n    return { type: \"mdx\", data: mdxContent };\n  }\n\n  // TIER 2: Fallback to listing generation\n  const listing = generateListingForPath(normalizedPath);\n  if (listing) {\n    return { type: \"listing\", data: listing };\n  }\n\n  // Not found\n  return null;\n}\n\n/**\n * Resolves pathname to MDX content\n */\nfunction resolveMdxContent(pathname: string): MDXData | null {\n  const segments = pathname.split(\"/\").filter(Boolean);\n\n  // Root path → home.mdx (confirmed: file exists)\n  if (segments.length === 0) {\n    try {\n      return getHomePage();\n    } catch {\n      // home.mdx doesn't exist, will fallback to listing\n      return null;\n    }\n  }\n\n  // Prefixed paths (category/slug format)\n  const [category, slug] = segments;\n\n  // Skip /blog/category/slug pattern (handled by listing generator)\n  if (slug && slug !== \"category\") {\n    switch (category) {\n      case \"blog\":\n        return getBlogPosts().find((p) => p.slug === slug) ?? null;\n      case \"changelog\":\n        return getChangelogPosts().find((p) => p.slug === slug) ?? null;\n      case \"compare\":\n        return getComparePages().find((p) => p.slug === slug) ?? null;\n      case \"guides\":\n        return getGuides().find((p) => p.slug === slug) ?? null;\n      case \"play\":\n        return getToolsPages().find((p) => p.slug === slug) ?? null;\n      case \"use-case\":\n        return getUseCasePages().find((p) => p.slug === slug) ?? null;\n      default:\n        return null;\n    }\n  }\n\n  // Single segment: try unrelated, then product\n  if (segments.length === 1) {\n    const singleSlug = segments[0];\n    return (\n      getUnrelatedPages().find((p) => p.slug === singleSlug) ??\n      getProductPages().find((p) => p.slug === singleSlug) ??\n      null\n    );\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "apps/web/src/content/shadcn-registry-example.tsx",
    "content": "/**\n * OpenStatus Registry Example\n *\n * This file demonstrates how to compose status page components together\n * to create a complete status page. It serves as a reference implementation\n * for users installing components via the shadcn CLI.\n *\n * Components used:\n * - Status, StatusHeader, StatusTitle, StatusDescription, StatusContent (layout)\n * - StatusBanner (system status indicator)\n * - StatusBar (45-day uptime visualization)\n * - StatusComponent block components (clean monitor composition)\n * - StatusComponentGroup (collapsible group container)\n * - StatusFeed (incidents and maintenance timeline)\n */\n\nimport { StatusBanner } from \"@openstatus/ui/components/blocks/status-banner\";\nimport { StatusBar } from \"@openstatus/ui/components/blocks/status-bar\";\nimport {\n  StatusComponent,\n  StatusComponentBody,\n  StatusComponentFooter,\n  StatusComponentHeader,\n  StatusComponentHeaderLeft,\n  StatusComponentHeaderRight,\n  StatusComponentStatus,\n  StatusComponentTitle,\n  StatusComponentUptime,\n} from \"@openstatus/ui/components/blocks/status-component\";\nimport { StatusComponentGroup } from \"@openstatus/ui/components/blocks/status-component-group\";\nimport { StatusFeed } from \"@openstatus/ui/components/blocks/status-feed\";\nimport {\n  Status,\n  StatusContent,\n  StatusDescription,\n  StatusHeader,\n  StatusTitle,\n} from \"@openstatus/ui/components/blocks/status-layout\";\nimport type {\n  Maintenance,\n  StatusBarData,\n  StatusReport,\n  StatusType,\n} from \"@openstatus/ui/components/blocks/status.types\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\n\n/**\n * Generates realistic mock uptime data for the last 45 days\n * Simulates ~98% uptime with occasional degraded periods\n */\nfunction generateMockUptimeData(): StatusBarData[] {\n  const data: StatusBarData[] = [];\n  const now = new Date();\n\n  for (let i = 44; i >= 0; i--) {\n    const day = new Date(now);\n    day.setDate(day.getDate() - i);\n    day.setHours(0, 0, 0, 0);\n\n    // Generate random status with high success rate\n    const rand = Math.random();\n    let bar: { status: StatusType; height: number }[];\n\n    if (rand < 0.02) {\n      // 2% chance of error day - partial outage\n      bar = [\n        { status: \"success\", height: 70 },\n        { status: \"error\", height: 30 },\n      ];\n    } else if (rand < 0.05) {\n      // 3% chance of degraded day\n      bar = [\n        { status: \"success\", height: 85 },\n        { status: \"degraded\", height: 15 },\n      ];\n    } else if (rand < 0.08) {\n      // 3% chance of minor degradation\n      bar = [\n        { status: \"success\", height: 95 },\n        { status: \"degraded\", height: 5 },\n      ];\n    } else {\n      // 92% fully operational\n      bar = [{ status: \"success\", height: 100 }];\n    }\n\n    // Calculate card durations based on actual bar segments (24 hours per day)\n    const totalHeight = bar.reduce((sum, segment) => sum + segment.height, 0);\n    const statusCounts: Record<string, number> = {\n      success: 0,\n      degraded: 0,\n      error: 0,\n    };\n\n    // Sum up heights by status\n    for (const segment of bar) {\n      statusCounts[segment.status] += segment.height;\n    }\n\n    // Create card entries only for statuses with non-zero values\n    const card = ([\"success\", \"degraded\", \"error\"] as const)\n      .map((status) => {\n        const percentage = statusCounts[status] / totalHeight;\n        const totalHours = percentage * 24;\n        const hours = Math.floor(totalHours);\n        const minutes = Math.round((totalHours - hours) * 60);\n\n        // Format duration\n        let value: string;\n        if (totalHours < 1) {\n          // Less than 1 hour: show only minutes\n          value = `${Math.round(totalHours * 60)}m`;\n        } else if (minutes === 0) {\n          // Whole hours: show only hours\n          value = `${hours}h`;\n        } else {\n          // Mixed: show hours and minutes\n          value = `${hours}h ${minutes}m`;\n        }\n\n        return { status, value, hours: totalHours };\n      })\n      .filter((entry) => entry.hours > 0) // Only show non-zero durations\n      .map(({ status, value }) => ({ status, value }));\n\n    data.push({\n      day: day.toISOString(),\n      bar,\n      card,\n      events: [],\n    });\n  }\n\n  return data;\n}\n\n/**\n * Adds incidents and maintenance events to the uptime data\n * This connects status reports to specific days on the status bar\n */\nfunction addEventsToData(\n  data: StatusBarData[],\n  statusReports: StatusReport[],\n  maintenances: Maintenance[],\n): StatusBarData[] {\n  // Add status report incidents to the data\n  for (const report of statusReports) {\n    const incidentDate = report.updates[0].date; // Use first update as incident start\n    const dayIndex = data.findIndex((d) => {\n      const dayDate = new Date(d.day);\n      const incDate = new Date(incidentDate);\n      return (\n        dayDate.getFullYear() === incDate.getFullYear() &&\n        dayDate.getMonth() === incDate.getMonth() &&\n        dayDate.getDate() === incDate.getDate()\n      );\n    });\n\n    if (dayIndex >= 0) {\n      // Calculate incident duration from updates\n      const startTime = report.updates[0].date;\n      const endTime = report.updates[report.updates.length - 1].date;\n\n      data[dayIndex].events = [\n        ...(data[dayIndex].events || []),\n        {\n          id: report.id,\n          name: report.title,\n          type: \"incident\",\n          from: new Date(startTime),\n          to: new Date(endTime),\n        },\n      ];\n\n      // Update bar to show degraded/error status on incident day\n      const duration =\n        (endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60); // hours\n      const affectedPercentage = Math.min((duration / 24) * 100, 100);\n\n      if (affectedPercentage > 0) {\n        data[dayIndex].bar = [\n          { status: \"success\" as const, height: 100 - affectedPercentage },\n          { status: \"degraded\" as const, height: affectedPercentage },\n        ].filter((segment) => segment.height > 0);\n\n        // Recalculate card with new bar data\n        const totalHeight = data[dayIndex].bar.reduce(\n          (sum, segment) => sum + segment.height,\n          0,\n        );\n        const statusCounts: Record<string, number> = {\n          success: 0,\n          degraded: 0,\n          error: 0,\n        };\n\n        for (const segment of data[dayIndex].bar) {\n          statusCounts[segment.status] += segment.height;\n        }\n\n        data[dayIndex].card = ([\"success\", \"degraded\", \"error\"] as const)\n          .map((status) => {\n            const percentage = statusCounts[status] / totalHeight;\n            const totalHours = percentage * 24;\n            const hours = Math.floor(totalHours);\n            const minutes = Math.round((totalHours - hours) * 60);\n\n            let value: string;\n            if (totalHours < 1) {\n              value = `${Math.round(totalHours * 60)}m`;\n            } else if (minutes === 0) {\n              value = `${hours}h`;\n            } else {\n              value = `${hours}h ${minutes}m`;\n            }\n\n            return { status, value, hours: totalHours };\n          })\n          .filter((entry) => entry.hours > 0)\n          .map(({ status, value }) => ({ status, value }));\n      }\n    }\n  }\n\n  // Add maintenance events to the data\n  for (const maintenance of maintenances) {\n    const maintenanceDate = new Date(maintenance.from);\n    const dayIndex = data.findIndex((d) => {\n      const dayDate = new Date(d.day);\n      return (\n        dayDate.getFullYear() === maintenanceDate.getFullYear() &&\n        dayDate.getMonth() === maintenanceDate.getMonth() &&\n        dayDate.getDate() === maintenanceDate.getDate()\n      );\n    });\n\n    if (dayIndex >= 0) {\n      data[dayIndex].events = [\n        ...(data[dayIndex].events || []),\n        {\n          id: maintenance.id,\n          name: maintenance.title,\n          type: \"maintenance\",\n          from: new Date(maintenance.from),\n          to: new Date(maintenance.to),\n        },\n      ];\n    }\n  }\n\n  return data;\n}\n\ninterface Monitor {\n  name: string;\n  status: Exclude<StatusType, \"empty\">;\n  uptime: string;\n  data: StatusBarData[];\n}\n\n/**\n * StatusPageExample - Complete working example of a status page\n *\n * This component demonstrates:\n * - Overall system status with StatusBanner\n * - Multiple monitors with uptime bars\n * - Proper component composition\n * - Mock data structure\n *\n * @example\n * ```tsx\n * import { StatusPageExample } from \"./shadcn-registry\"\n *\n * export default function Page() {\n *   return <StatusPageExample />\n * }\n * ```\n */\nexport function StatusPageExample() {\n  // Mock status reports (incidents) - defined first so we can link them to data\n  const statusReports: StatusReport[] = [\n    {\n      id: 1,\n      title: \"API Response Time Degradation\",\n      affected: [\"API\", \"Database\"],\n      updates: [\n        {\n          date: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // 5 days ago\n          message:\n            \"We are investigating elevated response times on our API endpoints.\",\n          status: \"investigating\",\n        },\n        {\n          date: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000 + 60 * 60 * 1000), // 1 hour later\n          message:\n            \"We have identified a database query optimization issue causing the slowdown.\",\n          status: \"identified\",\n        },\n        {\n          date: new Date(\n            Date.now() - 5 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,\n          ), // 2 hours later\n          message:\n            \"Database queries have been optimized. Monitoring performance improvements.\",\n          status: \"monitoring\",\n        },\n        {\n          date: new Date(\n            Date.now() - 5 * 24 * 60 * 60 * 1000 + 3 * 60 * 60 * 1000,\n          ), // 3 hours later\n          message:\n            \"Response times have returned to normal. This incident has been resolved.\",\n          status: \"resolved\",\n        },\n      ],\n    },\n  ];\n\n  // Mock maintenance windows\n  const maintenances: Maintenance[] = [\n    {\n      id: 3,\n      title: \"Scheduled Database Maintenance\",\n      affected: [\"API\", \"Database\"],\n      message:\n        \"We will be performing routine database maintenance to improve performance and apply security updates. Expect brief periods of increased latency during this window.\",\n      from: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now\n      to: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000), // 2 hours duration\n    },\n  ];\n\n  // Generate mock data for each monitor and add events\n  let apiData = generateMockUptimeData();\n  apiData = addEventsToData(apiData, statusReports, maintenances);\n\n  const websiteData = generateMockUptimeData();\n  const databaseData = generateMockUptimeData();\n\n  const monitors: Monitor[] = [\n    {\n      name: \"API\",\n      status: \"success\",\n      uptime: \"99.8%\",\n      data: apiData,\n    },\n    {\n      name: \"Website\",\n      status: \"success\",\n      uptime: \"99.9%\",\n      data: websiteData,\n    },\n    {\n      name: \"Database\",\n      status: \"success\",\n      uptime: \"100%\",\n      data: databaseData,\n    },\n  ];\n\n  // Determine overall system status based on monitors\n  const hasError = monitors.some((m) => m.status === \"error\");\n  const hasDegraded = monitors.some((m) => m.status === \"degraded\");\n  const systemStatus: Exclude<StatusType, \"empty\"> = hasError\n    ? \"error\"\n    : hasDegraded\n      ? \"degraded\"\n      : \"success\";\n\n  return (\n    <Status\n      variant={systemStatus}\n      className=\"mx-auto max-w-[800px] px-2 py-2 sm:px-4 md:px-[125px]\"\n    >\n      <StatusHeader>\n        <StatusTitle>System Status</StatusTitle>\n        <StatusDescription>\n          Current status of all services and infrastructure\n        </StatusDescription>\n      </StatusHeader>\n      <StatusBanner status={systemStatus} />\n      <StatusContent>\n        <StatusComponent variant={monitors[0].status}>\n          <StatusComponentHeader>\n            <StatusComponentHeaderLeft>\n              <StatusComponentTitle>{monitors[0].name}</StatusComponentTitle>\n            </StatusComponentHeaderLeft>\n            <StatusComponentHeaderRight>\n              <StatusComponentUptime>\n                {monitors[0].uptime}\n              </StatusComponentUptime>\n              <StatusComponentStatus />\n            </StatusComponentHeaderRight>\n          </StatusComponentHeader>\n          <StatusComponentBody>\n            <StatusBar data={monitors[0].data} />\n            <StatusComponentFooter data={monitors[0].data} />\n          </StatusComponentBody>\n        </StatusComponent>\n        <StatusComponentGroup\n          title=\"Infrastructure\"\n          status={\n            monitors.slice(1).some((m) => m.status === \"error\")\n              ? \"error\"\n              : monitors.slice(1).some((m) => m.status === \"degraded\")\n                ? \"degraded\"\n                : \"success\"\n          }\n          defaultOpen={true}\n        >\n          {monitors.slice(1).map((monitor) => (\n            <StatusComponent key={monitor.name} variant={monitor.status}>\n              <StatusComponentHeader>\n                <StatusComponentHeaderLeft>\n                  <StatusComponentTitle>{monitor.name}</StatusComponentTitle>\n                </StatusComponentHeaderLeft>\n                <StatusComponentHeaderRight>\n                  <StatusComponentUptime>\n                    {monitor.uptime}\n                  </StatusComponentUptime>\n                  <StatusComponentStatus />\n                </StatusComponentHeaderRight>\n              </StatusComponentHeader>\n              <StatusComponentBody>\n                <StatusBar data={monitor.data} />\n                <StatusComponentFooter data={monitor.data} />\n              </StatusComponentBody>\n            </StatusComponent>\n          ))}\n        </StatusComponentGroup>\n        <Separator className=\"my-6\" />\n        <div className=\"space-y-4\">\n          <h2 className=\"font-semibold text-lg\">Recent Events</h2>\n          <StatusFeed\n            statusReports={statusReports}\n            maintenances={maintenances}\n          />\n        </div>\n      </StatusContent>\n    </Status>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/simple-chart.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { ChartConfig } from \"@openstatus/ui/components/ui/chart\";\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@openstatus/ui/components/ui/chart\";\nimport { Line, LineChart, XAxis } from \"recharts\";\n\nconst chartConfig = {\n  latency: {\n    label: \"Latency\",\n    color: \"var(--color-success)\",\n  },\n} as ChartConfig;\n\nexport interface SimpleChartProps {\n  data: { timestamp: string; latency: number | undefined }[];\n  className?: string;\n}\n\nexport function SimpleChart({ data, className }: SimpleChartProps) {\n  return (\n    <ChartContainer\n      config={chartConfig}\n      className={cn(\"h-12 w-full\", className)}\n    >\n      <LineChart\n        accessibilityLayer\n        data={data}\n        margin={{\n          left: 12,\n          right: 12,\n        }}\n      >\n        <XAxis dataKey=\"timestamp\" hide />\n        <ChartTooltip\n          cursor={false}\n          content={\n            <ChartTooltipContent\n              indicator=\"dot\"\n              labelFormatter={(value) => {\n                return new Date(value).toLocaleDateString(\"en-US\", {\n                  day: \"numeric\",\n                  month: \"short\",\n                  hour: \"numeric\",\n                  minute: \"numeric\",\n                });\n              }}\n              formatter={(value, name) => (\n                <>\n                  <div\n                    className=\"h-full w-1 shrink-0 self-center rounded-[2px] bg-(--color-bg)\"\n                    style={\n                      {\n                        \"--color-bg\": `var(--color-${name})`,\n                      } as React.CSSProperties\n                    }\n                  />\n                  {chartConfig[name as keyof typeof chartConfig]?.label || name}\n                  <div className=\"ml-auto flex items-baseline gap-0.5 font-medium font-mono text-foreground tabular-nums\">\n                    {value}\n                    <span className=\"font-normal text-muted-foreground\">\n                      ms\n                    </span>\n                  </div>\n                </>\n              )}\n            />\n          }\n        />\n        <Line\n          dataKey=\"latency\"\n          type=\"monotone\"\n          stroke=\"var(--color-latency)\"\n          strokeWidth={2}\n          dot={false}\n        />\n      </LineChart>\n    </ChartContainer>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/sub-nav.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { Fragment } from \"react\";\nimport { CopyDropdownButton } from \"./copy-button\";\n\nexport function SubNav({ className, ...props }: React.ComponentProps<\"div\">) {\n  const pathname = usePathname();\n  const segments = pathname.split(\"/\").filter(Boolean).slice(0, -1);\n\n  return (\n    <div\n      className={cn(\"flex items-center justify-between gap-2\", className)}\n      {...props}\n    >\n      <div className=\"prose px-4 text-muted-foreground\">\n        {segments.map((segment, index) => (\n          <Fragment key={segment}>\n            <Link href={`/${segments.slice(0, index + 1).join(\"/\")}`}>\n              {segment.split(\"-\").join(\" \")}\n            </Link>\n            {index < segments.length - 1 ? (\n              <span className=\"text-muted-foreground\">{\" | \"}</span>\n            ) : null}\n          </Fragment>\n        ))}\n      </div>\n      <CopyDropdownButton />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Laptop, Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport type * as React from \"react\";\nimport { useEffect, useState } from \"react\";\n\nexport function ThemeToggle({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const { setTheme, theme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return (\n      <div\n        className={cn(\n          \"flex items-center gap-px bg-border [&>*]:flex [&>*]:flex-1 [&>*]:items-center [&>*]:justify-center [&>*]:bg-background [&>*]:p-4\",\n          className,\n        )}\n        {...props}\n      >\n        <div>\n          <Sun className=\"h-6 w-6\" />\n        </div>\n        <div>\n          <Moon className=\"h-6 w-6\" />\n        </div>\n        <div>\n          <Laptop className=\"h-6 w-6\" />\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-px bg-border [&>*]:flex [&>*]:flex-1 [&>*]:items-center [&>*]:justify-center [&>*]:bg-background [&>*]:p-4 [&>*]:hover:bg-muted [&>*]:data-[active=true]:bg-muted\",\n        className,\n      )}\n      {...props}\n    >\n      <button\n        type=\"button\"\n        data-active={theme === \"light\"}\n        onClick={() => setTheme(\"light\")}\n      >\n        [light]\n      </button>\n      <button\n        type=\"button\"\n        data-active={theme === \"dark\"}\n        onClick={() => setTheme(\"dark\")}\n      >\n        [dark]\n      </button>\n      <button\n        type=\"button\"\n        data-active={theme === \"system\"}\n        onClick={() => setTheme(\"system\")}\n      >\n        [system]\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/content/utils.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport matter from \"gray-matter\";\nimport slugify from \"slugify\";\nimport { z } from \"zod\";\n\n// Structured data schemas\nconst howtoStepSchema = z.object({\n  name: z.string(),\n  text: z.string(),\n  image: z.string().optional(),\n  url: z.string().optional(),\n});\n\nconst howtoSchema = z.object({\n  totalTime: z.string().optional(), // ISO 8601 duration format (e.g., \"PT2H\")\n  steps: z.array(howtoStepSchema),\n});\n\nconst faqItemSchema = z.object({\n  question: z.string(),\n  answer: z.string(),\n});\n\nconst metadataSchema = z.object({\n  title: z.string(),\n  publishedAt: z.coerce.date(),\n  description: z.string(),\n  category: z.string(),\n  author: z.string(),\n  image: z.string().optional(),\n  // Structured data fields\n  howto: howtoSchema.optional(),\n  faq: z.array(faqItemSchema).optional(),\n});\n\nexport type Metadata = z.infer<typeof metadataSchema>;\nexport type HowToStep = z.infer<typeof howtoStepSchema>;\nexport type HowToData = z.infer<typeof howtoSchema>;\nexport type FAQItem = z.infer<typeof faqItemSchema>;\n\nfunction parseFrontmatter(fileContent: string) {\n  const { data, content } = matter(fileContent);\n\n  const validatedMetadata = metadataSchema.safeParse(data);\n\n  if (!validatedMetadata.success) {\n    console.error(validatedMetadata.error);\n    throw new Error(`Invalid metadata: ${validatedMetadata.error.message}`);\n  }\n\n  return { metadata: validatedMetadata.data, content };\n}\n\nfunction getMDXFiles(dir: string) {\n  return fs.readdirSync(dir).filter((file) => path.extname(file) === \".mdx\");\n}\n\nfunction readMDXFile(filePath: string) {\n  const rawContent = fs.readFileSync(filePath, \"utf-8\");\n  return parseFrontmatter(rawContent);\n}\n\nfunction getMDXDataFromDir(dir: string, prefix = \"\") {\n  const mdxFiles = getMDXFiles(dir);\n  return mdxFiles.map((file) => {\n    return getMDXDataFromFile(path.join(dir, file), prefix);\n  });\n}\n\nfunction getMDXDataFromFile(filePath: string, prefix = \"\") {\n  const { metadata, content } = readMDXFile(filePath);\n  const slugRaw = path.basename(filePath, path.extname(filePath));\n  const slug = slugify(slugRaw, { lower: true, strict: true });\n  const href = prefix ? `${prefix}/${slug}` : `/${slug}`;\n  return {\n    metadata,\n    slug,\n    content,\n    href,\n  };\n}\n\nexport type MDXData = ReturnType<typeof getMDXDataFromFile>;\n\nexport function getBlogPosts(): MDXData[] {\n  return getMDXDataFromDir(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"blog\"),\n    \"/blog\",\n  );\n}\n\nexport function getChangelogPosts(): MDXData[] {\n  return getMDXDataFromDir(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"changelog\"),\n    \"/changelog\",\n  );\n}\n\nexport function getProductPages(): MDXData[] {\n  return getMDXDataFromDir(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"product\"),\n    \"\",\n  );\n}\n\nexport function getGuides(): MDXData[] {\n  return getMDXDataFromDir(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"guides\"),\n    \"/guides\",\n  );\n}\n\nexport function getUseCasePages(): MDXData[] {\n  return getMDXDataFromDir(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"use-case\"),\n    \"/use-case\",\n  );\n}\n\nexport function getUnrelatedPages(): MDXData[] {\n  return getMDXDataFromDir(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"unrelated\"),\n    \"\",\n  );\n}\n\nexport function getUnrelatedPage(slug: string): MDXData {\n  return getMDXDataFromFile(\n    path.join(\n      process.cwd(),\n      \"src\",\n      \"content\",\n      \"pages\",\n      \"unrelated\",\n      `${slug}.mdx`,\n    ),\n    \"\",\n  );\n}\n\nexport function getMainPages(): MDXData[] {\n  return [...getUnrelatedPages(), ...getProductPages()];\n}\n\nexport function getComparePages(): MDXData[] {\n  return getMDXDataFromDir(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"compare\"),\n    \"/compare\",\n  );\n}\n\nexport function getHomePage(): MDXData {\n  return getMDXDataFromFile(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"home.mdx\"),\n    \"\",\n  );\n}\n\nexport function getToolsPages(): MDXData[] {\n  return getMDXDataFromDir(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"tools\"),\n    \"/play\",\n  );\n}\n\nexport function getToolsPage(slug: string): MDXData {\n  return getMDXDataFromFile(\n    path.join(process.cwd(), \"src\", \"content\", \"pages\", \"tools\", `${slug}.mdx`),\n    \"/play\",\n  );\n}\n\nexport const PAGE_TYPES = [\n  \"blog\",\n  \"changelog\",\n  \"product\",\n  \"unrelated\",\n  \"compare\",\n  \"tools\",\n  \"guides\",\n  \"use-case\",\n  \"all\",\n] as const;\n\nexport type PageType = (typeof PAGE_TYPES)[number];\n\nexport function getPages(type: PageType) {\n  switch (type) {\n    case \"blog\":\n      return getBlogPosts();\n    case \"changelog\":\n      return getChangelogPosts();\n    case \"product\":\n      return getProductPages();\n    case \"unrelated\":\n      return getUnrelatedPages();\n    case \"compare\":\n      return getComparePages();\n    case \"tools\":\n      return getToolsPages();\n    case \"guides\":\n      return getGuides();\n    case \"use-case\":\n      return getUseCasePages();\n    case \"all\":\n      return [\n        ...getBlogPosts(),\n        ...getChangelogPosts(),\n        ...getProductPages(),\n        ...getUnrelatedPages(),\n        ...getComparePages(),\n        ...getToolsPages(),\n        ...getGuides(),\n        ...getUseCasePages(),\n      ];\n    default:\n      throw new Error(`Unknown page type: ${type}`);\n  }\n}\n\nexport function getCategories() {\n  return [\n    ...new Set([\n      ...getBlogPosts().map((post) => post.metadata.category),\n      ...getChangelogPosts().map((post) => post.metadata.category),\n      ...getProductPages().map((post) => post.metadata.category),\n      ...getUnrelatedPages().map((post) => post.metadata.category),\n      ...getComparePages().map((post) => post.metadata.category),\n      ...getToolsPages().map((post) => post.metadata.category),\n    ]),\n  ] as const;\n}\n\nexport function formatDate(targetDate: Date, includeRelative = false) {\n  const currentDate = new Date();\n\n  const yearsAgo = currentDate.getFullYear() - targetDate.getFullYear();\n  const monthsAgo = currentDate.getMonth() - targetDate.getMonth();\n  const daysAgo = currentDate.getDate() - targetDate.getDate();\n\n  let formattedDate = \"\";\n\n  if (yearsAgo > 0) {\n    formattedDate = `${yearsAgo}y ago`;\n  } else if (monthsAgo > 0) {\n    formattedDate = `${monthsAgo}mo ago`;\n  } else if (daysAgo > 0) {\n    formattedDate = `${daysAgo}d ago`;\n  } else {\n    formattedDate = \"Today\";\n  }\n\n  const fullDate = targetDate.toLocaleString(\"en-us\", {\n    month: \"short\",\n    day: \"2-digit\",\n    year: \"numeric\",\n  });\n\n  if (!includeRelative) {\n    return fullDate;\n  }\n\n  return `${fullDate} (${formattedDate})`;\n}\n"
  },
  {
    "path": "apps/web/src/data/author.ts",
    "content": "export const author = {\n  \"Maximilian Kaske\": {\n    name: \"Maximilian Kaske\",\n    url: \"https://x.com/mxkaske\",\n  },\n  \"Thibault Le Ouay Ducasse\": {\n    name: \"Thibault Le Ouay Ducasse\",\n    url: \"https://bsky.app/profile/thibaultleouay.dev\",\n  },\n  \"Moulik Aggarwal\": {\n    name: \"Moulik Aggarwal\",\n    url: \"https://x.com/aggmoulik\",\n  },\n} as const;\n\nexport function getAuthor(name: string) {\n  if (name in author) {\n    return author[name as keyof typeof author];\n  }\n  return name;\n}\n"
  },
  {
    "path": "apps/web/src/data/code-dictionary.ts",
    "content": "export const codesDict = {\n  \"1xx\": {\n    prefix: 1,\n    label: \"1xx\",\n    name: \"Informational\",\n  },\n  \"2xx\": {\n    prefix: 2,\n    label: \"2xx\",\n    name: \"Successfull\",\n  },\n  \"3xx\": {\n    prefix: 3,\n    label: \"3xx\",\n    name: \"Redirection\",\n  },\n  \"4xx\": {\n    prefix: 4,\n    label: \"4xx\",\n    name: \"Client Error\",\n  },\n  \"5xx\": {\n    prefix: 5,\n    label: \"5xx\",\n    name: \"Server Error\",\n  },\n} as const;\n"
  },
  {
    "path": "apps/web/src/data/content.ts",
    "content": "import {\n  getBlogPosts,\n  getChangelogPosts,\n  getComparePages,\n  getProductPages,\n  getToolsPages,\n  getUseCasePages,\n} from \"@/content/utils\";\nimport type { Region } from \"@openstatus/regions\";\n\nconst products = getProductPages();\n\nconst productsSection = {\n  label: \"Products\",\n  items: products.map((product) => ({\n    label: product.metadata.title,\n    href: `/${product.slug}`,\n  })),\n};\n\nconst resourcesFooterSection = {\n  label: \"Resources\",\n  items: [\n    {\n      label: \"Blog\",\n      href: \"/blog\",\n    },\n    {\n      label: \"Pricing\",\n      href: \"/pricing\",\n    },\n    {\n      label: \"Docs\",\n      href: \"https://docs.openstatus.dev\",\n    },\n    {\n      label: \"Guides\",\n      href: \"/guides\",\n    },\n    {\n      label: \"External Status\",\n      href: \"/status\",\n    },\n    {\n      label: \"OSS Friends\",\n      href: \"/oss-friends\",\n    },\n  ],\n};\n\nconst useCases = getUseCasePages();\n\nconst useCasesSection = {\n  label: \"Use Cases\",\n  items: useCases.map((page) => ({\n    label: page.metadata.title,\n    href: `/use-case/${page.slug}`,\n  })),\n};\n\nconst resourcesHeaderSection = {\n  label: \"Resources\",\n  items: [\n    {\n      label: \"Use Cases\",\n      href: \"/use-case\",\n    },\n    {\n      label: \"Docs\",\n      href: \"https://docs.openstatus.dev\",\n    },\n    {\n      label: \"Blog\",\n      href: \"/blog\",\n    },\n    {\n      label: \"Changelog\",\n      href: \"/changelog\",\n    },\n    {\n      label: \"Global Speed Checker\",\n      href: \"/play/checker\",\n    },\n    {\n      label: \"Compare\",\n      href: \"/compare\",\n    },\n  ],\n};\n\nconst companySection = {\n  label: \"Company\",\n  items: [\n    {\n      label: \"About\",\n      href: \"/about\",\n    },\n    {\n      label: \"Changelog\",\n      href: \"/changelog\",\n    },\n    {\n      label: \"I'm an LLM\",\n      href: \"https://www.openstatus.dev/llms.txt\",\n    },\n    {\n      label: \"Terms\",\n      href: \"/terms\",\n    },\n    {\n      label: \"Privacy\",\n      href: \"/privacy\",\n    },\n    {\n      label: \"Subprocessors\",\n      href: \"/subprocessors\",\n    },\n  ],\n};\n\nconst blogSection = {\n  label: \"Blog\",\n  items: getBlogPosts()\n    .sort(\n      (a, b) =>\n        b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(),\n    )\n    .slice(0, 6)\n    .map((post) => ({\n      label: post.metadata.title,\n      href: `/blog/${post.slug}`,\n    })),\n};\n\nconst changelogSection = {\n  label: \"Changelog\",\n  items: getChangelogPosts()\n    .sort(\n      (a, b) =>\n        b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(),\n    )\n    .slice(0, 6)\n    .map((post) => ({\n      label: post.metadata.title,\n      href: `/changelog/${post.slug}`,\n    })),\n};\n\nconst compareSection = {\n  label: \"Compare\",\n  items: getComparePages()\n    .map((page) => ({\n      label: page.metadata.title,\n      href: `/compare/${page.slug}`,\n    }))\n    .slice(0, 6),\n};\n\nconst toolsSection = {\n  label: \"Tools\",\n  items: [\n    ...getToolsPages()\n      .filter(\n        (page) => ![\"checker-slug\", \"severity-matrix\"].includes(page.slug),\n      )\n      .map((page) => ({\n        label: page.metadata.title,\n        href: `/play/${page.slug}`,\n      })),\n    {\n      label: \"Shadcn Component Registry\",\n      href: \"/registry\",\n    },\n    {\n      label: \"Theme Explorer\",\n      href: \"https://themes.openstatus.dev\",\n    },\n    {\n      label: \"All Status Codes\",\n      href: \"https://openstat.us\",\n    },\n  ],\n};\n\nconst communitySection = {\n  label: \"Community\",\n  items: [\n    {\n      label: \"Discord\",\n      href: \"https://openstatus.dev/discord\",\n    },\n    {\n      label: \"GitHub\",\n      href: \"https://openstatus.dev/github\",\n    },\n    {\n      label: \"Twitter\",\n      href: \"https://openstatus.dev/twitter\",\n    },\n    {\n      label: \"BlueSky\",\n      href: \"https://openstatus.dev/bsky\",\n    },\n    {\n      label: \"YouTube\",\n      href: \"https://openstatus.dev/youtube\",\n    },\n    {\n      label: \"LinkedIn\",\n      href: \"https://openstatus.dev/linkedin\",\n    },\n  ],\n};\n\nexport const playSection = {\n  label: \"Play\",\n  items: [\n    ...getToolsPages()\n      .filter((page) => page.slug !== \"checker-slug\")\n      .map((page) => ({\n        label: page.metadata.title,\n        href: `/play/${page.slug}`,\n      })),\n    {\n      label: \"Theme Explorer\",\n      href: \"https://themes.openstatus.dev\",\n    },\n    {\n      label: \"Shadcn Component Registry\",\n      href: \"/registry\",\n    },\n    {\n      label: \"All Status Codes\",\n      href: \"https://openstat.us\",\n    },\n    {\n      label: \"Vercel Edge Ping\",\n      href: \"https://light.openstatus.dev\",\n    },\n    {\n      label: \"React Data Table\",\n      href: \"https://logs.run\",\n    },\n    {\n      label: \"Shadcn Time Picker\",\n      href: \"https://time.openstatus.dev\",\n    },\n    {\n      label: \"Astro Status Page\",\n      href: \"https://astro.openstat.us\",\n    },\n  ],\n};\n\nexport const headerLinks = [productsSection, resourcesHeaderSection];\n\nexport const footerLinks = [\n  productsSection,\n  useCasesSection,\n  resourcesFooterSection,\n  companySection,\n  compareSection,\n  blogSection,\n  changelogSection,\n  toolsSection,\n  communitySection,\n];\n\n// --------------------------------\n\nexport type RegionMetricsChartTable = {\n  region: Region;\n  count: number;\n  ok: number;\n  p50Latency: number | null;\n  p75Latency: number | null;\n  p90Latency: number | null;\n  p95Latency: number | null;\n  p99Latency: number | null;\n};\n"
  },
  {
    "path": "apps/web/src/data/incidents-dictionary.ts",
    "content": "export const statusDict = {\n  investigating: {\n    value: \"investigating\",\n    label: \"Investigating\",\n    icon: \"search\",\n    color: \"border-status-down/20 bg-status-down/10 text-status-down\",\n    order: 1,\n  },\n  identified: {\n    value: \"identified\",\n    label: \"Identified\",\n    icon: \"fingerprint\",\n    color:\n      \"border-status-degraded/20 bg-status-degraded/10 text-status-degraded\",\n    order: 2,\n  },\n  monitoring: {\n    value: \"monitoring\",\n    label: \"Monitoring\",\n    icon: \"activity\",\n    color:\n      \"border-status-monitoring/20 bg-status-monitoring/10 text-status-monitoring\",\n    order: 3,\n  },\n  resolved: {\n    value: \"resolved\",\n    label: \"Resolved\",\n    icon: \"search-check\",\n    color:\n      \"border-status-operational/20 bg-status-operational/10 text-status-operational\",\n    order: 4,\n  },\n  // FIXME: check source of thruth\n  maintenance: {\n    value: \"maintenance\",\n    label: \"Maintenance\",\n    icon: \"hammer\",\n    color:\n      \"border-status-monitoring/20 bg-status-monitoring/10 text-status-monitoring\",\n    order: 0,\n  },\n} as const;\n"
  },
  {
    "path": "apps/web/src/data/trigger-dictionary.ts",
    "content": "import type { ValidIcon } from \"@/components/icons\";\nimport type { Trigger } from \"@/lib/monitor/utils\";\n\nexport const triggerDict = {\n  cron: {\n    value: \"cron\",\n    label: \"Scheduled\",\n    icon: \"clock\",\n  },\n  api: {\n    value: \"api\",\n    label: \"On-Demand\",\n    icon: \"network\",\n  },\n  unknown: {\n    value: \"unknown\",\n    label: \"Unknown\",\n    icon: \"cog\",\n  },\n} satisfies Record<\n  string,\n  {\n    icon: ValidIcon;\n    label: string;\n    value: Trigger | \"unknown\";\n  }\n>;\n"
  },
  {
    "path": "apps/web/src/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-nextjs\";\nimport { z } from \"zod\";\n\nimport \"@openstatus/analytics/env\";\nimport \"@openstatus/db/env.mjs\";\n\nexport const env = createEnv({\n  server: {\n    TINY_BIRD_API_KEY: z.string().min(1),\n    RESEND_API_KEY: z.string().min(1),\n    QSTASH_CURRENT_SIGNING_KEY: z.string().min(1),\n    QSTASH_NEXT_SIGNING_KEY: z.string().min(1),\n    QSTASH_TOKEN: z.string().min(1),\n    STRIPE_WEBHOOK_SECRET_KEY: z.string(),\n    UNKEY_TOKEN: z.string(),\n    UNKEY_API_ID: z.string(),\n    GCP_PROJECT_ID: z.string(),\n    GCP_LOCATION: z.string(),\n    GCP_CLIENT_EMAIL: z.string(),\n    GCP_PRIVATE_KEY: z.string(),\n    CRON_SECRET: z.string(),\n    EXTERNAL_API_URL: z.url(),\n    CLICKHOUSE_URL: z.string(),\n    CLICKHOUSE_USERNAME: z.string(),\n    CLICKHOUSE_PASSWORD: z.string(),\n    PAGERDUTY_APP_ID: z.string().optional(),\n    SLACK_SUPPORT_WEBHOOK_URL: z.string().optional(),\n  },\n  client: {\n    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string(),\n    NEXT_PUBLIC_URL: z.string(),\n    NEXT_PUBLIC_SENTRY_DSN: z.string(),\n    NEXT_PUBLIC_OPENPANEL_CLIENT_ID: z.string(),\n  },\n  runtimeEnv: {\n    NEXT_PUBLIC_OPENPANEL_CLIENT_ID:\n      process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID,\n    TINY_BIRD_API_KEY: process.env.TINY_BIRD_API_KEY,\n    RESEND_API_KEY: process.env.RESEND_API_KEY,\n    QSTASH_CURRENT_SIGNING_KEY: process.env.QSTASH_CURRENT_SIGNING_KEY,\n    QSTASH_NEXT_SIGNING_KEY: process.env.QSTASH_NEXT_SIGNING_KEY,\n    QSTASH_TOKEN: process.env.QSTASH_TOKEN,\n    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:\n      process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,\n    STRIPE_WEBHOOK_SECRET_KEY: process.env.STRIPE_WEBHOOK_SECRET_KEY,\n    NEXT_PUBLIC_URL: process.env.NEXT_PUBLIC_URL,\n    NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,\n    UNKEY_TOKEN: process.env.UNKEY_TOKEN,\n    UNKEY_API_ID: process.env.UNKEY_API_ID,\n    GCP_PROJECT_ID: process.env.GCP_PROJECT_ID,\n    GCP_LOCATION: process.env.GCP_LOCATION,\n    GCP_CLIENT_EMAIL: process.env.GCP_CLIENT_EMAIL,\n    GCP_PRIVATE_KEY: process.env.GCP_PRIVATE_KEY,\n    CRON_SECRET: process.env.CRON_SECRET,\n    EXTERNAL_API_URL: process.env.EXTERNAL_API_URL,\n    CLICKHOUSE_URL: process.env.CLICKHOUSE_URL,\n    CLICKHOUSE_USERNAME: process.env.CLICKHOUSE_USERNAME,\n    CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD,\n    PAGERDUTY_APP_ID: process.env.PAGERDUTY_APP_ID,\n    SLACK_SUPPORT_WEBHOOK_URL: process.env.SLACK_SUPPORT_WEBHOOK_URL,\n  },\n  skipValidation: true,\n});\n"
  },
  {
    "path": "apps/web/src/instrumentation.ts",
    "content": "import * as Sentry from \"@sentry/nextjs\";\n\nexport async function register() {\n  if (process.env.NEXT_RUNTIME === \"nodejs\") {\n    await import(\"../sentry.server.config\");\n  }\n\n  if (process.env.NEXT_RUNTIME === \"edge\") {\n    await import(\"../sentry.edge.config\");\n  }\n}\n\nexport const onRequestError = Sentry.captureRequestError;\n"
  },
  {
    "path": "apps/web/src/lib/checker/mock.ts",
    "content": "import { type RegionChecker, cachedCheckerSchema } from \"@/lib/checker/utils\";\nimport { wait } from \"@/lib/utils\";\nimport type { Region } from \"@openstatus/db/src/schema/constants\";\n\nexport async function mockCheckRegion(region: Region) {\n  const response = data.checks\n    .filter((i) => i.state === \"success\")\n    .find((check) => check.region === region);\n\n  if (!response) {\n    throw new Error(\"Region not found\");\n  }\n\n  await wait(response.latency);\n\n  return response as unknown as RegionChecker;\n}\n\nexport async function mockCheckAllRegions() {\n  const res = cachedCheckerSchema.safeParse(data);\n  if (!res.success) {\n    throw new Error(res.error.message);\n  }\n  return res.data;\n}\n\nexport const data = {\n  url: \"https://openstatus.dev\",\n  timestamp: 1724415466600,\n  method: \"GET\",\n  checks: [\n    {\n      type: \"http\",\n      status: 200,\n      state: \"success\",\n      latency: 889,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:47 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=60f9ccb1cb9453b783b14b29eb9e43601eff00062d94b8c57fdaad37e6f10ce7%7C53949e7215f2a3d72af01e3f56727018ff57976e927e05aec83b795736fda2e7; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Id\": \"fra1::fra1::82mqm-1724415466843-d608bd28fa1c\",\n      },\n      timestamp: 1724415466739,\n      timing: {\n        dnsStart: 1724415466810,\n        dnsDone: 1724415466812,\n        connectStart: 1724415466812,\n        connectDone: 1724415466813,\n        tlsHandshakeStart: 1724415466813,\n        tlsHandshakeDone: 1724415466833,\n        firstByteStart: 1724415466833,\n        firstByteDone: 1724415467629,\n        transferStart: 1724415467629,\n        transferDone: 1724415467629,\n      },\n      region: \"ams\",\n    },\n    {\n      type: \"http\",\n      status: 200,\n      state: \"success\",\n\n      latency: 1602,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:48 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=25da392216bca70f47304c9c2eb42f471d91d7b118247a1d24dcb33e38e478b8%7Cf1d4e0f661eb0c8ba478cd5e7fa75d5b1e78f463e28f819c506f9a526f6e4412; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"arn1::fra1::9855t-1724415466874-b441298d1373\",\n      },\n      timestamp: 1724415466771,\n      timing: {\n        dnsStart: 1724415466822,\n        dnsDone: 1724415466839,\n        connectStart: 1724415466839,\n        connectDone: 1724415466841,\n        tlsHandshakeStart: 1724415466841,\n        tlsHandshakeDone: 1724415466850,\n        firstByteStart: 1724415466850,\n        firstByteDone: 1724415468373,\n        transferStart: 1724415468373,\n        transferDone: 1724415468373,\n      },\n      region: \"arn\",\n    },\n    {\n      type: \"http\",\n      status: 200,\n      state: \"success\",\n\n      latency: 823,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:47 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=ea47cff80032077dfa9caa8a7462eabe1ab0885848e7c5b3c2867d1245236cfc%7C751f839b3c6906deca1fc497bfcd09a88cbbd716d187b0da3d1b3cddf929a469; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"iad1::fra1::knlx7-1724415466923-6d8a2defb96b\",\n      },\n      timestamp: 1724415466780,\n      timing: {\n        dnsStart: 1724415466872,\n        dnsDone: 1724415466874,\n        connectStart: 1724415466874,\n        connectDone: 1724415466875,\n        tlsHandshakeStart: 1724415466875,\n        tlsHandshakeDone: 1724415466914,\n        firstByteStart: 1724415466914,\n        firstByteDone: 1724415467603,\n        transferStart: 1724415467603,\n        transferDone: 1724415467603,\n      },\n      region: \"atl\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1198,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:47 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=b257a8d1854130d47bca81546e2b9f7b4ece148a7cfe12067b9ac8ffead17d86%7Cb55b9f314f742151aacdb968ff02b9fe974fff849b7f42edb542bbbb3f144c76; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"iad1::fra1::zv2ml-1724415467303-bd78546dd164\",\n      },\n      timestamp: 1724415466805,\n      timing: {\n        dnsStart: 1724415467056,\n        dnsDone: 1724415467130,\n        connectStart: 1724415467130,\n        connectDone: 1724415467131,\n        tlsHandshakeStart: 1724415467131,\n        tlsHandshakeDone: 1724415467267,\n        firstByteStart: 1724415467267,\n        firstByteDone: 1724415468003,\n        transferStart: 1724415468003,\n        transferDone: 1724415468003,\n      },\n      region: \"bog\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1423,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:48 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=8a075b8d193fee6d0bb5b7b146a8091a4823b6fdcf130ccfe59c6ef3f0231503%7C576e86b7bc8fd6583eeec0e420fe6e24fad8236c48855c9cca71bc487d3cfc0d; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"bom1::fra1::24p8d-1724415467151-89ab94155a7b\",\n      },\n      timestamp: 1724415466933,\n      timing: {\n        dnsStart: 1724415467117,\n        dnsDone: 1724415467144,\n        connectStart: 1724415467144,\n        connectDone: 1724415467145,\n        tlsHandshakeStart: 1724415467145,\n        tlsHandshakeDone: 1724415467152,\n        firstByteStart: 1724415467152,\n        firstByteDone: 1724415468356,\n        transferStart: 1724415468356,\n        transferDone: 1724415468356,\n      },\n      region: \"bom\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1134,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:47 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=790cc402a44b4a6fec5a32657bd549f2e70c0893007b1e799f9625039564b7dd%7C42453bba78875cf8010fe68ab340db0b3cf23c37eed94fe448566d143d25f579; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"iad1::fra1::89mxf-1724415466920-c57e2b6af701\",\n      },\n      timestamp: 1724415466802,\n      timing: {\n        dnsStart: 1724415466872,\n        dnsDone: 1724415466885,\n        connectStart: 1724415466885,\n        connectDone: 1724415466885,\n        tlsHandshakeStart: 1724415466885,\n        tlsHandshakeDone: 1724415466912,\n        firstByteStart: 1724415466913,\n        firstByteDone: 1724415467936,\n        transferStart: 1724415467936,\n        transferDone: 1724415467936,\n      },\n      region: \"bos\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 812,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:48 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=5dac3b94bba1f3cbefd8d50fb0fea119f159004d32847669434c86a5d8d54b15%7C1c00c016c8ba9f442c0c7aa616706c3f8b162c82d1f715aa49a51699637b7208; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"cdg1::fra1::g7g65-1724415467776-ef2e03e01f49\",\n      },\n      timestamp: 1724415467743,\n      timing: {\n        dnsStart: 1724415467766,\n        dnsDone: 1724415467769,\n        connectStart: 1724415467769,\n        connectDone: 1724415467770,\n        tlsHandshakeStart: 1724415467770,\n        tlsHandshakeDone: 1724415467775,\n        firstByteStart: 1724415467775,\n        firstByteDone: 1724415468555,\n        transferStart: 1724415468555,\n        transferDone: 1724415468555,\n      },\n      region: \"cdg\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1081,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:48 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=58ff47d67b2d21ac08cb6c1d3b20eb12e29dd53aaa2051d90c6f364ab5e0d544%7C46683fe543ad2887543880291cd12267eeb2d82b956d3c59ff01e324fe846fd5; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"sfo1::fra1::k8shj-1724415468145-cc12e589b9ec\",\n      },\n      timestamp: 1724415467973,\n      timing: {\n        dnsStart: 1724415468074,\n        dnsDone: 1724415468075,\n        connectStart: 1724415468075,\n        connectDone: 1724415468075,\n        tlsHandshakeStart: 1724415468075,\n        tlsHandshakeDone: 1724415468132,\n        firstByteStart: 1724415468133,\n        firstByteDone: 1724415469055,\n        transferStart: 1724415469055,\n        transferDone: 1724415469055,\n      },\n      region: \"den\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1329,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:49 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=76ffd678bc935c42a7dbca29fd4f0ba40698fa20d2554b224a581fa7f0f09853%7C44a90f79d338272da4630c8275f8c3b5571b5f8fb39de169ab11e25632d2c144; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"cle1::fra1::sxq92-1724415468662-2cd7c277d058\",\n      },\n      timestamp: 1724415468380,\n      timing: {\n        dnsStart: 1724415468552,\n        dnsDone: 1724415468554,\n        connectStart: 1724415468554,\n        connectDone: 1724415468554,\n        tlsHandshakeStart: 1724415468554,\n        tlsHandshakeDone: 1724415468641,\n        firstByteStart: 1724415468641,\n        firstByteDone: 1724415469710,\n        transferStart: 1724415469710,\n        transferDone: 1724415469710,\n      },\n      region: \"dfw\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 380,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:48 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=195d8721e93ff6a135dc1a2bf7c4c7b76961ddd3662e56815c7ec6a3ce1abd3d%7C2993b6c22635423908d6f2ec1a1aaced8ed391d05fc3125f31dac992d7428197; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"iad1::fra1::8dpsb-1724415468744-2a4aa1e3d5ab\",\n      },\n      timestamp: 1724415468672,\n      timing: {\n        dnsStart: 1724415468716,\n        dnsDone: 1724415468721,\n        connectStart: 1724415468721,\n        connectDone: 1724415468722,\n        tlsHandshakeStart: 1724415468722,\n        tlsHandshakeDone: 1724415468740,\n        firstByteStart: 1724415468740,\n        firstByteDone: 1724415469052,\n        transferStart: 1724415469052,\n        transferDone: 1724415469052,\n      },\n      region: \"ewr\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 802,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:49 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=4c6a43acbb9675aa48a54684ba789486dcdd8551b3cce40cc3089646627b9226%7C1ed25c2ef54680a28c75592c15f459c2b48bf294ec578191f9c1047a3b8ffe79; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"gru1::fra1::g8pl7-1724415469119-c65e63554739\",\n      },\n      timestamp: 1724415468749,\n      timing: {\n        dnsStart: 1724415468984,\n        dnsDone: 1724415469043,\n        connectStart: 1724415469043,\n        connectDone: 1724415469043,\n        tlsHandshakeStart: 1724415469043,\n        tlsHandshakeDone: 1724415469103,\n        firstByteStart: 1724415469104,\n        firstByteDone: 1724415469552,\n        transferStart: 1724415469552,\n        transferDone: 1724415469552,\n      },\n      region: \"eze\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 615,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:49 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=1cc1e9eb9bdc56c9b698df0afc275633753d12d06f312e353cc34f7a84531412%7Ce2e75b4bfba94809563f68e206c52f0389ac93aa31094b5b4db3771fd5c41781; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Id\": \"fra1::fra1::kv28h-1724415468785-c9192137c9ed\",\n      },\n      timestamp: 1724415468739,\n      timing: {\n        dnsStart: 1724415468775,\n        dnsDone: 1724415468779,\n        connectStart: 1724415468779,\n        connectDone: 1724415468782,\n        tlsHandshakeStart: 1724415468782,\n        tlsHandshakeDone: 1724415468789,\n        firstByteStart: 1724415468789,\n        firstByteDone: 1724415469354,\n        transferStart: 1724415469354,\n        transferDone: 1724415469354,\n      },\n      region: \"fra\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1481,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:50 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=938d20933200b21638ed84e60832085dc135e3193a233720de87b0e41a24e2fd%7Cfcd423e5d8e965dc0e23728cf52a6f3e531cfe5a007942f6cb0a57fc90ac2bd8; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"sfo1::fra1::jn45p-1724415470146-7160122c2733\",\n      },\n      timestamp: 1724415469619,\n      timing: {\n        dnsStart: 1724415469889,\n        dnsDone: 1724415469947,\n        connectStart: 1724415469947,\n        connectDone: 1724415469953,\n        tlsHandshakeStart: 1724415469953,\n        tlsHandshakeDone: 1724415470104,\n        firstByteStart: 1724415470104,\n        firstByteDone: 1724415471100,\n        transferStart: 1724415471100,\n        transferDone: 1724415471100,\n      },\n      region: \"gdl\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 768,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:50 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=05ef42987dc7df7b05cee0541b776c2cd51eba97ba713153f215e8550ae0448c%7C3c1a941480a5c26bc3e7434709576dacc81ea4f05359505eebd2d195e63aeb74; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"gru1::fra1::df9rf-1724415469769-16109f11a918\",\n      },\n      timestamp: 1724415469592,\n      timing: {\n        dnsStart: 1724415469639,\n        dnsDone: 1724415469748,\n        connectStart: 1724415469748,\n        connectDone: 1724415469748,\n        tlsHandshakeStart: 1724415469748,\n        tlsHandshakeDone: 1724415469765,\n        firstByteStart: 1724415469765,\n        firstByteDone: 1724415470358,\n        transferStart: 1724415470358,\n        transferDone: 1724415470360,\n      },\n      region: \"gig\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 662,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:50 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=79dbfc2a5ba28ebb28864cdeee17cbc42ce9901946370dec247bcdb0f363cd5e%7Ca332b8cc3c8a8713af109bdbc5b9ef831e6302f86a2455c662d84c74e62b09b8; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"gru1::fra1::ps6s8-1724415469878-6e2b2b9481e6\",\n      },\n      timestamp: 1724415469642,\n      timing: {\n        dnsStart: 1724415469762,\n        dnsDone: 1724415469871,\n        connectStart: 1724415469871,\n        connectDone: 1724415469872,\n        tlsHandshakeStart: 1724415469872,\n        tlsHandshakeDone: 1724415469877,\n        firstByteStart: 1724415469877,\n        firstByteDone: 1724415470304,\n        transferStart: 1724415470304,\n        transferDone: 1724415470304,\n      },\n      region: \"gru\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1543,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:51 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=f0b519b719c63760474410a3ae680668fbc06da25fddab90c3e5fb04e3dd5320%7C630e28ddd164b4c4107414be9daca7a65cee210ca88c355366ef7855a63efb0f; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"hkg1::fra1::fxvwk-1724415469898-52ddb6281608\",\n      },\n      timestamp: 1724415469801,\n      timing: {\n        dnsStart: 1724415469859,\n        dnsDone: 1724415469892,\n        connectStart: 1724415469892,\n        connectDone: 1724415469893,\n        tlsHandshakeStart: 1724415469893,\n        tlsHandshakeDone: 1724415469898,\n        firstByteStart: 1724415469898,\n        firstByteDone: 1724415471344,\n        transferStart: 1724415471344,\n        transferDone: 1724415471344,\n      },\n      region: \"hkg\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 369,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:50 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=e40f4b2e630f81375e79b742870729b7e008bf85b4523cd8c8b83a39fd31bb9f%7Ca062303e57c49fefa52f5bb256282d5b257bc8c98e8b34a1ededddfe77215003; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"iad1::fra1::lpxzz-1724415470208-6564146436d5\",\n      },\n      timestamp: 1724415470173,\n      timing: {\n        dnsStart: 1724415470193,\n        dnsDone: 1724415470196,\n        connectStart: 1724415470196,\n        connectDone: 1724415470198,\n        tlsHandshakeStart: 1724415470198,\n        tlsHandshakeDone: 1724415470206,\n        firstByteStart: 1724415470206,\n        firstByteDone: 1724415470542,\n        transferStart: 1724415470542,\n        transferDone: 1724415470542,\n      },\n      region: \"iad\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1264,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:51 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=d4ad7992b9fd60115ddc589306804e8cd65d569d6afd66f95eec72045a0c74ed%7C0dcddba799b4a2e429448cea0f347886e730f2905e8a464d332889cf0dd2afc2; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"cpt1::fra1::lnlth-1724415471275-cf922a128799\",\n      },\n      timestamp: 1724415470963,\n      timing: {\n        dnsStart: 1724415471216,\n        dnsDone: 1724415471218,\n        connectStart: 1724415471218,\n        connectDone: 1724415471218,\n        tlsHandshakeStart: 1724415471218,\n        tlsHandshakeDone: 1724415471262,\n        firstByteStart: 1724415471262,\n        firstByteDone: 1724415472228,\n        transferStart: 1724415472228,\n        transferDone: 1724415472228,\n      },\n      region: \"jnb\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 642,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:51 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=7c255e198252b31b58e293332d0cea5198cd5d92acf2a3745214680530a48bae%7C15bfe39b4d88d60cb55d7e7e34ccff17ccb7bbe9a5c76200584c510cfd310586; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"sfo1::fra1::tpm8t-1724415471358-ec064b0eab3d\",\n      },\n      timestamp: 1724415471269,\n      timing: {\n        dnsStart: 1724415471325,\n        dnsDone: 1724415471327,\n        connectStart: 1724415471327,\n        connectDone: 1724415471328,\n        tlsHandshakeStart: 1724415471328,\n        tlsHandshakeDone: 1724415471352,\n        firstByteStart: 1724415471352,\n        firstByteDone: 1724415471912,\n        transferStart: 1724415471912,\n        transferDone: 1724415471912,\n      },\n      region: \"lax\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 627,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:51 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=399ab06338d52f31d31203af84d5f3657e380eb9317fd1a71bfd41d77abe27bb%7Cae0a0909fe500b07deb5867b0336f0176a501bb34460de004432aa7c3ccd1de2; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"lhr1::fra1::7lf7n-1724415471374-7ef5cab51600\",\n      },\n      timestamp: 1724415471324,\n      timing: {\n        dnsStart: 1724415471360,\n        dnsDone: 1724415471366,\n        connectStart: 1724415471366,\n        connectDone: 1724415471368,\n        tlsHandshakeStart: 1724415471368,\n        tlsHandshakeDone: 1724415471373,\n        firstByteStart: 1724415471373,\n        firstByteDone: 1724415471952,\n        transferStart: 1724415471952,\n        transferDone: 1724415471952,\n      },\n      region: \"lhr\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 951,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:52 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=d3c263dfb22b4a07fb9fde65227c2d3b97d969874b6c7a394a73a543c35d9364%7Cdea8ecc64ee8311f4a7904b6fea3aefeca1250b9c1ee9b3ae7efd23d9938d5bb; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"cdg1::fra1::jszwl-1724415471627-5821aab52b06\",\n      },\n      timestamp: 1724415471373,\n      timing: {\n        dnsStart: 1724415471554,\n        dnsDone: 1724415471556,\n        connectStart: 1724415471556,\n        connectDone: 1724415471571,\n        tlsHandshakeStart: 1724415471571,\n        tlsHandshakeDone: 1724415471613,\n        firstByteStart: 1724415471613,\n        firstByteDone: 1724415472324,\n        transferStart: 1724415472324,\n        transferDone: 1724415472324,\n      },\n      region: \"mad\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 808,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:52 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=6b10a5354dfa3d39ee556f78017e896e0bde4abe9e9583693044bf81f9af32ee%7Cea36450ea545200dc9a035d5b4c6ca6c8df7e88b1a4c71c97a9c9313ca39aebf; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"iad1::fra1::jscbc-1724415472309-b6db76af787c\",\n      },\n      timestamp: 1724415472154,\n      timing: {\n        dnsStart: 1724415472239,\n        dnsDone: 1724415472240,\n        connectStart: 1724415472240,\n        connectDone: 1724415472266,\n        tlsHandshakeStart: 1724415472266,\n        tlsHandshakeDone: 1724415472296,\n        firstByteStart: 1724415472296,\n        firstByteDone: 1724415472963,\n        transferStart: 1724415472963,\n        transferDone: 1724415472963,\n      },\n      region: \"mia\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1301,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:53 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=3414880cc0d90ac4a648e9e8a1aad136d3db41001cfcb8fda20129777cae0449%7Cf971b35cc728fe0fc205ef61ca344be04e81e52e0b92067745fdd3b520a481cd; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"hnd1::fra1::kxkwr-1724415472423-82ba874bb590\",\n      },\n      timestamp: 1724415472357,\n      timing: {\n        dnsStart: 1724415472405,\n        dnsDone: 1724415472410,\n        connectStart: 1724415472410,\n        connectDone: 1724415472412,\n        tlsHandshakeStart: 1724415472412,\n        tlsHandshakeDone: 1724415472420,\n        firstByteStart: 1724415472421,\n        firstByteDone: 1724415473658,\n        transferStart: 1724415473658,\n        transferDone: 1724415473658,\n      },\n      region: \"nrt\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1079,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:53 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=b840e2646e8198e2c657d1002914c88d6c5b93189b34ed82855975164b9675ac%7Cf35eda6a163c51d096d35e6c520d663115bfaa195889068885e68bd38b9f8ac7; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"cle1::fra1::8zclb-1724415472414-c402d33465fc\",\n      },\n      timestamp: 1724415472323,\n      timing: {\n        dnsStart: 1724415472378,\n        dnsDone: 1724415472380,\n        connectStart: 1724415472380,\n        connectDone: 1724415472382,\n        tlsHandshakeStart: 1724415472382,\n        tlsHandshakeDone: 1724415472408,\n        firstByteStart: 1724415472408,\n        firstByteDone: 1724415473402,\n        transferStart: 1724415473402,\n        transferDone: 1724415473402,\n      },\n      region: \"ord\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1349,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:53 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=637d460fe50112a95818857adbea59b9cf1f184f57d90001eaefc2e3b613ad61%7Cfa046a39b9170382ba0a848d9ea21ad86f035d0ff11f677cf7cac182d2a8a744; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Id\": \"fra1::fra1::tjkng-1724415472738-a6c3d672835c\",\n      },\n      timestamp: 1724415472487,\n      timing: {\n        dnsStart: 1724415472627,\n        dnsDone: 1724415472658,\n        connectStart: 1724415472658,\n        connectDone: 1724415472676,\n        tlsHandshakeStart: 1724415472676,\n        tlsHandshakeDone: 1724415472723,\n        firstByteStart: 1724415472723,\n        firstByteDone: 1724415473837,\n        transferStart: 1724415473837,\n        transferDone: 1724415473837,\n      },\n      region: \"otp\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 970,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:53 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=d9b23db6a8a5c5d9cfee5f1803059ee708aca2e2bd43c9b59854ba3899acd6f0%7Cac93af4a4b2944ebd4bbdadbed95d64726f7e841c6ce75ba3d73d40516e1652b; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"sfo1::fra1::jn45p-1724415473337-466a2679cdf6\",\n      },\n      timestamp: 1724415472895,\n      timing: {\n        dnsStart: 1724415473151,\n        dnsDone: 1724415473169,\n        connectStart: 1724415473169,\n        connectDone: 1724415473212,\n        tlsHandshakeStart: 1724415473212,\n        tlsHandshakeDone: 1724415473311,\n        firstByteStart: 1724415473311,\n        firstByteDone: 1724415473865,\n        transferStart: 1724415473865,\n        transferDone: 1724415473865,\n      },\n      region: \"phx\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1539,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:55 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=6c483b6918c385243a810bdfad2268d79f7a5501a45b2f12d5b073c10c1ebd96%7C56115dbd9537df4a9579c3bcb15ace5ea2265f679eac9f8ea1d0e850bf618f41; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"sfo1::fra1::cfhm9-1724415474388-d835d118b121\",\n      },\n      timestamp: 1724415473741,\n      timing: {\n        dnsStart: 1724415474074,\n        dnsDone: 1724415474203,\n        connectStart: 1724415474203,\n        connectDone: 1724415474204,\n        tlsHandshakeStart: 1724415474204,\n        tlsHandshakeDone: 1724415474350,\n        firstByteStart: 1724415474350,\n        firstByteDone: 1724415475280,\n        transferStart: 1724415475280,\n        transferDone: 1724415475280,\n      },\n      region: \"qro\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1347,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:55 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=06921285d96e59b991fb4e99210f3d65e85d8f56adb67d3e722598ef8766ce33%7Cc197f92fb2deac3a9cce800c50c2be456fdd3e076f8e2faecbbf72c4bdae4559; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"gru1::fra1::dtczd-1724415474697-bc49fc211c26\",\n      },\n      timestamp: 1724415474183,\n      timing: {\n        dnsStart: 1724415474470,\n        dnsDone: 1724415474578,\n        connectStart: 1724415474578,\n        connectDone: 1724415474578,\n        tlsHandshakeStart: 1724415474578,\n        tlsHandshakeDone: 1724415474676,\n        firstByteStart: 1724415474676,\n        firstByteDone: 1724415475530,\n        transferStart: 1724415475530,\n        transferDone: 1724415475530,\n      },\n      region: \"scl\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 400,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:54 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=810c745c559dd6cd590e3ecf94d45604336ef8be9ffea1cde0c53e48da57347d%7C561ec5a1fb89b523d861a3d0da4f095a50ad8386ac6a67c1325a0fee483467de; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"sfo1::fra1::wwf2m-1724415474188-b0c6f71df2db\",\n      },\n      timestamp: 1724415474161,\n      timing: {\n        dnsStart: 1724415474177,\n        dnsDone: 1724415474180,\n        connectStart: 1724415474180,\n        connectDone: 1724415474180,\n        tlsHandshakeStart: 1724415474180,\n        tlsHandshakeDone: 1724415474187,\n        firstByteStart: 1724415474187,\n        firstByteDone: 1724415474561,\n        transferStart: 1724415474561,\n        transferDone: 1724415474562,\n      },\n      region: \"sjc\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 883,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:55 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=401b5b1bf92e519ea7487cbc10260464ed784af62dd6469cca701013a256a801%7C2b2e8fd45b7c29091dfe92642ea51a4473ec6f06af28181ce374a1db856b5d38; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"pdx1::fra1::fdxft-1724415474318-e900cbf850a7\",\n      },\n      timestamp: 1724415474239,\n      timing: {\n        dnsStart: 1724415474291,\n        dnsDone: 1724415474294,\n        connectStart: 1724415474294,\n        connectDone: 1724415474294,\n        tlsHandshakeStart: 1724415474294,\n        tlsHandshakeDone: 1724415474315,\n        firstByteStart: 1724415474315,\n        firstByteDone: 1724415475122,\n        transferStart: 1724415475122,\n        transferDone: 1724415475122,\n      },\n      region: \"sea\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 825,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:55 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=7f7ea0f4e87ec4aa383bf93a55a06f51cc8f94c6b47f1dd7a4fa64ddd30f7224%7Cfe689d60c5036e0b56e410d2422c0f96dca2039184d3bbc41f8e910fd7824bbc; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"sin1::fra1::b8msv-1724415474849-b7ad6db342b4\",\n      },\n      timestamp: 1724415474736,\n      timing: {\n        dnsStart: 1724415474838,\n        dnsDone: 1724415474840,\n        connectStart: 1724415474840,\n        connectDone: 1724415474841,\n        tlsHandshakeStart: 1724415474841,\n        tlsHandshakeDone: 1724415474848,\n        firstByteStart: 1724415474848,\n        firstByteDone: 1724415475561,\n        transferStart: 1724415475561,\n        transferDone: 1724415475561,\n      },\n      region: \"sin\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 526,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:55 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=9fafdef83adf38bf4b48c801e2b1d5c2ce04858dcddbe69ca576e6d9945f2d9a%7Ced402758800ee1c6dee8fd49119ef549b0a4ecbbccd9f178c87ac32c26d29433; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"syd1::fra1::p48zw-1724415475067-dce86bb55ee4\",\n      },\n      timestamp: 1724415475023,\n      timing: {\n        dnsStart: 1724415475058,\n        dnsDone: 1724415475059,\n        connectStart: 1724415475059,\n        connectDone: 1724415475060,\n        tlsHandshakeStart: 1724415475060,\n        tlsHandshakeDone: 1724415475065,\n        firstByteStart: 1724415475065,\n        firstByteDone: 1724415475549,\n        transferStart: 1724415475549,\n        transferDone: 1724415475549,\n      },\n      region: \"syd\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 869,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:56 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=7c97710f74a86e1e01522a7b84e73f02099553eb09a1b3f6bb2bdaad009b2dbd%7C5e26b720c7c54a98dc858077c2496989dfaf4f24b129ad9ecce09136c1bc1d4f; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Id\": \"fra1::fra1::dlk76-1724415475594-7c5b9c827a3d\",\n      },\n      timestamp: 1724415475389,\n      timing: {\n        dnsStart: 1724415475504,\n        dnsDone: 1724415475527,\n        connectStart: 1724415475527,\n        connectDone: 1724415475545,\n        tlsHandshakeStart: 1724415475545,\n        tlsHandshakeDone: 1724415475582,\n        firstByteStart: 1724415475582,\n        firstByteDone: 1724415476259,\n        transferStart: 1724415476259,\n        transferDone: 1724415476259,\n      },\n      region: \"waw\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 1133,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:56 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=c00e54372624088fced657d119128dc6d43eee5d5ea8209d16655f4cc67d930a%7Cfff1ca52bcf8e9cd9b92ac4de667991ffa5b445b8c77aad31cfa38c6e86a3d14; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"iad1::fra1::dc8tg-1724415476028-5d259d762ec1\",\n      },\n      timestamp: 1724415475892,\n      timing: {\n        dnsStart: 1724415475952,\n        dnsDone: 1724415475978,\n        connectStart: 1724415475978,\n        connectDone: 1724415475979,\n        tlsHandshakeStart: 1724415475979,\n        tlsHandshakeDone: 1724415476020,\n        firstByteStart: 1724415476020,\n        firstByteDone: 1724415477025,\n        transferStart: 1724415477025,\n        transferDone: 1724415477025,\n      },\n      region: \"yul\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n\n      status: 200,\n      latency: 447,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:56 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Vercel\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=aca1d19c78d030efdcedc23883f78d9bc1f2d6d21bd5e2a8ac0026560f3d7f17%7Cf87108886605e4a306ce630f3b322c83fd8cab2dc49c709be5b4a59f5c22b4d0; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Vercel-Cache\": \"MISS\",\n        \"X-Vercel-Execution-Region\": \"fra1\",\n        \"X-Vercel-Id\": \"cle1::fra1::bckhc-1724415476215-f3c71058df41\",\n      },\n      timestamp: 1724415476081,\n      timing: {\n        dnsStart: 1724415476155,\n        dnsDone: 1724415476157,\n        connectStart: 1724415476157,\n        connectDone: 1724415476158,\n        tlsHandshakeStart: 1724415476158,\n        tlsHandshakeDone: 1724415476204,\n        firstByteStart: 1724415476204,\n        firstByteDone: 1724415476529,\n        transferStart: 1724415476529,\n        transferDone: 1724415476529,\n      },\n      region: \"yyz\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 720,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:57 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Koyeb\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=8f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b%7C4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Koyeb-Cache\": \"MISS\",\n        \"X-Koyeb-Execution-Region\": \"fra\",\n        \"X-Koyeb-Id\": \"koyeb-fra::fra::abc123-1724415477000-def456789\",\n      },\n      timestamp: 1724415477000,\n      timing: {\n        dnsStart: 1724415477100,\n        dnsDone: 1724415477102,\n        connectStart: 1724415477102,\n        connectDone: 1724415477103,\n        tlsHandshakeStart: 1724415477103,\n        tlsHandshakeDone: 1724415477120,\n        firstByteStart: 1724415477120,\n        firstByteDone: 1724415477820,\n        transferStart: 1724415477820,\n        transferDone: 1724415477820,\n      },\n      region: \"koyeb_fra\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 680,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:57 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Koyeb\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=9a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d%7C5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Koyeb-Cache\": \"MISS\",\n        \"X-Koyeb-Execution-Region\": \"par\",\n        \"X-Koyeb-Id\": \"koyeb-par::par::xyz789-1724415477100-ghi012345\",\n      },\n      timestamp: 1724415477100,\n      timing: {\n        dnsStart: 1724415477200,\n        dnsDone: 1724415477202,\n        connectStart: 1724415477202,\n        connectDone: 1724415477203,\n        tlsHandshakeStart: 1724415477203,\n        tlsHandshakeDone: 1724415477220,\n        firstByteStart: 1724415477220,\n        firstByteDone: 1724415477900,\n        transferStart: 1724415477900,\n        transferDone: 1724415477900,\n      },\n      region: \"koyeb_par\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 450,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:57 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Koyeb\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e%7C6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Koyeb-Cache\": \"MISS\",\n        \"X-Koyeb-Execution-Region\": \"sfo\",\n        \"X-Koyeb-Id\": \"koyeb-sfo::sfo::mno456-1724415477200-pqr789012\",\n      },\n      timestamp: 1724415477200,\n      timing: {\n        dnsStart: 1724415477300,\n        dnsDone: 1724415477302,\n        connectStart: 1724415477302,\n        connectDone: 1724415477303,\n        tlsHandshakeStart: 1724415477303,\n        tlsHandshakeDone: 1724415477320,\n        firstByteStart: 1724415477320,\n        firstByteDone: 1724415477650,\n        transferStart: 1724415477650,\n        transferDone: 1724415477650,\n      },\n      region: \"koyeb_sfo\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 920,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:58 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Koyeb\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f%7C7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Koyeb-Cache\": \"MISS\",\n        \"X-Koyeb-Execution-Region\": \"sin\",\n        \"X-Koyeb-Id\": \"koyeb-sin::sin::stu345-1724415477300-vwx678901\",\n      },\n      timestamp: 1724415477300,\n      timing: {\n        dnsStart: 1724415477400,\n        dnsDone: 1724415477402,\n        connectStart: 1724415477402,\n        connectDone: 1724415477403,\n        tlsHandshakeStart: 1724415477403,\n        tlsHandshakeDone: 1724415477420,\n        firstByteStart: 1724415477420,\n        firstByteDone: 1724415478340,\n        transferStart: 1724415478340,\n        transferDone: 1724415478340,\n      },\n      region: \"koyeb_sin\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 1100,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:58 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Koyeb\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a%7C8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Koyeb-Cache\": \"MISS\",\n        \"X-Koyeb-Execution-Region\": \"tyo\",\n        \"X-Koyeb-Id\": \"koyeb-tyo::tyo::yza234-1724415477400-bcd567890\",\n      },\n      timestamp: 1724415477400,\n      timing: {\n        dnsStart: 1724415477500,\n        dnsDone: 1724415477502,\n        connectStart: 1724415477502,\n        connectDone: 1724415477503,\n        tlsHandshakeStart: 1724415477503,\n        tlsHandshakeDone: 1724415477520,\n        firstByteStart: 1724415477520,\n        firstByteDone: 1724415478620,\n        transferStart: 1724415478620,\n        transferDone: 1724415478620,\n      },\n      region: \"koyeb_tyo\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 380,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:58 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Koyeb\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b%7C9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Koyeb-Cache\": \"MISS\",\n        \"X-Koyeb-Execution-Region\": \"was\",\n        \"X-Koyeb-Id\": \"koyeb-was::was::efg123-1724415477500-hij456789\",\n      },\n      timestamp: 1724415477500,\n      timing: {\n        dnsStart: 1724415477600,\n        dnsDone: 1724415477602,\n        connectStart: 1724415477602,\n        connectDone: 1724415477603,\n        tlsHandshakeStart: 1724415477603,\n        tlsHandshakeDone: 1724415477620,\n        firstByteStart: 1724415477620,\n        firstByteDone: 1724415478000,\n        transferStart: 1724415478000,\n        transferDone: 1724415478000,\n      },\n      region: \"koyeb_was\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 1100,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:58 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Railway\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c%7C0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Railway-Cache\": \"MISS\",\n        \"X-Railway-Execution-Region\": \"us-west2\",\n        \"X-Railway-Id\":\n          \"railway-us-west2::us-west2::klm345-1724415477600-nop67890\",\n      },\n      timestamp: 1724415477600,\n      timing: {\n        dnsStart: 1724415477700,\n        dnsDone: 1724415477702,\n        connectStart: 1724415477702,\n        connectDone: 1724415477703,\n        tlsHandshakeStart: 1724415477703,\n        tlsHandshakeDone: 1724415477720,\n        firstByteStart: 1724415477720,\n        firstByteDone: 1724415478720,\n        transferStart: 1724415478720,\n        transferDone: 1724415478720,\n      },\n      region: \"railway_us-west2\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 1201,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:58 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Railway\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c%7C0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Railway-Cache\": \"MISS\",\n        \"X-Railway-Execution-Region\": \"us-west2\",\n        \"X-Railway-Id\":\n          \"railway-us-west2::us-west2::klm345-1724415477600-nop67890\",\n      },\n      timestamp: 1724415477700,\n      timing: {\n        dnsStart: 1724415477800,\n        dnsDone: 1724415477802,\n        connectStart: 1724415477802,\n        connectDone: 1724415477803,\n        tlsHandshakeStart: 1724415477803,\n        tlsHandshakeDone: 1724415477820,\n        firstByteStart: 1724415477820,\n        firstByteDone: 1724415478820,\n        transferStart: 1724415478820,\n        transferDone: 1724415478820,\n      },\n      region: \"railway_us-east4-eqdc4a\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 612,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:58 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Railway\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c%7C0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Railway-Cache\": \"MISS\",\n        \"X-Railway-Execution-Region\": \"us-west2\",\n        \"X-Railway-Id\":\n          \"railway-us-west2::us-west2::klm345-1724415477600-nop67890\",\n      },\n      timestamp: 1724415477800,\n      timing: {\n        dnsStart: 1724415477900,\n        dnsDone: 1724415477902,\n        connectStart: 1724415477902,\n        connectDone: 1724415477903,\n        tlsHandshakeStart: 1724415477903,\n        tlsHandshakeDone: 1724415477920,\n        firstByteStart: 1724415477920,\n        firstByteDone: 1724415478920,\n        transferStart: 1724415478920,\n        transferDone: 1724415478920,\n      },\n      region: \"railway_europe-west4-drams3a\",\n    },\n    {\n      type: \"http\",\n      state: \"success\",\n      status: 200,\n      latency: 705,\n      headers: {\n        Age: \"0\",\n        \"Cache-Control\":\n          \"private, no-cache, no-store, max-age=0, must-revalidate\",\n        \"Content-Type\": \"text/html; charset=utf-8\",\n        Date: \"Fri, 23 Aug 2024 12:17:58 GMT\",\n        Link: '</_next/static/media/162bf645eb375add-s.p.ttf>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/ttf\", </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as=\"font\"; crossorigin=\"\"; type=\"font/woff2\"',\n        Server: \"Railway\",\n        \"Set-Cookie\":\n          \"__Host-authjs.csrf-token=e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c%7C0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"Strict-Transport-Security\": \"max-age=63072000\",\n        Vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n        \"X-Matched-Path\": \"/\",\n        \"X-Powered-By\": \"Next.js\",\n        \"X-Railway-Cache\": \"MISS\",\n        \"X-Railway-Execution-Region\": \"us-west2\",\n        \"X-Railway-Id\":\n          \"railway-us-west2::us-west2::klm345-1724415477600-nop67890\",\n      },\n      timestamp: 1724415477900,\n      timing: {\n        dnsStart: 1724415478000,\n        dnsDone: 1724415478002,\n        connectStart: 1724415478002,\n        connectDone: 1724415478003,\n        tlsHandshakeStart: 1724415478003,\n        tlsHandshakeDone: 1724415478020,\n        firstByteStart: 1724415478020,\n        firstByteDone: 1724415479020,\n        transferStart: 1724415479020,\n        transferDone: 1724415479020,\n      },\n      region: \"railway_asia-southeast1-eqsg3a\",\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/web/src/lib/checker/utils.ts",
    "content": "import { z } from \"zod\";\n\nimport { monitorRegionSchema } from \"@openstatus/db/src/schema/constants\";\nimport type { Region } from \"@openstatus/db/src/schema/constants\";\nimport { continentDict, getRegionInfo, regionDict } from \"@openstatus/regions\";\nimport { Redis } from \"@upstash/redis\";\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst FLY_CHECKER_URL = \"https://checker.openstatus.dev/ping\";\nconst KOYEB_CHECKER_URL = \"https://openstatus-checker.koyeb.app/ping\";\nconst RAILWAY_CHECKER_URL =\n  \"https://railway-proxy-production-9cb1.up.railway.app/ping\";\n\n// ============================================================================\n// Formatters\n// ============================================================================\n\nexport function latencyFormatter(value: number) {\n  return `${new Intl.NumberFormat(\"us\").format(value).toString()}ms`;\n}\n\nexport function timestampFormatter(timestamp: number) {\n  return new Date(timestamp).toUTCString(); // GMT format\n}\n\nexport function continentFormatter(region: Region) {\n  const continent = regionDict[region].continent;\n  return continentDict[continent].code;\n}\n\nexport function regionFormatter(\n  region: string,\n  type: \"short\" | \"long\" = \"short\",\n) {\n  const { code, flag, location } = getRegionInfo(region);\n  if (type === \"short\") return `${code} ${flag}`;\n  return `${location} ${flag}`;\n}\n\n// ============================================================================\n// Timing Utilities\n// ============================================================================\n\nexport function getTotalLatency(timing: Timing) {\n  const { dns, connection, tls, ttfb, transfer } = getTimingPhases(timing);\n  return dns + connection + tls + ttfb + transfer;\n}\n\nexport function getTimingPhases(timing: Timing) {\n  const dns = timing.dnsDone - timing.dnsStart;\n  const connection = timing.connectDone - timing.connectStart;\n  const tls = timing.tlsHandshakeDone - timing.tlsHandshakeStart;\n  const ttfb = timing.firstByteDone - timing.firstByteStart;\n  const transfer = timing.transferDone - timing.transferStart;\n\n  return {\n    dns,\n    connection,\n    tls,\n    ttfb,\n    transfer,\n  };\n}\n\nexport function getTimingPhasesWidth(timing: Timing) {\n  const total = getTotalLatency(timing);\n  const phases = getTimingPhases(timing);\n\n  const dns = { preWidth: 0, width: (phases.dns / total) * 100 };\n\n  const connection = {\n    preWidth: dns.preWidth + dns.width,\n    width: (phases.connection / total) * 100,\n  };\n\n  const tls = {\n    preWidth: connection.preWidth + connection.width,\n    width: (phases.tls / total) * 100,\n  };\n\n  const ttfb = {\n    preWidth: tls.preWidth + tls.width,\n    width: (phases.ttfb / total) * 100,\n  };\n\n  const transfer = {\n    preWidth: ttfb.preWidth + ttfb.width,\n    width: (phases.transfer / total) * 100,\n  };\n\n  return {\n    dns,\n    connection,\n    tls,\n    ttfb,\n    transfer,\n  };\n}\n\n// ============================================================================\n// Schemas & Types\n// ============================================================================\n\nexport const timingSchema = z.object({\n  dnsStart: z.number(),\n  dnsDone: z.number(),\n  connectStart: z.number(),\n  connectDone: z.number(),\n  tlsHandshakeStart: z.number(),\n  tlsHandshakeDone: z.number(),\n  firstByteStart: z.number(),\n  firstByteDone: z.number(),\n  transferStart: z.number(),\n  transferDone: z.number(),\n});\n\nexport const checkerSchema = z.object({\n  type: z.literal(\"http\").prefault(\"http\"),\n  state: z.literal(\"success\").prefault(\"success\"),\n  status: z.number(),\n  latency: z.number(),\n  headers: z.record(z.string(), z.string()),\n  timestamp: z.number(),\n  timing: timingSchema,\n  body: z.string().optional().nullable(),\n});\n\nexport const cachedCheckerSchema = z.object({\n  url: z.string(),\n  timestamp: z.number(),\n  method: z.string(), // Simplified - validation happens at runtime\n  headers: z.array(z.object({ key: z.string(), value: z.string() })).optional(),\n  body: z.string().optional(),\n  checks: checkerSchema.extend({ region: monitorRegionSchema }).array(),\n});\n\nconst errorRequest = z.object({\n  message: z.string(),\n  state: z.literal(\"error\").prefault(\"error\"),\n});\n\nexport const regionCheckerSchema = checkerSchema.extend({\n  region: monitorRegionSchema,\n  state: z.literal(\"success\").prefault(\"success\"),\n});\n\nexport const regionCheckerSchemaResponse = regionCheckerSchema.or(\n  errorRequest.extend({\n    region: monitorRegionSchema,\n  }),\n);\n\nexport type Timing = z.infer<typeof timingSchema>;\nexport type Checker = z.infer<typeof checkerSchema>;\nexport type RegionChecker = z.infer<typeof regionCheckerSchema>;\nexport type RegionCheckerResponse = z.infer<typeof regionCheckerSchemaResponse>;\nexport type Method =\n  | \"GET\"\n  | \"HEAD\"\n  | \"OPTIONS\"\n  | \"POST\"\n  | \"PUT\"\n  | \"DELETE\"\n  | \"PATCH\"\n  | \"CONNECT\"\n  | \"TRACE\";\nexport type CachedRegionChecker = z.infer<typeof cachedCheckerSchema>;\nexport type ErrorRequest = z.infer<typeof errorRequest>;\n\ntype CheckRegionRequest = {\n  url: string;\n  region: Region;\n  method?: Method;\n  headers?: { value: string; key: string }[];\n  body?: string;\n};\n\n// ============================================================================\n// API Functions\n// ============================================================================\n\nexport async function checkRegion(\n  props: CheckRegionRequest,\n): Promise<RegionCheckerResponse> {\n  const { url, region, method, headers, body } = props;\n  const regionInfo = regionDict[region];\n\n  let endpoint = \"\";\n  let regionHeader = {};\n  switch (regionInfo.provider) {\n    case \"fly\":\n      endpoint = `${FLY_CHECKER_URL}/${region}`;\n      regionHeader = { \"fly-prefer-region\": region };\n      break;\n    case \"koyeb\":\n      endpoint = `${KOYEB_CHECKER_URL}/${region}`;\n      regionHeader = {\n        \"X-KOYEB-REGION-OVERRIDE\": region.replace(\"koyeb_\", \"\"),\n      };\n      break;\n    case \"railway\":\n      endpoint = `${RAILWAY_CHECKER_URL}/${region}`;\n      regionHeader = { \"railway-region\": region.replace(\"railway_\", \"\") };\n      break;\n    default:\n      break;\n  }\n\n  const res = await fetch(endpoint, {\n    headers: {\n      Authorization: `Basic ${process.env.CRON_SECRET}`,\n      \"Content-Type\": \"application/json\",\n      ...regionHeader,\n    },\n    method: \"POST\",\n    body: JSON.stringify({\n      url,\n      method: method || \"GET\",\n      headers: headers?.reduce(\n        (acc, { key, value }) => {\n          if (!key) return acc; // key === \"\" is an invalid header\n          return { ...acc, [key]: value };\n        },\n        {} as Record<string, string>,\n      ),\n      body: body ? body : undefined,\n    }),\n    next: { revalidate: 0 },\n  });\n\n  const json = await res.json();\n\n  const data = checkerSchema.or(errorRequest).safeParse(json);\n\n  if (!data.success) {\n    console.error(JSON.stringify(res));\n    console.error(JSON.stringify(json));\n    console.error(\n      `something went wrong with request to ${url} error ${data.error.message}`,\n    );\n    throw new Error(data.error.message);\n  }\n\n  return {\n    region,\n    ...data.data,\n  };\n}\n\n// ============================================================================\n// Redis Caching\n// ============================================================================\n\nexport async function storeBaseCheckerData({\n  url,\n  method,\n  id,\n}: {\n  url: string;\n  method: Method;\n  id: string;\n}) {\n  const redis = Redis.fromEnv();\n  const timestamp = new Date().getTime();\n  const cache = { url, method, timestamp };\n\n  const parsed = cachedCheckerSchema\n    .pick({ url: true, method: true, timestamp: true })\n    .safeParse(cache);\n\n  if (!parsed.success) {\n    throw new Error(parsed.error.message);\n  }\n\n  await redis.hset(`check:base:${id}`, parsed.data);\n  const expire = 60 * 60 * 24 * 7; // 7days\n  await redis.expire(`check:base:${id}`, expire);\n}\n\nexport async function storeCheckerData({\n  check,\n  id,\n}: {\n  check: RegionChecker;\n  id: string;\n}) {\n  const parsed = cachedCheckerSchema\n    .pick({ checks: true })\n    .safeParse({ checks: [check] });\n\n  if (!parsed.success) {\n    throw new Error(parsed.error.message);\n  }\n\n  const first = parsed.data.checks?.[0];\n\n  const redis = Redis.fromEnv();\n  if (first) await redis.sadd(`check:data:${id}`, first);\n\n  return id;\n}\n\nexport async function getCheckerDataById(id: string) {\n  const redis = Redis.fromEnv();\n  const pipe = redis.pipeline();\n  pipe.hgetall(`check:base:${id}`);\n  pipe.smembers(`check:data:${id}`);\n\n  const res =\n    await pipe.exec<\n      [{ url: string; method: Method; time: number }, RegionChecker]\n    >();\n\n  if (!res) {\n    return null;\n  }\n\n  const parsed = cachedCheckerSchema.safeParse({ ...res[0], checks: res[1] });\n\n  if (!parsed.success) {\n    // throw new Error(parsed.error.message);\n    return null;\n  }\n\n  return parsed.data;\n}\n\n// ============================================================================\n// Validation Utilities\n// ============================================================================\n\n/**\n * Simple function to validate crypto.randomUUID() format like \"aec4e0ec3c4f4557b8ce46e55078fc95\"\n * @param uuid\n * @returns\n */\nexport function is32CharHex(uuid: string) {\n  const hexRegex = /^[0-9a-fA-F]{32}$/;\n  return hexRegex.test(uuid);\n}\n"
  },
  {
    "path": "apps/web/src/lib/domains.ts",
    "content": "export const getSubdomain = (name: string, apexName: string) => {\n  if (name === apexName) return null;\n  return name.slice(0, name.length - apexName.length - 1);\n};\n\nexport const getApexDomain = (url: string) => {\n  let domain: string;\n  try {\n    domain = new URL(url).hostname;\n  } catch (_e) {\n    return \"\";\n  }\n  const parts = domain.split(\".\");\n  if (parts.length > 2) {\n    // if it's a subdomain (e.g. dub.vercel.app), return the last 2 parts\n    return parts.slice(-2).join(\".\");\n  }\n  // if it's a normal domain (e.g. dub.sh), we return the domain\n  return domain;\n};\n"
  },
  {
    "path": "apps/web/src/lib/github.ts",
    "content": "import * as z from \"zod\";\n\nconst schema = z.object({\n  stargazers_count: z.number(),\n});\n\nexport async function getGitHubStars() {\n  const res = await fetch(\n    \"https://api.github.com/repos/openstatusHQ/openstatus\",\n    { next: { revalidate: 600 } }, // 10min\n  );\n  const json = await res.json();\n  const github = schema.safeParse(json);\n\n  if (!github.success) return 0;\n  return github.data.stargazers_count;\n}\n"
  },
  {
    "path": "apps/web/src/lib/image-dimensions.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\ninterface ImageDimensions {\n  width: number;\n  height: number;\n}\n\n/**\n * Get image dimensions from PNG file\n * PNG spec: width and height are stored at bytes 16-23 as 4-byte integers\n */\nfunction getPngDimensions(buffer: Buffer): ImageDimensions {\n  const width = buffer.readUInt32BE(16);\n  const height = buffer.readUInt32BE(20);\n  return { width, height };\n}\n\n/**\n * Get image dimensions from JPEG file\n */\nfunction getJpegDimensions(buffer: Buffer): ImageDimensions {\n  let offset = 2; // Skip SOI marker\n\n  while (offset < buffer.length) {\n    // Check for marker\n    if (buffer[offset] !== 0xff) break;\n\n    const marker = buffer[offset + 1];\n    offset += 2;\n\n    // SOF markers (Start of Frame)\n    if (\n      marker >= 0xc0 &&\n      marker <= 0xcf &&\n      marker !== 0xc4 &&\n      marker !== 0xc8 &&\n      marker !== 0xcc\n    ) {\n      const height = buffer.readUInt16BE(offset + 3);\n      const width = buffer.readUInt16BE(offset + 5);\n      return { width, height };\n    }\n\n    // Skip to next marker\n    const segmentLength = buffer.readUInt16BE(offset);\n    offset += segmentLength;\n  }\n\n  throw new Error(\"Could not find JPEG dimensions\");\n}\n\n/**\n * Get dimensions for an image in the public directory\n */\nexport function getImageDimensions(publicPath: string): ImageDimensions | null {\n  try {\n    const fullPath = join(process.cwd(), \"public\", publicPath);\n    const buffer = readFileSync(fullPath);\n\n    // Check file signature\n    if (buffer[0] === 0x89 && buffer.toString(\"ascii\", 1, 4) === \"PNG\") {\n      return getPngDimensions(buffer);\n    }\n\n    if (buffer[0] === 0xff && buffer[1] === 0xd8) {\n      return getJpegDimensions(buffer);\n    }\n\n    return null;\n  } catch (error) {\n    console.warn(`Failed to get dimensions for ${publicPath}:`, error);\n    return null;\n  }\n}\n"
  },
  {
    "path": "apps/web/src/lib/maintenances/utils.ts",
    "content": "import type { Maintenance } from \"@openstatus/db/src/schema\";\n\nexport function isActiveMaintenance(maintenances?: Maintenance[]) {\n  if (!maintenances) return false;\n  return maintenances.some((maintenance) => {\n    return maintenance.from <= new Date() && maintenance.to >= new Date();\n  });\n}\n"
  },
  {
    "path": "apps/web/src/lib/metadata/shared-metadata.ts",
    "content": "import type { MDXData } from \"@/content/utils\";\nimport type { Metadata } from \"next\";\n\nexport const TITLE = \"openstatus\";\nexport const HOMEPAGE_TITLE =\n  \"openstatus - The open-source status page trusted by growing teams\";\nexport const DESCRIPTION =\n  \"Ship your status page before your SOC 2 auditor asks for it. Communicate incidents, prove compliance readiness, and monitor uptime from 28 global regions. Open source and free to start.\";\n\nexport const OG_DESCRIPTION =\n  \"The open-source status page for compliance-ready teams\";\n\nexport const BASE_URL =\n  process.env.NODE_ENV === \"production\"\n    ? \"https://www.openstatus.dev\"\n    : \"http://localhost:3000\";\n\nexport const twitterMetadata: Metadata[\"twitter\"] = {\n  title: TITLE,\n  description: DESCRIPTION,\n  card: \"summary_large_image\",\n  images: [\"/api/og\"],\n};\n\nexport const ogMetadata: Metadata[\"openGraph\"] = {\n  title: TITLE,\n  description: DESCRIPTION,\n  type: \"website\",\n  images: [\"/api/og\"],\n};\n\nexport const defaultMetadata: Metadata = {\n  title: {\n    template: `%s | ${TITLE}`,\n    default: HOMEPAGE_TITLE,\n  },\n  description: DESCRIPTION,\n  metadataBase: new URL(BASE_URL),\n  alternates: {\n    canonical: \"/\",\n  },\n  twitter: twitterMetadata,\n  openGraph: ogMetadata,\n};\n\nexport const getPageMetadata = (page: MDXData, basePath?: string): Metadata => {\n  const { slug, metadata } = page;\n  const { title, description, category, publishedAt } = metadata;\n\n  const ogImage = `${BASE_URL}/api/og?title=${encodeURIComponent(\n    title,\n  )}&description=${encodeURIComponent(\n    description,\n  )}&category=${encodeURIComponent(category)}`;\n\n  const url = basePath\n    ? `${BASE_URL}/${basePath}/${slug}`\n    : `${BASE_URL}/${slug}`;\n\n  return {\n    title,\n    description,\n    alternates: {\n      canonical: url,\n    },\n    openGraph: {\n      title,\n      description,\n      type: \"article\",\n      publishedTime: publishedAt.toISOString(),\n      url,\n      images: [\n        {\n          url: ogImage,\n        },\n      ],\n    },\n    twitter: {\n      card: \"summary_large_image\",\n      title,\n      description,\n      images: [ogImage],\n    },\n  };\n};\n"
  },
  {
    "path": "apps/web/src/lib/metadata/structured-data.ts",
    "content": "import type { MDXData } from \"@/content/utils\";\nimport { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport type {\n  BlogPosting,\n  BreadcrumbList,\n  FAQPage,\n  Graph,\n  HowTo,\n  Organization,\n  Person,\n  Product,\n  SoftwareApplication,\n  Thing,\n  WebPage,\n  WithContext,\n} from \"schema-dts\";\nimport { BASE_URL } from \"./shared-metadata\";\n\nexport const getJsonLDWebPage = (page: MDXData): WithContext<WebPage> => {\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"WebPage\",\n    name: `${page.metadata.title} | openstatus`,\n    headline: page.metadata.description,\n    mainEntityOfPage: {\n      \"@type\": \"WebPage\",\n      \"@id\": BASE_URL,\n    },\n  };\n};\n\nexport const getJsonLDBlogPosting = (\n  post: MDXData,\n  basePath: string,\n): WithContext<BlogPosting> => {\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"BlogPosting\",\n    headline: post.metadata.title,\n    datePublished: post.metadata.publishedAt.toISOString(),\n    dateModified: post.metadata.publishedAt.toISOString(),\n    description: post.metadata.description,\n    image: post.metadata.image\n      ? `${BASE_URL}${post.metadata.image}`\n      : `/api/og?title=${encodeURIComponent(\n          post.metadata.title,\n        )}&description=${encodeURIComponent(\n          post.metadata.description,\n        )}&category=${encodeURIComponent(post.metadata.category)}`,\n    url: `${BASE_URL}/${basePath}/${post.slug}`,\n    author: {\n      \"@type\": \"Person\",\n      name: post.metadata.author,\n    },\n  };\n};\n\nexport const getJsonLDOrganization = (): WithContext<Organization> => {\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Organization\",\n    name: \"openstatus\",\n    url: BASE_URL,\n    logo: `${BASE_URL}/assets/logos/OpenStatus-Logo.svg`,\n    sameAs: [\n      \"https://github.com/openstatushq\",\n      \"https://linkedin.com/company/openstatus\",\n      \"https://bsky.app/profile/openstatus.dev\",\n      \"https://x.com/openstatushq\",\n    ],\n    contactPoint: [\n      {\n        \"@type\": \"ContactPoint\",\n        contactType: \"Support\",\n        email: \"ping@openstatus.dev\",\n      },\n    ],\n  };\n};\n\nexport const getJsonLDProduct = (): WithContext<Product> => {\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Product\",\n    name: \"openstatus\",\n    description:\n      \"Open-source uptime and synthetic monitoring with status pages.\",\n    image: `${BASE_URL}/assets/logos/OpenStatus-Logo.svg`,\n    url: BASE_URL,\n    brand: {\n      \"@type\": \"Brand\",\n      name: \"openstatus\",\n      logo: `${BASE_URL}/assets/logos/OpenStatus-Logo.svg`,\n    },\n    offers: Object.entries(allPlans).map(([_, value]) => ({\n      \"@type\": \"Offer\",\n      price: value.price.USD,\n      name: value.title,\n      priceCurrency: \"USD\",\n      availability: \"https://schema.org/InStock\",\n    })),\n  };\n};\n\nexport const getJsonLDSoftwareApplication =\n  (): WithContext<SoftwareApplication> => {\n    return {\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"SoftwareApplication\",\n      name: \"openstatus\",\n      url: BASE_URL,\n      description:\n        \"Open-source uptime and synthetic monitoring with status pages.\",\n      applicationCategory: \"BusinessApplication\",\n      applicationSubCategory: \"Monitoring & Observability\",\n      operatingSystem: \"Web, Self-hosted\",\n      offers: Object.entries(allPlans).map(([_, value]) => ({\n        \"@type\": \"Offer\",\n        price: value.price.USD,\n        name: value.title,\n        priceCurrency: \"USD\",\n        availability: \"https://schema.org/InStock\",\n      })),\n      featureList: [\n        \"Multi-region uptime monitoring\",\n        \"API monitoring (REST, GraphQL)\",\n        \"Branded status pages\",\n        \"Incident notifications (Slack, PagerDuty, email)\",\n        \"Monitoring as code\",\n        \"Self-hosting option\",\n        \"Open-source\",\n      ],\n      screenshot: `${BASE_URL}/assets/landing/dashboard.png`,\n    };\n  };\n\nexport const getJsonLDPerson = (item: {\n  name: string;\n  url?: string;\n}): WithContext<Person> => {\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Person\",\n    name: item.name,\n    url: item.url,\n  };\n};\n\nexport const getJsonLDBreadcrumbList = (\n  items: { name: string; url: string }[],\n): WithContext<BreadcrumbList> => {\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"BreadcrumbList\",\n    itemListElement: items.map((item, index) => ({\n      \"@type\": \"ListItem\",\n      position: index + 1,\n      name: item.name,\n      item: item.url,\n    })),\n  };\n};\n\nexport function getJsonLDFAQPage(input: MDXData): WithContext<FAQPage> | null {\n  if (!input.metadata.faq) {\n    return null;\n  }\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"FAQPage\",\n    mainEntity: input.metadata.faq.map((faq) => ({\n      \"@type\": \"Question\",\n      name: faq.question,\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: faq.answer,\n      },\n    })),\n  };\n}\n\nexport const getJsonLDHowTo = (post: MDXData): WithContext<HowTo> | null => {\n  if (!post.metadata.howto) {\n    return null;\n  }\n\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"HowTo\",\n    name: post.metadata.title,\n    description: post.metadata.description,\n    totalTime: post.metadata.howto.totalTime,\n    step: post.metadata.howto.steps.map((step, index) => ({\n      \"@type\": \"HowToStep\",\n      position: index + 1,\n      name: step.name,\n      text: step.text,\n      image: step.image ?? undefined,\n      url: step.url ?? undefined,\n    })),\n  };\n};\n\n/**\n * Creates a unified JSON-LD graph from multiple schema objects\n * Combines multiple schemas into a single @graph structure\n *\n * @param items - Array of schema objects with @context\n * @returns A single schema object with @graph containing all items\n *\n * @example\n * const graph = createJsonLDGraph([\n *   getJsonLDWebPage(data),\n *   getJsonLDBlogPosting(data),\n *   getJsonLDFAQPage(faqs),\n * ]);\n */\nexport const createJsonLDGraph = (\n  items: (WithContext<Thing> | null)[],\n): Graph => {\n  const graphItems = items\n    .filter((item): item is WithContext<Thing> => item !== null)\n    .map(\n      (item) =>\n        Object.fromEntries(\n          Object.entries(item).filter(([key]) => key !== \"@context\"),\n        ) as Thing,\n    );\n\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@graph\": graphItems,\n  };\n};\n"
  },
  {
    "path": "apps/web/src/lib/monitor/utils.ts",
    "content": "import { endOfDay, startOfDay, startOfHour, subDays } from \"date-fns\";\n\nimport type { MonitorPeriodicity } from \"@openstatus/db/src/schema\";\n\nexport const periods = [\"1d\", \"7d\", \"14d\"] as const; // If neeeded (e.g. Pro plans), \"7d\", \"30d\"\nexport const quantiles = [\"p99\", \"p95\", \"p90\", \"p75\", \"p50\"] as const;\nexport const intervals = [\"1m\", \"10m\", \"30m\", \"1h\"] as const;\nexport const triggers = [\"cron\", \"api\"] as const;\n\nexport type Period = (typeof periods)[number];\nexport type Quantile = (typeof quantiles)[number];\nexport type Interval = (typeof intervals)[number];\nexport type Trigger = (typeof triggers)[number];\n\nexport function getDateByPeriod(period: Period) {\n  switch (period) {\n    case \"1d\":\n      return {\n        from: subDays(startOfHour(new Date()), 1),\n        to: endOfDay(new Date()),\n      };\n    case \"7d\":\n      return {\n        from: subDays(startOfDay(new Date()), 7),\n        to: endOfDay(new Date()),\n      };\n    case \"14d\":\n      return {\n        from: subDays(startOfDay(new Date()), 14),\n        to: endOfDay(new Date()),\n      };\n    default: {\n      const _exhaustiveCheck: never = period;\n      throw new Error(`Unhandled period: ${_exhaustiveCheck}`);\n    }\n  }\n}\n\nexport function getHoursByPeriod(period: Period) {\n  switch (period) {\n    case \"1d\":\n      return 24;\n    case \"7d\":\n      return 168;\n    case \"14d\":\n      return 336;\n    default: {\n      const _exhaustiveCheck: never = period;\n      throw new Error(`Unhandled period: ${_exhaustiveCheck}`);\n    }\n  }\n}\n\nexport function periodFormatter(period: Period) {\n  switch (period) {\n    case \"1d\":\n      return \"Last day\";\n    case \"7d\":\n      return \"Last 7 days\";\n    case \"14d\":\n      return \"Last 14 days\";\n    default: {\n      const _exhaustiveCheck: never = period;\n      return _exhaustiveCheck;\n    }\n  }\n}\n\nexport function getMinutesByInterval(interval: MonitorPeriodicity) {\n  switch (interval) {\n    case \"30s\":\n      // return 0.5;\n      return 1; // FIX TINYBIRD\n    case \"1m\":\n      return 1;\n    case \"5m\":\n      return 5;\n    case \"10m\":\n      return 10;\n    case \"30m\":\n      return 30;\n    case \"1h\":\n      return 60;\n    case \"other\":\n      return 60; // TODO: remove \"other\" from here\n    default: {\n      const _exhaustiveCheck: never = interval;\n      throw new Error(`Unhandled interval: ${_exhaustiveCheck}`);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/web/src/lib/preferred-settings/client.ts",
    "content": "import { useState } from \"react\";\n\nimport { COOKIE_NAME } from \"./shared\";\nimport type { PreferredSettings } from \"./validation\";\nimport { preferencesSchema } from \"./validation\";\n\nfunction getPreferredSettingsCookie() {\n  const cookie = document.cookie\n    .split(\";\")\n    .find((cookie) => cookie.trim().startsWith(`${COOKIE_NAME}=`));\n  if (!cookie) return {};\n  const settings = preferencesSchema.safeParse(\n    JSON.parse(cookie.split(\"=\")[1]),\n  );\n  if (!settings.success) return {};\n  return settings.data;\n}\n\nfunction setPreferredSettingsCookie(value: Record<string, unknown>) {\n  const month = 30 * 24 * 60 * 60 * 1000;\n  const expires = new Date(Date.now() + month).toUTCString();\n  document.cookie = `${COOKIE_NAME}=${JSON.stringify(\n    value,\n  )}; path=/; expires=${expires}`;\n}\n\n/**\n * Update user preferences and store them in a cookie accessible on the client and server\n */\nexport function usePreferredSettings(defaultValue: PreferredSettings) {\n  const [settings, setSettings] = useState<PreferredSettings>(defaultValue);\n\n  const handleChange = (value: Record<string, unknown>) => {\n    const settings = preferencesSchema.safeParse(value);\n\n    if (!settings.success) return;\n\n    const currentSettings = getPreferredSettingsCookie();\n    const newSettings = { ...currentSettings, ...settings.data };\n\n    setPreferredSettingsCookie(newSettings);\n    setSettings(newSettings);\n  };\n\n  return [settings, handleChange] as const;\n}\n"
  },
  {
    "path": "apps/web/src/lib/preferred-settings/server.ts",
    "content": "import { cookies } from \"next/headers\";\n\nimport { COOKIE_NAME } from \"./shared\";\nimport { preferencesSchema } from \"./validation\";\n\nexport async function getPreferredSettings() {\n  const cookieStore = await cookies();\n  const cookie = cookieStore.get(COOKIE_NAME);\n  const parsed = cookie ? JSON.parse(cookie.value) : {};\n  const settings = preferencesSchema.safeParse(parsed);\n  if (!settings.success) return undefined;\n  return settings.data;\n}\n\nexport type PreferredSettings = Awaited<\n  ReturnType<typeof getPreferredSettings>\n>;\n"
  },
  {
    "path": "apps/web/src/lib/preferred-settings/shared.ts",
    "content": "export const COOKIE_NAME = \"preferred-settings\";\n"
  },
  {
    "path": "apps/web/src/lib/preferred-settings/validation.ts",
    "content": "import { z } from \"zod\";\n\nexport const preferencesSchema = z\n  .object({\n    combinedRegions: z.boolean().nullable().prefault(false).optional(),\n    // ... other settings to store user preferences\n    // accessible via document.cookie in the client and cookies() on the server\n  })\n  .optional();\n\nexport type PreferredSettings = z.infer<typeof preferencesSchema>;\n"
  },
  {
    "path": "apps/web/src/lib/ratelimit.ts",
    "content": "import { redis } from \"@openstatus/upstash\";\n\ninterface RateLimitConfig {\n  window: number; // in seconds\n  limit: number; // max requests per window\n}\n\ninterface RateLimitResult {\n  success: boolean;\n  limit: number;\n  remaining: number;\n  reset: number; // timestamp when the window resets\n}\n\n/**\n * Simple fixed window rate limiter using Redis\n * @param identifier - Unique identifier for the rate limit (e.g., IP address)\n * @param config - Rate limit configuration\n * @returns Rate limit result\n */\nexport async function ratelimit(\n  identifier: string,\n  config: RateLimitConfig,\n): Promise<RateLimitResult> {\n  const key = `ratelimit:${identifier}`;\n  const now = Date.now();\n\n  // Increment the counter\n  const count = await redis.incr(key);\n\n  // If this is the first request, set the expiry\n  if (count === 1) {\n    await redis.expire(key, config.window);\n  }\n\n  // Get the TTL to calculate reset time\n  const ttl = await redis.ttl(key);\n  const reset = now + (ttl > 0 ? ttl * 1000 : config.window * 1000);\n\n  const success = count <= config.limit;\n  const remaining = Math.max(0, config.limit - count);\n\n  return {\n    success,\n    limit: config.limit,\n    remaining,\n    reset,\n  };\n}\n\n/**\n * Extract IP address from request headers\n * @param headers - Request headers\n * @returns IP address or null\n */\nexport function getClientIP(headers: Headers): string | null {\n  // Check x-real-ip first (commonly set by Vercel, Cloudflare, etc.)\n  const realIP = headers.get(\"x-real-ip\");\n  if (realIP) {\n    return realIP;\n  }\n\n  // Check x-forwarded-for (can contain multiple IPs, take the first one)\n  const forwardedFor = headers.get(\"x-forwarded-for\");\n  if (forwardedFor) {\n    return forwardedFor.split(\",\")[0].trim();\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "apps/web/src/lib/stream.ts",
    "content": "export async function* yieldMany(promises: Promise<unknown>[]) {\n  // Attach .then() handlers to the promises to remove them as soon as they resolve\n  // biome-ignore lint/complexity/noForEach: REMINDER: do not use for await (const p of promises) as it will not work as expected\n  promises.forEach((p) => {\n    p.then((value) => {\n      promises.splice(promises.indexOf(p), 1);\n      return value;\n    });\n  });\n\n  // Continue yielding the results of the promises as they resolve\n  while (promises.length > 0) {\n    yield await Promise.race(promises);\n  }\n\n  return \"done\";\n}\n\nexport function iteratorToStream(iterator: AsyncGenerator) {\n  return new ReadableStream({\n    async pull(controller) {\n      try {\n        const { value, done } = await iterator.next();\n        if (done) {\n          controller.close();\n        } else {\n          controller.enqueue(value);\n        }\n      } catch (err) {\n        console.error(\"Stream error:\", err);\n        controller.error(err);\n      }\n    },\n  });\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/**\n * HOW TO USE IT IN YOUR ROUTE\n * @returns {Response}\n */\nexport async function POST(request: Request) {\n  // extract your params from the request\n  const _json = await request.json();\n\n  const generator = yieldMany([\n    new Promise((resolve) =>\n      setTimeout(() => resolve(encoder.encode(\"1\")), 200),\n    ),\n    new Promise((resolve) => resolve(encoder.encode(\"2\"))),\n    new Promise((resolve) =>\n      setTimeout(() => resolve(encoder.encode(\"3\")), 500),\n    ),\n  ]);\n\n  const stream = iteratorToStream(generator);\n  return new Response(stream);\n}\n\n/**\n * HOW TO USE IT IN YOUR CLIENT\n */\nasync function clientConsumeStream() {\n  const response = await POST(new Request(\"\")); // fetch(\"/api/path/to/route\", { method: \"POST\" });\n  const reader = response.body?.getReader();\n  if (!reader) return;\n\n  while (true) {\n    const { value, done } = await reader.read();\n    if (done) break;\n    console.log(\"Stream output:\", decoder.decode(value));\n  }\n\n  console.log(\"Stream processing complete.\");\n}\n"
  },
  {
    "path": "apps/web/src/lib/stripe/client.ts",
    "content": "import type { Stripe as StripeProps } from \"@stripe/stripe-js\";\nimport { loadStripe } from \"@stripe/stripe-js\";\n\nimport { env } from \"@/env\";\n\nlet stripePromise: Promise<StripeProps | null>;\n\nexport const getStripe = () => {\n  if (!stripePromise) {\n    stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);\n  }\n\n  return stripePromise;\n};\n"
  },
  {
    "path": "apps/web/src/lib/tb.ts",
    "content": "import { OSTinybird } from \"@openstatus/tinybird\";\n\nimport { env } from \"@/env\";\n\nconst tb = new OSTinybird(env.TINY_BIRD_API_KEY);\n\n// REMINDER: we could extend the limits (WorkspacePlan) by\n// knowing which plan the user is on and disable some periods\nconst periods = [\"1d\", \"7d\", \"14d\"] as const;\nconst types = [\"http\", \"tcp\"] as const;\n\n// FIXME: check we we can also use Period from elswhere\ntype Period = (typeof periods)[number];\n// FIMXE: use JobType instead!\ntype Type = (typeof types)[number];\n\n// REMINDER: extend if needed\nexport function prepareListByPeriod(period: Period, type: Type = \"http\") {\n  switch (period) {\n    case \"1d\": {\n      const getData = {\n        http: tb.legacy_httpListDaily,\n        tcp: tb.legacy_tcpListDaily,\n      } as const;\n      return { getData: getData[type] };\n    }\n    case \"7d\": {\n      const getData = {\n        http: tb.legacy_httpListWeekly,\n        tcp: tb.legacy_tcpListWeekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    case \"14d\": {\n      const getData = {\n        http: tb.legacy_httpListBiweekly,\n        tcp: tb.legacy_tcpListBiweekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    default: {\n      const getData = {\n        http: tb.legacy_httpListDaily,\n        tcp: tb.legacy_tcpListDaily,\n      } as const;\n      return { getData: getData[type] };\n    }\n  }\n}\n\nexport function prepareMetricsByPeriod(period: Period, type: Type = \"http\") {\n  switch (period) {\n    case \"1d\": {\n      const getData = {\n        http: tb.legacy_httpMetricsDaily,\n        tcp: tb.legacy_tcpMetricsDaily,\n      } as const;\n      return { getData: getData[type] };\n    }\n    case \"7d\": {\n      const getData = {\n        http: tb.legacy_httpMetricsWeekly,\n        tcp: tb.legacy_tcpMetricsWeekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    case \"14d\": {\n      const getData = {\n        http: tb.legacy_httpMetricsBiweekly,\n        tcp: tb.legacy_tcpMetricsBiweekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    default: {\n      const getData = {\n        http: tb.legacy_httpMetricsDaily,\n        tcp: tb.legacy_tcpMetricsDaily,\n      } as const;\n      return { getData: getData[type] };\n    }\n  }\n}\n\nexport function prepareMetricByRegionByPeriod(\n  period: Period,\n  type: Type = \"http\",\n) {\n  switch (period) {\n    case \"1d\": {\n      const getData = {\n        http: tb.httpMetricsByRegionDaily,\n        tcp: tb.tcpMetricsByRegionDaily,\n      } as const;\n      return { getData: getData[type] };\n    }\n    case \"7d\": {\n      const getData = {\n        http: tb.httpMetricsByRegionWeekly,\n        tcp: tb.tcpMetricsByRegionWeekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    case \"14d\": {\n      const getData = {\n        http: tb.httpMetricsByRegionBiweekly,\n        tcp: tb.tcpMetricsByRegionBiweekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    default: {\n      const getData = {\n        http: tb.httpMetricsByRegionDaily,\n        tcp: tb.tcpMetricsByRegionDaily,\n      } as const;\n      return { getData: getData[type] };\n    }\n  }\n}\n\nexport function prepareMetricByIntervalByPeriod(\n  period: Period,\n  type: Type = \"http\",\n) {\n  switch (period) {\n    case \"1d\": {\n      const getData = {\n        http: tb.httpMetricsByIntervalDaily,\n        tcp: tb.tcpMetricsByIntervalDaily,\n      } as const;\n      return { getData: getData[type] };\n    }\n    case \"7d\": {\n      const getData = {\n        http: tb.httpMetricsByIntervalWeekly,\n        tcp: tb.tcpMetricsByIntervalWeekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    case \"14d\": {\n      const getData = {\n        http: tb.httpMetricsByIntervalBiweekly,\n        tcp: tb.tcpMetricsByIntervalBiweekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    default: {\n      const getData = {\n        http: tb.httpMetricsByIntervalDaily,\n        tcp: tb.tcpMetricsByIntervalDaily,\n      } as const;\n      return { getData: getData[type] };\n    }\n  }\n}\n\nexport function prepareStatusByPeriod(\n  period: \"7d\" | \"45d\",\n  type: Type = \"http\",\n) {\n  switch (period) {\n    case \"7d\": {\n      const getData = {\n        http: tb.httpStatusWeekly,\n        tcp: tb.tcpStatusWeekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    case \"45d\": {\n      const getData = {\n        http: tb.legacy_httpStatus45d,\n        tcp: tb.legacy_tcpStatus45d,\n      } as const;\n      return { getData: getData[type] };\n    }\n    default: {\n      const getData = {\n        http: tb.httpStatusWeekly,\n        tcp: tb.tcpStatusWeekly,\n      } as const;\n      return { getData: getData[type] };\n    }\n  }\n}\n\nexport function prepareGetByPeriod(period: \"30d\", type: Type = \"http\") {\n  switch (period) {\n    case \"30d\": {\n      const getData = {\n        http: tb.httpGetMonthly,\n        tcp: tb.tcpGetMonthly,\n      } as const;\n      return { getData: getData[type] };\n    }\n    default: {\n      const getData = {\n        http: tb.httpGetMonthly,\n        tcp: tb.tcpGetMonthly,\n      } as const;\n      return { getData: getData[type] };\n    }\n  }\n}\n\n// FOR MIGRATION\nexport type ResponseTimeMetrics = Awaited<\n  ReturnType<OSTinybird[\"legacy_httpMetricsDaily\"]>\n>[\"data\"][number];\n\nexport type ResponseTimeMetricsByRegion = Awaited<\n  ReturnType<OSTinybird[\"httpMetricsByRegionDaily\"]>\n>[\"data\"][number];\n\nexport type ResponseGraph = Awaited<\n  ReturnType<OSTinybird[\"httpMetricsByIntervalDaily\"]>\n>[\"data\"][number];\n\nexport type ResponseStatusTracker = Awaited<\n  ReturnType<OSTinybird[\"httpStatusWeekly\"]>\n>[\"data\"][number];\n"
  },
  {
    "path": "apps/web/src/lib/timezone.ts",
    "content": "import { format, getTimezoneOffset, utcToZonedTime } from \"date-fns-tz\";\nimport { headers } from \"next/headers\";\n\nexport async function getRequestHeaderTimezone() {\n  const headersList = await headers();\n\n  /**\n   * Vercel includes ip timezone to request header\n   */\n  const requestTimezone = headersList.get(\"x-vercel-ip-timezone\");\n\n  return requestTimezone;\n}\n\nexport async function convertTimezoneToGMT(defaultTimezone?: string) {\n  const requestTimezone = await getRequestHeaderTimezone();\n\n  /**\n   * Server region timezone as fallback\n   */\n  const clientTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\n  const timezone = defaultTimezone || requestTimezone || clientTimezone;\n\n  if (!supportedTimezones.includes(timezone)) return \"Etc/UTC\";\n\n  const msOffset = getTimezoneOffset(timezone);\n\n  if (Number.isNaN(msOffset)) return \"Etc/UTC\";\n\n  const hrOffset = Math.round(msOffset / (1000 * 60 * 60)); // avoid weird 30min timezones\n  const offset = hrOffset >= 0 ? `-${hrOffset}` : `+${Math.abs(hrOffset)}`;\n\n  return `Etc/GMT${offset}` as const;\n}\n\nexport function getServerTimezoneUTCFormat() {\n  const now = new Date();\n  const now_utc = new Date(now.toUTCString().slice(0, -4)); // remove the GMT end\n\n  return format(now_utc, \"LLL dd, y HH:mm:ss (z)\", { timeZone: \"UTC\" });\n}\n\nexport function getServerTimezoneFormat() {\n  return format(new Date(), \"LLL dd, y HH:mm:ss (z)\");\n}\n\nexport function formatDate(date: Date) {\n  return format(date, \"LLL dd, y\", { timeZone: \"UTC\" });\n}\n\nexport function formatDateTime(date: Date) {\n  return format(date, \"LLL dd, y HH:mm:ss zzz\", { timeZone: \"UTC\" });\n}\n\nexport function formatTime(date: Date) {\n  return format(date, \"HH:mm:ss zzz\", { timeZone: \"UTC\" });\n}\n\n/**\n * All supported browser / node timezones\n */\nexport const supportedTimezones = Intl.supportedValuesOf(\"timeZone\");\n\nexport async function getClosestTimezone(defaultTimezone?: string) {\n  const requestTimezone = await getRequestHeaderTimezone();\n\n  /**\n   * Server region timezone as fallback\n   */\n  const clientTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\n  const timezone = defaultTimezone || requestTimezone || clientTimezone;\n\n  if (!supportedTimezones.includes(timezone)) return \"CET\";\n\n  const now = new Date();\n\n  const estTime = utcToZonedTime(now, \"America/New_York\");\n  const pstTime = utcToZonedTime(now, \"America/Los_Angeles\");\n  const cetTime = utcToZonedTime(now, \"Europe/Paris\");\n  const utcTime = utcToZonedTime(now, \"UTC\");\n\n  const timeDifferences = {\n    EST: Math.abs(now.getTime() - estTime.getTime()),\n    PST: Math.abs(now.getTime() - pstTime.getTime()),\n    CET: Math.abs(now.getTime() - cetTime.getTime()),\n    UTC: Math.abs(now.getTime() - utcTime.getTime()),\n  };\n\n  const closestTimezone = Object.keys(timeDifferences).reduce(\n    (prev, curr) => {\n      if (\n        timeDifferences[curr as keyof typeof timeDifferences] <\n        prev.minDifference\n      ) {\n        return {\n          timezone: curr,\n          minDifference: timeDifferences[curr as keyof typeof timeDifferences],\n        };\n      }\n      return prev;\n    },\n    { timezone: \"UTC\", minDifference: Number.POSITIVE_INFINITY },\n  );\n\n  return closestTimezone.timezone as keyof typeof timeDifferences;\n}\n"
  },
  {
    "path": "apps/web/src/lib/toast.tsx",
    "content": "import type { ExternalToast } from \"sonner\";\nimport { toast } from \"sonner\";\n\ntype ToastType =\n  | \"default\"\n  | \"description\"\n  | \"success\"\n  | \"warning\"\n  | \"info\"\n  | \"error\"\n  | \"promise\";\n\nconst config = {\n  error: {\n    type: \"error\",\n    title: \"Something went wrong\",\n    description: \"Please try again\",\n    action: {\n      label: \"Discord\",\n      onClick: () => {\n        if (typeof window === \"undefined\") return;\n        window.open(\"/discord\", \"_blank\");\n      },\n    },\n  },\n  \"unique-slug\": {\n    type: \"warning\",\n    title: \"Slug is already taken\",\n    description: \"Please select another slug. Every slug is unique.\",\n  },\n  success: { type: \"success\", title: \"Success\" },\n  deleted: { type: \"success\", title: \"Deleted successfully\" }, // TODO: we are not informing the user besides the visual changes when an entry has been deleted\n  removed: { type: \"success\", title: \"Removed successfully\" },\n  saved: { type: \"success\", title: \"Saved successfully\" },\n  \"test-error\": {\n    type: \"error\",\n    title: \"Connection Failed\",\n    description: \"Please enter a correct URL\",\n  },\n  \"test-warning-empty-url\": {\n    type: \"warning\",\n    title: \"URL is Empty\",\n    description: \"Please enter a valid, non-empty URL\",\n  },\n  \"test-success\": {\n    type: \"success\",\n    title: \"Connection Established\",\n  },\n} as const;\n\nconst _config: Record<\n  string,\n  Pick<ExternalToast, \"action\" | \"description\"> & {\n    type: ToastType;\n    title: string;\n  }\n> = config;\n\ntype ToastAction = keyof typeof config;\n\nexport function toastAction(action: ToastAction) {\n  const { title, type, ...props } = _config[action];\n\n  if (type === \"default\") return toast(title, props);\n  if (type === \"success\") return toast.success(title, props);\n  if (type === \"error\") return toast.error(title, props);\n  if (type === \"warning\") return toast.warning(title, props);\n  if (type === \"description\") return toast.message(title, props);\n  if (type === \"info\") return toast.info(title, props);\n}\n\nexport { toast };\n"
  },
  {
    "path": "apps/web/src/lib/utils.ts",
    "content": "import type { ClassValue } from \"clsx\";\nimport { clsx } from \"clsx\";\nimport { addDays, format } from \"date-fns\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport function wait(ms: number) {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport function formatDate(date: Date) {\n  return format(date, \"LLL dd, y\");\n}\n\nexport function formatDateTime(date: Date) {\n  return format(date, \"LLL dd, y HH:mm:ss\");\n}\n\nexport function formatTime(date: Date) {\n  return format(date, \"HH:mm:ss\");\n}\n\nexport function formatDuration(ms: number) {\n  let v = ms;\n  if (ms < 0) v = -ms;\n  const time = {\n    day: Math.floor(v / 86400000),\n    hour: Math.floor(v / 3600000) % 24,\n    min: Math.floor(v / 60000) % 60,\n    sec: Math.floor(v / 1000) % 60,\n    ms: Math.floor(v) % 1000,\n  };\n  return Object.entries(time)\n    .filter((val) => val[1] !== 0)\n    .map(([key, val]) => `${val} ${key}${val !== 1 && key !== \"ms\" ? \"s\" : \"\"}`)\n    .join(\", \");\n}\n\nexport function notEmpty<TValue>(\n  value: TValue | null | undefined,\n): value is TValue {\n  return value !== null && value !== undefined;\n}\n\nexport const slugify = (str: string) => {\n  return str\n    .toLowerCase()\n    .replace(/ /g, \"-\")\n    .replace(/[^\\w-]+/g, \"\");\n};\n\nexport async function copyToClipboard(value: string) {\n  await navigator.clipboard.writeText(value);\n}\n\nexport function numberFormatter(value: number) {\n  const formatter = Intl.NumberFormat(\"en\", { notation: \"compact\" });\n  return formatter.format(value);\n}\n\n/**\n * Whenever you select a date, it will use the midnight timestamp of that date.\n * We need to add a day minus one second to include the whole day.\n */\nexport function manipulateDate(\n  date?: {\n    from: Date | undefined;\n    to?: Date | undefined;\n  } | null,\n) {\n  const isToDateMidnight = String(date?.to?.getTime()).endsWith(\"00000\");\n\n  // We might wanna use `endOfDay(new Date(date.to))` here\n  const addOneDayToDate = date?.to\n    ? addDays(new Date(date.to), 1).getTime() - 1\n    : null;\n\n  return {\n    fromDate: date?.from?.getTime() || null,\n    toDate: isToDateMidnight ? addOneDayToDate : date?.to?.getTime() || null,\n  };\n}\n\nexport function toCapitalize(inputString: string) {\n  const words = inputString.split(/[\\s_]+/); // Split the input string by spaces or underscores\n\n  // Capitalize the first letter of each word\n  return words\n    .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n    .join(\"\");\n}\n"
  },
  {
    "path": "apps/web/src/react-table.d.ts",
    "content": "import \"@tanstack/react-table\";\n\ndeclare module \"@tanstack/react-table\" {\n  interface ColumnMeta {\n    headerClassName?: string;\n    cellClassName?: string;\n  }\n}\n"
  },
  {
    "path": "apps/web/src/styles/globals.css",
    "content": "@import 'tailwindcss';\n@import '@openstatus/ui/globals';\n\n@plugin 'tailwindcss-animate';\n@plugin '@tailwindcss/container-queries';\n\n@source '../../../../packages/ui/**/*.{ts,tsx}';\n@source '../../node_modules/@openstatus/react/**/*.{js,ts,jsx,tsx}';\n\n@theme {\n  --color-sh-class: var(--sh-class);\n  --color-sh-identifier: var(--sh-identifier);\n  --color-sh-sign: var(--sh-sign);\n  --color-sh-string: var(--sh-string);\n  --color-sh-keyword: var(--sh-keyword);\n  --color-sh-comment: var(--sh-comment);\n  --color-sh-jsxliterals: var(--sh-jsxliterals);\n  --color-sh-property: var(--sh-property);\n  --color-sh-entity: var(--sh-entity);\n\n  --font-sans:\n    var(--font-sans), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',\n    'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\n  --font-cal: var(--font-calsans);\n\n  --animate-accordion-down: accordion-down 0.2s ease-out;\n  --animate-accordion-up: accordion-up 0.2s ease-out;\n\n  @keyframes accordion-down {\n    from {\n      height: 0;\n    }\n    to {\n      height: var(--radix-accordion-content-height);\n    }\n  }\n  @keyframes accordion-up {\n    from {\n      height: var(--radix-accordion-content-height);\n    }\n    to {\n      height: 0;\n    }\n  }\n}\n\n@utility container {\n  margin-inline: auto;\n  padding-inline: 2rem;\n  @media (width >= --theme(--breakpoint-sm)) {\n    max-width: none;\n  }\n  @media (width >= 1400px) {\n    max-width: 1400px;\n  }\n}\n\n/*\n  The default border color has changed to `currentcolor` in Tailwind CSS v4,\n  so we've added these compatibility styles to make sure everything still\n  looks the same as it did with Tailwind CSS v3.\n\n  If we ever want to remove these styles, we need to add an explicit border\n  color utility to any element that depends on these defaults.\n*/\n@layer base {\n  *,\n  ::after,\n  ::before,\n  ::backdrop,\n  ::file-selector-button {\n    border-color: var(--color-gray-200, currentcolor);\n  }\n}\n\n@layer base {\n  :root {\n    --sh-class: #2d5e9d;\n    --sh-identifier: #354150;\n    --sh-sign: #8996a3;\n    --sh-string: #007f7a;\n    --sh-keyword: #e02518;\n    --sh-comment: #a19595;\n    --sh-jsxliterals: #6266d1;\n    --sh-property: #e25a1c;\n    --sh-entity: #e25a1c;\n  }\n\n  .dark {\n    --sh-class: #4c97f8;\n    --sh-identifier: white;\n    --sh-keyword: #f47067;\n    --sh-string: #0fa295;\n  }\n\n  .dark [data-theme='dark'] {\n    display: block;\n  }\n\n  [data-theme='dark'] {\n    display: none;\n  }\n\n  [data-theme='light'] {\n    display: block;\n  }\n\n  .dark [data-theme='light'] {\n    display: none;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n/* Typography */\n\n::selection {\n  background-color: var(--color-primary);\n  color: var(--color-primary-foreground);\n}\n\n.prose {\n  @apply text-foreground/80;\n}\n\n.prose .anchor {\n  @apply absolute invisible no-underline;\n\n  margin-left: -1em;\n  padding-right: 0.5em;\n  width: 80%;\n  max-width: 700px;\n  cursor: pointer;\n}\n\n.anchor:hover {\n  @apply visible;\n}\n\n.prose a {\n  @apply text-foreground underline transition-all decoration-muted-foreground/50 underline-offset-2 decoration-2 hover:decoration-muted-foreground;\n}\n\n.prose .anchor:after {\n  @apply text-muted-foreground;\n  content: '#';\n}\n\n.prose *:hover > .anchor {\n  @apply visible;\n}\n\n.prose pre {\n  @apply relative bg-muted overflow-x-auto border border-border py-2.5 px-3.5 text-sm rounded-none;\n}\n\n.prose code {\n  @apply px-1 py-0.5 bg-muted text-muted-foreground;\n}\n\n.prose pre code {\n  @apply p-0;\n  border: initial;\n  line-height: 1.5;\n}\n\n.prose code span {\n  @apply font-medium;\n}\n\n.prose img {\n  /* Don't apply styles to next/image */\n  @apply m-0;\n}\n\n.prose p {\n  @apply my-4 leading-7;\n}\n\n.prose h1 {\n  @apply text-balance text-3xl text-foreground font-medium tracking-tight my-6 relative;\n}\n\n.prose h2 {\n  @apply text-2xl text-foreground font-medium tracking-tight my-6 relative;\n}\n\n.prose h3 {\n  @apply text-xl text-foreground font-medium tracking-tight my-6 relative;\n}\n\n.prose h4 {\n  @apply text-lg text-foreground font-medium tracking-tight my-6 relative;\n}\n\n.prose hr {\n  @apply my-6\n}\n\n.prose strong {\n  @apply text-foreground font-semibold;\n}\n\n.prose blockquote {\n  @apply border-l-2 border-foreground/50 pl-4;\n}\n\n.prose blockquote p::before {\n  content: '\"';\n  @apply text-muted-foreground;\n}\n\n.prose blockquote p::after {\n  content: '\"';\n  @apply text-muted-foreground;\n}\n\n.prose figure {\n  @apply my-4;\n}\n\n.prose figure img {\n  @apply aspect-video object-cover border border-border;\n}\n\n.prose figure figcaption {\n  @apply text-sm text-muted-foreground not-empty:mt-1;\n}\n\n.prose em {\n  @apply text-foreground italic;\n}\n\n.prose ul {\n  list-style-type: \"- \";\n  @apply list-inside marker:text-muted-foreground;\n}\n\n.prose ol {\n  @apply list-inside list-decimal marker:text-muted-foreground;\n}\n\n.prose li {\n  @apply leading-7;\n}\n\n.prose details {\n  @apply border border-border p-4 my-4;\n}\n\n.prose details p:last-child {\n  @apply mb-0;\n}\n\n.prose details summary {\n  @apply cursor-pointer font-medium text-foreground select-none;\n}\n\n.prose details summary:hover {\n  @apply bg-muted;\n}\n\n.prose details summary::marker {\n  @apply mr-1;\n}\n\n.prose details[open] summary {\n  @apply mb-2;\n}\n\n.prose form {\n  @apply my-4;\n}\n\n.prose form label {\n  @apply text-foreground font-medium;\n}\n\n/* Wrapper for scrollable tables */\n.prose .table-wrapper {\n  @apply w-full overflow-x-auto my-4;\n}\n\n.prose table {\n  @apply w-full border-collapse border border-border text-foreground;\n}\n\n.prose table th {\n  @apply font-medium bg-muted/50 p-4 text-left border border-border;\n}\n\n.prose table td {\n  @apply font-normal p-4 border border-border;\n}\n\n.prose table caption {\n  @apply text-muted-foreground text-sm caption-bottom mt-4;\n}\n\npre::-webkit-scrollbar {\n  display: none;\n}\n\npre {\n  -ms-overflow-style: none; /* IE and Edge */\n  scrollbar-width: none; /* Firefox */\n}\n\n/* Remove Safari input shadow on mobile */\ninput[type='text'],\ninput[type='email'] {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n}\n\n.title {\n  text-wrap: balance;\n}"
  },
  {
    "path": "apps/web/src/trpc/client.ts",
    "content": "import { createTRPCClient, loggerLink } from \"@trpc/client\";\n\nimport type { AppRouter } from \"@openstatus/api\";\n\nimport { endingLink } from \"./shared\";\n\nexport const api = createTRPCClient<AppRouter>({\n  links: [\n    loggerLink({\n      enabled: (opts) =>\n        process.env.NODE_ENV === \"development\" ||\n        (opts.direction === \"down\" && opts.result instanceof Error),\n    }),\n    endingLink({\n      headers: {\n        \"x-trpc-source\": \"client\",\n      },\n    }),\n  ],\n});\n\nexport { type RouterInputs, type RouterOutputs } from \"@openstatus/api\";\n"
  },
  {
    "path": "apps/web/src/trpc/query-client.ts",
    "content": "import {\n  QueryClient,\n  defaultShouldDehydrateQuery,\n} from \"@tanstack/react-query\";\nimport superjson from \"superjson\";\n\nexport function makeQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        staleTime: 30 * 1000,\n      },\n      dehydrate: {\n        serializeData: superjson.serialize,\n        shouldDehydrateQuery: (query) =>\n          defaultShouldDehydrateQuery(query) ||\n          query.state.status === \"pending\",\n      },\n      hydrate: {\n        deserializeData: superjson.deserialize,\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "apps/web/src/trpc/rq-client.tsx",
    "content": "\"use client\";\n\nimport type { AppRouter } from \"@openstatus/api\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\nimport { createTRPCReact } from \"@trpc/react-query\";\nimport { useState } from \"react\";\nimport { makeQueryClient } from \"./query-client\";\nimport { endingLink } from \"./shared\";\n\nexport const api = createTRPCReact<AppRouter>();\nlet clientQueryClientSingleton: QueryClient;\nfunction getQueryClient() {\n  if (typeof window === \"undefined\") {\n    // Server: always make a new query client\n    return makeQueryClient();\n  }\n  // Browser: use singleton pattern to keep the same query client\n  // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>\n  return (clientQueryClientSingleton ??= makeQueryClient());\n}\n\nexport function TRPCReactQueryProvider(\n  props: Readonly<{\n    children: React.ReactNode;\n  }>,\n) {\n  // NOTE: Avoid useState when initializing the query client if you don't\n  //       have a suspense boundary between this and the code that may\n  //       suspend because React will throw away the client on the initial\n  //       render if it suspends and there is no boundary\n  const queryClient = getQueryClient();\n  const [trpcClient] = useState(() =>\n    api.createClient({\n      links: [\n        endingLink({\n          headers: {\n            \"x-trpc-source\": \"client\",\n          },\n        }),\n      ],\n    }),\n  );\n  return (\n    <api.Provider client={trpcClient} queryClient={queryClient}>\n      <QueryClientProvider client={queryClient}>\n        {props.children}\n        <ReactQueryDevtools initialIsOpen={false} />\n      </QueryClientProvider>\n    </api.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/web/src/trpc/rq-server.ts",
    "content": "import \"server-only\";\n\nimport { type AppRouter, appRouter, t } from \"@openstatus/api\";\nimport type { Context } from \"@openstatus/api/src/trpc\";\nimport { db } from \"@openstatus/db\";\nimport { createHydrationHelpers } from \"@trpc/react-query/rsc\";\nimport { cache } from \"react\";\nimport { makeQueryClient } from \"./query-client\";\n\nconst createContextCached = cache(\n  async (..._args: unknown[]): Promise<Context> => {\n    return {\n      req: undefined,\n      db,\n      session: null,\n    };\n  },\n);\n\n// IMPORTANT: Create a stable getter for the query client that\n//            will return the same client during the same request.\nexport const getQueryClient = cache(makeQueryClient);\nconst caller = t.createCallerFactory(appRouter)(createContextCached);\nexport const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(\n  caller,\n  getQueryClient,\n);\n"
  },
  {
    "path": "apps/web/src/trpc/server.ts",
    "content": "import { createTRPCClient, loggerLink } from \"@trpc/client\";\nimport { headers } from \"next/headers\";\n\nimport type { AppRouter } from \"@openstatus/api\";\n\nimport { endingLink } from \"./shared\";\n\nexport const api = createTRPCClient<AppRouter>({\n  links: [\n    loggerLink({\n      enabled: (opts) =>\n        process.env.NODE_ENV === \"development\" ||\n        (opts.direction === \"down\" && opts.result instanceof Error),\n    }),\n    endingLink({\n      headers: async () => {\n        const h = new Map(await headers());\n        h.delete(\"connection\");\n        h.delete(\"transfer-encoding\");\n        h.set(\"x-trpc-source\", \"server\");\n        return Object.fromEntries(h.entries());\n      },\n    }),\n  ],\n});\n\nexport { type RouterInputs, type RouterOutputs } from \"@openstatus/api\";\n"
  },
  {
    "path": "apps/web/src/trpc/shared.ts",
    "content": "import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from \"@trpc/client\";\nimport { httpBatchLink } from \"@trpc/client\";\n\nimport type { AppRouter } from \"@openstatus/api\";\nimport superjson from \"superjson\";\n\nconst getBaseUrl = () => {\n  if (typeof window !== \"undefined\") return \"\";\n  if (process.env.VERCEL_ENV === \"production\")\n    return \"https://www.openstatus.dev\";\n  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // Vercel\n  return \"http://localhost:3000\"; // Local dev\n};\n\nconst lambdas = [\"stripeRouter\", \"emailRouter\"];\n\nexport const endingLink = (opts?: {\n  headers?: HTTPHeaders | (() => HTTPHeaders | Promise<HTTPHeaders>);\n}) =>\n  ((runtime) => {\n    const sharedOpts = {\n      headers: opts?.headers, // REMINDER: fails when trying to `getTotalActiveMonitors()`\n      transformer: superjson,\n      // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any\n    } satisfies Partial<HTTPBatchLinkOptions<any>>;\n\n    const edgeLink = httpBatchLink({\n      ...sharedOpts,\n      url: `${getBaseUrl()}/api/trpc/edge`,\n    })(runtime);\n    const lambdaLink = httpBatchLink({\n      ...sharedOpts,\n      url: `${getBaseUrl()}/api/trpc/lambda`,\n    })(runtime);\n\n    return (ctx) => {\n      const path = ctx.op.path.split(\".\") as [string, ...string[]];\n      const endpoint = lambdas.includes(path[0]) ? \"lambda\" : \"edge\";\n\n      const newCtx = {\n        ...ctx,\n        op: { ...ctx.op, path: path.join(\".\") },\n      };\n      return endpoint === \"edge\" ? edgeLink(newCtx) : lambdaLink(newCtx);\n    };\n  }) satisfies TRPCLink<AppRouter>;\n"
  },
  {
    "path": "apps/web/src/types/utils.ts",
    "content": "export type Writeable<T> = { -readonly [P in keyof T]: T[P] };\n"
  },
  {
    "path": "apps/web/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"compilerOptions\": {\n    \"moduleResolution\": \"bundler\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"*\": [\"./*\"]\n    },\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"strictNullChecks\": true,\n    \"strict\": true\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \"**/*.mjs\"\n  ],\n  \"exclude\": [\"node_modules\", \"env.ts\"]\n}\n"
  },
  {
    "path": "apps/web/turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"extends\": [\"//\"],\n  \"tasks\": {\n    \"build\": {\n      \"dependsOn\": [\"@openstatus/react#build\"]\n    }\n  }\n}\n"
  },
  {
    "path": "apps/web/vercel.json",
    "content": "{\n  \"crons\": [\n    {\n      \"path\": \"/api/internal/email\",\n      \"schedule\": \"13 17 * * *\"\n    },\n    {\n      \"path\": \"/api/internal/email/team-invite\",\n      \"schedule\": \"12 14 * * *\"\n    },\n    {\n      \"path\": \"/api/internal/email/feedback\",\n      \"schedule\": \"30 15 * * *\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/workflows/.dockerignore",
    "content": "# This file is generated by Dofigen v2.8.0\n# See https://github.com/lenra-io/dofigen\n\nnode_modules\n/apps/docs\n/apps/screenshot-service\n/apps/server\n/apps/web\n/apps/dashboard\n/apps/status-page\n/packages/analytics\n/packages/api\n/packages/error\n/packages/tracker\n"
  },
  {
    "path": "apps/workflows/.gitignore",
    "content": "# deps\nnode_modules/\n"
  },
  {
    "path": "apps/workflows/Dockerfile",
    "content": "# syntax=docker/dockerfile:1.19.0\n# This file is generated by Dofigen v2.8.0\n# See https://github.com/lenra-io/dofigen\n\n# docker\nFROM oven/bun@sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a AS docker\nLABEL \\\n    org.opencontainers.image.base.digest=\"sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a\" \\\n    org.opencontainers.image.base.name=\"docker.io/oven/bun:1.3.6\"\nWORKDIR /app/apps/workflows\nCOPY \\\n    --link \\\n    \".\" \"/app/\"\nRUN bun run src/build-docker.ts\n\n# install\nFROM oven/bun@sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a AS install\nLABEL \\\n    org.opencontainers.image.base.digest=\"sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a\" \\\n    org.opencontainers.image.base.name=\"docker.io/oven/bun:1.3.6\" \\\n    org.opencontainers.image.stage=\"install\"\nWORKDIR /app/\nRUN \\\n    --mount=type=bind,target=bunfig.toml,source=bunfig.toml \\\n    --mount=type=bind,target=package.json,source=package.json \\\n    --mount=type=bind,target=apps/workflows/package.json,source=apps/workflows/package.json \\\n    --mount=type=bind,target=packages/assertions/package.json,source=packages/assertions/package.json \\\n    --mount=type=bind,target=packages/db/package.json,source=packages/db/package.json \\\n    --mount=type=bind,target=packages/emails/package.json,source=packages/emails/package.json \\\n    --mount=type=bind,target=packages/notifications/base/package.json,source=packages/notifications/base/package.json \\\n    --mount=type=bind,target=packages/notifications/discord/package.json,source=packages/notifications/discord/package.json \\\n    --mount=type=bind,target=packages/notifications/email/package.json,source=packages/notifications/email/package.json \\\n    --mount=type=bind,target=packages/notifications/grafana-oncall/package.json,source=packages/notifications/grafana-oncall/package.json \\\n    --mount=type=bind,target=packages/notifications/google-chat/package.json,source=packages/notifications/google-chat/package.json \\\n    --mount=type=bind,target=packages/notifications/ntfy/package.json,source=packages/notifications/ntfy/package.json \\\n    --mount=type=bind,target=packages/notifications/opsgenie/package.json,source=packages/notifications/opsgenie/package.json \\\n    --mount=type=bind,target=packages/notifications/pagerduty/package.json,source=packages/notifications/pagerduty/package.json \\\n    --mount=type=bind,target=packages/notifications/slack/package.json,source=packages/notifications/slack/package.json \\\n    --mount=type=bind,target=packages/notifications/telegram/package.json,source=packages/notifications/telegram/package.json \\\n    --mount=type=bind,target=packages/notifications/twillio-whatsapp/package.json,source=packages/notifications/twillio-whatsapp/package.json \\\n    --mount=type=bind,target=packages/notifications/twillio-sms/package.json,source=packages/notifications/twillio-sms/package.json \\\n    --mount=type=bind,target=packages/notifications/webhook/package.json,source=packages/notifications/webhook/package.json \\\n    --mount=type=bind,target=packages/regions/package.json,source=packages/regions/package.json \\\n    --mount=type=bind,target=packages/utils/package.json,source=packages/utils/package.json \\\n    --mount=type=bind,target=packages/tsconfig/package.json,source=packages/tsconfig/package.json \\\n    --mount=type=bind,target=packages/tinybird/package.json,source=packages/tinybird/package.json \\\n    --mount=type=bind,target=packages/upstash/package.json,source=packages/upstash/package.json \\\n    --mount=type=bind,target=packages/theme-store/package.json,source=packages/theme-store/package.json \\\n    --mount=type=cache,target=/root/.bun/install/cache,sharing=locked \\\n    bun install --production  --frozen-lockfile --verbose\n\n# build\nFROM oven/bun@sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a AS build\nLABEL \\\n    org.opencontainers.image.base.digest=\"sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a\" \\\n    org.opencontainers.image.base.name=\"docker.io/oven/bun:1.3.6\" \\\n    org.opencontainers.image.stage=\"build\"\nENV NODE_ENV=\"production\"\nWORKDIR /app/apps/workflows\nCOPY \\\n    --link \\\n    \".\" \"/app/\"\nCOPY \\\n    --from=install \\\n    --link \\\n    \"/app/node_modules\" \"/app/node_modules\"\nRUN bun build --compile --target bun  --sourcemap src/index.ts --outfile=app\n\n# libsql\nFROM oven/bun@sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a AS libsql\nLABEL \\\n    org.opencontainers.image.base.digest=\"sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a\" \\\n    org.opencontainers.image.base.name=\"docker.io/oven/bun:1.3.6\"\nWORKDIR /app/\nCOPY \\\n    --from=docker \\\n    --link \\\n    \"/app/apps/build-docker/package.json\" \"/app/package.json\"\nRUN bun install\n\n# runtime\nFROM debian@sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 AS runtime\nLABEL \\\n    io.dofigen.version=\"2.8.0\" \\\n    org.opencontainers.image.authors=\"OpenStatus Team\" \\\n    org.opencontainers.image.base.digest=\"sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734\" \\\n    org.opencontainers.image.base.name=\"docker.io/debian:bullseye-slim\" \\\n    org.opencontainers.image.description=\"Background job processing and probe scheduling for OpenStatus\" \\\n    org.opencontainers.image.source=\"https://github.com/openstatusHQ/openstatus\" \\\n    org.opencontainers.image.title=\"OpenStatus Workflows\" \\\n    org.opencontainers.image.vendor=\"OpenStatus\"\nWORKDIR /app/\nCOPY \\\n    --from=build \\\n    --chown=1000:1000 \\\n    --chmod=555 \\\n    --link \\\n    \"/app/apps/workflows/app\" \"/app/apps/workflows/\"\nCOPY \\\n    --from=libsql \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/app/node_modules\" \"/app/packages/db/node_modules\"\nCOPY \\\n    --from=libsql \\\n    --chown=1000:1000 \\\n    --link \\\n    \"/app/node_modules\" \"/app/node_modules\"\nUSER 0:0\nRUN apt-get update -q && apt-get install -y -q --no-install-recommends ca-certificates curl && update-ca-certificates && rm -rf /var/lib/apt/lists/* && mkdir -p /app/data && chown -R 1000:1000 /app/data\nUSER 1000:1000\nEXPOSE 3000\nENTRYPOINT [\"/app/apps/workflows/app\"]\n"
  },
  {
    "path": "apps/workflows/README.md",
    "content": "# Workflows\n\n## Development\n\nTo install dependencies:\n\n```sh\nbun install\n```\n\nTo run:\n\n```sh\nbun run dev\n```\n\nopen <http://localhost:3000>\n\n## Deploy\n\nFrom root\n\n```bash\nflyctl deploy --config apps/workflows/fly.toml --dockerfile  apps/workflows/Dockerfile\n```\n\n## Docker\n\nThe Dockerfile is generated thanks to [Dofigen](https://github.com/lenra-io/dofigen).\nTo generate the Dockerfile, run the following command from the `apps/workflows` directory:\n\n```bash\n# Install Dofigen\ncargo install dofigen\n# Update the dependent image versions\ndofigen update\n# Generate the Dockerfile\ndofigen gen\n```"
  },
  {
    "path": "apps/workflows/docker-compose.yaml",
    "content": "name: workflows-test\nservices:\n    workflows-test:\n        build:\n         context: ../..\n         dockerfile: apps/workflows/Dockerfile\n        environment:\n            - DATABASE_URL=http://host.docker.internal:8081\n            - DATABASE_AUTH_TOKEN=test\n            - RESEND_API_KEY=test\n            - UPSTASH_REDIS_REST_URL=test\n            - UPSTASH_REDIS_REST_TOKEN=test\n            - NODE_ENV=production\n            - GCP_PROJECT_ID=test\n        extra_hosts:\n          - \"host.docker.internal:host-gateway\"\n\n        ports:\n            - 3000:3000\n            - 8081:8081\n        volumes:\n            - type: bind\n              source: ./data\n              target: /app/data\n        image: workflows-test\n        # command: .\n"
  },
  {
    "path": "apps/workflows/dofigen.yml",
    "content": "ignore:\n  - node_modules\n  - /apps/docs\n  - /apps/screenshot-service\n  - /apps/server\n  - /apps/web\n  - /apps/dashboard\n  - /apps/status-page\n  - /packages/analytics\n  - /packages/api\n  - /packages/error\n  - /packages/tracker\nbuilders:\n  install:\n    fromImage: oven/bun:1.3.6\n    workdir: /app/\n    labels:\n      org.opencontainers.image.stage: install\n    bind:\n      - bunfig.toml\n      - package.json\n      - apps/workflows/package.json\n      - packages/assertions/package.json\n      - packages/db/package.json\n      - packages/emails/package.json\n      - packages/notifications/base/package.json\n      - packages/notifications/discord/package.json\n      - packages/notifications/email/package.json\n      - packages/notifications/grafana-oncall/package.json\n      - packages/notifications/google-chat/package.json\n      - packages/notifications/ntfy/package.json\n      - packages/notifications/opsgenie/package.json\n      - packages/notifications/pagerduty/package.json\n      - packages/notifications/slack/package.json\n      - packages/notifications/telegram/package.json\n      - packages/notifications/twillio-whatsapp/package.json\n      - packages/notifications/twillio-sms/package.json\n      - packages/notifications/webhook/package.json\n      - packages/regions/package.json\n      - packages/utils/package.json\n      - packages/tsconfig/package.json\n      - packages/tinybird/package.json\n      - packages/upstash/package.json\n      - packages/theme-store/package.json\n    # Install dependencies\n    run: bun install --production  --frozen-lockfile --verbose\n    cache:\n      - /root/.bun/install/cache\n\n  # Stage 4: Build application (compile to binary)\n  build:\n    fromImage: oven/bun:1.3.6\n    workdir: /app/apps/workflows\n    labels:\n      org.opencontainers.image.stage: build\n    copy:\n      - . /app/\n      - fromBuilder: install\n        source: /app/node_modules\n        target: /app/node_modules\n      #  Should set env to production here\n    # Compile the TypeScript application\n    env:\n      NODE_ENV: production\n    run: bun build --compile --target bun  --sourcemap src/index.ts --outfile=app\n\n  docker:\n    fromImage: oven/bun:1.3.6\n    workdir: /app/apps/workflows\n    copy:\n     - . /app/\n    run: bun run src/build-docker.ts\n\n  libsql:\n    fromImage: oven/bun:1.3.6\n    workdir: /app/\n    copy:\n      - fromBuilder: docker\n        source: /app/apps/build-docker/package.json\n        target: /app/package.json\n    run: bun install\n\nfromImage: debian:bullseye-slim\nworkdir: /app/\nroot:\n  run: apt-get update -q && apt-get install -y -q --no-install-recommends ca-certificates curl && update-ca-certificates && rm -rf /var/lib/apt/lists/* && mkdir -p /app/data && chown -R 1000:1000 /app/data\n\n# Metadata labels\nlabels:\n  org.opencontainers.image.title: OpenStatus Workflows\n  org.opencontainers.image.description: Background job processing and probe scheduling for OpenStatus\n  org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus\n  org.opencontainers.image.vendor: OpenStatus\n  org.opencontainers.image.authors: OpenStatus Team\n\n# Copy artifacts from build stages\ncopy:\n  - fromBuilder: build\n    source: /app/apps/workflows/app\n    target: /app/apps/workflows/\n    chmod: \"555\"\n  - fromBuilder: libsql\n    source: /app/node_modules\n    target: /app/packages/db/node_modules\n  - fromBuilder: libsql\n    source: /app/node_modules\n    target: /app/node_modules\nexpose: \"3000\"\n\nentrypoint: /app/apps/workflows/app\n"
  },
  {
    "path": "apps/workflows/fly.toml",
    "content": "# fly.toml app configuration file generated for openstatus-workflows on 2024-11-09T11:20:33+01:00\n#\n# See https://fly.io/docs/reference/configuration/ for information about how to use this file.\n#\n\napp = 'openstatus-workflows'\nprimary_region = 'ams'\n\n[build]\n  dockerfile = \"./Dockerfile\"\n\n[[vm]]\n  cpu_kind = \"shared\"\n  cpus = 2\n  memory_mb = 1024\n\n[http_service]\n  internal_port = 3000\n  force_https = true\n  auto_stop_machines = \"suspend\"\n  auto_start_machines = true\n  min_machines_running = 2\n  processes = [\"app\"]\n\n[http_service.concurrency]\n    type = \"requests\"\n    hard_limit = 1000\n    soft_limit = 500\n\n[deploy]\n  strategy = \"rolling\"\n\n[[http_service.checks]]\n  grace_period = \"10s\"\n  interval = \"1m\"\n  method = \"GET\"\n  timeout = \"5s\"\n  path = \"/ping\"\n\n[env]\n  NODE_ENV = \"production\"\n  PORT = \"3000\"\n\n[[mounts]]\n    source = \"libsql_data\"\n    destination = \"/app/data\"\n"
  },
  {
    "path": "apps/workflows/package.json",
    "content": "{\n  \"name\": \"@openstatus/workflows\",\n  \"scripts\": {\n    \"dev\": \"NODE_ENV=development bun run --hot src/index.ts\",\n    \"start\": \"NODE_ENV=production bun run src/index.ts\",\n    \"test\": \"bun test\"\n  },\n  \"dependencies\": {\n    \"@google-cloud/tasks\": \"4.0.1\",\n    \"@hono/sentry\": \"1.2.2\",\n    \"@libsql/client\": \"0.15.15\",\n    \"@logtape/logtape\": \"2.0.1\",\n    \"@logtape/otel\": \"2.0.1\",\n    \"@logtape/sentry\": \"2.0.1\",\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/emails\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"@openstatus/notification-discord\": \"workspace:*\",\n    \"@openstatus/notification-emails\": \"workspace:*\",\n    \"@openstatus/notification-google-chat\": \"workspace:*\",\n    \"@openstatus/notification-grafana-oncall\": \"workspace:*\",\n    \"@openstatus/notification-ntfy\": \"workspace:*\",\n    \"@openstatus/notification-opsgenie\": \"workspace:*\",\n    \"@openstatus/notification-pagerduty\": \"workspace:*\",\n    \"@openstatus/notification-slack\": \"workspace:*\",\n    \"@openstatus/notification-telegram\": \"workspace:*\",\n    \"@openstatus/notification-twillio-sms\": \"workspace:*\",\n    \"@openstatus/notification-twillio-whatsapp\": \"workspace:*\",\n    \"@openstatus/notification-webhook\": \"workspace:*\",\n    \"@openstatus/regions\": \"workspace:*\",\n    \"@openstatus/tinybird\": \"workspace:*\",\n    \"@openstatus/upstash\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:*\",\n    \"@upstash/qstash\": \"2.6.2\",\n    \"drizzle-orm\": \"0.44.4\",\n    \"effect\": \"3.19.12\",\n    \"hono\": \"4.5.3\",\n    \"limiter\": \"^3.0.0\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/bun\": \"latest\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "apps/workflows/src/build-docker.ts",
    "content": "import path from \"node:path\";\n\n// Create package.json that contains @libsql/client as dependency. It will be used to create node_modules and copy them alongside compiled server https://github.com/oven-sh/bun/issues/18909\ntype PackageJson = Record<\"name\" | \"description\" | \"version\", string> &\n  Record<\"dependencies\", Record<string, string>>;\nconst packageJson: PackageJson = await Bun.file(\n  path.join(__dirname, \"../../../packages/db\", \"package.json\"),\n).json();\n\nconst extractDependenciesNames = [\"@libsql/client\"];\nconst extractedDependencies = extractDependenciesNames.reduce(\n  (acc, cur) => {\n    if (packageJson.dependencies[cur]) {\n      acc[cur] = packageJson.dependencies[cur];\n    }\n\n    return acc;\n  },\n  {} as Record<string, string>,\n);\n\nconst packageJsonBuild = {\n  name: packageJson.name,\n  description: packageJson.description,\n  version: packageJson.version,\n  // type: \"module\",\n  dependencies: extractedDependencies,\n};\n\nawait Bun.write(\n  \"../build-docker/package.json\",\n  JSON.stringify(packageJsonBuild, null, 2),\n);\n"
  },
  {
    "path": "apps/workflows/src/checker/alerting.test.ts",
    "content": "import { afterAll, afterEach, describe, expect, mock, test } from \"bun:test\";\nimport { db, eq, inArray } from \"@openstatus/db\";\nimport {\n  notification,\n  notificationTrigger,\n  notificationsToMonitors,\n} from \"@openstatus/db/src/schema\";\n\n// Mock audit log to avoid calling Tinybird\nmock.module(\"../utils/audit-log\", () => ({\n  checkerAudit: {\n    publishAuditLog: mock(() => Promise.resolve()),\n  },\n}));\n\n// Mock notification providers to avoid real HTTP calls\nconst mockEmailSendAlert = mock(() => Promise.resolve());\nconst mockEmailSendRecovery = mock(() => Promise.resolve());\nconst mockEmailSendDegraded = mock(() => Promise.resolve());\nconst mockSlackSendAlert = mock(() => Promise.resolve());\nconst mockSlackSendRecovery = mock(() => Promise.resolve());\nconst mockSlackSendDegraded = mock(() => Promise.resolve());\nconst mockDiscordSendAlert = mock(() => Promise.resolve());\nconst mockDiscordSendRecovery = mock(() => Promise.resolve());\nconst mockDiscordSendDegraded = mock(() => Promise.resolve());\n\nmock.module(\"./utils\", () => ({\n  providerToFunction: {\n    email: {\n      sendAlert: mockEmailSendAlert,\n      sendRecovery: mockEmailSendRecovery,\n      sendDegraded: mockEmailSendDegraded,\n    },\n    discord: {\n      sendAlert: mockDiscordSendAlert,\n      sendRecovery: mockDiscordSendRecovery,\n      sendDegraded: mockDiscordSendDegraded,\n    },\n    slack: {\n      sendAlert: mockSlackSendAlert,\n      sendRecovery: mockSlackSendRecovery,\n      sendDegraded: mockSlackSendDegraded,\n    },\n    sms: { sendAlert: mock(), sendRecovery: mock(), sendDegraded: mock() },\n    webhook: { sendAlert: mock(), sendRecovery: mock(), sendDegraded: mock() },\n    telegram: { sendAlert: mock(), sendRecovery: mock(), sendDegraded: mock() },\n    pagerduty: {\n      sendAlert: mock(),\n      sendRecovery: mock(),\n      sendDegraded: mock(),\n    },\n    opsgenie: { sendAlert: mock(), sendRecovery: mock(), sendDegraded: mock() },\n    ntfy: { sendAlert: mock(), sendRecovery: mock(), sendDegraded: mock() },\n    \"google-chat\": {\n      sendAlert: mock(),\n      sendRecovery: mock(),\n      sendDegraded: mock(),\n    },\n    \"grafana-oncall\": {\n      sendAlert: mock(),\n      sendRecovery: mock(),\n      sendDegraded: mock(),\n    },\n    whatsapp: { sendAlert: mock(), sendRecovery: mock(), sendDegraded: mock() },\n  },\n}));\n\n// Import after mocks are set up\nconst { triggerNotifications } = await import(\"./alerting\");\n\n// Seed data has: monitor 1 (workspace 1) linked to notification 1 (email provider)\n// We use unique cronTimestamp per test to avoid unique constraint conflicts\n\ndescribe(\"triggerNotifications\", () => {\n  afterEach(() => {\n    mockEmailSendAlert.mockClear();\n    mockEmailSendRecovery.mockClear();\n    mockEmailSendDegraded.mockClear();\n    mockSlackSendAlert.mockClear();\n    mockSlackSendRecovery.mockClear();\n    mockSlackSendDegraded.mockClear();\n    mockDiscordSendAlert.mockClear();\n    mockDiscordSendRecovery.mockClear();\n    mockDiscordSendDegraded.mockClear();\n  });\n\n  afterAll(async () => {\n    // Clean up notification triggers created during tests\n    await db\n      .delete(notificationTrigger)\n      .where(eq(notificationTrigger.monitorId, 1))\n      .run();\n  });\n\n  test(\"should send alert notification and return triggered list\", async () => {\n    const cronTimestamp = 9000001;\n\n    const result = await triggerNotifications({\n      monitorId: \"1\",\n      statusCode: 500,\n      message: \"Internal Server Error\",\n      notifType: \"alert\",\n      cronTimestamp,\n      incidentId: undefined,\n      regions: [\"ams\"],\n      latency: 1500,\n    });\n\n    expect(mockEmailSendAlert).toHaveBeenCalledTimes(1);\n    expect(result).toHaveLength(1);\n    expect(result[0]).toEqual({\n      notificationId: 1,\n      provider: \"email\",\n    });\n  });\n\n  test(\"should send recovery notification and return triggered list\", async () => {\n    const cronTimestamp = 9000002;\n\n    const result = await triggerNotifications({\n      monitorId: \"1\",\n      statusCode: 200,\n      notifType: \"recovery\",\n      cronTimestamp,\n      regions: [\"ams\"],\n    });\n\n    expect(mockEmailSendRecovery).toHaveBeenCalledTimes(1);\n    expect(result).toHaveLength(1);\n    expect(result[0]).toEqual({\n      notificationId: 1,\n      provider: \"email\",\n    });\n  });\n\n  test(\"should send degraded notification and return triggered list\", async () => {\n    const cronTimestamp = 9000003;\n\n    const result = await triggerNotifications({\n      monitorId: \"1\",\n      statusCode: 200,\n      notifType: \"degraded\",\n      cronTimestamp,\n      latency: 5000,\n      regions: [\"ams\"],\n    });\n\n    expect(mockEmailSendDegraded).toHaveBeenCalledTimes(1);\n    expect(result).toHaveLength(1);\n    expect(result[0]).toEqual({\n      notificationId: 1,\n      provider: \"email\",\n    });\n  });\n\n  test(\"should return empty list when monitor has no notifications\", async () => {\n    const cronTimestamp = 9000004;\n\n    // Monitor 2 has no notifications linked in seed data\n    const result = await triggerNotifications({\n      monitorId: \"2\",\n      statusCode: 500,\n      notifType: \"alert\",\n      cronTimestamp,\n    });\n\n    expect(mockEmailSendAlert).not.toHaveBeenCalled();\n    expect(result).toHaveLength(0);\n  });\n\n  test(\"should skip duplicate notification trigger for same cronTimestamp\", async () => {\n    const cronTimestamp = 9000005;\n\n    const first = await triggerNotifications({\n      monitorId: \"1\",\n      statusCode: 500,\n      notifType: \"alert\",\n      cronTimestamp,\n    });\n\n    expect(first).toHaveLength(1);\n    mockEmailSendAlert.mockClear();\n\n    // Same cronTimestamp should be skipped due to unique constraint\n    const second = await triggerNotifications({\n      monitorId: \"1\",\n      statusCode: 500,\n      notifType: \"alert\",\n      cronTimestamp,\n    });\n\n    expect(mockEmailSendAlert).not.toHaveBeenCalled();\n    expect(second).toHaveLength(0);\n  });\n});\n\ndescribe(\"triggerNotifications with multiple providers\", () => {\n  const testNotificationIds: number[] = [];\n\n  // Monitor 3 (workspace 1) exists in seed but has no notifications.\n  // We link slack and discord notifications to it for this test suite.\n  const testMonitorId = 3;\n\n  afterEach(() => {\n    mockEmailSendAlert.mockClear();\n    mockSlackSendAlert.mockClear();\n    mockDiscordSendAlert.mockClear();\n    mockSlackSendRecovery.mockClear();\n    mockDiscordSendRecovery.mockClear();\n  });\n\n  afterAll(async () => {\n    // Clean up notification triggers\n    await db\n      .delete(notificationTrigger)\n      .where(eq(notificationTrigger.monitorId, testMonitorId))\n      .run();\n\n    // Clean up notification-to-monitor links\n    await db\n      .delete(notificationsToMonitors)\n      .where(eq(notificationsToMonitors.monitorId, testMonitorId))\n      .run();\n\n    // Clean up test notifications\n    if (testNotificationIds.length > 0) {\n      await db\n        .delete(notification)\n        .where(inArray(notification.id, testNotificationIds))\n        .run();\n    }\n  });\n\n  test(\"should trigger all linked providers and return each in the result\", async () => {\n    // Create slack notification\n    const [slackNotif] = await db\n      .insert(notification)\n      .values({\n        name: \"test-slack\",\n        provider: \"slack\",\n        data: '{\"slack\":\"https://hooks.slack.com/test\"}',\n        workspaceId: 1,\n      })\n      .returning();\n\n    // Create discord notification\n    const [discordNotif] = await db\n      .insert(notification)\n      .values({\n        name: \"test-discord\",\n        provider: \"discord\",\n        data: '{\"discord\":\"https://discord.com/api/webhooks/test\"}',\n        workspaceId: 1,\n      })\n      .returning();\n\n    testNotificationIds.push(slackNotif.id, discordNotif.id);\n\n    // Link both to monitor 3\n    await db\n      .insert(notificationsToMonitors)\n      .values([\n        { monitorId: testMonitorId, notificationId: slackNotif.id },\n        { monitorId: testMonitorId, notificationId: discordNotif.id },\n      ])\n      .run();\n\n    const cronTimestamp = 9100001;\n\n    const result = await triggerNotifications({\n      monitorId: String(testMonitorId),\n      statusCode: 500,\n      message: \"Server Error\",\n      notifType: \"alert\",\n      cronTimestamp,\n      regions: [\"ams\"],\n    });\n\n    expect(mockSlackSendAlert).toHaveBeenCalledTimes(1);\n    expect(mockDiscordSendAlert).toHaveBeenCalledTimes(1);\n    expect(mockEmailSendAlert).not.toHaveBeenCalled();\n\n    expect(result).toHaveLength(2);\n    expect(result).toContainEqual({\n      notificationId: slackNotif.id,\n      provider: \"slack\",\n    });\n    expect(result).toContainEqual({\n      notificationId: discordNotif.id,\n      provider: \"discord\",\n    });\n  });\n\n  test(\"should trigger recovery on all linked providers\", async () => {\n    const cronTimestamp = 9100002;\n\n    const result = await triggerNotifications({\n      monitorId: String(testMonitorId),\n      statusCode: 200,\n      notifType: \"recovery\",\n      cronTimestamp,\n      regions: [\"ams\"],\n    });\n\n    expect(mockSlackSendRecovery).toHaveBeenCalledTimes(1);\n    expect(mockDiscordSendRecovery).toHaveBeenCalledTimes(1);\n\n    expect(result).toHaveLength(2);\n    const providers = result.map((r) => r.provider).sort();\n    expect(providers).toEqual([\"discord\", \"slack\"]);\n  });\n});\n"
  },
  {
    "path": "apps/workflows/src/checker/alerting.ts",
    "content": "import { and, count, db, eq, gte, inArray, schema } from \"@openstatus/db\";\nimport type { Incident, MonitorStatus } from \"@openstatus/db/src/schema\";\nimport {\n  selectMonitorSchema,\n  selectNotificationSchema,\n  selectWorkspaceSchema,\n} from \"@openstatus/db/src/schema\";\n\nimport { getLogger } from \"@logtape/logtape\";\nimport type { Region } from \"@openstatus/db/src/schema/constants\";\nimport { Effect, Schedule } from \"effect\";\nimport { checkerAudit } from \"../utils/audit-log\";\nimport { providerToFunction } from \"./utils\";\n\nconst logger = getLogger(\"workflow\");\n\nexport const triggerNotifications = async ({\n  monitorId,\n  statusCode,\n  message,\n  notifType,\n  cronTimestamp,\n  incidentId,\n  regions,\n  latency,\n}: {\n  monitorId: string;\n  statusCode?: number;\n  message?: string;\n  notifType: \"alert\" | \"recovery\" | \"degraded\";\n  cronTimestamp: number;\n  incidentId?: number;\n  regions?: string[];\n  latency?: number;\n}): Promise<{ notificationId: number; provider: string }[]> => {\n  logger.info(\"Triggering alerting\", {\n    monitor_id: monitorId,\n    notification_type: notifType,\n  });\n\n  const triggered: { notificationId: number; provider: string }[] = [];\n\n  let incident: Incident | undefined;\n  if (incidentId) {\n    try {\n      incident = await db.query.incidentTable.findFirst({\n        where: eq(schema.incidentTable.id, incidentId),\n      });\n    } catch (err) {\n      logger.warn(\"Failed to fetch incident data\", {\n        incident_id: incidentId,\n        error_message: err instanceof Error ? err.message : String(err),\n      });\n    }\n  }\n\n  const notifications = await db\n    .select()\n    .from(schema.notificationsToMonitors)\n    .innerJoin(\n      schema.notification,\n      eq(schema.notification.id, schema.notificationsToMonitors.notificationId),\n    )\n    .innerJoin(\n      schema.monitor,\n      eq(schema.monitor.id, schema.notificationsToMonitors.monitorId),\n    )\n    .where(eq(schema.monitor.id, Number(monitorId)))\n    .all();\n  for (const notif of notifications) {\n    // for sms check we are in the quota\n    if (notif.notification.provider === \"sms\") {\n      if (notif.notification.workspaceId === null) {\n        continue;\n      }\n\n      const workspace = await db\n        .select()\n        .from(schema.workspace)\n        .where(eq(schema.workspace.id, notif.notification.workspaceId));\n\n      if (workspace.length !== 1) {\n        continue;\n      }\n\n      const data = selectWorkspaceSchema.parse(workspace[0]);\n\n      const oneMonthAgo = new Date();\n      oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);\n\n      const smsNotification = await db\n        .select()\n        .from(schema.notification)\n        .where(\n          and(\n            eq(schema.notification.workspaceId, notif.notification.workspaceId),\n            eq(schema.notification.provider, \"sms\"),\n          ),\n        );\n      const ids = smsNotification.map((notification) => notification.id);\n\n      const smsSent = await db\n        .select({ count: count() })\n        .from(schema.notificationTrigger)\n        .where(\n          and(\n            gte(\n              schema.notificationTrigger.cronTimestamp,\n              Math.floor(oneMonthAgo.getTime() / 1000),\n            ),\n            inArray(schema.notificationTrigger.notificationId, ids),\n          ),\n        )\n        .all();\n\n      if ((smsSent[0]?.count ?? 0) > data.limits[\"sms-limit\"]) {\n        logger.warn(\n          `SMS quota exceeded for workspace ${notif.notification.workspaceId}`,\n        );\n        continue;\n      }\n    }\n    logger.info(\"Sending notification\", {\n      monitor_id: monitorId,\n      provider: notif.notification.provider,\n      notification_type: notifType,\n      notification_id: notif.notification.id,\n    });\n    const monitor = selectMonitorSchema.parse(notif.monitor);\n    try {\n      await insertNotificationTrigger({\n        monitorId: monitor.id,\n        notificationId: notif.notification.id,\n        cronTimestamp: cronTimestamp,\n      });\n    } catch (_e) {\n      logger.error(\"notification trigger already exists dont send again\");\n      continue;\n    }\n    triggered.push({\n      notificationId: notif.notification.id,\n      provider: notif.notification.provider,\n    });\n    switch (notifType) {\n      case \"alert\":\n        const alertResult = Effect.tryPromise({\n          try: () =>\n            providerToFunction[notif.notification.provider].sendAlert({\n              monitor,\n              notification: selectNotificationSchema.parse(notif.notification),\n              statusCode,\n              message,\n              incident,\n              cronTimestamp,\n              regions,\n              latency,\n            }),\n\n          catch: (_unknown) =>\n            new Error(\n              `Failed sending notification via ${notif.notification.provider} for monitor ${monitorId}`,\n            ),\n        }).pipe(\n          Effect.retry({\n            times: 3,\n            schedule: Schedule.exponential(\"1000 millis\"),\n          }),\n        );\n        await Effect.runPromise(alertResult).catch((err) =>\n          logger.error(\"Failed to send alert notification\", {\n            monitor_id: monitorId,\n            provider: notif.notification.provider,\n            error_message: err instanceof Error ? err.message : String(err),\n          }),\n        );\n        break;\n      case \"recovery\":\n        const recoveryResult = Effect.tryPromise({\n          try: () =>\n            providerToFunction[notif.notification.provider].sendRecovery({\n              monitor,\n              notification: selectNotificationSchema.parse(notif.notification),\n              statusCode,\n              message,\n              incident,\n              cronTimestamp,\n              regions,\n              latency,\n            }),\n          catch: (_unknown) =>\n            new Error(\n              `Failed sending notification via ${notif.notification.provider} for monitor ${monitorId}`,\n            ),\n        }).pipe(\n          Effect.retry({\n            times: 3,\n            schedule: Schedule.exponential(\"1000 millis\"),\n          }),\n        );\n        await Effect.runPromise(recoveryResult).catch((err) =>\n          logger.error(\"Failed to send recovery notification\", {\n            monitor_id: monitorId,\n            provider: notif.notification.provider,\n            error_message: err instanceof Error ? err.message : String(err),\n          }),\n        );\n        break;\n      case \"degraded\":\n        const degradedResult = Effect.tryPromise({\n          try: () =>\n            providerToFunction[notif.notification.provider].sendDegraded({\n              monitor,\n              notification: selectNotificationSchema.parse(notif.notification),\n              statusCode,\n              message,\n              incident,\n              cronTimestamp,\n              regions,\n              latency,\n            }),\n          catch: (_unknown) =>\n            new Error(\n              `Failed sending notification via ${notif.notification.provider} for monitor ${monitorId}`,\n            ),\n        }).pipe(\n          Effect.retry({\n            times: 3,\n            schedule: Schedule.exponential(\"1000 millis\"),\n          }),\n        );\n        await Effect.runPromise(degradedResult).catch((err) =>\n          logger.error(\"Failed to send degraded notification\", {\n            monitor_id: monitorId,\n            provider: notif.notification.provider,\n            error_message: err instanceof Error ? err.message : String(err),\n          }),\n        );\n        break;\n    }\n    // ALPHA\n    await checkerAudit.publishAuditLog({\n      id: `monitor:${monitorId}`,\n      action: \"notification.sent\",\n      targets: [{ id: monitorId, type: \"monitor\" }],\n      metadata: {\n        provider: notif.notification.provider,\n        cronTimestamp,\n        type: notifType,\n        notificationId: notif.notification.id,\n      },\n    });\n  }\n\n  return triggered;\n};\n\nconst insertNotificationTrigger = async ({\n  monitorId,\n  notificationId,\n  cronTimestamp,\n}: {\n  monitorId: number;\n  notificationId: number;\n  cronTimestamp: number;\n}) => {\n  await db\n    .insert(schema.notificationTrigger)\n    .values({\n      monitorId: Number(monitorId),\n      notificationId: notificationId,\n      cronTimestamp: cronTimestamp,\n    })\n    .returning();\n};\n\nexport const upsertMonitorStatus = async ({\n  monitorId,\n  status,\n  region,\n}: {\n  monitorId: string;\n  status: MonitorStatus;\n  region: Region;\n}) => {\n  const newData = await db\n    .insert(schema.monitorStatusTable)\n    .values({ status, region, monitorId: Number(monitorId) })\n    .onConflictDoUpdate({\n      target: [\n        schema.monitorStatusTable.monitorId,\n        schema.monitorStatusTable.region,\n      ],\n      set: { status, updatedAt: new Date() },\n    })\n    .returning();\n  logger.debug(\"Upserted monitor status\", {\n    monitor_id: monitorId,\n    region,\n    status,\n    updated_at: newData[0]?.updatedAt,\n  });\n};\n"
  },
  {
    "path": "apps/workflows/src/checker/index.ts",
    "content": "import { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport { and, db, eq, inArray, isNull, schema } from \"@openstatus/db\";\nimport { incidentTable } from \"@openstatus/db/src/schema\";\nimport {\n  monitorStatusSchema,\n  selectMonitorSchema,\n} from \"@openstatus/db/src/schema/monitors/validation\";\n\nimport { getLogger } from \"@logtape/logtape\";\nimport { monitorRegions } from \"@openstatus/db/src/schema/constants\";\nimport { env } from \"../env\";\nimport type { Env } from \"../index\";\nimport { checkerAudit } from \"../utils/audit-log\";\nimport { triggerNotifications, upsertMonitorStatus } from \"./alerting\";\n\nexport const checkerRoute = new Hono<Env>();\n\nconst payloadSchema = z.object({\n  monitorId: z.string(),\n  message: z.string().optional(),\n  statusCode: z.number().optional(),\n  region: z.enum(monitorRegions),\n  cronTimestamp: z.number(),\n  status: monitorStatusSchema,\n  latency: z.number().optional(),\n});\n\nconst logger = getLogger([\"workflow\"]);\n\n/**\n * Finds an open incident (not resolved and not acknowledged) for the given monitor.\n */\nasync function findOpenIncident(monitorId: number) {\n  return db\n    .select()\n    .from(incidentTable)\n    .where(\n      and(\n        eq(incidentTable.monitorId, monitorId),\n        isNull(incidentTable.resolvedAt),\n      ),\n    )\n    .get();\n}\n\n/**\n * Resolves an open incident by setting resolvedAt and autoResolved flag.\n */\nasync function resolveIncident(params: {\n  monitorId: string;\n  cronTimestamp: number;\n}) {\n  const { monitorId, cronTimestamp } = params;\n  const incident = await findOpenIncident(Number(monitorId));\n\n  if (!incident || incident.resolvedAt) {\n    return null;\n  }\n\n  logger.info(\"Recovering incident\", {\n    incident_id: incident.id,\n    monitor_id: monitorId,\n  });\n\n  await db\n    .update(incidentTable)\n    .set({\n      resolvedAt: new Date(cronTimestamp),\n      autoResolved: true,\n    })\n    .where(eq(incidentTable.id, incident.id))\n    .run();\n\n  await checkerAudit.publishAuditLog({\n    id: `monitor:${monitorId}`,\n    action: \"incident.resolved\",\n    targets: [{ id: monitorId, type: \"monitor\" }],\n    metadata: { cronTimestamp, incidentId: incident.id },\n  });\n\n  return incident;\n}\n\ncheckerRoute.post(\"/updateStatus\", async (c) => {\n  const auth = c.req.header(\"Authorization\");\n  if (auth !== `Basic ${env().CRON_SECRET}`) {\n    logger.error(\"Unauthorized\");\n    return c.text(\"Unauthorized\", 401);\n  }\n\n  const event = c.get(\"event\");\n  const json = await c.req.json();\n\n  const result = payloadSchema.safeParse(json);\n\n  if (!result.success) {\n    return c.text(\"Unprocessable Entity\", 422);\n  }\n\n  const {\n    monitorId,\n    message,\n    region,\n    statusCode,\n    cronTimestamp,\n    status,\n    latency,\n  } = result.data;\n\n  logger.info(\"Updating monitor status\", {\n    monitor_id: monitorId,\n    region,\n    status,\n    status_code: statusCode,\n    cron_timestamp: cronTimestamp,\n    latency_ms: latency,\n  });\n\n  // First we upsert the monitor status\n  await upsertMonitorStatus({\n    monitorId: monitorId,\n    status,\n    region: region,\n  });\n\n  const currentMonitor = await db\n    .select()\n    .from(schema.monitor)\n    .where(eq(schema.monitor.id, Number(monitorId)))\n    .get();\n\n  const monitor = selectMonitorSchema.parse(currentMonitor);\n  const numberOfRegions = monitor.regions.length;\n\n  // Fetch all affected regions for notifications (single query)\n  const affectedRegions = await db\n    .select({ region: schema.monitorStatusTable.region })\n    .from(schema.monitorStatusTable)\n    .where(\n      and(\n        eq(schema.monitorStatusTable.monitorId, monitor.id),\n        eq(schema.monitorStatusTable.status, status),\n        inArray(schema.monitorStatusTable.region, monitor.regions),\n      ),\n    )\n    .all();\n\n  const affectedRegionsList = affectedRegions.map((r) => r.region);\n  const affectedRegionCount = affectedRegionsList.length;\n\n  event.status_update = {\n    status: result.data.status,\n    message: result.data.message,\n    region: result.data.region,\n    status_code: result.data.statusCode,\n    cron_timestamp: result.data.cronTimestamp,\n    latency_ms: result.data.latency,\n    affectedRegionsCount: affectedRegionCount,\n    monitorId: monitor.id,\n  };\n\n  if (affectedRegionCount === 0) {\n    return c.json({ success: true }, 200);\n  }\n\n  // audit log the current state of the ping\n\n  switch (status) {\n    case \"active\":\n      await checkerAudit.publishAuditLog({\n        id: `monitor:${monitorId}`,\n        action: \"monitor.recovered\",\n        targets: [{ id: monitorId, type: \"monitor\" }],\n        metadata: {\n          region,\n          statusCode: statusCode ?? -1,\n          cronTimestamp,\n          latency,\n        },\n      });\n      break;\n    case \"degraded\":\n      await checkerAudit.publishAuditLog({\n        id: `monitor:${monitorId}`,\n        action: \"monitor.degraded\",\n        targets: [{ id: monitorId, type: \"monitor\" }],\n        metadata: {\n          region,\n          statusCode: statusCode ?? -1,\n          cronTimestamp,\n          latency,\n        },\n      });\n      break;\n    case \"error\":\n      await checkerAudit.publishAuditLog({\n        id: `monitor:${monitorId}`,\n        action: \"monitor.failed\",\n        targets: [{ id: monitorId, type: \"monitor\" }],\n        metadata: {\n          region,\n          statusCode: statusCode ?? -1,\n          message,\n          cronTimestamp,\n          latency,\n        },\n      });\n      break;\n  }\n\n  let triggeredNotifications: { notificationId: number; provider: string }[] =\n    [];\n\n  if (affectedRegionCount >= numberOfRegions / 2 || numberOfRegions === 1) {\n    switch (status) {\n      case \"active\": {\n        if (monitor.status === \"active\") {\n          break;\n        }\n\n        logger.info(\"Monitor status changed to active\", {\n          monitor_id: monitor.id,\n          workspace_id: monitor.workspaceId,\n        });\n        await db\n          .update(schema.monitor)\n          .set({ status: \"active\" })\n          .where(eq(schema.monitor.id, monitor.id));\n\n        let incident = null;\n        if (monitor.status === \"error\") {\n          incident = await resolveIncident({ monitorId, cronTimestamp });\n        }\n\n        triggeredNotifications = await triggerNotifications({\n          monitorId,\n          statusCode,\n          message,\n          notifType: \"recovery\",\n          cronTimestamp,\n          regions: affectedRegionsList,\n          latency,\n          incidentId: incident?.id,\n        });\n\n        break;\n      }\n      case \"degraded\":\n        if (monitor.status === \"degraded\") {\n          break;\n        }\n\n        logger.info(\"Monitor status changed to degraded\", {\n          monitor_id: monitor.id,\n          workspace_id: monitor.workspaceId,\n        });\n\n        await db\n          .update(schema.monitor)\n          .set({ status: \"degraded\" })\n          .where(eq(schema.monitor.id, monitor.id));\n\n        let incident = null;\n        if (monitor.status === \"error\") {\n          incident = await resolveIncident({\n            monitorId,\n            cronTimestamp,\n          });\n        }\n\n        triggeredNotifications = await triggerNotifications({\n          monitorId,\n          statusCode,\n          message,\n          notifType: \"degraded\",\n          cronTimestamp,\n          latency,\n          regions: affectedRegionsList,\n          incidentId: incident?.id,\n        });\n\n        break;\n      case \"error\":\n        if (monitor.status === \"error\") {\n          break;\n        }\n\n        logger.info(\"Monitor status changed to error\", {\n          monitor_id: monitor.id,\n          workspace_id: monitor.workspaceId,\n        });\n\n        await db\n          .update(schema.monitor)\n          .set({ status: \"error\" })\n          .where(eq(schema.monitor.id, monitor.id));\n\n        try {\n          const existingIncident = await findOpenIncident(Number(monitorId));\n          if (existingIncident) {\n            logger.info(\"Already in incident\", {\n              incident_id: existingIncident.id,\n            });\n            break;\n          }\n\n          const [newIncident] = await db\n            .insert(incidentTable)\n            .values({\n              monitorId: Number(monitorId),\n              workspaceId: monitor.workspaceId,\n              startedAt: new Date(cronTimestamp),\n            })\n            .returning();\n\n          if (!newIncident?.id) {\n            break;\n          }\n\n          await checkerAudit.publishAuditLog({\n            id: `monitor:${monitorId}`,\n            action: \"incident.created\",\n            targets: [{ id: monitorId, type: \"monitor\" }],\n            metadata: { cronTimestamp, incidentId: newIncident.id },\n          });\n\n          triggeredNotifications = await triggerNotifications({\n            monitorId,\n            statusCode,\n            message,\n            notifType: \"alert\",\n            cronTimestamp,\n            latency,\n            regions: affectedRegionsList,\n            incidentId: newIncident.id,\n          });\n        } catch (error) {\n          logger.warning(\"Failed to create incident\", { error });\n        }\n\n        break;\n      default:\n        logger.error(\"should not happen\");\n        break;\n    }\n  }\n\n  (event.status_update as Record<string, unknown>).notificationTriggered =\n    triggeredNotifications.length > 0;\n  (event.status_update as Record<string, unknown>).notifications =\n    triggeredNotifications;\n\n  return c.text(\"Ok\", 200);\n});\n"
  },
  {
    "path": "apps/workflows/src/checker/utils.ts",
    "content": "import type { NotificationProvider } from \"@openstatus/db/src/schema\";\nimport type { NotificationContext } from \"@openstatus/notification-base\";\nimport {\n  sendAlert as sendDiscordAlert,\n  sendDegraded as sendDiscordDegraded,\n  sendRecovery as sendDiscordRecovery,\n} from \"@openstatus/notification-discord\";\nimport {\n  sendAlert as sendEmailAlert,\n  sendDegraded as sendEmailDegraded,\n  sendRecovery as sendEmailRecovery,\n} from \"@openstatus/notification-emails\";\nimport {\n  sendAlert as sendGoogleChatAlert,\n  sendDegraded as sendGoogleChatDegraded,\n  sendRecovery as sendGoogleChatRecovery,\n} from \"@openstatus/notification-google-chat\";\nimport {\n  sendAlert as sendGrafanaOncallAlert,\n  sendDegraded as sendGrafanaOncallDegraded,\n  sendRecovery as sendGrafanaOncallRecovery,\n} from \"@openstatus/notification-grafana-oncall\";\nimport {\n  sendAlert as sendNtfyAlert,\n  sendDegraded as sendNtfyDegraded,\n  sendRecovery as sendNtfyRecovery,\n} from \"@openstatus/notification-ntfy\";\nimport {\n  sendAlert as sendOpsGenieAlert,\n  sendDegraded as sendOpsGenieDegraded,\n  sendRecovery as sendOpsGenieRecovery,\n} from \"@openstatus/notification-opsgenie\";\nimport {\n  sendDegraded as sendPagerDutyDegraded,\n  sendRecovery as sendPagerDutyRecovery,\n  sendAlert as sendPagerdutyAlert,\n} from \"@openstatus/notification-pagerduty\";\nimport {\n  sendAlert as sendSlackAlert,\n  sendDegraded as sendSlackDegraded,\n  sendRecovery as sendSlackRecovery,\n} from \"@openstatus/notification-slack\";\nimport {\n  sendAlert as sendTelegramAlert,\n  sendDegraded as sendTelegramDegraded,\n  sendRecovery as sendTelegramRecovery,\n} from \"@openstatus/notification-telegram\";\nimport {\n  sendAlert as sendSmsAlert,\n  sendDegraded as sendSmsDegraded,\n  sendRecovery as sendSmsRecovery,\n} from \"@openstatus/notification-twillio-sms\";\nimport {\n  sendAlert as sendWhatsappAlert,\n  sendDegraded as sendWhatsappDegraded,\n  sendRecovery as sendWhatsappRecovery,\n} from \"@openstatus/notification-twillio-whatsapp\";\nimport {\n  sendAlert as sendWebhookAlert,\n  sendDegraded as sendWebhookDegraded,\n  sendRecovery as sendWebhookRecovery,\n} from \"@openstatus/notification-webhook\";\n\ntype SendNotification = (props: NotificationContext) => Promise<void>;\n\ntype Notif = {\n  sendAlert: SendNotification;\n  sendRecovery: SendNotification;\n  sendDegraded: SendNotification;\n};\n\nexport const providerToFunction: Record<NotificationProvider, Notif> = {\n  discord: {\n    sendAlert: sendDiscordAlert,\n    sendRecovery: sendDiscordRecovery,\n    sendDegraded: sendDiscordDegraded,\n  },\n  email: {\n    sendAlert: sendEmailAlert,\n    sendRecovery: sendEmailRecovery,\n    sendDegraded: sendEmailDegraded,\n  },\n  \"google-chat\": {\n    sendAlert: sendGoogleChatAlert,\n    sendRecovery: sendGoogleChatRecovery,\n    sendDegraded: sendGoogleChatDegraded,\n  },\n  \"grafana-oncall\": {\n    sendAlert: sendGrafanaOncallAlert,\n    sendRecovery: sendGrafanaOncallRecovery,\n    sendDegraded: sendGrafanaOncallDegraded,\n  },\n  ntfy: {\n    sendAlert: sendNtfyAlert,\n    sendRecovery: sendNtfyRecovery,\n    sendDegraded: sendNtfyDegraded,\n  },\n  opsgenie: {\n    sendAlert: sendOpsGenieAlert,\n    sendRecovery: sendOpsGenieRecovery,\n    sendDegraded: sendOpsGenieDegraded,\n  },\n  pagerduty: {\n    sendAlert: sendPagerdutyAlert,\n    sendRecovery: sendPagerDutyRecovery,\n    sendDegraded: sendPagerDutyDegraded,\n  },\n  slack: {\n    sendAlert: sendSlackAlert,\n    sendRecovery: sendSlackRecovery,\n    sendDegraded: sendSlackDegraded,\n  },\n  sms: {\n    sendAlert: sendSmsAlert,\n    sendRecovery: sendSmsRecovery,\n    sendDegraded: sendSmsDegraded,\n  },\n  webhook: {\n    sendAlert: sendWebhookAlert,\n    sendRecovery: sendWebhookRecovery,\n    sendDegraded: sendWebhookDegraded,\n  },\n  whatsapp: {\n    sendAlert: sendWhatsappAlert,\n    sendRecovery: sendWhatsappRecovery,\n    sendDegraded: sendWhatsappDegraded,\n  },\n  telegram: {\n    sendAlert: sendTelegramAlert,\n    sendRecovery: sendTelegramRecovery,\n    sendDegraded: sendTelegramDegraded,\n  },\n};\n"
  },
  {
    "path": "apps/workflows/src/cron/checker.ts",
    "content": "import { CloudTasksClient } from \"@google-cloud/tasks\";\nimport type { google } from \"@google-cloud/tasks/build/protos/protos\";\nimport { z } from \"zod\";\n\nimport { and, eq, gte, isNotNull, lte, notInArray } from \"@openstatus/db\";\nimport {\n  type MonitorStatus,\n  maintenance,\n  monitor,\n  monitorStatusTable,\n  selectMonitorSchema,\n  selectMonitorStatusSchema,\n} from \"@openstatus/db/src/schema\";\nimport type { Region } from \"@openstatus/db/src/schema/constants\";\nimport {\n  maintenancesToPageComponents,\n  pageComponent,\n} from \"@openstatus/db/src/schema/page_components\";\nimport { regionDict } from \"@openstatus/regions\";\nimport { db } from \"../lib/db\";\n\nimport { getSentry } from \"@hono/sentry\";\nimport { getLogger } from \"@logtape/logtape\";\nimport type { monitorPeriodicitySchema } from \"@openstatus/db/src/schema/constants\";\nimport {\n  type DNSPayloadSchema,\n  type httpPayloadSchema,\n  type tpcPayloadSchema,\n  transformHeaders,\n} from \"@openstatus/utils\";\nimport type { Context } from \"hono\";\nimport { env } from \"../env\";\n\nexport const isAuthorizedDomain = (url: string) => {\n  return url.includes(env().SITE_URL);\n};\n\nconst logger = getLogger(\"workflow\");\n\nconst channelOptions = {\n  // Conservative 5-minute keepalive (gRPC best practice)\n  \"grpc.keepalive_time_ms\": 300000,\n  // 5-second timeout sufficient for ping response\n  \"grpc.keepalive_timeout_ms\": 5000,\n  // Disable pings without active calls to avoid server conflicts\n  \"grpc.keepalive_permit_without_calls\": 1,\n};\n\nexport async function sendCheckerTasks(\n  periodicity: z.infer<typeof monitorPeriodicitySchema>,\n  c: Context,\n) {\n  const client = new CloudTasksClient({\n    fallback: \"rest\",\n    channelOptions,\n    projectId: env().GCP_PROJECT_ID,\n    credentials: {\n      client_email: env().GCP_CLIENT_EMAIL,\n      private_key: env().GCP_PRIVATE_KEY.replaceAll(\"\\\\n\", \"\\n\"),\n    },\n  });\n\n  const parent = client.queuePath(\n    env().GCP_PROJECT_ID,\n    env().GCP_LOCATION,\n    periodicity,\n  );\n\n  const timestamp = Date.now();\n\n  const currentMaintenance = db\n    .select({ id: maintenance.id })\n    .from(maintenance)\n    .where(\n      and(lte(maintenance.from, new Date()), gte(maintenance.to, new Date())),\n    )\n    .as(\"currentMaintenance\");\n\n  const currentMaintenanceMonitors = db\n    .select({ id: pageComponent.monitorId })\n    .from(maintenancesToPageComponents)\n    .innerJoin(\n      currentMaintenance,\n      eq(maintenancesToPageComponents.maintenanceId, currentMaintenance.id),\n    )\n    .innerJoin(\n      pageComponent,\n      eq(maintenancesToPageComponents.pageComponentId, pageComponent.id),\n    )\n    .where(isNotNull(pageComponent.monitorId));\n\n  const result = await db\n    .select()\n    .from(monitor)\n    .where(\n      and(\n        eq(monitor.periodicity, periodicity),\n        eq(monitor.active, true),\n        notInArray(monitor.id, currentMaintenanceMonitors),\n      ),\n    )\n    .all();\n\n  logger.info(\"Starting cron job\", {\n    periodicity,\n    monitor_count: result.length,\n  });\n\n  const monitors = z.array(selectMonitorSchema).safeParse(result);\n  const allResult = [];\n  if (!monitors.success) {\n    logger.error(`Error while fetching the monitors ${monitors.error}`);\n    throw new Error(\"Error while fetching the monitors\");\n  }\n\n  for (const row of monitors.data) {\n    // const selectedRegions = row.regions.length > 0 ? row.regions : [\"ams\"];\n\n    const result = await db\n      .select()\n      .from(monitorStatusTable)\n      .where(eq(monitorStatusTable.monitorId, row.id))\n      .all();\n    const monitorStatus = z.array(selectMonitorStatusSchema).safeParse(result);\n    if (!monitorStatus.success) {\n      logger.error(\"Failed to parse monitor status\", {\n        monitor_id: row.id,\n        error_message: monitorStatus.error.message,\n      });\n      continue;\n    }\n\n    for (const region of row.regions) {\n      const status =\n        monitorStatus.data.find((m) => region === m.region)?.status || \"active\";\n\n      const r = regionDict[region as keyof typeof regionDict];\n\n      if (!r) {\n        logger.error(`Invalid region ${region}`);\n        continue;\n      }\n      if (r.deprecated) {\n        // Let's uncomment this when we are ready to remove deprecated regions\n        // We should not use deprecated regions anymore\n        logger.error(`Deprecated region ${region}`);\n        continue;\n      }\n      const response = createCronTask({\n        row,\n        timestamp,\n        client,\n        parent,\n        status,\n        region,\n      });\n      allResult.push(response);\n      if (periodicity === \"30s\") {\n        // we schedule another task in 30s\n        const scheduledAt = timestamp + 30 * 1000;\n        const response = createCronTask({\n          row,\n          timestamp: scheduledAt,\n          client,\n          parent,\n          status,\n          region,\n        });\n        allResult.push(response);\n      }\n    }\n  }\n\n  const allRequests = await Promise.allSettled(allResult);\n\n  const success = allRequests.filter((r) => r.status === \"fulfilled\").length;\n  const failed = allRequests.filter((r) => r.status === \"rejected\").length;\n\n  logger.info(\"Completed cron job\", {\n    periodicity,\n    total_tasks: allResult.length,\n    success_count: success,\n    failed_count: failed,\n  });\n  if (failed > 0) {\n    logger.error(\"Cron job had failures\", {\n      periodicity,\n      failed_count: failed,\n      success_count: success,\n    });\n    getSentry(c).captureMessage(\n      `sendCheckerTasks for ${periodicity} ended with ${failed} failed tasks`,\n      \"error\",\n    );\n  }\n}\n// timestamp needs to be in ms\nconst createCronTask = async ({\n  row,\n  timestamp,\n  client,\n  parent,\n  status,\n  region,\n}: {\n  row: z.infer<typeof selectMonitorSchema>;\n  timestamp: number;\n  client: CloudTasksClient;\n  parent: string;\n  status: MonitorStatus;\n  region: Region;\n}) => {\n  let payload:\n    | z.infer<typeof httpPayloadSchema>\n    | z.infer<typeof tpcPayloadSchema>\n    | z.infer<typeof DNSPayloadSchema>\n    | null = null;\n\n  //\n  if (row.jobType === \"http\") {\n    payload = {\n      workspaceId: String(row.workspaceId),\n      monitorId: String(row.id),\n      url: row.url,\n      method: row.method || \"GET\",\n      cronTimestamp: timestamp,\n      body: row.body,\n      headers: row.headers,\n      status: status,\n      assertions: row.assertions ? JSON.parse(row.assertions) : null,\n      degradedAfter: row.degradedAfter,\n      timeout: row.timeout,\n      trigger: \"cron\",\n      otelConfig: row.otelEndpoint\n        ? {\n            endpoint: row.otelEndpoint,\n            headers: transformHeaders(row.otelHeaders),\n          }\n        : undefined,\n      retry: row.retry || 3,\n      followRedirects:\n        row.followRedirects === null ? true : row.followRedirects,\n    };\n  }\n  if (row.jobType === \"tcp\") {\n    payload = {\n      workspaceId: String(row.workspaceId),\n      monitorId: String(row.id),\n      uri: row.url,\n      status: status,\n      assertions: row.assertions ? JSON.parse(row.assertions) : null,\n      cronTimestamp: timestamp,\n      degradedAfter: row.degradedAfter,\n      timeout: row.timeout,\n      trigger: \"cron\",\n      retry: row.retry || 3,\n      otelConfig: row.otelEndpoint\n        ? {\n            endpoint: row.otelEndpoint,\n            headers: transformHeaders(row.otelHeaders),\n          }\n        : undefined,\n    };\n  }\n  if (row.jobType === \"dns\") {\n    payload = {\n      workspaceId: String(row.workspaceId),\n      monitorId: String(row.id),\n      uri: row.url,\n      cronTimestamp: timestamp,\n      status: status,\n      assertions: row.assertions ? JSON.parse(row.assertions) : null,\n      degradedAfter: row.degradedAfter,\n      timeout: row.timeout,\n      trigger: \"cron\",\n      otelConfig: row.otelEndpoint\n        ? {\n            endpoint: row.otelEndpoint,\n            headers: transformHeaders(row.otelHeaders),\n          }\n        : undefined,\n      retry: row.retry || 3,\n    };\n  }\n\n  if (!payload) {\n    throw new Error(\"Invalid jobType\");\n  }\n  const regionInfo = regionDict[region];\n  let regionHeader = {};\n  if (regionInfo.provider === \"fly\") {\n    regionHeader = { \"fly-prefer-region\": region };\n  }\n  if (regionInfo.provider === \"koyeb\") {\n    regionHeader = { \"X-KOYEB-REGION-OVERRIDE\": region.replace(\"koyeb_\", \"\") };\n  }\n  if (regionInfo.provider === \"railway\") {\n    regionHeader = { \"railway-region\": region.replace(\"railway_\", \"\") };\n  }\n  const newTask: google.cloud.tasks.v2beta3.ITask = {\n    httpRequest: {\n      headers: {\n        \"Content-Type\": \"application/json\", // Set content type to ensure compatibility your application's request parsing\n        ...regionHeader,\n        Authorization: `Basic ${env().CRON_SECRET}`,\n      },\n      httpMethod: \"POST\",\n      url: generateUrl({ row, region }),\n      body: Buffer.from(JSON.stringify(payload)).toString(\"base64\"),\n    },\n    scheduleTime: {\n      seconds: timestamp / 1000,\n    },\n  };\n\n  const request = { parent: parent, task: newTask };\n  return client.createTask(request);\n};\n\nfunction generateUrl({\n  row,\n  region,\n}: {\n  row: z.infer<typeof selectMonitorSchema>;\n  region: Region;\n}) {\n  const regionInfo = regionDict[region];\n\n  switch (regionInfo.provider) {\n    case \"fly\":\n      return `https://openstatus-checker.fly.dev/checker/${row.jobType}?monitor_id=${row.id}`;\n    case \"koyeb\":\n      return `https://openstatus-checker.koyeb.app/checker/${row.jobType}?monitor_id=${row.id}`;\n    case \"railway\":\n      return `https://railway-proxy-production-9cb1.up.railway.app/checker/${row.jobType}?monitor_id=${row.id}`;\n\n    default:\n      throw new Error(\"Invalid jobType\");\n  }\n}\n"
  },
  {
    "path": "apps/workflows/src/cron/emails.ts",
    "content": "import { and, eq, gte, inArray, lte } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db\";\nimport {\n  integration,\n  user,\n  usersToWorkspaces,\n} from \"@openstatus/db/src/schema\";\nimport { EmailClient } from \"@openstatus/emails\";\nimport { env } from \"../env\";\n\nconst email = new EmailClient({ apiKey: env().RESEND_API_KEY });\n\nexport async function sendFollowUpEmails() {\n  const date1 = new Date();\n  date1.setDate(date1.getDate() - 2);\n  const date2 = new Date();\n  date2.setDate(date2.getDate() - 1);\n\n  const users = await db\n    .select({\n      email: user.email,\n      workspaceId: usersToWorkspaces.workspaceId,\n    })\n    .from(user)\n    .innerJoin(usersToWorkspaces, eq(user.id, usersToWorkspaces.userId))\n    .where(and(gte(user.createdAt, date1), lte(user.createdAt, date2)))\n    .all();\n\n  console.log(`Found ${users.length} users to send follow ups.`);\n\n  const workspaceIds = [\n    ...new Set(users.map((u) => u.workspaceId).filter(Boolean)),\n  ];\n\n  const slackWorkspaceIds = new Set<number>();\n  if (workspaceIds.length > 0) {\n    const slackIntegrations = await db\n      .select({ workspaceId: integration.workspaceId })\n      .from(integration)\n      .where(\n        and(\n          eq(integration.name, \"slack-agent\"),\n          inArray(integration.workspaceId, workspaceIds),\n        ),\n      )\n      .all();\n    for (const row of slackIntegrations) {\n      if (row.workspaceId) {\n        slackWorkspaceIds.add(row.workspaceId);\n      }\n    }\n  }\n\n  const slackEmails: string[] = [];\n  const noSlackEmails: string[] = [];\n\n  for (const u of users) {\n    if (!u.email || u.email.trim() === \"\") continue;\n    const hasSlack = u.workspaceId\n      ? slackWorkspaceIds.has(u.workspaceId)\n      : false;\n    if (hasSlack) {\n      slackEmails.push(u.email);\n    } else {\n      noSlackEmails.push(u.email);\n    }\n  }\n\n  const batchSize = 80;\n\n  for (let i = 0; i < noSlackEmails.length; i += batchSize) {\n    const batch = noSlackEmails.slice(i, i + batchSize);\n    console.log(`Sending follow-up batch with ${batch.length} emails...`);\n    try {\n      await email.sendFollowUpBatched({ to: batch });\n    } catch {\n      console.error(\"Rate limit exceeded. Stopping further sends.\");\n      break;\n    }\n  }\n\n  for (let i = 0; i < slackEmails.length; i += batchSize) {\n    const batch = slackEmails.slice(i, i + batchSize);\n    console.log(`Sending slack feedback batch with ${batch.length} emails...`);\n    try {\n      await email.sendSlackFeedbackBatched({ to: batch });\n    } catch {\n      console.error(\"Rate limit exceeded. Stopping further sends.\");\n      break;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/workflows/src/cron/index.ts",
    "content": "import { getSentry } from \"@hono/sentry\";\nimport { monitorPeriodicitySchema } from \"@openstatus/db/src/schema/constants\";\nimport { Hono } from \"hono\";\nimport { env } from \"../env\";\nimport { sendCheckerTasks } from \"./checker\";\nimport { sendFollowUpEmails } from \"./emails\";\nimport {\n  LaunchMonitorWorkflow,\n  Step3Days,\n  Step14Days,\n  StepPaused,\n  workflowStepSchema,\n} from \"./monitor\";\n\nconst app = new Hono({ strict: false });\n\napp.use(\"*\", async (c, next) => {\n  if (c.req.header(\"authorization\") !== env().CRON_SECRET) {\n    return c.text(\"Unauthorized\", 401);\n  }\n\n  return next();\n});\n\napp.get(\"/checker/:period\", async (c) => {\n  const period = c.req.param(\"period\");\n\n  const schema = monitorPeriodicitySchema.safeParse(period);\n\n  if (!schema.success) {\n    return c.json({ error: schema.error.issues?.[0].message }, 400);\n  }\n  const sentry = getSentry(c);\n  const checkInId = sentry.captureCheckIn({\n    monitorSlug: period,\n    status: \"in_progress\",\n  });\n  try {\n    await sendCheckerTasks(schema.data, c);\n    sentry.captureCheckIn({\n      checkInId,\n      monitorSlug: period,\n      status: \"ok\",\n    });\n    return c.json({ success: schema.data }, 200);\n  } catch (e) {\n    console.error(e);\n    sentry.captureMessage(`Error in /checker/${period} cron: ${e}`, \"error\");\n    sentry.captureCheckIn({\n      checkInId,\n      monitorSlug: period,\n      status: \"error\",\n    });\n    return c.text(\"Internal Server Error\", 500);\n  }\n});\n\napp.get(\"/emails/follow-up\", async (c) => {\n  try {\n    await sendFollowUpEmails();\n    return c.json({ success: true }, 200);\n  } catch (e) {\n    console.error(e);\n    return c.text(\"Internal Server Error\", 500);\n  }\n});\n\napp.get(\"/monitors\", async (c) => {\n  await LaunchMonitorWorkflow();\n  return c.json({ success: true }, 200);\n});\n\napp.get(\"/monitors/:step\", async (c) => {\n  const step = c.req.param(\"step\");\n  const schema = workflowStepSchema.safeParse(step);\n\n  const userId = c.req.query(\"userId\");\n  const initialRun = c.req.query(\"initialRun\");\n  if (!schema.success) {\n    return c.json({ error: schema.error.issues?.[0].message }, 400);\n  }\n\n  if (!userId) {\n    getSentry(c).captureMessage(\n      \"userId is missing in /monitors/:step cron\",\n      \"error\",\n    );\n    return c.json({ error: \"userId is required\" }, 400);\n  }\n  if (!initialRun) {\n    getSentry(c).captureMessage(\n      \"initalRun is missing in /monitors/:step cron\",\n      \"error\",\n    );\n    return c.json({ error: \"initialRun is required\" }, 400);\n  }\n\n  switch (schema.data) {\n    case \"14days\":\n      // We send the first email\n      await Step14Days(Number(userId), Number(initialRun));\n      break;\n    case \"3days\":\n      await Step3Days(Number(userId), Number(initialRun));\n      // 3 days before we send the second email\n      break;\n    case \"paused\":\n      // Let's pause the monitor\n      await StepPaused(Number(userId), Number(initialRun));\n      break;\n    default:\n      throw new Error(\"Invalid step\");\n  }\n  // Swith on step\n  // and do the right action\n  //\n  return c.json({ success: true }, 200);\n});\n\nexport { app as cronRouter };\n"
  },
  {
    "path": "apps/workflows/src/cron/monitor.ts",
    "content": "import { CloudTasksClient } from \"@google-cloud/tasks\";\nimport type { google } from \"@google-cloud/tasks/build/protos/protos\";\nimport {\n  and,\n  db,\n  desc,\n  eq,\n  isNull,\n  lte,\n  max,\n  or,\n  schema,\n} from \"@openstatus/db\";\nimport { session, user } from \"@openstatus/db/src/schema\";\nimport {\n  monitorDeactivationEmail,\n  monitorPausedEmail,\n} from \"@openstatus/emails\";\nimport { sendBatchEmailHtml } from \"@openstatus/emails/src/send\";\nimport { Redis } from \"@openstatus/upstash\";\nimport { RateLimiter } from \"limiter\";\nimport { z } from \"zod\";\nimport { env } from \"../env\";\n\nconst redis = Redis.fromEnv();\n\nconst client = new CloudTasksClient({\n  projectId: env().GCP_PROJECT_ID,\n  fallback: \"rest\",\n  credentials: {\n    client_email: env().GCP_CLIENT_EMAIL,\n    private_key: env().GCP_PRIVATE_KEY.replaceAll(\"\\\\n\", \"\\n\"),\n  },\n});\n\nconst parent = client.queuePath(\n  env().GCP_PROJECT_ID,\n  env().GCP_LOCATION,\n  \"workflow\",\n);\n\nconst limiter = new RateLimiter({ tokensPerInterval: 15, interval: \"second\" });\n\nexport async function LaunchMonitorWorkflow() {\n  // Expires is one month after last connection, so if we want to reach people who connected 3 months ago we need to check for people with  expires 2 months ago\n  const twoMonthAgo = new Date().setMonth(new Date().getMonth() - 2);\n\n  const date = new Date(twoMonthAgo);\n  // User without session\n  const userWithoutSession = db\n    .select({\n      userId: schema.user.id,\n      email: schema.user.email,\n      updatedAt: schema.user.updatedAt,\n    })\n    .from(schema.user)\n    .leftJoin(schema.session, eq(schema.session.userId, schema.user.id))\n    .where(isNull(schema.session.userId))\n    .as(\"query\");\n  // Only free users monitors are paused\n  // We don't need to handle multi users per workspace because free workspaces only have one user\n  // Only free users monitors are paused\n\n  const u1 = await db\n    .select({\n      userId: userWithoutSession.userId,\n      email: userWithoutSession.email,\n      workspaceId: schema.workspace.id,\n    })\n    .from(userWithoutSession)\n    .innerJoin(\n      schema.usersToWorkspaces,\n      eq(userWithoutSession.userId, schema.usersToWorkspaces.userId),\n    )\n    .innerJoin(\n      schema.workspace,\n      eq(schema.usersToWorkspaces.workspaceId, schema.workspace.id),\n    )\n    .where(\n      and(\n        or(\n          lte(userWithoutSession.updatedAt, date),\n          isNull(userWithoutSession.updatedAt),\n        ),\n        or(isNull(schema.workspace.plan), eq(schema.workspace.plan, \"free\")),\n      ),\n    );\n\n  console.log(`Found ${u1.length} users without session to start the workflow`);\n  const maxSessionPerUser = db\n    .select({\n      userId: schema.user.id,\n      email: schema.user.email,\n      lastConnection: max(schema.session.expires).as(\"lastConnection\"),\n    })\n    .from(schema.user)\n    .innerJoin(schema.session, eq(schema.session.userId, schema.user.id))\n    .groupBy(schema.user.id)\n    .as(\"maxSessionPerUser\");\n  // Only free users monitors are paused\n  // We don't need to handle multi users per workspace because free workspaces only have one user\n  // Only free users monitors are paused\n\n  const u = await db\n    .select({\n      userId: maxSessionPerUser.userId,\n      email: maxSessionPerUser.email,\n      workspaceId: schema.workspace.id,\n    })\n    .from(maxSessionPerUser)\n    .innerJoin(\n      schema.usersToWorkspaces,\n      eq(maxSessionPerUser.userId, schema.usersToWorkspaces.userId),\n    )\n    .innerJoin(\n      schema.workspace,\n      eq(schema.usersToWorkspaces.workspaceId, schema.workspace.id),\n    )\n    .where(\n      and(\n        lte(maxSessionPerUser.lastConnection, date),\n        or(isNull(schema.workspace.plan), eq(schema.workspace.plan, \"free\")),\n      ),\n    );\n  // Let's merge both results\n  const users = [...u, ...u1];\n  // iterate over users\n\n  const allResult = [];\n\n  for (const user of users) {\n    await limiter.removeTokens(1);\n    const workflow = workflowInit({ user });\n    allResult.push(workflow);\n  }\n\n  const allRequests = await Promise.allSettled(allResult);\n\n  const success = allRequests.filter((r) => r.status === \"fulfilled\").length;\n  const failed = allRequests.filter((r) => r.status === \"rejected\").length;\n\n  console.log(\n    `End cron with ${allResult.length} jobs with ${success} success and ${failed} failed`,\n  );\n}\n\nasync function workflowInit({\n  user,\n}: {\n  user: {\n    userId: number;\n    email: string | null;\n    workspaceId: number;\n  };\n}) {\n  console.log(`Starting workflow for ${user.userId}`);\n  // Let's check if the user is in the workflow\n  const isMember = await redis.sismember(\"workflow:users\", user.userId);\n  if (isMember) {\n    console.log(`user workflow already started for ${user.userId}`);\n    return;\n  }\n  // check if user has some running monitors\n  const nbRunningMonitor = await db.$count(\n    schema.monitor,\n    and(\n      eq(schema.monitor.workspaceId, user.workspaceId),\n      eq(schema.monitor.active, true),\n      isNull(schema.monitor.deletedAt),\n    ),\n  );\n  if (nbRunningMonitor === 0) {\n    console.log(`user has no running monitors for ${user.userId}`);\n    return;\n  }\n  await CreateTask({\n    parent,\n    client: client,\n    step: \"14days\",\n    userId: user.userId,\n    initialRun: new Date().getTime(),\n  });\n  // // Add our user to the list of users that have started the workflow\n\n  await redis.sadd(\"workflow:users\", user.userId);\n  console.log(`user workflow started for ${user.userId}`);\n}\n\nexport async function Step14Days(userId: number, workFlowRunTimestamp: number) {\n  const user = await getUser(userId);\n\n  // Send email saying we are going to pause the monitors\n  // The task has just been created we don't double check if the user has logged in :scary:\n  // send First email\n  // TODO: Send email\n\n  if (user.email) {\n    await sendBatchEmailHtml([\n      {\n        to: user.email,\n        subject: \"Your OpenStatus monitors will be paused in 14 days\",\n        from: \"Thibault From OpenStatus <thibault@notifications.openstatus.dev>\",\n        reply_to: \"thibault@openstatus.dev\",\n        html: monitorDeactivationEmail({\n          date: new Date(\n            new Date().setDate(new Date().getDate() + 14),\n          ).toDateString(),\n        }),\n      },\n    ]);\n\n    await CreateTask({\n      parent,\n      client: client,\n      step: \"3days\",\n      userId: user.id,\n      initialRun: workFlowRunTimestamp,\n    });\n  }\n}\n\nexport async function Step3Days(userId: number, workFlowRunTimestamp: number) {\n  // check if user has connected\n  const hasConnected = await hasUserLoggedIn({\n    userId,\n    date: new Date(workFlowRunTimestamp),\n  });\n\n  if (hasConnected) {\n    //\n    await redis.srem(\"workflow:users\", userId);\n    return;\n  }\n\n  const user = await getUser(userId);\n\n  if (user.email) {\n    await sendBatchEmailHtml([\n      {\n        to: user.email,\n        subject: \"Your OpenStatus monitors will be paused in 3 days\",\n        from: \"Thibault From OpenStatus <thibault@notifications.openstatus.dev>\",\n        reply_to: \"thibault@openstatus.dev\",\n        html: monitorDeactivationEmail({\n          date: new Date(\n            new Date().setDate(new Date().getDate() + 3),\n          ).toDateString(),\n        }),\n      },\n    ]);\n  }\n\n  // Send second email\n  //TODO: Send email\n  // Let's schedule the next task\n  await CreateTask({\n    client,\n    parent,\n    step: \"paused\",\n    userId,\n    initialRun: workFlowRunTimestamp,\n  });\n}\n\nexport async function StepPaused(userId: number, workFlowRunTimestamp: number) {\n  const hasConnected = await hasUserLoggedIn({\n    userId,\n    date: new Date(workFlowRunTimestamp),\n  });\n  if (!hasConnected) {\n    // sendSecond pause email\n    const users = await db\n      .select({\n        userId: schema.user.id,\n        email: schema.user.email,\n        workspaceId: schema.workspace.id,\n      })\n      .from(user)\n      .innerJoin(session, eq(schema.user.id, schema.session.userId))\n      .innerJoin(\n        schema.usersToWorkspaces,\n        eq(schema.user.id, schema.usersToWorkspaces.userId),\n      )\n      .innerJoin(\n        schema.workspace,\n        eq(schema.usersToWorkspaces.workspaceId, schema.workspace.id),\n      )\n      .where(\n        and(\n          or(isNull(schema.workspace.plan), eq(schema.workspace.plan, \"free\")),\n          eq(schema.user.id, userId),\n        ),\n      )\n      .get();\n    // We should only have one user :)\n    if (!users) {\n      console.error(`No user found for ${userId}`);\n      return;\n    }\n\n    await db\n      .update(schema.monitor)\n      .set({ active: false })\n      .where(eq(schema.monitor.workspaceId, users.workspaceId));\n    // Send last email with pause monitor\n  }\n\n  const currentUser = await getUser(userId);\n  // TODO: Send email\n  // Remove user for workflow\n\n  if (currentUser.email) {\n    await sendBatchEmailHtml([\n      {\n        to: currentUser.email,\n        subject: \"Your monitors have been paused\",\n        from: \"Thibault From OpenStatus <thibault@notifications.openstatus.dev>\",\n        reply_to: \"thibault@openstatus.dev\",\n        html: monitorPausedEmail(),\n      },\n    ]);\n  }\n  await redis.srem(\"workflow:users\", userId);\n}\n\nasync function hasUserLoggedIn({\n  userId,\n  date,\n}: {\n  userId: number;\n  date: Date;\n}) {\n  const userResult = await db\n    .select({ lastSession: schema.session.expires })\n    .from(schema.session)\n    .where(eq(schema.session.userId, userId))\n    .orderBy(desc(schema.session.expires));\n\n  if (userResult.length === 0) {\n    return false;\n  }\n  const user = userResult[0];\n  if (user.lastSession === null) {\n    return false;\n  }\n  return user.lastSession > date;\n}\n\nfunction CreateTask({\n  parent,\n  client,\n  step,\n  userId,\n  initialRun,\n}: {\n  parent: string;\n  client: CloudTasksClient;\n  step: z.infer<typeof workflowStepSchema>;\n  userId: number;\n  initialRun: number;\n}) {\n  const url = `https://openstatus-workflows.fly.dev/cron/monitors/${step}?userId=${userId}&initialRun=${initialRun}`;\n  const timestamp = getScheduledTime(step);\n  const newTask: google.cloud.tasks.v2beta3.ITask = {\n    httpRequest: {\n      headers: {\n        \"Content-Type\": \"application/json\", // Set content type to ensure compatibility your application's request parsing\n        Authorization: `${env().CRON_SECRET}`,\n      },\n      httpMethod: \"GET\",\n      url,\n    },\n    scheduleTime: {\n      seconds: timestamp,\n    },\n  };\n\n  const request = { parent: parent, task: newTask };\n  return client.createTask(request);\n}\n\nfunction getScheduledTime(step: z.infer<typeof workflowStepSchema>) {\n  switch (step) {\n    case \"14days\":\n      // let's triger it now\n      return new Date().getTime() / 1000;\n    case \"3days\":\n      // it's 11 days after the 14 days\n      return new Date().setDate(new Date().getDate() + 11) / 1000;\n    case \"paused\":\n      // it's 3 days after the 3 days step\n      return new Date().setDate(new Date().getDate() + 3) / 1000;\n    default:\n      throw new Error(\"Invalid step\");\n  }\n}\n\nexport const workflowStep = [\"14days\", \"3days\", \"paused\"] as const;\nexport const workflowStepSchema = z.enum(workflowStep);\n\nasync function getUser(userId: number) {\n  const currentUser = await db\n    .select()\n    .from(user)\n    .where(eq(schema.user.id, userId))\n    .get();\n\n  if (!currentUser) {\n    throw new Error(\"User not found\");\n  }\n  if (!currentUser.email) {\n    throw new Error(\"User email not found\");\n  }\n  return currentUser;\n}\n"
  },
  {
    "path": "apps/workflows/src/env.ts",
    "content": "import { z } from \"zod\";\n\nexport const env = () =>\n  z\n    .object({\n      NODE_ENV: z.string().prefault(\"development\"),\n      PORT: z.coerce.number().prefault(3000),\n      GCP_PROJECT_ID: z.string().prefault(\"\"),\n      GCP_CLIENT_EMAIL: z.string().prefault(\"\"),\n      GCP_PRIVATE_KEY: z.string().prefault(\"\"),\n      GCP_LOCATION: z.string().prefault(\"europe-west1\"),\n      CRON_SECRET: z.string().prefault(\"\"),\n      SITE_URL: z.string().prefault(\"http://localhost:3000\"),\n      DATABASE_URL: z.string().prefault(\"http://localhost:8080\"),\n      DATABASE_AUTH_TOKEN: z.string().prefault(\"\"),\n      RESEND_API_KEY: z.string().prefault(\"\"),\n      TINY_BIRD_API_KEY: z.string().prefault(\"\"),\n      QSTASH_TOKEN: z.string().prefault(\"\"),\n      SCREENSHOT_SERVICE_URL: z.string().prefault(\"\"),\n      TWILLIO_AUTH_TOKEN: z.string().prefault(\"\"),\n      TWILLIO_ACCOUNT_ID: z.string().prefault(\"\"),\n      SENTRY_DSN: z.string().prefault(\"\"),\n      AXIOM_TOKEN: z.string().prefault(\"\"),\n      AXIOM_DATASET: z.string().prefault(\"\"),\n    })\n    .parse(process.env);\n"
  },
  {
    "path": "apps/workflows/src/incident/index.ts",
    "content": "import { schema } from \"@openstatus/db\";\nimport { and, eq, inArray, isNull, ne } from \"drizzle-orm\";\nimport { Hono } from \"hono\";\nimport { env } from \"../env\";\nimport { db } from \"../lib/db\";\n\nexport const incidentRoute = new Hono({ strict: false });\n\nincidentRoute.use(\"*\", async (c, next) => {\n  if (c.req.header(\"authorization\") !== env().CRON_SECRET) {\n    return c.text(\"Unauthorized\", 401);\n  }\n\n  return next();\n});\n\nincidentRoute.get(\"/cleanup\", async (c) => {\n  // Find monitors that have unresolved incidents but are active\n  const unresolvedIncidentMonitorIds = db\n    .select({ monitorId: schema.incidentTable.monitorId })\n    .from(schema.incidentTable)\n    .where(isNull(schema.incidentTable.resolvedAt));\n\n  const activeMonitorsWithUnresolvedIncidents = await db\n    .select({ id: schema.monitor.id })\n    .from(schema.monitor)\n    .where(\n      and(\n        inArray(schema.monitor.id, unresolvedIncidentMonitorIds),\n        eq(schema.monitor.active, true),\n        ne(schema.monitor.status, \"error\"),\n      ),\n    )\n    .all();\n\n  const monitorIds = activeMonitorsWithUnresolvedIncidents.map((m) => m.id);\n\n  if (monitorIds.length === 0) {\n    return c.json({ status: \"ok\", updated: 0 });\n  }\n\n  // Update incidents for these monitors: set resolvedAt to now and autoResolved to true\n  const result = await db\n    .update(schema.incidentTable)\n    .set({\n      resolvedAt: new Date(),\n      autoResolved: true,\n    })\n    .where(\n      and(\n        inArray(schema.incidentTable.monitorId, monitorIds),\n        isNull(schema.incidentTable.resolvedAt),\n      ),\n    )\n    .returning({ id: schema.incidentTable.id });\n\n  return c.json({ status: \"ok\", updated: result.length });\n});\n"
  },
  {
    "path": "apps/workflows/src/index.ts",
    "content": "import { AsyncLocalStorage } from \"node:async_hooks\";\n// import * as Sentry from \"@sentry/node\";\nimport { sentry } from \"@hono/sentry\";\nimport {\n  configure,\n  getConsoleSink,\n  getLogger,\n  jsonLinesFormatter,\n  withContext,\n} from \"@logtape/logtape\";\nimport { getOpenTelemetrySink } from \"@logtape/otel\";\n\n// import { getSentrySink } from \"@logtape/sentry\";\nimport { Hono } from \"hono\";\nimport { showRoutes } from \"hono/dev\";\nimport { requestId } from \"hono/request-id\";\n// import { logger } from \"hono/logger\";\nimport { checkerRoute } from \"./checker\";\nimport { cronRouter } from \"./cron\";\nimport { env } from \"./env\";\n\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { ATTR_DEPLOYMENT_ENVIRONMENT_NAME } from \"@opentelemetry/semantic-conventions/incubating\";\nimport { incidentRoute } from \"./incident\";\n\nconst { NODE_ENV, PORT } = env();\n\nexport type Env = {\n  Variables: {\n    event: Record<string, unknown>;\n  };\n};\n\n/**\n * Tail sampling strategy based on loggingsucks.com best practices\n * Makes sampling decisions post-request completion to capture:\n * - All errors (5xx status codes, explicit errors)\n * - Slow requests (above p99 threshold)\n * - Client errors (4xx) at higher rate than successful requests\n * - Random sample of remaining successful, fast requests\n */\nfunction shouldSample(event: Record<string, unknown>): boolean {\n  const statusCode = event.status_code as number | undefined;\n  const durationMs = event.duration_ms as number | undefined;\n\n  // Always capture: server errors\n  if (statusCode && statusCode >= 500) return true;\n\n  // Always capture: explicit errors\n  if (event.error) return true;\n\n  // Always capture: slow requests (above p99 - 2s threshold)\n  if (durationMs && durationMs > 2000) return true;\n\n  // Higher sampling for client errors (4xx) - 50%\n  if (statusCode && statusCode >= 400 && statusCode < 500) {\n    return true;\n  }\n\n  // Random sample successful, fast requests at 20%\n  return Math.random() < 0.2;\n}\n\nconst defaultLogger = getOpenTelemetrySink({\n  serviceName: \"openstatus-workflows\",\n  otlpExporterConfig: {\n    url: \"https://eu-central-1.aws.edge.axiom.co/v1/logs\",\n    headers: {\n      Authorization: `Bearer ${env().AXIOM_TOKEN}`,\n      \"X-Axiom-Dataset\": env().AXIOM_DATASET,\n    },\n  },\n  additionalResource: resourceFromAttributes({\n    [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: env().NODE_ENV,\n  }),\n});\n\nawait configure({\n  sinks: {\n    console: getConsoleSink({ formatter: jsonLinesFormatter }),\n    // sentry: getSentrySink(),\n    otel: defaultLogger,\n  },\n  loggers: [\n    {\n      category: \"workflow\",\n      lowestLevel: \"debug\",\n      sinks: [\"console\"],\n    },\n    {\n      category: \"workflow-otel\",\n      lowestLevel: \"info\",\n      sinks: [\"otel\"],\n    },\n  ],\n  contextLocalStorage: new AsyncLocalStorage(),\n});\n\nconst logger = getLogger([\"workflow\"]);\nconst otelLogger = getLogger([\"workflow-otel\"]);\n\nconst app = new Hono<Env>({ strict: false });\n\napp.use(\"*\", requestId());\n\napp.use(\"*\", sentry({ dsn: env().SENTRY_DSN }));\n\napp.use(\"*\", async (c, next) => {\n  const requestId = c.get(\"requestId\");\n  const startTime = Date.now();\n\n  const event: Record<string, unknown> = {\n    timestamp: new Date().toISOString(),\n  };\n  c.set(\"event\", event);\n\n  await withContext(\n    {\n      request_id: requestId,\n      method: c.req.method,\n      url: c.req.url,\n      user_agent: c.req.header(\"User-Agent\"),\n      // ipAddress: c.req.header(\"CF-Connecting-IP\") || c.req.header(\"X-Forwarded-For\")\n    },\n    async () => {\n      // Build wide event context at request start\n      event.request_id = requestId;\n      event.method = c.req.method;\n      event.path = c.req.path;\n      event.url = c.req.url;\n      event.user_agent = c.req.header(\"User-Agent\");\n      event.content_type = c.req.header(\"Content-Type\");\n      event.cf_ray = c.req.header(\"CF-Ray\");\n      event.cf_connecting_ip = c.req.header(\"CF-Connecting-IP\");\n\n      await next();\n\n      const duration = Date.now() - startTime;\n\n      event.status_code = c.res.status;\n      if (c.error) {\n        event.outcome = \"error\";\n        event.error = {\n          type: c.error.name,\n          message: c.error.message,\n          stack: c.error.stack,\n        };\n      } else {\n        event.outcome = \"success\";\n      }\n      event.duration_ms = duration;\n      // Emit canonical log line with all context (wide event pattern)\n      if (shouldSample(event)) {\n        otelLogger.info(\"request\", event);\n      }\n      logger.debug(\"Request completed\", {\n        status_code: c.res.status,\n        duration_ms: duration,\n        request_id: requestId,\n      });\n    },\n  );\n});\n\napp.onError((err, c) => {\n  logger.error(\"Unhandled request error\", {\n    error_name: err.name,\n    error_message: err.message,\n    error_stack: err.stack,\n    method: c.req.method,\n    path: c.req.path,\n    url: c.req.url,\n    request_id: c.get(\"requestId\"),\n  });\n  c.get(\"sentry\").captureException(err);\n\n  return c.json({ error: \"Internal server error\" }, 500);\n});\n\napp.get(\"/\", (c) => c.text(\"workflows\", 200));\n\n/**\n * Ping Pong\n */\napp.get(\"/ping\", (c) => c.json({ ping: \"pong\" }, 200));\n\n/**\n * Cron Routes\n */\napp.route(\"/cron\", cronRouter);\n\napp.route(\"/\", checkerRoute);\n\napp.route(\"/incident\", incidentRoute);\nif (NODE_ENV === \"development\") {\n  showRoutes(app, { verbose: true, colorize: true });\n}\n\nlogger.info(\"Starting server\", { port: PORT, environment: NODE_ENV });\n\nconst server = { port: PORT, fetch: app.fetch };\n\nexport default server;\n"
  },
  {
    "path": "apps/workflows/src/lib/db.ts",
    "content": "import { drizzle } from \"drizzle-orm/libsql\";\n\nimport { createClient } from \"@libsql/client\";\nimport { schema } from \"@openstatus/db\";\nimport { env } from \"../env\";\n\nconst file =\n  env().NODE_ENV === \"development\" ? \"./dev.db\" : \"///app/data/replica.db\";\nconst client = createClient({\n  url: `file:${file}`,\n  syncUrl: env().DATABASE_URL,\n  authToken: env().DATABASE_AUTH_TOKEN,\n  syncInterval: 60,\n});\n\nexport const db = drizzle({\n  client: client,\n  schema,\n});\n"
  },
  {
    "path": "apps/workflows/src/scripts/tinybird.ts",
    "content": "import { db, eq } from \"@openstatus/db\";\nimport { type WorkspacePlan, workspace } from \"@openstatus/db/src/schema\";\nimport { env } from \"../env\";\n\nimport readline from \"node:readline\";\n\n// Function to prompt user for confirmation\nconst askConfirmation = async (question: string): Promise<boolean> => {\n  const rl = readline.createInterface({\n    input: process.stdin,\n    output: process.stdout,\n  });\n\n  return new Promise((resolve) => {\n    rl.question(`${question} (y/n): `, (answer) => {\n      rl.close();\n      resolve(answer.trim().toLowerCase() === \"y\");\n    });\n  });\n};\n\n/**\n * Calculates the unix timestamp in milliseconds for a given number of days in the past.\n * @param days The number of days to subtract from the current date.\n * @returns The calculated unix timestamp in milliseconds.\n */\nfunction calculatePastTimestamp(days: number) {\n  const date = new Date();\n  date.setDate(date.getDate() - days);\n  const timestamp = date.getTime();\n  console.log(`${days}d back: ${timestamp}`);\n  return timestamp;\n}\n\n/**\n * Get the array of workspace IDs for a given plan.\n * @param plan The plan to filter by.\n * @returns The array of workspace IDs.\n */\nasync function getWorkspaceIdsByPlan(plan: WorkspacePlan) {\n  const workspaces = await db\n    .select()\n    .from(workspace)\n    .where(eq(workspace.plan, plan))\n    .all();\n  const workspaceIds = workspaces.map((w) => w.id);\n  console.log(`${plan}: ${workspaceIds}`);\n  return workspaceIds;\n}\n\n/**\n *\n * @param timestamp  timestamp to delete logs before (in milliseconds)\n * @param workspaceIds array of workspace IDs to delete logs for\n * @param reverse allows to NOT delete the logs for the given workspace IDs\n * @returns\n */\nasync function deleteLogs(\n  timestamp: number,\n  workspaceIds: number[],\n  reverse = false,\n) {\n  const response = await fetch(\n    \"https://api.tinybird.co/v0/datasources/ping_response__v8/delete\",\n    {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        Authorization: `Bearer ${env().TINY_BIRD_API_KEY}`,\n      },\n      body: new URLSearchParams({\n        delete_condition: `timestamp <= ${timestamp} AND ${reverse ? \"NOT\" : \"\"} arrayExists(x -> x IN (${workspaceIds.join(\", \")}), [workspaceId])`,\n      }),\n    },\n  );\n  const json = await response.json();\n  console.log(json);\n\n  return json;\n}\n\nasync function main() {\n  // check if the script is running in production\n  console.log(`DATABASE_URL: ${env().DATABASE_URL}`);\n\n  const isConfirmed = await askConfirmation(\n    \"Are you sure you want to run this script?\",\n  );\n\n  if (!isConfirmed) {\n    console.log(\"Script execution cancelled.\");\n    return;\n  }\n\n  const lastTwoWeeks = calculatePastTimestamp(14);\n  const lastThreeMonths = calculatePastTimestamp(90);\n  const lastYear = calculatePastTimestamp(365);\n  // const _lastTwoYears = calculatePastTimestamp(730);\n\n  const starters = await getWorkspaceIdsByPlan(\"starter\");\n  const teams = await getWorkspaceIdsByPlan(\"team\");\n  // const pros = await getWorkspaceIdsByPlan(\"pro\");\n\n  // all other workspaces, we need to 'reverse' the deletion here to NOT include those workspaces\n  const rest = [...starters, ...teams];\n\n  deleteLogs(lastTwoWeeks, rest, true);\n  deleteLogs(lastThreeMonths, starters);\n  deleteLogs(lastYear, teams);\n  // deleteLogs(lastYear, pros);\n}\n\n/**\n * REMINDER: do it manually (to avoid accidental deletion on dev mode)\n * Within the app/workflows folder, run the following command:\n * $ bun src/scripts/tinybird.ts\n */\n\n// main().catch(console.error);\n"
  },
  {
    "path": "apps/workflows/src/utils/audit-log.ts",
    "content": "import { AuditLog, Tinybird } from \"@openstatus/tinybird\";\n\nimport { env } from \"../env\";\n\nconst tb = new Tinybird({ token: env().TINY_BIRD_API_KEY });\n\nexport const checkerAudit = new AuditLog({ tb });\n"
  },
  {
    "path": "apps/workflows/start.sh",
    "content": "#!/bin/sh\nset -e\n\necho \"Running database migrations...\"\ncd /app/packages/db && bun src/migrate.mts\n\necho \"Starting workflows service...\"\ncd /app/apps/workflows\nexec /app/apps/workflows/app\n"
  },
  {
    "path": "apps/workflows/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"hono/jsx\",\n    \"lib\": [\"ES2022\"],\n    \"module\": \"ESNext\",\n    \"target\": \"ESNext\",\n    \"moduleResolution\": \"bundler\"\n  }\n}\n"
  },
  {
    "path": "biome.jsonc",
    "content": "{\n  \"$schema\": \"https://biomejs.dev/schemas/1.6.2/schema.json\",\n  \"files\": {\n    \"ignore\": [\n      \"packages/ui/src/components/*.tsx\",\n      \"packages/ui/src/components/*.ts\",\n      \"apps/dashboard/src/scripts/*.ts\",\n      \"packages/proto/gen\",\n      \".devbox\"\n    ]\n  },\n  \"linter\": {\n    \"enabled\": true,\n    \"rules\": {\n      \"recommended\": true,\n      \"suspicious\": {\n        \"noArrayIndexKey\": \"off\"\n      },\n      \"performance\": {\n        \"noAccumulatingSpread\": \"warn\"\n      },\n      \"complexity\": {\n        \"noForEach\": \"off\"\n      },\n      \"a11y\": {\n        \"noSvgWithoutTitle\": \"off\",\n        \"useKeyWithClickEvents\": \"off\"\n      },\n      \"correctness\": {\n        \"noUnusedVariables\": \"warn\",\n        \"noUnusedImports\": \"error\",\n        \"noSwitchDeclarations\": \"off\"\n      },\n      \"nursery\": {\n        \"useSortedClasses\": \"warn\"\n      }\n    },\n    \"ignore\": [\n      \"node_modules\",\n      \".next\",\n      \"dist\",\n      \".wrangler\",\n      \".react-email\",\n      \".content-collections\",\n      \"meta\",\n      \"*.astro\",\n      \".astro\"\n    ]\n  },\n  \"formatter\": {\n    \"indentStyle\": \"space\",\n    \"indentWidth\": 2,\n    \"enabled\": true,\n    \"lineWidth\": 80,\n    \"ignore\": [\n      \"node_modules\",\n      \".next\",\n      \"dist\",\n      \".nuxt\",\n      \".wrangler\",\n      \".react-email\",\n      \".content-collections\",\n      \"meta\",\n      \"*.astro\",\n      \".astro\"\n    ]\n  },\n  \"organizeImports\": {\n    \"enabled\": true,\n    \"ignore\": [\n      \"node_modules\",\n      \".next\",\n      \"dist\",\n      \".nuxt\",\n      \".wrangler\",\n      \".react-email\",\n      \".content-collections\",\n      \"*.astro\",\n      \".astro\"\n    ]\n  }\n}\n"
  },
  {
    "path": "bunfig.toml",
    "content": "[install]\nlinker = \"hoisted\"\n"
  },
  {
    "path": "config.openstatus.yaml",
    "content": "tests:\n  ids:\n    - 1\n    - 771\n    - 2662\n"
  },
  {
    "path": "coolify-deployment.yaml",
    "content": "# Coolify Deployment Configuration\n# This file uses environment variables that can be configured directly in Coolify\n#\n# 🔧 REQUIRED: Set these environment variables in Coolify:\n# DATABASE_URL=http://libsql:8080\n# DATABASE_AUTH_TOKEN= (leave empty for local libsql)\n# AUTH_SECRET=your-32-character-secret\n# NEXT_PUBLIC_URL=https://your-domain.com\n# RESEND_API_KEY=your-resend-api-key\n#\n# 📚 Full Guide: See COOLIFY_ENVIRONMENT_GUIDE.md\n#\n# 🚀 Quick Import URL:\n# https://raw.githubusercontent.com/openstatusHQ/openstatus/main/coolify-deployment.yaml\n\nversion: \"3.8\"\n\n# Environment variable definitions with defaults\nx-common-variables: &common-env # Database Configuration\n  DATABASE_URL: ${DATABASE_URL:-http://libsql:8080}\n  DATABASE_AUTH_TOKEN: ${DATABASE_AUTH_TOKEN:-}\n\n  # Authentication\n  AUTH_SECRET: ${AUTH_SECRET:-default-secret-change-me}\n  NEXT_PUBLIC_URL: ${NEXT_PUBLIC_URL:-http://localhost:3002}\n  SELF_HOST: ${SELF_HOST:-true}\n\n  # OAuth Providers (Optional)\n  AUTH_GITHUB_ID: ${AUTH_GITHUB_ID:-}\n  AUTH_GITHUB_SECRET: ${AUTH_GITHUB_SECRET:-}\n  AUTH_GOOGLE_ID: ${AUTH_GOOGLE_ID:-}\n  AUTH_GOOGLE_SECRET: ${AUTH_GOOGLE_SECRET:-}\n\n  # Email Service\n  RESEND_API_KEY: ${RESEND_API_KEY:-}\n\n  # Analytics (Optional)\n  TINY_BIRD_API_KEY: ${TINY_BIRD_API_KEY:-}\n  TINYBIRD_URL: ${TINYBIRD_URL:-}\n\n  # Redis/Queue (Optional)\n  UPSTASH_REDIS_REST_URL: ${UPSTASH_REDIS_REST_URL:-http://localhost:6379}\n  UPSTASH_REDIS_REST_TOKEN: ${UPSTASH_REDIS_REST_TOKEN:-}\n  QSTASH_CURRENT_SIGNING_KEY: ${QSTASH_CURRENT_SIGNING_KEY:-}\n  QSTASH_NEXT_SIGNING_KEY: ${QSTASH_NEXT_SIGNING_KEY:-}\n  QSTASH_TOKEN: ${QSTASH_TOKEN:-}\n  QSTASH_URL: ${QSTASH_URL:-https://qstash.upstash.io/v1/publish/}\n\n  # Google Cloud (Optional)\n  GCP_PROJECT_ID: ${GCP_PROJECT_ID:-}\n  GCP_LOCATION: ${GCP_LOCATION:-}\n  GCP_CLIENT_EMAIL: ${GCP_CLIENT_EMAIL:-}\n  GCP_PRIVATE_KEY: ${GCP_PRIVATE_KEY:-}\n  CRON_SECRET: ${CRON_SECRET:-}\n\n  # API Keys (Optional)\n  UNKEY_API_ID: ${UNKEY_API_ID:-}\n  UNKEY_TOKEN: ${UNKEY_TOKEN:-}\n  SUPER_ADMIN_TOKEN: ${SUPER_ADMIN_TOKEN:-}\n  FLY_REGION: ${FLY_REGION:-self-hosted}\n\n  # Stripe (Optional)\n  STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}\n  STRIPE_WEBHOOK_SECRET_KEY: ${STRIPE_WEBHOOK_SECRET_KEY:-}\n  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:-}\n\n  # Vercel (Optional)\n  PROJECT_ID_VERCEL: ${PROJECT_ID_VERCEL:-}\n  TEAM_ID_VERCEL: ${TEAM_ID_VERCEL:-}\n  VERCEL_AUTH_BEARER_TOKEN: ${VERCEL_AUTH_BEARER_TOKEN:-}\n  BLOB_READ_WRITE_TOKEN: ${BLOB_READ_WRITE_TOKEN:-}\n\n  # Observability (Optional)\n  NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN:-}\n  SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN:-}\n  NEXT_PUBLIC_OPENPANEL_CLIENT_ID: ${NEXT_PUBLIC_OPENPANEL_CLIENT_ID:-}\n  OPENPANEL_CLIENT_SECRET: ${OPENPANEL_CLIENT_SECRET:-}\n  PAGERDUTY_APP_ID: ${PAGERDUTY_APP_ID:-}\n  SLACK_SUPPORT_WEBHOOK_URL: ${SLACK_SUPPORT_WEBHOOK_URL:-}\n  TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}\n\n  # External Services\n  OPENSTATUS_INGEST_URL: ${OPENSTATUS_INGEST_URL:-https://openstatus-private-location.fly.dev}\n  SCREENSHOT_SERVICE_URL: ${SCREENSHOT_SERVICE_URL:-}\n\n  # Development & Testing\n  TURBO_ENV_MODE: ${TURBO_ENV_MODE:-loose}\n  PLAYGROUND_UNKEY_API_KEY: ${PLAYGROUND_UNKEY_API_KEY:-}\n  WORKSPACES_LOOKBACK_30: ${WORKSPACES_LOOKBACK_30:-}\n  WORKSPACES_HIDE_URL: ${WORKSPACES_HIDE_URL:-}\n\n  # Common Settings\n  NODE_ENV: ${NODE_ENV:-production}\n  ENCRYPTION_KEY: ${ENCRYPTION_KEY:-default-encryption-key}\n\nx-service-variables: &service-env\n  <<: *common-env\n  PORT: ${PORT:-3000}\n\nnetworks:\n  openstatus:\n    driver: bridge\n    name: openstatus\n\nvolumes:\n  libsql-data:\n    name: openstatus-libsql-data\n  workflows-data:\n    name: openstatus-workflows-data\n\nservices:\n  # External Dependencies\n  libsql:\n    image: ghcr.io/tursodatabase/libsql-server:latest\n    container_name: openstatus-libsql\n    networks:\n      - openstatus\n    ports:\n      - \"8085:8080\"\n      - \"5001:5001\"\n    volumes:\n      - libsql-data:/var/lib/sqld\n    environment:\n      - SQLD_NODE=primary\n    healthcheck:\n      test:\n        [\n          \"CMD\",\n          \"sh\",\n          \"-c\",\n          'perl -e ''use IO::Socket::INET; exit(IO::Socket::INET->new(PeerAddr=>\"127.0.0.1:8080\",Timeout=>1) ? 0 : 1);''',\n        ]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n      start_period: 10s\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 512M\n        reservations:\n          memory: 256M\n\n  tinybird:\n    image: tinybirdco/tinybird-local:latest\n    container_name: openstatus-tinybird\n    networks:\n      - openstatus\n    ports:\n      - \"7181:7181\"\n    environment:\n      - COMPATIBILITY_MODE=1\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:7181/\"]\n      interval: 15s\n      timeout: 5s\n      retries: 5\n      start_period: 20s\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 1G\n        reservations:\n          memory: 512M\n\n  # Core OpenStatus Services\n  workflows:\n    image: ghcr.io/openstatushq/openstatus-workflows:latest\n    container_name: openstatus-workflows\n    networks:\n      - openstatus\n    ports:\n      - \"3000:3000\"\n    volumes:\n      - workflows-data:/app/data\n    environment:\n      - DATABASE_URL=${DATABASE_URL:-http://libsql:8080}\n      - DATABASE_AUTH_TOKEN=${DATABASE_AUTH_TOKEN:-}\n      - PORT=${WORKFLOWS_PORT:-3000}\n    depends_on:\n      libsql:\n        condition: service_healthy\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3000/ping\"]\n      interval: 15s\n      timeout: 10s\n      retries: 3\n      start_period: 30s\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 512M\n        reservations:\n          memory: 256M\n\n  server:\n    image: ghcr.io/openstatushq/openstatus-server:latest\n    container_name: openstatus-server\n    networks:\n      - openstatus\n    ports:\n      - \"3001:3000\"\n    environment:\n      - DATABASE_URL=${DATABASE_URL:-http://libsql:8080}\n      - DATABASE_AUTH_TOKEN=${DATABASE_AUTH_TOKEN:-}\n      - PORT=${SERVER_PORT:-3000}\n    depends_on:\n      workflows:\n        condition: service_healthy\n      libsql:\n        condition: service_healthy\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3000/ping\"]\n      interval: 15s\n      timeout: 10s\n      retries: 3\n      start_period: 30s\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 512M\n        reservations:\n          memory: 256M\n\n  private-location:\n    image: ghcr.io/openstatushq/openstatus-private-location:latest\n    container_name: openstatus-private-location\n    networks:\n      - openstatus\n    ports:\n      - \"8083:8080\"\n    environment:\n      - DB_URL=${DATABASE_URL:-http://libsql:8080}\n      - TINYBIRD_URL=${TINYBIRD_URL:-http://tinybird:7181}\n      - GIN_MODE=release\n      - PORT=${PRIVATE_LOCATION_PORT:-8080}\n    depends_on:\n      server:\n        condition: service_healthy\n    healthcheck:\n      test: [\"CMD\", \"wget\", \"--spider\", \"-q\", \"http://localhost:8080/health\"]\n      interval: 15s\n      timeout: 10s\n      retries: 3\n      start_period: 30s\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 256M\n        reservations:\n          memory: 128M\n\n  checker:\n    image: ghcr.io/openstatushq/openstatus-checker:latest\n    container_name: openstatus-checker\n    networks:\n      - openstatus\n    ports:\n      - \"8082:8080\"\n    environment:\n      - DATABASE_URL=${DATABASE_URL:-http://libsql:8080}\n      - DATABASE_AUTH_TOKEN=${DATABASE_AUTH_TOKEN:-}\n      - PORT=${CHECKER_PORT:-8080}\n    depends_on:\n      server:\n        condition: service_healthy\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8080/health\"]\n      interval: 15s\n      timeout: 10s\n      retries: 3\n      start_period: 30s\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 256M\n        reservations:\n          memory: 128M\n\n  dashboard:\n    image: ghcr.io/openstatushq/openstatus-dashboard:latest\n    container_name: openstatus-dashboard\n    networks:\n      - openstatus\n    ports:\n      - \"3002:3000\"\n    environment:\n      - DATABASE_URL=${DATABASE_URL:-http://libsql:8080}\n      - DATABASE_AUTH_TOKEN=${DATABASE_AUTH_TOKEN:-}\n      - PORT=${DASHBOARD_PORT:-3000}\n      - HOSTNAME=${DASHBOARD_HOSTNAME:-0.0.0.0}\n      - AUTH_TRUST_HOST=${DASHBOARD_AUTH_TRUST_HOST:-true}\n    depends_on:\n      workflows:\n        condition: service_healthy\n      libsql:\n        condition: service_healthy\n      server:\n        condition: service_healthy\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3000/\"]\n      interval: 15s\n      timeout: 10s\n      retries: 3\n      start_period: 45s\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 512M\n        reservations:\n          memory: 256M\n\n  status-page:\n    image: ghcr.io/openstatushq/openstatus-status-page:latest\n    container_name: openstatus-status-page\n    networks:\n      - openstatus\n    ports:\n      - \"3003:3000\"\n    environment:\n      - DATABASE_URL=${DATABASE_URL:-http://libsql:8080}\n      - DATABASE_AUTH_TOKEN=${DATABASE_AUTH_TOKEN:-}\n      - PORT=${STATUS_PAGE_PORT:-3000}\n      - HOSTNAME=${STATUS_PAGE_HOSTNAME:-0.0.0.0}\n      - AUTH_TRUST_HOST=${STATUS_PAGE_AUTH_TRUST_HOST:-true}\n    depends_on:\n      workflows:\n        condition: service_healthy\n      libsql:\n        condition: service_healthy\n      server:\n        condition: service_healthy\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3000/\"]\n      interval: 15s\n      timeout: 10s\n      retries: 3\n      start_period: 45s\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 512M\n        reservations:\n          memory: 256M\n\n# Coolify-specific labels for better management\nx-coolify-labels:\n  app: openstatus\n  version: \"1.0\"\n  environment: production\n"
  },
  {
    "path": "devbox.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/jetify-com/devbox/0.10.6/.schema/devbox.schema.json\",\n  \"packages\": [\"turso-cli@latest\", \"nodejs@22\", \"bun@latest\", \"sqld@latest\"],\n  \"env\": {\n    \"DEVBOX_COREPACK_ENABLED\": \"true\",\n    \"COREPACK_ENABLE_DOWNLOAD_PROMPT\": \"0\"\n  }\n}\n"
  },
  {
    "path": "docker-compose-lightweight.yaml",
    "content": "networks:\n    openstatus:\n        driver: bridge\n        name: openstatus\n\nvolumes:\n    libsql-data:\n        name: openstatus-libsql-data\n\nservices:\n    # External services\n    libsql:\n        container_name: openstatus-libsql\n        image: ghcr.io/tursodatabase/libsql-server:latest\n        networks:\n            - openstatus\n        ports:\n            - \"8080:8080\"\n            - \"5001:5001\"\n        volumes:\n            - libsql-data:/var/lib/sqld\n        environment:\n            - SQLD_NODE=primary\n        healthcheck:\n            test:\n                [\n                    \"CMD-SHELL\",\n                    'perl -e ''use IO::Socket::INET; exit(IO::Socket::INET->new(PeerAddr=>\"127.0.0.1:8080\",Timeout=>1) ? 0 : 1);''',\n                ]\n            interval: 10s\n            timeout: 5s\n            retries: 5\n            start_period: 10s\n        restart: unless-stopped\n\n    db-migrate:\n        container_name: openstatus-db-migrate\n        image: oven/bun:1.3.6\n        networks:\n            - openstatus\n        working_dir: /app/packages/db\n        volumes:\n            - .:/app\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n        command: [\"sh\", \"-c\", \"bun install && bun run migrate\"]\n        depends_on:\n            libsql:\n                condition: service_healthy\n        restart: \"no\"\n\n\n    dashboard:\n        container_name: openstatus-dashboard\n        build:\n            context: .\n            dockerfile: apps/dashboard/Dockerfile\n        image: openstatus/dashboard:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3000:3000\"\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n            - HOSTNAME=0.0.0.0\n            - AUTH_TRUST_HOST=true\n        depends_on:\n            db-migrate:\n                condition: service_completed_successfully\n            libsql:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 45s\n        restart: unless-stopped\n\n    status-page:\n        container_name: openstatus-status-page\n        build:\n            context: .\n            dockerfile: apps/status-page/Dockerfile\n        image: openstatus/status-page:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3001:3000\"\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n            - HOSTNAME=0.0.0.0\n            - AUTH_TRUST_HOST=true\n        depends_on:\n            db-migrate:\n                condition: service_completed_successfully\n            libsql:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 45s\n        restart: unless-stopped\n"
  },
  {
    "path": "docker-compose.github-packages.yaml",
    "content": "networks:\n    openstatus:\n        driver: bridge\n        name: openstatus\n\nvolumes:\n    libsql-data:\n        name: openstatus-libsql-data\n    workflows-data:\n        name: openstatus-workflows-data\n\nservices:\n    # External services\n    libsql:\n        container_name: openstatus-libsql\n        image: ghcr.io/tursodatabase/libsql-server:latest\n        networks:\n            - openstatus\n        ports:\n            - \"8080:8080\"\n            - \"5001:5001\"\n        volumes:\n            - libsql-data:/var/lib/sqld\n        environment:\n            - SQLD_NODE=primary\n        healthcheck:\n            test:\n                [\n                    \"CMD-SHELL\",\n                    'perl -e ''use IO::Socket::INET; exit(IO::Socket::INET->new(PeerAddr=>\"127.0.0.1:8080\",Timeout=>1) ? 0 : 1);''',\n                ]\n            interval: 10s\n            timeout: 5s\n            retries: 5\n            start_period: 10s\n        restart: unless-stopped\n\n    tinybird-local:\n        container_name: openstatus-tinybird\n        image: tinybirdco/tinybird-local:latest\n        platform: linux/amd64\n        networks:\n            - openstatus\n        ports:\n            - \"7181:7181\"\n        environment:\n            - COMPATIBILITY_MODE=1\n        healthcheck:\n            test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:7181/\"]\n            interval: 15s\n            timeout: 5s\n            retries: 5\n            start_period: 20s\n        restart: unless-stopped\n\n    # Internal Services - Using GitHub Packages\n    workflows:\n        container_name: openstatus-workflows\n        image: ghcr.io/openstatushq/openstatus-workflows:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3000:3000\"\n        volumes:\n            - workflows-data:/app/data\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n        depends_on:\n            libsql:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ping || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 30s\n        restart: unless-stopped\n\n    server:\n        container_name: openstatus-server\n        image: ghcr.io/openstatushq/openstatus-server:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3001:3000\"\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n        depends_on:\n            workflows:\n                condition: service_healthy\n            libsql:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ping || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 30s\n        restart: unless-stopped\n\n    private-location:\n        container_name: openstatus-private-location\n        image: ghcr.io/openstatushq/openstatus-private-location:latest\n        networks:\n            - openstatus\n        ports:\n            - \"8081:8080\"\n        env_file:\n            - .env.docker\n        environment:\n            - DB_URL=http://libsql:8080\n            - TINYBIRD_URL=http://tinybird-local:7181\n            - GIN_MODE=release\n            - PORT=8080\n        depends_on:\n            server:\n                condition: service_healthy\n        healthcheck:\n            test:\n                [\n                    \"CMD\",\n                    \"wget\",\n                    \"--spider\",\n                    \"-q\",\n                    \"http://localhost:8080/health\",\n                ]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 30s\n        restart: unless-stopped\n\n    checker:\n        container_name: openstatus-checker\n        image: ghcr.io/openstatushq/openstatus-checker:latest\n        networks:\n            - openstatus\n        ports:\n            - \"8082:8080\"\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=8080\n        depends_on:\n            server:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:8080/health || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 30s\n        restart: unless-stopped\n\n    dashboard:\n        container_name: openstatus-dashboard\n        image: ghcr.io/openstatushq/openstatus-dashboard:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3002:3000\"\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n            - HOSTNAME=0.0.0.0\n            - AUTH_TRUST_HOST=true\n        depends_on:\n            workflows:\n                condition: service_healthy\n            libsql:\n                condition: service_healthy\n            server:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 45s\n        restart: unless-stopped\n\n    status-page:\n        container_name: openstatus-status-page\n        image: ghcr.io/openstatushq/openstatus-status-page:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3003:3000\"\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n            - HOSTNAME=0.0.0.0\n            - AUTH_TRUST_HOST=true\n        depends_on:\n            workflows:\n                condition: service_healthy\n            libsql:\n                condition: service_healthy\n            server:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 45s\n        restart: unless-stopped\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "networks:\n    openstatus:\n        driver: bridge\n        name: openstatus\n\nvolumes:\n    libsql-data:\n        name: openstatus-libsql-data\n    workflows-data:\n        name: openstatus-workflows-data\n\nservices:\n    # External services\n    libsql:\n        container_name: openstatus-libsql\n        image: ghcr.io/tursodatabase/libsql-server:latest\n        networks:\n            - openstatus\n        ports:\n            - \"8080:8080\"\n            - \"5001:5001\"\n        volumes:\n            - libsql-data:/var/lib/sqld\n        environment:\n            - SQLD_NODE=primary\n        healthcheck:\n            test:\n                [\n                    \"CMD-SHELL\",\n                    'perl -e ''use IO::Socket::INET; exit(IO::Socket::INET->new(PeerAddr=>\"127.0.0.1:8080\",Timeout=>1) ? 0 : 1);''',\n                ]\n            interval: 10s\n            timeout: 5s\n            retries: 5\n            start_period: 10s\n        restart: unless-stopped\n\n    tinybird-local:\n        container_name: openstatus-tinybird\n        image: tinybirdco/tinybird-local:latest\n        platform: linux/amd64\n        networks:\n            - openstatus\n        ports:\n            - \"7181:7181\"\n        environment:\n            - COMPATIBILITY_MODE=1\n        healthcheck:\n            test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:7181/\"]\n            interval: 15s\n            timeout: 5s\n            retries: 5\n            start_period: 20s\n        restart: unless-stopped\n\n    # Internal Services\n    workflows:\n        container_name: openstatus-workflows\n        build:\n            context: .\n            dockerfile: apps/workflows/Dockerfile\n        image: openstatus/workflows:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3000:3000\"\n        volumes:\n            - workflows-data:/app/data\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n        depends_on:\n            libsql:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ping || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 30s\n        restart: unless-stopped\n\n    server:\n        container_name: openstatus-server\n        build:\n            context: .\n            dockerfile: apps/server/Dockerfile\n        image: openstatus/server:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3001:3000\"\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n        depends_on:\n            workflows:\n                condition: service_healthy\n            libsql:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ping || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 30s\n        restart: unless-stopped\n\n    private-location:\n        container_name: openstatus-private-location\n        build:\n            context: apps/private-location\n            dockerfile: Dockerfile\n        image: openstatus/private-location:latest\n        networks:\n            - openstatus\n        ports:\n            - \"8081:8080\"\n        env_file:\n            - .env.docker\n        environment:\n            - DB_URL=http://libsql:8080\n            - TINYBIRD_URL=http://tinybird-local:7181\n            - GIN_MODE=release\n            - PORT=8080\n        depends_on:\n            server:\n                condition: service_healthy\n        healthcheck:\n            test:\n                [\n                    \"CMD\",\n                    \"wget\",\n                    \"--spider\",\n                    \"-q\",\n                    \"http://localhost:8080/health\",\n                ]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 30s\n        restart: unless-stopped\n\n    dashboard:\n        container_name: openstatus-dashboard\n        build:\n            context: .\n            dockerfile: apps/dashboard/Dockerfile\n        image: openstatus/dashboard:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3002:3000\"\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n            - HOSTNAME=0.0.0.0\n            - AUTH_TRUST_HOST=true\n        depends_on:\n            workflows:\n                condition: service_healthy\n            libsql:\n                condition: service_healthy\n            server:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 45s\n        restart: unless-stopped\n\n    status-page:\n        container_name: openstatus-status-page\n        build:\n            context: .\n            dockerfile: apps/status-page/Dockerfile\n        image: openstatus/status-page:latest\n        networks:\n            - openstatus\n        ports:\n            - \"3003:3000\"\n        env_file:\n            - .env.docker\n        environment:\n            - DATABASE_URL=http://libsql:8080\n            - PORT=3000\n            - HOSTNAME=0.0.0.0\n            - AUTH_TRUST_HOST=true\n        depends_on:\n            workflows:\n                condition: service_healthy\n            libsql:\n                condition: service_healthy\n            server:\n                condition: service_healthy\n        healthcheck:\n            test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/ || exit 1\"]\n            interval: 15s\n            timeout: 10s\n            retries: 3\n            start_period: 45s\n        restart: unless-stopped\n"
  },
  {
    "path": "infra/openstatus.yaml",
    "content": "# yaml-language-server: $schema=https://www.openstatus.dev/schema.json\n\n\"openstatus\":\n  active: true\n  description: \"Our website \\U0001F310\"\n  frequency: 10m\n  kind: http\n  name: 'OpenStatus '\n  public: true\n  regions:\n  - ams\n  - iad\n  - jnb\n  - gru\n  - syd\n  - cdg\n  - bom\n  - sin\n  - fra\n  - lhr\n  - arn\n  - den\n  - dfw\n  - ord\n  - lax\n  - sjc\n  - ewr\n  - yyz\n  - koyeb_fra\n  - koyeb_par\n  - koyeb_sfo\n  - koyeb_sin\n  - koyeb_tyo\n  - koyeb_was\n  - railway_asia-southeast1-eqsg3a\n  - railway_europe-west4-drams3a\n  - railway_us-east4-eqdc4a\n  - railway_us-west2\n  request:\n    method: GET\n    url: https://www.openstatus.dev\n  retry: 3\n\"openstatus status page\":\n  active: true\n  description: \"We are monitoring our own Status Page  \\U0001F4DD\"\n  frequency: 10m\n  kind: http\n  name: OpenStatus  Status Page\n  public: true\n  regions:\n  - ams\n  - iad\n  - jnb\n  - gru\n  - syd\n  - bom\n  - sin\n  - fra\n  - lhr\n  - cdg\n  - arn\n  - den\n  - dfw\n  - ord\n  - lax\n  - sjc\n  - ewr\n  - yyz\n  - koyeb_fra\n  - koyeb_par\n  - koyeb_sfo\n  - koyeb_sin\n  - koyeb_tyo\n  - koyeb_was\n  - railway_asia-southeast1-eqsg3a\n  - railway_europe-west4-drams3a\n  - railway_us-east4-eqdc4a\n  - railway_us-west2\n\n  request:\n    method: GET\n    url: https://status.openstatus.dev/\n  retry: 3\n\"openstatus api server\":\n  active: true\n  description: \"Our API Server \\U0001F50C\"\n  frequency: 10m\n  kind: http\n  name: OpenStatus API\n  public: true\n  regions:\n  - ams\n  - iad\n  - jnb\n  - gru\n  - syd\n  - bom\n  - sin\n  - fra\n  - lhr\n  - cdg\n  - arn\n  - den\n  - dfw\n  - ord\n  - lax\n  - sjc\n  - ewr\n  - yyz\n  - koyeb_fra\n  - koyeb_par\n  - koyeb_sfo\n  - koyeb_sin\n  - koyeb_tyo\n  - koyeb_was\n  - railway_asia-southeast1-eqsg3a\n  - railway_europe-west4-drams3a\n  - railway_us-east4-eqdc4a\n  - railway_us-west2\n  request:\n    method: GET\n    url: https://api.openstatus.dev/ping\n  retry: 3\n\"openstat.us\":\n  active: true\n  frequency: 1m\n  kind: http\n  name: openstat.us\n  public: true\n  regions:\n  - ams\n  - iad\n  - syd\n  - gru\n  - sin\n  request:\n    method: GET\n    url: https://openstat.us\n  retry: 3\n\"openstatus astro status page\":\n  active: true\n  frequency: 1h\n  kind: http\n  name: OpenStatus Astro Status Page\n  public: true\n  regions:\n  - ams\n  - iad\n  - sin\n  - jnb\n  - syd\n  - gru\n  request:\n    method: GET\n    url: https://astro.openstat.us/\n  retry: 3\n\"openstatus tcp check\":\n  active: true\n  frequency: 1m\n  kind: tcp\n  name: 'OpenStatus TCP'\n  regions:\n  - ams\n  - iad\n  - jnb\n  - gru\n  - syd\n  - bom\n  - sin\n  - fra\n  - lhr\n  - cdg\n  - arn\n  - den\n  - dfw\n  - ord\n  - lax\n  - sjc\n  - ewr\n  - yyz\n  - koyeb_fra\n  - koyeb_par\n  - koyeb_sfo\n  - koyeb_sin\n  - koyeb_tyo\n  - koyeb_was\n  - railway_asia-southeast1-eqsg3a\n  - railway_europe-west4-drams3a\n  - railway_us-east4-eqdc4a\n  - railway_us-west2\n  request:\n    host: openstatus.dev\n    port: 443\n  retry: 3\n"
  },
  {
    "path": "knip.ts",
    "content": "/** @type {import('knip').KnipConfig} */\nconst config = {\n  workspaces: {\n    \"packages/shared\": {\n      includeEntryExports: true,\n    },\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"turbo run build\",\n    \"dev\": \"turbo run dev\",\n    \"env\": \"bun env.ts\",\n    \"lint\": \"biome lint .\",\n    \"format\": \"pnpm biome format . --write && pnpm biome check . --write \",\n    \"format:fix\": \"pnpm biome check --fix --unsafe .\",\n    \"lint:fix\": \"pnpm biome lint --write --unsafe .\",\n    \"lint:turbo\": \"turbo run lint\",\n    \"lint:oxc\": \"oxlint .\",\n    \"lint:oxc-type\": \"oxlint . --type-aware --fix --fix-suggestions\",\n    \"format:oxc\": \"oxfmt --check\",\n    \"dev:web\": \"turbo run dev --filter='./apps/web' --filter='./packages/db'\",\n    \"dev:status-page\": \"turbo run dev --filter='./apps/status-page' --filter='./packages/db'\",\n    \"dev:dashboard\": \"turbo run dev --filter='./apps/dashboard' --filter='./packages/db'\",\n    \"dx\": \"turbo run dx\",\n    \"tsc\": \"tsc\",\n    \"test\": \"turbo run test\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"1.8.3\",\n    \"@turbo/gen\": \"1.13.3\",\n    \"oxfmt\": \"0.20.0\",\n    \"oxlint\": \"1.36.0\",\n    \"oxlint-tsgolint\": \"0.10.1\",\n    \"@types/node\": \"24.0.8\",\n    \"knip\": \"5.75.1\",\n    \"turbo\": \"2.6.3\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"packageManager\": \"pnpm@10.26.0\",\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\"@sentry/cli\", \"@swc/core\", \"sharp\"]\n  },\n  \"name\": \"openstatus\",\n  \"workspaces\": [\"apps/*\", \"packages/**/*\", \"packages/config/*\"],\n  \"overrides\": {\n    \"@react-email/preview-server\": \"npm:next@16.0.10\"\n  }\n}\n"
  },
  {
    "path": "packages/analytics/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    OPENPANEL_CLIENT_SECRET: z.string(),\n  },\n  client: {\n    NEXT_PUBLIC_OPENPANEL_CLIENT_ID: z.string(),\n  },\n  clientPrefix: \"NEXT_PUBLIC_\",\n  runtimeEnv: {\n    OPENPANEL_CLIENT_SECRET: process.env.OPENPANEL_CLIENT_SECRET,\n    NEXT_PUBLIC_OPENPANEL_CLIENT_ID:\n      process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID,\n  },\n  skipValidation: true,\n});\n"
  },
  {
    "path": "packages/analytics/package.json",
    "content": "{\n  \"name\": \"@openstatus/analytics\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"ISC\",\n  \"author\": \"\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {},\n  \"dependencies\": {\n    \"@openpanel/sdk\": \"1.0.3\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"22.10.2\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/analytics/src/events.ts",
    "content": "export type EventProps = {\n  name: string;\n  channel: string;\n};\n\nexport const Events = {\n  CreateUser: {\n    name: \"user_created\",\n    channel: \"registration\",\n  },\n  SkipOnboarding: {\n    name: \"onboarding_skipped\",\n    channel: \"onboarding\",\n  },\n  CompleteOnboarding: {\n    name: \"onboarding_completed\",\n    channel: \"onboarding\",\n  },\n  SignInUser: {\n    name: \"user_signed_in\",\n    channel: \"login\",\n  },\n  SignOutUser: {\n    name: \"user_signed_out\",\n    channel: \"login\",\n  },\n  CreateMonitor: {\n    name: \"monitor_created\",\n    channel: \"monitor\",\n  },\n  UpdateMonitor: {\n    name: \"monitor_updated\",\n    channel: \"monitor\",\n  },\n  DeleteMonitor: {\n    name: \"monitor_deleted\",\n    channel: \"monitor\",\n  },\n  CloneMonitor: {\n    name: \"monitor_cloned\",\n    channel: \"monitor\",\n  },\n  TestMonitor: {\n    name: \"monitor_tested\",\n    channel: \"monitor\",\n  },\n  CreatePage: {\n    name: \"page_created\",\n    channel: \"page\",\n  },\n  UpdatePage: {\n    name: \"page_updated\",\n    channel: \"page\",\n  },\n  UpdatePageDomain: {\n    name: \"page_domain_updated\",\n    channel: \"page\",\n  },\n  DeletePage: {\n    name: \"page_deleted\",\n    channel: \"page\",\n  },\n  DeletePageComponent: {\n    name: \"page_component_deleted\",\n    channel: \"page\",\n  },\n  UpdatePageComponentOrder: {\n    name: \"page_component_order_updated\",\n    channel: \"page\",\n  },\n  SubscribePage: {\n    name: \"user_subscribed\",\n    channel: \"page\",\n  },\n  VerifySubscribePage: {\n    name: \"user_subscribe_verified\",\n    channel: \"page\",\n  },\n  ValidateEmailDomain: {\n    name: \"email_domain_validated\",\n    channel: \"page\",\n  },\n  CreateReport: {\n    name: \"report_created\",\n    channel: \"report\",\n  },\n  UpdateReport: {\n    name: \"report_updated\",\n    channel: \"report\",\n  },\n  DeleteReport: {\n    name: \"report_deleted\",\n    channel: \"report\",\n  },\n  CreateReportUpdate: {\n    name: \"report_update_created\",\n    channel: \"report\",\n  },\n  UpdateReportUpdate: {\n    name: \"report_update_updated\",\n    channel: \"report\",\n  },\n  DeleteReportUpdate: {\n    name: \"report_update_deleted\",\n    channel: \"report\",\n  },\n  CreateMaintenance: {\n    name: \"maintenance_created\",\n    channel: \"maintenance\",\n  },\n  UpdateMaintenance: {\n    name: \"maintenance_updated\",\n    channel: \"maintenance\",\n  },\n  DeleteMaintenance: {\n    name: \"maintenance_deleted\",\n    channel: \"maintenance\",\n  },\n  CreateNotification: {\n    name: \"notification_created\",\n    channel: \"notification\",\n  },\n  UpdateNotification: {\n    name: \"notification_updated\",\n    channel: \"notification\",\n  },\n  DeleteNotification: {\n    name: \"notification_deleted\",\n    channel: \"notification\",\n  },\n  AcknowledgeIncident: {\n    name: \"incident_acknowledged\",\n    channel: \"incident\",\n  },\n  ResolveIncident: {\n    name: \"incident_resolved\",\n    channel: \"incident\",\n  },\n  UpdateIncident: {\n    name: \"incident_updated\",\n    channel: \"incident\",\n  },\n  DeleteIncident: {\n    name: \"incident_deleted\",\n    channel: \"incident\",\n  },\n  InviteUser: {\n    name: \"user_invited\",\n    channel: \"team\",\n  },\n  DeleteInvite: {\n    name: \"invitation_deleted\",\n    channel: \"team\",\n  },\n  AcceptInvite: {\n    name: \"invitation_accepted\",\n    channel: \"team\",\n  },\n  RemoveUser: {\n    name: \"user_removed\",\n    channel: \"team\",\n  },\n  CreateAPI: {\n    name: \"api_key_created\",\n    channel: \"api_key\",\n  },\n  RevokeAPI: {\n    name: \"api_key_revoked\",\n    channel: \"api_key\",\n  },\n  UpdateWorkspace: {\n    name: \"workspace_updated\",\n    channel: \"workspace\",\n  },\n  AddFeature: {\n    name: \"feature_added\",\n    channel: \"billing\",\n  },\n  UpgradeWorkspace: {\n    name: \"workspace_upgraded\",\n    channel: \"billing\",\n  },\n  StripePortal: {\n    name: \"stripe_portal\",\n    channel: \"billing\",\n  },\n  DowngradeWorkspace: {\n    name: \"workspace_downgraded\",\n    channel: \"billing\",\n  },\n  GlobalSpeedChecker: {\n    name: \"global_speed_checker\",\n    channel: \"checker\",\n  },\n} as const satisfies Record<string, EventProps>;\n"
  },
  {
    "path": "packages/analytics/src/index.ts",
    "content": "export * from \"./events\";\nexport * from \"./server\";\nexport * from \"./utils\";\n"
  },
  {
    "path": "packages/analytics/src/server.ts",
    "content": "import { OpenPanel, type PostEventPayload } from \"@openpanel/sdk\";\nimport { env } from \"../env\";\nimport type { EventProps } from \"./events\";\n\nconst op = new OpenPanel({\n  clientId: env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID,\n  clientSecret: env.OPENPANEL_CLIENT_SECRET,\n});\n\nop.setGlobalProperties({\n  env: process.env.VERCEL_ENV || process.env.NODE_ENV || \"localhost\",\n  // app_version\n});\n\nexport type IdentifyProps = {\n  userId?: string;\n  fullName?: string | null;\n  email?: string;\n  workspaceId?: string;\n  plan?: \"free\" | \"starter\" | \"team\";\n  // headers from the request\n  location?: string;\n  userAgent?: string;\n};\n\nexport async function setupAnalytics(props: IdentifyProps) {\n  if (process.env.NODE_ENV !== \"production\") {\n    return noop();\n  }\n\n  if (props.location) {\n    op.api.addHeader(\"x-client-ip\", props.location);\n  }\n\n  if (props.userAgent) {\n    op.api.addHeader(\"user-agent\", props.userAgent);\n  }\n\n  if (props.userId) {\n    const [firstName, lastName] = props.fullName?.split(\" \") || [];\n    await op.identify({\n      profileId: props.userId,\n      email: props.email,\n      firstName: firstName,\n      lastName: lastName,\n      properties: {\n        workspaceId: props.workspaceId,\n        plan: props.plan,\n      },\n    });\n  }\n\n  return {\n    track: (opts: EventProps & PostEventPayload[\"properties\"]) => {\n      const { name, ...rest } = opts;\n      return op.track(name, rest);\n    },\n  };\n}\n\n/**\n * Noop analytics for development environment\n */\nasync function noop() {\n  return {\n    track: (\n      opts: EventProps & PostEventPayload[\"properties\"],\n    ): Promise<unknown> => {\n      return new Promise((resolve) => {\n        console.log(`>>> Track Noop Event: ${opts.name}`);\n        resolve(null);\n      });\n    },\n  };\n}\n"
  },
  {
    "path": "packages/analytics/src/utils.ts",
    "content": "export function parseInputToProps(\n  json: unknown,\n  eventProps?: string[],\n): Record<string, unknown> {\n  if (typeof json !== \"object\" || json === null) return {};\n\n  if (!eventProps) return {};\n\n  return eventProps.reduce(\n    (acc, prop) => {\n      if (prop in json) {\n        acc[prop] = json[prop as keyof typeof json];\n      }\n      return acc;\n    },\n    {} as Record<string, unknown>,\n  );\n}\n"
  },
  {
    "path": "packages/analytics/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"exclude\": [\"dist\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"target\": \"es2021\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"noUncheckedIndexedAccess\": true\n  },\n  \"include\": [\"**/*.ts\", \"env.ts\"]\n}\n"
  },
  {
    "path": "packages/api/env.ts",
    "content": "const file = Bun.file(\"./.env.test\");\nawait Bun.write(\"./.env\", file);\n"
  },
  {
    "path": "packages/api/index.ts",
    "content": "import type { inferRouterInputs, inferRouterOutputs } from \"@trpc/server\";\n\nimport type { AppRouter } from \"./src/root\";\n\nexport { createTRPCContext, createInnerTRPCContext } from \"./src/trpc\";\n\n// TODO: Maybe just export `createAction` instead of the whole `trpc` object?\nexport { t } from \"./src/trpc\";\n\nexport type { AppRouter } from \"./src/root\";\nexport { appRouter } from \"./src/root\";\n\n/**\n * Inference helpers for input types\n * @example type HelloInput = RouterInputs['example']['hello']\n **/\nexport type RouterInputs = inferRouterInputs<AppRouter>;\n\n/**\n * Inference helpers for output types\n * @example type HelloOutput = RouterOutputs['example']['hello']\n **/\nexport type RouterOutputs = inferRouterOutputs<AppRouter>;\n"
  },
  {
    "path": "packages/api/package.json",
    "content": "{\n  \"name\": \"@openstatus/api\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"env\": \"bun env.ts\"\n  },\n  \"dependencies\": {\n    \"@openstatus/analytics\": \"workspace:*\",\n    \"@openstatus/subscriptions\": \"workspace:*\",\n    \"@openstatus/assertions\": \"workspace:*\",\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/emails\": \"workspace:*\",\n    \"@openstatus/error\": \"workspace:*\",\n    \"@openstatus/importers\": \"workspace:*\",\n    \"@openstatus/notification-google-chat\": \"workspace:*\",\n    \"@openstatus/notification-grafana-oncall\": \"workspace:*\",\n    \"@openstatus/notification-telegram\": \"workspace:*\",\n    \"@openstatus/notification-twillio-whatsapp\": \"workspace:*\",\n    \"@openstatus/regions\": \"workspace:*\",\n    \"@openstatus/tinybird\": \"workspace:*\",\n    \"@openstatus/upstash\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:*\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"@trpc/client\": \"11.4.4\",\n    \"@trpc/server\": \"11.4.4\",\n    \"@unkey/api\": \"2.2.0\",\n    \"@vercel/blob\": \"0.23.3\",\n    \"date-fns\": \"3.6.0\",\n    \"nanoid\": \"5.0.7\",\n    \"nanoid-dictionary\": \"5.0.0-beta.1\",\n    \"next\": \"16.1.6\",\n    \"random-word-slugs\": \"0.1.7\",\n    \"stripe\": \"13.8.0\",\n    \"superjson\": \"2.2.2\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"bun-types\": \"1.0.8\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/api/src/edge.ts",
    "content": "import { blobRouter } from \"./router/blob\";\nimport { checkerRouter } from \"./router/checker\";\nimport { domainRouter } from \"./router/domain\";\nimport { feedbackRouter } from \"./router/feedback\";\nimport { importRouter } from \"./router/import\";\nimport { incidentRouter } from \"./router/incident\";\nimport { invitationRouter } from \"./router/invitation\";\nimport { maintenanceRouter } from \"./router/maintenance\";\nimport { memberRouter } from \"./router/member\";\nimport { monitorRouter } from \"./router/monitor\";\nimport { monitorTagRouter } from \"./router/monitorTag\";\nimport { notificationRouter } from \"./router/notification\";\nimport { pageRouter } from \"./router/page\";\nimport { pageComponentRouter } from \"./router/pageComponent\";\nimport { pageSubscriberRouter } from \"./router/pageSubscriber\";\nimport { privateLocationRouter } from \"./router/privateLocation\";\nimport { statusPageRouter } from \"./router/statusPage\";\nimport { statusReportRouter } from \"./router/statusReport\";\nimport { tinybirdRouter } from \"./router/tinybird\";\nimport { userRouter } from \"./router/user\";\nimport { workspaceRouter } from \"./router/workspace\";\nimport { createTRPCRouter } from \"./trpc\";\n\n// Deployed to /trpc/edge/**\nexport const edgeRouter = createTRPCRouter({\n  workspace: workspaceRouter,\n  monitor: monitorRouter,\n  page: pageRouter,\n  pageComponent: pageComponentRouter,\n  statusReport: statusReportRouter,\n  domain: domainRouter,\n  user: userRouter,\n  notification: notificationRouter,\n  invitation: invitationRouter,\n  incident: incidentRouter,\n  pageSubscriber: pageSubscriberRouter,\n  tinybird: tinybirdRouter,\n  monitorTag: monitorTagRouter,\n  maintenance: maintenanceRouter,\n  member: memberRouter,\n  checker: checkerRouter,\n  blob: blobRouter,\n  feedback: feedbackRouter,\n  statusPage: statusPageRouter,\n  privateLocation: privateLocationRouter,\n  import: importRouter,\n});\n"
  },
  {
    "path": "packages/api/src/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    STRIPE_SECRET_KEY: z.string(),\n    PROJECT_ID_VERCEL: z.string(),\n    TEAM_ID_VERCEL: z.string(),\n    VERCEL_AUTH_BEARER_TOKEN: z.string(),\n    TINY_BIRD_API_KEY: z.string(),\n    RESEND_API_KEY: z.string(),\n    CRON_SECRET: z.string(),\n    UNKEY_TOKEN: z.string(),\n    UNKEY_API_ID: z.string(),\n    SLACK_FEEDBACK_WEBHOOK_URL: z.string().optional(),\n  },\n\n  runtimeEnv: {\n    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,\n    PROJECT_ID_VERCEL: process.env.PROJECT_ID_VERCEL,\n    TEAM_ID_VERCEL: process.env.TEAM_ID_VERCEL,\n    VERCEL_AUTH_BEARER_TOKEN: process.env.VERCEL_AUTH_BEARER_TOKEN,\n    TINY_BIRD_API_KEY: process.env.TINY_BIRD_API_KEY,\n    RESEND_API_KEY: process.env.RESEND_API_KEY,\n    CRON_SECRET: process.env.CRON_SECRET,\n    UNKEY_TOKEN: process.env.UNKEY_TOKEN,\n    UNKEY_API_ID: process.env.UNKEY_API_ID,\n    SLACK_FEEDBACK_WEBHOOK_URL: process.env.SLACK_FEEDBACK_WEBHOOK_URL,\n  },\n  skipValidation: process.env.NODE_ENV === \"test\",\n});\n"
  },
  {
    "path": "packages/api/src/lambda.ts",
    "content": "import { apiKeyRouter } from \"./router/apiKey\";\nimport { emailRouter } from \"./router/email\";\nimport { integrationRouter } from \"./router/integration\";\nimport { stripeRouter } from \"./router/stripe\";\nimport { createTRPCRouter } from \"./trpc\";\n// Deployed to /trpc/lambda/**\nexport const lambdaRouter = createTRPCRouter({\n  stripeRouter: stripeRouter,\n  emailRouter: emailRouter,\n  apiKeyRouter: apiKeyRouter,\n  integrationRouter: integrationRouter,\n});\n\nexport { stripe } from \"./router/stripe/shared\";\n"
  },
  {
    "path": "packages/api/src/root.ts",
    "content": "import { edgeRouter } from \"./edge\";\nimport { lambdaRouter } from \"./lambda\";\nimport { mergeRouters } from \"./trpc\";\n\nexport const appRouter = mergeRouters(edgeRouter, lambdaRouter);\nexport type AppRouter = typeof appRouter;\n"
  },
  {
    "path": "packages/api/src/router/apiKey.ts",
    "content": "import { z } from \"zod\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { db, eq } from \"@openstatus/db\";\nimport { user, usersToWorkspaces, workspace } from \"@openstatus/db/src/schema\";\nimport { createApiKeySchema } from \"@openstatus/db/src/schema/api-keys/validation\";\n\nimport { TRPCError } from \"@trpc/server\";\nimport {\n  createApiKey as createCustomApiKey,\n  getApiKeys,\n  revokeApiKey,\n} from \"../service/apiKey\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const apiKeyRouter = createTRPCRouter({\n  create: protectedProcedure\n    .meta({ track: Events.CreateAPI })\n    .input(createApiKeySchema)\n    .mutation(async ({ input, ctx }) => {\n      // Verify user has access to the workspace\n      const allowedWorkspaces = await db\n        .select()\n        .from(usersToWorkspaces)\n        .innerJoin(user, eq(user.id, usersToWorkspaces.userId))\n        .innerJoin(workspace, eq(workspace.id, usersToWorkspaces.workspaceId))\n        .where(eq(user.id, ctx.user.id))\n        .all();\n\n      const allowedIds = allowedWorkspaces.map((i) => i.workspace.id);\n\n      if (!allowedIds.includes(ctx.workspace.id)) {\n        throw new TRPCError({\n          code: \"UNAUTHORIZED\",\n          message: \"Unauthorized\",\n        });\n      }\n\n      // Create the API key using the custom service\n      const { token, key } = await createCustomApiKey(\n        ctx.workspace.id,\n        ctx.user.id,\n        input.name,\n        input.description,\n        input.expiresAt,\n      );\n\n      // Return both the key details and the full token (one-time display)\n      return {\n        token,\n        key,\n      };\n    }),\n\n  revoke: protectedProcedure\n    .meta({ track: Events.RevokeAPI })\n    .input(z.object({ keyId: z.number() }))\n    .mutation(async ({ input, ctx }) => {\n      // Revoke the key with workspace ownership verification\n      const success = await revokeApiKey(input.keyId, ctx.workspace.id);\n\n      if (!success) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"API key not found or unauthorized\",\n        });\n      }\n\n      return;\n    }),\n\n  getAll: protectedProcedure.query(async ({ ctx }) => {\n    // Get all API keys for the workspace\n    const keys = await getApiKeys(ctx.workspace.id);\n\n    // Fetch user information for each key's creator\n    const keysWithUserInfo = await Promise.all(\n      keys.map(async (key) => {\n        const creator = await db\n          .select({\n            id: user.id,\n            email: user.email,\n            firstName: user.firstName,\n            lastName: user.lastName,\n          })\n          .from(user)\n          .where(eq(user.id, key.createdById))\n          .get();\n\n        return {\n          ...key,\n          createdBy: creator,\n        };\n      }),\n    );\n\n    return keysWithUserInfo;\n  }),\n});\n"
  },
  {
    "path": "packages/api/src/router/blob.ts",
    "content": "import { put } from \"@vercel/blob\";\nimport { z } from \"zod\";\n\nimport { TRPCError } from \"@trpc/server\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const blobRouter = createTRPCRouter({\n  upload: protectedProcedure\n    .input(\n      z.object({\n        filename: z.string().min(1),\n        // Base64 encoded string (without data: prefix)\n        file: z.string().min(1),\n      }),\n    )\n    .mutation(async (opts) => {\n      const { filename, file } = opts.input;\n\n      // If the client sent a data URL, strip the prefix\n      const base64 = file.includes(\"base64,\")\n        ? file.split(\"base64,\").pop()\n        : file;\n\n      if (!base64) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Invalid file\",\n        });\n      }\n\n      const buffer = Buffer.from(base64, \"base64\");\n\n      const blob = await put(`${opts.ctx.workspace.slug}/${filename}`, buffer, {\n        access: \"public\",\n      });\n\n      return blob;\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/checker.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport {\n  deserialize,\n  dnsRecords,\n  headerAssertion,\n  jsonBodyAssertion,\n  recordAssertion,\n  statusAssertion,\n  textBodyAssertion,\n} from \"@openstatus/assertions\";\nimport { and, db, eq } from \"@openstatus/db\";\nimport { monitor, selectMonitorSchema } from \"@openstatus/db/src/schema\";\nimport { monitorRegionSchema } from \"@openstatus/db/src/schema/constants\";\nimport {\n  type httpPayloadSchema,\n  type tpcPayloadSchema,\n  transformHeaders,\n} from \"@openstatus/utils\";\nimport { z } from \"zod\";\nimport { env } from \"../env\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nconst ABORT_TIMEOUT = 10000;\n\n// Input schemas\nconst httpTestInput = z.object({\n  url: z.url(),\n  method: z\n    .enum([\n      \"GET\",\n      \"HEAD\",\n      \"OPTIONS\",\n      \"POST\",\n      \"PUT\",\n      \"DELETE\",\n      \"PATCH\",\n      \"CONNECT\",\n      \"TRACE\",\n    ])\n    .prefault(\"GET\"),\n  headers: z.array(z.object({ key: z.string(), value: z.string() })).optional(),\n  body: z.string().optional(),\n  region: monitorRegionSchema.optional().prefault(\"ams\"),\n  assertions: z\n    .array(\n      z.discriminatedUnion(\"type\", [\n        statusAssertion,\n        headerAssertion,\n        textBodyAssertion,\n        jsonBodyAssertion,\n        recordAssertion,\n      ]),\n    )\n    .prefault([]),\n});\n\nconst tcpTestInput = z.object({\n  url: z.string(),\n  region: monitorRegionSchema.optional().prefault(\"ams\"),\n});\n\nconst dnsTestInput = z.object({\n  url: z.string(),\n  region: monitorRegionSchema.optional().prefault(\"ams\"),\n  assertions: z\n    .array(\n      z.discriminatedUnion(\"type\", [\n        recordAssertion,\n        statusAssertion,\n        headerAssertion,\n        textBodyAssertion,\n        jsonBodyAssertion,\n      ]),\n    )\n    .prefault([]),\n});\n\nexport const tcpOutput = z\n  .object({\n    state: z.literal(\"success\").prefault(\"success\"),\n    type: z.literal(\"tcp\").prefault(\"tcp\"),\n    requestId: z.number().optional(),\n    workspaceId: z.number().optional(),\n    monitorId: z.number().optional(),\n    timestamp: z.number(),\n    timing: z.object({\n      tcpStart: z.number(),\n      tcpDone: z.number(),\n    }),\n    error: z.string().optional(),\n    region: monitorRegionSchema,\n    latency: z.number().optional(),\n  })\n  .or(\n    z.object({\n      state: z.literal(\"error\").prefault(\"error\"),\n      message: z.string(),\n    }),\n  );\n\nexport const httpOutput = z\n  .object({\n    state: z.literal(\"success\").prefault(\"success\"),\n    type: z.literal(\"http\").prefault(\"http\"),\n    status: z.number(),\n    latency: z.number(),\n    headers: z.record(z.string(), z.string()),\n    timestamp: z.number(),\n    timing: z.object({\n      dnsStart: z.number(),\n      dnsDone: z.number(),\n      connectStart: z.number(),\n      connectDone: z.number(),\n      tlsHandshakeStart: z.number(),\n      tlsHandshakeDone: z.number(),\n      firstByteStart: z.number(),\n      firstByteDone: z.number(),\n      transferStart: z.number(),\n      transferDone: z.number(),\n    }),\n    body: z.string().optional().nullable(),\n    region: monitorRegionSchema,\n  })\n  .or(\n    z.object({\n      state: z.literal(\"error\").prefault(\"error\"),\n      message: z.string(),\n    }),\n  );\n\nexport const dnsOutput = z\n  .object({\n    state: z.literal(\"success\").prefault(\"success\"),\n    type: z.literal(\"dns\").prefault(\"dns\"),\n    records: z\n      .partialRecord(z.enum(dnsRecords), z.array(z.string()))\n      .prefault({}),\n    latency: z.number().optional(),\n    timestamp: z.number(),\n    region: monitorRegionSchema,\n  })\n  .or(\n    z.object({\n      state: z.literal(\"error\").prefault(\"error\"),\n      message: z.string(),\n    }),\n  );\n\nexport async function testHttp(input: z.infer<typeof httpTestInput>) {\n  // Reject requests to our own domain to avoid loops\n  if (input.url.includes(\"openstatus.dev\")) {\n    throw new TRPCError({\n      code: \"BAD_REQUEST\",\n      message: \"Self-requests are not allowed\",\n    });\n  }\n\n  try {\n    const res = await fetch(\n      `https://openstatus-checker.fly.dev/ping/${input.region}`,\n      {\n        method: \"POST\",\n        headers: {\n          Authorization: `Basic ${env.CRON_SECRET}`,\n          \"Content-Type\": \"application/json\",\n          \"fly-prefer-region\": input.region,\n        },\n        body: JSON.stringify({\n          url: input.url,\n          method: input.method,\n          headers: input.headers?.reduce(\n            (acc, { key, value }) => {\n              if (!key) return acc;\n              return { ...acc, [key]: value };\n            },\n            {} as Record<string, string>,\n          ),\n          body: input.body,\n        }),\n        signal: AbortSignal.timeout(ABORT_TIMEOUT),\n      },\n    );\n\n    const json = await res.json();\n    const result = httpOutput.safeParse(json);\n\n    if (!result.success) {\n      console.error(\n        `Checker HTTP test failed for ${input.url}:`,\n        result.error.message,\n      );\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message:\n          \"Checker response is not valid. Please try again. If the problem persists, please contact support.\",\n      });\n    }\n\n    if (result.data.state === \"error\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: result.data.message,\n      });\n    }\n\n    if (result.data.state === \"success\") {\n      const { body, headers, status } = result.data;\n\n      const assertions = deserialize(JSON.stringify(input.assertions)).map(\n        (assertion) =>\n          assertion.assert({\n            body: body ?? \"\",\n            header: headers ?? {},\n            status: status,\n          }),\n      );\n\n      if (assertions.some((assertion) => !assertion.success)) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: `Assertion error: ${\n            assertions.find((assertion) => !assertion.success)?.message\n          }`,\n        });\n      }\n\n      if (assertions.length === 0 && (status < 200 || status >= 300)) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: `Assertion error: The response status was not 2XX: ${status}.`,\n        });\n      }\n    }\n\n    return result.data;\n  } catch (error) {\n    console.error(\"Checker HTTP test failed\", error);\n    if (error instanceof TRPCError) {\n      throw error;\n    }\n\n    throw new TRPCError({\n      code: \"INTERNAL_SERVER_ERROR\",\n      message: error instanceof Error ? error.message : \"HTTP check failed\",\n    });\n  }\n}\n\nexport async function testTcp(input: z.infer<typeof tcpTestInput>) {\n  try {\n    const res = await fetch(\n      `https://openstatus-checker.fly.dev/tcp/${input.region}`,\n      {\n        method: \"POST\",\n        headers: {\n          Authorization: `Basic ${env.CRON_SECRET}`,\n          \"Content-Type\": \"application/json\",\n          \"fly-prefer-region\": input.region,\n        },\n        body: JSON.stringify({ uri: input.url }),\n        signal: AbortSignal.timeout(ABORT_TIMEOUT),\n      },\n    );\n\n    const json = await res.json();\n    const result = tcpOutput.safeParse(json);\n\n    if (!result.success) {\n      console.error(\n        `Checker TCP test failed for ${input.url}:`,\n        result.error.message,\n      );\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: `Checker response is not valid. Please try again. If the problem persists, please contact support. ${result.error.message}`,\n      });\n    }\n\n    if (result.data.state === \"error\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: result.data.message,\n      });\n    }\n\n    return result.data;\n  } catch (error) {\n    console.error(\"Checker TCP test failed\", error);\n    if (error instanceof TRPCError) {\n      throw error;\n    }\n\n    throw new TRPCError({\n      code: \"INTERNAL_SERVER_ERROR\",\n      message: \"TCP check failed\",\n    });\n  }\n}\n\nexport async function testDns(input: z.infer<typeof dnsTestInput>) {\n  try {\n    const res = await fetch(\n      `https://openstatus-checker.fly.dev/dns/${input.region}`,\n      {\n        method: \"POST\",\n        headers: {\n          Authorization: `Basic ${env.CRON_SECRET}`,\n          \"Content-Type\": \"application/json\",\n          \"fly-prefer-region\": input.region,\n        },\n        body: JSON.stringify({\n          uri: input.url,\n        }),\n        signal: AbortSignal.timeout(ABORT_TIMEOUT),\n      },\n    );\n\n    const json = await res.json();\n    const result = dnsOutput.safeParse(json);\n\n    if (!result.success) {\n      console.error(\n        `Checker DNS test failed for ${input.url}:`,\n        result.error.message,\n      );\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: `Checker response is not valid. Please try again. If the problem persists, please contact support. ${result.error.message}`,\n      });\n    }\n\n    if (result.data.state === \"error\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: result.data.message,\n      });\n    }\n\n    if (result.data.state === \"success\") {\n      const { records } = result.data;\n\n      const assertions = deserialize(JSON.stringify(input.assertions)).map(\n        (assertion) => assertion.assert({ records }),\n      );\n\n      if (assertions.some((assertion) => !assertion.success)) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: `Assertion error: ${\n            assertions.find((assertion) => !assertion.success)?.message\n          }`,\n        });\n      }\n    }\n\n    return result.data;\n  } catch (error) {\n    console.error(\"Checker DNS test failed\", error);\n    if (error instanceof TRPCError) {\n      throw error;\n    }\n\n    throw new TRPCError({\n      code: \"INTERNAL_SERVER_ERROR\",\n      message: \"DNS check failed\",\n    });\n  }\n}\n\nexport async function triggerChecker(\n  input: z.infer<typeof selectMonitorSchema>,\n) {\n  let payload:\n    | z.infer<typeof httpPayloadSchema>\n    | z.infer<typeof tpcPayloadSchema>\n    | null = null;\n\n  if (process.env.NODE_ENV !== \"production\") {\n    return;\n  }\n\n  const timestamp = Date.now();\n\n  if (input.jobType === \"http\") {\n    payload = {\n      workspaceId: String(input.workspaceId),\n      monitorId: String(input.id),\n      url: input.url,\n      method: input.method || \"GET\",\n      cronTimestamp: timestamp,\n      body: input.body,\n      headers: input.headers,\n      status: \"active\",\n      assertions: input.assertions ? JSON.parse(input.assertions) : null,\n      degradedAfter: input.degradedAfter,\n      timeout: input.timeout,\n      trigger: \"cron\",\n      otelConfig: input.otelEndpoint\n        ? {\n            endpoint: input.otelEndpoint,\n            headers: transformHeaders(input.otelHeaders),\n          }\n        : undefined,\n      retry: input.retry || 3,\n      followRedirects: input.followRedirects || true,\n    };\n  }\n  if (input.jobType === \"tcp\") {\n    payload = {\n      workspaceId: String(input.workspaceId),\n      monitorId: String(input.id),\n      uri: input.url,\n      status: \"active\",\n      assertions: input.assertions ? JSON.parse(input.assertions) : null,\n      cronTimestamp: timestamp,\n      degradedAfter: input.degradedAfter,\n      timeout: input.timeout,\n      trigger: \"cron\",\n      retry: input.retry || 3,\n      otelConfig: input.otelEndpoint\n        ? {\n            endpoint: input.otelEndpoint,\n            headers: transformHeaders(input.otelHeaders),\n          }\n        : undefined,\n      followRedirects: input.followRedirects || true,\n    };\n  }\n  if (input.jobType === \"dns\") {\n    payload = {\n      workspaceId: String(input.workspaceId),\n      monitorId: String(input.id),\n      uri: input.url,\n      status: \"active\",\n      assertions: input.assertions ? JSON.parse(input.assertions) : null,\n      cronTimestamp: timestamp,\n      degradedAfter: input.degradedAfter,\n      timeout: input.timeout,\n      trigger: \"cron\",\n      retry: input.retry || 3,\n      otelConfig: input.otelEndpoint\n        ? {\n            endpoint: input.otelEndpoint,\n            headers: transformHeaders(input.otelHeaders),\n          }\n        : undefined,\n      followRedirects: input.followRedirects || true,\n    };\n  }\n  const allResult = [];\n\n  for (const region of input.regions) {\n    const res = fetch(generateUrl({ row: input }), {\n      method: \"POST\",\n      headers: {\n        Authorization: `Basic ${env.CRON_SECRET}`,\n        \"Content-Type\": \"application/json\",\n        \"fly-prefer-region\": region,\n      },\n      body: JSON.stringify(payload),\n      signal: AbortSignal.timeout(ABORT_TIMEOUT),\n    });\n    allResult.push(res);\n  }\n\n  await Promise.allSettled(allResult);\n}\n\nfunction generateUrl({ row }: { row: z.infer<typeof selectMonitorSchema> }) {\n  switch (row.jobType) {\n    case \"http\":\n      return `https://openstatus-checker.fly.dev/checker/http?monitor_id=${row.id}`;\n    case \"tcp\":\n      return `https://openstatus-checker.fly.dev/checker/tcp?monitor_id=${row.id}`;\n    case \"dns\":\n      return `https://openstatus-checker.fly.dev/checker/dns?monitor_id=${row.id}`;\n    default:\n      throw new Error(\"Invalid jobType\");\n  }\n}\n\nexport const checkerRouter = createTRPCRouter({\n  testHttp: protectedProcedure\n    .meta({ track: Events.TestMonitor })\n    .input(httpTestInput)\n    .mutation(async ({ input }) => {\n      return testHttp(input);\n    }),\n\n  testTcp: protectedProcedure\n    .meta({ track: Events.TestMonitor })\n    .input(tcpTestInput)\n    .mutation(async ({ input }) => {\n      return testTcp(input);\n    }),\n  testDns: protectedProcedure\n    .meta({ track: Events.TestMonitor })\n    .input(dnsTestInput)\n    .mutation(async ({ input }) => {\n      return testDns(input);\n    }),\n\n  triggerChecker: protectedProcedure\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const m = await db\n        .select()\n        .from(monitor)\n        .where(\n          and(\n            eq(monitor.id, opts.input.id),\n            eq(monitor.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .get();\n      if (!m) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found\",\n        });\n      }\n      const input = selectMonitorSchema.parse(m);\n\n      return await triggerChecker(input);\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/domain.ts",
    "content": "import { z } from \"zod\";\n\nimport { env } from \"../env\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const domainConfigResponseSchema = z.object({\n  configuredBy: z\n    .union([z.literal(\"CNAME\"), z.literal(\"A\"), z.literal(\"http\")])\n    .optional()\n    .nullable(),\n  acceptedChallenges: z\n    .array(z.union([z.literal(\"dns-01\"), z.literal(\"http-01\")]))\n    .optional()\n    .nullable(),\n  misconfigured: z.boolean().prefault(true).optional(),\n});\n\nexport const domainResponseSchema = z.object({\n  name: z.string().optional(),\n  apexName: z.string().optional(),\n  projectId: z.string().optional(),\n  redirect: z.string().optional().nullable(),\n  redirectStatusCode: z\n    .union([z.literal(307), z.literal(301), z.literal(302), z.literal(308)])\n    .optional()\n    .nullable(),\n  gitBranch: z.string().optional().nullable(),\n  updatedAt: z.number().optional(),\n  createdAt: z.number().optional(),\n  verified: z.boolean().optional(),\n  verification: z\n    .array(\n      z.object({\n        type: z.string(),\n        domain: z.string(),\n        value: z.string(),\n        reason: z.string(),\n      }),\n    )\n    .optional(),\n});\n\nexport type DomainVerificationResponse = z.infer<typeof domainResponseSchema>;\nexport type DomainConfigResponse = z.infer<typeof domainConfigResponseSchema>;\nexport type DomainResponse = z.infer<typeof domainResponseSchema>;\nexport type DomainVerificationStatusProps =\n  | \"Valid Configuration\"\n  | \"Invalid Configuration\"\n  | \"Pending Verification\"\n  | \"Domain Not Found\"\n  | \"Unknown Error\";\n\nexport const domainRouter = createTRPCRouter({\n  getDomainResponse: protectedProcedure\n    .input(z.object({ domain: z.string().optional() }))\n    .query(async (opts) => {\n      if (!opts.input.domain) {\n        return null;\n      }\n      const data = await fetch(\n        `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${opts.input.domain}?teamId=${env.TEAM_ID_VERCEL}`,\n        {\n          method: \"GET\",\n          headers: {\n            Authorization: `Bearer ${env.VERCEL_AUTH_BEARER_TOKEN}`,\n            \"Content-Type\": \"application/json\",\n          },\n        },\n      );\n      const json = await data.json();\n      const result = domainResponseSchema\n        .extend({\n          error: z\n            .object({\n              code: z.string(),\n              message: z.string(),\n            })\n            .optional(),\n        })\n        .parse(json);\n      console.log({ result });\n      return result;\n    }),\n  getConfigResponse: protectedProcedure\n    .input(z.object({ domain: z.string().optional() }))\n    .query(async (opts) => {\n      if (!opts.input.domain) {\n        return null;\n      }\n      const data = await fetch(\n        `https://api.vercel.com/v6/domains/${opts.input.domain}/config?teamId=${env.TEAM_ID_VERCEL}`,\n        {\n          method: \"GET\",\n          headers: {\n            Authorization: `Bearer ${env.VERCEL_AUTH_BEARER_TOKEN}`,\n            \"Content-Type\": \"application/json\",\n          },\n        },\n      );\n      const json = await data.json();\n      const result = domainConfigResponseSchema.parse(json);\n      return result;\n    }),\n  verifyDomain: protectedProcedure\n    .input(z.object({ domain: z.string().optional() }))\n    .query(async (opts) => {\n      if (!opts.input.domain) {\n        return null;\n      }\n      const data = await fetch(\n        `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${opts.input.domain}/verify?teamId=${env.TEAM_ID_VERCEL}`,\n        {\n          method: \"POST\",\n          headers: {\n            Authorization: `Bearer ${env.VERCEL_AUTH_BEARER_TOKEN}`,\n            \"Content-Type\": \"application/json\",\n          },\n        },\n      );\n      const json = await data.json();\n      const result = domainResponseSchema.parse(json);\n      return result;\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/email/index.ts",
    "content": "import { z } from \"zod\";\n\nimport { and, eq } from \"@openstatus/db\";\nimport {\n  invitation,\n  maintenance,\n  pageSubscriber,\n  selectWorkspaceSchema,\n  statusReportUpdate,\n} from \"@openstatus/db/src/schema\";\nimport { EmailClient } from \"@openstatus/emails\";\nimport {\n  dispatchMaintenanceUpdate,\n  dispatchStatusReportUpdate,\n  getChannel,\n} from \"@openstatus/subscriptions\";\nimport { TRPCError } from \"@trpc/server\";\nimport { env } from \"../../env\";\nimport {\n  createTRPCRouter,\n  protectedProcedure,\n  publicProcedure,\n} from \"../../trpc\";\n\nconst emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY });\n\nexport const emailRouter = createTRPCRouter({\n  /**\n   * PUBLIC: Send verification email for a new page subscription\n   * Called after upsert to trigger the verification flow\n   */\n  sendPageSubscriptionVerification: publicProcedure\n    .input(\n      z.object({\n        id: z.number().int().positive(),\n        token: z.uuid(),\n      }),\n    )\n    .mutation(async (opts) => {\n      const subscriber = await opts.ctx.db.query.pageSubscriber.findFirst({\n        where: and(\n          eq(pageSubscriber.id, opts.input.id),\n          eq(pageSubscriber.token, opts.input.token),\n        ),\n        with: {\n          page: {\n            with: {\n              workspace: true,\n            },\n          },\n        },\n      });\n\n      if (!subscriber) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Subscriber not found\",\n        });\n      }\n\n      const workspace = selectWorkspaceSchema.safeParse(\n        subscriber.page?.workspace,\n      );\n\n      if (!workspace.success) {\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Invalid workspace\",\n        });\n      }\n\n      if (!workspace.data.limits[\"status-subscribers\"]) {\n        return;\n      }\n\n      if (!subscriber.email) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"No email associated with this subscription\",\n        });\n      }\n\n      if (subscriber.acceptedAt) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Subscription already verified\",\n        });\n      }\n\n      if (!subscriber.token) {\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Subscription has no verification token\",\n        });\n      }\n\n      const verifyUrl = subscriber.page.customDomain\n        ? `https://${subscriber.page.customDomain}/verify/${subscriber.token}`\n        : `https://${subscriber.page.slug}.openstatus.dev/verify/${subscriber.token}`;\n\n      const channel = getChannel(\"email\");\n      if (!channel) {\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Email channel not found\",\n        });\n      }\n\n      await channel.sendVerification?.(\n        {\n          id: subscriber.id,\n          pageId: subscriber.pageId,\n          pageName: subscriber.page.title,\n          pageSlug: subscriber.page.slug,\n          customDomain: subscriber.page.customDomain,\n          componentIds: [],\n          channelType: \"email\",\n          email: subscriber.email,\n          token: subscriber.token,\n          acceptedAt: subscriber.acceptedAt ?? undefined,\n        },\n        verifyUrl,\n      );\n\n      return { success: true };\n    }),\n\n  /**\n   * PROTECTED: Send status report update notifications via dispatcher\n   */\n  sendStatusReport: protectedProcedure\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const limits = opts.ctx.workspace.limits;\n\n      if (!limits[\"status-subscribers\"]) {\n        return;\n      }\n\n      const update = await opts.ctx.db.query.statusReportUpdate.findFirst({\n        where: eq(statusReportUpdate.id, opts.input.id),\n        with: {\n          statusReport: true,\n        },\n      });\n\n      if (!update?.statusReport) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Status report update not found\",\n        });\n      }\n\n      if (update.statusReport.workspaceId !== opts.ctx.workspace.id) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"Status report does not belong to your workspace\",\n        });\n      }\n\n      await dispatchStatusReportUpdate(opts.input.id);\n\n      return { success: true };\n    }),\n\n  /**\n   * PROTECTED: Send maintenance notifications via dispatcher\n   */\n  sendMaintenance: protectedProcedure\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const limits = opts.ctx.workspace.limits;\n\n      if (!limits[\"status-subscribers\"]) {\n        return;\n      }\n\n      const _maintenance = await opts.ctx.db.query.maintenance.findFirst({\n        where: and(\n          eq(maintenance.id, opts.input.id),\n          eq(maintenance.workspaceId, opts.ctx.workspace.id),\n        ),\n      });\n\n      if (!_maintenance) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Maintenance not found\",\n        });\n      }\n\n      await dispatchMaintenanceUpdate(opts.input.id);\n\n      return { success: true };\n    }),\n\n  sendTeamInvitation: protectedProcedure\n    .input(z.object({ id: z.number(), baseUrl: z.string().optional() }))\n    .mutation(async (opts) => {\n      const limits = opts.ctx.workspace.limits;\n\n      if (limits.members === \"Unlimited\" || limits.members > 1) {\n        const _invitation = await opts.ctx.db.query.invitation.findFirst({\n          where: and(\n            eq(invitation.id, opts.input.id),\n            eq(invitation.workspaceId, opts.ctx.workspace.id),\n          ),\n        });\n\n        if (!_invitation) return;\n\n        await emailClient.sendTeamInvitation({\n          to: _invitation.email,\n          token: _invitation.token,\n          invitedBy: `${opts.ctx.user.email}`,\n          workspaceName: opts.ctx.workspace.name || \"OpenStatus\",\n          baseUrl: opts.input.baseUrl,\n        });\n      }\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/feedback.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\nimport { env } from \"../env\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const feedbackRouter = createTRPCRouter({\n  submit: protectedProcedure\n    .input(\n      z.object({\n        // NOTE: coming from NavFeedback\n        message: z.string().min(1, \"Message required\"),\n        path: z.string().optional(),\n        isMobile: z.boolean().optional(),\n        // NOTE: coming from ContactForm\n        name: z.string().optional(),\n        email: z.email().optional(),\n        blocker: z.boolean().optional(),\n        type: z.string().optional(),\n      }),\n    )\n    .mutation(async (opts) => {\n      if (!env.SLACK_FEEDBACK_WEBHOOK_URL) {\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Slack feedback webhook not configured.\",\n        });\n      }\n\n      const textLines: string[] = [];\n      if (opts.input.name) textLines.push(`*Name:* ${opts.input.name}`);\n      if (opts.input.email) textLines.push(`*Email:* ${opts.input.email}`);\n      if (opts.input.blocker)\n        textLines.push(`*Blocker:* ${opts.input.blocker}`);\n      if (opts.input.type) textLines.push(`*Type:* ${opts.input.type}`);\n      if (opts.ctx.user) textLines.push(`*User:* ${opts.ctx.user.email}`);\n      if (opts.input.path) textLines.push(`*Path:* ${opts.input.path}`);\n      if (opts.input.isMobile)\n        textLines.push(`*Mobile:* ${opts.input.isMobile}`);\n      if (opts.ctx.metadata?.userAgent)\n        textLines.push(`*User Agent:* ${opts.ctx.metadata.userAgent}`);\n      if (opts.ctx.metadata?.location)\n        textLines.push(`*Location:* ${opts.ctx.metadata.location}`);\n\n      textLines.push(\"--------------------------------\");\n\n      textLines.push(`*Message:* ${opts.input.message}`);\n\n      await fetch(env.SLACK_FEEDBACK_WEBHOOK_URL, {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify({\n          text: textLines.join(\"\\n\"),\n        }),\n      });\n\n      return { success: true } as const;\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/import.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq, inArray } from \"@openstatus/db\";\nimport {\n  maintenance,\n  maintenancesToPageComponents,\n  page,\n  pageComponent,\n  pageComponentGroup,\n  statusReport,\n  statusReportUpdate,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\nimport { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport {\n  MOCK_COMPONENTS,\n  MOCK_COMPONENT_GROUPS,\n  MOCK_INCIDENTS,\n  MOCK_PAGES,\n  MOCK_SUBSCRIBERS,\n} from \"@openstatus/importers/statuspage/fixtures\";\n\nimport { edgeRouter } from \"../edge\";\nimport { previewImport, runImport } from \"../service/import\";\nimport { createInnerTRPCContext } from \"../trpc\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst originalFetch = globalThis.fetch;\n\nfunction mockStatuspageFetch() {\n  globalThis.fetch = ((url: string | URL | Request) => {\n    const urlStr = typeof url === \"string\" ? url : url.toString();\n    let data: unknown = [];\n\n    // For paginated endpoints, return empty on page > 1\n    const pageParam = urlStr.includes(\"?\")\n      ? new URL(urlStr).searchParams.get(\"page\")\n      : null;\n    const isFirstPage = !pageParam || pageParam === \"1\";\n\n    if (urlStr.endsWith(\"/pages\")) data = MOCK_PAGES;\n    else if (urlStr.includes(\"/component-groups\"))\n      data = isFirstPage ? MOCK_COMPONENT_GROUPS : [];\n    else if (urlStr.includes(\"/components\"))\n      data = isFirstPage ? MOCK_COMPONENTS : [];\n    else if (urlStr.includes(\"/incidents/scheduled\"))\n      data = isFirstPage\n        ? MOCK_INCIDENTS.filter((i) => i.scheduled_for != null)\n        : [];\n    else if (urlStr.includes(\"/incidents\"))\n      data = isFirstPage ? MOCK_INCIDENTS : [];\n    else if (urlStr.includes(\"/subscribers\"))\n      data = isFirstPage ? MOCK_SUBSCRIBERS : [];\n\n    return Promise.resolve(\n      new Response(JSON.stringify(data), {\n        status: 200,\n        headers: { \"Content-Type\": \"application/json\" },\n      }),\n    );\n  }) as typeof globalThis.fetch;\n}\n\nfunction restoreFetch() {\n  globalThis.fetch = originalFetch;\n}\n\nfunction makeCaller(limitsOverride?: Partial<Limits>) {\n  const limits = { ...allPlans.starter.limits, ...limitsOverride };\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1, limits },\n  });\n  return edgeRouter.createCaller(ctx);\n}\n\n// Track all created IDs for cleanup\nconst createdIds = {\n  pages: [] as number[],\n  componentGroups: [] as number[],\n  components: [] as number[],\n  statusReports: [] as number[],\n  maintenances: [] as number[],\n};\n\nasync function cleanup() {\n  // Delete in reverse dependency order\n\n  // 1. statusReportsToPageComponents\n  if (createdIds.statusReports.length > 0) {\n    await db\n      .delete(statusReportsToPageComponents)\n      .where(\n        inArray(\n          statusReportsToPageComponents.statusReportId,\n          createdIds.statusReports,\n        ),\n      );\n  }\n\n  // 2. maintenancesToPageComponents\n  if (createdIds.maintenances.length > 0) {\n    await db\n      .delete(maintenancesToPageComponents)\n      .where(\n        inArray(\n          maintenancesToPageComponents.maintenanceId,\n          createdIds.maintenances,\n        ),\n      );\n  }\n\n  // 3. statusReportUpdate (via statusReportId)\n  if (createdIds.statusReports.length > 0) {\n    await db\n      .delete(statusReportUpdate)\n      .where(\n        inArray(statusReportUpdate.statusReportId, createdIds.statusReports),\n      );\n  }\n\n  // 4. statusReport\n  if (createdIds.statusReports.length > 0) {\n    await db\n      .delete(statusReport)\n      .where(inArray(statusReport.id, createdIds.statusReports));\n  }\n\n  // 5. maintenance\n  if (createdIds.maintenances.length > 0) {\n    await db\n      .delete(maintenance)\n      .where(inArray(maintenance.id, createdIds.maintenances));\n  }\n\n  // 6. pageComponent\n  if (createdIds.components.length > 0) {\n    await db\n      .delete(pageComponent)\n      .where(inArray(pageComponent.id, createdIds.components));\n  }\n\n  // 7. pageComponentGroup\n  if (createdIds.componentGroups.length > 0) {\n    await db\n      .delete(pageComponentGroup)\n      .where(inArray(pageComponentGroup.id, createdIds.componentGroups));\n  }\n\n  // 8. page (only ones we created, never the seeded page 1)\n  if (createdIds.pages.length > 0) {\n    await db.delete(page).where(inArray(page.id, createdIds.pages));\n  }\n\n  // Reset trackers\n  createdIds.pages = [];\n  createdIds.componentGroups = [];\n  createdIds.components = [];\n  createdIds.statusReports = [];\n  createdIds.maintenances = [];\n}\n\n/** Collect IDs from import summary phases for cleanup tracking. */\nfunction trackCreatedIds(summary: {\n  phases: Array<{\n    phase: string;\n    resources: Array<{ openstatusId?: number; status: string }>;\n  }>;\n}) {\n  for (const phase of summary.phases) {\n    const ids = phase.resources\n      .filter((r) => r.status === \"created\" && r.openstatusId)\n      .map((r) => r.openstatusId as number);\n\n    switch (phase.phase) {\n      case \"page\":\n        createdIds.pages.push(...ids);\n        break;\n      case \"componentGroups\":\n        createdIds.componentGroups.push(...ids);\n        break;\n      case \"components\":\n        createdIds.components.push(...ids);\n        break;\n      case \"incidents\":\n        createdIds.statusReports.push(...ids);\n        break;\n      case \"maintenances\":\n        createdIds.maintenances.push(...ids);\n        break;\n    }\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tests\n// ---------------------------------------------------------------------------\n\nbeforeAll(() => {\n  mockStatuspageFetch();\n});\n\nafterAll(async () => {\n  restoreFetch();\n  await cleanup();\n});\n\nconst caller = makeCaller();\n\ntest(\"preview returns mapped data without DB writes\", async () => {\n  const result = await caller.import.preview({\n    provider: \"statuspage\",\n    apiKey: \"test-key\",\n  });\n\n  expect(result.provider).toBe(\"statuspage\");\n  expect(result.phases.length).toBeGreaterThan(0);\n\n  // Check that expected phases exist\n  const phaseNames = result.phases.map((p) => p.phase);\n  expect(phaseNames).toContain(\"page\");\n  expect(phaseNames).toContain(\"components\");\n  expect(phaseNames).toContain(\"componentGroups\");\n\n  // Verify resource counts match fixture data\n  const pagePh = result.phases.find((p) => p.phase === \"page\");\n  expect(pagePh?.resources.length).toBe(MOCK_PAGES.length);\n\n  const compPh = result.phases.find((p) => p.phase === \"components\");\n  expect(compPh?.resources.length).toBe(MOCK_COMPONENTS.length);\n\n  const groupPh = result.phases.find((p) => p.phase === \"componentGroups\");\n  expect(groupPh?.resources.length).toBe(MOCK_COMPONENT_GROUPS.length);\n\n  // Verify no pages were created in DB with the slug from mock\n  const dbPage = await db\n    .select()\n    .from(page)\n    .where(eq(page.slug, \"acmecorp\"))\n    .get();\n  expect(dbPage).toBeUndefined();\n});\n\ntest(\"run creates page, components, and groups in DB\", async () => {\n  const result = await caller.import.run({\n    provider: \"statuspage\",\n    apiKey: \"test-key\",\n    options: { includeStatusReports: true, includeSubscribers: false },\n  });\n\n  trackCreatedIds(result);\n\n  // Overall status should be completed (subscribers skipped but that's fine)\n  expect([\"completed\", \"partial\"]).toContain(result.status);\n\n  // Page should be created with slug \"acmecorp\"\n  const dbPage = await db\n    .select()\n    .from(page)\n    .where(eq(page.slug, \"acmecorp\"))\n    .get();\n  if (!dbPage)\n    throw new Error(\"Expected page to be created with slug 'acmecorp'\");\n  expect(dbPage.title).toBe(\"Acme Corp Status\");\n  expect(dbPage.workspaceId).toBe(1);\n\n  // Components should be created\n  const dbComponents = await db\n    .select()\n    .from(pageComponent)\n    .where(eq(pageComponent.pageId, dbPage.id))\n    .all();\n  expect(dbComponents.length).toBe(MOCK_COMPONENTS.length);\n\n  // Groups should be created\n  const dbGroups = await db\n    .select()\n    .from(pageComponentGroup)\n    .where(eq(pageComponentGroup.pageId, dbPage.id))\n    .all();\n  expect(dbGroups.length).toBe(MOCK_COMPONENT_GROUPS.length);\n\n  // Subscribers phase should be skipped\n  const subPhase = result.phases.find((p) => p.phase === \"subscribers\");\n  expect(subPhase?.status).toBe(\"skipped\");\n\n  // Clean up for next tests\n  await cleanup();\n});\n\ntest(\"run with existing pageId imports into that page\", async () => {\n  const result = await caller.import.run({\n    provider: \"statuspage\",\n    apiKey: \"test-key\",\n    pageId: 1,\n    options: { includeStatusReports: false, includeSubscribers: false },\n  });\n\n  trackCreatedIds(result);\n\n  expect([\"completed\", \"partial\"]).toContain(result.status);\n\n  // Page phase should show \"skipped\" since we used an existing page\n  const pagePhase = result.phases.find((p) => p.phase === \"page\");\n  expect(pagePhase?.resources[0]?.status).toBe(\"skipped\");\n  expect(pagePhase?.resources[0]?.openstatusId).toBe(1);\n\n  // Components should be created under page 1\n  const dbComponents = await db\n    .select()\n    .from(pageComponent)\n    .where(eq(pageComponent.pageId, 1))\n    .all();\n  // At least the components we imported should exist\n  const importedCompNames = MOCK_COMPONENTS.map((c) => c.name);\n  const matchingComps = dbComponents.filter((c) =>\n    importedCompNames.includes(c.name),\n  );\n  expect(matchingComps.length).toBe(MOCK_COMPONENTS.length);\n\n  await cleanup();\n});\n\ntest(\"re-run skips already-imported resources (idempotency)\", async () => {\n  // First run\n  const result1 = await caller.import.run({\n    provider: \"statuspage\",\n    apiKey: \"test-key\",\n    options: { includeStatusReports: false, includeSubscribers: false },\n  });\n  trackCreatedIds(result1);\n\n  // Second run (same data)\n  const result2 = await caller.import.run({\n    provider: \"statuspage\",\n    apiKey: \"test-key\",\n    options: { includeStatusReports: false, includeSubscribers: false },\n  });\n  // Don't track again - same resources should be skipped\n\n  // Page should be skipped on second run\n  const pagePhase = result2.phases.find((p) => p.phase === \"page\");\n  expect(pagePhase?.resources[0]?.status).toBe(\"skipped\");\n\n  // Components should be skipped on second run\n  const compPhase = result2.phases.find((p) => p.phase === \"components\");\n  const allSkipped = compPhase?.resources.every((r) => r.status === \"skipped\");\n  expect(allSkipped).toBe(true);\n\n  // Groups should be skipped on second run\n  const groupPhase = result2.phases.find((p) => p.phase === \"componentGroups\");\n  const allGroupsSkipped = groupPhase?.resources.every(\n    (r) => r.status === \"skipped\",\n  );\n  expect(allGroupsSkipped).toBe(true);\n\n  await cleanup();\n});\n\ntest(\"run with includeStatusReports creates status reports\", async () => {\n  const result = await caller.import.run({\n    provider: \"statuspage\",\n    apiKey: \"test-key\",\n    options: { includeStatusReports: true, includeSubscribers: false },\n  });\n\n  trackCreatedIds(result);\n\n  // Incidents phase should be completed\n  const incPhase = result.phases.find((p) => p.phase === \"incidents\");\n  expect(incPhase).toBeDefined();\n  expect(incPhase?.status).toBe(\"completed\");\n\n  if (!incPhase) throw new Error(\"Expected incidents phase to exist\");\n\n  // Verify status reports were created in DB\n  const createdReportIds = incPhase.resources\n    .filter((r) => r.status === \"created\" && r.openstatusId)\n    .map((r) => r.openstatusId as number);\n  expect(createdReportIds.length).toBeGreaterThan(0);\n\n  // Check that status report updates were created\n  const updates = await db\n    .select()\n    .from(statusReportUpdate)\n    .where(inArray(statusReportUpdate.statusReportId, createdReportIds))\n    .all();\n  expect(updates.length).toBeGreaterThan(0);\n\n  // Check that statusReportsToPageComponents links exist\n  const links = await db\n    .select()\n    .from(statusReportsToPageComponents)\n    .where(\n      inArray(statusReportsToPageComponents.statusReportId, createdReportIds),\n    )\n    .all();\n  expect(links.length).toBeGreaterThan(0);\n\n  // Maintenances phase should also have entries (scheduled incident)\n  const maintPhase = result.phases.find((p) => p.phase === \"maintenances\");\n  if (!maintPhase) throw new Error(\"Expected maintenances phase to exist\");\n  const createdMaintIds = maintPhase.resources\n    .filter((r) => r.status === \"created\" && r.openstatusId)\n    .map((r) => r.openstatusId as number);\n  expect(createdMaintIds.length).toBeGreaterThan(0);\n\n  await cleanup();\n});\n\n// ---------------------------------------------------------------------------\n// Limit warnings (call service directly to control limits)\n// ---------------------------------------------------------------------------\n\nconst freeLimits = { ...allPlans.free.limits };\nconst starterLimits = { ...allPlans.starter.limits };\n\ntest(\"preview shows component limit warning on free plan\", async () => {\n  const result = await previewImport({\n    apiKey: \"test-key\",\n    workspaceId: 1,\n    limits: { ...freeLimits, \"page-components\": 3 },\n  });\n\n  expect(result.errors.length).toBeGreaterThan(0);\n  expect(result.errors.some((e) => e.includes(\"3 of 4\"))).toBe(true);\n});\n\ntest(\"preview shows custom domain warning on free plan\", async () => {\n  const result = await previewImport({\n    apiKey: \"test-key\",\n    workspaceId: 1,\n    limits: { ...freeLimits, \"custom-domain\": false },\n  });\n\n  expect(result.errors.some((e) => e.includes(\"Custom domain\"))).toBe(true);\n});\n\ntest(\"preview shows subscriber warning on free plan\", async () => {\n  const result = await previewImport({\n    apiKey: \"test-key\",\n    workspaceId: 1,\n    limits: { ...freeLimits, \"status-subscribers\": false },\n  });\n\n  expect(result.errors.some((e) => e.includes(\"Subscribers\"))).toBe(true);\n});\n\ntest(\"preview shows no warnings on starter plan\", async () => {\n  const result = await previewImport({\n    apiKey: \"test-key\",\n    workspaceId: 1,\n    limits: starterLimits,\n  });\n\n  // Only informational warnings about non-email subscribers are expected\n  const limitWarnings = result.errors.filter(\n    (e) => !e.includes(\"non-email subscriber\"),\n  );\n  expect(limitWarnings).toEqual([]);\n});\n\ntest(\"run enforces component limit by truncating\", async () => {\n  // Create a dedicated page so we control the state entirely\n  const [testPage] = await db\n    .insert(page)\n    .values({\n      workspaceId: 1,\n      title: \"Truncation Test Page\",\n      description: \"\",\n      slug: \"truncation-test\",\n      customDomain: \"\",\n      icon: \"\",\n    })\n    .returning({ id: page.id });\n\n  if (!testPage) throw new Error(\"Failed to create test page\");\n  createdIds.pages.push(testPage.id);\n\n  const result = await runImport({\n    apiKey: \"test-key\",\n    workspaceId: 1,\n    pageId: testPage.id,\n    limits: { ...starterLimits, \"page-components\": 2 },\n    options: { includeStatusReports: false, includeSubscribers: false },\n  });\n\n  trackCreatedIds(result);\n\n  // Should have warning about component limit\n  expect(\n    result.errors.some(\n      (e) =>\n        e.includes(\"page-components\") ||\n        e.includes(\"Component limit\") ||\n        e.includes(\"components can be imported\") ||\n        e.includes(\"plan limit\"),\n    ),\n  ).toBe(true);\n\n  // Components phase should show enforcement (either truncated or fully blocked)\n  const compPhase = result.phases.find((p) => p.phase === \"components\");\n  expect(compPhase).toBeDefined();\n  // With limit of 2, not all 4 mock components should be created\n  const created =\n    compPhase?.resources.filter((r) => r.status === \"created\") ?? [];\n  expect(created.length).toBeLessThanOrEqual(2);\n\n  await cleanup();\n});\n\ntest(\"run skips subscribers on free plan\", async () => {\n  const result = await runImport({\n    apiKey: \"test-key\",\n    workspaceId: 1,\n    limits: { ...freeLimits, \"status-subscribers\": false },\n    options: { includeStatusReports: false, includeSubscribers: true },\n  });\n\n  trackCreatedIds(result);\n\n  const subPhase = result.phases.find((p) => p.phase === \"subscribers\");\n  expect(subPhase?.status).toBe(\"skipped\");\n  expect(result.errors.some((e) => e.includes(\"Subscribers\"))).toBe(true);\n\n  await cleanup();\n});\n"
  },
  {
    "path": "packages/api/src/router/import.ts",
    "content": "import { and, db, eq } from \"@openstatus/db\";\nimport { page } from \"@openstatus/db/src/schema\";\nimport { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\nimport { previewImport, runImport } from \"../service/import\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const importRouter = createTRPCRouter({\n  preview: protectedProcedure\n    .input(\n      z.object({\n        provider: z.enum([\"statuspage\"]),\n        apiKey: z.string().min(1),\n        statuspagePageId: z.string().nullish(),\n        pageId: z.number().optional(),\n      }),\n    )\n    .mutation(async (opts) => {\n      return previewImport({\n        apiKey: opts.input.apiKey,\n        statuspagePageId: opts.input.statuspagePageId ?? undefined,\n        workspaceId: opts.ctx.workspace.id,\n        pageId: opts.input.pageId,\n        limits: opts.ctx.workspace.limits,\n      });\n    }),\n\n  run: protectedProcedure\n    .input(\n      z.object({\n        provider: z.enum([\"statuspage\"]),\n        apiKey: z.string().min(1),\n        pageId: z.number().optional(),\n        statuspagePageId: z.string().nullish(),\n        options: z\n          .object({\n            includeStatusReports: z.boolean().default(true),\n            includeSubscribers: z.boolean().default(false),\n            includeComponents: z.boolean().default(true),\n          })\n          .optional(),\n      }),\n    )\n    .mutation(async (opts) => {\n      // If pageId provided, verify it belongs to workspace\n      if (opts.input.pageId) {\n        const existing = await db\n          .select()\n          .from(page)\n          .where(\n            and(\n              eq(page.id, opts.input.pageId),\n              eq(page.workspaceId, opts.ctx.workspace.id),\n            ),\n          )\n          .get();\n\n        if (!existing) {\n          throw new TRPCError({\n            code: \"NOT_FOUND\",\n            message: \"Page not found or does not belong to this workspace\",\n          });\n        }\n      }\n\n      return runImport({\n        apiKey: opts.input.apiKey,\n        statuspagePageId: opts.input.statuspagePageId ?? undefined,\n        workspaceId: opts.ctx.workspace.id,\n        pageId: opts.input.pageId,\n        options: opts.input.options,\n        limits: opts.ctx.workspace.limits,\n      });\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/incident.ts",
    "content": "import { z } from \"zod\";\n\nimport { type SQL, and, asc, desc, eq, gte, schema } from \"@openstatus/db\";\nimport {\n  incidentTable,\n  selectIncidentSchema,\n  selectMonitorSchema,\n} from \"@openstatus/db/src/schema\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { TRPCError } from \"@trpc/server\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\nimport { getPeriodDate, periods } from \"./utils\";\n\nexport const incidentRouter = createTRPCRouter({\n  delete: protectedProcedure\n    .meta({ track: Events.DeleteIncident })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const incidentToDelete = await opts.ctx.db\n        .select()\n        .from(schema.incidentTable)\n        .where(\n          and(\n            eq(schema.incidentTable.id, opts.input.id),\n            eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .get();\n      if (!incidentToDelete) return;\n\n      await opts.ctx.db\n        .delete(schema.incidentTable)\n        .where(eq(schema.incidentTable.id, incidentToDelete.id))\n        .run();\n    }),\n\n  list: protectedProcedure\n    .input(\n      z\n        .object({\n          period: z.enum(periods).optional(),\n          monitorId: z.number().nullish(),\n          order: z.enum([\"asc\", \"desc\"]).optional(),\n        })\n        .optional(),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(incidentTable.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      if (opts.input?.period) {\n        whereConditions.push(\n          gte(incidentTable.startedAt, getPeriodDate(opts.input.period)),\n        );\n      }\n\n      if (opts.input?.monitorId) {\n        whereConditions.push(eq(incidentTable.monitorId, opts.input.monitorId));\n      }\n\n      const result = await opts.ctx.db.query.incidentTable.findMany({\n        where: and(...whereConditions),\n        with: {\n          monitor: true,\n        },\n        orderBy:\n          opts.input?.order === \"asc\"\n            ? asc(incidentTable.startedAt)\n            : desc(incidentTable.startedAt),\n      });\n\n      return selectIncidentSchema\n        .extend({\n          monitor: selectMonitorSchema,\n        })\n        .array()\n        .parse(result);\n    }),\n\n  acknowledge: protectedProcedure\n    .meta({ track: Events.AcknowledgeIncident })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const currentIncident = await opts.ctx.db\n        .select()\n        .from(schema.incidentTable)\n        .where(\n          and(\n            eq(schema.incidentTable.id, opts.input.id),\n            eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .get();\n      if (!currentIncident) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Incident not found\",\n        });\n      }\n      if (currentIncident.acknowledgedAt) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Incident already acknowledged\",\n        });\n      }\n      await opts.ctx.db\n        .update(schema.incidentTable)\n        .set({\n          acknowledgedAt: new Date(),\n          acknowledgedBy: opts.ctx.user.id,\n          updatedAt: new Date(),\n        })\n        .where(\n          and(\n            eq(schema.incidentTable.id, opts.input.id),\n            eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),\n          ),\n        );\n      return true;\n    }),\n\n  resolve: protectedProcedure\n    .meta({ track: Events.ResolveIncident })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const currentIncident = await opts.ctx.db\n        .select()\n        .from(schema.incidentTable)\n        .where(\n          and(\n            eq(schema.incidentTable.id, opts.input.id),\n            eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .get();\n      if (!currentIncident) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Incident not found\",\n        });\n      }\n      if (currentIncident.resolvedAt) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Incident already resolved\",\n        });\n      }\n      await opts.ctx.db\n        .update(schema.incidentTable)\n        .set({\n          resolvedAt: new Date(),\n          resolvedBy: opts.ctx.user.id,\n          updatedAt: new Date(),\n        })\n        .where(\n          and(\n            eq(schema.incidentTable.id, opts.input.id),\n            eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),\n          ),\n        );\n      return true;\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/integration.ts",
    "content": "// biome-ignore lint/style/useNodejsImportProtocol: some error with build\nimport crypto from \"crypto\";\nimport { z } from \"zod\";\n\nimport { and, eq, sql } from \"@openstatus/db\";\nimport { integration } from \"@openstatus/db/src/schema\";\n\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nfunction signInstallToken(workspaceId: number, ts: number): string {\n  const secret = process.env.SLACK_SIGNING_SECRET;\n  if (!secret) throw new Error(\"Slack not configured\");\n  const payload = JSON.stringify({ workspaceId, ts });\n  const sig = crypto.createHmac(\"sha256\", secret).update(payload).digest(\"hex\");\n  return Buffer.from(`${payload}.${sig}`).toString(\"base64url\");\n}\n\nfunction safeJsonParse(value: string | null): Record<string, unknown> {\n  if (!value) return {};\n  try {\n    return JSON.parse(value) as Record<string, unknown>;\n  } catch {\n    return {};\n  }\n}\n\nexport const integrationRouter = createTRPCRouter({\n  list: protectedProcedure.query(async (opts) => {\n    const rows = await opts.ctx.db\n      .select({\n        id: integration.id,\n        name: integration.name,\n        externalId: integration.externalId,\n        rawData: sql<string>`${integration.data}`,\n        createdAt: integration.createdAt,\n      })\n      .from(integration)\n      .where(eq(integration.workspaceId, opts.ctx.workspace.id))\n      .all();\n\n    return rows.map((row) => ({\n      id: row.id,\n      name: row.name,\n      externalId: row.externalId,\n      data: safeJsonParse(row.rawData),\n      createdAt: row.createdAt,\n    }));\n  }),\n\n  generateInstallToken: protectedProcedure.mutation(async (opts) => {\n    const token = signInstallToken(opts.ctx.workspace.id, Date.now());\n    return { token };\n  }),\n\n  deleteIntegration: protectedProcedure\n    .input(z.object({ integrationId: z.number() }))\n    .mutation(async (opts) => {\n      const existing = await opts.ctx.db\n        .select()\n        .from(integration)\n        .where(\n          and(\n            eq(integration.id, opts.input.integrationId),\n            eq(integration.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .get();\n\n      if (!existing) return;\n\n      const cred = existing.credential as { botToken?: string } | null;\n      if (cred?.botToken) {\n        try {\n          await fetch(\"https://slack.com/api/auth.revoke\", {\n            method: \"POST\",\n            headers: {\n              Authorization: `Bearer ${cred.botToken}`,\n              \"Content-Type\": \"application/x-www-form-urlencoded\",\n            },\n          });\n        } catch {\n          // Token may already be invalid\n        }\n      }\n\n      await opts.ctx.db\n        .delete(integration)\n        .where(eq(integration.id, existing.id));\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/invitation.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { type SQL, and, db, eq, gte, isNull } from \"@openstatus/db\";\nimport {\n  insertInvitationSchema,\n  invitation,\n  selectInvitationSchema,\n  selectWorkspaceSchema,\n  usersToWorkspaces,\n  workspace,\n} from \"@openstatus/db/src/schema\";\n\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const invitationRouter = createTRPCRouter({\n  create: protectedProcedure\n    .meta({ track: Events.InviteUser, trackProps: [\"email\"] })\n    .input(insertInvitationSchema.pick({ email: true }))\n    .mutation(async (opts) => {\n      const { email } = opts.input;\n\n      const _members = opts.ctx.workspace.limits.members;\n      const membersLimit = _members === \"Unlimited\" ? 420 : _members;\n\n      const usersToWorkspacesNumbers = (\n        await opts.ctx.db.query.usersToWorkspaces.findMany({\n          where: eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id),\n        })\n      ).length;\n\n      const openInvitationsNumbers = (\n        await opts.ctx.db.query.invitation.findMany({\n          where: and(\n            eq(invitation.workspaceId, opts.ctx.workspace.id),\n            gte(invitation.expiresAt, new Date()),\n            isNull(invitation.acceptedAt),\n          ),\n        })\n      ).length;\n\n      // the user has reached the limits\n      if (usersToWorkspacesNumbers + openInvitationsNumbers >= membersLimit) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You reached your member limits.\",\n        });\n      }\n\n      const expiresAt = new Date();\n      expiresAt.setDate(expiresAt.getDate() + 7);\n\n      const token = crypto.randomUUID();\n\n      const _invitation = await opts.ctx.db\n        .insert(invitation)\n        .values({ email, expiresAt, token, workspaceId: opts.ctx.workspace.id })\n        .returning()\n        .get();\n\n      if (process.env.NODE_ENV === \"development\") {\n        console.log(\n          `>>>> Invitation token: http://localhost:3000/invite?token=${token} <<<< `,\n        );\n      }\n\n      return _invitation;\n    }),\n\n  delete: protectedProcedure\n    .input(z.object({ id: z.number() }))\n    .meta({ track: Events.DeleteInvite })\n    .mutation(async (opts) => {\n      await opts.ctx.db\n        .delete(invitation)\n        .where(\n          and(\n            eq(invitation.id, opts.input.id),\n            eq(invitation.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .run();\n    }),\n\n  list: protectedProcedure.query(async (opts) => {\n    const whereConditions: SQL[] = [\n      eq(invitation.workspaceId, opts.ctx.workspace.id),\n      gte(invitation.expiresAt, new Date()),\n      isNull(invitation.acceptedAt),\n    ];\n\n    const result = await opts.ctx.db.query.invitation.findMany({\n      where: and(...whereConditions),\n    });\n\n    return result;\n  }),\n\n  get: protectedProcedure\n    .input(z.object({ token: z.string().nullable() }))\n    .query(async (opts) => {\n      if (!opts.ctx.user.email) {\n        throw new TRPCError({\n          code: \"UNAUTHORIZED\",\n          message: \"You are not authorized to access this resource.\",\n        });\n      }\n\n      if (!opts.input.token) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Token is required.\",\n        });\n      }\n\n      const result = await opts.ctx.db.query.invitation.findFirst({\n        where: and(\n          eq(invitation.token, opts.input.token),\n          isNull(invitation.acceptedAt),\n          gte(invitation.expiresAt, new Date()),\n          eq(invitation.email, opts.ctx.user.email),\n        ),\n        with: {\n          workspace: true,\n        },\n      });\n\n      if (!result) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Invitation not found.\",\n        });\n      }\n\n      return selectInvitationSchema\n        .extend({\n          workspace: selectWorkspaceSchema,\n        })\n        .parse(result);\n    }),\n\n  accept: protectedProcedure\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      if (!opts.ctx.user.email) {\n        throw new TRPCError({\n          code: \"UNAUTHORIZED\",\n          message: \"You are not authorized to access this resource.\",\n        });\n      }\n\n      const _invitation = await opts.ctx.db.query.invitation.findFirst({\n        where: and(\n          eq(invitation.id, opts.input.id),\n          eq(invitation.email, opts.ctx.user.email),\n          isNull(invitation.acceptedAt),\n          gte(invitation.expiresAt, new Date()),\n        ),\n        with: {\n          workspace: true,\n        },\n      });\n\n      if (!_invitation) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Invitation not found.\",\n        });\n      }\n\n      if (_invitation.acceptedAt) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Invitation already accepted.\",\n        });\n      }\n\n      const result = await db.transaction(async (tx) => {\n        await tx\n          .update(invitation)\n          .set({\n            acceptedAt: new Date(),\n          })\n          .where(eq(invitation.id, opts.input.id))\n          .run();\n\n        await tx\n          .insert(usersToWorkspaces)\n          .values({\n            userId: opts.ctx.user.id,\n            workspaceId: _invitation.workspaceId,\n            role: _invitation.role,\n          })\n          .run();\n\n        const _workspace = await tx.query.workspace.findFirst({\n          where: eq(workspace.id, _invitation.workspaceId),\n        });\n\n        return _workspace;\n      });\n\n      return result;\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/maintenance.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  maintenance,\n  maintenancesToPageComponents,\n  page,\n} from \"@openstatus/db/src/schema\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { edgeRouter } from \"../edge\";\nimport { createInnerTRPCContext } from \"../trpc\";\n\nlet otherWorkspaceMaintenanceId: number;\nlet otherWorkspacePageId: number;\n\nbeforeAll(async () => {\n  const p = await db\n    .insert(page)\n    .values({\n      workspaceId: 2,\n      title: \"Maintenance IDOR Test Page\",\n      description: \"Page for maintenance IDOR testing\",\n      slug: \"maintenance-idor-test-page\",\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n  otherWorkspacePageId = p.id;\n\n  // Maintenance for workspace 2, referencing page 1 (page ownership is not\n  // enforced at DB level, only at API level, so this insert works for testing)\n  const m = await db\n    .insert(maintenance)\n    .values({\n      workspaceId: 2,\n      title: \"Other workspace maintenance\",\n      message: \"Scheduled maintenance\",\n      pageId: 1,\n      from: new Date(),\n      to: new Date(Date.now() + 3_600_000),\n    })\n    .returning()\n    .get();\n  otherWorkspaceMaintenanceId = m.id;\n\n  await db\n    .insert(maintenancesToPageComponents)\n    .values({\n      maintenanceId: otherWorkspaceMaintenanceId,\n      pageComponentId: 1,\n    })\n    .run();\n});\n\nafterAll(async () => {\n  await db\n    .delete(maintenancesToPageComponents)\n    .where(\n      eq(\n        maintenancesToPageComponents.maintenanceId,\n        otherWorkspaceMaintenanceId,\n      ),\n    );\n  await db\n    .delete(maintenance)\n    .where(eq(maintenance.id, otherWorkspaceMaintenanceId));\n  await db.delete(page).where(eq(page.id, otherWorkspacePageId));\n});\n\ntest(\"maintenance.update rejects maintenance from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.maintenance.update({\n      id: otherWorkspaceMaintenanceId,\n      title: \"Unauthorized Modification\",\n      message: \"Should not work\",\n      startDate: new Date(),\n      endDate: new Date(Date.now() + 3_600_000),\n      pageComponents: [],\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n\n  // Verify page component associations were NOT deleted\n  const associations = await db.query.maintenancesToPageComponents.findMany({\n    where: eq(\n      maintenancesToPageComponents.maintenanceId,\n      otherWorkspaceMaintenanceId,\n    ),\n  });\n  expect(associations.length).toBe(1);\n});\n\ntest(\"maintenance.update succeeds for own workspace maintenance\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  // Seed maintenance 1 belongs to workspace 1\n  await caller.maintenance.update({\n    id: 1,\n    title: \"Updated Maintenance Title\",\n    message: \"Updated message\",\n    startDate: new Date(),\n    endDate: new Date(Date.now() + 7_200_000),\n    pageComponents: [1],\n  });\n\n  const updated = await db.query.maintenance.findFirst({\n    where: eq(maintenance.id, 1),\n  });\n  expect(updated?.title).toBe(\"Updated Maintenance Title\");\n});\n\ntest(\"maintenance.new rejects pageId from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.maintenance.new({\n      title: \"Cross-workspace maintenance\",\n      message: \"Should be rejected\",\n      pageId: otherWorkspacePageId, // page from workspace 2\n      startDate: new Date(),\n      endDate: new Date(Date.now() + 3_600_000),\n      pageComponents: [],\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n});\n\ntest(\"maintenance.new succeeds for own workspace page\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  const result = await caller.maintenance.new({\n    title: \"Valid Maintenance\",\n    message: \"Own workspace maintenance\",\n    pageId: 1, // page 1 belongs to workspace 1\n    startDate: new Date(),\n    endDate: new Date(Date.now() + 3_600_000),\n    pageComponents: [1],\n  });\n\n  expect(result).toBeDefined();\n\n  // Cleanup\n  await db\n    .delete(maintenancesToPageComponents)\n    .where(eq(maintenancesToPageComponents.maintenanceId, result.id));\n  await db.delete(maintenance).where(eq(maintenance.id, result.id));\n});\n"
  },
  {
    "path": "packages/api/src/router/maintenance.ts",
    "content": "import { z } from \"zod\";\n\nimport { type SQL, and, asc, desc, eq, gte, inArray } from \"@openstatus/db\";\nimport {\n  maintenance,\n  maintenancesToPageComponents,\n  page,\n  pageComponent,\n  selectMaintenanceSchema,\n  selectPageComponentSchema,\n} from \"@openstatus/db/src/schema\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { TRPCError } from \"@trpc/server\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\nimport { getPeriodDate, periods } from \"./utils\";\n\nexport const maintenanceRouter = createTRPCRouter({\n  delete: protectedProcedure\n    .meta({ track: Events.DeleteMaintenance })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      return await opts.ctx.db\n        .delete(maintenance)\n        .where(\n          and(\n            eq(maintenance.id, opts.input.id),\n            eq(maintenance.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .returning();\n    }),\n\n  list: protectedProcedure\n    .input(\n      z\n        .object({\n          period: z.enum(periods).optional(),\n          pageId: z.number().optional(),\n          order: z.enum([\"asc\", \"desc\"]).optional(),\n        })\n        .optional(),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(maintenance.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      if (opts.input?.period) {\n        whereConditions.push(\n          gte(maintenance.createdAt, getPeriodDate(opts.input.period)),\n        );\n      }\n\n      if (opts.input?.pageId) {\n        whereConditions.push(eq(maintenance.pageId, opts.input.pageId));\n      }\n\n      const query = opts.ctx.db.query.maintenance.findMany({\n        where: and(...whereConditions),\n        orderBy:\n          opts.input?.order === \"asc\"\n            ? asc(maintenance.createdAt)\n            : desc(maintenance.createdAt),\n        with: {\n          maintenancesToPageComponents: { with: { pageComponent: true } },\n        },\n      });\n\n      const result = await query;\n\n      return selectMaintenanceSchema\n        .extend({\n          pageComponents: z.array(selectPageComponentSchema).prefault([]),\n        })\n        .array()\n        .parse(\n          result.map((m) => ({\n            ...m,\n            pageComponents: m.maintenancesToPageComponents.map(\n              ({ pageComponent }) => pageComponent,\n            ),\n          })),\n        );\n    }),\n\n  new: protectedProcedure\n    .meta({ track: Events.CreateMaintenance })\n    .input(\n      z.object({\n        pageId: z.number(),\n        title: z.string(),\n        message: z.string(),\n        startDate: z.coerce.date(),\n        endDate: z.coerce.date(),\n        pageComponents: z.array(z.number()).optional(),\n        notifySubscribers: z.boolean().nullish(),\n      }),\n    )\n    .mutation(async (opts) => {\n      const existingPage = await opts.ctx.db.query.page.findFirst({\n        where: and(\n          eq(page.id, opts.input.pageId),\n          eq(page.workspaceId, opts.ctx.workspace.id),\n        ),\n      });\n\n      if (!existingPage) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Page not found.\",\n        });\n      }\n\n      // Check if the user has access to the page components\n      if (opts.input.pageComponents?.length) {\n        const whereConditions: SQL[] = [\n          eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n          inArray(pageComponent.id, opts.input.pageComponents),\n        ];\n        const pageComponents = await opts.ctx.db\n          .select()\n          .from(pageComponent)\n          .where(and(...whereConditions))\n          .all();\n\n        if (pageComponents.length !== opts.input.pageComponents.length) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: \"You do not have access to all the page components\",\n          });\n        }\n      }\n\n      const newMaintenance = await opts.ctx.db.transaction(async (tx) => {\n        const newMaintenance = await tx\n          .insert(maintenance)\n          .values({\n            pageId: opts.input.pageId,\n            workspaceId: opts.ctx.workspace.id,\n            title: opts.input.title,\n            message: opts.input.message,\n            from: opts.input.startDate,\n            to: opts.input.endDate,\n          })\n          .returning()\n          .get();\n\n        if (opts.input.pageComponents?.length) {\n          await tx.insert(maintenancesToPageComponents).values(\n            opts.input.pageComponents.map((pageComponentId) => ({\n              maintenanceId: newMaintenance.id,\n              pageComponentId,\n            })),\n          );\n        }\n\n        return newMaintenance;\n      });\n\n      return {\n        ...newMaintenance,\n        notifySubscribers: opts.input.notifySubscribers,\n      };\n    }),\n\n  update: protectedProcedure\n    .meta({ track: Events.UpdateMaintenance })\n    .input(\n      z.object({\n        id: z.number(),\n        title: z.string(),\n        message: z.string(),\n        startDate: z.coerce.date(),\n        endDate: z.coerce.date(),\n        pageComponents: z.array(z.number()).optional(),\n      }),\n    )\n    .mutation(async (opts) => {\n      // Check if the user has access to the monitors\n      if (opts.input.pageComponents?.length) {\n        const whereConditions: SQL[] = [\n          eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n          inArray(pageComponent.id, opts.input.pageComponents),\n        ];\n        const pageComponents = await opts.ctx.db\n          .select()\n          .from(pageComponent)\n          .where(and(...whereConditions))\n          .all();\n\n        if (pageComponents.length !== opts.input.pageComponents.length) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: \"You do not have access to all the page components\",\n          });\n        }\n      }\n\n      const existing = await opts.ctx.db.query.maintenance.findFirst({\n        where: and(\n          eq(maintenance.id, opts.input.id),\n          eq(maintenance.workspaceId, opts.ctx.workspace.id),\n        ),\n      });\n\n      if (!existing) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Maintenance not found\",\n        });\n      }\n\n      await opts.ctx.db.transaction(async (tx) => {\n        // Update the maintenance\n        const _maintenance = await tx\n          .update(maintenance)\n          .set({\n            title: opts.input.title,\n            message: opts.input.message,\n            from: opts.input.startDate,\n            to: opts.input.endDate,\n            workspaceId: opts.ctx.workspace.id,\n            updatedAt: new Date(),\n          })\n          .where(\n            and(\n              eq(maintenance.id, opts.input.id),\n              eq(maintenance.workspaceId, opts.ctx.workspace.id),\n            ),\n          )\n          .returning()\n          .get();\n\n        // Delete all existing relations\n        await tx\n          .delete(maintenancesToPageComponents)\n          .where(\n            eq(maintenancesToPageComponents.maintenanceId, _maintenance.id),\n          )\n          .run();\n\n        // Create new relations if page components are provided\n        if (opts.input.pageComponents?.length) {\n          await tx.insert(maintenancesToPageComponents).values(\n            opts.input.pageComponents.map((pageComponentId) => ({\n              maintenanceId: _maintenance.id,\n              pageComponentId,\n            })),\n          );\n        }\n\n        return _maintenance;\n      });\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/member.ts",
    "content": "import { z } from \"zod\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { and, eq } from \"@openstatus/db\";\nimport {\n  selectUserSchema,\n  usersToWorkspaces,\n  workspaceRole,\n} from \"@openstatus/db/src/schema\";\n\nimport { TRPCError } from \"@trpc/server\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const memberRouter = createTRPCRouter({\n  list: protectedProcedure.query(async (opts) => {\n    const result = await opts.ctx.db.query.usersToWorkspaces.findMany({\n      where: eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id),\n      with: {\n        user: true,\n      },\n    });\n\n    return z\n      .object({\n        role: z.enum(workspaceRole),\n        createdAt: z.coerce.date(),\n        user: selectUserSchema,\n      })\n      .array()\n      .parse(result);\n  }),\n\n  delete: protectedProcedure\n    .meta({ track: Events.RemoveUser })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const currentUser = await opts.ctx.db.query.usersToWorkspaces.findFirst({\n        where: and(\n          eq(usersToWorkspaces.userId, opts.ctx.user.id),\n          eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id),\n        ),\n      });\n\n      if (!currentUser) throw new Error(\"No user to workspace found\");\n\n      if (![\"owner\"].includes(currentUser.role)) {\n        throw new TRPCError({\n          code: \"PRECONDITION_FAILED\",\n          message: \"Not authorized to remove user from workspace\",\n        });\n      }\n\n      if (opts.input.id === opts.ctx.user.id) {\n        throw new TRPCError({\n          code: \"PRECONDITION_FAILED\",\n          message: \"Cannot remove yourself from workspace\",\n        });\n      }\n\n      await opts.ctx.db\n        .delete(usersToWorkspaces)\n        .where(\n          and(\n            eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id),\n            eq(usersToWorkspaces.userId, opts.input.id),\n          ),\n        );\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/monitor.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  monitorTag,\n  monitorTagsToMonitors,\n  privateLocation,\n  privateLocationToMonitors,\n} from \"@openstatus/db/src/schema\";\nimport { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { edgeRouter } from \"../edge\";\nimport { createInnerTRPCContext } from \"../trpc\";\n\nlet tagId: number;\nlet otherWorkspacePrivateLocationId: number;\nlet ownWorkspacePrivateLocationId: number;\n\nbeforeAll(async () => {\n  const tag = await db\n    .insert(monitorTag)\n    .values({\n      workspaceId: 1,\n      name: \"idor-test-tag\",\n      color: \"#ff0000\",\n    })\n    .returning()\n    .get();\n  tagId = tag.id;\n\n  const otherLoc = await db\n    .insert(privateLocation)\n    .values({\n      workspaceId: 2,\n      name: \"Other workspace private location\",\n      token: \"monitor-test-other-token\",\n    })\n    .returning()\n    .get();\n  otherWorkspacePrivateLocationId = otherLoc.id;\n\n  const ownLoc = await db\n    .insert(privateLocation)\n    .values({\n      workspaceId: 1,\n      name: \"Own workspace private location\",\n      token: \"monitor-test-own-token\",\n    })\n    .returning()\n    .get();\n  ownWorkspacePrivateLocationId = ownLoc.id;\n});\n\nafterAll(async () => {\n  await db\n    .delete(monitorTagsToMonitors)\n    .where(eq(monitorTagsToMonitors.monitorTagId, tagId));\n  await db.delete(monitorTag).where(eq(monitorTag.id, tagId));\n  await db\n    .delete(privateLocationToMonitors)\n    .where(\n      eq(\n        privateLocationToMonitors.privateLocationId,\n        ownWorkspacePrivateLocationId,\n      ),\n    );\n  await db\n    .delete(privateLocation)\n    .where(eq(privateLocation.id, otherWorkspacePrivateLocationId));\n  await db\n    .delete(privateLocation)\n    .where(eq(privateLocation.id, ownWorkspacePrivateLocationId));\n});\n\ntest(\"monitor.updateTags rejects monitor from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.monitor.updateTags({ id: 5, tags: [] });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n});\n\ntest(\"monitor.updateTags succeeds for own workspace monitor\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  await caller.monitor.updateTags({ id: 1, tags: [tagId] });\n\n  const result = await db.query.monitorTagsToMonitors.findFirst({\n    where: eq(monitorTagsToMonitors.monitorId, 1),\n  });\n  expect(result).toBeDefined();\n  expect(result?.monitorTagId).toBe(tagId);\n});\n\ntest(\"monitor.updateNotifiers rejects monitor from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.monitor.updateNotifiers({ id: 5, notifiers: [] });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n});\n\ntest(\"monitor.updateNotifiers succeeds for own workspace monitor\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  await caller.monitor.updateNotifiers({ id: 1, notifiers: [1] });\n});\n\ntest(\"monitor.updateSchedulingRegions rejects monitor from another workspace\", async () => {\n  // Monitor 5 belongs to workspace 3; try to update it from workspace 1\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1, limits: allPlans.team.limits },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  // Verify the seed private location association exists before the attack\n  const before = await db.query.privateLocationToMonitors.findMany({\n    where: eq(privateLocationToMonitors.monitorId, 5),\n  });\n  const beforeCount = before.length;\n\n  try {\n    await caller.monitor.updateSchedulingRegions({\n      id: 5,\n      regions: [\"ams\"],\n      periodicity: \"30s\",\n      privateLocations: [],\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n\n  // Verify private location associations were NOT deleted\n  const after = await db.query.privateLocationToMonitors.findMany({\n    where: eq(privateLocationToMonitors.monitorId, 5),\n  });\n  expect(after.length).toBe(beforeCount);\n});\n\ntest(\"monitor.updateSchedulingRegions succeeds for own workspace monitor\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1, limits: allPlans.team.limits },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  // Monitor 1 belongs to workspace 1\n  await caller.monitor.updateSchedulingRegions({\n    id: 1,\n    regions: [\"ams\"],\n    periodicity: \"1m\",\n    privateLocations: [],\n  });\n});\n\ntest(\"monitor.updateSchedulingRegions rejects privateLocations from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1, limits: allPlans.team.limits },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.monitor.updateSchedulingRegions({\n      id: 1, // own monitor\n      regions: [\"ams\"],\n      periodicity: \"1m\",\n      privateLocations: [otherWorkspacePrivateLocationId], // workspace 2\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"FORBIDDEN\");\n  }\n});\n\ntest(\"monitor.updateSchedulingRegions succeeds with own workspace privateLocations\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1, limits: allPlans.team.limits },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  await caller.monitor.updateSchedulingRegions({\n    id: 1,\n    regions: [\"ams\"],\n    periodicity: \"1m\",\n    privateLocations: [ownWorkspacePrivateLocationId],\n  });\n\n  const associations = await db.query.privateLocationToMonitors.findMany({\n    where: eq(privateLocationToMonitors.monitorId, 1),\n  });\n  const linked = associations.find(\n    (a) => a.privateLocationId === ownWorkspacePrivateLocationId,\n  );\n  expect(linked).toBeDefined();\n});\n"
  },
  {
    "path": "packages/api/src/router/monitor.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport {\n  type Assertion,\n  DnsRecordAssertion,\n  HeaderAssertion,\n  StatusAssertion,\n  TextBodyAssertion,\n  headerAssertion,\n  jsonBodyAssertion,\n  recordAssertion,\n  serialize,\n  statusAssertion,\n  textBodyAssertion,\n} from \"@openstatus/assertions\";\nimport { type SQL, and, count, eq, inArray, isNull } from \"@openstatus/db\";\nimport {\n  insertMonitorSchema,\n  monitor,\n  monitorJobTypes,\n  monitorMethods,\n  monitorTag,\n  monitorTagsToMonitors,\n  notification,\n  notificationsToMonitors,\n  pageComponent,\n  privateLocation,\n  privateLocationToMonitors,\n  selectIncidentSchema,\n  selectMonitorSchema,\n  selectMonitorTagSchema,\n  selectNotificationSchema,\n  selectPrivateLocationSchema,\n} from \"@openstatus/db/src/schema\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport {\n  freeFlyRegions,\n  monitorPeriodicity,\n  monitorRegions,\n} from \"@openstatus/db/src/schema/constants\";\nimport { regionDict } from \"@openstatus/regions\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\nimport { testDns, testHttp, testTcp } from \"./checker\";\n\nexport const monitorRouter = createTRPCRouter({\n  delete: protectedProcedure\n    .meta({ track: Events.DeleteMonitor })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const monitorToDelete = await opts.ctx.db\n        .select()\n        .from(monitor)\n        .where(\n          and(\n            eq(monitor.id, opts.input.id),\n            eq(monitor.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .get();\n      if (!monitorToDelete) return;\n\n      await opts.ctx.db.transaction(async (tx) => {\n        await tx\n          .update(monitor)\n          .set({ deletedAt: new Date(), active: false })\n          .where(eq(monitor.id, monitorToDelete.id));\n        await tx\n          .delete(monitorTagsToMonitors)\n          .where(eq(monitorTagsToMonitors.monitorId, monitorToDelete.id));\n        await tx\n          .delete(notificationsToMonitors)\n          .where(eq(notificationsToMonitors.monitorId, monitorToDelete.id));\n        await tx\n          .delete(pageComponent)\n          .where(eq(pageComponent.monitorId, monitorToDelete.id));\n      });\n    }),\n\n  deleteMonitors: protectedProcedure\n    .input(z.object({ ids: z.number().array() }))\n    .mutation(async (opts) => {\n      const _monitors = await opts.ctx.db\n        .select()\n        .from(monitor)\n        .where(\n          and(\n            inArray(monitor.id, opts.input.ids),\n            eq(monitor.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .all();\n\n      if (_monitors.length !== opts.input.ids.length) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found.\",\n        });\n      }\n\n      await opts.ctx.db.transaction(async (tx) => {\n        await tx\n          .update(monitor)\n          .set({ deletedAt: new Date(), active: false })\n          .where(inArray(monitor.id, opts.input.ids));\n        await tx\n          .delete(monitorTagsToMonitors)\n          .where(inArray(monitorTagsToMonitors.monitorId, opts.input.ids));\n        await tx\n          .delete(notificationsToMonitors)\n          .where(inArray(notificationsToMonitors.monitorId, opts.input.ids));\n        await tx\n          .delete(pageComponent)\n          .where(inArray(pageComponent.monitorId, opts.input.ids));\n      });\n    }),\n\n  updateMonitors: protectedProcedure\n    .input(\n      insertMonitorSchema\n        .pick({ public: true, active: true })\n        .partial() // batched updates\n        .extend({ ids: z.number().array() }), // array of monitor ids to update\n    )\n    .mutation(async (opts) => {\n      await opts.ctx.db\n        .update(monitor)\n        .set(opts.input)\n        .where(\n          and(\n            inArray(monitor.id, opts.input.ids),\n            eq(monitor.workspaceId, opts.ctx.workspace.id),\n            isNull(monitor.deletedAt),\n          ),\n        );\n    }),\n\n  list: protectedProcedure\n    .input(\n      z\n        .object({\n          order: z.enum([\"asc\", \"desc\"]).optional(),\n        })\n        .optional(),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      const result = await opts.ctx.db.query.monitor.findMany({\n        where: and(...whereConditions),\n        with: {\n          monitorTagsToMonitors: {\n            with: { monitorTag: true },\n          },\n          incidents: {\n            orderBy: (incident, { desc }) => [desc(incident.createdAt)],\n          },\n        },\n        orderBy: (monitor, { asc, desc }) =>\n          opts.input?.order === \"asc\"\n            ? [asc(monitor.active), asc(monitor.createdAt)]\n            : [desc(monitor.active), desc(monitor.createdAt)],\n      });\n\n      return z\n        .array(\n          selectMonitorSchema.extend({\n            tags: z.array(selectMonitorTagSchema).prefault([]),\n            incidents: z.array(selectIncidentSchema).prefault([]),\n          }),\n        )\n        .parse(\n          result.map((data) => ({\n            ...data,\n            tags: data.monitorTagsToMonitors.map((t) => t.monitorTag),\n          })),\n        );\n    }),\n\n  get: protectedProcedure\n    .input(z.object({ id: z.coerce.number() }))\n    .query(async ({ ctx, input }) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, input.id),\n        eq(monitor.workspaceId, ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      const data = await ctx.db.query.monitor.findFirst({\n        where: and(...whereConditions),\n        with: {\n          monitorsToNotifications: {\n            with: { notification: true },\n          },\n          monitorTagsToMonitors: {\n            with: { monitorTag: true },\n          },\n          incidents: true,\n          privateLocationToMonitors: {\n            with: { privateLocation: true },\n          },\n        },\n      });\n\n      if (!data) return null;\n\n      return selectMonitorSchema\n        .extend({\n          notifications: z.array(selectNotificationSchema).prefault([]),\n          tags: z.array(selectMonitorTagSchema).prefault([]),\n          incidents: z.array(selectIncidentSchema).prefault([]),\n          privateLocations: z.array(selectPrivateLocationSchema).prefault([]),\n        })\n        .parse({\n          ...data,\n          notifications: data.monitorsToNotifications.map(\n            (m) => m.notification,\n          ),\n          tags: data.monitorTagsToMonitors.map((t) => t.monitorTag),\n          incidents: data.incidents,\n          privateLocations: data.privateLocationToMonitors.map(\n            (p) => p.privateLocation,\n          ),\n        });\n    }),\n\n  clone: protectedProcedure\n    .meta({ track: Events.CloneMonitor })\n    .input(z.object({ id: z.number() }))\n    .mutation(async ({ ctx, input }) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, input.id),\n        eq(monitor.workspaceId, ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      const _monitors = await ctx.db.query.monitor.findMany({\n        where: and(\n          eq(monitor.workspaceId, ctx.workspace.id),\n          isNull(monitor.deletedAt),\n        ),\n      });\n\n      if (_monitors.length >= ctx.workspace.limits.monitors) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You have reached the maximum number of monitors.\",\n        });\n      }\n\n      const data = await ctx.db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!data) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found.\",\n        });\n      }\n\n      const [newMonitor] = await ctx.db\n        .insert(monitor)\n        .values({\n          ...data,\n          id: undefined, // let the db generate the id\n          name: `${data.name} (Copy)`,\n          createdAt: new Date(),\n          updatedAt: new Date(),\n        })\n        .returning();\n\n      if (!newMonitor) {\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Failed to clone monitor.\",\n        });\n      }\n\n      return newMonitor;\n    }),\n\n  updateRetry: protectedProcedure\n    .meta({ track: Events.UpdateMonitor })\n    .input(z.object({ id: z.number(), retry: z.number() }))\n    .mutation(async ({ ctx, input }) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, input.id),\n        eq(monitor.workspaceId, ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      await ctx.db\n        .update(monitor)\n        .set({ retry: input.retry, updatedAt: new Date() })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updateFollowRedirects: protectedProcedure\n    .meta({ track: Events.UpdateMonitor })\n    .input(z.object({ id: z.number(), followRedirects: z.boolean() }))\n    .mutation(async ({ ctx, input }) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, input.id),\n        eq(monitor.workspaceId, ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      await ctx.db\n        .update(monitor)\n        .set({\n          followRedirects: input.followRedirects,\n          updatedAt: new Date(),\n        })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updateOtel: protectedProcedure\n    .meta({ track: Events.UpdateMonitor })\n    .input(\n      z.object({\n        id: z.number(),\n        otelEndpoint: z.string(),\n        otelHeaders: z\n          .array(z.object({ key: z.string(), value: z.string() }))\n          .optional(),\n      }),\n    )\n    .mutation(async ({ ctx, input }) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, input.id),\n        eq(monitor.workspaceId, ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      await ctx.db\n        .update(monitor)\n        .set({\n          otelEndpoint: input.otelEndpoint,\n          otelHeaders: input.otelHeaders\n            ? JSON.stringify(input.otelHeaders)\n            : undefined,\n          updatedAt: new Date(),\n        })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updatePublic: protectedProcedure\n    .meta({ track: Events.UpdateMonitor })\n    .input(z.object({ id: z.number(), public: z.boolean() }))\n    .mutation(async ({ ctx, input }) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, input.id),\n        eq(monitor.workspaceId, ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      await ctx.db\n        .update(monitor)\n        .set({ public: input.public, updatedAt: new Date() })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updateSchedulingRegions: protectedProcedure\n    .meta({ track: Events.UpdateMonitor })\n    .input(\n      z.object({\n        id: z.number(),\n        regions: z.array(z.string()),\n        periodicity: z.enum(monitorPeriodicity),\n        privateLocations: z.array(z.number()),\n      }),\n    )\n    .mutation(async ({ ctx, input }) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, input.id),\n        eq(monitor.workspaceId, ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      const limits = ctx.workspace.limits;\n\n      if (!limits.periodicity.includes(input.periodicity)) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"Upgrade to check more often.\",\n        });\n      }\n\n      if (limits[\"max-regions\"] < input.regions.length) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You have reached the maximum number of regions.\",\n        });\n      }\n\n      if (\n        input.regions.length > 0 &&\n        !input.regions.every((r) =>\n          limits.regions.includes(r as (typeof limits)[\"regions\"][number]),\n        )\n      ) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You don't have access to this region.\",\n        });\n      }\n\n      const existingMonitor = await ctx.db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!existingMonitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found.\",\n        });\n      }\n\n      if (input.privateLocations && input.privateLocations.length > 0) {\n        const validLocations = await ctx.db.query.privateLocation.findMany({\n          where: and(\n            eq(privateLocation.workspaceId, ctx.workspace.id),\n            inArray(privateLocation.id, input.privateLocations),\n          ),\n        });\n        if (validLocations.length !== input.privateLocations.length) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message: \"Invalid private location IDs.\",\n          });\n        }\n      }\n\n      await ctx.db.transaction(async (tx) => {\n        await tx\n          .update(monitor)\n          .set({\n            regions: input.regions.join(\",\"),\n            periodicity: input.periodicity,\n            updatedAt: new Date(),\n          })\n          .where(and(...whereConditions))\n          .run();\n\n        await tx\n          .delete(privateLocationToMonitors)\n          .where(eq(privateLocationToMonitors.monitorId, input.id));\n\n        if (input.privateLocations && input.privateLocations.length > 0) {\n          await tx.insert(privateLocationToMonitors).values(\n            input.privateLocations.map((privateLocationId) => ({\n              monitorId: input.id,\n              privateLocationId,\n            })),\n          );\n        }\n      });\n    }),\n\n  updateResponseTime: protectedProcedure\n    .meta({ track: Events.UpdateMonitor })\n    .input(\n      z.object({\n        id: z.number(),\n        timeout: z.number(),\n        degradedAfter: z.number().nullish(),\n      }),\n    )\n    .mutation(async ({ ctx, input }) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, input.id),\n        eq(monitor.workspaceId, ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      await ctx.db\n        .update(monitor)\n        .set({\n          timeout: input.timeout,\n          degradedAfter: input.degradedAfter,\n          updatedAt: new Date(),\n        })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updateTags: protectedProcedure\n    .meta({ track: Events.UpdateMonitor })\n    .input(z.object({ id: z.number(), tags: z.array(z.number()) }))\n    .mutation(async ({ ctx, input }) => {\n      const existingMonitor = await ctx.db.query.monitor.findFirst({\n        where: and(\n          eq(monitor.id, input.id),\n          eq(monitor.workspaceId, ctx.workspace.id),\n        ),\n      });\n\n      if (!existingMonitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found.\",\n        });\n      }\n\n      const allTags = await ctx.db.query.monitorTag.findMany({\n        where: and(\n          eq(monitorTag.workspaceId, ctx.workspace.id),\n          inArray(monitorTag.id, input.tags),\n        ),\n      });\n\n      if (allTags.length !== input.tags.length) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You don't have access to this tag.\",\n        });\n      }\n\n      await ctx.db.transaction(async (tx) => {\n        await tx\n          .delete(monitorTagsToMonitors)\n          .where(and(eq(monitorTagsToMonitors.monitorId, input.id)));\n\n        if (input.tags.length > 0) {\n          await tx.insert(monitorTagsToMonitors).values(\n            input.tags.map((tagId) => ({\n              monitorId: input.id,\n              monitorTagId: tagId,\n            })),\n          );\n        }\n      });\n    }),\n\n  updateGeneral: protectedProcedure\n    .meta({ track: Events.UpdateMonitor })\n    .input(\n      z.object({\n        id: z.number(),\n        jobType: z.enum(monitorJobTypes),\n        url: z.string(),\n        method: z.enum(monitorMethods),\n        headers: z.array(z.object({ key: z.string(), value: z.string() })),\n        body: z.string().optional(),\n        name: z.string(),\n        assertions: z.array(\n          z.discriminatedUnion(\"type\", [\n            statusAssertion,\n            headerAssertion,\n            textBodyAssertion,\n            jsonBodyAssertion,\n            recordAssertion,\n          ]),\n        ),\n        active: z.boolean().prefault(true),\n        // skip the test check if assertions are OK\n        skipCheck: z.boolean().prefault(true),\n        // save check in db (iff success? -> e.g. onboarding to get a first ping)\n        saveCheck: z.boolean().prefault(false),\n      }),\n    )\n    .mutation(async ({ ctx, input }) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, input.id),\n        eq(monitor.workspaceId, ctx.workspace.id),\n        isNull(monitor.deletedAt),\n      ];\n\n      const assertions: Assertion[] = [];\n      for (const a of input.assertions ?? []) {\n        if (a.type === \"status\") {\n          assertions.push(new StatusAssertion(a));\n        }\n        if (a.type === \"header\") {\n          assertions.push(new HeaderAssertion(a));\n        }\n        if (a.type === \"textBody\") {\n          assertions.push(new TextBodyAssertion(a));\n        }\n        if (a.type === \"dnsRecord\") {\n          assertions.push(new DnsRecordAssertion(a));\n        }\n      }\n\n      // NOTE: we are checking the endpoint before saving\n      if (!input.skipCheck && input.active) {\n        if (input.jobType === \"http\") {\n          await testHttp({\n            url: input.url,\n            method: input.method,\n            headers: input.headers,\n            body: input.body,\n            // Filter out DNS record assertions as they can't be validated via HTTP\n            assertions: input.assertions.filter((a) => a.type !== \"dnsRecord\"),\n            region: \"ams\",\n          });\n        } else if (input.jobType === \"tcp\") {\n          await testTcp({\n            url: input.url,\n            region: \"ams\",\n          });\n        } else if (input.jobType === \"dns\") {\n          await testDns({\n            url: input.url,\n            region: \"ams\",\n            assertions: input.assertions.filter((a) => a.type === \"dnsRecord\"),\n          });\n        }\n      }\n\n      await ctx.db\n        .update(monitor)\n        .set({\n          name: input.name,\n          jobType: input.jobType,\n          url: input.url,\n          method: input.method,\n          headers: input.headers ? JSON.stringify(input.headers) : undefined,\n          body: input.body,\n          active: input.active,\n          assertions: serialize(assertions),\n          updatedAt: new Date(),\n        })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updateNotifiers: protectedProcedure\n    .meta({ track: Events.UpdateMonitor })\n    .input(z.object({ id: z.number(), notifiers: z.array(z.number()) }))\n    .mutation(async ({ ctx, input }) => {\n      const existingMonitor = await ctx.db.query.monitor.findFirst({\n        where: and(\n          eq(monitor.id, input.id),\n          eq(monitor.workspaceId, ctx.workspace.id),\n        ),\n      });\n\n      if (!existingMonitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found.\",\n        });\n      }\n\n      const allNotifiers = await ctx.db.query.notification.findMany({\n        where: and(\n          eq(notification.workspaceId, ctx.workspace.id),\n          inArray(notification.id, input.notifiers),\n        ),\n      });\n\n      if (allNotifiers.length !== input.notifiers.length) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You don't have access to this notifier.\",\n        });\n      }\n\n      await ctx.db.transaction(async (tx) => {\n        await tx\n          .delete(notificationsToMonitors)\n          .where(and(eq(notificationsToMonitors.monitorId, input.id)));\n\n        if (input.notifiers.length > 0) {\n          await tx.insert(notificationsToMonitors).values(\n            input.notifiers.map((notifierId) => ({\n              monitorId: input.id,\n              notificationId: notifierId,\n            })),\n          );\n        }\n      });\n    }),\n\n  new: protectedProcedure\n    .meta({ track: Events.CreateMonitor, trackProps: [\"url\", \"jobType\"] })\n    .input(\n      z.object({\n        name: z.string(),\n        jobType: z.enum(monitorJobTypes),\n        url: z.string(),\n        method: z.enum(monitorMethods),\n        headers: z.array(z.object({ key: z.string(), value: z.string() })),\n        body: z.string().optional(),\n        assertions: z.array(\n          z.discriminatedUnion(\"type\", [\n            statusAssertion,\n            headerAssertion,\n            textBodyAssertion,\n            jsonBodyAssertion,\n            recordAssertion,\n          ]),\n        ),\n        active: z.boolean().prefault(false),\n        saveCheck: z.boolean().prefault(false),\n        skipCheck: z.boolean().prefault(false),\n      }),\n    )\n    .mutation(async ({ ctx, input }) => {\n      const limits = ctx.workspace.limits;\n\n      const res = await ctx.db\n        .select({ count: count() })\n        .from(monitor)\n        .where(\n          and(\n            eq(monitor.workspaceId, ctx.workspace.id),\n            isNull(monitor.deletedAt),\n          ),\n        )\n        .get();\n\n      // the user has reached the limits\n      if (res && res.count >= limits.monitors) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You reached your monitor limits.\",\n        });\n      }\n\n      const assertions: Assertion[] = [];\n      for (const a of input.assertions ?? []) {\n        if (a.type === \"status\") {\n          assertions.push(new StatusAssertion(a));\n        }\n        if (a.type === \"header\") {\n          assertions.push(new HeaderAssertion(a));\n        }\n        if (a.type === \"textBody\") {\n          assertions.push(new TextBodyAssertion(a));\n        }\n        if (a.type === \"dnsRecord\") {\n          assertions.push(new DnsRecordAssertion(a));\n        }\n      }\n\n      // NOTE: we are checking the endpoint before saving\n      if (!input.skipCheck) {\n        if (input.jobType === \"http\") {\n          await testHttp({\n            url: input.url,\n            method: input.method,\n            headers: input.headers,\n            body: input.body,\n            // Filter out DNS record assertions as they can't be validated via HTTP\n            assertions: input.assertions.filter((a) => a.type !== \"dnsRecord\"),\n            region: \"ams\",\n          });\n        } else if (input.jobType === \"tcp\") {\n          await testTcp({\n            url: input.url,\n            region: \"ams\",\n          });\n        } else if (input.jobType === \"dns\") {\n          await testDns({\n            url: input.url,\n            region: \"ams\",\n            assertions: input.assertions.filter((a) => a.type === \"dnsRecord\"),\n          });\n        }\n      }\n\n      const selectableRegions =\n        ctx.workspace.plan === \"free\" ? freeFlyRegions : monitorRegions;\n      const randomRegions = ctx.workspace.plan === \"free\" ? 4 : 6;\n\n      const regions = [...selectableRegions]\n        // NOTE: make sure we don't use deprecated regions\n        .filter((r) => {\n          const deprecated = regionDict[r].deprecated;\n          if (!deprecated) return true;\n          return false;\n        })\n        .sort(() => 0.5 - Math.random())\n        .slice(0, randomRegions);\n\n      const newMonitor = await ctx.db\n        .insert(monitor)\n        .values({\n          name: input.name,\n          jobType: input.jobType,\n          url: input.url,\n          method: input.method,\n          headers: input.headers ? JSON.stringify(input.headers) : undefined,\n          body: input.body,\n          active: input.active,\n          workspaceId: ctx.workspace.id,\n          periodicity: ctx.workspace.plan === \"free\" ? \"30m\" : \"1m\",\n          regions: regions.join(\",\"),\n          assertions: serialize(assertions),\n          updatedAt: new Date(),\n        })\n        .returning()\n        .get();\n\n      return newMonitor;\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/monitorTag.ts",
    "content": "import { z } from \"zod\";\n\nimport { and, eq, inArray } from \"@openstatus/db\";\nimport { monitorTag } from \"@openstatus/db/src/schema\";\n\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nconst tagSchema = z.object({\n  id: z.number().optional(),\n  name: z.string(),\n  color: z.string(),\n});\n\nexport const monitorTagRouter = createTRPCRouter({\n  list: protectedProcedure.query(async (opts) => {\n    return opts.ctx.db.query.monitorTag.findMany({\n      where: eq(monitorTag.workspaceId, opts.ctx.workspace.id),\n      with: { monitor: true },\n    });\n  }),\n\n  syncTags: protectedProcedure\n    .input(z.array(tagSchema))\n    .mutation(async (opts) => {\n      const { ctx } = opts;\n      const workspaceId = ctx.workspace.id;\n\n      return await ctx.db.transaction(async (tx) => {\n        // Get all existing tags for this workspace\n        const existingTags = await tx.query.monitorTag.findMany({\n          where: eq(monitorTag.workspaceId, workspaceId),\n        });\n\n        // Get IDs of tags that should be kept (have an ID in the input)\n        const keepTagIds = new Set(\n          opts.input\n            .map((tag) => tag.id)\n            .filter((id): id is number => id !== undefined),\n        );\n\n        // Delete tags that are not in the input\n        const tagsToDelete = existingTags.filter(\n          (tag) => !keepTagIds.has(tag.id),\n        );\n        if (tagsToDelete.length > 0) {\n          await tx\n            .delete(monitorTag)\n            .where(\n              and(\n                eq(monitorTag.workspaceId, workspaceId),\n                inArray(\n                  monitorTag.id,\n                  tagsToDelete.map((t) => t.id),\n                ),\n              ),\n            )\n            .run();\n        }\n\n        // Update or create tags\n        const results = await Promise.all(\n          opts.input.map(async (tag) => {\n            if (tag.id) {\n              // Update existing tag\n              return tx\n                .update(monitorTag)\n                .set({\n                  name: tag.name,\n                  color: tag.color,\n                  updatedAt: new Date(),\n                })\n                .where(\n                  and(\n                    eq(monitorTag.workspaceId, workspaceId),\n                    eq(monitorTag.id, tag.id),\n                  ),\n                )\n                .returning()\n                .get();\n            }\n            // Create new tag\n            return tx\n              .insert(monitorTag)\n              .values({\n                name: tag.name,\n                color: tag.color,\n                workspaceId,\n              })\n              .returning()\n              .get();\n          }),\n        );\n\n        console.error(results);\n\n        return results;\n      });\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/notification.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  notification,\n  notificationsToMonitors,\n} from \"@openstatus/db/src/schema\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { edgeRouter } from \"../edge\";\nimport { createInnerTRPCContext } from \"../trpc\";\n\nlet otherWorkspaceNotifId: number;\n\nbeforeAll(async () => {\n  const notif = await db\n    .insert(notification)\n    .values({\n      provider: \"email\",\n      name: \"workspace 2 idor test notification\",\n      data: '{\"email\":\"test@openstatus.dev\"}',\n      workspaceId: 2,\n    })\n    .returning()\n    .get();\n  otherWorkspaceNotifId = notif.id;\n\n  // Link the other workspace's notification to a monitor so we can verify\n  // the junction table is not wiped by a cross-workspace updateNotifier call\n  await db\n    .insert(notificationsToMonitors)\n    .values({ notificationId: otherWorkspaceNotifId, monitorId: 5 })\n    .run();\n});\n\nafterAll(async () => {\n  await db\n    .delete(notificationsToMonitors)\n    .where(eq(notificationsToMonitors.notificationId, otherWorkspaceNotifId));\n  await db\n    .delete(notification)\n    .where(eq(notification.id, otherWorkspaceNotifId));\n});\n\ntest(\"notification.delete does not delete a notification from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  await caller.notification.delete({ id: otherWorkspaceNotifId });\n\n  const notifAfter = await db.query.notification.findFirst({\n    where: eq(notification.id, otherWorkspaceNotifId),\n  });\n  expect(notifAfter).toBeDefined();\n  expect(notifAfter?.id).toBe(otherWorkspaceNotifId);\n});\n\ntest(\"notification.delete succeeds for own workspace notification\", async () => {\n  const tempNotif = await db\n    .insert(notification)\n    .values({\n      provider: \"email\",\n      name: \"temp test notification\",\n      data: '{\"email\":\"temp@openstatus.dev\"}',\n      workspaceId: 1,\n    })\n    .returning()\n    .get();\n\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  await caller.notification.delete({ id: tempNotif.id });\n\n  const notifAfter = await db.query.notification.findFirst({\n    where: eq(notification.id, tempNotif.id),\n  });\n  expect(notifAfter).toBeUndefined();\n});\n\ntest(\"notification.updateNotifier rejects notification from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.notification.updateNotifier({\n      id: otherWorkspaceNotifId,\n      name: \"Hacked notification\",\n      data: { email: \"hacker@evil.com\" },\n      monitors: [],\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n\n  // Verify monitor associations were NOT deleted\n  const associations = await db.query.notificationsToMonitors.findMany({\n    where: eq(notificationsToMonitors.notificationId, otherWorkspaceNotifId),\n  });\n  expect(associations.length).toBe(1);\n});\n"
  },
  {
    "path": "packages/api/src/router/notification.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport { and, count, eq, inArray } from \"@openstatus/db\";\nimport {\n  NotificationDataSchema,\n  googleChatDataSchema,\n  grafanaOncallDataSchema,\n  monitor,\n  notification,\n  notificationProvider,\n  notificationsToMonitors,\n  selectMonitorSchema,\n  selectNotificationSchema,\n  telegramDataSchema,\n  whatsappDataSchema,\n} from \"@openstatus/db/src/schema\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { SchemaError } from \"@openstatus/error\";\nimport { sendTest as sendGoogleChatTest } from \"@openstatus/notification-google-chat\";\nimport { sendTest as sendGrafanaTest } from \"@openstatus/notification-grafana-oncall\";\nimport { sendTest as sendTelegramTest } from \"@openstatus/notification-telegram\";\nimport { sendTest as sendWhatsAppTest } from \"@openstatus/notification-twillio-whatsapp\";\nimport { redis } from \"@openstatus/upstash\";\nimport { nanoid } from \"nanoid\";\n\nimport {\n  type TelegramGetUpdatesResponse,\n  processTelegramUpdates,\n} from \"../service/telegram-updates\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const notificationRouter = createTRPCRouter({\n  list: protectedProcedure.query(async (opts) => {\n    const notifications = await opts.ctx.db.query.notification.findMany({\n      where: eq(notification.workspaceId, opts.ctx.workspace.id),\n      with: {\n        monitor: {\n          with: {\n            monitor: true,\n          },\n        },\n      },\n    });\n\n    return selectNotificationSchema\n      .extend({\n        monitors: selectMonitorSchema.array(),\n      })\n      .array()\n      .parse(\n        notifications.map((notification) => ({\n          ...notification,\n          monitors: notification.monitor.map(({ monitor }) => monitor),\n        })),\n      );\n  }),\n\n  // TODO: rename to update after migration\n  updateNotifier: protectedProcedure\n    .meta({ track: Events.UpdateNotification })\n    .input(\n      z.object({\n        id: z.number(),\n        name: z.string(),\n        data: z.partialRecord(\n          z.enum(notificationProvider),\n          z.string().or(z.record(z.string(), z.string())),\n        ),\n        monitors: z.array(z.number()),\n      }),\n    )\n    .mutation(async (opts) => {\n      const existing = await opts.ctx.db.query.notification.findFirst({\n        where: and(\n          eq(notification.id, opts.input.id),\n          eq(notification.workspaceId, opts.ctx.workspace.id),\n        ),\n      });\n\n      if (!existing) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Notification not found\",\n        });\n      }\n\n      const allMonitors = await opts.ctx.db.query.monitor.findMany({\n        where: and(\n          eq(monitor.workspaceId, opts.ctx.workspace.id),\n          inArray(monitor.id, opts.input.monitors),\n        ),\n      });\n\n      if (allMonitors.length !== opts.input.monitors.length) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You don't have access to all the monitors.\",\n        });\n      }\n\n      const _data = NotificationDataSchema.safeParse(opts.input.data);\n\n      if (!_data.success) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: SchemaError.fromZod(_data.error, opts.input).message,\n        });\n      }\n\n      await opts.ctx.db.transaction(async (tx) => {\n        await tx\n          .update(notification)\n          .set({\n            name: opts.input.name,\n            data: JSON.stringify(opts.input.data),\n            updatedAt: new Date(),\n          })\n          .where(\n            and(\n              eq(notification.id, opts.input.id),\n              eq(notification.workspaceId, opts.ctx.workspace.id),\n            ),\n          );\n\n        await tx\n          .delete(notificationsToMonitors)\n          .where(\n            and(eq(notificationsToMonitors.notificationId, opts.input.id)),\n          );\n\n        if (opts.input.monitors.length) {\n          await tx.insert(notificationsToMonitors).values(\n            opts.input.monitors.map((monitorId) => ({\n              notificationId: opts.input.id,\n              monitorId,\n            })),\n          );\n        }\n      });\n    }),\n\n  new: protectedProcedure\n    .meta({ track: Events.CreateNotification, trackProps: [\"provider\"] })\n    .input(\n      z.object({\n        provider: z.enum(notificationProvider),\n        data: z.partialRecord(\n          z.enum(notificationProvider),\n          z.record(z.string(), z.string()).or(z.string()),\n        ),\n        name: z.string(),\n        monitors: z.array(z.number()).prefault([]),\n      }),\n    )\n    .mutation(async (opts) => {\n      const limits = opts.ctx.workspace.limits;\n\n      const res = await opts.ctx.db\n        .select({ count: count() })\n        .from(notification)\n        .where(eq(notification.workspaceId, opts.ctx.workspace.id))\n        .get();\n\n      // the user has reached the limits\n      if (res && res.count >= limits[\"notification-channels\"]) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You reached your notification limits.\",\n        });\n      }\n\n      const allMonitors = await opts.ctx.db.query.monitor.findMany({\n        where: and(\n          eq(monitor.workspaceId, opts.ctx.workspace.id),\n          inArray(monitor.id, opts.input.monitors),\n        ),\n      });\n\n      if (allMonitors.length !== opts.input.monitors.length) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You don't have access to all the monitors.\",\n        });\n      }\n\n      const limitedProviders = [\n        \"sms\",\n        \"pagerduty\",\n        \"opsgenie\",\n        \"grafana-oncall\",\n        \"whatsapp\",\n      ] as const;\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      if (limitedProviders.includes(opts.input.provider as any)) {\n        const isAllowed =\n          opts.ctx.workspace.limits[\n            opts.input.provider as\n              | \"sms\"\n              | \"pagerduty\"\n              | \"opsgenie\"\n              | \"grafana-oncall\"\n              | \"whatsapp\"\n          ];\n\n        if (!isAllowed) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message: \"Upgrade to use the notification channel.\",\n          });\n        }\n      }\n\n      const _data = NotificationDataSchema.safeParse(opts.input.data);\n\n      if (!_data.success) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: SchemaError.fromZod(_data.error, opts.input).message,\n        });\n      }\n\n      const _notification = await opts.ctx.db.transaction(async (tx) => {\n        const _notification = await tx\n          .insert(notification)\n          .values({\n            name: opts.input.name,\n            provider: opts.input.provider,\n            data: JSON.stringify(opts.input.data),\n            workspaceId: opts.ctx.workspace.id,\n          })\n          .returning()\n          .get();\n\n        if (opts.input.monitors.length) {\n          await tx.insert(notificationsToMonitors).values(\n            opts.input.monitors.map((monitorId) => ({\n              notificationId: _notification.id,\n              monitorId,\n            })),\n          );\n        }\n\n        return _notification;\n      });\n\n      return _notification;\n    }),\n\n  delete: protectedProcedure\n    .meta({ track: Events.DeleteNotification })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      await opts.ctx.db\n        .delete(notification)\n        .where(\n          and(\n            eq(notification.id, opts.input.id),\n            eq(notification.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .run();\n    }),\n\n  sendTest: protectedProcedure\n    .input(\n      z.object({\n        provider: z.enum(notificationProvider),\n        data: z.partialRecord(\n          z.enum(notificationProvider),\n          z.record(z.string(), z.string()).or(z.string()),\n        ),\n      }),\n    )\n    .mutation(async (opts) => {\n      if (opts.input.provider === \"telegram\") {\n        const _data = telegramDataSchema.safeParse(opts.input.data);\n        if (!_data.success) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: SchemaError.fromZod(_data.error, opts.input).message,\n          });\n        }\n        await sendTelegramTest({\n          chatId: _data.data.telegram.chatId,\n        });\n\n        return;\n      }\n      if (opts.input.provider === \"whatsapp\") {\n        const _data = whatsappDataSchema.safeParse(opts.input.data);\n        if (!_data.success) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: SchemaError.fromZod(_data.error, opts.input).message,\n          });\n        }\n        await sendWhatsAppTest({ phoneNumber: _data.data.whatsapp });\n\n        return;\n      }\n      if (opts.input.provider === \"google-chat\") {\n        const _data = googleChatDataSchema.safeParse(opts.input.data);\n        if (!_data.success) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: SchemaError.fromZod(_data.error, opts.input).message,\n          });\n        }\n\n        await sendGoogleChatTest(_data.data[\"google-chat\"]);\n        return;\n      }\n      if (opts.input.provider === \"grafana-oncall\") {\n        const _data = grafanaOncallDataSchema.safeParse(opts.input.data);\n        if (!_data.success) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: SchemaError.fromZod(_data.error, opts.input).message,\n          });\n        }\n\n        await sendGrafanaTest(_data.data[\"grafana-oncall\"]);\n        return;\n      }\n\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Invalid provider\",\n      });\n    }),\n\n  createTelegramToken: protectedProcedure.query(async (opts) => {\n    const workspaceId = opts.ctx.workspace.id;\n    const randomId = nanoid(12);\n    const EXPIRY = 1800; // 30 minutes\n\n    await redis.set(`telegram:workspace_token:${workspaceId}`, randomId, {\n      ex: EXPIRY,\n    });\n\n    return { token: randomId };\n  }),\n\n  getTelegramUpdates: protectedProcedure\n    .input(\n      z\n        .object({\n          privateChatId: z.string().optional(),\n          since: z.number().optional(),\n        })\n        .optional(),\n    )\n    .query(async (opts) => {\n      const res = await fetch(\n        `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/getUpdates`,\n      );\n      const data = (await res.json()) as TelegramGetUpdatesResponse;\n      if (!data.ok || !data.result) return [];\n\n      const botUsername = process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME;\n      if (!botUsername) {\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Telegram bot username not configured\",\n        });\n      }\n\n      return processTelegramUpdates({\n        updates: data.result,\n        workspaceId: opts.ctx.workspace.id,\n        privateChatId: opts.input?.privateChatId,\n        since: opts.input?.since,\n        botUsername,\n        redisClient: redis,\n      });\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/page.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport { type SQL, and, desc, eq, inArray, isNull, sql } from \"@openstatus/db\";\nimport {\n  insertPageSchema,\n  monitor,\n  page,\n  pageAccessTypes,\n  pageComponent,\n  selectMaintenanceSchema,\n  selectPageComponentGroupSchema,\n  selectPageComponentSchema,\n  selectPageSchema,\n  subdomainSafeList,\n} from \"@openstatus/db/src/schema\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { env } from \"../env\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nif (process.env.NODE_ENV === \"test\") {\n  require(\"../test/preload\");\n}\n\n// Helper functions to reuse Vercel API logic\nasync function addDomainToVercel(domain: string) {\n  const data = await fetch(\n    `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains?teamId=${env.TEAM_ID_VERCEL}`,\n    {\n      body: JSON.stringify({ name: domain }),\n      headers: {\n        Authorization: `Bearer ${env.VERCEL_AUTH_BEARER_TOKEN}`,\n        \"Content-Type\": \"application/json\",\n      },\n      method: \"POST\",\n    },\n  );\n  return data.json();\n}\n\nasync function removeDomainFromVercel(domain: string) {\n  const data = await fetch(\n    `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${domain}?teamId=${env.TEAM_ID_VERCEL}`,\n    {\n      headers: {\n        Authorization: `Bearer ${env.VERCEL_AUTH_BEARER_TOKEN}`,\n      },\n      method: \"DELETE\",\n    },\n  );\n  return data.json();\n}\n\nexport const pageRouter = createTRPCRouter({\n  create: protectedProcedure\n    .meta({ track: Events.CreatePage, trackProps: [\"slug\"] })\n    .input(insertPageSchema)\n    .mutation(async (opts) => {\n      const { monitors, workspaceId, id, configuration, ...pageProps } =\n        opts.input;\n\n      const monitorIds = monitors?.map((item) => item.monitorId) || [];\n\n      const pageNumbers = (\n        await opts.ctx.db.query.page.findMany({\n          where: eq(page.workspaceId, opts.ctx.workspace.id),\n        })\n      ).length;\n\n      const limit = opts.ctx.workspace.limits;\n\n      // the user has reached the status page number limits\n      if (pageNumbers >= limit[\"status-pages\"]) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You reached your status-page limits.\",\n        });\n      }\n\n      // the user is not eligible for password protection\n      if (\n        limit[\"password-protection\"] === false &&\n        opts.input.passwordProtected === true\n      ) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message:\n            \"Password protection is not available for your current plan.\",\n        });\n      }\n\n      const newPage = await opts.ctx.db\n        .insert(page)\n        .values({\n          workspaceId: opts.ctx.workspace.id,\n          configuration: JSON.stringify(configuration),\n          ...pageProps,\n          authEmailDomains: pageProps.authEmailDomains?.join(\",\"),\n        })\n        .returning()\n        .get();\n\n      if (monitorIds.length) {\n        // We should make sure the user has access to the monitors AND they are active\n        const allMonitors = await opts.ctx.db.query.monitor.findMany({\n          where: and(\n            inArray(monitor.id, monitorIds),\n            eq(monitor.workspaceId, opts.ctx.workspace.id),\n            eq(monitor.active, true), // Only allow active monitors\n            isNull(monitor.deletedAt),\n          ),\n        });\n\n        if (allMonitors.length !== monitorIds.length) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message:\n              \"You don't have access to all the monitors or some monitors are inactive.\",\n          });\n        }\n\n        // Build a map for quick lookup\n        const monitorMap = new Map(allMonitors.map((m) => [m.id, m]));\n\n        // Build pageComponent values (primary table)\n        const pageComponentValues = monitors\n          .map(({ monitorId }, index) => {\n            const m = monitorMap.get(monitorId);\n            if (!m || !m.workspaceId) return null;\n            return {\n              workspaceId: m.workspaceId,\n              pageId: newPage.id,\n              type: \"monitor\" as const,\n              monitorId,\n              name: m.externalName || m.name,\n              order: index,\n              groupId: null,\n              groupOrder: 0,\n            };\n          })\n          .filter((v): v is NonNullable<typeof v> => v !== null);\n\n        // Insert into pageComponents (primary table)\n        await opts.ctx.db\n          .insert(pageComponent)\n          .values(pageComponentValues)\n          .run();\n      }\n\n      return newPage;\n    }),\n\n  delete: protectedProcedure\n    .meta({ track: Events.DeletePage })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(page.id, opts.input.id),\n        eq(page.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      await opts.ctx.db\n        .delete(page)\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  getSlugUniqueness: protectedProcedure\n    .input(z.object({ slug: z.string().toLowerCase() }))\n    .query(async (opts) => {\n      // had filter on some words we want to keep for us\n      if (subdomainSafeList.includes(opts.input.slug)) {\n        return false;\n      }\n      const result = await opts.ctx.db.query.page.findMany({\n        where: sql`lower(${page.slug}) = ${opts.input.slug}`,\n      });\n      return !(result?.length > 0);\n    }),\n\n  list: protectedProcedure\n    .input(\n      z\n        .object({\n          order: z.enum([\"asc\", \"desc\"]).optional(),\n        })\n        .optional(),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(page.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      const result = await opts.ctx.db.query.page.findMany({\n        where: and(...whereConditions),\n        with: {\n          statusReports: true,\n        },\n        orderBy: (pages, { asc }) => [\n          opts.input?.order === \"asc\"\n            ? asc(pages.createdAt)\n            : desc(pages.createdAt),\n        ],\n      });\n\n      return result;\n    }),\n\n  get: protectedProcedure\n    .input(z.object({ id: z.number() }))\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(page.workspaceId, opts.ctx.workspace.id),\n        eq(page.id, opts.input.id),\n      ];\n\n      const data = await opts.ctx.db.query.page.findFirst({\n        where: and(...whereConditions),\n        with: {\n          maintenances: true,\n          pageComponents: true,\n          pageComponentGroups: true,\n        },\n      });\n\n      return selectPageSchema\n        .extend({\n          pageComponentGroups: z\n            .array(selectPageComponentGroupSchema)\n            .prefault([]),\n          maintenances: z.array(selectMaintenanceSchema).prefault([]),\n          pageComponents: z.array(selectPageComponentSchema).prefault([]),\n        })\n        .parse({\n          ...data,\n          pageComponentGroups: data?.pageComponentGroups ?? [],\n          maintenances: data?.maintenances,\n          pageComponents: data?.pageComponents,\n        });\n    }),\n\n  // TODO: rename to create\n  new: protectedProcedure\n    .meta({ track: Events.CreatePage, trackProps: [\"slug\"] })\n    .input(\n      z.object({\n        title: z.string(),\n        slug: z.string().toLowerCase(),\n        icon: z.string().nullish(),\n        description: z.string().nullish(),\n      }),\n    )\n    .mutation(async (opts) => {\n      const pageNumbers = (\n        await opts.ctx.db.query.page.findMany({\n          where: eq(page.workspaceId, opts.ctx.workspace.id),\n        })\n      ).length;\n\n      const limit = opts.ctx.workspace.limits;\n\n      // the user has reached the status page number limits\n      if (pageNumbers >= limit[\"status-pages\"]) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You reached your status-page limits.\",\n        });\n      }\n\n      const result = await opts.ctx.db.query.page.findMany({\n        where: sql`lower(${page.slug}) = ${opts.input.slug}`,\n      });\n\n      if (subdomainSafeList.includes(opts.input.slug) || result?.length > 0) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"This slug is already taken. Please choose another one.\",\n        });\n      }\n\n      // REMINDER: default config from legacy page\n      const defaultConfiguration = {\n        type: \"absolute\",\n        value: \"requests\",\n        uptime: true,\n        theme: \"default-rounded\",\n      } satisfies Record<string, string | boolean | undefined>;\n\n      const newPage = await opts.ctx.db\n        .insert(page)\n        .values({\n          workspaceId: opts.ctx.workspace.id,\n          title: opts.input.title,\n          slug: opts.input.slug,\n          description: opts.input.description ?? \"\",\n          icon: opts.input.icon ?? \"\",\n          legacyPage: false,\n          configuration: defaultConfiguration,\n          customDomain: \"\", // TODO: make nullable\n        })\n        .returning()\n        .get();\n\n      return newPage;\n    }),\n\n  updateGeneral: protectedProcedure\n    .meta({ track: Events.UpdatePage })\n    .input(\n      z.object({\n        id: z.number(),\n        title: z.string(),\n        slug: z.string().toLowerCase(),\n        description: z.string().nullish(),\n        icon: z.string().nullish(),\n      }),\n    )\n    .mutation(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(page.workspaceId, opts.ctx.workspace.id),\n        eq(page.id, opts.input.id),\n      ];\n\n      const result = await opts.ctx.db.query.page.findMany({\n        where: sql`lower(${page.slug}) = ${opts.input.slug}`,\n      });\n\n      const oldSlug = await opts.ctx.db.query.page.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (\n        subdomainSafeList.includes(opts.input.slug) ||\n        (oldSlug?.slug !== opts.input.slug && result?.length > 0)\n      ) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"This slug is already taken. Please choose another one.\",\n        });\n      }\n\n      await opts.ctx.db\n        .update(page)\n        .set({\n          title: opts.input.title,\n          slug: opts.input.slug,\n          description: opts.input.description ?? \"\",\n          icon: opts.input.icon ?? \"\",\n          updatedAt: new Date(),\n        })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updateCustomDomain: protectedProcedure\n    .meta({ track: Events.UpdatePageDomain, trackProps: [\"customDomain\"] })\n    .input(z.object({ id: z.number(), customDomain: z.string().toLowerCase() }))\n    .mutation(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(page.workspaceId, opts.ctx.workspace.id),\n        eq(page.id, opts.input.id),\n      ];\n\n      if (opts.input.customDomain.includes(\"openstatus\")) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Domain cannot contain 'openstatus'\",\n        });\n      }\n\n      // Get the current page to check the existing custom domain\n      const currentPage = await opts.ctx.db.query.page.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!currentPage) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Page not found\",\n        });\n      }\n\n      const oldDomain = currentPage.customDomain;\n      const newDomain = opts.input.customDomain;\n\n      try {\n        // Handle domain changes\n        if (newDomain && !oldDomain) {\n          // Adding a new domain\n          await opts.ctx.db\n            .update(page)\n            .set({ customDomain: newDomain, updatedAt: new Date() })\n            .where(and(...whereConditions))\n            .run();\n\n          // Add domain to Vercel using the domain router logic\n          await addDomainToVercel(newDomain);\n        } else if (oldDomain && newDomain !== oldDomain) {\n          // Changing domain - remove old and add new\n          await opts.ctx.db\n            .update(page)\n            .set({ customDomain: newDomain, updatedAt: new Date() })\n            .where(and(...whereConditions))\n            .run();\n\n          // Remove old domain from Vercel\n          await removeDomainFromVercel(oldDomain);\n\n          // Add new domain to Vercel\n          if (newDomain) {\n            await addDomainToVercel(newDomain);\n          }\n        } else if (oldDomain && newDomain === \"\") {\n          // Removing domain\n          await opts.ctx.db\n            .update(page)\n            .set({ customDomain: \"\", updatedAt: new Date() })\n            .where(and(...whereConditions))\n            .run();\n\n          // Remove domain from Vercel\n          await removeDomainFromVercel(oldDomain);\n        } else {\n          // No change needed, just update the database\n          await opts.ctx.db\n            .update(page)\n            .set({ customDomain: newDomain, updatedAt: new Date() })\n            .where(and(...whereConditions))\n            .run();\n        }\n      } catch (error) {\n        // If Vercel operations fail, we should rollback the database change\n        // For now, we'll just throw the error\n        console.error(\"Error updating custom domain:\", error);\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Failed to update custom domain\",\n        });\n      }\n    }),\n\n  updatePasswordProtection: protectedProcedure\n    .meta({ track: Events.UpdatePage })\n    .input(\n      z.object({\n        id: z.number(),\n        accessType: z.enum(pageAccessTypes),\n        authEmailDomains: z.array(z.string()).nullish(),\n        password: z.string().nullish(),\n      }),\n    )\n    .mutation(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(page.workspaceId, opts.ctx.workspace.id),\n        eq(page.id, opts.input.id),\n      ];\n\n      const limit = opts.ctx.workspace.limits;\n\n      // the user is not eligible for password protection\n      if (\n        limit[\"password-protection\"] === false &&\n        opts.input.accessType === \"password\"\n      ) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message:\n            \"Password protection is not available for your current plan.\",\n        });\n      }\n\n      if (\n        limit[\"email-domain-protection\"] === false &&\n        opts.input.accessType === \"email-domain\"\n      ) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message:\n            \"Email domain protection is not available for your current plan.\",\n        });\n      }\n\n      await opts.ctx.db\n        .update(page)\n        .set({\n          accessType: opts.input.accessType,\n          authEmailDomains: opts.input.authEmailDomains?.join(\",\"),\n          password: opts.input.password,\n          updatedAt: new Date(),\n        })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updateAppearance: protectedProcedure\n    .meta({ track: Events.UpdatePage })\n    .input(\n      z.object({\n        id: z.number(),\n        forceTheme: z.enum([\"light\", \"dark\", \"system\"]),\n        configuration: z.object({\n          theme: z.string(),\n        }),\n      }),\n    )\n    .mutation(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(page.workspaceId, opts.ctx.workspace.id),\n        eq(page.id, opts.input.id),\n      ];\n\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!_page) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Page not found\",\n        });\n      }\n\n      const currentConfiguration =\n        (typeof _page.configuration === \"object\" &&\n          _page.configuration !== null &&\n          _page.configuration) ||\n        {};\n      const updatedConfiguration = {\n        ...currentConfiguration,\n        theme: opts.input.configuration.theme,\n      };\n\n      await opts.ctx.db\n        .update(page)\n        .set({\n          forceTheme: opts.input.forceTheme,\n          configuration: updatedConfiguration,\n          updatedAt: new Date(),\n        })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updateLinks: protectedProcedure\n    .meta({ track: Events.UpdatePage })\n    .input(\n      z.object({\n        id: z.number(),\n        homepageUrl: z.string().nullish(),\n        contactUrl: z.string().nullish(),\n      }),\n    )\n    .mutation(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(page.workspaceId, opts.ctx.workspace.id),\n        eq(page.id, opts.input.id),\n      ];\n\n      await opts.ctx.db\n        .update(page)\n        .set({\n          homepageUrl: opts.input.homepageUrl,\n          contactUrl: opts.input.contactUrl,\n          updatedAt: new Date(),\n        })\n        .where(and(...whereConditions))\n        .run();\n    }),\n\n  updatePageConfiguration: protectedProcedure\n    .meta({ track: Events.UpdatePage })\n    .input(\n      z.object({\n        id: z.number(),\n        configuration: z\n          .record(z.string(), z.string().or(z.boolean()).optional())\n          .nullish(),\n      }),\n    )\n    .mutation(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(page.workspaceId, opts.ctx.workspace.id),\n        eq(page.id, opts.input.id),\n      ];\n\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!_page) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Page not found\",\n        });\n      }\n\n      const currentConfiguration =\n        (typeof _page.configuration === \"object\" &&\n          _page.configuration !== null &&\n          _page.configuration) ||\n        {};\n      const updatedConfiguration = {\n        ...currentConfiguration,\n        ...opts.input.configuration,\n      };\n\n      await opts.ctx.db\n        .update(page)\n        .set({\n          configuration: updatedConfiguration,\n          updatedAt: new Date(),\n        })\n        .where(and(...whereConditions))\n        .run();\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/pageComponent.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\nimport { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { edgeRouter } from \"../edge\";\nimport { createInnerTRPCContext } from \"../trpc\";\n\nlet otherWorkspaceMonitorId: number;\n\nbeforeAll(async () => {\n  const m = await db\n    .insert(monitor)\n    .values({\n      workspaceId: 2,\n      name: \"Other workspace monitor\",\n      jobType: \"http\",\n      periodicity: \"1m\",\n      url: \"https://example.com\",\n      regions: \"ams\",\n      active: true,\n    })\n    .returning()\n    .get();\n  otherWorkspaceMonitorId = m.id;\n});\n\nafterAll(async () => {\n  await db.delete(monitor).where(eq(monitor.id, otherWorkspaceMonitorId));\n});\n\ntest(\"pageComponent.updateOrder rejects monitorId from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1, limits: allPlans.team.limits },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.pageComponent.updateOrder({\n      pageId: 1, // page 1 belongs to workspace 1\n      components: [\n        {\n          id: 1,\n          monitorId: otherWorkspaceMonitorId, // monitor from workspace 2\n          order: 0,\n          name: \"Injected Monitor\",\n          type: \"monitor\",\n        },\n      ],\n      groups: [],\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"FORBIDDEN\");\n  }\n});\n\ntest(\"pageComponent.updateOrder rejects monitorId from another workspace in groups\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1, limits: allPlans.team.limits },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.pageComponent.updateOrder({\n      pageId: 1,\n      components: [],\n      groups: [\n        {\n          order: 0,\n          name: \"Injected Group\",\n          components: [\n            {\n              monitorId: otherWorkspaceMonitorId, // monitor from workspace 2\n              order: 0,\n              name: \"Injected Monitor in Group\",\n              type: \"monitor\",\n            },\n          ],\n        },\n      ],\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"FORBIDDEN\");\n  }\n});\n\ntest(\"pageComponent.updateOrder succeeds with own workspace monitorId\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1, limits: allPlans.team.limits },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  const result = await caller.pageComponent.updateOrder({\n    pageId: 1,\n    components: [\n      {\n        monitorId: 1, // monitor 1 belongs to workspace 1\n        order: 0,\n        name: \"OpenStatus Monitor\",\n        type: \"monitor\",\n      },\n      {\n        monitorId: 2, // monitor 2 belongs to workspace 1\n        order: 1,\n        name: \"Google Monitor\",\n        type: \"monitor\",\n      },\n    ],\n    groups: [],\n  });\n\n  expect(result).toEqual({ success: true });\n});\n"
  },
  {
    "path": "packages/api/src/router/pageComponent.ts",
    "content": "import { z } from \"zod\";\n\nimport { type SQL, and, asc, desc, eq, inArray, ne, sql } from \"@openstatus/db\";\nimport {\n  monitor,\n  page,\n  pageComponent,\n  pageComponentGroup,\n  selectMaintenanceSchema,\n  selectMonitorSchema,\n  selectPageComponentGroupSchema,\n  selectPageComponentSchema,\n  selectStatusReportSchema,\n} from \"@openstatus/db/src/schema\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { TRPCError } from \"@trpc/server\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const pageComponentRouter = createTRPCRouter({\n  list: protectedProcedure\n    .input(\n      z\n        .object({\n          pageId: z.number().optional(),\n          order: z.enum([\"asc\", \"desc\"]).optional(),\n        })\n        .optional(),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      if (opts.input?.pageId) {\n        whereConditions.push(eq(pageComponent.pageId, opts.input.pageId));\n      }\n\n      const result = await opts.ctx.db.query.pageComponent.findMany({\n        where: and(...whereConditions),\n        orderBy:\n          opts.input?.order === \"desc\"\n            ? desc(pageComponent.order)\n            : asc(pageComponent.order),\n        with: {\n          monitor: true,\n          group: true,\n          statusReportsToPageComponents: {\n            with: {\n              statusReport: true,\n            },\n            orderBy: (statusReportsToPageComponents, { desc }) =>\n              desc(statusReportsToPageComponents.createdAt),\n          },\n          maintenancesToPageComponents: {\n            with: {\n              maintenance: true,\n            },\n            orderBy: (maintenancesToPageComponents, { desc }) =>\n              desc(maintenancesToPageComponents.createdAt),\n          },\n        },\n      });\n\n      // Transform and parse the result to flatten the junction tables\n      return selectPageComponentSchema\n        .extend({\n          monitor: selectMonitorSchema.nullish(),\n          group: selectPageComponentGroupSchema.nullish(),\n          statusReports: z.array(selectStatusReportSchema).default([]),\n          maintenances: z.array(selectMaintenanceSchema).default([]),\n        })\n        .array()\n        .parse(\n          result.map((component) => ({\n            ...component,\n            statusReports:\n              component.statusReportsToPageComponents?.map(\n                (sr) => sr.statusReport,\n              ) ?? [],\n            maintenances:\n              component.maintenancesToPageComponents?.map(\n                (m) => m.maintenance,\n              ) ?? [],\n          })),\n        );\n    }),\n\n  delete: protectedProcedure\n    .meta({ track: Events.DeletePageComponent, trackProps: [\"id\"] })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      return await opts.ctx.db\n        .delete(pageComponent)\n        .where(\n          and(\n            eq(pageComponent.id, opts.input.id),\n            eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .returning();\n    }),\n\n  updateOrder: protectedProcedure\n    .meta({ track: Events.UpdatePageComponentOrder, trackProps: [\"pageId\"] })\n    .input(\n      z.object({\n        pageId: z.number(),\n        components: z.array(\n          z.object({\n            id: z.number().optional(), // Optional for new components\n            monitorId: z.number().nullish(),\n            order: z.number(),\n            name: z.string(),\n            description: z.string().nullish(),\n            type: z.enum([\"monitor\", \"static\"]),\n          }),\n        ),\n        groups: z.array(\n          z.object({\n            order: z.number(),\n            name: z.string(),\n            components: z.array(\n              z.object({\n                id: z.number().optional(), // Optional for new components\n                monitorId: z.number().nullish(),\n                order: z.number(),\n                name: z.string(),\n                description: z.string().nullish(),\n                type: z.enum([\"monitor\", \"static\"]),\n              }),\n            ),\n          }),\n        ),\n      }),\n    )\n    .mutation(async (opts) => {\n      await opts.ctx.db.transaction(async (tx) => {\n        // Verify the page belongs to the current workspace\n        const ownedPage = await tx\n          .select({ id: page.id })\n          .from(page)\n          .where(\n            and(\n              eq(page.id, opts.input.pageId),\n              eq(page.workspaceId, opts.ctx.workspace.id),\n            ),\n          )\n          .get();\n\n        if (!ownedPage) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message: \"You don't have access to this page.\",\n          });\n        }\n\n        const pageComponentLimit = opts.ctx.workspace.limits[\"page-components\"];\n\n        // Get existing state\n        const existingComponents = await tx\n          .select()\n          .from(pageComponent)\n          .where(\n            and(\n              eq(pageComponent.pageId, opts.input.pageId),\n              eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n            ),\n          )\n          .all();\n\n        // Count components on OTHER pages in this workspace\n        const otherPagesComponentCount = await tx\n          .select({ id: pageComponent.id })\n          .from(pageComponent)\n          .where(\n            and(\n              eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n              ne(pageComponent.pageId, opts.input.pageId),\n            ),\n          )\n          .all();\n\n        const inputComponentCount =\n          opts.input.components.length +\n          opts.input.groups.reduce((sum, g) => sum + g.components.length, 0);\n\n        const totalAfterUpdate =\n          otherPagesComponentCount.length + inputComponentCount;\n\n        if (totalAfterUpdate > pageComponentLimit) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message: \"You reached your page component limits.\",\n          });\n        }\n\n        const existingGroups = await tx\n          .select()\n          .from(pageComponentGroup)\n          .where(\n            and(\n              eq(pageComponentGroup.pageId, opts.input.pageId),\n              eq(pageComponentGroup.workspaceId, opts.ctx.workspace.id),\n            ),\n          )\n          .all();\n\n        const existingGroupIds = existingGroups.map((g) => g.id);\n\n        // Collect all monitorIds from input (for monitor-type components)\n        const inputMonitorIds = [\n          ...opts.input.components\n            .filter((c) => c.type === \"monitor\" && c.monitorId)\n            .map((c) => c.monitorId),\n          ...opts.input.groups.flatMap((g) =>\n            g.components\n              .filter((c) => c.type === \"monitor\" && c.monitorId)\n              .map((c) => c.monitorId),\n          ),\n        ] as number[];\n\n        if (inputMonitorIds.length > 0) {\n          const validMonitors = await tx.query.monitor.findMany({\n            where: and(\n              eq(monitor.workspaceId, opts.ctx.workspace.id),\n              inArray(monitor.id, inputMonitorIds),\n            ),\n          });\n          if (validMonitors.length !== inputMonitorIds.length) {\n            throw new TRPCError({\n              code: \"FORBIDDEN\",\n              message: \"Invalid monitor IDs.\",\n            });\n          }\n        }\n\n        // Collect IDs for static components that have IDs in input\n        const inputStaticComponentIds = [\n          ...opts.input.components\n            .filter((c) => c.type === \"static\" && c.id)\n            .map((c) => c.id),\n          ...opts.input.groups.flatMap((g) =>\n            g.components\n              .filter((c) => c.type === \"static\" && c.id)\n              .map((c) => c.id),\n          ),\n        ] as number[];\n\n        // Find components that are being removed\n        // For monitor components: those with monitorIds not in the input\n        // For static components with IDs: those with IDs not in the input\n        // For static components without IDs in input: delete all existing static components\n        const removedMonitorComponents = existingComponents.filter(\n          (c) =>\n            c.type === \"monitor\" &&\n            c.monitorId &&\n            !inputMonitorIds.includes(c.monitorId),\n        );\n\n        const hasStaticComponentsInInput =\n          opts.input.components.some((c) => c.type === \"static\") ||\n          opts.input.groups.some((g) =>\n            g.components.some((c) => c.type === \"static\"),\n          );\n\n        // If input has static components but they don't have IDs, we need to delete old ones\n        // If input has static components with IDs, only delete those not in input\n        const removedStaticComponents = existingComponents.filter((c) => {\n          if (c.type !== \"static\") return false;\n          // If we have static components in input\n          if (hasStaticComponentsInInput) {\n            // If the input has IDs, only remove those not in the list\n            if (inputStaticComponentIds.length > 0) {\n              return !inputStaticComponentIds.includes(c.id);\n            }\n            // If input doesn't have IDs, remove all existing static components\n            return true;\n          }\n          // If no static components in input at all, remove existing ones\n          return true;\n        });\n\n        const removedComponentIds = [\n          ...removedMonitorComponents.map((c) => c.id),\n          ...removedStaticComponents.map((c) => c.id),\n        ];\n\n        // Delete removed components\n        if (removedComponentIds.length > 0) {\n          await tx\n            .delete(pageComponent)\n            .where(\n              and(\n                eq(pageComponent.pageId, opts.input.pageId),\n                eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n                inArray(pageComponent.id, removedComponentIds),\n              ),\n            );\n        }\n\n        // Clear groupId from all components before deleting groups\n        // This prevents foreign key constraint errors\n        if (existingGroupIds.length > 0) {\n          await tx\n            .update(pageComponent)\n            .set({ groupId: null })\n            .where(\n              and(\n                eq(pageComponent.pageId, opts.input.pageId),\n                eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n                inArray(pageComponent.groupId, existingGroupIds),\n              ),\n            );\n        }\n\n        // Delete old groups and create new ones\n        if (existingGroupIds.length > 0) {\n          await tx\n            .delete(pageComponentGroup)\n            .where(\n              and(\n                eq(pageComponentGroup.pageId, opts.input.pageId),\n                eq(pageComponentGroup.workspaceId, opts.ctx.workspace.id),\n              ),\n            );\n        }\n\n        // Create new groups\n        const newGroups: Array<{ id: number; name: string }> = [];\n        if (opts.input.groups.length > 0) {\n          const createdGroups = await tx\n            .insert(pageComponentGroup)\n            .values(\n              opts.input.groups.map((g) => ({\n                pageId: opts.input.pageId,\n                workspaceId: opts.ctx.workspace.id,\n                name: g.name,\n              })),\n            )\n            .returning();\n          newGroups.push(...createdGroups);\n        }\n\n        // Prepare values for upsert - both grouped and ungrouped components\n        const groupComponentValues = opts.input.groups.flatMap((g, i) =>\n          g.components.map((c) => ({\n            id: c.id, // Will be undefined for new components\n            pageId: opts.input.pageId,\n            workspaceId: opts.ctx.workspace.id,\n            name: c.name,\n            description: c.description,\n            type: c.type,\n            monitorId: c.monitorId,\n            order: g.order,\n            groupId: newGroups[i].id,\n            groupOrder: c.order,\n          })),\n        );\n\n        const standaloneComponentValues = opts.input.components.map((c) => ({\n          id: c.id, // Will be undefined for new components\n          pageId: opts.input.pageId,\n          workspaceId: opts.ctx.workspace.id,\n          name: c.name,\n          description: c.description,\n          type: c.type,\n          monitorId: c.monitorId,\n          order: c.order,\n          groupId: null as number | null,\n          groupOrder: null as number | null,\n        }));\n\n        const allComponentValues = [\n          ...groupComponentValues,\n          ...standaloneComponentValues,\n        ];\n\n        // Separate monitor and static components for different upsert strategies\n        const monitorComponents = allComponentValues.filter(\n          (c) => c.type === \"monitor\" && c.monitorId,\n        );\n        const staticComponents = allComponentValues.filter(\n          (c) => c.type === \"static\",\n        );\n\n        // Upsert monitor components using SQL-level conflict resolution\n        // This uses the (pageId, monitorId) unique constraint to preserve component IDs\n        if (monitorComponents.length > 0) {\n          await tx\n            .insert(pageComponent)\n            .values(monitorComponents)\n            .onConflictDoUpdate({\n              target: [pageComponent.pageId, pageComponent.monitorId],\n              set: {\n                name: sql.raw(\"excluded.`name`\"),\n                description: sql.raw(\"excluded.`description`\"),\n                order: sql.raw(\"excluded.`order`\"),\n                groupId: sql.raw(\"excluded.`group_id`\"),\n                groupOrder: sql.raw(\"excluded.`group_order`\"),\n                updatedAt: sql`(strftime('%s', 'now'))`,\n              },\n            });\n        }\n\n        // Handle static components\n        // If they have a valid existing ID, update them; otherwise insert new ones\n        const existingComponentIds = new Set(\n          existingComponents.map((c) => c.id),\n        );\n\n        for (const componentValue of staticComponents) {\n          if (\n            componentValue.id &&\n            existingComponentIds.has(componentValue.id)\n          ) {\n            // Update existing static component (preserves ID and relationships)\n            await tx\n              .update(pageComponent)\n              .set({\n                name: componentValue.name,\n                description: componentValue.description,\n                type: componentValue.type,\n                monitorId: componentValue.monitorId,\n                order: componentValue.order,\n                groupId: componentValue.groupId,\n                groupOrder: componentValue.groupOrder,\n                updatedAt: new Date(),\n              })\n              .where(\n                and(\n                  eq(pageComponent.id, componentValue.id),\n                  eq(pageComponent.pageId, opts.input.pageId),\n                  eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n                ),\n              );\n          } else {\n            // Insert new static component\n            await tx.insert(pageComponent).values({\n              pageId: componentValue.pageId,\n              workspaceId: componentValue.workspaceId,\n              name: componentValue.name,\n              description: componentValue.description,\n              type: componentValue.type,\n              monitorId: componentValue.monitorId,\n              order: componentValue.order,\n              groupId: componentValue.groupId,\n              groupOrder: componentValue.groupOrder,\n            });\n          }\n        }\n      });\n\n      return { success: true };\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/pageSubscriber.ts",
    "content": "import { and, eq } from \"@openstatus/db\";\nimport { page, pageSubscriber } from \"@openstatus/db/src/schema\";\nimport {\n  getSubscriptionByToken,\n  hasPendingUnexpiredSubscription,\n  unsubscribe,\n  updateSubscriptionScope,\n  upsertEmailSubscription,\n  verifySubscription,\n} from \"@openstatus/subscriptions\";\nimport { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport { createTRPCRouter, protectedProcedure, publicProcedure } from \"../trpc\";\n\nexport const pageSubscriberRouter = createTRPCRouter({\n  /**\n   * PUBLIC: Subscribe to a status page (or update existing subscription)\n   */\n  upsert: publicProcedure\n    .input(\n      z.object({\n        email: z.email(),\n        pageId: z.number().int().positive(),\n        componentIds: z.array(z.number().int().positive()).optional(),\n      }),\n    )\n    .mutation(async (opts) => {\n      // Guard against email spam: reject if a pending (unverified, unexpired) subscription exists\n      const isPending = await hasPendingUnexpiredSubscription(\n        opts.input.email,\n        opts.input.pageId,\n      );\n      if (isPending) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message:\n            \"A confirmation link was already sent. Please check your email or wait until it expires to request a new one.\",\n        });\n      }\n\n      try {\n        const subscription = await upsertEmailSubscription({\n          email: opts.input.email,\n          pageId: opts.input.pageId,\n          componentIds: opts.input.componentIds,\n        });\n\n        return {\n          success: true,\n          subscription: {\n            id: subscription.id,\n            acceptedAt: subscription.acceptedAt,\n            componentIds: subscription.componentIds,\n          },\n        };\n      } catch (error) {\n        if (error instanceof Error) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: error.message,\n          });\n        }\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Failed to create subscription\",\n        });\n      }\n    }),\n\n  /**\n   * PUBLIC: Verify email subscription by token\n   */\n  verify: publicProcedure\n    .input(\n      z.object({\n        token: z.uuid(),\n        domain: z.string().toLowerCase().optional(),\n      }),\n    )\n    .mutation(async (opts) => {\n      try {\n        const subscription = await verifySubscription(\n          opts.input.token,\n          opts.input.domain,\n        );\n\n        if (!subscription) {\n          throw new TRPCError({\n            code: \"NOT_FOUND\",\n            message: \"Subscription not found or token invalid\",\n          });\n        }\n\n        return {\n          success: true,\n          subscription: {\n            id: subscription.id,\n            email: subscription.email,\n            pageSlug: subscription.pageSlug,\n            pageName: subscription.pageName,\n            componentIds: subscription.componentIds,\n          },\n        };\n      } catch (error) {\n        if (error instanceof TRPCError) throw error;\n        if (error instanceof Error) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: error.message,\n          });\n        }\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Failed to verify subscription\",\n        });\n      }\n    }),\n\n  /**\n   * PUBLIC: Get subscription by token (for management UI)\n   */\n  getByToken: publicProcedure\n    .input(\n      z.object({\n        token: z.string(),\n        domain: z.string().toLowerCase().optional(),\n      }),\n    )\n    .query(async (opts) => {\n      const subscription = await getSubscriptionByToken(\n        opts.input.token,\n        opts.input.domain,\n      );\n\n      if (!subscription) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Subscription not found\",\n        });\n      }\n\n      return {\n        id: subscription.id,\n        email: subscription.email,\n        pageName: subscription.pageName,\n        pageSlug: subscription.pageSlug,\n        customDomain: subscription.customDomain,\n        channelType: subscription.channelType,\n        componentIds: subscription.componentIds,\n        acceptedAt: subscription.acceptedAt,\n        unsubscribedAt: subscription.unsubscribedAt,\n      };\n    }),\n\n  /**\n   * PUBLIC: Update subscription scope (replace components)\n   */\n  updateScope: publicProcedure\n    .input(\n      z.object({\n        token: z.string(),\n        componentIds: z.array(z.number().int().positive()),\n        domain: z.string().toLowerCase().optional(),\n      }),\n    )\n    .mutation(async (opts) => {\n      try {\n        const subscription = await updateSubscriptionScope({\n          token: opts.input.token,\n          componentIds: opts.input.componentIds,\n          domain: opts.input.domain,\n        });\n\n        return {\n          success: true,\n          subscription: {\n            id: subscription.id,\n            componentIds: subscription.componentIds,\n          },\n        };\n      } catch (error) {\n        if (error instanceof Error) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: error.message,\n          });\n        }\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Failed to update subscription scope\",\n        });\n      }\n    }),\n\n  /**\n   * PUBLIC: Unsubscribe by token\n   */\n  unsubscribe: publicProcedure\n    .input(\n      z.object({\n        token: z.string(),\n        domain: z.string().toLowerCase().optional(),\n      }),\n    )\n    .mutation(async (opts) => {\n      try {\n        await unsubscribe(opts.input.token, opts.input.domain);\n\n        return { success: true };\n      } catch (error) {\n        if (error instanceof Error) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: error.message,\n          });\n        }\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Failed to unsubscribe\",\n        });\n      }\n    }),\n\n  /**\n   * PROTECTED: List all subscriptions for a page (dashboard)\n   */\n  list: protectedProcedure\n    .input(\n      z.object({\n        pageId: z.number(),\n        order: z.enum([\"asc\", \"desc\"]).optional(),\n      }),\n    )\n    .query(async (opts) => {\n      const data = await opts.ctx.db.transaction(async (tx) => {\n        const _page = await tx.query.page.findFirst({\n          where: and(\n            eq(page.workspaceId, opts.ctx.workspace.id),\n            eq(page.id, opts.input.pageId),\n          ),\n        });\n\n        if (!_page) {\n          throw new TRPCError({\n            code: \"NOT_FOUND\",\n            message: \"Page not found\",\n          });\n        }\n\n        const subscriptions = await tx.query.pageSubscriber.findMany({\n          where: eq(pageSubscriber.pageId, _page.id),\n          with: {\n            components: {\n              with: {\n                pageComponent: true,\n              },\n            },\n          },\n          orderBy: (subs, { desc, asc }) =>\n            opts.input.order === \"asc\"\n              ? asc(subs.createdAt)\n              : desc(subs.createdAt),\n        });\n\n        return subscriptions.map((sub) => ({\n          id: sub.id,\n          channelType: sub.channelType,\n          email: sub.email,\n          webhookUrl: sub.webhookUrl,\n          acceptedAt: sub.acceptedAt,\n          unsubscribedAt: sub.unsubscribedAt,\n          createdAt: sub.createdAt,\n          components: sub.components.map((c) => ({\n            id: c.pageComponent.id,\n            name: c.pageComponent.name,\n          })),\n          isEntirePage: sub.components.length === 0,\n          pageId: sub.pageId,\n        }));\n      });\n\n      return data;\n    }),\n\n  /**\n   * PROTECTED: Delete a subscription (dashboard)\n   */\n  delete: protectedProcedure\n    .input(z.object({ id: z.number(), pageId: z.number() }))\n    .mutation(async (opts) => {\n      await opts.ctx.db.transaction(async (tx) => {\n        const _page = await tx.query.page.findFirst({\n          where: and(\n            eq(page.workspaceId, opts.ctx.workspace.id),\n            eq(page.id, opts.input.pageId),\n          ),\n        });\n\n        if (!_page) {\n          throw new TRPCError({\n            code: \"NOT_FOUND\",\n            message: \"Page not found\",\n          });\n        }\n\n        const subscriber = await tx.query.pageSubscriber.findFirst({\n          where: and(\n            eq(pageSubscriber.id, opts.input.id),\n            eq(pageSubscriber.pageId, opts.input.pageId),\n          ),\n        });\n\n        if (!subscriber) {\n          throw new TRPCError({\n            code: \"NOT_FOUND\",\n            message: \"Subscriber not found\",\n          });\n        }\n\n        return await tx\n          .delete(pageSubscriber)\n          .where(eq(pageSubscriber.id, opts.input.id));\n      });\n\n      return { success: true };\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/privateLocation.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  privateLocation,\n  privateLocationToMonitors,\n} from \"@openstatus/db/src/schema\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { edgeRouter } from \"../edge\";\nimport { createInnerTRPCContext } from \"../trpc\";\n\nlet otherWorkspaceLocationId: number;\nlet ownWorkspaceLocationId: number;\n\nbeforeAll(async () => {\n  const otherLoc = await db\n    .insert(privateLocation)\n    .values({\n      workspaceId: 2,\n      name: \"Other workspace location\",\n      token: \"test-token-idor\",\n    })\n    .returning()\n    .get();\n  otherWorkspaceLocationId = otherLoc.id;\n\n  await db\n    .insert(privateLocationToMonitors)\n    .values({\n      privateLocationId: otherWorkspaceLocationId,\n      monitorId: 5,\n    })\n    .run();\n\n  const ownLoc = await db\n    .insert(privateLocation)\n    .values({\n      workspaceId: 1,\n      name: \"Own workspace location\",\n      token: \"test-token-own\",\n    })\n    .returning()\n    .get();\n  ownWorkspaceLocationId = ownLoc.id;\n});\n\nafterAll(async () => {\n  await db\n    .delete(privateLocationToMonitors)\n    .where(\n      eq(privateLocationToMonitors.privateLocationId, otherWorkspaceLocationId),\n    );\n  await db\n    .delete(privateLocation)\n    .where(eq(privateLocation.id, otherWorkspaceLocationId));\n  await db\n    .delete(privateLocationToMonitors)\n    .where(\n      eq(privateLocationToMonitors.privateLocationId, ownWorkspaceLocationId),\n    );\n  await db\n    .delete(privateLocation)\n    .where(eq(privateLocation.id, ownWorkspaceLocationId));\n});\n\ntest(\"privateLocation.update rejects location from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.privateLocation.update({\n      id: otherWorkspaceLocationId,\n      name: \"Unauthorized Name Change\",\n      monitors: [],\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n\n  // Verify monitor associations were NOT deleted\n  const associations = await db.query.privateLocationToMonitors.findMany({\n    where: eq(\n      privateLocationToMonitors.privateLocationId,\n      otherWorkspaceLocationId,\n    ),\n  });\n  expect(associations.length).toBe(1);\n});\n\ntest(\"privateLocation.update succeeds for own workspace location\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  const result = await caller.privateLocation.update({\n    id: ownWorkspaceLocationId,\n    name: \"Updated Location Name\",\n    monitors: [1],\n  });\n\n  expect(result).toBeDefined();\n  expect(result.name).toBe(\"Updated Location Name\");\n});\n\ntest(\"privateLocation.new rejects monitors from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.privateLocation.new({\n      name: \"Injected Location\",\n      token: \"test-token-inject\",\n      monitors: [5], // monitor 5 belongs to workspace 3\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"FORBIDDEN\");\n  }\n});\n\ntest(\"privateLocation.new succeeds with own workspace monitors\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  const result = await caller.privateLocation.new({\n    name: \"Valid Location\",\n    token: \"test-token-valid-new\",\n    monitors: [1], // monitor 1 belongs to workspace 1\n  });\n\n  expect(result).toBeDefined();\n  expect(result.name).toBe(\"Valid Location\");\n\n  // Verify the monitor association was created\n  const associations = await db.query.privateLocationToMonitors.findMany({\n    where: eq(privateLocationToMonitors.privateLocationId, result.id),\n  });\n  expect(associations.length).toBe(1);\n  expect(associations[0].monitorId).toBe(1);\n\n  // Cleanup\n  await db\n    .delete(privateLocationToMonitors)\n    .where(eq(privateLocationToMonitors.privateLocationId, result.id));\n  await db.delete(privateLocation).where(eq(privateLocation.id, result.id));\n});\n\ntest(\"privateLocation.update rejects monitors from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.privateLocation.update({\n      id: ownWorkspaceLocationId,\n      name: \"Updated Location Name\",\n      monitors: [5], // monitor 5 belongs to workspace 3\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"FORBIDDEN\");\n  }\n});\n"
  },
  {
    "path": "packages/api/src/router/privateLocation.ts",
    "content": "import { and, eq, inArray } from \"@openstatus/db\";\nimport {\n  monitor,\n  privateLocation,\n  privateLocationToMonitors,\n} from \"@openstatus/db/src/schema\";\nimport { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const privateLocationRouter = createTRPCRouter({\n  list: protectedProcedure.query(async (opts) => {\n    const privateLocations = await opts.ctx.db.transaction(async (tx) => {\n      return await tx.query.privateLocation.findMany({\n        where: eq(privateLocation.workspaceId, opts.ctx.workspace.id),\n        with: {\n          privateLocationToMonitors: {\n            with: { monitor: true },\n          },\n        },\n      });\n    });\n    const result = privateLocations.map((privateLocation) => ({\n      ...privateLocation,\n      monitors: privateLocation.privateLocationToMonitors\n        .map((m) => m.monitor)\n        .filter((m) => m !== null),\n    }));\n    return result;\n  }),\n  new: protectedProcedure\n    .input(\n      z.object({\n        name: z.string(),\n        monitors: z.array(z.number()),\n        token: z.string(),\n      }),\n    )\n    .mutation(async (opts) => {\n      if (opts.input.monitors.length) {\n        const validMonitors = await opts.ctx.db.query.monitor.findMany({\n          where: and(\n            eq(monitor.workspaceId, opts.ctx.workspace.id),\n            inArray(monitor.id, opts.input.monitors),\n          ),\n        });\n        if (validMonitors.length !== opts.input.monitors.length) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message: \"Invalid monitor IDs.\",\n          });\n        }\n      }\n\n      return await opts.ctx.db.transaction(async (tx) => {\n        const _privateLocation = await tx\n          .insert(privateLocation)\n          .values({\n            name: opts.input.name,\n            token: opts.input.token,\n            workspaceId: opts.ctx.workspace.id,\n          })\n          .returning()\n          .get();\n\n        if (opts.input.monitors.length) {\n          await tx.insert(privateLocationToMonitors).values(\n            opts.input.monitors.map((monitorId) => ({\n              privateLocationId: _privateLocation.id,\n              monitorId,\n            })),\n          );\n        }\n        return _privateLocation;\n      });\n    }),\n  update: protectedProcedure\n    .input(\n      z.object({\n        id: z.number(),\n        name: z.string(),\n        monitors: z.array(z.number()),\n      }),\n    )\n    .mutation(async (opts) => {\n      const existing = await opts.ctx.db.query.privateLocation.findFirst({\n        where: and(\n          eq(privateLocation.id, opts.input.id),\n          eq(privateLocation.workspaceId, opts.ctx.workspace.id),\n        ),\n      });\n\n      if (!existing) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Private location not found\",\n        });\n      }\n\n      if (opts.input.monitors.length) {\n        const validMonitors = await opts.ctx.db.query.monitor.findMany({\n          where: and(\n            eq(monitor.workspaceId, opts.ctx.workspace.id),\n            inArray(monitor.id, opts.input.monitors),\n          ),\n        });\n        if (validMonitors.length !== opts.input.monitors.length) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message: \"Invalid monitor IDs.\",\n          });\n        }\n      }\n\n      return await opts.ctx.db.transaction(async (tx) => {\n        const _privateLocation = await tx\n          .update(privateLocation)\n          .set({ name: opts.input.name, updatedAt: new Date() })\n          .where(\n            and(\n              eq(privateLocation.id, opts.input.id),\n              eq(privateLocation.workspaceId, opts.ctx.workspace.id),\n            ),\n          )\n          .returning()\n          .get();\n\n        await tx\n          .delete(privateLocationToMonitors)\n          .where(\n            eq(\n              privateLocationToMonitors.privateLocationId,\n              _privateLocation.id,\n            ),\n          );\n\n        if (opts.input.monitors.length) {\n          await tx.insert(privateLocationToMonitors).values(\n            opts.input.monitors.map((monitorId) => ({\n              privateLocationId: _privateLocation.id,\n              monitorId,\n            })),\n          );\n        }\n\n        return _privateLocation;\n      });\n    }),\n  delete: protectedProcedure\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      console.log(\"delete private location\", opts.input.id);\n      return await opts.ctx.db.transaction(async (tx) => {\n        await tx\n          .delete(privateLocation)\n          .where(\n            and(\n              eq(privateLocation.id, opts.input.id),\n              eq(privateLocation.workspaceId, opts.ctx.workspace.id),\n            ),\n          );\n      });\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/statusPage.e2e.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\nimport { and, db, eq, isNotNull, isNull } from \"@openstatus/db\";\nimport { page, pageSubscriber, workspace } from \"@openstatus/db/src/schema\";\n\n/**\n * End-to-end integration tests for the full unsubscribe flow.\n * These tests simulate the complete user journey:\n * subscribe -> verify -> receive email -> unsubscribe\n */\n\nlet testPageId: number;\nlet testWorkspaceId: number;\nconst testSlug = \"e2e-unsubscribe-test-page\";\nconst testEmail = \"e2e-test-user@example.com\";\nlet subscriberToken: string;\n\nbeforeAll(async () => {\n  // Clean up any existing test data\n  await db.delete(pageSubscriber).where(eq(pageSubscriber.email, testEmail));\n  await db.delete(page).where(eq(page.slug, testSlug));\n\n  // Get an existing workspace (use workspace id 1 from seed data)\n  const existingWorkspace = await db.query.workspace.findFirst({\n    where: eq(workspace.id, 1),\n  });\n\n  if (!existingWorkspace) {\n    throw new Error(\n      \"Test workspace not found. Please ensure seed data exists.\",\n    );\n  }\n\n  testWorkspaceId = existingWorkspace.id;\n\n  // Create a test page\n  const testPage = await db\n    .insert(page)\n    .values({\n      workspaceId: testWorkspaceId,\n      title: \"E2E Test Status Page\",\n      description: \"A test page for E2E unsubscribe flow tests\",\n      slug: testSlug,\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n\n  testPageId = testPage.id;\n});\n\nafterAll(async () => {\n  // Clean up test data\n  await db.delete(pageSubscriber).where(eq(pageSubscriber.email, testEmail));\n  await db.delete(page).where(eq(page.slug, testSlug));\n});\n\ndescribe(\"Full unsubscribe flow: subscribe -> verify -> unsubscribe\", () => {\n  test(\"Step 1: User subscribes to status page\", async () => {\n    // Simulate subscription by inserting a subscriber\n    const subscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: testEmail,\n        token: crypto.randomUUID(),\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 7 days\n      })\n      .returning()\n      .get();\n\n    expect(subscriber.id).toBeDefined();\n    expect(subscriber.email).toBe(testEmail);\n    expect(subscriber.token).toBeDefined();\n    expect(subscriber.acceptedAt).toBeNull();\n    expect(subscriber.unsubscribedAt).toBeNull();\n\n    if (!subscriber.token) {\n      throw new Error(\"Subscriber token is undefined\");\n    }\n\n    subscriberToken = subscriber.token;\n  });\n\n  test(\"Step 2: User verifies their email subscription\", async () => {\n    // Verify the subscription\n    await db\n      .update(pageSubscriber)\n      .set({ acceptedAt: new Date() })\n      .where(eq(pageSubscriber.token, subscriberToken));\n\n    // Verify the subscription is now active\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, subscriberToken),\n    });\n\n    expect(subscriber?.acceptedAt).not.toBeNull();\n    expect(subscriber?.unsubscribedAt).toBeNull();\n  });\n\n  test(\"Step 3: Verified subscriber is included in email recipient list\", async () => {\n    // This query mirrors the exact query used in statusReports/post.ts\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    expect(subscribers.length).toBe(1);\n    expect(subscribers[0].email).toBe(testEmail);\n    expect(subscribers[0].token).toBe(subscriberToken);\n  });\n\n  test(\"Step 4: User clicks unsubscribe and sets unsubscribedAt\", async () => {\n    // Simulate the unsubscribe action\n    await db\n      .update(pageSubscriber)\n      .set({ unsubscribedAt: new Date() })\n      .where(eq(pageSubscriber.token, subscriberToken));\n\n    // Verify the unsubscription\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, subscriberToken),\n    });\n\n    expect(subscriber?.unsubscribedAt).not.toBeNull();\n    expect(subscriber?.unsubscribedAt).toBeInstanceOf(Date);\n  });\n\n  test(\"Step 5: Unsubscribed user is excluded from email recipient list\", async () => {\n    // This query mirrors the exact query used in statusReports/post.ts\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    expect(subscribers.length).toBe(0);\n  });\n});\n\ndescribe(\"Confirmation page displays correct information\", () => {\n  let confirmPageToken: string;\n\n  beforeAll(async () => {\n    // Create a fresh subscriber for confirmation page tests\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"confirm-page-test@example.com\"));\n\n    const subscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: \"confirm-page-test@example.com\",\n        token: crypto.randomUUID(),\n        acceptedAt: new Date(), // Already verified\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .returning()\n      .get();\n\n    if (!subscriber.token) {\n      throw new Error(\"Subscriber token is undefined\");\n    }\n\n    confirmPageToken = subscriber.token;\n  });\n\n  afterAll(async () => {\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"confirm-page-test@example.com\"));\n  });\n\n  test(\"Confirmation page displays correct page name\", async () => {\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, confirmPageToken),\n      with: {\n        page: true,\n      },\n    });\n\n    expect(subscriber?.page.title).toBe(\"E2E Test Status Page\");\n  });\n\n  test(\"Confirmation page displays masked email (first char + *** + @domain)\", async () => {\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, confirmPageToken),\n    });\n\n    if (!subscriber) {\n      throw new Error(\"Subscriber not found\");\n    }\n\n    const email = subscriber.email;\n    expect(email).toBe(\"confirm-page-test@example.com\");\n\n    // Apply the same masking logic as in the API\n    const [localPart, domain] = email.split(\"@\");\n    const maskedEmail =\n      localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n    expect(maskedEmail).toBe(\"c***@example.com\");\n  });\n\n  test(\"Email masking works for single character local part\", async () => {\n    const email = \"a@example.com\";\n    const [localPart, domain] = email.split(\"@\");\n    const maskedEmail =\n      localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n    expect(maskedEmail).toBe(\"a***@example.com\");\n  });\n\n  test(\"Email masking works for long local part\", async () => {\n    const email = \"verylongemailaddress@example.com\";\n    const [localPart, domain] = email.split(\"@\");\n    const maskedEmail =\n      localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n    expect(maskedEmail).toBe(\"v***@example.com\");\n  });\n});\n\ndescribe(\"Clicking confirm sets unsubscribedAt timestamp\", () => {\n  let unsubscribeToken: string;\n\n  beforeAll(async () => {\n    // Create a fresh subscriber\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"unsubscribe-click-test@example.com\"));\n\n    const subscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: \"unsubscribe-click-test@example.com\",\n        token: crypto.randomUUID(),\n        acceptedAt: new Date(), // Already verified\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .returning()\n      .get();\n\n    if (!subscriber.token) {\n      throw new Error(\"Subscriber token is undefined\");\n    }\n\n    unsubscribeToken = subscriber.token;\n  });\n\n  afterAll(async () => {\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"unsubscribe-click-test@example.com\"));\n  });\n\n  test(\"Before clicking confirm, unsubscribedAt is null\", async () => {\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, unsubscribeToken),\n    });\n\n    expect(subscriber?.unsubscribedAt).toBeNull();\n  });\n\n  test(\"After clicking confirm, unsubscribedAt is set to current timestamp\", async () => {\n    const beforeUnsubscribe = new Date();\n\n    // Simulate clicking \"Confirm Unsubscribe\"\n    await db\n      .update(pageSubscriber)\n      .set({ unsubscribedAt: new Date() })\n      .where(eq(pageSubscriber.token, unsubscribeToken));\n\n    const afterUnsubscribe = new Date();\n\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, unsubscribeToken),\n    });\n\n    if (!subscriber) {\n      throw new Error(\"Subscriber not found\");\n    }\n\n    expect(subscriber.unsubscribedAt).not.toBeNull();\n    expect(subscriber.unsubscribedAt).toBeInstanceOf(Date);\n\n    // Verify the timestamp is within the expected range\n    if (!subscriber.unsubscribedAt) {\n      throw new Error(\"Subscriber unsubscribedAt is undefined\");\n    }\n\n    // SQLite stores timestamps in seconds, so we compare at second precision\n    const unsubscribedTime = Math.floor(\n      subscriber.unsubscribedAt.getTime() / 1000,\n    );\n    const beforeTime = Math.floor(beforeUnsubscribe.getTime() / 1000);\n    const afterTime = Math.floor(afterUnsubscribe.getTime() / 1000);\n\n    expect(unsubscribedTime).toBeGreaterThanOrEqual(beforeTime);\n    expect(unsubscribedTime).toBeLessThanOrEqual(afterTime);\n  });\n\n  test(\"Subscriber state transitions correctly through the flow\", async () => {\n    // Verify the subscriber has completed the full lifecycle\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, unsubscribeToken),\n    });\n\n    // Has been verified (acceptedAt is set)\n    expect(subscriber?.acceptedAt).not.toBeNull();\n\n    // Has been unsubscribed (unsubscribedAt is set)\n    expect(subscriber?.unsubscribedAt).not.toBeNull();\n\n    // Token is still present (for audit purposes)\n    expect(subscriber?.token).toBe(unsubscribeToken);\n  });\n});\n\ndescribe(\"Unsubscribed user does not receive new emails\", () => {\n  let unsubscribedToken: string;\n  let pendingToken: string;\n\n  beforeAll(async () => {\n    // Clean up and create multiple subscribers with different states\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"active-user@example.com\"));\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"unsubscribed-user@example.com\"));\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"pending-user@example.com\"));\n\n    // Active subscriber\n    const active = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: \"active-user@example.com\",\n        token: crypto.randomUUID(),\n        acceptedAt: new Date(),\n        unsubscribedAt: null,\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .returning()\n      .get();\n\n    if (!active.token) {\n      throw new Error(\"Active subscriber token is undefined\");\n    }\n\n    // Unsubscribed subscriber\n    const unsubscribed = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: \"unsubscribed-user@example.com\",\n        token: crypto.randomUUID(),\n        acceptedAt: new Date(),\n        unsubscribedAt: new Date(),\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .returning()\n      .get();\n\n    if (!unsubscribed.token) {\n      throw new Error(\"Unsubscribed subscriber token is undefined\");\n    }\n\n    unsubscribedToken = unsubscribed.token;\n\n    // Pending (unverified) subscriber\n    const pending = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: \"pending-user@example.com\",\n        token: crypto.randomUUID(),\n        acceptedAt: null,\n        unsubscribedAt: null,\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .returning()\n      .get();\n\n    if (!pending.token) {\n      throw new Error(\"Pending subscriber token is undefined\");\n    }\n\n    pendingToken = pending.token;\n  });\n\n  afterAll(async () => {\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"active-user@example.com\"));\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"unsubscribed-user@example.com\"));\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"pending-user@example.com\"));\n  });\n\n  test(\"Email query returns only active subscribers with valid tokens\", async () => {\n    // This mirrors the exact query pattern used in email-sending routes\n    const emailRecipients = await db\n      .select({\n        email: pageSubscriber.email,\n        token: pageSubscriber.token,\n      })\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    // Should only include active subscriber\n    expect(emailRecipients.length).toBeGreaterThanOrEqual(1);\n\n    const emails = emailRecipients.map((r) => r.email);\n    expect(emails).toContain(\"active-user@example.com\");\n    expect(emails).not.toContain(\"unsubscribed-user@example.com\");\n    expect(emails).not.toContain(\"pending-user@example.com\");\n  });\n\n  test(\"Unsubscribed users are filtered out even with acceptedAt set\", async () => {\n    // Verify the unsubscribed user has acceptedAt set\n    const unsubscribedUser = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, unsubscribedToken),\n    });\n\n    expect(unsubscribedUser?.acceptedAt).not.toBeNull();\n    expect(unsubscribedUser?.unsubscribedAt).not.toBeNull();\n\n    // Query with proper filters\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    const foundUnsubscribed = subscribers.find(\n      (s) => s.email === \"unsubscribed-user@example.com\",\n    );\n    expect(foundUnsubscribed).toBeUndefined();\n  });\n\n  test(\"Pending users are filtered out (not verified)\", async () => {\n    // Verify the pending user has no acceptedAt\n    const pendingUser = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, pendingToken),\n    });\n\n    expect(pendingUser?.acceptedAt).toBeNull();\n\n    // Query with proper filters\n    const subscribers = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    const foundPending = subscribers.find(\n      (s) => s.email === \"pending-user@example.com\",\n    );\n    expect(foundPending).toBeUndefined();\n  });\n\n  test(\"Email recipients list includes token for unsubscribe URL generation\", async () => {\n    const emailRecipients = await db\n      .select({\n        email: pageSubscriber.email,\n        token: pageSubscriber.token,\n      })\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    // Filter for valid tokens (as done in email sending routes)\n    const validRecipients = emailRecipients.filter(\n      (r): r is { email: string; token: string } => r.token !== null,\n    );\n\n    expect(validRecipients.length).toBeGreaterThanOrEqual(1);\n\n    // Each valid recipient should have a UUID token\n    for (const recipient of validRecipients) {\n      expect(recipient.token).toMatch(\n        /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,\n      );\n    }\n  });\n});\n\ndescribe(\"Re-subscription after unsubscribe flow\", () => {\n  let resubscribeToken: string;\n\n  beforeAll(async () => {\n    // Clean up\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"resubscribe-test@example.com\"));\n\n    // Create an initially subscribed and verified user\n    const subscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: \"resubscribe-test@example.com\",\n        token: crypto.randomUUID(),\n        acceptedAt: new Date(),\n        unsubscribedAt: null,\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .returning()\n      .get();\n\n    if (!subscriber.token) {\n      throw new Error(\"Subscriber token is undefined\");\n    }\n\n    resubscribeToken = subscriber.token;\n  });\n\n  afterAll(async () => {\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"resubscribe-test@example.com\"));\n  });\n\n  test(\"User can complete full subscribe -> unsubscribe -> resubscribe cycle\", async () => {\n    // Step 1: Verify initial subscription state\n    let subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.email, \"resubscribe-test@example.com\"),\n    });\n\n    if (!subscriber) {\n      throw new Error(\"Subscriber ID is undefined\");\n    }\n\n    expect(subscriber?.acceptedAt).not.toBeNull();\n    expect(subscriber?.unsubscribedAt).toBeNull();\n\n    // Step 2: User unsubscribes\n    await db\n      .update(pageSubscriber)\n      .set({ unsubscribedAt: new Date() })\n      .where(eq(pageSubscriber.id, subscriber.id));\n\n    subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.id, subscriber.id),\n    });\n\n    expect(subscriber?.unsubscribedAt).not.toBeNull();\n\n    // Step 3: User is excluded from emails\n    const subscribersAfterUnsub = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          eq(pageSubscriber.email, \"resubscribe-test@example.com\"),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    expect(subscribersAfterUnsub.length).toBe(0);\n\n    if (!subscriber) {\n      throw new Error(\"Subscriber is undefined\");\n    }\n\n    // Step 4: User re-subscribes (simulating the re-subscription flow)\n    const newToken = crypto.randomUUID();\n    await db\n      .update(pageSubscriber)\n      .set({\n        unsubscribedAt: null,\n        acceptedAt: null, // Requires re-verification\n        token: newToken,\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .where(eq(pageSubscriber.id, subscriber.id));\n\n    // Step 5: User is still excluded (not yet verified)\n    const subscribersPendingVerify = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          eq(pageSubscriber.email, \"resubscribe-test@example.com\"),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    expect(subscribersPendingVerify.length).toBe(0);\n\n    // Step 6: User verifies their email again\n    await db\n      .update(pageSubscriber)\n      .set({ acceptedAt: new Date() })\n      .where(eq(pageSubscriber.token, newToken));\n\n    // Step 7: User is now included in email list again\n    const subscribersAfterReverify = await db\n      .select()\n      .from(pageSubscriber)\n      .where(\n        and(\n          eq(pageSubscriber.pageId, testPageId),\n          eq(pageSubscriber.email, \"resubscribe-test@example.com\"),\n          isNotNull(pageSubscriber.acceptedAt),\n          isNull(pageSubscriber.unsubscribedAt),\n        ),\n      )\n      .all();\n\n    expect(subscribersAfterReverify.length).toBe(1);\n    expect(subscribersAfterReverify[0].token).toBe(newToken);\n    expect(subscribersAfterReverify[0].token).not.toBe(resubscribeToken);\n  });\n});\n\ndescribe(\"Invalid token handling\", () => {\n  test(\"Non-existent token returns no subscriber\", async () => {\n    const fakeToken = crypto.randomUUID();\n\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, fakeToken),\n    });\n\n    expect(subscriber).toBeUndefined();\n  });\n\n  test(\"Invalid UUID format is handled gracefully\", async () => {\n    const invalidToken = \"not-a-valid-uuid\";\n\n    // The database query will still work, just return no results\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, invalidToken),\n    });\n\n    expect(subscriber).toBeUndefined();\n  });\n\n  test(\"Already unsubscribed token returns subscriber with unsubscribedAt set\", async () => {\n    // Create an unsubscribed subscriber\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"already-unsub@example.com\"));\n\n    const subscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: \"already-unsub@example.com\",\n        token: crypto.randomUUID(),\n        acceptedAt: new Date(),\n        unsubscribedAt: new Date(),\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .returning()\n      .get();\n\n    if (!subscriber.token) {\n      throw new Error(\"Subscriber token is undefined\");\n    }\n\n    // Query the subscriber\n    const found = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, subscriber.token),\n    });\n\n    expect(found).toBeDefined();\n    expect(found?.unsubscribedAt).not.toBeNull();\n\n    // Clean up\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, \"already-unsub@example.com\"));\n  });\n});\n\ndescribe(\"statusPage.get endpoint validation\", () => {\n  test(\"Returns all required output fields with correct types\", async () => {\n    // Use the edgeRouter to call the statusPage.get endpoint\n    const { edgeRouter } = await import(\"../edge\");\n    const { createInnerTRPCContext } = await import(\"../trpc\");\n\n    const ctx = createInnerTRPCContext({\n      req: undefined,\n      // @ts-expect-error - auth not required for public procedure\n      auth: undefined,\n    });\n\n    const caller = edgeRouter.createCaller(ctx);\n    const result = await caller.statusPage.get({ slug: testSlug });\n\n    // Validate that result is not null\n    expect(result).toBeDefined();\n    expect(result).not.toBeNull();\n\n    if (!result) {\n      throw new Error(\"Result should not be null\");\n    }\n\n    // Validate core page fields with specific types\n    expect(typeof result.slug).toBe(\"string\");\n    expect(typeof result.title).toBe(\"string\");\n    expect(typeof result.description).toBe(\"string\");\n    expect(result.createdAt).toBeInstanceOf(Date);\n    expect(result.updatedAt).toBeInstanceOf(Date);\n\n    // Validate slug matches what we requested\n    expect(result.slug).toBe(testSlug);\n\n    // Validate all array fields exist and are arrays\n    expect(Array.isArray(result.monitors)).toBe(true);\n    expect(Array.isArray(result.monitorGroups)).toBe(true);\n    expect(Array.isArray(result.pageComponents)).toBe(true);\n    expect(Array.isArray(result.pageComponentGroups)).toBe(true);\n    expect(Array.isArray(result.trackers)).toBe(true);\n    expect(Array.isArray(result.lastEvents)).toBe(true);\n    expect(Array.isArray(result.openEvents)).toBe(true);\n    expect(Array.isArray(result.statusReports)).toBe(true);\n    expect(Array.isArray(result.incidents)).toBe(true);\n    expect(Array.isArray(result.maintenances)).toBe(true);\n\n    // Validate status field is one of the allowed values\n    expect([\"success\", \"degraded\", \"error\", \"info\"]).toContain(result.status);\n\n    // Validate workspacePlan field\n    expect(result.workspacePlan).toBeDefined();\n    expect(typeof result.workspacePlan).toBe(\"string\");\n\n    // Validate whiteLabel field\n    expect(typeof result.whiteLabel).toBe(\"boolean\");\n  });\n\n  test(\"Returns null for non-existent slug\", async () => {\n    const { edgeRouter } = await import(\"../edge\");\n    const { createInnerTRPCContext } = await import(\"../trpc\");\n\n    const ctx = createInnerTRPCContext({\n      req: undefined,\n      // @ts-expect-error - auth not required for public procedure\n      auth: undefined,\n    });\n\n    const caller = edgeRouter.createCaller(ctx);\n    const result = await caller.statusPage.get({\n      slug: \"non-existent-slug-12345\",\n    });\n\n    expect(result).toBeNull();\n  });\n\n  test(\"Tracker objects have correct discriminated union types\", async () => {\n    const { edgeRouter } = await import(\"../edge\");\n    const { createInnerTRPCContext } = await import(\"../trpc\");\n\n    const ctx = createInnerTRPCContext({\n      req: undefined,\n      // @ts-expect-error - auth not required for public procedure\n      auth: undefined,\n    });\n\n    const caller = edgeRouter.createCaller(ctx);\n    const result = await caller.statusPage.get({ slug: testSlug });\n\n    if (!result) {\n      // If no result, skip this test as there are no trackers to validate\n      return;\n    }\n\n    // Validate each tracker has the correct structure\n    for (const tracker of result.trackers) {\n      expect(tracker).toHaveProperty(\"type\");\n      expect(tracker).toHaveProperty(\"order\");\n\n      if (tracker.type === \"component\") {\n        expect(tracker).toHaveProperty(\"component\");\n        expect(tracker.component).toHaveProperty(\"id\");\n        expect(tracker.component).toHaveProperty(\"name\");\n        expect(tracker.component).toHaveProperty(\"status\");\n        expect(tracker.component).toHaveProperty(\"type\");\n        expect([\"monitor\", \"static\"]).toContain(tracker.component.type);\n        expect([\"success\", \"degraded\", \"error\", \"info\"]).toContain(\n          tracker.component.status,\n        );\n\n        // Monitor-type components should have monitor relation\n        if (tracker.component.type === \"monitor\") {\n          expect(tracker.component).toHaveProperty(\"monitor\");\n          expect(tracker.component.monitor).toBeDefined();\n        }\n      } else if (tracker.type === \"group\") {\n        expect(tracker).toHaveProperty(\"groupId\");\n        expect(tracker).toHaveProperty(\"groupName\");\n        expect(tracker).toHaveProperty(\"components\");\n        expect(tracker).toHaveProperty(\"status\");\n        expect(Array.isArray(tracker.components)).toBe(true);\n        expect([\"success\", \"degraded\", \"error\", \"info\"]).toContain(\n          tracker.status,\n        );\n      }\n    }\n  });\n\n  test(\"Event objects have required fields\", async () => {\n    const { edgeRouter } = await import(\"../edge\");\n    const { createInnerTRPCContext } = await import(\"../trpc\");\n\n    const ctx = createInnerTRPCContext({\n      req: undefined,\n      // @ts-expect-error - auth not required for public procedure\n      auth: undefined,\n    });\n\n    const caller = edgeRouter.createCaller(ctx);\n    const result = await caller.statusPage.get({ slug: testSlug });\n\n    if (!result) {\n      return;\n    }\n\n    // Validate lastEvents structure\n    for (const event of result.lastEvents) {\n      expect(event).toMatchObject({\n        id: expect.any(Number),\n        name: expect.any(String),\n        from: expect.any(Date),\n        status: expect.any(String),\n        type: expect.any(String),\n      });\n      expect([\"maintenance\", \"incident\", \"report\"]).toContain(event.type);\n      expect([\"success\", \"degraded\", \"error\", \"info\"]).toContain(event.status);\n    }\n\n    // Validate openEvents structure\n    for (const event of result.openEvents) {\n      expect(event).toMatchObject({\n        id: expect.any(Number),\n        name: expect.any(String),\n        from: expect.any(Date),\n        status: expect.any(String),\n        type: expect.any(String),\n      });\n      expect([\"maintenance\", \"incident\", \"report\"]).toContain(event.type);\n      expect([\"success\", \"degraded\", \"error\", \"info\"]).toContain(event.status);\n    }\n  });\n\n  test(\"Monitor objects contain status field\", async () => {\n    const { edgeRouter } = await import(\"../edge\");\n    const { createInnerTRPCContext } = await import(\"../trpc\");\n\n    const ctx = createInnerTRPCContext({\n      req: undefined,\n      // @ts-expect-error - auth not required for public procedure\n      auth: undefined,\n    });\n\n    const caller = edgeRouter.createCaller(ctx);\n    const result = await caller.statusPage.get({ slug: testSlug });\n\n    if (!result || result.monitors.length === 0) {\n      return;\n    }\n\n    // Validate each monitor has status field\n    for (const monitor of result.monitors) {\n      expect(monitor).toHaveProperty(\"status\");\n      expect([\"success\", \"degraded\", \"error\", \"info\"]).toContain(\n        monitor.status,\n      );\n      expect(monitor).toHaveProperty(\"id\");\n      expect(monitor).toHaveProperty(\"name\");\n    }\n  });\n});\n"
  },
  {
    "path": "packages/api/src/router/statusPage.ts",
    "content": "import { z } from \"zod\";\n\nimport { and, eq, inArray, sql } from \"@openstatus/db\";\nimport {\n  maintenance,\n  page,\n  pageComponent,\n  pageConfigurationSchema,\n  selectMaintenancePageSchema,\n  selectPageComponentWithMonitorRelation,\n  selectPublicMonitorSchema,\n  selectPublicPageLightSchemaWithRelation,\n  selectPublicPageSchemaWithRelation,\n  selectStatusReportPageSchema,\n  selectWorkspaceSchema,\n  statusReport,\n} from \"@openstatus/db/src/schema\";\nimport {\n  getSubscriptionByToken,\n  hasPendingUnexpiredSubscription,\n  unsubscribe,\n  updateSubscriptionScope,\n  upsertEmailSubscription,\n  verifySubscription,\n} from \"@openstatus/subscriptions\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { TRPCError } from \"@trpc/server\";\nimport { endOfDay, startOfDay, subDays } from \"date-fns\";\nimport { createTRPCRouter, publicProcedure } from \"../trpc\";\nimport {\n  type StatusData,\n  fillStatusDataFor45Days,\n  fillStatusDataFor45DaysNoop,\n  getEvents,\n  getUptime,\n  getWorstVariant,\n  isMonitorComponent,\n  setDataByType,\n} from \"./statusPage.utils\";\nimport {\n  getMetricsLatencyMultiProcedure,\n  getMetricsLatencyProcedure,\n  getMetricsRegionsProcedure,\n  getStatusProcedure,\n  getUptimeProcedure,\n} from \"./tinybird\";\n\n// NOTE: publicProcedure is used to get the status page\n// TODO: improve performance of SQL query (make a single query with joins)\n\n// IMPORTANT: we cannot use the tinybird procedure because it has protectedProcedure\n// instead, we should add TB logic in here!!!!\n\n// NOTE: this router is used on status pages only - do not confuse with the page router which is used in the dashboard for the config\n\n/**\n * Right now, we do not allow workspaces to have a custom lookback period.\n * If we decide to allow this in the future, we should move this to the database.\n */\nconst WORKSPACES =\n  process.env.WORKSPACES_LOOKBACK_30?.split(\",\").map(Number) || [];\n\nexport const statusPageRouter = createTRPCRouter({\n  get: publicProcedure\n    .input(\n      z.object({\n        slug: z.string().toLowerCase(),\n        // NOTE: override the defaults we are getting from the page configuration\n        cardType: z\n          .enum([\"requests\", \"duration\", \"dominant\", \"manual\"])\n          .nullish(),\n        barType: z.enum([\"absolute\", \"dominant\", \"manual\"]).nullish(),\n      }),\n    )\n    .output(selectPublicPageSchemaWithRelation.nullish())\n    .query(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: sql`lower(${page.slug}) = ${opts.input.slug} OR lower(${page.customDomain}) = ${opts.input.slug}`,\n        with: {\n          workspace: true,\n          statusReports: {\n            // TODO: we need to order the based on statusReportUpdates instead\n            // orderBy: (reports, { desc }) => desc(reports.createdAt),\n            with: {\n              statusReportUpdates: {\n                orderBy: (reports, { desc }) => desc(reports.date),\n              },\n              statusReportsToPageComponents: { with: { pageComponent: true } },\n            },\n          },\n          maintenances: {\n            with: {\n              maintenancesToPageComponents: { with: { pageComponent: true } },\n            },\n            orderBy: (maintenances, { desc }) => desc(maintenances.from),\n          },\n          pageComponents: {\n            with: {\n              monitor: {\n                with: {\n                  incidents: true,\n                },\n              },\n              group: true,\n            },\n            orderBy: (pageComponents, { asc }) => asc(pageComponents.order),\n          },\n          pageComponentGroups: true,\n        },\n      });\n\n      if (!_page) return null;\n\n      const ws = selectWorkspaceSchema.safeParse(_page.workspace);\n      const pageComponents = selectPageComponentWithMonitorRelation\n        .array()\n        .parse(_page.pageComponents);\n\n      const configuration = pageConfigurationSchema.safeParse(\n        _page.configuration ?? {},\n      );\n\n      if (!configuration.success) {\n        console.error(\"Invalid configuration\", configuration.error);\n        return null;\n      }\n\n      const barType = opts.input.barType ?? configuration.data.type;\n      // const cardType = opts.input.cardType ?? configuration.data.value;\n\n      const monitorComponents = pageComponents.filter(isMonitorComponent);\n\n      // Transform all page components (both monitor and static types)\n      const components = pageComponents.map((c) => {\n        const events = getEvents({\n          maintenances: _page.maintenances,\n          incidents: c.monitor?.incidents ?? [],\n          reports: _page.statusReports,\n          pageComponentId: c.id,\n          monitorId: c.monitorId ?? undefined,\n          componentType: c.type,\n        });\n\n        // Calculate status based on component type\n        let status: \"success\" | \"degraded\" | \"error\" | \"info\";\n\n        if (c.type === \"static\") {\n          // Static: only reports and maintenances affect status\n          status = events.some((e) => e.type === \"report\" && !e.to)\n            ? \"degraded\"\n            : events.some(\n                  (e) =>\n                    e.type === \"maintenance\" &&\n                    e.to &&\n                    e.from.getTime() <= new Date().getTime() &&\n                    e.to.getTime() >= new Date().getTime(),\n                )\n              ? \"info\"\n              : \"success\";\n        } else {\n          // Monitor: incidents, reports, and maintenances affect status\n          status =\n            events.some((e) => e.type === \"incident\" && !e.to) &&\n            barType !== \"manual\"\n              ? \"error\"\n              : events.some((e) => e.type === \"report\" && !e.to)\n                ? \"degraded\"\n                : events.some(\n                      (e) =>\n                        e.type === \"maintenance\" &&\n                        e.to &&\n                        e.from.getTime() <= new Date().getTime() &&\n                        e.to.getTime() >= new Date().getTime(),\n                    )\n                  ? \"info\"\n                  : \"success\";\n        }\n\n        return {\n          ...c,\n          status,\n          events,\n        };\n      });\n\n      // Keep monitors for backward compatibility with existing fields\n      const monitors = monitorComponents.map((c) => {\n        const events = getEvents({\n          maintenances: _page.maintenances,\n          incidents: c.monitor.incidents ?? [],\n          reports: _page.statusReports,\n          monitorId: c.monitor.id,\n        });\n        const status =\n          events.some((e) => e.type === \"incident\" && !e.to) &&\n          barType !== \"manual\"\n            ? \"error\"\n            : events.some((e) => e.type === \"report\" && !e.to)\n              ? \"degraded\"\n              : events.some(\n                    (e) =>\n                      e.type === \"maintenance\" &&\n                      e.to &&\n                      e.from.getTime() <= new Date().getTime() &&\n                      e.to.getTime() >= new Date().getTime(),\n                  )\n                ? \"info\"\n                : \"success\";\n        return {\n          ...c.monitor,\n          status,\n          events,\n          monitorGroupId: c.groupId,\n          order: c.order,\n          groupOrder: c.groupOrder,\n        };\n      });\n\n      const status =\n        monitors.some((m) => m.status === \"error\") && barType !== \"manual\"\n          ? \"error\"\n          : monitors.some((m) => m.status === \"degraded\")\n            ? \"degraded\"\n            : monitors.some((m) => m.status === \"info\")\n              ? \"info\"\n              : \"success\";\n\n      // Get page-wide events (not tied to specific monitors)\n      const pageEvents = getEvents({\n        maintenances: _page.maintenances,\n        incidents: monitorComponents.flatMap((c) => c.monitor.incidents ?? []),\n        reports: _page.statusReports,\n        // No monitorId provided, so we get all events for the page\n      });\n\n      const threshold = new Date().getTime() - 7 * 24 * 60 * 60 * 1000;\n      const lastEvents = pageEvents\n        .filter((e) => {\n          if (e.type === \"incident\") return false;\n          if (!e.from || e.from.getTime() >= threshold) return true;\n          if (e.type === \"report\" && e.status !== \"success\") return true;\n          return false;\n        })\n        .sort((a, b) => a.from.getTime() - b.from.getTime());\n\n      const openEvents = pageEvents.filter((event) => {\n        if (event.type === \"incident\" && barType !== \"manual\") {\n          if (!event.to) return true;\n          if (event.to < new Date()) return false;\n          return false;\n        }\n        if (event.type === \"report\") {\n          if (!event.to) return true;\n          if (event.to < new Date()) return false;\n          return false;\n        }\n        if (event.type === \"maintenance\") {\n          if (!event.to) return false; // NOTE: this never happens\n          if (event.from <= new Date() && event.to >= new Date()) return true;\n          return false;\n        }\n        return false;\n      });\n\n      const monitorGroups = _page.pageComponentGroups;\n\n      // Create trackers array with grouped and ungrouped components\n      const groupedMap = new Map<\n        number | null,\n        {\n          groupId: number | null;\n          groupName: string | null;\n          components: typeof components;\n          minOrder: number;\n        }\n      >();\n\n      components.forEach((component) => {\n        const groupId = component.groupId ?? null;\n        const group = groupId\n          ? monitorGroups.find((g) => g?.id === groupId)\n          : null;\n        const groupName = group?.name ?? null;\n\n        if (!groupedMap.has(groupId)) {\n          groupedMap.set(groupId, {\n            groupId,\n            groupName,\n            components: [],\n            minOrder: component.order ?? 0,\n          });\n        }\n        const currentGroup = groupedMap.get(groupId);\n        if (currentGroup) {\n          currentGroup.components.push(component);\n          currentGroup.minOrder = Math.min(\n            currentGroup.minOrder,\n            component.order ?? 0,\n          );\n        }\n      });\n\n      // Convert to trackers array\n      type PageComponentTracker = {\n        type: \"component\";\n        component: (typeof components)[number];\n        order: number;\n      };\n\n      type GroupTracker = {\n        type: \"group\";\n        groupId: number;\n        groupName: string;\n        components: typeof components;\n        status: \"success\" | \"degraded\" | \"error\" | \"info\" | \"empty\";\n        order: number;\n      };\n\n      type Tracker = PageComponentTracker | GroupTracker;\n\n      const trackers: Tracker[] = Array.from(groupedMap.values())\n        .flatMap((group): Tracker[] => {\n          if (group.groupId === null) {\n            // Ungrouped components - return as individual trackers\n            return group.components.map(\n              (component): PageComponentTracker => ({\n                type: \"component\",\n                component,\n                order: component.order ?? 0,\n              }),\n            );\n          }\n          // Grouped components - return as single group tracker\n          const sortedComponents = group.components.sort(\n            (a, b) => (a.groupOrder ?? 0) - (b.groupOrder ?? 0),\n          );\n          return [\n            {\n              type: \"group\",\n              groupId: group.groupId,\n              groupName: group.groupName ?? \"\",\n              components: sortedComponents,\n              status: getWorstVariant(\n                group.components.map(\n                  (c) => c.status as \"success\" | \"degraded\" | \"error\" | \"info\",\n                ),\n              ),\n              order: group.minOrder,\n            },\n          ];\n        })\n        .sort((a, b) => a.order - b.order);\n\n      const whiteLabel = ws.data?.limits[\"white-label\"] ?? false;\n\n      const statusReports = _page.statusReports.sort((a, b) => {\n        // Sort reports without updates to the beginning\n        if (\n          a.statusReportUpdates.length === 0 &&\n          b.statusReportUpdates.length === 0\n        )\n          return 0;\n        if (a.statusReportUpdates.length === 0) return -1;\n        if (b.statusReportUpdates.length === 0) return -1;\n        return (\n          b.statusReportUpdates[\n            b.statusReportUpdates.length - 1\n          ].date.getTime() -\n          a.statusReportUpdates[a.statusReportUpdates.length - 1].date.getTime()\n        );\n      });\n\n      const maintenances = _page.maintenances.sort(\n        (a, b) => b.from.getTime() - a.from.getTime(),\n      );\n\n      return selectPublicPageSchemaWithRelation.parse({\n        ..._page,\n        monitors,\n        monitorGroups,\n        trackers,\n        incidents: monitors.flatMap((m) => m.incidents) ?? [],\n        statusReports,\n        maintenances,\n        workspacePlan: _page.workspace.plan,\n        status,\n        lastEvents,\n        openEvents,\n        pageComponents,\n        pageComponentGroups: _page.pageComponentGroups,\n        whiteLabel,\n      });\n    }),\n\n  getLight: publicProcedure\n    .input(z.object({ slug: z.string().toLowerCase() }))\n    .query(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      // Single query with all relations\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: sql`lower(${page.slug}) = ${opts.input.slug} OR lower(${page.customDomain}) = ${opts.input.slug}`,\n        with: {\n          workspace: true,\n          statusReports: {\n            with: {\n              statusReportUpdates: {\n                orderBy: (reports, { desc }) => desc(reports.date),\n              },\n              statusReportsToPageComponents: { with: { pageComponent: true } },\n            },\n          },\n          maintenances: {\n            with: {\n              maintenancesToPageComponents: { with: { pageComponent: true } },\n            },\n            orderBy: (maintenances, { desc }) => desc(maintenances.from),\n          },\n          pageComponents: {\n            with: {\n              monitor: { with: { incidents: true } },\n              group: true,\n            },\n            orderBy: (pageComponents, { asc }) => asc(pageComponents.order),\n          },\n          pageComponentGroups: true,\n        },\n      });\n\n      if (!_page) return null;\n\n      // Extract monitor components for backwards compatibility\n      const monitorComponents = _page.pageComponents.filter(\n        (c) =>\n          c.type === \"monitor\" &&\n          c.monitor &&\n          c.monitor.active &&\n          !c.monitor.deletedAt,\n      );\n\n      // Build legacy monitors array (sorted by order)\n      const monitors = monitorComponents\n        .map((c) => ({\n          ...c.monitor,\n          name: c.monitor?.externalName ?? c.monitor?.name ?? \"\",\n        }))\n        .sort((a, b) => {\n          const aComp = monitorComponents.find((m) => m.monitor?.id === a.id);\n          const bComp = monitorComponents.find((m) => m.monitor?.id === b.id);\n          return (aComp?.order ?? 0) - (bComp?.order ?? 0);\n        });\n\n      // Extract all incidents from monitor components\n      const incidents = monitorComponents.flatMap(\n        (c) => c.monitor?.incidents ?? [],\n      );\n\n      return selectPublicPageLightSchemaWithRelation.parse({\n        ..._page,\n        monitors,\n        incidents,\n        statusReports: _page.statusReports,\n        maintenances: _page.maintenances,\n        pageComponents: _page.pageComponents,\n        pageComponentGroups: _page.pageComponentGroups,\n        workspacePlan: _page.workspace.plan,\n      });\n    }),\n\n  getMaintenance: publicProcedure\n    .input(z.object({ slug: z.string().toLowerCase(), id: z.number() }))\n    .query(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      const _page = await opts.ctx.db\n        .select()\n        .from(page)\n        .where(\n          sql`lower(${page.slug}) = ${opts.input.slug} OR  lower(${page.customDomain}) = ${opts.input.slug}`,\n        )\n        .get();\n\n      if (!_page) return null;\n\n      const _maintenance = await opts.ctx.db.query.maintenance.findFirst({\n        where: and(\n          eq(maintenance.id, opts.input.id),\n          eq(maintenance.pageId, _page.id),\n        ),\n        with: {\n          maintenancesToPageComponents: {\n            with: { pageComponent: { with: { monitor: true } } },\n          },\n        },\n      });\n\n      if (!_maintenance) return null;\n\n      const props: z.infer<typeof selectMaintenancePageSchema> = _maintenance;\n      return selectMaintenancePageSchema.parse(props);\n    }),\n\n  getUptime: publicProcedure\n    .input(\n      z.object({\n        slug: z.string().toLowerCase(),\n        pageComponentIds: z.string().array(),\n        cardType: z\n          .enum([\"requests\", \"duration\", \"dominant\", \"manual\"])\n          .prefault(\"requests\"),\n        barType: z\n          .enum([\"absolute\", \"dominant\", \"manual\"])\n          .prefault(\"dominant\"),\n      }),\n    )\n    .query(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: sql`lower(${page.slug}) = ${opts.input.slug} OR  lower(${page.customDomain}) = ${opts.input.slug}`,\n        with: {\n          maintenances: {\n            with: {\n              maintenancesToPageComponents: { with: { pageComponent: true } },\n            },\n          },\n          statusReports: {\n            with: {\n              statusReportsToPageComponents: { with: { pageComponent: true } },\n              statusReportUpdates: true,\n            },\n          },\n          pageComponents: {\n            where: inArray(\n              pageComponent.id,\n              opts.input.pageComponentIds.map(Number),\n            ),\n            with: {\n              monitor: {\n                with: {\n                  incidents: true,\n                },\n              },\n            },\n          },\n        },\n      });\n\n      if (!_page) return null;\n\n      const pageComponents = selectPageComponentWithMonitorRelation\n        .array()\n        .parse(_page.pageComponents);\n\n      // Early return if no components to process\n      if (pageComponents.length === 0) return [];\n\n      const monitors = pageComponents.filter(isMonitorComponent);\n\n      const monitorsByType = {\n        http: monitors.filter((c) => c.monitor.jobType === \"http\"),\n        tcp: monitors.filter((c) => c.monitor.jobType === \"tcp\"),\n        dns: monitors.filter((c) => c.monitor.jobType === \"dns\"),\n      };\n\n      const proceduresByType = {\n        http: getStatusProcedure(\"45d\", \"http\"),\n        tcp: getStatusProcedure(\"45d\", \"tcp\"),\n        dns: getStatusProcedure(\"45d\", \"dns\"),\n      };\n\n      const [statusHttp, statusTcp, statusDns] = await Promise.all(\n        Object.entries(proceduresByType).map(([type, procedure]) => {\n          const monitorIds = monitorsByType[\n            type as keyof typeof proceduresByType\n          ].map((c) => c.monitor.id.toString());\n          if (monitorIds.length === 0) return null;\n          // NOTE: if manual mode, don't fetch data from tinybird\n          return opts.input.barType === \"manual\"\n            ? null\n            : procedure({ monitorIds });\n        }),\n      );\n\n      const statusDataByMonitorId = new Map<\n        string,\n        | Awaited<ReturnType<(typeof proceduresByType)[\"http\"]>>[\"data\"]\n        | Awaited<ReturnType<(typeof proceduresByType)[\"tcp\"]>>[\"data\"]\n        | Awaited<ReturnType<(typeof proceduresByType)[\"dns\"]>>[\"data\"]\n      >();\n\n      // Consolidate status data from all monitor types into the map\n      for (const statusResult of [statusHttp, statusTcp, statusDns]) {\n        if (statusResult?.data) {\n          statusResult.data.forEach((status) => {\n            const monitorId = status.monitorId;\n            if (!statusDataByMonitorId.has(monitorId)) {\n              statusDataByMonitorId.set(monitorId, []);\n            }\n            statusDataByMonitorId.get(monitorId)?.push(status);\n          });\n        }\n      }\n\n      const lookbackPeriod = WORKSPACES.includes(_page.workspaceId ?? 0)\n        ? 30\n        : 45;\n\n      return pageComponents.map((c) => {\n        const events = getEvents({\n          maintenances: _page.maintenances,\n          incidents: c.monitor?.incidents ?? [],\n          reports: _page.statusReports,\n          pageComponentId: c.id,\n          monitorId: c.monitorId ?? undefined,\n          componentType: c.type,\n        });\n\n        // Determine whether to use real Tinybird data or synthetic data\n        const shouldUseRealData =\n          c.type === \"monitor\" &&\n          c.monitor &&\n          opts.input.barType !== \"manual\" &&\n          process.env.NOOP_UPTIME !== \"true\";\n\n        let filledData: StatusData[];\n        if (shouldUseRealData) {\n          // Monitor components with real data: use Tinybird data\n          const monitorId = c.monitor?.id.toString() || \"\";\n          const rawData = statusDataByMonitorId.get(monitorId) || [];\n          filledData = fillStatusDataFor45Days(\n            rawData,\n            monitorId,\n            lookbackPeriod,\n          );\n        } else {\n          // Static components, manual mode, or NOOP mode: use synthetic data\n          filledData = fillStatusDataFor45DaysNoop({\n            errorDays: [],\n            degradedDays: [],\n            lookbackPeriod,\n          });\n        }\n\n        // Static components always use manual mode since they don't have real monitoring data\n        const effectiveBarType =\n          c.type === \"static\" ? \"manual\" : opts.input.barType;\n        const effectiveCardType =\n          c.type === \"static\" ? \"manual\" : opts.input.cardType;\n\n        const processedData = setDataByType({\n          events,\n          data: filledData,\n          cardType: effectiveCardType,\n          barType: effectiveBarType,\n        });\n        const uptime = getUptime({\n          data: filledData,\n          events,\n          barType: effectiveBarType,\n          cardType: effectiveCardType,\n        });\n\n        return {\n          id: c.id,\n          pageComponentId: c.id,\n          name: c.name,\n          description: c.description,\n          type: c.type,\n          // For monitor-type components, include monitor fields\n          ...(c.monitor ? { monitor: c.monitor } : {}),\n          data: processedData,\n          uptime,\n        };\n      });\n    }),\n\n  // NOTE: used for the theme store\n  getNoopUptime: publicProcedure.query(async () => {\n    const data = fillStatusDataFor45DaysNoop({\n      errorDays: [4],\n      degradedDays: [40],\n    });\n    const processedData = setDataByType({\n      events: [\n        {\n          type: \"maintenance\",\n          from: new Date(new Date().setDate(new Date().getDate() - 10)),\n          to: new Date(new Date().setDate(new Date().getDate() - 10)),\n          name: \"DB migration\",\n          id: 1,\n          status: \"info\",\n        },\n      ],\n      data,\n      cardType: \"requests\",\n      barType: \"dominant\",\n    });\n    return {\n      data: processedData,\n      uptime: \"100%\",\n    };\n  }),\n\n  getReport: publicProcedure\n    .input(z.object({ slug: z.string().toLowerCase(), id: z.number() }))\n    .query(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      const _page = await opts.ctx.db\n        .select()\n        .from(page)\n        .where(\n          sql`lower(${page.slug}) = ${opts.input.slug} OR  lower(${page.customDomain}) = ${opts.input.slug}`,\n        )\n        .get();\n\n      if (!_page) return null;\n\n      const _report = await opts.ctx.db.query.statusReport.findFirst({\n        where: and(\n          eq(statusReport.id, opts.input.id),\n          eq(statusReport.pageId, _page.id),\n        ),\n        with: {\n          statusReportsToPageComponents: {\n            with: { pageComponent: { with: { monitor: true } } },\n          },\n          statusReportUpdates: {\n            orderBy: (reports, { desc }) => desc(reports.date),\n          },\n        },\n      });\n\n      if (!_report) return null;\n\n      const result: z.infer<typeof selectStatusReportPageSchema> = _report;\n      return selectStatusReportPageSchema.parse(result);\n    }),\n\n  getNoopReport: publicProcedure.query(async () => {\n    const date = new Date(new Date().setDate(new Date().getDate() - 4));\n\n    const resolvedDate = new Date(date.setMinutes(date.getMinutes() - 81));\n    const monitoringDate = new Date(date.setMinutes(date.getMinutes() - 54));\n    const identifiedDate = new Date(date.setMinutes(date.getMinutes() - 32));\n    const investigatingDate = new Date(date.setMinutes(date.getMinutes() - 4));\n\n    const props: z.infer<typeof selectStatusReportPageSchema> = {\n      id: 1,\n      pageId: 1,\n      workspaceId: 1,\n      status: \"investigating\" as const,\n      title: \"API Latency Issues\",\n      createdAt: new Date(new Date().setDate(new Date().getDate() - 2)),\n      updatedAt: new Date(new Date().setDate(new Date().getDate() - 1)),\n      statusReportsToPageComponents: [\n        {\n          pageComponentId: 1,\n          statusReportId: 1,\n          pageComponent: {\n            workspaceId: 1,\n            pageId: 1,\n            id: 1,\n            name: \"API Monitor\",\n            type: \"monitor\" as const,\n            monitorId: 1,\n            order: 1,\n            groupId: null,\n            groupOrder: null,\n            description: \"Main API endpoint\",\n            createdAt: new Date(new Date().setDate(new Date().getDate() - 30)),\n            updatedAt: new Date(new Date().setDate(new Date().getDate() - 30)),\n          },\n        },\n      ],\n      statusReportUpdates: [\n        {\n          id: 4,\n          statusReportId: 1,\n          status: \"resolved\" as const,\n          message:\n            \"All systems are operating normally. The issue has been fully resolved.\",\n          date: resolvedDate,\n          createdAt: resolvedDate,\n          updatedAt: resolvedDate,\n        },\n        {\n          id: 3,\n          statusReportId: 1,\n          status: \"monitoring\" as const,\n          message:\n            \"We are continuing to monitor the situation to ensure that the issue is resolved.\",\n          date: monitoringDate,\n          createdAt: monitoringDate,\n          updatedAt: monitoringDate,\n        },\n        {\n          id: 2,\n          statusReportId: 1,\n          status: \"identified\" as const,\n          message: \"The issue has been identified and a fix is being deployed.\",\n          date: identifiedDate,\n          createdAt: identifiedDate,\n          updatedAt: identifiedDate,\n        },\n        {\n          id: 1,\n          statusReportId: 1,\n          status: \"investigating\" as const,\n          message:\n            \"We are investigating reports of increased latency on our API endpoints.\",\n          date: investigatingDate,\n          createdAt: investigatingDate,\n          updatedAt: investigatingDate,\n        },\n      ],\n    };\n\n    return selectStatusReportPageSchema.parse(props);\n  }),\n\n  getMonitors: publicProcedure\n    .input(z.object({ slug: z.string().toLowerCase() }))\n    .query(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      // NOTE: revalidate the public monitors first\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: sql`lower(${page.slug}) = ${opts.input.slug} OR  lower(${page.customDomain}) = ${opts.input.slug}`,\n        with: {\n          pageComponents: {\n            with: {\n              monitor: true,\n            },\n          },\n        },\n      });\n\n      if (!_page) return null;\n\n      const pageComponents = selectPageComponentWithMonitorRelation\n        .array()\n        .parse(_page.pageComponents);\n\n      const publicMonitors = pageComponents\n        .filter(isMonitorComponent)\n        .filter((c) => c.monitor?.public);\n\n      const monitorsByType = {\n        http: publicMonitors.filter((c) => c.monitor.jobType === \"http\"),\n        tcp: publicMonitors.filter((c) => c.monitor.jobType === \"tcp\"),\n        dns: publicMonitors.filter((c) => c.monitor.jobType === \"dns\"),\n      };\n\n      const proceduresByType = {\n        http: getMetricsLatencyMultiProcedure(\"1d\", \"http\"),\n        tcp: getMetricsLatencyMultiProcedure(\"1d\", \"tcp\"),\n        dns: getMetricsLatencyMultiProcedure(\"1d\", \"dns\"),\n      };\n\n      const [\n        metricsLatencyMultiHttp,\n        metricsLatencyMultiTcp,\n        metricsLatencyMultiDns,\n      ] = await Promise.all(\n        Object.entries(proceduresByType).map(([type, procedure]) => {\n          const monitorIds = monitorsByType[\n            type as keyof typeof proceduresByType\n          ].map((c) => c.monitor.id.toString());\n          if (monitorIds.length === 0) return null;\n          return procedure({ monitorIds });\n        }),\n      );\n\n      const metricsDataByMonitorId = new Map<\n        string,\n        | Awaited<ReturnType<(typeof proceduresByType)[\"http\"]>>[\"data\"]\n        | Awaited<ReturnType<(typeof proceduresByType)[\"tcp\"]>>[\"data\"]\n        | Awaited<ReturnType<(typeof proceduresByType)[\"dns\"]>>[\"data\"]\n      >();\n\n      if (metricsLatencyMultiHttp?.data) {\n        metricsLatencyMultiHttp.data.forEach((metric) => {\n          const monitorId = metric.monitorId;\n          if (!metricsDataByMonitorId.has(monitorId)) {\n            metricsDataByMonitorId.set(monitorId, []);\n          }\n          metricsDataByMonitorId.get(monitorId)?.push(metric);\n        });\n      }\n\n      if (metricsLatencyMultiTcp?.data) {\n        metricsLatencyMultiTcp.data.forEach((metric) => {\n          const monitorId = metric.monitorId;\n          if (!metricsDataByMonitorId.has(monitorId)) {\n            metricsDataByMonitorId.set(monitorId, []);\n          }\n          metricsDataByMonitorId.get(monitorId)?.push(metric);\n        });\n      }\n\n      if (metricsLatencyMultiDns?.data) {\n        metricsLatencyMultiDns.data.forEach((metric) => {\n          const monitorId = metric.monitorId;\n          if (!metricsDataByMonitorId.has(monitorId)) {\n            metricsDataByMonitorId.set(monitorId, []);\n          }\n          metricsDataByMonitorId.get(monitorId)?.push(metric);\n        });\n      }\n\n      return publicMonitors.map((c) => {\n        const monitorId = c.monitor.id.toString();\n        const data = metricsDataByMonitorId.get(monitorId) || [];\n\n        return {\n          ...selectPublicMonitorSchema.parse(c.monitor),\n          data,\n        };\n      });\n    }),\n\n  getMonitor: publicProcedure\n    .input(z.object({ slug: z.string().toLowerCase(), id: z.number() }))\n    .query(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: sql`lower(${page.slug}) = ${opts.input.slug} OR  lower(${page.customDomain}) = ${opts.input.slug}`,\n        with: {\n          pageComponents: {\n            where: eq(pageComponent.monitorId, opts.input.id),\n            with: {\n              monitor: true,\n            },\n          },\n        },\n      });\n\n      if (!_page) return null;\n\n      const pageComponents = selectPageComponentWithMonitorRelation\n        .array()\n        .parse(_page.pageComponents);\n\n      const monitorComponents = pageComponents.filter(isMonitorComponent);\n\n      const _monitor = monitorComponents.find(\n        (c) => c.monitor.id === opts.input.id,\n      )?.monitor;\n\n      if (!_monitor) return null;\n      if (!_monitor.public) return null;\n      if (_monitor.deletedAt) return null;\n\n      const type = _monitor.jobType as \"http\" | \"tcp\";\n\n      const proceduresByType = {\n        http: {\n          latency: getMetricsLatencyProcedure(\"7d\", \"http\"),\n          regions: getMetricsRegionsProcedure(\"7d\", \"http\"),\n          uptime: getUptimeProcedure(\"7d\", \"http\"),\n        },\n        tcp: {\n          latency: getMetricsLatencyProcedure(\"7d\", \"tcp\"),\n          regions: getMetricsRegionsProcedure(\"7d\", \"tcp\"),\n          uptime: getUptimeProcedure(\"7d\", \"tcp\"),\n        },\n        dns: {\n          latency: getMetricsLatencyProcedure(\"7d\", \"dns\"),\n          regions: getMetricsRegionsProcedure(\"7d\", \"dns\"),\n          uptime: getUptimeProcedure(\"7d\", \"dns\"),\n        },\n      };\n\n      const fromDate = startOfDay(subDays(new Date(), 7)).toISOString();\n      const toDate = endOfDay(new Date()).toISOString();\n\n      const [latency, regions, uptime] = await Promise.all([\n        await proceduresByType[type].latency({\n          monitorId: _monitor.id.toString(),\n          fromDate,\n          toDate,\n        }),\n        await proceduresByType[type].regions({\n          monitorId: _monitor.id.toString(),\n          fromDate,\n          toDate,\n        }),\n        await proceduresByType[type].uptime({\n          monitorId: _monitor.id.toString(),\n          interval: 240,\n          fromDate,\n          toDate,\n        }),\n      ]);\n\n      return {\n        ...selectPublicMonitorSchema.parse(_monitor),\n        data: {\n          latency,\n          regions,\n          uptime,\n        },\n      };\n    }),\n\n  subscribe: publicProcedure\n    .meta({ track: Events.SubscribePage, trackProps: [\"slug\", \"email\"] })\n    .input(\n      z.object({\n        slug: z.string().toLowerCase(),\n        email: z.email(),\n        subscribeComponents: z.boolean(),\n        pageComponents: z.array(z.number().int().positive()),\n      }),\n    )\n    .mutation(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: sql`lower(${page.slug}) = ${opts.input.slug} OR  lower(${page.customDomain}) = ${opts.input.slug}`,\n        with: {\n          workspace: true,\n        },\n      });\n\n      if (!_page) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Page not found\",\n        });\n      }\n\n      const workspace = selectWorkspaceSchema.safeParse(_page.workspace);\n\n      if (!workspace.success) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Workspace data is invalid\",\n        });\n      }\n\n      if (!workspace.data.limits[\"status-subscribers\"]) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"Upgrade to use status subscribers\",\n        });\n      }\n\n      // Guard against email spam: reject if a pending (unverified, unexpired) subscription exists\n      const isPending = await hasPendingUnexpiredSubscription(\n        opts.input.email,\n        _page.id,\n      );\n      if (isPending) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message:\n            \"A confirmation link was already sent. Please check your email or wait until it expires to request a new one.\",\n        });\n      }\n\n      const subscription = await upsertEmailSubscription({\n        email: opts.input.email,\n        pageId: _page.id,\n        componentIds: opts.input.subscribeComponents\n          ? opts.input.pageComponents\n          : [],\n      });\n\n      // Already verified — no need to send another verification email\n      if (subscription.acceptedAt) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Email already subscribed\",\n        });\n      }\n\n      return { id: subscription.id, token: subscription.token };\n    }),\n\n  getSubscriptionByToken: publicProcedure\n    .input(z.object({ slug: z.string().toLowerCase(), token: z.uuid() }))\n    .query(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      const subscription = await getSubscriptionByToken(\n        opts.input.token,\n        opts.input.slug,\n      );\n\n      if (!subscription) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Subscription not found\",\n        });\n      }\n\n      return subscription;\n    }),\n\n  updateSubscription: publicProcedure\n    .input(\n      z.object({\n        slug: z.string().toLowerCase(),\n        token: z.uuid(),\n        subscribeComponents: z.boolean(),\n        pageComponents: z.array(z.number().int().positive()),\n      }),\n    )\n    .mutation(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      try {\n        await updateSubscriptionScope({\n          token: opts.input.token,\n          componentIds: opts.input.subscribeComponents\n            ? opts.input.pageComponents\n            : [],\n          domain: opts.input.slug,\n        });\n      } catch (error) {\n        if (error instanceof Error) {\n          const code = error.message.toLowerCase().includes(\"not found\")\n            ? \"NOT_FOUND\"\n            : \"BAD_REQUEST\";\n          throw new TRPCError({ code, message: error.message });\n        }\n        throw error;\n      }\n\n      return { success: true };\n    }),\n\n  validateEmailDomain: publicProcedure\n    .meta({ track: Events.ValidateEmailDomain, trackProps: [\"slug\", \"email\"] })\n    .input(z.object({ slug: z.string().toLowerCase(), email: z.string() }))\n    .query(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: sql`lower(${page.slug}) = ${opts.input.slug} OR  lower(${page.customDomain}) = ${opts.input.slug}`,\n      });\n\n      if (!_page) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Page not found\",\n        });\n      }\n\n      if (_page.accessType !== \"email-domain\") {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message:\n            \"Page is not configured to allow email domain authentication\",\n        });\n      }\n\n      const allowedDomains = _page.authEmailDomains?.split(\",\") ?? [];\n\n      if (!allowedDomains.includes(opts.input.email.split(\"@\")[1])) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Invalid email domain\",\n        });\n      }\n\n      return {\n        email: opts.input.email,\n        slug: opts.input.slug,\n        page: _page,\n      };\n    }),\n\n  verifyEmail: publicProcedure\n    .meta({ track: Events.VerifySubscribePage, trackProps: [\"slug\"] })\n    .input(z.object({ slug: z.string().toLowerCase(), token: z.uuid() }))\n    .mutation(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      try {\n        const subscription = await verifySubscription(\n          opts.input.token,\n          opts.input.slug,\n        );\n\n        if (!subscription) {\n          throw new TRPCError({\n            code: \"NOT_FOUND\",\n            message: \"Subscription not found\",\n          });\n        }\n\n        return subscription;\n      } catch (error) {\n        if (error instanceof TRPCError) throw error;\n        if (error instanceof Error) {\n          throw new TRPCError({ code: \"BAD_REQUEST\", message: error.message });\n        }\n        throw error;\n      }\n    }),\n\n  verifyPassword: publicProcedure\n    .input(z.object({ slug: z.string().toLowerCase(), password: z.string() }))\n    .mutation(async (opts) => {\n      if (!opts.input.slug) return null;\n\n      const _page = await opts.ctx.db.query.page.findFirst({\n        where: sql`lower(${page.slug}) = ${opts.input.slug} OR  lower(${page.customDomain}) = ${opts.input.slug}`,\n      });\n\n      if (!_page) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Page not found\",\n        });\n      }\n\n      if (_page.accessType !== \"password\") {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Page is not configured to allow password authentication\",\n        });\n      }\n\n      if (_page.password !== opts.input.password) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Invalid password\",\n        });\n      }\n\n      return true;\n    }),\n\n  getSubscriberByToken: publicProcedure\n    .input(z.object({ token: z.uuid(), domain: z.string().toLowerCase() }))\n    .query(async (opts) => {\n      const subscription = await getSubscriptionByToken(\n        opts.input.token,\n        opts.input.domain,\n      );\n\n      if (\n        !subscription ||\n        subscription.unsubscribedAt ||\n        subscription.channelType !== \"email\"\n      ) {\n        return null;\n      }\n\n      return {\n        pageName: subscription.pageName,\n        maskedEmail: subscription.email,\n      };\n    }),\n\n  unsubscribe: publicProcedure\n    .input(z.object({ token: z.uuid(), domain: z.string().toLowerCase() }))\n    .mutation(async (opts) => {\n      try {\n        await unsubscribe(opts.input.token, opts.input.domain);\n        return { success: true };\n      } catch (error) {\n        if (error instanceof Error) {\n          throw new TRPCError({ code: \"BAD_REQUEST\", message: error.message });\n        }\n        throw error;\n      }\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/statusPage.unsubscribe.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { page, pageSubscriber } from \"@openstatus/db/src/schema\";\n\n// Test data setup\nlet testPageId: number;\nlet testToken: string;\nconst testWorkspaceId = 1; // Use existing test workspace from seed data\n\nbeforeAll(async () => {\n  // Clean up any existing test data\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"test-unsubscribe@example.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"test-unsubscribe-2@example.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"test-unsubscribe-3@example.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"test-unsubscribe-4@example.com\"));\n  await db.delete(page).where(eq(page.slug, \"test-unsubscribe-page\"));\n\n  // Create a test page\n  const testPage = await db\n    .insert(page)\n    .values({\n      workspaceId: testWorkspaceId,\n      title: \"Test Unsubscribe Page\",\n      description: \"A test page for unsubscribe tests\",\n      slug: \"test-unsubscribe-page\",\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n\n  testPageId = testPage.id;\n\n  // Create a verified subscriber for testing\n  testToken = crypto.randomUUID();\n  await db\n    .insert(pageSubscriber)\n    .values({\n      pageId: testPageId,\n      email: \"test-unsubscribe@example.com\",\n      token: testToken,\n      acceptedAt: new Date(),\n      expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n    })\n    .returning()\n    .get();\n});\n\nafterAll(async () => {\n  // Clean up test data\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"test-unsubscribe@example.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"test-unsubscribe-2@example.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"test-unsubscribe-3@example.com\"));\n  await db\n    .delete(pageSubscriber)\n    .where(eq(pageSubscriber.email, \"test-unsubscribe-4@example.com\"));\n  await db.delete(page).where(eq(page.slug, \"test-unsubscribe-page\"));\n});\n\ndescribe(\"getSubscriberByToken\", () => {\n  test(\"should return subscriber info with masked email for valid token\", async () => {\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, testToken),\n      with: { page: true },\n    });\n\n    expect(subscriber).toBeDefined();\n    expect(subscriber?.page.title).toBe(\"Test Unsubscribe Page\");\n\n    // Manually mask the email to test the masking logic\n    const email = subscriber?.email;\n    expect(email).toBeDefined();\n    if (email) {\n      const [localPart, domain] = email.split(\"@\");\n      const maskedEmail =\n        localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n      expect(maskedEmail).toBe(\"t***@example.com\");\n    }\n  });\n\n  test(\"should return null for non-existent token\", async () => {\n    const nonExistentToken = crypto.randomUUID();\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, nonExistentToken),\n    });\n\n    expect(subscriber).toBeUndefined();\n  });\n\n  test(\"should return null for already unsubscribed user\", async () => {\n    // Create an unsubscribed subscriber\n    const unsubscribedToken = crypto.randomUUID();\n    await db.insert(pageSubscriber).values({\n      pageId: testPageId,\n      email: \"test-unsubscribe-2@example.com\",\n      token: unsubscribedToken,\n      acceptedAt: new Date(),\n      unsubscribedAt: new Date(),\n      expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n    });\n\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, unsubscribedToken),\n    });\n\n    expect(subscriber).toBeDefined();\n    expect(subscriber?.unsubscribedAt).not.toBeNull();\n  });\n\n  test(\"should properly mask emails with single character local part\", async () => {\n    const email = \"a@example.com\";\n    const [localPart, domain] = email.split(\"@\");\n    const maskedEmail =\n      localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n    expect(maskedEmail).toBe(\"a***@example.com\");\n  });\n\n  test(\"should properly mask emails with long local part\", async () => {\n    const email = \"verylongemail@example.com\";\n    const [localPart, domain] = email.split(\"@\");\n    const maskedEmail =\n      localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n    expect(maskedEmail).toBe(\"v***@example.com\");\n  });\n});\n\ndescribe(\"unsubscribe mutation\", () => {\n  test(\"should unsubscribe a verified subscriber successfully\", async () => {\n    // Create a fresh subscriber for this test\n    const newToken = crypto.randomUUID();\n    const newSubscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: \"test-unsubscribe-3@example.com\",\n        token: newToken,\n        acceptedAt: new Date(),\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .returning()\n      .get();\n\n    // Simulate unsubscribe operation\n    await db\n      .update(pageSubscriber)\n      .set({ unsubscribedAt: new Date() })\n      .where(eq(pageSubscriber.id, newSubscriber.id));\n\n    // Verify unsubscribed\n    const updatedSubscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.id, newSubscriber.id),\n    });\n\n    expect(updatedSubscriber?.unsubscribedAt).not.toBeNull();\n    expect(updatedSubscriber?.unsubscribedAt).toBeInstanceOf(Date);\n  });\n\n  test(\"should fail for non-existent token\", async () => {\n    const nonExistentToken = crypto.randomUUID();\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, nonExistentToken),\n    });\n\n    expect(subscriber).toBeUndefined();\n  });\n\n  test(\"should fail for unverified subscriber\", async () => {\n    // Create an unverified subscriber\n    const unverifiedToken = crypto.randomUUID();\n    const unverifiedSubscriber = await db\n      .insert(pageSubscriber)\n      .values({\n        pageId: testPageId,\n        email: \"test-unsubscribe-4@example.com\",\n        token: unverifiedToken,\n        acceptedAt: null, // Not verified\n        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),\n      })\n      .returning()\n      .get();\n\n    expect(unverifiedSubscriber.acceptedAt).toBeNull();\n  });\n\n  test(\"should fail for already unsubscribed user\", async () => {\n    // Get the unsubscribed subscriber from earlier test\n    const unsubscribedSubscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.email, \"test-unsubscribe-2@example.com\"),\n    });\n\n    expect(unsubscribedSubscriber).toBeDefined();\n    expect(unsubscribedSubscriber?.unsubscribedAt).not.toBeNull();\n  });\n\n  test(\"should set unsubscribedAt to current timestamp\", async () => {\n    const beforeUnsubscribe = new Date();\n\n    // Get subscriber and unsubscribe\n    const subscriber = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.email, \"test-unsubscribe-3@example.com\"),\n    });\n\n    if (subscriber) {\n      await db\n        .update(pageSubscriber)\n        .set({ unsubscribedAt: new Date() })\n        .where(eq(pageSubscriber.id, subscriber.id));\n\n      const updatedSubscriber = await db.query.pageSubscriber.findFirst({\n        where: eq(pageSubscriber.id, subscriber.id),\n      });\n\n      const afterUnsubscribe = new Date();\n\n      expect(updatedSubscriber?.unsubscribedAt).toBeDefined();\n      expect(\n        updatedSubscriber?.unsubscribedAt?.getTime(),\n      ).toBeGreaterThanOrEqual(beforeUnsubscribe.getTime() - 1000);\n      expect(updatedSubscriber?.unsubscribedAt?.getTime()).toBeLessThanOrEqual(\n        afterUnsubscribe.getTime() + 1000,\n      );\n    }\n  });\n});\n\ndescribe(\"email masking logic\", () => {\n  test(\"should mask email j***@example.com correctly\", () => {\n    const email = \"john@example.com\";\n    const [localPart, domain] = email.split(\"@\");\n    const maskedEmail =\n      localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n    expect(maskedEmail).toBe(\"j***@example.com\");\n  });\n\n  test(\"should handle empty local part gracefully\", () => {\n    // Edge case: if somehow we have @domain only\n    const email = \"@example.com\";\n    const [localPart, domain] = email.split(\"@\");\n    const maskedEmail =\n      localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n    expect(maskedEmail).toBe(\"***@example.com\");\n  });\n\n  test(\"should preserve domain in masked email\", () => {\n    const email = \"user@custom-domain.io\";\n    const [localPart, domain] = email.split(\"@\");\n    const maskedEmail =\n      localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n    expect(maskedEmail).toBe(\"u***@custom-domain.io\");\n  });\n\n  test(\"should handle complex domain\", () => {\n    const email = \"test@subdomain.example.co.uk\";\n    const [localPart, domain] = email.split(\"@\");\n    const maskedEmail =\n      localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`;\n\n    expect(maskedEmail).toBe(\"t***@subdomain.example.co.uk\");\n  });\n});\n\ndescribe(\"token validation\", () => {\n  test(\"should validate UUID format for token\", () => {\n    const validToken = crypto.randomUUID();\n    const uuidRegex =\n      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n    expect(validToken).toMatch(uuidRegex);\n  });\n\n  test(\"should reject invalid UUID format\", () => {\n    const invalidToken = \"not-a-valid-uuid\";\n    const uuidRegex =\n      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n    expect(invalidToken).not.toMatch(uuidRegex);\n  });\n});\n"
  },
  {
    "path": "packages/api/src/router/statusPage.utils.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\";\nimport type {\n  Incident,\n  Maintenance,\n  PageComponent,\n  StatusReport,\n  StatusReportUpdate,\n} from \"@openstatus/db/src/schema\";\nimport {\n  fillStatusDataFor45Days,\n  getEvents,\n  getUptime,\n  setDataByType,\n} from \"./statusPage.utils\";\n\ntype StatusData = {\n  day: string;\n  count: number;\n  ok: number;\n  degraded: number;\n  error: number;\n  monitorId: string;\n};\n\ntype Event = {\n  id: number;\n  name: string;\n  from: Date;\n  to: Date | null;\n  type: \"maintenance\" | \"incident\" | \"report\";\n  status: \"success\" | \"degraded\" | \"error\" | \"info\";\n};\n\n// Helper functions to create test data\nfunction createStatusData(\n  daysAgo: number,\n  ok = 0,\n  degraded = 0,\n  error = 0,\n): StatusData {\n  const date = new Date();\n  date.setDate(date.getDate() - daysAgo);\n  date.setUTCHours(0, 0, 0, 0);\n\n  return {\n    day: date.toISOString(),\n    count: ok + degraded + error,\n    ok,\n    degraded,\n    error,\n    monitorId: \"1\",\n  };\n}\n\nfunction createIncident(id: number, daysAgo: number, durationHours = 1): Event {\n  const from = new Date();\n  from.setDate(from.getDate() - daysAgo);\n  from.setHours(from.getHours() - durationHours);\n\n  const to = new Date(from);\n  to.setHours(to.getHours() + durationHours);\n\n  return {\n    id,\n    name: \"Downtime\",\n    from,\n    to,\n    type: \"incident\",\n    status: \"error\",\n  };\n}\n\nfunction createReport(id: number, daysAgo: number, durationHours = 2): Event {\n  const from = new Date();\n  from.setDate(from.getDate() - daysAgo);\n  from.setHours(from.getHours() - durationHours);\n\n  const to = new Date(from);\n  to.setHours(to.getHours() + durationHours);\n\n  return {\n    id,\n    name: \"Performance Issues\",\n    from,\n    to,\n    type: \"report\",\n    status: \"degraded\",\n  };\n}\n\nfunction createMaintenance(\n  id: number,\n  daysAgo: number,\n  durationHours = 1,\n): Event {\n  const from = new Date();\n  from.setDate(from.getDate() - daysAgo);\n  from.setHours(from.getHours() - durationHours);\n\n  const to = new Date(from);\n  to.setHours(to.getHours() + durationHours);\n\n  return {\n    id,\n    name: \"Scheduled Maintenance\",\n    from,\n    to,\n    type: \"maintenance\",\n    status: \"info\",\n  };\n}\n\ndescribe(\"setDataByType\", () => {\n  describe(\"barType: absolute\", () => {\n    it(\"should show proportional bar segments with error-only incident\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createIncident(1, 0, 2)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"absolute\",\n      });\n\n      expect(result).toHaveLength(1);\n      expect(result[0].bar).toHaveLength(2);\n      expect(result[0].bar[0].status).toBe(\"success\");\n      expect(result[0].bar[1].status).toBe(\"error\");\n      // Should have uptime and downtime segments\n      expect(result[0].bar[0].height).toBeGreaterThan(0);\n      expect(result[0].bar[1].height).toBeGreaterThan(0);\n    });\n\n    it(\"should show proportional segments with multiple event types\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [\n        createIncident(1, 0, 1),\n        createReport(2, 0, 2),\n        createMaintenance(3, 0, 1),\n      ];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"absolute\",\n      });\n\n      expect(result[0].bar.length).toBeGreaterThan(1);\n      // Should include info, degraded, and error segments\n      const statuses = result[0].bar.map((b) => b.status);\n      expect(statuses).toContain(\"error\");\n      expect(statuses).toContain(\"degraded\");\n      expect(statuses).toContain(\"info\");\n    });\n\n    it(\"should show empty bar when no data available\", () => {\n      const data = [createStatusData(0, 0, 0, 0)];\n      const events: Event[] = [];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"absolute\",\n      });\n\n      expect(result[0].bar).toHaveLength(1);\n      expect(result[0].bar[0].status).toBe(\"empty\");\n      expect(result[0].bar[0].height).toBe(100);\n    });\n\n    it(\"should show operational bar with duration cardType and no events\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events: Event[] = [];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"duration\",\n        barType: \"absolute\",\n      });\n\n      expect(result[0].bar).toHaveLength(1);\n      expect(result[0].bar[0].status).toBe(\"success\");\n      expect(result[0].bar[0].height).toBe(100);\n    });\n\n    it(\"should show proportional status segments with mixed data and no events\", () => {\n      const data = [createStatusData(0, 80, 15, 5)];\n      const events: Event[] = [];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"absolute\",\n      });\n\n      expect(result[0].bar.length).toBeGreaterThan(1);\n      const statuses = result[0].bar.map((b) => b.status);\n      expect(statuses).toContain(\"success\");\n      expect(statuses).toContain(\"degraded\");\n      expect(statuses).toContain(\"error\");\n    });\n  });\n\n  describe(\"barType: dominant\", () => {\n    it(\"should show error as dominant status when incident exists\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createIncident(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].bar).toHaveLength(1);\n      expect(result[0].bar[0].status).toBe(\"error\");\n      expect(result[0].bar[0].height).toBe(100);\n    });\n\n    it(\"should show degraded when only reports exist\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createReport(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].bar).toHaveLength(1);\n      expect(result[0].bar[0].status).toBe(\"degraded\");\n      expect(result[0].bar[0].height).toBe(100);\n    });\n\n    it(\"should show info when only maintenance exists\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createMaintenance(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].bar).toHaveLength(1);\n      expect(result[0].bar[0].status).toBe(\"info\");\n      expect(result[0].bar[0].height).toBe(100);\n    });\n\n    it(\"should prioritize error over other statuses\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [\n        createIncident(1, 0),\n        createReport(2, 0),\n        createMaintenance(3, 0),\n      ];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].bar[0].status).toBe(\"error\");\n    });\n\n    it(\"should show data status when no events\", () => {\n      const data = [createStatusData(0, 0, 100, 0)];\n      const events: Event[] = [];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].bar[0].status).toBe(\"degraded\");\n    });\n  });\n\n  describe(\"barType: manual\", () => {\n    it(\"should show degraded when reports exist\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createReport(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"manual\",\n        barType: \"manual\",\n      });\n\n      expect(result[0].bar).toHaveLength(1);\n      expect(result[0].bar[0].status).toBe(\"degraded\");\n      expect(result[0].bar[0].height).toBe(100);\n    });\n\n    it(\"should show info when only maintenance exists\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createMaintenance(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"manual\",\n        barType: \"manual\",\n      });\n\n      expect(result[0].bar).toHaveLength(1);\n      expect(result[0].bar[0].status).toBe(\"info\");\n      expect(result[0].bar[0].height).toBe(100);\n    });\n\n    it(\"should ignore incidents and show success\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createIncident(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"manual\",\n        barType: \"manual\",\n      });\n\n      expect(result[0].bar).toHaveLength(1);\n      expect(result[0].bar[0].status).toBe(\"success\");\n    });\n\n    it(\"should prioritize reports over maintenance\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createReport(1, 0), createMaintenance(2, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"manual\",\n        barType: \"manual\",\n      });\n\n      expect(result[0].bar[0].status).toBe(\"degraded\");\n    });\n  });\n\n  describe(\"cardType: requests\", () => {\n    it(\"should show request counts for each status\", () => {\n      const data = [createStatusData(0, 100, 50, 10)];\n      const events: Event[] = [];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].card.length).toBe(3);\n      expect(result[0].card.some((c) => c.value.includes(\"100 reqs\"))).toBe(\n        true,\n      );\n      expect(result[0].card.some((c) => c.value.includes(\"50 reqs\"))).toBe(\n        true,\n      );\n      expect(result[0].card.some((c) => c.value.includes(\"10 reqs\"))).toBe(\n        true,\n      );\n    });\n\n    it(\"should format large numbers correctly\", () => {\n      const data = [createStatusData(0, 5000, 0, 0)];\n      const events: Event[] = [];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].card[0].value).toBe(\"5.0k reqs\");\n    });\n\n    it(\"should show empty card when no data\", () => {\n      const data = [createStatusData(0, 0, 0, 0)];\n      const events: Event[] = [];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].card).toHaveLength(1);\n      expect(result[0].card[0].value).toBe(\"\");\n      expect(result[0].card[0].status).toBe(\"empty\");\n    });\n\n    it(\"should show event status in empty card when no data but events exist\", () => {\n      const data = [createStatusData(0, 0, 0, 0)];\n      const events = [createIncident(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].card).toHaveLength(1);\n      expect(result[0].card[0].status).toBe(\"error\");\n    });\n  });\n\n  describe(\"cardType: duration\", () => {\n    it(\"should calculate duration for events\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [\n        createIncident(1, 0, 1), // 1 hour\n        createReport(2, 0, 2), // 2 hours\n      ];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"duration\",\n        barType: \"absolute\",\n      });\n\n      expect(result[0].card.length).toBeGreaterThan(0);\n      // Should have durations for error, degraded, and success\n      const hasError = result[0].card.some(\n        (c) => c.status === \"error\" && c.value.includes(\"h\"),\n      );\n      const hasDegraded = result[0].card.some(\n        (c) => c.status === \"degraded\" && c.value.includes(\"h\"),\n      );\n      expect(hasError || hasDegraded).toBe(true);\n    });\n\n    it(\"should format duration in hours and minutes\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createIncident(1, 0, 1.5)]; // 1.5 hours = 1h 30m\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"duration\",\n        barType: \"absolute\",\n      });\n\n      const errorCard = result[0].card.find((c) => c.status === \"error\");\n      expect(errorCard).toBeDefined();\n      // Should contain hour notation and optionally minutes\n      expect(errorCard?.value).toMatch(/\\d+h(\\s\\d+m)?/);\n    });\n\n    it(\"should show success duration as remaining time\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createIncident(1, 0, 1)]; // 1 hour downtime\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"duration\",\n        barType: \"absolute\",\n      });\n\n      const successCard = result[0].card.find((c) => c.status === \"success\");\n      expect(successCard).toBeDefined();\n      // Success duration should be total time minus downtime\n      expect(successCard?.value).toBeTruthy();\n    });\n\n    it(\"should exclude maintenance from success calculation\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createMaintenance(1, 0, 2)]; // 2 hours maintenance\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"duration\",\n        barType: \"absolute\",\n      });\n\n      const successCard = result[0].card.find((c) => c.status === \"success\");\n      // Success should account for maintenance being excluded from total time\n      expect(successCard).toBeDefined();\n    });\n\n    it(\"should show empty card when no data\", () => {\n      const data = [createStatusData(0, 0, 0, 0)];\n      const events: Event[] = [];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"duration\",\n        barType: \"absolute\",\n      });\n\n      expect(result[0].card).toHaveLength(1);\n      expect(result[0].card[0].value).toBe(\"\");\n    });\n  });\n\n  describe(\"cardType: dominant\", () => {\n    it(\"should show dominant status without value\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createIncident(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"dominant\",\n        barType: \"dominant\",\n      });\n\n      expect(result[0].card).toHaveLength(1);\n      expect(result[0].card[0].status).toBe(\"error\");\n      expect(result[0].card[0].value).toBe(\"\");\n    });\n  });\n\n  describe(\"cardType: manual\", () => {\n    it(\"should show degraded for reports\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createReport(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"manual\",\n        barType: \"manual\",\n      });\n\n      expect(result[0].card).toHaveLength(1);\n      expect(result[0].card[0].status).toBe(\"degraded\");\n      expect(result[0].card[0].value).toBe(\"\");\n    });\n\n    it(\"should show success when no manual events\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [createIncident(1, 0)];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"manual\",\n        barType: \"manual\",\n      });\n\n      expect(result[0].card[0].status).toBe(\"success\");\n    });\n  });\n\n  describe(\"event bundling\", () => {\n    it(\"should bundle more than 4 incidents into single event\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [\n        createIncident(1, 0),\n        createIncident(2, 0),\n        createIncident(3, 0),\n        createIncident(4, 0),\n        createIncident(5, 0),\n      ];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"absolute\",\n      });\n\n      // Should have bundled incident with special id -1\n      const bundledIncident = result[0].events.find((e) => e.id === -1);\n      expect(bundledIncident).toBeDefined();\n      expect(bundledIncident?.name).toContain(\"5 incidents\");\n    });\n\n    it(\"should not bundle 4 or fewer incidents\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [\n        createIncident(1, 0),\n        createIncident(2, 0),\n        createIncident(3, 0),\n        createIncident(4, 0),\n      ];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"absolute\",\n      });\n\n      // Should not have bundled incident\n      const bundledIncident = result[0].events.find((e) => e.id === -1);\n      expect(bundledIncident).toBeUndefined();\n    });\n\n    it(\"should not bundle incidents for non-absolute bar types\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events = [\n        createIncident(1, 0),\n        createIncident(2, 0),\n        createIncident(3, 0),\n        createIncident(4, 0),\n        createIncident(5, 0),\n      ];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      // Should not include any incidents in events array\n      expect(result[0].events.length).toBe(0);\n    });\n  });\n\n  describe(\"multiple days\", () => {\n    it(\"should handle data across multiple days\", () => {\n      const data = [\n        createStatusData(0, 100, 0, 0),\n        createStatusData(1, 80, 20, 0),\n        createStatusData(2, 60, 30, 10),\n      ];\n      const events = [\n        createIncident(1, 0),\n        createReport(2, 1),\n        createMaintenance(3, 2),\n      ];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result).toHaveLength(3);\n      expect(result[0].bar[0].status).toBe(\"error\"); // Day 0 has incident\n      expect(result[1].bar[0].status).toBe(\"degraded\"); // Day 1 has report\n      expect(result[2].bar[0].status).toBe(\"info\"); // Day 2 has maintenance\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    it(\"should handle empty data array\", () => {\n      const data: StatusData[] = [];\n      const events: Event[] = [];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      expect(result).toHaveLength(0);\n    });\n\n    it(\"should handle events with null end date\", () => {\n      const data = [createStatusData(0, 100, 0, 0)];\n      const events: Event[] = [\n        {\n          id: 1,\n          name: \"Ongoing Incident\",\n          from: new Date(),\n          to: null,\n          type: \"incident\",\n          status: \"error\",\n        },\n      ];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"duration\",\n        barType: \"absolute\",\n      });\n\n      expect(result[0].bar.some((b) => b.status === \"error\")).toBe(true);\n    });\n\n    it(\"should handle events spanning multiple days\", () => {\n      const data = [\n        createStatusData(0, 100, 0, 0),\n        createStatusData(1, 100, 0, 0),\n      ];\n      const from = new Date();\n      from.setDate(from.getDate() - 1);\n      from.setHours(12, 0, 0, 0);\n      const to = new Date();\n      to.setHours(12, 0, 0, 0);\n\n      const events: Event[] = [\n        {\n          id: 1,\n          name: \"Multi-day Incident\",\n          from,\n          to,\n          type: \"incident\",\n          status: \"error\",\n        },\n      ];\n\n      const result = setDataByType({\n        events,\n        data,\n        cardType: \"requests\",\n        barType: \"dominant\",\n      });\n\n      // Both days should show error status\n      expect(result[0].bar[0].status).toBe(\"error\");\n      expect(result[1].bar[0].status).toBe(\"error\");\n    });\n  });\n});\n\ndescribe(\"fillStatusDataFor45Days\", () => {\n  it(\"should fill all 45 days\", () => {\n    const data: StatusData[] = [];\n    const result = fillStatusDataFor45Days(data, \"1\");\n\n    expect(result).toHaveLength(45);\n  });\n\n  it(\"should sort data by day oldest first\", () => {\n    const data: StatusData[] = [];\n    const result = fillStatusDataFor45Days(data, \"1\");\n\n    for (let i = 1; i < result.length; i++) {\n      const prev = new Date(result[i - 1].day);\n      const curr = new Date(result[i].day);\n      expect(curr.getTime()).toBeGreaterThan(prev.getTime());\n    }\n  });\n\n  it(\"should preserve existing data\", () => {\n    const existingData = [createStatusData(5, 100, 50, 10)];\n    const result = fillStatusDataFor45Days(existingData, \"1\");\n\n    expect(result).toHaveLength(45);\n    const matchingDay = result.find(\n      (d) => d.ok === 100 && d.degraded === 50 && d.error === 10,\n    );\n    expect(matchingDay).toBeDefined();\n  });\n\n  it(\"should fill missing days with zeros\", () => {\n    const existingData = [createStatusData(5, 100, 0, 0)];\n    const result = fillStatusDataFor45Days(existingData, \"1\");\n\n    const emptyDays = result.filter((d) => d.count === 0);\n    expect(emptyDays.length).toBe(44);\n  });\n});\n\ndescribe(\"getUptime\", () => {\n  describe(\"manual bar type\", () => {\n    it(\"should calculate uptime based on report durations\", () => {\n      const data = Array.from({ length: 45 }, (_, i) =>\n        createStatusData(i, 100, 0, 0),\n      );\n      const events = [createReport(1, 0, 24)]; // 1 day downtime\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"manual\",\n        cardType: \"manual\",\n      });\n\n      // Should be approximately 97.78% (44/45 days)\n      expect(Number.parseFloat(uptime)).toBeGreaterThan(97);\n      expect(Number.parseFloat(uptime)).toBeLessThan(98);\n    });\n\n    it(\"should only consider reports not incidents\", () => {\n      const data = Array.from({ length: 45 }, (_, i) =>\n        createStatusData(i, 100, 0, 0),\n      );\n      const events = [createIncident(1, 0, 24)]; // Should be ignored\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"manual\",\n        cardType: \"manual\",\n      });\n\n      expect(uptime).toBe(\"100%\");\n    });\n\n    it(\"should clamp report durations to the lookback window and never go negative\", () => {\n      const data = Array.from({ length: 45 }, (_, i) =>\n        createStatusData(i, 100, 0, 0),\n      );\n      // Create reports that started well before the 45-day window\n      const events: Event[] = Array.from({ length: 15 }, (_, i) => {\n        const from = new Date();\n        from.setDate(from.getDate() - (60 + i * 30)); // 60 to 480 days ago\n        const to = new Date(from);\n        to.setDate(to.getDate() + 2); // each 2 days long\n        return {\n          id: i + 1,\n          name: `Old Report ${i}`,\n          from,\n          to,\n          type: \"report\" as const,\n          status: \"degraded\" as const,\n        };\n      });\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"manual\",\n        cardType: \"manual\",\n      });\n\n      // Reports are entirely outside the window, so uptime should be 100%\n      expect(Number.parseFloat(uptime)).toBe(100);\n    });\n\n    it(\"should clamp partially overlapping reports to the window boundary\", () => {\n      const data = Array.from({ length: 45 }, (_, i) =>\n        createStatusData(i, 100, 0, 0),\n      );\n      // Report started 50 days ago (before window) and ended 40 days ago (inside window)\n      const from = new Date();\n      from.setDate(from.getDate() - 50);\n      const to = new Date();\n      to.setDate(to.getDate() - 40);\n      const events: Event[] = [\n        {\n          id: 1,\n          name: \"Overlapping Report\",\n          from,\n          to,\n          type: \"report\",\n          status: \"degraded\",\n        },\n      ];\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"manual\",\n        cardType: \"manual\",\n      });\n\n      // Only ~5 days should count (from window start to report end), not 10 days\n      const uptimeNum = Number.parseFloat(uptime);\n      expect(uptimeNum).toBeGreaterThan(85);\n      expect(uptimeNum).toBeLessThan(100);\n    });\n  });\n\n  describe(\"duration card type\", () => {\n    it(\"should calculate uptime based on incident durations\", () => {\n      const data = Array.from({ length: 45 }, (_, i) =>\n        createStatusData(i, 100, 0, 0),\n      );\n      const events = [createIncident(1, 0, 24)]; // 1 day downtime\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"absolute\",\n        cardType: \"duration\",\n      });\n\n      // Should be approximately 97.78% (44/45 days)\n      expect(Number.parseFloat(uptime)).toBeGreaterThan(97);\n      expect(Number.parseFloat(uptime)).toBeLessThan(98);\n    });\n    it(\"should ignore reports when calculating duration uptime\", () => {\n      const data = Array.from({ length: 45 }, (_, i) =>\n        createStatusData(i, 100, 0, 0),\n      );\n      const events = [createReport(2, 0, 24)]; // Should be ignored\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"absolute\",\n        cardType: \"duration\",\n      });\n\n      expect(uptime).toBe(\"100%\");\n    });\n  });\n\n  describe(\"request card type\", () => {\n    it(\"should calculate uptime based on ok vs total requests\", () => {\n      const data = [\n        createStatusData(0, 90, 5, 5), // 95 ok, 100 total\n        createStatusData(1, 100, 0, 0), // 100 ok, 100 total\n      ];\n      const events: Event[] = [];\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"absolute\",\n        cardType: \"requests\",\n      });\n\n      // (90+5+100) / (90+5+5+100) = 195/200 = 97.5%\n      expect(uptime).toBe(\"97.5%\");\n    });\n\n    it(\"should count degraded as ok\", () => {\n      const data = [createStatusData(0, 80, 20, 0)];\n      const events: Event[] = [];\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"absolute\",\n        cardType: \"requests\",\n      });\n\n      expect(uptime).toBe(\"100%\");\n    });\n\n    it(\"should return 100% for empty data\", () => {\n      const data: StatusData[] = [];\n      const events: Event[] = [];\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"absolute\",\n        cardType: \"requests\",\n      });\n\n      expect(uptime).toBe(\"100%\");\n    });\n\n    it(\"should return 100% when total is zero\", () => {\n      const data = [createStatusData(0, 0, 0, 0)];\n      const events: Event[] = [];\n\n      const uptime = getUptime({\n        data,\n        events,\n        barType: \"absolute\",\n        cardType: \"requests\",\n      });\n\n      expect(uptime).toBe(\"100%\");\n    });\n  });\n});\n\ndescribe(\"getEvents - pageComponent filtering\", () => {\n  // Helper to create a mock page component\n  function createMockPageComponent(\n    id: number,\n    monitorId?: number,\n  ): PageComponent {\n    return {\n      id,\n      workspaceId: 1,\n      pageId: 1,\n      type: monitorId ? (\"monitor\" as const) : (\"static\" as const),\n      monitorId: monitorId ?? null,\n      name: `Component ${id}`,\n      description: null,\n      order: 0,\n      groupId: null,\n      groupOrder: 0,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n    };\n  }\n\n  // Helper to create a mock maintenance\n  function createMockMaintenance(\n    id: number,\n    pageComponentIds: number[],\n  ): Maintenance & {\n    maintenancesToPageComponents: {\n      pageComponent: PageComponent | null;\n    }[];\n  } {\n    const now = new Date();\n    const from = new Date(now.getTime() - 1000 * 60 * 60); // 1 hour ago\n    const to = new Date(now.getTime() + 1000 * 60 * 60); // 1 hour from now\n\n    return {\n      id,\n      title: `Maintenance ${id}`,\n      message: \"Test maintenance\",\n      from,\n      to,\n      workspaceId: 1,\n      pageId: 1,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      maintenancesToPageComponents: pageComponentIds.map((pcId) => ({\n        pageComponent: createMockPageComponent(pcId, pcId * 10),\n      })),\n    };\n  }\n\n  // Helper to create a mock status report\n  function createMockStatusReport(\n    id: number,\n    pageComponentIds: number[],\n    status: \"investigating\" | \"resolved\" = \"investigating\",\n  ): StatusReport & {\n    statusReportsToPageComponents: {\n      pageComponent: PageComponent | null;\n    }[];\n    statusReportUpdates: StatusReportUpdate[];\n  } {\n    const now = new Date();\n    const updateDate = new Date(now.getTime() - 1000 * 60 * 60); // 1 hour ago\n\n    return {\n      id,\n      title: `Status Report ${id}`,\n      status,\n      workspaceId: 1,\n      pageId: 1,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      statusReportsToPageComponents: pageComponentIds.map((pcId) => ({\n        pageComponent: createMockPageComponent(pcId, pcId * 10),\n      })),\n      statusReportUpdates: [\n        {\n          id: id * 100,\n          statusReportId: id,\n          date: updateDate,\n          status: \"investigating\",\n          message: \"Investigating the issue\",\n          createdAt: new Date(),\n          updatedAt: new Date(),\n        },\n      ],\n    };\n  }\n\n  // Helper to create a mock incident\n  function createMockIncident(id: number, monitorId: number): Incident {\n    const now = new Date();\n    const startedAt = new Date(now.getTime() - 1000 * 60 * 60); // 1 hour ago\n\n    return {\n      id,\n      title: `Incident ${id}`,\n      summary: \"Test incident\",\n      status: \"investigating\",\n      monitorId,\n      workspaceId: 1,\n      startedAt,\n      acknowledgedAt: null,\n      acknowledgedBy: null,\n      resolvedAt: null,\n      resolvedBy: null,\n      incidentScreenshotUrl: null,\n      recoveryScreenshotUrl: null,\n      autoResolved: false,\n      createdAt: startedAt,\n      updatedAt: new Date(),\n    };\n  }\n\n  it(\"should filter maintenances by pageComponentId\", () => {\n    const maintenances = [\n      createMockMaintenance(1, [1, 2]),\n      createMockMaintenance(2, [3, 4]),\n      createMockMaintenance(3, [1, 5]),\n    ];\n\n    const events = getEvents({\n      maintenances,\n      incidents: [],\n      reports: [],\n      pageComponentId: 1,\n      pastDays: 365,\n    });\n\n    const maintenanceEvents = events.filter((e) => e.type === \"maintenance\");\n    expect(maintenanceEvents).toHaveLength(2);\n    expect(maintenanceEvents.map((e) => e.id).sort()).toEqual([1, 3]);\n  });\n\n  it(\"should filter status reports by pageComponentId\", () => {\n    const reports = [\n      createMockStatusReport(1, [1, 2]),\n      createMockStatusReport(2, [3, 4]),\n      createMockStatusReport(3, [1, 5]),\n    ];\n\n    const events = getEvents({\n      maintenances: [],\n      incidents: [],\n      reports,\n      pageComponentId: 1,\n      pastDays: 365,\n    });\n\n    const reportEvents = events.filter((e) => e.type === \"report\");\n    expect(reportEvents).toHaveLength(2);\n    expect(reportEvents.map((e) => e.id).sort()).toEqual([1, 3]);\n  });\n\n  it(\"should exclude incidents for static components\", () => {\n    const incidents = [createMockIncident(1, 10), createMockIncident(2, 20)];\n\n    const events = getEvents({\n      maintenances: [],\n      incidents,\n      reports: [],\n      componentType: \"static\",\n      pastDays: 365,\n    });\n\n    const incidentEvents = events.filter((e) => e.type === \"incident\");\n    expect(incidentEvents).toHaveLength(0);\n  });\n\n  it(\"should include incidents for monitor components\", () => {\n    const incidents = [createMockIncident(1, 10), createMockIncident(2, 20)];\n\n    const events = getEvents({\n      maintenances: [],\n      incidents,\n      reports: [],\n      monitorId: 10,\n      componentType: \"monitor\",\n      pastDays: 365,\n    });\n\n    const incidentEvents = events.filter((e) => e.type === \"incident\");\n    expect(incidentEvents).toHaveLength(1);\n    expect(incidentEvents[0].id).toBe(1);\n  });\n\n  it(\"should maintain backward compatibility with monitorId filtering\", () => {\n    const maintenances = [\n      createMockMaintenance(1, [1]),\n      createMockMaintenance(2, [2]),\n    ];\n\n    const events = getEvents({\n      maintenances,\n      incidents: [],\n      reports: [],\n      monitorId: 10,\n      pastDays: 365,\n    });\n\n    const maintenanceEvents = events.filter((e) => e.type === \"maintenance\");\n    expect(maintenanceEvents).toHaveLength(1);\n    expect(maintenanceEvents[0].id).toBe(1);\n  });\n\n  it(\"should prioritize pageComponentId over monitorId\", () => {\n    const maintenances = [\n      createMockMaintenance(1, [1]),\n      createMockMaintenance(2, [2]),\n    ];\n\n    const events = getEvents({\n      maintenances,\n      incidents: [],\n      reports: [],\n      pageComponentId: 1,\n      monitorId: 20,\n      pastDays: 365,\n    });\n\n    const maintenanceEvents = events.filter((e) => e.type === \"maintenance\");\n    expect(maintenanceEvents).toHaveLength(1);\n    expect(maintenanceEvents[0].id).toBe(1);\n  });\n\n  it(\"should return success status for resolved reports\", () => {\n    const reports = [createMockStatusReport(1, [1], \"resolved\")];\n\n    const events = getEvents({\n      maintenances: [],\n      incidents: [],\n      reports,\n      pageComponentId: 1,\n      pastDays: 365,\n    });\n\n    const reportEvents = events.filter((e) => e.type === \"report\");\n    expect(reportEvents).toHaveLength(1);\n    expect(reportEvents[0].status).toBe(\"success\");\n  });\n\n  it(\"should return degraded status for unresolved reports\", () => {\n    const reports = [createMockStatusReport(1, [1], \"investigating\")];\n\n    const events = getEvents({\n      maintenances: [],\n      incidents: [],\n      reports,\n      pageComponentId: 1,\n      pastDays: 365,\n    });\n\n    const reportEvents = events.filter((e) => e.type === \"report\");\n    expect(reportEvents).toHaveLength(1);\n    expect(reportEvents[0].status).toBe(\"degraded\");\n  });\n});\n"
  },
  {
    "path": "packages/api/src/router/statusPage.utils.ts",
    "content": "import type {\n  Incident,\n  Maintenance,\n  PageComponent,\n  PageComponentType,\n  PageComponentWithMonitorRelation,\n  StatusReport,\n  StatusReportUpdate,\n} from \"@openstatus/db/src/schema\";\n\n/**\n * Type for a monitor component with a non-null monitor relation\n */\nexport type MonitorComponentWithNonNullMonitor =\n  PageComponentWithMonitorRelation & {\n    type: \"monitor\";\n    monitorId: number;\n    monitor: NonNullable<PageComponentWithMonitorRelation[\"monitor\"]>;\n  };\n\n/**\n * Type guard to check if a pageComponent is a monitor type with a monitor relation\n * Works with any object that has the shape of a pageComponent with a valid monitor relation\n */\nexport function isMonitorComponent(\n  component: PageComponentWithMonitorRelation,\n): component is MonitorComponentWithNonNullMonitor {\n  return (\n    component.type === \"monitor\" &&\n    component.monitor !== null &&\n    component.monitor !== undefined &&\n    component.monitor.active === true &&\n    component.monitor.deletedAt === null\n  );\n}\n\nexport type StatusData = {\n  day: string;\n  count: number;\n  ok: number;\n  degraded: number;\n  error: number;\n  monitorId: string;\n};\n\nexport function fillStatusDataFor45Days(\n  data: Array<StatusData>,\n  monitorId: string,\n  lookbackPeriod = 45,\n): Array<StatusData> {\n  const result = [];\n  const dataByDay = new Map();\n\n  // Index existing data by day\n  data.forEach((item) => {\n    const dayKey = new Date(item.day).toISOString().split(\"T\")[0]; // YYYY-MM-DD format\n    dataByDay.set(dayKey, item);\n  });\n\n  // Generate all 45 days from today backwards\n  const now = new Date();\n  for (let i = 0; i < lookbackPeriod; i++) {\n    const date = new Date(now);\n    date.setUTCDate(date.getUTCDate() - i);\n    date.setUTCHours(0, 0, 0, 0); // Set to start of day in UTC\n\n    const dayKey = date.toISOString().split(\"T\")[0]; // YYYY-MM-DD format\n    const isoString = date.toISOString();\n\n    if (dataByDay.has(dayKey)) {\n      // Use existing data but ensure the day is properly formatted\n      const existingData = dataByDay.get(dayKey);\n      result.push({\n        ...existingData,\n        day: isoString,\n      });\n    } else {\n      // Fill missing day with default values\n      result.push({\n        day: isoString,\n        count: 0,\n        ok: 0,\n        degraded: 0,\n        error: 0,\n        monitorId,\n      });\n    }\n  }\n\n  // Sort by day (oldest first)\n  return result.sort(\n    (a, b) => new Date(a.day).getTime() - new Date(b.day).getTime(),\n  );\n}\n\nexport function fillStatusDataFor45DaysNoop({\n  errorDays,\n  degradedDays,\n  lookbackPeriod = 45,\n}: {\n  errorDays: number[];\n  degradedDays: number[];\n  lookbackPeriod?: number;\n}): Array<StatusData> {\n  const issueDays = [...errorDays, ...degradedDays];\n  const data: StatusData[] = Array.from({ length: 45 }, (_, i) => {\n    return {\n      day: new Date(new Date().setDate(new Date().getDate() - i)).toISOString(),\n      count: 1,\n      ok: issueDays.includes(i) ? 0 : 1,\n      degraded: degradedDays.includes(i) ? 1 : 0,\n      error: errorDays.includes(i) ? 1 : 0,\n      monitorId: \"1\",\n    };\n  });\n  return fillStatusDataFor45Days(data, \"1\", lookbackPeriod);\n}\n\ntype Event = {\n  id: number;\n  name: string;\n  from: Date;\n  to: Date | null;\n  type: \"maintenance\" | \"incident\" | \"report\";\n  status: \"success\" | \"degraded\" | \"error\" | \"info\";\n};\n\nexport function getEvents({\n  maintenances,\n  incidents,\n  reports,\n  pageComponentId,\n  monitorId,\n  componentType,\n  pastDays = 45,\n}: {\n  maintenances: (Maintenance & {\n    maintenancesToPageComponents: {\n      pageComponent: PageComponent | null;\n    }[];\n  })[];\n  incidents: Incident[];\n  reports: (StatusReport & {\n    statusReportsToPageComponents: {\n      pageComponent: PageComponent | null;\n    }[];\n    statusReportUpdates: StatusReportUpdate[];\n  })[];\n  pageComponentId?: number;\n  monitorId?: number;\n  componentType?: PageComponentType;\n  pastDays?: number;\n}): Event[] {\n  const events: Event[] = [];\n  const pastThreshod = new Date();\n  pastThreshod.setDate(pastThreshod.getDate() - pastDays);\n\n  // Filter maintenances - prioritize pageComponentId, fallback to monitorId for backward compatibility\n  maintenances\n    .filter((maintenance) => {\n      if (pageComponentId) {\n        return maintenance.maintenancesToPageComponents.some(\n          (m) => m.pageComponent?.id === pageComponentId,\n        );\n      }\n      if (monitorId) {\n        return maintenance.maintenancesToPageComponents.some(\n          (m) => m.pageComponent?.monitorId === monitorId,\n        );\n      }\n      return true;\n    })\n    .forEach((maintenance) => {\n      if (maintenance.from < pastThreshod) return;\n      events.push({\n        id: maintenance.id,\n        name: maintenance.title,\n        from: maintenance.from,\n        to: maintenance.to,\n        type: \"maintenance\",\n        status: \"info\" as const,\n      });\n    });\n\n  // Filter incidents - only for monitor-type components\n  // Static components don't have incidents\n  if (componentType !== \"static\") {\n    incidents\n      .filter((incident) =>\n        monitorId ? incident.monitorId === monitorId : true,\n      )\n      .forEach((incident) => {\n        if (!incident.createdAt || incident.createdAt < pastThreshod) return;\n        events.push({\n          id: incident.id,\n          name: \"Downtime\",\n          from: incident.createdAt,\n          to: incident.resolvedAt,\n          type: \"incident\",\n          status: \"error\" as const,\n        });\n      });\n  }\n\n  // Filter reports - prioritize pageComponentId, fallback to monitorId for backward compatibility\n  reports\n    .filter((report) => {\n      if (pageComponentId) {\n        return report.statusReportsToPageComponents.some(\n          (r) => r.pageComponent?.id === pageComponentId,\n        );\n      }\n      if (monitorId) {\n        return report.statusReportsToPageComponents.some(\n          (r) => r.pageComponent?.monitorId === monitorId,\n        );\n      }\n      return true;\n    })\n    .map((report) => {\n      const updates = report.statusReportUpdates.sort(\n        (a, b) => a.date.getTime() - b.date.getTime(),\n      );\n      if (updates.length === 0) return;\n\n      const firstUpdate = updates[0];\n      const lastUpdate = updates[updates.length - 1];\n\n      // NOTE: we don't check threshold here because we display all unresolved reports\n      if (!firstUpdate?.date) return;\n\n      // HACKY: LEGACY: we shouldn't have report.status anymore and instead use the update status for that.\n      // Ideally, we could replace the status with \"downtime\", \"degraded\", \"operational\" to indicate the gravity of the issue\n      if (report.status === \"resolved\") {\n        events.push({\n          id: report.id,\n          name: report.title,\n          from: firstUpdate?.date,\n          to: lastUpdate?.date,\n          type: \"report\",\n          status: \"success\" as const,\n        });\n        return;\n      }\n\n      events.push({\n        id: report.id,\n        name: report.title,\n        from: firstUpdate?.date,\n        to:\n          lastUpdate?.status === \"resolved\" ||\n          lastUpdate?.status === \"monitoring\"\n            ? lastUpdate?.date\n            : null,\n        type: \"report\",\n        status: \"degraded\" as const,\n      });\n    });\n\n  return events;\n}\n\n// Keep the old function name for backward compatibility\nexport const getEventsByMonitorId = getEvents;\n\nexport function getWorstVariant(\n  statuses: (keyof typeof STATUS_PRIORITY)[],\n): keyof typeof STATUS_PRIORITY {\n  if (statuses.length === 0) return \"success\";\n\n  return statuses.reduce(\n    (worst, current) => {\n      return STATUS_PRIORITY[current] > STATUS_PRIORITY[worst]\n        ? current\n        : worst;\n    },\n    \"success\" as keyof typeof STATUS_PRIORITY,\n  );\n}\n\ntype UptimeData = {\n  day: string;\n  events: Event[];\n  bar: {\n    status: \"success\" | \"degraded\" | \"error\" | \"info\" | \"empty\";\n    height: number; // percentage\n  }[];\n  card: {\n    status: \"success\" | \"degraded\" | \"error\" | \"info\" | \"empty\";\n    value: string;\n  }[];\n};\n\n// Priority mapping for status types (higher number = higher priority)\nconst STATUS_PRIORITY = {\n  error: 3,\n  degraded: 2,\n  info: 1,\n  success: 0,\n  empty: -1,\n} as const;\n\n// Constants for time calculations\nconst MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;\nconst MILLISECONDS_PER_MINUTE = 1000 * 60;\n\n// Helper to get highest priority status from data\nfunction getHighestPriorityStatus(\n  item: StatusData,\n): keyof typeof STATUS_PRIORITY {\n  if (item.error > 0) return \"error\";\n  if (item.degraded > 0) return \"degraded\";\n  if (item.ok > 0) return \"success\";\n\n  return \"empty\";\n}\n\n// Helper to format numbers\nfunction formatNumber(num: number): string {\n  if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;\n  if (num >= 1000) return `${(num / 1000).toFixed(1)}k`;\n  return num.toString();\n}\n\n// Helper to check if date is today\nfunction isToday(date: Date): boolean {\n  const today = new Date();\n  return (\n    date.getDate() === today.getDate() &&\n    date.getMonth() === today.getMonth() &&\n    date.getFullYear() === today.getFullYear()\n  );\n}\n\n// Helper to format duration from minutes\nfunction formatDuration(minutes: number): string {\n  if (minutes < 60) return `${minutes}m`;\n  const hours = Math.floor(minutes / 60);\n  const remainingMinutes = minutes % 60;\n  if (remainingMinutes === 0) return `${hours}h`;\n  return `${hours}h ${remainingMinutes}m`;\n}\n\n// Helper to check if date is within event range\nfunction isDateWithinEvent(date: Date, event: Event): boolean {\n  const startOfDay = new Date(date);\n  startOfDay.setUTCHours(0, 0, 0, 0);\n\n  const endOfDay = new Date(date);\n  endOfDay.setUTCHours(23, 59, 59, 999);\n\n  const eventStart = new Date(event.from);\n  const eventEnd = event.to ? new Date(event.to) : new Date();\n\n  return (\n    eventStart.getTime() <= endOfDay.getTime() &&\n    eventEnd.getTime() >= startOfDay.getTime()\n  );\n}\n\n// Helper to calculate total minutes in a day (handles today vs past days)\nfunction getTotalMinutesInDay(date: Date): number {\n  const now = new Date();\n  const startOfDay = new Date(date);\n  startOfDay.setUTCHours(0, 0, 0, 0);\n\n  if (isToday(date)) {\n    const minutesElapsed = Math.floor(\n      (now.getTime() - startOfDay.getTime()) / MILLISECONDS_PER_MINUTE,\n    );\n    return minutesElapsed;\n  }\n  return 24 * 60;\n}\n\n// Helper to calculate duration in minutes for a specific event type\nfunction calculateEventDurationMinutes(events: Event[], date: Date): number {\n  const totalDuration = getTotalEventsDurationMs(events, date);\n  return Math.round(totalDuration / MILLISECONDS_PER_MINUTE);\n}\n\n// Helper to calculate maintenance duration in minutes for a specific day\nfunction getMaintenanceDurationMinutes(\n  maintenances: Event[],\n  date: Date,\n): number {\n  return calculateEventDurationMinutes(maintenances, date);\n}\n\n// Helper to get adjusted total minutes accounting for maintenance\nfunction getAdjustedTotalMinutesInDay(\n  date: Date,\n  maintenances: Event[],\n): number {\n  const totalMinutes = getTotalMinutesInDay(date);\n  const maintenanceMinutes = getMaintenanceDurationMinutes(maintenances, date);\n  return Math.max(0, totalMinutes - maintenanceMinutes);\n}\n\nfunction getTotalEventsDurationMs(events: Event[], date: Date): number {\n  if (events.length === 0) return 0;\n\n  const startOfDay = new Date(date);\n  startOfDay.setUTCHours(0, 0, 0, 0);\n\n  const endOfDay = new Date(date);\n  endOfDay.setUTCHours(23, 59, 59, 999);\n\n  const total = events.reduce((acc, curr) => {\n    if (!curr.from) return acc;\n\n    const eventStart = new Date(curr.from);\n    const eventEnd = curr.to ? new Date(curr.to) : new Date();\n\n    // Only count events that overlap with this date\n    if (\n      eventEnd.getTime() < startOfDay.getTime() ||\n      eventStart.getTime() > endOfDay.getTime()\n    ) {\n      return acc;\n    }\n\n    // Calculate the overlapping duration within the date boundaries\n    const overlapStart = Math.max(eventStart.getTime(), startOfDay.getTime());\n    const overlapEnd = Math.min(eventEnd.getTime(), endOfDay.getTime());\n\n    const duration = overlapEnd - overlapStart;\n    return acc + Math.max(0, duration);\n  }, 0);\n\n  // Cap at 24 hours per day\n  return Math.min(total, MILLISECONDS_PER_DAY);\n}\n\nexport function setDataByType({\n  events,\n  data,\n  cardType,\n  barType,\n}: {\n  events: Event[];\n  data: StatusData[];\n  cardType: \"requests\" | \"duration\" | \"dominant\" | \"manual\";\n  barType: \"absolute\" | \"dominant\" | \"manual\";\n}): UptimeData[] {\n  // Helper functions moved inside to share inputs and avoid parameter passing\n  function createEventSegments(\n    incidents: Event[],\n    reports: Event[],\n    maintenances: Event[],\n    date: Date,\n  ): Array<{ status: \"info\" | \"degraded\" | \"error\"; count: number }> {\n    const eventTypes = [\n      { status: \"info\" as const, events: maintenances },\n      { status: \"degraded\" as const, events: reports },\n      { status: \"error\" as const, events: incidents },\n    ];\n\n    return eventTypes\n      .filter(({ events }) => events.length > 0)\n      .map(({ status, events }) => ({\n        status,\n        count: getTotalEventsDurationMs(events, date),\n      }));\n  }\n\n  function createErrorOnlyBarData(\n    errorSegmentCount: number,\n  ): UptimeData[\"bar\"] {\n    return [\n      {\n        status: \"success\" as const,\n        height:\n          ((MILLISECONDS_PER_DAY - errorSegmentCount) / MILLISECONDS_PER_DAY) *\n          100,\n      },\n      {\n        status: \"error\" as const,\n        height: (errorSegmentCount / MILLISECONDS_PER_DAY) * 100,\n      },\n    ];\n  }\n\n  function createProportionalBarData(\n    segments: Array<{ status: \"info\" | \"degraded\" | \"error\"; count: number }>,\n  ): UptimeData[\"bar\"] {\n    const totalDuration = segments.reduce(\n      (sum, segment) => sum + segment.count,\n      0,\n    );\n\n    return segments.map((segment) => ({\n      status: segment.status,\n      // NOTE: if totalDuration is 0 (single event without duration), we want to show 100% for the segment\n      height: totalDuration > 0 ? (segment.count / totalDuration) * 100 : 100,\n    }));\n  }\n\n  function createStatusSegments(\n    dayData: StatusData,\n  ): Array<{ status: \"success\" | \"degraded\" | \"error\"; count: number }> {\n    return [\n      { status: \"success\" as const, count: dayData.ok },\n      { status: \"degraded\" as const, count: dayData.degraded },\n      { status: \"error\" as const, count: dayData.error },\n    ];\n  }\n\n  function segmentsToBarData(\n    segments: Array<{\n      status: \"success\" | \"degraded\" | \"error\";\n      count: number;\n    }>,\n    total: number,\n  ): UptimeData[\"bar\"] {\n    return segments\n      .filter((segment) => segment.count > 0)\n      .map((segment) => ({\n        status: segment.status,\n        height: (segment.count / total) * 100,\n      }));\n  }\n\n  function createOperationalBarData(): UptimeData[\"bar\"] {\n    return [\n      {\n        status: \"success\",\n        height: 100,\n      },\n    ];\n  }\n\n  function createEmptyBarData(): UptimeData[\"bar\"] {\n    return [\n      {\n        status: \"empty\",\n        height: 100,\n      },\n    ];\n  }\n\n  function createEmptyCardData(\n    eventStatus?: \"error\" | \"degraded\" | \"info\" | \"success\" | \"empty\",\n  ): UptimeData[\"card\"] {\n    return [{ status: eventStatus ?? \"empty\", value: \"\" }];\n  }\n\n  function createRequestEntries(dayData: StatusData): Array<{\n    status: \"success\" | \"degraded\" | \"error\" | \"info\";\n    count: number;\n  }> {\n    return [\n      { status: \"success\" as const, count: dayData.ok },\n      { status: \"degraded\" as const, count: dayData.degraded },\n      { status: \"error\" as const, count: dayData.error },\n      { status: \"info\" as const, count: 0 },\n    ];\n  }\n\n  function createDurationEntries(dayData: StatusData): Array<{\n    status: \"success\" | \"degraded\" | \"error\" | \"info\";\n    count: number;\n  }> {\n    return [\n      { status: \"error\" as const, count: dayData.error },\n      { status: \"degraded\" as const, count: dayData.degraded },\n      { status: \"success\" as const, count: dayData.ok },\n      { status: \"info\" as const, count: 0 },\n    ];\n  }\n\n  function entriesToRequestCardData(\n    entries: Array<{\n      status: \"success\" | \"degraded\" | \"error\" | \"info\";\n      count: number;\n    }>,\n  ): UptimeData[\"card\"] {\n    return entries\n      .filter((entry) => entry.count > 0)\n      .map((entry) => ({\n        status: entry.status,\n        value: `${formatNumber(entry.count)} reqs`,\n      }));\n  }\n\n  // Helper to calculate duration in minutes for a specific event type\n  function calculateEventDurationMinutes(events: Event[], date: Date): number {\n    const totalDuration = getTotalEventsDurationMs(events, date);\n    return Math.round(totalDuration / MILLISECONDS_PER_MINUTE);\n  }\n\n  // Helper to create duration card data for a specific status\n  function createDurationCardEntry(\n    status: \"error\" | \"degraded\" | \"info\" | \"success\",\n    events: Event[],\n    date: Date,\n    durationMap: Map<string, number>,\n    maintenances: Event[] = [],\n  ): {\n    status: \"error\" | \"degraded\" | \"info\" | \"success\";\n    value: string;\n  } | null {\n    if (status === \"success\") {\n      // Calculate success duration as remaining time\n      let totalEventMinutes = 0;\n      // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>\n      durationMap.forEach((minutes) => (totalEventMinutes += minutes));\n\n      // Use adjusted total minutes accounting for maintenance\n      const totalMinutesInDay = getAdjustedTotalMinutesInDay(\n        date,\n        maintenances,\n      );\n      const successMinutes = Math.max(totalMinutesInDay - totalEventMinutes, 0);\n\n      if (successMinutes === 0) return null;\n      return {\n        status,\n        value: formatDuration(successMinutes),\n      };\n    }\n\n    // For error, degraded, info - calculate from events\n    const minutes = calculateEventDurationMinutes(events, date);\n    durationMap.set(status, minutes);\n\n    if (minutes === 0) return null;\n    return {\n      status,\n      value: formatDuration(minutes),\n    };\n  }\n\n  return data.map((dayData) => {\n    const date = new Date(dayData.day);\n\n    // Find events for this day\n    const dayEvents = events.filter((event) => isDateWithinEvent(date, event));\n\n    // Determine status override based on events\n    const incidents = dayEvents.filter((e) => e.type === \"incident\");\n    const reports = dayEvents.filter((e) => e.type === \"report\");\n    const maintenances = dayEvents.filter((e) => e.type === \"maintenance\");\n\n    const hasIncidents = incidents.length > 0;\n    const hasReports = reports.length > 0;\n    const hasMaintenances = maintenances.length > 0;\n\n    const eventStatus = hasIncidents\n      ? \"error\"\n      : hasReports\n        ? \"degraded\"\n        : hasMaintenances\n          ? \"info\"\n          : undefined;\n\n    // Calculate bar data based on barType\n    // TODO: transform into a new Map<type, number>();\n    let barData: UptimeData[\"bar\"];\n\n    const total = dayData.ok + dayData.degraded + dayData.error;\n    const dataStatus = getHighestPriorityStatus(dayData);\n\n    switch (barType) {\n      case \"absolute\":\n        if (eventStatus) {\n          // Create segments based on event durations for the day\n          const eventSegments = createEventSegments(\n            incidents,\n            reports,\n            maintenances,\n            date,\n          );\n\n          // Special case: if only errors exist, show uptime vs downtime\n          if (\n            eventSegments.length === 1 &&\n            eventSegments[0].status === \"error\"\n          ) {\n            barData = createErrorOnlyBarData(eventSegments[0].count);\n          } else {\n            // Multiple segments: show proportional distribution\n            barData = createProportionalBarData(eventSegments);\n          }\n        } else if (total === 0) {\n          // Empty day - no data available\n          barData = createEmptyBarData();\n        } else {\n          if (cardType === \"duration\") {\n            // If no eventStatus and cardType is duration, show operational bar\n            barData = createOperationalBarData();\n          } else {\n            // Multiple segments for absolute view - show proportional distribution of status data\n            const statusSegments = createStatusSegments(dayData);\n            barData = segmentsToBarData(statusSegments, total);\n          }\n        }\n        break;\n      case \"dominant\":\n        barData = [\n          {\n            status: eventStatus ?? dataStatus,\n            height: 100,\n          },\n        ];\n        break;\n      case \"manual\":\n        const manualEventStatus = hasReports\n          ? \"degraded\"\n          : hasMaintenances\n            ? \"info\"\n            : undefined;\n        barData = [\n          {\n            status: manualEventStatus || \"success\",\n            height: 100,\n          },\n        ];\n        break;\n      default:\n        // Default to dominant behavior\n        barData = [\n          {\n            status: eventStatus ?? dataStatus,\n            height: 100,\n          },\n        ];\n        break;\n    }\n\n    // Calculate card data based on cardType\n    // TODO: transform into a new Map<type, number>();\n    let cardData: UptimeData[\"card\"] = [];\n\n    switch (cardType) {\n      case \"requests\":\n        if (total === 0) {\n          cardData = createEmptyCardData(eventStatus);\n        } else {\n          const requestEntries = createRequestEntries(dayData);\n          cardData = entriesToRequestCardData(requestEntries);\n        }\n        break;\n\n      case \"duration\":\n        if (total === 0) {\n          cardData = createEmptyCardData(eventStatus);\n        } else {\n          const entries = createDurationEntries(dayData);\n          const durationMap = new Map<string, number>();\n\n          cardData = entries\n            .map((entry) => {\n              // Map each entry status to its corresponding events\n              const eventMap = {\n                error: incidents,\n                degraded: reports,\n                info: maintenances,\n                success: [], // Success is calculated differently\n              };\n\n              const events = eventMap[entry.status as keyof typeof eventMap];\n              return createDurationCardEntry(\n                entry.status,\n                events,\n                date,\n                durationMap,\n                maintenances,\n              );\n            })\n            .filter((item): item is NonNullable<typeof item> => item !== null);\n        }\n        break;\n\n      case \"dominant\":\n        cardData = [\n          {\n            status: eventStatus ?? dataStatus,\n            value: \"\",\n          },\n        ];\n        break;\n\n      case \"manual\":\n        const manualEventStatus = hasReports\n          ? \"degraded\"\n          : hasMaintenances\n            ? \"info\"\n            : undefined;\n        cardData = [\n          {\n            status: manualEventStatus || \"success\",\n            value: \"\",\n          },\n        ];\n        break;\n      default:\n        // Default to requests behavior\n        if (total === 0) {\n          cardData = createEmptyCardData(eventStatus);\n        } else {\n          const defaultEntries = createRequestEntries(dayData);\n          cardData = entriesToRequestCardData(defaultEntries);\n        }\n        break;\n    }\n\n    // Bundle incidents that occur on the same day if there are more than 4\n    const bundledIncidents =\n      incidents.length > 4\n        ? [\n            {\n              id: -1, // Use -1 to indicate bundled incidents\n              name: `Downtime (${incidents.length} incidents)`,\n              from: new Date(\n                Math.min(...incidents.map((i) => i.from.getTime())),\n              ),\n              to: new Date(\n                Math.max(\n                  ...incidents.map((i) => (i.to || new Date()).getTime()),\n                ),\n              ),\n              type: \"incident\" as const,\n              status: \"error\" as const,\n            },\n          ]\n        : incidents;\n\n    return {\n      day: dayData.day,\n      events: [\n        ...reports,\n        ...maintenances,\n        ...(barType === \"absolute\" ? bundledIncidents : []),\n      ],\n      bar: barData,\n      card: cardData,\n    };\n  });\n}\n\nexport function getUptime({\n  data,\n  events,\n  barType,\n  cardType,\n}: {\n  data: StatusData[];\n  events: Event[];\n  barType: \"absolute\" | \"dominant\" | \"manual\";\n  cardType: \"requests\" | \"duration\" | \"dominant\" | \"manual\";\n}): string {\n  // Clamp event durations to the data lookback window to avoid\n  // events outside the window producing negative uptime values.\n  const timestamps = data.map((d) => new Date(d.day).getTime());\n  const windowStart = timestamps.length > 0 ? Math.min(...timestamps) : 0;\n  const windowEndDate = new Date(\n    timestamps.length > 0 ? Math.max(...timestamps) : Date.now(),\n  );\n  windowEndDate.setUTCHours(23, 59, 59, 999);\n  const windowEnd = windowEndDate.getTime();\n\n  function clampedDuration(item: Event): number {\n    if (!item.from) return 0;\n    const from = Math.max(item.from.getTime(), windowStart);\n    const to = Math.min((item.to || new Date()).getTime(), windowEnd);\n    return Math.max(0, to - from);\n  }\n\n  if (barType === \"manual\") {\n    const duration = events\n      // NOTE: we want only user events\n      .filter((e) => e.type === \"report\")\n      .reduce((acc, item) => acc + clampedDuration(item), 0);\n\n    const total = data.length * MILLISECONDS_PER_DAY;\n\n    return `${Math.floor(((total - duration) / total) * 10000) / 100}%`;\n  }\n\n  if (cardType === \"duration\") {\n    const duration = events\n      .filter((e) => e.type === \"incident\")\n      .reduce((acc, item) => acc + clampedDuration(item), 0);\n\n    const total = data.length * MILLISECONDS_PER_DAY;\n    return `${Math.floor(((total - duration) / total) * 10000) / 100}%`;\n  }\n\n  const { ok, total } = data.reduce(\n    (acc, item) => ({\n      ok: acc.ok + item.ok + item.degraded,\n      total: acc.total + item.ok + item.degraded + item.error,\n    }),\n    {\n      ok: 0,\n      total: 0,\n    },\n  );\n\n  if (total === 0) return \"100%\";\n  return `${Math.floor((ok / total) * 10000) / 100}%`;\n}\n"
  },
  {
    "path": "packages/api/src/router/statusReport.test.ts",
    "content": "import { afterAll, beforeAll, expect, test } from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  page,\n  pageComponent,\n  statusReport,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { edgeRouter } from \"../edge\";\nimport { createInnerTRPCContext } from \"../trpc\";\n\nlet otherWorkspacePageId: number;\nlet otherWorkspaceReportId: number;\nlet otherWorkspaceComponentId: number;\n\nbeforeAll(async () => {\n  const p = await db\n    .insert(page)\n    .values({\n      workspaceId: 2,\n      title: \"IDOR Test Page\",\n      description: \"Page for IDOR testing\",\n      slug: \"idor-test-page\",\n      customDomain: \"\",\n    })\n    .returning()\n    .get();\n  otherWorkspacePageId = p.id;\n\n  const component = await db\n    .insert(pageComponent)\n    .values({\n      workspaceId: 2,\n      pageId: otherWorkspacePageId,\n      name: \"Other workspace component\",\n      type: \"static\",\n    })\n    .returning()\n    .get();\n  otherWorkspaceComponentId = component.id;\n\n  const report = await db\n    .insert(statusReport)\n    .values({\n      workspaceId: 2,\n      title: \"Other workspace report\",\n      status: \"investigating\",\n      pageId: otherWorkspacePageId,\n    })\n    .returning()\n    .get();\n  otherWorkspaceReportId = report.id;\n\n  await db\n    .insert(statusReportsToPageComponents)\n    .values({ statusReportId: otherWorkspaceReportId, pageComponentId: 1 })\n    .run();\n});\n\nafterAll(async () => {\n  await db\n    .delete(statusReportsToPageComponents)\n    .where(\n      eq(statusReportsToPageComponents.statusReportId, otherWorkspaceReportId),\n    );\n  await db\n    .delete(statusReport)\n    .where(eq(statusReport.id, otherWorkspaceReportId));\n  await db\n    .delete(pageComponent)\n    .where(eq(pageComponent.id, otherWorkspaceComponentId));\n  await db.delete(page).where(eq(page.id, otherWorkspacePageId));\n});\n\ntest(\"statusReport.create rejects pageId from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.statusReport.create({\n      title: \"Test Report\",\n      status: \"investigating\",\n      pageId: otherWorkspacePageId,\n      pageComponents: [],\n      date: new Date(),\n      message: \"Testing cross-workspace access\",\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n});\n\ntest(\"statusReport.create succeeds for own workspace page\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  const result = await caller.statusReport.create({\n    title: \"Valid Test Report\",\n    status: \"investigating\",\n    pageId: 1,\n    pageComponents: [],\n    date: new Date(),\n    message: \"Testing own workspace access\",\n  });\n\n  expect(result).toBeDefined();\n\n  // Clean up\n  await db.delete(statusReport).where(eq(statusReport.id, result.id));\n});\n\ntest(\"statusReport.updateStatus rejects report from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.statusReport.updateStatus({\n      id: otherWorkspaceReportId,\n      pageComponents: [],\n      title: \"Unauthorized Modification\",\n      status: \"investigating\",\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n\n  // Verify page component associations were NOT deleted\n  const associations = await db.query.statusReportsToPageComponents.findMany({\n    where: eq(\n      statusReportsToPageComponents.statusReportId,\n      otherWorkspaceReportId,\n    ),\n  });\n  expect(associations.length).toBe(1);\n});\n\ntest(\"statusReport.updateStatus succeeds for own workspace report\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  // Seed status report 1 belongs to workspace 1\n  await caller.statusReport.updateStatus({\n    id: 1,\n    pageComponents: [1],\n    title: \"Updated Title\",\n    status: \"resolved\",\n  });\n\n  const updated = await db.query.statusReport.findFirst({\n    where: eq(statusReport.id, 1),\n  });\n  expect(updated?.title).toBe(\"Updated Title\");\n});\n\ntest(\"statusReport.create rejects pageComponents from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.statusReport.create({\n      title: \"Cross-workspace injection\",\n      status: \"investigating\",\n      pageId: 1, // valid page for workspace 1\n      pageComponents: [otherWorkspaceComponentId], // component from workspace 2\n      date: new Date(),\n      message: \"Should be rejected\",\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"FORBIDDEN\");\n  }\n});\n\ntest(\"statusReport.updateStatus rejects pageComponents from another workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: 1 },\n  });\n  const caller = edgeRouter.createCaller(ctx);\n\n  try {\n    await caller.statusReport.updateStatus({\n      id: 1, // valid report for workspace 1\n      pageComponents: [otherWorkspaceComponentId], // component from workspace 2\n      title: \"Cross-workspace injection\",\n      status: \"investigating\",\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"FORBIDDEN\");\n  }\n\n  // Verify the report was NOT modified\n  const report = await db.query.statusReport.findFirst({\n    where: eq(statusReport.id, 1),\n  });\n  expect(report?.title).toBe(\"Updated Title\"); // still the value from the previous test\n});\n"
  },
  {
    "path": "packages/api/src/router/statusReport.ts",
    "content": "import { z } from \"zod\";\n\nimport { type SQL, and, asc, desc, eq, gte } from \"@openstatus/db\";\nimport {\n  insertStatusReportUpdateSchema,\n  page,\n  pageComponent,\n  selectPageComponentSchema,\n  selectPageSchema,\n  selectStatusReportSchema,\n  selectStatusReportUpdateSchema,\n  statusReport,\n  statusReportStatus,\n  statusReportUpdate,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { TRPCError } from \"@trpc/server\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\nimport { getPeriodDate, periods } from \"./utils\";\n\nexport const statusReportRouter = createTRPCRouter({\n  createStatusReportUpdate: protectedProcedure\n    .meta({ track: Events.CreateReportUpdate })\n    .input(\n      insertStatusReportUpdateSchema.extend({\n        notifySubscribers: z.boolean().nullish(),\n      }),\n    )\n    .mutation(async (opts) => {\n      // update parent status report with latest status\n      const _statusReport = await opts.ctx.db\n        .update(statusReport)\n        .set({ status: opts.input.status, updatedAt: new Date() })\n        .where(\n          and(\n            eq(statusReport.id, opts.input.statusReportId),\n            eq(statusReport.workspaceId, opts.ctx.workspace.id),\n          ),\n        )\n        .returning()\n        .get();\n\n      if (!_statusReport) return;\n\n      const { id, ...statusReportUpdateInput } = opts.input;\n\n      const updatedValue = await opts.ctx.db\n        .insert(statusReportUpdate)\n        .values(statusReportUpdateInput)\n        .returning()\n        .get();\n\n      return {\n        ...selectStatusReportUpdateSchema.parse(updatedValue),\n        notifySubscribers: opts.input.notifySubscribers,\n      };\n    }),\n\n  updateStatusReportUpdate: protectedProcedure\n    .meta({ track: Events.UpdateReportUpdate })\n    .input(insertStatusReportUpdateSchema)\n    .mutation(async (opts) => {\n      const statusReportUpdateInput = opts.input;\n\n      if (!statusReportUpdateInput.id) return;\n\n      const existing = await opts.ctx.db.query.statusReportUpdate.findFirst({\n        where: eq(statusReportUpdate.id, statusReportUpdateInput.id),\n        with: { statusReport: true },\n      });\n\n      if (existing?.statusReport.workspaceId !== opts.ctx.workspace.id) {\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: \"You are not allowed to update this status report update\",\n        });\n      }\n\n      const { id, statusReportId, ...updateFields } = statusReportUpdateInput;\n      const currentStatusReportUpdate = await opts.ctx.db\n        .update(statusReportUpdate)\n        .set({ ...updateFields, updatedAt: new Date() })\n        .where(eq(statusReportUpdate.id, statusReportUpdateInput.id))\n        .returning()\n        .get();\n\n      return selectStatusReportUpdateSchema.parse(currentStatusReportUpdate);\n    }),\n\n  get: protectedProcedure\n    .input(z.object({ id: z.number() }))\n    .query(async (opts) => {\n      const result = await opts.ctx.db.query.statusReport.findFirst({\n        where: and(\n          eq(statusReport.id, opts.input.id),\n          eq(statusReport.workspaceId, opts.ctx.workspace.id),\n        ),\n        with: {\n          statusReportUpdates: true,\n          statusReportsToPageComponents: { with: { pageComponent: true } },\n          page: { with: { pageComponents: true } },\n        },\n      });\n\n      if (!result) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Status report not found\",\n        });\n      }\n\n      return selectStatusReportSchema\n        .extend({\n          updates: z.array(selectStatusReportUpdateSchema).prefault([]),\n          pageComponents: z.array(selectPageComponentSchema).prefault([]),\n          page: selectPageSchema.extend({\n            pageComponents: z.array(selectPageComponentSchema).prefault([]),\n          }),\n        })\n        .parse({\n          ...result,\n          updates: result.statusReportUpdates,\n          pageComponents: result.statusReportsToPageComponents.map(\n            ({ pageComponent }) => pageComponent,\n          ),\n        });\n    }),\n\n  list: protectedProcedure\n    .input(\n      z.object({\n        period: z.enum(periods).optional(),\n        order: z.enum([\"asc\", \"desc\"]).optional(),\n        pageId: z.number().optional(),\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(statusReport.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      if (opts.input?.period) {\n        whereConditions.push(\n          gte(statusReport.createdAt, getPeriodDate(opts.input.period)),\n        );\n      }\n\n      if (opts.input?.pageId) {\n        whereConditions.push(eq(statusReport.pageId, opts.input.pageId));\n      }\n\n      const result = await opts.ctx.db.query.statusReport.findMany({\n        where: and(...whereConditions),\n        with: {\n          statusReportUpdates: true,\n          statusReportsToPageComponents: { with: { pageComponent: true } },\n          page: { with: { pageComponents: true } },\n        },\n        orderBy: (statusReport) => [\n          opts.input.order === \"asc\"\n            ? asc(statusReport.createdAt)\n            : desc(statusReport.createdAt),\n        ],\n      });\n\n      return selectStatusReportSchema\n        .extend({\n          updates: z.array(selectStatusReportUpdateSchema).prefault([]),\n          pageComponents: z.array(selectPageComponentSchema).prefault([]),\n          page: selectPageSchema.extend({\n            pageComponents: z.array(selectPageComponentSchema).prefault([]),\n          }),\n        })\n        .array()\n        .parse(\n          result.map((report) => ({\n            ...report,\n            updates: report.statusReportUpdates,\n            pageComponents: report.statusReportsToPageComponents.map(\n              ({ pageComponent }) => pageComponent,\n            ),\n          })),\n        );\n    }),\n\n  create: protectedProcedure\n    .meta({ track: Events.CreateReport })\n    .input(\n      z.object({\n        title: z.string(),\n        status: z.enum(statusReportStatus),\n        pageId: z.number(),\n        pageComponents: z.array(z.number()),\n        date: z.coerce.date(),\n        message: z.string(),\n        notifySubscribers: z.boolean().nullish(),\n      }),\n    )\n    .mutation(async (opts) => {\n      const existingPage = await opts.ctx.db.query.page.findFirst({\n        where: and(\n          eq(page.id, opts.input.pageId),\n          eq(page.workspaceId, opts.ctx.workspace.id),\n        ),\n      });\n\n      if (!existingPage) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Page not found.\",\n        });\n      }\n\n      if (opts.input.pageComponents.length > 0) {\n        const components = await opts.ctx.db.query.pageComponent.findMany({\n          where: and(\n            eq(pageComponent.pageId, opts.input.pageId),\n            eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n          ),\n        });\n        const validIds = new Set(components.map((c) => c.id));\n        const invalid = opts.input.pageComponents.filter(\n          (id) => !validIds.has(id),\n        );\n        if (invalid.length > 0) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message: \"Invalid page component IDs.\",\n          });\n        }\n      }\n\n      return opts.ctx.db.transaction(async (tx) => {\n        const newStatusReport = await tx\n          .insert(statusReport)\n          .values({\n            workspaceId: opts.ctx.workspace.id,\n            title: opts.input.title,\n            status: opts.input.status,\n            pageId: opts.input.pageId,\n          })\n          .returning()\n          .get();\n\n        const newStatusReportUpdate = await tx\n          .insert(statusReportUpdate)\n          .values({\n            statusReportId: newStatusReport.id,\n            status: opts.input.status,\n            date: opts.input.date,\n            message: opts.input.message,\n          })\n          .returning()\n          .get();\n\n        if (opts.input.pageComponents.length > 0) {\n          await tx\n            .insert(statusReportsToPageComponents)\n            .values(\n              opts.input.pageComponents.map((pageComponent) => ({\n                pageComponentId: pageComponent,\n                statusReportId: newStatusReport.id,\n              })),\n            )\n            .run();\n        }\n\n        return {\n          ...newStatusReportUpdate,\n          notifySubscribers: opts.input.notifySubscribers,\n        };\n      });\n    }),\n\n  updateStatus: protectedProcedure\n    .meta({ track: Events.UpdateReport })\n    .input(\n      z.object({\n        id: z.number(),\n        pageComponents: z.array(z.number()),\n        title: z.string(),\n        status: z.enum(statusReportStatus),\n      }),\n    )\n    .mutation(async (opts) => {\n      const existing = await opts.ctx.db.query.statusReport.findFirst({\n        where: and(\n          eq(statusReport.id, opts.input.id),\n          eq(statusReport.workspaceId, opts.ctx.workspace.id),\n        ),\n      });\n\n      if (!existing || !existing.pageId) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Status report not found\",\n        });\n      }\n\n      if (opts.input.pageComponents.length > 0) {\n        const components = await opts.ctx.db.query.pageComponent.findMany({\n          where: and(\n            eq(pageComponent.pageId, existing.pageId),\n            eq(pageComponent.workspaceId, opts.ctx.workspace.id),\n          ),\n        });\n        const validIds = new Set(components.map((c) => c.id));\n        const invalid = opts.input.pageComponents.filter(\n          (id) => !validIds.has(id),\n        );\n        if (invalid.length > 0) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message: \"Invalid page component IDs.\",\n          });\n        }\n      }\n\n      await opts.ctx.db.transaction(async (tx) => {\n        await tx\n          .update(statusReport)\n          .set({\n            title: opts.input.title,\n            status: opts.input.status,\n            updatedAt: new Date(),\n          })\n          .where(\n            and(\n              eq(statusReport.id, opts.input.id),\n              eq(statusReport.workspaceId, opts.ctx.workspace.id),\n            ),\n          )\n          .run();\n\n        await tx\n          .delete(statusReportsToPageComponents)\n          .where(\n            eq(statusReportsToPageComponents.statusReportId, opts.input.id),\n          )\n          .run();\n\n        if (opts.input.pageComponents.length > 0) {\n          await tx\n            .insert(statusReportsToPageComponents)\n            .values(\n              opts.input.pageComponents.map((pageComponent) => ({\n                pageComponentId: pageComponent,\n                statusReportId: opts.input.id,\n              })),\n            )\n            .run();\n        }\n      });\n    }),\n\n  delete: protectedProcedure\n    .meta({ track: Events.DeleteReport })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(statusReport.id, opts.input.id),\n        eq(statusReport.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      await opts.ctx.db.transaction(async (tx) => {\n        await tx\n          .delete(statusReport)\n          .where(and(...whereConditions))\n          .run();\n      });\n    }),\n\n  deleteUpdate: protectedProcedure\n    .meta({ track: Events.DeleteReportUpdate })\n    .input(z.object({ id: z.number() }))\n    .mutation(async (opts) => {\n      await opts.ctx.db.transaction(async (tx) => {\n        const update = await tx.query.statusReportUpdate.findFirst({\n          where: eq(statusReportUpdate.id, opts.input.id),\n          with: {\n            statusReport: true,\n          },\n        });\n\n        if (update?.statusReport.workspaceId !== opts.ctx.workspace.id) {\n          throw new TRPCError({\n            code: \"FORBIDDEN\",\n            message: \"You are not allowed to delete this update\",\n          });\n        }\n\n        await tx\n          .delete(statusReportUpdate)\n          .where(eq(statusReportUpdate.id, opts.input.id))\n          .run();\n      });\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/stripe/index.ts",
    "content": "import { z } from \"zod\";\n\nimport { count, eq, schema } from \"@openstatus/db\";\nimport {\n  selectWorkspaceSchema,\n  user,\n  usersToWorkspaces,\n  workspace,\n  workspacePlans,\n} from \"@openstatus/db/src/schema\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport { addons } from \"@openstatus/db/src/schema/plan/schema\";\nimport { updateAddonInLimits } from \"@openstatus/db/src/schema/plan/utils\";\nimport { TRPCError } from \"@trpc/server\";\nimport type { Stripe } from \"stripe\";\nimport { createTRPCRouter, protectedProcedure } from \"../../trpc\";\nimport { stripe } from \"./shared\";\nimport { getPriceIdForFeature, getPriceIdForPlan } from \"./utils\";\nimport { webhookRouter } from \"./webhook\";\n\nconst url =\n  process.env.NODE_ENV === \"production\"\n    ? \"https://www.openstatus.dev\"\n    : \"http://localhost:3000\";\n\nexport const stripeRouter = createTRPCRouter({\n  webhooks: webhookRouter,\n\n  getUserCustomerPortal: protectedProcedure\n    .input(\n      z.object({ workspaceSlug: z.string(), returnUrl: z.string().optional() }),\n    )\n    .mutation(async (opts) => {\n      const result = await opts.ctx.db\n        .select()\n        .from(workspace)\n        .where(eq(workspace.slug, opts.input.workspaceSlug))\n        .get();\n\n      if (!result) return;\n\n      const currentUser = opts.ctx.db\n        .select()\n        .from(user)\n        .where(eq(user.id, opts.ctx.user.id))\n        .as(\"currentUser\");\n      const userHasAccess = await opts.ctx.db\n        .select()\n        .from(usersToWorkspaces)\n        .where(eq(usersToWorkspaces.workspaceId, result.id))\n        .innerJoin(currentUser, eq(usersToWorkspaces.userId, currentUser.id))\n        .get();\n\n      if (!userHasAccess || !userHasAccess.users_to_workspaces) return;\n      let stripeId = result.stripeId;\n      if (!stripeId) {\n        const customerData: {\n          metadata: { workspaceId: string };\n          email?: string;\n        } = {\n          metadata: {\n            workspaceId: String(result.id),\n          },\n          email: userHasAccess.currentUser.email || \"\",\n        };\n\n        const stripeUser = await stripe.customers.create(customerData);\n\n        stripeId = stripeUser.id;\n        await opts.ctx.db\n          .update(workspace)\n          .set({ stripeId })\n          .where(eq(workspace.id, result.id))\n          .run();\n      }\n\n      const session = await stripe.billingPortal.sessions.create({\n        customer: stripeId,\n        return_url:\n          opts.input.returnUrl || `${url}/app/${result.slug}/settings`,\n      });\n\n      return session.url;\n    }),\n\n  getCheckoutSession: protectedProcedure\n    .input(\n      z.object({\n        workspaceSlug: z.string(),\n        plan: z.enum(workspacePlans),\n        successUrl: z.string().optional(),\n        cancelUrl: z.string().optional(),\n        // TODO: plan: workspacePlanSchema\n      }),\n    )\n    .mutation(async (opts) => {\n      console.log(\"getCheckoutSession\");\n      // The following code is duplicated we should extract it\n      const result = await opts.ctx.db\n        .select()\n        .from(workspace)\n        .where(eq(workspace.slug, opts.input.workspaceSlug))\n        .get();\n\n      if (!result) return;\n\n      const currentUser = opts.ctx.db\n        .select()\n        .from(user)\n        .where(eq(user.id, opts.ctx.user.id))\n        .as(\"currentUser\");\n      const userHasAccess = await opts.ctx.db\n        .select()\n        .from(usersToWorkspaces)\n        .where(eq(usersToWorkspaces.workspaceId, result.id))\n        .innerJoin(currentUser, eq(usersToWorkspaces.userId, currentUser.id))\n        .get();\n\n      if (!userHasAccess || !userHasAccess.users_to_workspaces) return;\n      let stripeId = result.stripeId;\n      if (!stripeId) {\n        const currentUser = await opts.ctx.db\n          .select()\n          .from(user)\n          .where(eq(user.id, opts.ctx.user.id))\n          .get();\n        const customerData: {\n          metadata: { workspaceId: string };\n          email?: string;\n        } = {\n          metadata: {\n            workspaceId: String(result.id),\n          },\n          email: currentUser?.email || \"\",\n        };\n        const stripeUser = await stripe.customers.create(customerData);\n\n        stripeId = stripeUser.id;\n        await opts.ctx.db\n          .update(workspace)\n          .set({ stripeId })\n          .where(eq(workspace.id, result.id))\n          .run();\n      }\n\n      const priceId = getPriceIdForPlan(opts.input.plan);\n      const session = await stripe.checkout.sessions.create({\n        payment_method_types: [\"card\"],\n        customer: stripeId,\n        customer_update: {\n          name: \"auto\",\n          address: \"auto\",\n        },\n        line_items: [\n          {\n            price: priceId,\n            quantity: 1,\n          },\n        ],\n        tax_id_collection: {\n          enabled: true,\n        },\n        mode: \"subscription\",\n        success_url:\n          opts.input.successUrl ||\n          `${url}/app/${result.slug}/settings/billing?success=true`,\n        cancel_url:\n          opts.input.cancelUrl || `${url}/app/${result.slug}/settings/billing`,\n      });\n\n      return session;\n    }),\n\n  addAddon: protectedProcedure\n    .meta({ track: Events.AddFeature, trackProps: [\"feature\"] })\n    .input(\n      z.object({\n        workspaceSlug: z.string(),\n        feature: z.enum(addons),\n        value: z.union([z.boolean(), z.number()]),\n      }),\n    )\n    .mutation(async (opts) => {\n      // The following code is duplicated we should extract it\n      const result = await opts.ctx.db\n        .select()\n        .from(workspace)\n        .where(eq(workspace.slug, opts.input.workspaceSlug))\n        .get();\n\n      if (!result) return;\n\n      const ws = selectWorkspaceSchema.parse(result);\n\n      const currentUser = opts.ctx.db\n        .select()\n        .from(user)\n        .where(eq(user.id, opts.ctx.user.id))\n        .as(\"currentUser\");\n      const userHasAccess = await opts.ctx.db\n        .select()\n        .from(usersToWorkspaces)\n        .where(eq(usersToWorkspaces.workspaceId, result.id))\n        .innerJoin(currentUser, eq(usersToWorkspaces.userId, currentUser.id))\n        .get();\n\n      if (!userHasAccess || !userHasAccess.users_to_workspaces) return;\n      const stripeId = result.stripeId;\n      if (!stripeId) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Workspace has no Stripe ID\",\n        });\n      }\n\n      const sub = (await stripe.customers.retrieve(stripeId, {\n        expand: [\"subscriptions\"],\n      })) as Stripe.Customer;\n\n      if (!sub) {\n        return;\n      }\n\n      if (!sub.subscriptions?.data[0]?.id) {\n        return;\n      }\n\n      const priceId = getPriceIdForFeature(opts.input.feature);\n\n      if (!priceId) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Invalid feature\",\n        });\n      }\n\n      const quantity =\n        typeof opts.input.value === \"number\" ? opts.input.value : 1;\n\n      // We need to check the total of status page\n      if (opts.input.feature === \"status-pages\") {\n        const statusPageCt = await opts.ctx.db\n          .select({ count: count() })\n          .from(schema.page)\n          .where(eq(schema.page.workspaceId, result.id))\n          .get();\n        const pageCount = statusPageCt?.count ?? 0;\n        if (pageCount > quantity + allPlans[ws.plan].limits[\"status-pages\"]) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: `You already have ${pageCount} status pages, please delete some status page first.`,\n          });\n        }\n      }\n\n      const items = await stripe.subscriptionItems.list({\n        subscription: sub.subscriptions?.data[0]?.id,\n      });\n\n      const item = items.data.find((item) => item.price.id === priceId);\n\n      if (!opts.input.value && typeof opts.input.value === \"boolean\" && item) {\n        await stripe.subscriptionItems.del(item.id);\n      } else if (typeof opts.input.value === \"number\" && item) {\n        await stripe.subscriptionItems.update(item.id, {\n          quantity,\n        });\n      } else {\n        await stripe.subscriptionItems.create({\n          price: priceId,\n          subscription: sub.subscriptions?.data[0]?.id,\n          quantity,\n        });\n      }\n\n      const defaultLimit = allPlans[ws.plan].limits[opts.input.feature];\n      const newValue =\n        typeof opts.input.value === \"number\" && typeof defaultLimit === \"number\"\n          ? opts.input.value + defaultLimit\n          : opts.input.value;\n\n      const newLimits = updateAddonInLimits(\n        ws.limits,\n        opts.input.feature,\n        newValue,\n      );\n\n      console.log(\"new Limits\");\n\n      await opts.ctx.db\n        .update(workspace)\n        .set({ limits: JSON.stringify(newLimits) })\n        .where(eq(workspace.id, result.id))\n        .run();\n\n      // TODO: send email to user notifying about the change if not already from stripe\n\n      return;\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/stripe/shared.ts",
    "content": "import Stripe from \"stripe\";\n\nimport { env } from \"../../env\";\n\nexport const stripe = new Stripe(env.STRIPE_SECRET_KEY ?? \"\", {\n  apiVersion: \"2023-08-16\",\n  appInfo: {\n    name: \"OpenStatus\",\n    version: \"0.1.0\",\n  },\n});\n\nexport async function cancelSubscription(customer?: string) {\n  if (!customer) return;\n\n  try {\n    const subscriptionId = await stripe.subscriptions\n      .list({\n        customer,\n      })\n      .then((res) => res.data[0]?.id);\n\n    if (!subscriptionId) return;\n\n    return await stripe.subscriptions.update(subscriptionId, {\n      cancel_at_period_end: true,\n      cancellation_details: {\n        comment: \"Customer deleted their OpenStatus project.\",\n      },\n    });\n  } catch (error) {\n    console.log(\"Error cancelling Stripe subscription\", error);\n    return;\n  }\n}\n"
  },
  {
    "path": "packages/api/src/router/stripe/utils.ts",
    "content": "// Shamelessly stolen from dub.co\n\nimport type { WorkspacePlan } from \"@openstatus/db/src/schema\";\nimport type { Addons } from \"@openstatus/db/src/schema/plan/schema\";\n\nexport const getPlanFromPriceId = (priceId: string) => {\n  const env =\n    process.env.NEXT_PUBLIC_VERCEL_ENV === \"production\" ? \"production\" : \"test\";\n  return PLANS.find((plan) => plan.price.monthly.priceIds[env] === priceId);\n};\n\nexport const getFeatureFromPriceId = (priceId: string) => {\n  const env =\n    process.env.NEXT_PUBLIC_VERCEL_ENV === \"production\" ? \"production\" : \"test\";\n  return FEATURES.find(\n    (feature) => feature.price.monthly.priceIds[env] === priceId,\n  );\n};\n\nexport const getPriceIdForPlan = (plan: WorkspacePlan) => {\n  const env =\n    process.env.NEXT_PUBLIC_VERCEL_ENV === \"production\" ? \"production\" : \"test\";\n  return PLANS.find((p) => p.plan === plan)?.price.monthly.priceIds[env];\n};\n\nexport const getPriceIdForFeature = (feature: keyof Addons) => {\n  const env =\n    process.env.NEXT_PUBLIC_VERCEL_ENV === \"production\" ? \"production\" : \"test\";\n  return FEATURES.find((f) => f.feature === feature)?.price.monthly.priceIds[\n    env\n  ];\n};\n\nexport const PLANS = [\n  {\n    plan: \"team\",\n    price: {\n      monthly: {\n        priceIds: {\n          test: \"price_1OVHQDBXJcTfzsyJjfiXl10Y\",\n          production: \"price_1RxsLNBXJcTfzsyJ7La5Jn5y\",\n        },\n      },\n    },\n  },\n  {\n    plan: \"starter\",\n    price: {\n      monthly: {\n        priceIds: {\n          test: \"price_1OVHPlBXJcTfzsyJvPlB1kNb\",\n          production: \"price_1RxsJzBXJcTfzsyJBOztaKlR\",\n        },\n      },\n    },\n  },\n] satisfies Array<{\n  plan: WorkspacePlan;\n  price: {\n    monthly: { priceIds: { test: string; production: string } };\n  };\n}>;\n\nexport const FEATURES = [\n  {\n    feature: \"email-domain-protection\",\n    price: {\n      monthly: {\n        priceIds: {\n          test: \"price_1Sl4xqBXJcTfzsyJlzpD1DDm\",\n          production: \"price_1Sl6oqBXJcTfzsyJCxtzDIx5\",\n        },\n      },\n    },\n  },\n  {\n    feature: \"white-label\",\n    price: {\n      monthly: {\n        priceIds: {\n          test: \"price_1SlbQsBXJcTfzsyJ1awtpOno\",\n          production: \"price_1SlbSdBXJcTfzsyJahJiFE8D\",\n        },\n      },\n    },\n  },\n  {\n    feature: \"status-pages\",\n    price: {\n      monthly: {\n        priceIds: {\n          test: \"price_1Slrk8BXJcTfzsyJXQxshFU4\",\n          production: \"price_1SlrkHBXJcTfzsyJIxHeKUYe\",\n        },\n      },\n    },\n  },\n] satisfies Array<{\n  feature: keyof Addons;\n  price: {\n    monthly: { priceIds: { test: string; production: string } };\n  };\n}>;\n"
  },
  {
    "path": "packages/api/src/router/stripe/webhook.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport type Stripe from \"stripe\";\nimport { z } from \"zod\";\n\nimport { Events, setupAnalytics } from \"@openstatus/analytics\";\nimport { and, asc, eq, isNull, ne } from \"@openstatus/db\";\nimport {\n  invitation,\n  monitor,\n  notification,\n  page,\n  selectWorkspaceSchema,\n  user,\n  usersToWorkspaces,\n  workspace,\n} from \"@openstatus/db/src/schema\";\n\nimport {\n  getLimits,\n  updateAddonInLimits,\n} from \"@openstatus/db/src/schema/plan/utils\";\nimport { createTRPCRouter, publicProcedure } from \"../../trpc\";\nimport { stripe } from \"./shared\";\nimport { getFeatureFromPriceId, getPlanFromPriceId } from \"./utils\";\n\nconst webhookProcedure = publicProcedure.input(\n  z.object({\n    // From type Stripe.Event\n    event: z.object({\n      id: z.string(),\n      account: z.string().nullish(),\n      created: z.number(),\n      data: z.object({\n        object: z.record(z.string(), z.any()),\n      }),\n      type: z.string(),\n    }),\n  }),\n);\n\nexport const webhookRouter = createTRPCRouter({\n  customerSubscriptionUpdated: webhookProcedure.mutation(async (opts) => {\n    const subscription = opts.input.event.data.object as Stripe.Subscription;\n\n    if (subscription.status !== \"active\") {\n      return;\n    }\n\n    const customerId =\n      typeof subscription.customer === \"string\"\n        ? subscription.customer\n        : subscription.customer.id;\n\n    const result = await opts.ctx.db\n      .select()\n      .from(workspace)\n      .where(eq(workspace.stripeId, customerId))\n      .get();\n    if (!result) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Workspace not found\",\n      });\n    }\n\n    const ws = selectWorkspaceSchema.parse(result);\n    const oldPlan = ws.plan;\n\n    let detectedPlan: ReturnType<typeof getPlanFromPriceId> = undefined;\n\n    for (const item of subscription.items.data) {\n      const plan = getPlanFromPriceId(item.price.id);\n      if (plan) {\n        detectedPlan = plan;\n        break;\n      }\n    }\n\n    if (!detectedPlan) {\n      return;\n    }\n\n    await opts.ctx.db\n      .update(workspace)\n      .set({\n        plan: detectedPlan.plan,\n        subscriptionId: subscription.id,\n        endsAt: new Date(subscription.current_period_end * 1000),\n        paidUntil: new Date(subscription.current_period_end * 1000),\n        limits: JSON.stringify(getLimits(detectedPlan.plan)),\n      })\n      .where(eq(workspace.id, result.id))\n      .run();\n\n    const allActive = await stripe.subscriptions.list({\n      customer: customerId,\n      status: \"active\",\n    });\n\n    for (const sub of allActive.data) {\n      if (sub.id === subscription.id) continue;\n      try {\n        await stripe.subscriptions.cancel(sub.id);\n      } catch (e) {\n        console.error(`Failed to cancel duplicate subscription ${sub.id}:`, e);\n      }\n    }\n\n    const newPlan = detectedPlan?.plan ?? oldPlan;\n    if (detectedPlan && newPlan !== oldPlan) {\n      const customer = await stripe.customers.retrieve(customerId);\n      if (!customer.deleted && customer.email) {\n        const userResult = await opts.ctx.db\n          .select()\n          .from(user)\n          .where(eq(user.email, customer.email))\n          .get();\n        if (!userResult) return;\n\n        const planOrder = [\"free\", \"starter\", \"team\"] as const;\n        const oldIndex = planOrder.indexOf(oldPlan ?? \"free\");\n        const newIndex = planOrder.indexOf(newPlan ?? \"free\");\n\n        const event =\n          newIndex > oldIndex\n            ? Events.UpgradeWorkspace\n            : Events.DowngradeWorkspace;\n\n        const analytics = await setupAnalytics({\n          userId: `usr_${userResult.id}`,\n          email: userResult.email || undefined,\n          workspaceId: String(result.id),\n          plan: newPlan,\n        });\n        await analytics.track(event);\n      }\n    }\n  }),\n  sessionCompleted: webhookProcedure.mutation(async (opts) => {\n    const session = opts.input.event.data.object as Stripe.Checkout.Session;\n    if (typeof session.subscription !== \"string\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Missing or invalid subscription id\",\n      });\n    }\n    const subscription = await stripe.subscriptions.retrieve(\n      session.subscription,\n    );\n    const customerId =\n      typeof subscription.customer === \"string\"\n        ? subscription.customer\n        : subscription.customer.id;\n\n    const result = await opts.ctx.db\n      .select()\n      .from(workspace)\n      .where(eq(workspace.stripeId, customerId))\n      .get();\n    if (!result) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Workspace not found\",\n      });\n    }\n\n    for (const item of subscription.items.data) {\n      const plan = getPlanFromPriceId(item.price.id);\n      if (!plan) {\n        const feature = getFeatureFromPriceId(item.price.id);\n        if (feature) {\n          const _ws = await opts.ctx.db\n            .select()\n            .from(workspace)\n            .where(eq(workspace.stripeId, customerId))\n            .get();\n\n          const ws = selectWorkspaceSchema.parse(_ws);\n\n          const currentValue = ws.limits[feature.feature];\n          const newValue =\n            typeof currentValue === \"boolean\"\n              ? true\n              : typeof currentValue === \"number\"\n                ? currentValue + 1\n                : currentValue;\n\n          const newLimits = updateAddonInLimits(\n            ws.limits,\n            feature.feature,\n            newValue,\n          );\n\n          await opts.ctx.db\n            .update(workspace)\n            .set({\n              limits: JSON.stringify(newLimits),\n            })\n            .where(eq(workspace.id, result.id))\n            .run();\n          continue;\n        }\n        console.error(\"Invalid plan\");\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Invalid plan\",\n        });\n      }\n      await opts.ctx.db\n        .update(workspace)\n        .set({\n          plan: plan.plan,\n          subscriptionId: subscription.id,\n          endsAt: new Date(subscription.current_period_end * 1000),\n          paidUntil: new Date(subscription.current_period_end * 1000),\n          limits: JSON.stringify(getLimits(plan.plan)),\n        })\n        .where(eq(workspace.id, result.id))\n        .run();\n      const customer = await stripe.customers.retrieve(customerId);\n      if (!customer.deleted && customer.email) {\n        const userResult = await opts.ctx.db\n          .select()\n          .from(user)\n          .where(eq(user.email, customer.email))\n          .get();\n        if (!userResult) return;\n\n        const analytics = await setupAnalytics({\n          userId: `usr_${userResult.id}`,\n          email: userResult.email || undefined,\n          workspaceId: String(result.id),\n          plan: plan.plan,\n        });\n        await analytics.track(Events.UpgradeWorkspace);\n      }\n    }\n  }),\n  customerSubscriptionDeleted: webhookProcedure.mutation(async (opts) => {\n    const subscription = opts.input.event.data.object as Stripe.Subscription;\n    const customerId =\n      typeof subscription.customer === \"string\"\n        ? subscription.customer\n        : subscription.customer.id;\n\n    const activeSubscriptions = await stripe.subscriptions.list({\n      customer: customerId,\n      status: \"active\",\n    });\n\n    if (activeSubscriptions.data.length > 0) {\n      return;\n    }\n\n    const _workspace = await opts.ctx.db.transaction(async (tx) => {\n      const _workspace = await tx\n        .update(workspace)\n        .set({\n          subscriptionId: null,\n          plan: \"free\",\n          paidUntil: null,\n          endsAt: null,\n          limits: JSON.stringify(getLimits(\"free\")),\n        })\n        .where(eq(workspace.stripeId, customerId))\n        .returning();\n\n      if (!_workspace.length) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Workspace not found\",\n        });\n      }\n\n      const workspaceId = _workspace[0].id;\n\n      const activeMonitors = await tx\n        .select({ id: monitor.id })\n        .from(monitor)\n        .where(\n          and(\n            eq(monitor.workspaceId, workspaceId),\n            eq(monitor.active, true),\n            isNull(monitor.deletedAt),\n          ),\n        )\n        .orderBy(asc(monitor.createdAt));\n\n      for (const m of activeMonitors.slice(1)) {\n        await tx\n          .update(monitor)\n          .set({ active: false })\n          .where(eq(monitor.id, m.id))\n          .run();\n      }\n\n      const statusPages = await tx\n        .select({ id: page.id })\n        .from(page)\n        .where(eq(page.workspaceId, workspaceId))\n        .orderBy(asc(page.createdAt));\n\n      for (const p of statusPages.slice(1)) {\n        await tx.delete(page).where(eq(page.id, p.id)).run();\n      }\n\n      if (statusPages.length > 0) {\n        await tx\n          .update(page)\n          .set({\n            customDomain: \"\",\n            password: null,\n            accessType: \"public\",\n            authEmailDomains: null,\n          })\n          .where(eq(page.id, statusPages[0].id))\n          .run();\n      }\n\n      const notifications = await tx\n        .select({ id: notification.id, provider: notification.provider })\n        .from(notification)\n        .where(eq(notification.workspaceId, workspaceId))\n        .orderBy(asc(notification.createdAt));\n\n      const keepNotification =\n        notifications.find((n) => n.provider === \"email\") ?? notifications[0];\n\n      for (const n of notifications.filter(\n        (n) => n.id !== keepNotification?.id,\n      )) {\n        await tx.delete(notification).where(eq(notification.id, n.id)).run();\n      }\n\n      // Remove all non-owner members from the workspace\n      await tx\n        .delete(usersToWorkspaces)\n        .where(\n          and(\n            eq(usersToWorkspaces.workspaceId, workspaceId),\n            ne(usersToWorkspaces.role, \"owner\"),\n          ),\n        )\n        .run();\n\n      // Remove all pending invitations for the workspace\n      await tx\n        .delete(invitation)\n        .where(eq(invitation.workspaceId, workspaceId))\n        .run();\n\n      return _workspace;\n    });\n\n    if (!_workspace[0]) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Workspace not found\",\n      });\n    }\n\n    const workspaceId = _workspace[0].id;\n    const customer = await stripe.customers.retrieve(customerId);\n\n    if (!customer.deleted && customer.email) {\n      const userResult = await opts.ctx.db\n        .select()\n        .from(user)\n        .where(eq(user.email, customer.email))\n        .get();\n      if (!userResult) return;\n\n      const analytics = await setupAnalytics({\n        userId: `usr_${userResult.id}`,\n        email: customer.email || undefined,\n        workspaceId: String(workspaceId),\n        plan: \"free\",\n      });\n      await analytics.track(Events.DowngradeWorkspace);\n    }\n  }),\n});\n"
  },
  {
    "path": "packages/api/src/router/tinybird/index.test.ts",
    "content": "import { expect, test } from \"bun:test\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { edgeRouter } from \"../../edge\";\nimport { createInnerTRPCContext } from \"../../trpc\";\n\n// Workspace 1 owns monitor 1; monitor 5 belongs to workspace 3.\n// These IDs come from the seed data (packages/db/src/seed.mts).\n\nfunction callerForWorkspace(workspaceId: number) {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: { user: { id: \"1\" } },\n    // @ts-expect-error - minimal workspace for test\n    workspace: { id: workspaceId, plan: \"team\" },\n  });\n  return edgeRouter.createCaller(ctx);\n}\n\n// ─── metricsLatency ──────────────────────────────────────────────\n\ntest(\"tinybird.metricsLatency rejects monitor from another workspace\", async () => {\n  const caller = callerForWorkspace(1);\n\n  try {\n    // Monitor 5 belongs to workspace 3\n    await caller.tinybird.metricsLatency({\n      monitorId: \"5\",\n      period: \"1d\",\n      type: \"http\",\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n});\n\ntest(\"tinybird.metricsLatency succeeds for own workspace monitor\", async () => {\n  const caller = callerForWorkspace(1);\n\n  // Monitor 1 belongs to workspace 1 — should not throw\n  const result = await caller.tinybird.metricsLatency({\n    monitorId: \"1\",\n    period: \"1d\",\n    type: \"http\",\n  });\n  expect(result).toBeDefined();\n});\n\n// ─── metricsTimingPhases ─────────────────────────────────────────\n\ntest(\"tinybird.metricsTimingPhases rejects monitor from another workspace\", async () => {\n  const caller = callerForWorkspace(1);\n\n  try {\n    // Monitor 5 belongs to workspace 3\n    await caller.tinybird.metricsTimingPhases({\n      monitorId: \"5\",\n      period: \"1d\",\n      type: \"http\",\n    });\n    throw new Error(\"Should have thrown\");\n  } catch (e) {\n    expect(e).toBeInstanceOf(TRPCError);\n    expect((e as TRPCError).code).toBe(\"NOT_FOUND\");\n  }\n});\n\ntest(\"tinybird.metricsTimingPhases succeeds for own workspace monitor\", async () => {\n  const caller = callerForWorkspace(1);\n\n  // Monitor 1 belongs to workspace 1 — should not throw\n  const result = await caller.tinybird.metricsTimingPhases({\n    monitorId: \"1\",\n    period: \"1d\",\n    type: \"http\",\n  });\n  expect(result).toBeDefined();\n});\n"
  },
  {
    "path": "packages/api/src/router/tinybird/index.ts",
    "content": "import { z } from \"zod\";\n\nimport { monitorRegions } from \"@openstatus/db/src/schema/constants\";\nimport { OSTinybird } from \"@openstatus/tinybird\";\n\nimport { type SQL, and, db, eq, inArray } from \"@openstatus/db\";\nimport { monitor } from \"@openstatus/db/src/schema\";\nimport { TRPCError } from \"@trpc/server\";\nimport { env } from \"../../env\";\nimport { createTRPCRouter, protectedProcedure } from \"../../trpc\";\nimport { calculatePeriod } from \"./utils\";\n\nconst tb = new OSTinybird(env.TINY_BIRD_API_KEY);\n\nconst periods = [\"1d\", \"7d\", \"14d\"] as const;\nconst types = [\"http\", \"tcp\", \"dns\"] as const;\ntype Period = (typeof periods)[number];\ntype Type = (typeof types)[number];\n\n// NEW: workspace-level counters helper\nexport function getWorkspace30dProcedure(type: Type) {\n  return type === \"http\" ? tb.httpWorkspace30d : tb.tcpWorkspace30d;\n}\n// Helper functions to get the right procedure based on period and type\nexport function getListProcedure(period: Period, type: Type) {\n  console.log({ period, type });\n  switch (period) {\n    case \"1d\":\n      if (type === \"http\") return tb.httpListDaily;\n      if (type === \"tcp\") return tb.tcpListDaily;\n      if (type === \"dns\") return tb.dnsListBiweekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    case \"7d\":\n      if (type === \"http\") return tb.httpListWeekly;\n      if (type === \"tcp\") return tb.tcpListWeekly;\n      if (type === \"dns\") return tb.dnsListBiweekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    case \"14d\":\n      if (type === \"http\") return tb.httpListBiweekly;\n      if (type === \"tcp\") return tb.tcpListBiweekly;\n      if (type === \"dns\") return tb.dnsListBiweekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    default:\n      if (type === \"http\") return tb.httpListDaily;\n      if (type === \"tcp\") return tb.tcpListDaily;\n      if (type === \"dns\") return tb.dnsListBiweekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n  }\n}\n\nexport function getMetricsProcedure(period: Period, type: Type) {\n  switch (period) {\n    case \"1d\":\n      if (type === \"dns\") return tb.dnsMetricsDaily;\n      if (type === \"http\") return tb.httpMetricsDaily;\n      if (type === \"tcp\") return tb.tcpMetricsDaily;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    case \"7d\":\n      if (type === \"dns\") return tb.dnsMetricsWeekly;\n      if (type === \"http\") return tb.httpMetricsWeekly;\n      if (type === \"tcp\") return tb.tcpMetricsWeekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    case \"14d\":\n      if (type === \"dns\") return tb.dnsMetricsBiweekly;\n      if (type === \"http\") return tb.httpMetricsBiweekly;\n      if (type === \"tcp\") return tb.tcpMetricsBiweekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    default:\n      if (type === \"dns\") return tb.dnsMetricsDaily;\n      if (type === \"http\") return tb.httpMetricsDaily;\n      if (type === \"tcp\") return tb.tcpMetricsDaily;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n  }\n}\n\n// FIXME: tb pipes are deprecated, we need new ones\nexport function getMetricsRegionsProcedure(period: Period, type: Type) {\n  switch (period) {\n    case \"1d\":\n      if (type === \"dns\") return tb.dnsMetricsRegionsBiweekly;\n      if (type === \"http\") return tb.httpMetricsRegionsDaily;\n      if (type === \"tcp\") return tb.tcpMetricsByIntervalDaily;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    case \"7d\":\n      if (type === \"dns\") return tb.dnsMetricsRegionsBiweekly;\n      if (type === \"http\") return tb.httpMetricsRegionsWeekly;\n      if (type === \"tcp\") return tb.tcpMetricsByIntervalWeekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    case \"14d\":\n      if (type === \"dns\") return tb.dnsMetricsRegionsBiweekly;\n      if (type === \"http\") return tb.httpMetricsRegionsBiweekly;\n      if (type === \"tcp\") return tb.tcpMetricsByIntervalBiweekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    default:\n      if (type === \"dns\") return tb.dnsMetricsRegionsBiweekly;\n      if (type === \"http\") return tb.httpMetricsRegionsDaily;\n      if (type === \"tcp\") return tb.tcpMetricsByIntervalDaily;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n  }\n}\n\nexport function getStatusProcedure(_period: \"45d\", type: Type) {\n  if (type === \"dns\") return tb.dnsStatus45d;\n  if (type === \"http\") return tb.httpStatus45d;\n  if (type === \"tcp\") return tb.tcpStatus45d;\n  throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n}\n\nexport function getGetProcedure(period: \"14d\", type: Type) {\n  switch (period) {\n    case \"14d\":\n      if (type === \"http\") return tb.httpGetBiweekly;\n      if (type === \"tcp\") return tb.tcpGetBiweekly;\n      if (type === \"dns\") return tb.dnsGetBiweekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    default:\n      if (type === \"http\") return tb.httpGetBiweekly;\n      if (type === \"tcp\") return tb.tcpGetBiweekly;\n      if (type === \"dns\") return tb.dnsGetBiweekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n  }\n}\n\nexport function getGlobalMetricsProcedure(type: Type) {\n  return type === \"http\" ? tb.httpGlobalMetricsDaily : tb.tcpGlobalMetricsDaily;\n}\n\nexport function getUptimeProcedure(period: \"7d\" | \"30d\", type: Type) {\n  switch (period) {\n    case \"7d\":\n      if (type === \"dns\") return tb.dnsUptime30d;\n      if (type === \"http\") return tb.httpUptimeWeekly;\n      if (type === \"tcp\") return tb.tcpUptimeWeekly;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    case \"30d\":\n      if (type === \"dns\") return tb.dnsUptime30d;\n      if (type === \"http\") return tb.httpUptime30d;\n      if (type === \"tcp\") return tb.tcpUptime30d;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    default:\n      if (type === \"dns\") return tb.dnsUptime30d;\n      if (type === \"http\") return tb.httpUptime30d;\n      if (type === \"tcp\") return tb.tcpUptime30d;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n  }\n}\n\n// TODO: missing pipes for other periods\nexport function getMetricsLatencyProcedure(_period: Period, type: Type) {\n  switch (_period) {\n    case \"1d\":\n      if (type === \"dns\") return tb.dnsMetricsLatency7d;\n      if (type === \"http\") return tb.httpMetricsLatency1d;\n      if (type === \"tcp\") return tb.tcpMetricsLatency1d;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    case \"7d\":\n      if (type === \"dns\") return tb.dnsMetricsLatency7d;\n      if (type === \"http\") return tb.httpMetricsLatency7d;\n      if (type === \"tcp\") return tb.tcpMetricsLatency7d;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n    default:\n      if (type === \"dns\") return tb.dnsMetricsLatency7d;\n      if (type === \"http\") return tb.httpMetricsLatency1d;\n      if (type === \"tcp\") return tb.tcpMetricsLatency1d;\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n  }\n}\n\nexport function getMetricsLatencyMultiProcedure(_period: Period, type: Type) {\n  if (type === \"dns\") return tb.dnsMetricsLatency1dMulti;\n  if (type === \"http\") return tb.httpMetricsLatency1dMulti;\n  if (type === \"tcp\") return tb.tcpMetricsLatency1dMulti;\n  throw new TRPCError({ code: \"NOT_FOUND\", message: \"Invalid type\" });\n}\n\nexport function getTimingPhasesProcedure(type: Type) {\n  return type === \"http\" ? tb.httpTimingPhases14d : null;\n}\n\nexport const tinybirdRouter = createTRPCRouter({\n  list: protectedProcedure\n    .input(\n      z.object({\n        monitorId: z.string(),\n        region: z.enum(monitorRegions).or(z.string()).optional(),\n        cronTimestamp: z.int().optional(),\n        from: z.coerce.date().optional(),\n        to: z.coerce.date().optional(),\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, Number.parseInt(opts.input.monitorId)),\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      const _monitor = await db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!_monitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found\",\n        });\n      }\n\n      const period = calculatePeriod(opts.input.from, opts.input.to);\n\n      const procedure = getListProcedure(\n        period,\n        _monitor.jobType as \"http\" | \"tcp\" | \"dns\",\n      );\n      return await procedure({\n        ...opts.input,\n        fromDate: opts.input.from?.getTime() ?? undefined,\n        toDate: opts.input.to?.getTime(),\n      });\n    }),\n\n  uptime: protectedProcedure\n    .input(\n      z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n        interval: z.int().optional(), // in minutes, default 30\n        regions: z.enum(monitorRegions).or(z.string()).array().optional(),\n        type: z.enum(types).prefault(\"http\"),\n        period: z.enum([\"7d\", \"30d\"]).prefault(\"30d\"),\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, Number.parseInt(opts.input.monitorId)),\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      const _monitor = await db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!_monitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found\",\n        });\n      }\n\n      const procedure = getUptimeProcedure(opts.input.period, opts.input.type);\n      return await procedure(opts.input);\n    }),\n\n  auditLog: protectedProcedure\n    .input(\n      z.object({\n        monitorId: z.string(),\n        interval: z.int().prefault(30), // in days\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, Number.parseInt(opts.input.monitorId)),\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      const _monitor = await db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!_monitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found\",\n        });\n      }\n\n      return await tb.getAuditLog({\n        monitorId: `monitor:${opts.input.monitorId}`,\n        interval: opts.input.interval,\n      });\n    }),\n\n  metrics: protectedProcedure\n    .input(\n      z.object({\n        monitorId: z.string(),\n        period: z.enum(periods),\n        type: z.enum(types).prefault(\"http\"),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        cronTimestamp: z.int().optional(),\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, Number.parseInt(opts.input.monitorId)),\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      const _monitor = await db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (opts.ctx.workspace.plan === \"free\") {\n        opts.input.regions = undefined;\n      }\n\n      if (!_monitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found\",\n        });\n      }\n\n      const procedure = getMetricsProcedure(opts.input.period, opts.input.type);\n      return await procedure(opts.input);\n    }),\n\n  metricsRegions: protectedProcedure\n    .input(\n      z.object({\n        monitorId: z.string(),\n        period: z.enum(periods),\n        type: z.enum(types).prefault(\"http\"),\n        // Additional filters\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        cronTimestamp: z.int().optional(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, Number.parseInt(opts.input.monitorId)),\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      const _monitor = await db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!_monitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found\",\n        });\n      }\n\n      if (opts.ctx.workspace.plan === \"free\") {\n        opts.input.regions = undefined;\n      }\n\n      const procedure = getMetricsRegionsProcedure(\n        opts.input.period,\n        opts.input.type,\n      );\n      return await procedure(opts.input);\n    }),\n\n  get: protectedProcedure\n    .input(\n      z.object({\n        id: z.string().nullable(),\n        monitorId: z.string(),\n        period: z.enum([\"14d\"]).prefault(\"14d\"),\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, Number.parseInt(opts.input.monitorId)),\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      const _monitor = await db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!_monitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found\",\n        });\n      }\n\n      const procedure = getGetProcedure(\n        opts.input.period,\n        _monitor.jobType as \"http\" | \"tcp\" | \"dns\",\n      );\n      return await procedure(opts.input);\n    }),\n\n  globalMetrics: protectedProcedure\n    .input(\n      z.object({\n        monitorIds: z.string().array(),\n        type: z.enum(types).prefault(\"http\"),\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n        inArray(monitor.id, opts.input.monitorIds.map(Number)),\n      ];\n\n      const _monitors = await db.query.monitor.findMany({\n        where: and(...whereConditions),\n      });\n\n      if (_monitors.length !== opts.input.monitorIds.length) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Some monitors not found\",\n        });\n      }\n\n      const procedure = getGlobalMetricsProcedure(opts.input.type);\n      return await procedure(opts.input);\n    }),\n\n  metricsLatency: protectedProcedure\n    .input(\n      z.object({\n        monitorId: z.string(),\n        period: z.enum(periods),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        type: z.enum(types).prefault(\"http\"),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, Number.parseInt(opts.input.monitorId)),\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      const _monitor = await db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!_monitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found\",\n        });\n      }\n\n      if (opts.ctx.workspace.plan === \"free\") {\n        opts.input.regions = undefined;\n      }\n\n      const procedure = getMetricsLatencyProcedure(\n        opts.input.period,\n        opts.input.type,\n      );\n      return await procedure(opts.input);\n    }),\n\n  metricsTimingPhases: protectedProcedure\n    .input(\n      z.object({\n        monitorId: z.string(),\n        period: z.enum(periods),\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        type: z.literal(\"http\"),\n      }),\n    )\n    .query(async (opts) => {\n      const whereConditions: SQL[] = [\n        eq(monitor.id, Number.parseInt(opts.input.monitorId)),\n        eq(monitor.workspaceId, opts.ctx.workspace.id),\n      ];\n\n      const _monitor = await db.query.monitor.findFirst({\n        where: and(...whereConditions),\n      });\n\n      if (!_monitor) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Monitor not found\",\n        });\n      }\n\n      if (opts.ctx.workspace.plan === \"free\") {\n        opts.input.regions = undefined;\n      }\n\n      const procedure = getTimingPhasesProcedure(opts.input.type);\n\n      if (!procedure) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Timing phases not supported for this type\",\n        });\n      }\n\n      return await procedure(opts.input);\n    }),\n\n  workspace30d: protectedProcedure\n    .input(\n      z.object({\n        type: z.enum(types).prefault(\"http\"),\n      }),\n    )\n    .query(async (opts) => {\n      const procedure = getWorkspace30dProcedure(opts.input.type);\n      return await procedure({ workspaceId: String(opts.ctx.workspace.id) });\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/router/tinybird/utils.ts",
    "content": "export function calculatePeriod(from: Date | undefined, _to: Date | undefined) {\n  const today = new Date();\n  const diffTime = Math.abs(today.getTime() - (from?.getTime() ?? 0));\n  const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));\n  console.log({ diffDays });\n  if (diffDays <= 1) return \"1d\";\n  if (diffDays <= 7) return \"7d\";\n  if (diffDays <= 14) return \"14d\";\n  return \"14d\";\n}\n"
  },
  {
    "path": "packages/api/src/router/user.ts",
    "content": "import { and, eq, isNull, ne } from \"@openstatus/db\";\nimport {\n  account,\n  session,\n  user,\n  usersToWorkspaces,\n} from \"@openstatus/db/src/schema\";\n\nimport { TRPCError } from \"@trpc/server\";\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const userRouter = createTRPCRouter({\n  get: protectedProcedure.query(async (opts) => {\n    return await opts.ctx.db\n      .select()\n      .from(user)\n      .where(and(eq(user.id, opts.ctx.user.id), isNull(user.deletedAt)))\n      .get();\n  }),\n\n  deleteAccount: protectedProcedure.mutation(async (opts) => {\n    const userId = opts.ctx.user.id;\n\n    // Check if user owns any workspace with a paid plan\n    const ownedWorkspaces = await opts.ctx.db.query.usersToWorkspaces.findMany({\n      where: and(\n        eq(usersToWorkspaces.userId, userId),\n        eq(usersToWorkspaces.role, \"owner\"),\n      ),\n      with: {\n        workspace: true,\n      },\n    });\n\n    const hasPaidWorkspace = ownedWorkspaces.some(\n      ({ workspace }) => workspace.plan && workspace.plan !== \"free\",\n    );\n\n    if (hasPaidWorkspace) {\n      throw new TRPCError({\n        code: \"PRECONDITION_FAILED\",\n        message:\n          \"You must cancel your subscription before deleting your account.\",\n      });\n    }\n\n    await opts.ctx.db.transaction(async (tx) => {\n      // Remove from non-owned workspaces\n      await tx\n        .delete(usersToWorkspaces)\n        .where(\n          and(\n            eq(usersToWorkspaces.userId, userId),\n            ne(usersToWorkspaces.role, \"owner\"),\n          ),\n        );\n\n      // Delete sessions\n      await tx.delete(session).where(eq(session.userId, userId));\n\n      // Delete OAuth accounts\n      await tx.delete(account).where(eq(account.userId, userId));\n\n      // Soft delete user\n      await tx\n        .update(user)\n        .set({\n          deletedAt: new Date(),\n          email: \"\",\n          firstName: \"\",\n          lastName: \"\",\n          photoUrl: \"\",\n          name: \"\",\n        })\n        .where(eq(user.id, userId));\n    });\n  }),\n});\n"
  },
  {
    "path": "packages/api/src/router/utils.ts",
    "content": "/**\n * Shared utilities for API routers\n */\n\n/**\n * Supported time period values for filtering data\n */\nexport const periods = [\"1d\", \"7d\", \"14d\"] as const;\n\n/**\n * Period type for filtering data by time range\n */\nexport type Period = (typeof periods)[number];\n\n/**\n * Converts a period string to a Date object representing the start of that period\n * @param period - The period to convert (e.g., \"1d\", \"7d\", \"14d\")\n * @returns Date object representing the start of the period (for use with gte filters)\n * @example\n * // Get date for 7 days ago\n * const date = getPeriodDate(\"7d\");\n * // Returns: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)\n */\nexport function getPeriodDate(period: Period): Date {\n  const now = Date.now();\n\n  switch (period) {\n    case \"1d\":\n      return new Date(now - 1 * 24 * 60 * 60 * 1000);\n    case \"7d\":\n      return new Date(now - 7 * 24 * 60 * 60 * 1000);\n    case \"14d\":\n      return new Date(now - 14 * 24 * 60 * 60 * 1000);\n    default:\n      // TypeScript ensures this is exhaustive, but return 7d as safe fallback\n      return new Date(now - 7 * 24 * 60 * 60 * 1000);\n  }\n}\n"
  },
  {
    "path": "packages/api/src/router/workspace.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\nimport { edgeRouter } from \"../edge\";\nimport { createInnerTRPCContext } from \"../trpc\";\n\ntest(\"Get Test Workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: {\n      user: {\n        id: \"1\",\n      },\n    },\n    //@ts-expect-error\n    workspace: {\n      id: 1,\n    },\n  });\n\n  const caller = edgeRouter.createCaller(ctx);\n  const result = await caller.workspace.getWorkspace();\n\n  expect(result).toMatchObject({\n    id: 1,\n    slug: \"love-openstatus\",\n    name: \"test\",\n    plan: \"team\",\n    paidUntil: null,\n    stripeId: \"stripeId1\",\n    subscriptionId: \"subscriptionId\",\n    updatedAt: expect.any(Date),\n    createdAt: expect.any(Date),\n    endsAt: null,\n  });\n});\n\ntest(\"by default we get the first workspace\", async () => {\n  const ctx = createInnerTRPCContext({\n    req: undefined,\n    session: {\n      user: {\n        // @ts-expect-error some issues with types\n        id: 1,\n      },\n    },\n    workspace: undefined,\n  });\n\n  const caller = edgeRouter.createCaller(ctx);\n  const result = await caller.workspace.getWorkspace();\n\n  expect(result).toMatchObject({\n    id: 1,\n    slug: \"love-openstatus\",\n    name: \"test\",\n    plan: \"team\",\n    paidUntil: null,\n    stripeId: \"stripeId1\",\n    subscriptionId: \"subscriptionId\",\n    updatedAt: expect.any(Date),\n    createdAt: expect.any(Date),\n    endsAt: null,\n  });\n});\n"
  },
  {
    "path": "packages/api/src/router/workspace.ts",
    "content": "import { z } from \"zod\";\n\nimport { Events } from \"@openstatus/analytics\";\nimport { type SQL, and, eq, isNull } from \"@openstatus/db\";\nimport {\n  monitor,\n  selectWorkspaceSchema,\n  usersToWorkspaces,\n  workspace,\n} from \"@openstatus/db/src/schema\";\n\nimport { createTRPCRouter, protectedProcedure } from \"../trpc\";\n\nexport const workspaceRouter = createTRPCRouter({\n  getWorkspace: protectedProcedure.query(async (opts) => {\n    const result = await opts.ctx.db.query.workspace.findFirst({\n      where: eq(workspace.id, opts.ctx.workspace.id),\n    });\n\n    return selectWorkspaceSchema.parse(result);\n  }),\n\n  get: protectedProcedure.query(async (opts) => {\n    const whereConditions: SQL[] = [eq(workspace.id, opts.ctx.workspace.id)];\n\n    const result = await opts.ctx.db.query.workspace.findFirst({\n      where: and(...whereConditions),\n      with: {\n        pages: {\n          with: {\n            pageComponents: true,\n          },\n        },\n        monitors: {\n          where: isNull(monitor.deletedAt),\n        },\n        notifications: true,\n      },\n    });\n\n    return selectWorkspaceSchema.parse({\n      ...result,\n      usage: {\n        monitors: result?.monitors?.length || 0,\n        notifications: result?.notifications?.length || 0,\n        pages: result?.pages?.length || 0,\n        pageComponents:\n          result?.pages?.flatMap((page) => page.pageComponents)?.length || 0,\n        // checks: result?.checks?.length || 0,\n        checks: 0,\n      },\n    });\n  }),\n\n  list: protectedProcedure.query(async (opts) => {\n    const result = await opts.ctx.db.query.usersToWorkspaces.findMany({\n      where: eq(usersToWorkspaces.userId, opts.ctx.user.id),\n      with: {\n        workspace: true,\n      },\n    });\n\n    return selectWorkspaceSchema\n      .array()\n      .parse(result.map(({ workspace }) => workspace));\n  }),\n\n  updateName: protectedProcedure\n    .meta({ track: Events.UpdateWorkspace })\n    .input(z.object({ name: z.string() }))\n    .mutation(async (opts) => {\n      const whereConditions: SQL[] = [eq(workspace.id, opts.ctx.workspace.id)];\n\n      await opts.ctx.db\n        .update(workspace)\n        .set({ name: opts.input.name, updatedAt: new Date() })\n        .where(and(...whereConditions));\n    }),\n});\n"
  },
  {
    "path": "packages/api/src/service/apiKey.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\n\nimport { db, eq } from \"@openstatus/db\";\nimport { apiKey } from \"@openstatus/db/src/schema\";\nimport { verifyApiKeyHash } from \"@openstatus/db/src/utils/api-key\";\n\nimport {\n  createApiKey,\n  getApiKeys,\n  revokeApiKey,\n  updateLastUsed,\n  verifyApiKey,\n} from \"./apiKey\";\n\n// Test data setup\nlet testWorkspaceId: number;\nlet testUserId: number;\nlet testApiKeyId: number;\nlet testToken: string;\n\nbeforeAll(async () => {\n  // Clean up any existing test data\n  await db.delete(apiKey).where(eq(apiKey.name, \"Test API Key\"));\n  await db.delete(apiKey).where(eq(apiKey.name, \"Test Key with Description\"));\n  await db.delete(apiKey).where(eq(apiKey.name, \"Test Key with Expiration\"));\n\n  // Use existing test workspace and user from seed data\n  testWorkspaceId = 1;\n  testUserId = 1;\n});\n\nafterAll(async () => {\n  // Clean up test data\n  await db.delete(apiKey).where(eq(apiKey.name, \"Test API Key\"));\n  await db.delete(apiKey).where(eq(apiKey.name, \"Test Key with Description\"));\n  await db.delete(apiKey).where(eq(apiKey.name, \"Test Key with Expiration\"));\n});\n\ndescribe(\"createApiKey\", () => {\n  test(\"should create API key with minimal parameters\", async () => {\n    const result = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Test API Key\",\n    );\n\n    expect(result).toBeDefined();\n    expect(result.token).toMatch(/^os_[a-f0-9]{32}$/);\n    expect(result.key).toMatchObject({\n      name: \"Test API Key\",\n      workspaceId: testWorkspaceId,\n      createdById: testUserId,\n      description: null,\n      expiresAt: null,\n    });\n    expect(result.key.prefix).toBe(result.token.slice(0, 11));\n    expect(await verifyApiKeyHash(result.token, result.key.hashedToken)).toBe(\n      true,\n    );\n\n    // Save for later tests\n    testApiKeyId = result.key.id;\n    testToken = result.token;\n  });\n\n  test(\"should create API key with description\", async () => {\n    const description = \"This is a test API key for integration testing\";\n    const result = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Test Key with Description\",\n      description,\n    );\n\n    expect(result.key.description).toBe(description);\n  });\n\n  test(\"should create API key with expiration\", async () => {\n    const expiresAt = new Date(Date.now() + 86400000); // 1 day from now\n    const result = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Test Key with Expiration\",\n      undefined,\n      expiresAt,\n    );\n\n    // SQLite stores timestamps with second precision, so compare with tolerance\n    expect(result.key.expiresAt?.getTime()).toBeCloseTo(\n      expiresAt.getTime(),\n      -4,\n    );\n  });\n\n  test(\"should create API key with both description and expiration\", async () => {\n    const description = \"Full featured key\";\n    const expiresAt = new Date(Date.now() + 86400000);\n    const result = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Full Featured Key\",\n      description,\n      expiresAt,\n    );\n\n    expect(result.key).toMatchObject({\n      name: \"Full Featured Key\",\n      description,\n      expiresAt,\n    });\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, result.key.id));\n  });\n\n  test(\"should generate unique tokens\", async () => {\n    const result1 = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Unique Key 1\",\n    );\n    const result2 = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Unique Key 2\",\n    );\n\n    expect(result1.token).not.toBe(result2.token);\n    expect(result1.key.prefix).not.toBe(result2.key.prefix);\n    expect(result1.key.hashedToken).not.toBe(result2.key.hashedToken);\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, result1.key.id));\n    await db.delete(apiKey).where(eq(apiKey.id, result2.key.id));\n  });\n});\n\ndescribe(\"verifyApiKey\", () => {\n  test(\"should verify valid API key\", async () => {\n    const result = await verifyApiKey(testToken);\n\n    expect(result).not.toBeNull();\n    expect(result).toMatchObject({\n      id: testApiKeyId,\n      name: \"Test API Key\",\n      workspaceId: testWorkspaceId,\n      createdById: testUserId,\n    });\n  });\n\n  test(\"should return null for invalid token format\", async () => {\n    const invalidToken = \"os_invalid\";\n    const result = await verifyApiKey(invalidToken);\n\n    expect(result).toBeNull();\n  });\n\n  test(\"should return null for non-existent token\", async () => {\n    const nonExistentToken = `os_${\"a\".repeat(32)}`;\n    const result = await verifyApiKey(nonExistentToken);\n\n    expect(result).toBeNull();\n  });\n\n  test(\"should return null for token with incorrect hash\", async () => {\n    // Create a token with same prefix but different hash\n    const wrongToken = testToken.slice(0, 11) + \"0\".repeat(24);\n    const result = await verifyApiKey(wrongToken);\n\n    expect(result).toBeNull();\n  });\n\n  test(\"should return null for expired token\", async () => {\n    // Create an expired key\n    const expiredDate = new Date(Date.now() - 86400000); // 1 day ago\n    const expiredKey = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Expired Key\",\n      undefined,\n      expiredDate,\n    );\n\n    const result = await verifyApiKey(expiredKey.token);\n\n    expect(result).toBeNull();\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, expiredKey.key.id));\n  });\n\n  test(\"should verify token that expires in the future\", async () => {\n    // Create a key that expires in the future\n    const futureDate = new Date(Date.now() + 86400000); // 1 day from now\n    const futureKey = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Future Expiry Key\",\n      undefined,\n      futureDate,\n    );\n\n    const result = await verifyApiKey(futureKey.token);\n\n    expect(result).not.toBeNull();\n    expect(result?.id).toBe(futureKey.key.id);\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, futureKey.key.id));\n  });\n});\n\ndescribe(\"revokeApiKey\", () => {\n  test(\"should revoke API key successfully\", async () => {\n    // Create a key to revoke\n    const keyToRevoke = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Key to Revoke\",\n    );\n\n    const result = await revokeApiKey(keyToRevoke.key.id, testWorkspaceId);\n\n    expect(result).toBe(true);\n\n    // Verify key is deleted\n    const deletedKey = await db\n      .select()\n      .from(apiKey)\n      .where(eq(apiKey.id, keyToRevoke.key.id))\n      .get();\n\n    expect(deletedKey).toBeUndefined();\n  });\n\n  test(\"should return false for non-existent key\", async () => {\n    const result = await revokeApiKey(999999, testWorkspaceId);\n\n    expect(result).toBe(false);\n  });\n\n  test(\"should return false when workspace ID doesn't match\", async () => {\n    // Create a key\n    const key = await createApiKey(testWorkspaceId, testUserId, \"Test Key\");\n\n    // Try to revoke with wrong workspace ID\n    const result = await revokeApiKey(key.key.id, 999);\n\n    expect(result).toBe(false);\n\n    // Verify key still exists\n    const stillExists = await db\n      .select()\n      .from(apiKey)\n      .where(eq(apiKey.id, key.key.id))\n      .get();\n\n    expect(stillExists).toBeDefined();\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, key.key.id));\n  });\n});\n\ndescribe(\"getApiKeys\", () => {\n  test(\"should get all API keys for a workspace\", async () => {\n    // Create multiple keys\n    const key1 = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Workspace Key 1\",\n    );\n    const key2 = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Workspace Key 2\",\n    );\n    const key3 = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Workspace Key 3\",\n    );\n\n    const keys = await getApiKeys(testWorkspaceId);\n\n    // Should include at least the 3 keys we just created plus the test key from earlier\n    expect(keys.length).toBeGreaterThanOrEqual(4);\n    expect(keys.some((k) => k.name === \"Workspace Key 1\")).toBe(true);\n    expect(keys.some((k) => k.name === \"Workspace Key 2\")).toBe(true);\n    expect(keys.some((k) => k.name === \"Workspace Key 3\")).toBe(true);\n\n    // All keys should belong to the test workspace\n    keys.forEach((key) => {\n      expect(key.workspaceId).toBe(testWorkspaceId);\n    });\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, key1.key.id));\n    await db.delete(apiKey).where(eq(apiKey.id, key2.key.id));\n    await db.delete(apiKey).where(eq(apiKey.id, key3.key.id));\n  });\n\n  test(\"should return empty array for workspace with no keys\", async () => {\n    // Use a non-existent workspace ID\n    const keys = await getApiKeys(999999);\n\n    expect(keys).toEqual([]);\n  });\n\n  test(\"should not include keys from other workspaces\", async () => {\n    // Assuming there might be other workspaces, verify isolation\n    const keys = await getApiKeys(testWorkspaceId);\n\n    keys.forEach((key) => {\n      expect(key.workspaceId).toBe(testWorkspaceId);\n    });\n  });\n});\n\ndescribe(\"updateLastUsed\", () => {\n  test(\"should update lastUsedAt when never used\", async () => {\n    const key = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Never Used Key\",\n    );\n\n    const result = await updateLastUsed(key.key.id, null);\n\n    expect(result).toBe(true);\n\n    // Verify the update\n    const updatedKey = await db\n      .select()\n      .from(apiKey)\n      .where(eq(apiKey.id, key.key.id))\n      .get();\n\n    expect(updatedKey?.lastUsedAt).not.toBeNull();\n    expect(updatedKey?.lastUsedAt).toBeInstanceOf(Date);\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, key.key.id));\n  });\n\n  test(\"should update lastUsedAt when debounce period has passed\", async () => {\n    const key = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Debounce Test Key\",\n    );\n\n    // Set lastUsedAt to 10 minutes ago (beyond 5-minute debounce)\n    const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);\n    await db\n      .update(apiKey)\n      .set({ lastUsedAt: tenMinutesAgo })\n      .where(eq(apiKey.id, key.key.id));\n\n    const result = await updateLastUsed(key.key.id, tenMinutesAgo);\n\n    expect(result).toBe(true);\n\n    // Verify the update\n    const updatedKey = await db\n      .select()\n      .from(apiKey)\n      .where(eq(apiKey.id, key.key.id))\n      .get();\n\n    expect(updatedKey?.lastUsedAt?.getTime()).toBeGreaterThan(\n      tenMinutesAgo.getTime(),\n    );\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, key.key.id));\n  });\n\n  test(\"should not update lastUsedAt within debounce period\", async () => {\n    const key = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Recent Use Key\",\n    );\n\n    // Set lastUsedAt to 2 minutes ago (within 5-minute debounce)\n    const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);\n    await db\n      .update(apiKey)\n      .set({ lastUsedAt: twoMinutesAgo })\n      .where(eq(apiKey.id, key.key.id));\n\n    const result = await updateLastUsed(key.key.id, twoMinutesAgo);\n\n    expect(result).toBe(false);\n\n    // Verify no update occurred (compare with tolerance due to SQLite timestamp precision)\n    const updatedKey = await db\n      .select()\n      .from(apiKey)\n      .where(eq(apiKey.id, key.key.id))\n      .get();\n\n    expect(updatedKey?.lastUsedAt?.getTime()).toBeCloseTo(\n      twoMinutesAgo.getTime(),\n      -4,\n    );\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, key.key.id));\n  });\n\n  test(\"should update at exactly 5 minutes (boundary test)\", async () => {\n    const key = await createApiKey(\n      testWorkspaceId,\n      testUserId,\n      \"Boundary Test Key\",\n    );\n\n    // Set lastUsedAt to exactly 5 minutes and 1ms ago\n    const fiveMinutesAgo = new Date(Date.now() - (5 * 60 * 1000 + 1));\n    await db\n      .update(apiKey)\n      .set({ lastUsedAt: fiveMinutesAgo })\n      .where(eq(apiKey.id, key.key.id));\n\n    const result = await updateLastUsed(key.key.id, fiveMinutesAgo);\n\n    expect(result).toBe(true);\n\n    // Clean up\n    await db.delete(apiKey).where(eq(apiKey.id, key.key.id));\n  });\n});\n"
  },
  {
    "path": "packages/api/src/service/apiKey.ts",
    "content": "import { eq } from \"@openstatus/db\";\nimport { db } from \"@openstatus/db\";\nimport { apiKey } from \"@openstatus/db/src/schema\";\nimport {\n  shouldUpdateLastUsed as checkShouldUpdateLastUsed,\n  generateApiKey as generateKey,\n  verifyApiKeyHash,\n} from \"@openstatus/db/src/utils/api-key\";\n\n/**\n * Creates a new API key for a workspace\n * @param workspaceId - The workspace ID\n * @param createdById - The ID of the user creating the key\n * @param name - The name of the API key\n * @param description - Optional description for the key\n * @param expiresAt - Optional expiration date\n * @returns The full token (only shown once) and the created key details\n */\nexport async function createApiKey(\n  workspaceId: number,\n  createdById: number,\n  name: string,\n  description?: string,\n  expiresAt?: Date,\n): Promise<{ token: string; key: typeof apiKey.$inferSelect }> {\n  const { token, prefix, hash } = await generateKey();\n\n  const [key] = await db\n    .insert(apiKey)\n    .values({\n      name,\n      description,\n      prefix,\n      hashedToken: hash,\n      workspaceId,\n      createdById,\n      expiresAt,\n    })\n    .returning();\n\n  if (!key) {\n    throw new Error(\"Failed to create API key\");\n  }\n\n  return { token, key };\n}\n\n/**\n * Verifies an API key token\n * @param token - The API key token to verify\n * @returns The API key details if valid, null otherwise\n */\nexport async function verifyApiKey(\n  token: string,\n): Promise<typeof apiKey.$inferSelect | null> {\n  // Validate token format before database query\n  if (!/^os_[a-f0-9]{32}$/.test(token)) {\n    return null;\n  }\n\n  // Extract prefix from token\n  const prefix = token.slice(0, 11); // \"os_\" + 8 chars = 11 total\n\n  // Look up key by prefix\n  const key = await db\n    .select()\n    .from(apiKey)\n    .where(eq(apiKey.prefix, prefix))\n    .get();\n\n  if (!key) {\n    return null;\n  }\n\n  // Verify hash using bcrypt-compatible verification\n  if (!(await verifyApiKeyHash(token, key.hashedToken))) {\n    return null;\n  }\n\n  // Check expiration\n  if (key.expiresAt && key.expiresAt < new Date()) {\n    return null;\n  }\n\n  return key;\n}\n\n/**\n * Revokes (deletes) an API key\n * @param id - The API key ID\n * @param workspaceId - The workspace ID for ownership verification\n * @returns True if successfully revoked, false otherwise\n */\nexport async function revokeApiKey(\n  id: number,\n  workspaceId: number,\n): Promise<boolean> {\n  // First, verify the key exists and belongs to the workspace\n  const key = await db.select().from(apiKey).where(eq(apiKey.id, id)).get();\n\n  if (!key || key.workspaceId !== workspaceId) {\n    return false;\n  }\n\n  // Delete the key\n  await db.delete(apiKey).where(eq(apiKey.id, id));\n\n  return true;\n}\n\n/**\n * Gets all API keys for a workspace\n * @param workspaceId - The workspace ID\n * @returns Array of API keys for the workspace\n */\nexport async function getApiKeys(\n  workspaceId: number,\n): Promise<Array<typeof apiKey.$inferSelect>> {\n  const keys = await db\n    .select()\n    .from(apiKey)\n    .where(eq(apiKey.workspaceId, workspaceId))\n    .all();\n\n  return keys;\n}\n\n/**\n * Updates the lastUsedAt timestamp for an API key (with debouncing)\n * @param id - The API key ID\n * @param lastUsedAt - The current lastUsedAt value (or null)\n * @returns True if updated, false if skipped due to debounce\n */\nexport async function updateLastUsed(\n  id: number,\n  lastUsedAt: Date | null,\n): Promise<boolean> {\n  // Check if update is needed (5-minute debounce)\n  if (!checkShouldUpdateLastUsed(lastUsedAt)) {\n    return false;\n  }\n\n  await db\n    .update(apiKey)\n    .set({ lastUsedAt: new Date() })\n    .where(eq(apiKey.id, id));\n\n  return true;\n}\n"
  },
  {
    "path": "packages/api/src/service/import.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\";\nimport { allPlans } from \"@openstatus/db/src/schema/plan/config\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport type { ImportSummary } from \"@openstatus/importers\";\nimport { addLimitWarnings } from \"./import\";\n\nfunction makeSummary(overrides?: Partial<ImportSummary>): ImportSummary {\n  return {\n    provider: \"statuspage\",\n    status: \"completed\",\n    startedAt: new Date(),\n    completedAt: new Date(),\n    phases: [],\n    errors: [],\n    ...overrides,\n  };\n}\n\nfunction makeLimits(overrides?: Partial<Limits>): Limits {\n  return { ...allPlans.free.limits, ...overrides };\n}\n\ndescribe(\"addLimitWarnings\", () => {\n  // -------------------------------------------------------------------------\n  // Component limits\n  // -------------------------------------------------------------------------\n  describe(\"component limits\", () => {\n    test(\"no warning when under limit\", async () => {\n      const summary = makeSummary({\n        phases: [\n          {\n            phase: \"components\",\n            status: \"completed\",\n            resources: [\n              { sourceId: \"1\", name: \"A\", status: \"created\" },\n              { sourceId: \"2\", name: \"B\", status: \"created\" },\n            ],\n          },\n        ],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"page-components\": 20 }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors).toEqual([]);\n    });\n\n    test(\"no warning when exactly at limit\", async () => {\n      const summary = makeSummary({\n        phases: [\n          {\n            phase: \"components\",\n            status: \"completed\",\n            resources: [\n              { sourceId: \"1\", name: \"A\", status: \"created\" },\n              { sourceId: \"2\", name: \"B\", status: \"created\" },\n              { sourceId: \"3\", name: \"C\", status: \"created\" },\n            ],\n          },\n        ],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"page-components\": 3 }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors).toEqual([]);\n    });\n\n    test(\"warns when import exceeds limit (no existing components)\", async () => {\n      const summary = makeSummary({\n        phases: [\n          {\n            phase: \"components\",\n            status: \"completed\",\n            resources: [\n              { sourceId: \"1\", name: \"A\", status: \"created\" },\n              { sourceId: \"2\", name: \"B\", status: \"created\" },\n              { sourceId: \"3\", name: \"C\", status: \"created\" },\n              { sourceId: \"4\", name: \"D\", status: \"created\" },\n            ],\n          },\n        ],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"page-components\": 3 }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors.length).toBe(1);\n      expect(summary.errors[0]).toContain(\"3 of 4\");\n    });\n\n    test(\"no warning when components phase is empty\", async () => {\n      const summary = makeSummary({\n        phases: [{ phase: \"components\", status: \"completed\", resources: [] }],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"page-components\": 3 }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors).toEqual([]);\n    });\n\n    test(\"no warning when components phase is missing\", async () => {\n      const summary = makeSummary({\n        phases: [{ phase: \"page\", status: \"completed\", resources: [] }],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"page-components\": 3 }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors).toEqual([]);\n    });\n  });\n\n  // -------------------------------------------------------------------------\n  // Custom domain\n  // -------------------------------------------------------------------------\n  describe(\"custom domain\", () => {\n    test(\"warns when custom domain present on free plan\", async () => {\n      const summary = makeSummary({\n        phases: [\n          {\n            phase: \"page\",\n            status: \"completed\",\n            resources: [\n              {\n                sourceId: \"p1\",\n                name: \"My Page\",\n                status: \"created\",\n                data: { customDomain: \"status.example.com\" },\n              },\n            ],\n          },\n        ],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"custom-domain\": false }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors.length).toBe(1);\n      expect(summary.errors[0]).toContain(\"Custom domain\");\n    });\n\n    test(\"no warning when custom domain is empty\", async () => {\n      const summary = makeSummary({\n        phases: [\n          {\n            phase: \"page\",\n            status: \"completed\",\n            resources: [\n              {\n                sourceId: \"p1\",\n                name: \"My Page\",\n                status: \"created\",\n                data: { customDomain: \"\" },\n              },\n            ],\n          },\n        ],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"custom-domain\": false }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors).toEqual([]);\n    });\n\n    test(\"no warning on paid plan with custom domain\", async () => {\n      const summary = makeSummary({\n        phases: [\n          {\n            phase: \"page\",\n            status: \"completed\",\n            resources: [\n              {\n                sourceId: \"p1\",\n                name: \"My Page\",\n                status: \"created\",\n                data: { customDomain: \"status.example.com\" },\n              },\n            ],\n          },\n        ],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"custom-domain\": true }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors).toEqual([]);\n    });\n  });\n\n  // -------------------------------------------------------------------------\n  // Subscribers\n  // -------------------------------------------------------------------------\n  describe(\"subscribers\", () => {\n    test(\"warns when subscribers present on free plan\", async () => {\n      const summary = makeSummary({\n        phases: [\n          {\n            phase: \"subscribers\",\n            status: \"completed\",\n            resources: [\n              { sourceId: \"s1\", name: \"alice@test.com\", status: \"created\" },\n              { sourceId: \"s2\", name: \"bob@test.com\", status: \"created\" },\n            ],\n          },\n        ],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"status-subscribers\": false }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors.length).toBe(1);\n      expect(summary.errors[0]).toContain(\"Subscribers\");\n    });\n\n    test(\"no warning when subscribers phase is empty\", async () => {\n      const summary = makeSummary({\n        phases: [{ phase: \"subscribers\", status: \"completed\", resources: [] }],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"status-subscribers\": false }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors).toEqual([]);\n    });\n\n    test(\"no warning on paid plan with subscribers\", async () => {\n      const summary = makeSummary({\n        phases: [\n          {\n            phase: \"subscribers\",\n            status: \"completed\",\n            resources: [\n              { sourceId: \"s1\", name: \"alice@test.com\", status: \"created\" },\n            ],\n          },\n        ],\n      });\n\n      await addLimitWarnings(summary, {\n        limits: makeLimits({ \"status-subscribers\": true }),\n        workspaceId: 1,\n      });\n\n      expect(summary.errors).toEqual([]);\n    });\n  });\n\n  // -------------------------------------------------------------------------\n  // Combined\n  // -------------------------------------------------------------------------\n  test(\"reports multiple warnings at once\", async () => {\n    const summary = makeSummary({\n      phases: [\n        {\n          phase: \"page\",\n          status: \"completed\",\n          resources: [\n            {\n              sourceId: \"p1\",\n              name: \"Page\",\n              status: \"created\",\n              data: { customDomain: \"status.example.com\" },\n            },\n          ],\n        },\n        {\n          phase: \"components\",\n          status: \"completed\",\n          resources: [\n            { sourceId: \"1\", name: \"A\", status: \"created\" },\n            { sourceId: \"2\", name: \"B\", status: \"created\" },\n            { sourceId: \"3\", name: \"C\", status: \"created\" },\n            { sourceId: \"4\", name: \"D\", status: \"created\" },\n          ],\n        },\n        {\n          phase: \"subscribers\",\n          status: \"completed\",\n          resources: [\n            { sourceId: \"s1\", name: \"alice@test.com\", status: \"created\" },\n          ],\n        },\n      ],\n    });\n\n    // Free plan: page-components=3, custom-domain=false, status-subscribers=false\n    await addLimitWarnings(summary, {\n      limits: makeLimits(),\n      workspaceId: 1,\n    });\n\n    expect(summary.errors.length).toBe(3);\n    expect(summary.errors.some((e) => e.includes(\"3 of 4\"))).toBe(true);\n    expect(summary.errors.some((e) => e.includes(\"Custom domain\"))).toBe(true);\n    expect(summary.errors.some((e) => e.includes(\"Subscribers\"))).toBe(true);\n  });\n\n  test(\"no warnings on starter plan within limits\", async () => {\n    const summary = makeSummary({\n      phases: [\n        {\n          phase: \"page\",\n          status: \"completed\",\n          resources: [\n            {\n              sourceId: \"p1\",\n              name: \"Page\",\n              status: \"created\",\n              data: { customDomain: \"status.example.com\" },\n            },\n          ],\n        },\n        {\n          phase: \"components\",\n          status: \"completed\",\n          resources: [\n            { sourceId: \"1\", name: \"A\", status: \"created\" },\n            { sourceId: \"2\", name: \"B\", status: \"created\" },\n            { sourceId: \"3\", name: \"C\", status: \"created\" },\n            { sourceId: \"4\", name: \"D\", status: \"created\" },\n          ],\n        },\n        {\n          phase: \"subscribers\",\n          status: \"completed\",\n          resources: [\n            { sourceId: \"s1\", name: \"alice@test.com\", status: \"created\" },\n          ],\n        },\n      ],\n    });\n\n    await addLimitWarnings(summary, {\n      limits: { ...allPlans.starter.limits },\n      workspaceId: 1,\n    });\n\n    expect(summary.errors).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/api/src/service/import.ts",
    "content": "import { and, db, eq } from \"@openstatus/db\";\nimport {\n  maintenance,\n  maintenancesToPageComponents,\n  page,\n  pageComponent,\n  pageComponentGroup,\n  pageSubscriber,\n  pageSubscriberToPageComponent,\n  statusReport,\n  statusReportUpdate,\n  statusReportsToPageComponents,\n} from \"@openstatus/db/src/schema\";\nimport type { Limits } from \"@openstatus/db/src/schema/plan/schema\";\nimport type {\n  ImportSummary,\n  PhaseResult,\n  ResourceResult,\n} from \"@openstatus/importers\";\nimport { createStatuspageProvider } from \"@openstatus/importers/statuspage\";\nimport { TRPCError } from \"@trpc/server\";\n\ntype ImportOptions = {\n  includeStatusReports?: boolean;\n  includeSubscribers?: boolean;\n  includeComponents?: boolean;\n};\n\n/**\n * Inspect an ImportSummary and push warnings into `summary.errors`\n * for any limits that would be hit during import.\n *\n * Used by both preview (to show warnings upfront) and run (before writes).\n */\nexport async function addLimitWarnings(\n  summary: ImportSummary,\n  config: {\n    limits: Limits;\n    workspaceId: number;\n    pageId?: number;\n  },\n): Promise<void> {\n  // 1. Component count limit\n  const componentsPhase = summary.phases.find((p) => p.phase === \"components\");\n  if (componentsPhase && componentsPhase.resources.length > 0) {\n    const maxComponents = config.limits[\"page-components\"];\n    let existingCount = 0;\n    if (config.pageId) {\n      const existing = await db\n        .select()\n        .from(pageComponent)\n        .where(\n          and(\n            eq(pageComponent.pageId, config.pageId),\n            eq(pageComponent.workspaceId, config.workspaceId),\n          ),\n        )\n        .all();\n      existingCount = existing.length;\n    }\n    const remaining = maxComponents - existingCount;\n    if (remaining <= 0) {\n      summary.errors.push(\n        `Component limit reached (${maxComponents}). Upgrade your plan to import components.`,\n      );\n    } else if (componentsPhase.resources.length > remaining) {\n      summary.errors.push(\n        `Only ${remaining} of ${componentsPhase.resources.length} components can be imported due to plan limit (${maxComponents}).`,\n      );\n    }\n  }\n\n  // 2. Custom domain\n  if (!config.limits[\"custom-domain\"]) {\n    const pagePhase = summary.phases.find((p) => p.phase === \"page\");\n    const pageData = pagePhase?.resources[0]?.data as\n      | { customDomain?: string }\n      | undefined;\n    if (pageData?.customDomain) {\n      summary.errors.push(\n        \"Custom domain will be stripped during import. Upgrade your plan to use custom domains.\",\n      );\n    }\n  }\n\n  // 3. Subscribers\n  if (!config.limits[\"status-subscribers\"]) {\n    const subscribersPhase = summary.phases.find(\n      (p) => p.phase === \"subscribers\",\n    );\n    if (subscribersPhase && subscribersPhase.resources.length > 0) {\n      summary.errors.push(\n        \"Subscribers cannot be imported on your current plan. Upgrade to enable status page subscribers.\",\n      );\n    }\n  }\n}\n\nexport async function previewImport(config: {\n  apiKey: string;\n  statuspagePageId?: string;\n  workspaceId: number;\n  pageId?: number;\n  limits: Limits;\n}): Promise<ImportSummary> {\n  const provider = createStatuspageProvider();\n\n  const validation = await provider.validate({\n    ...config,\n    dryRun: true,\n  });\n  if (!validation.valid) {\n    throw new TRPCError({\n      code: \"BAD_REQUEST\",\n      message: `Provider validation failed: ${validation.error}`,\n    });\n  }\n\n  const summary = await provider.run({ ...config, dryRun: true });\n  await addLimitWarnings(summary, config);\n  return summary;\n}\n\nexport async function runImport(config: {\n  apiKey: string;\n  statuspagePageId?: string;\n  workspaceId: number;\n  pageId?: number;\n  options?: ImportOptions;\n  limits: Limits;\n}): Promise<ImportSummary> {\n  const provider = createStatuspageProvider();\n\n  const validation = await provider.validate(config);\n  if (!validation.valid) {\n    throw new TRPCError({\n      code: \"BAD_REQUEST\",\n      message: `Provider validation failed: ${validation.error}`,\n    });\n  }\n\n  // Fetch and map all data\n  const summary = await provider.run(config);\n\n  // Add limit warnings (same as preview)\n  await addLimitWarnings(summary, config);\n\n  // Now write to DB phase by phase\n  const idMaps = {\n    groups: new Map<string, number>(), // sourceId -> openstatusId\n    components: new Map<string, number>(), // sourceId -> openstatusId\n  };\n\n  let targetPageId = config.pageId;\n  let phaseAborted = false;\n\n  for (const phase of summary.phases) {\n    if (phaseAborted) {\n      phase.status = \"skipped\";\n      continue;\n    }\n\n    try {\n      switch (phase.phase) {\n        case \"page\":\n          targetPageId = await writePagePhase(\n            phase,\n            config.workspaceId,\n            config.pageId,\n            config.limits,\n          );\n          break;\n        case \"componentGroups\":\n          if (targetPageId && config.options?.includeComponents !== false) {\n            await writeComponentGroupsPhase(\n              phase,\n              config.workspaceId,\n              targetPageId,\n              idMaps.groups,\n            );\n          } else if (config.options?.includeComponents === false) {\n            phase.status = \"skipped\";\n          }\n          break;\n        case \"components\":\n          if (targetPageId && config.options?.includeComponents !== false) {\n            // Check page-components limit\n            const existingCount = await db\n              .select()\n              .from(pageComponent)\n              .where(\n                and(\n                  eq(pageComponent.pageId, targetPageId),\n                  eq(pageComponent.workspaceId, config.workspaceId),\n                ),\n              )\n              .all();\n            const maxComponents = config.limits[\"page-components\"];\n            const remaining = maxComponents - existingCount.length;\n            if (remaining <= 0) {\n              phase.status = \"failed\";\n              break;\n            }\n            if (phase.resources.length > remaining) {\n              // Trim resources to fit within limit\n              const skipped = phase.resources.splice(remaining);\n              for (const r of skipped) {\n                r.status = \"skipped\";\n                r.error = `Skipped: would exceed component limit (${maxComponents})`;\n              }\n              phase.resources.push(...skipped);\n            }\n            await writeComponentsPhase(\n              phase,\n              config.workspaceId,\n              targetPageId,\n              idMaps.groups,\n              idMaps.components,\n            );\n          } else if (config.options?.includeComponents === false) {\n            phase.status = \"skipped\";\n          }\n          break;\n        case \"incidents\":\n          if (targetPageId && config.options?.includeStatusReports !== false) {\n            await writeIncidentsPhase(\n              phase,\n              config.workspaceId,\n              targetPageId,\n              idMaps.components,\n            );\n          } else if (config.options?.includeStatusReports === false) {\n            phase.status = \"skipped\";\n          }\n          break;\n        case \"maintenances\":\n          if (targetPageId && config.options?.includeStatusReports !== false) {\n            await writeMaintenancesPhase(\n              phase,\n              config.workspaceId,\n              targetPageId,\n              idMaps.components,\n            );\n          } else if (config.options?.includeStatusReports === false) {\n            phase.status = \"skipped\";\n          }\n          break;\n        case \"subscribers\":\n          if (targetPageId && config.options?.includeSubscribers) {\n            if (!config.limits[\"status-subscribers\"]) {\n              phase.status = \"skipped\";\n              break;\n            }\n            await writeSubscribersPhase(phase, targetPageId, idMaps.components);\n          } else {\n            phase.status = \"skipped\";\n          }\n          break;\n      }\n    } catch (err) {\n      const msg = err instanceof Error ? err.message : String(err);\n      summary.errors.push(`Phase \"${phase.phase}\" failed: ${msg}`);\n      phase.status = \"failed\";\n      phaseAborted = true;\n    }\n  }\n\n  // Compute overall status\n  const hasFailures = summary.phases.some((p) => p.status === \"failed\");\n  const hasPartial = summary.phases.some((p) => p.status === \"partial\");\n  const allSkippedOrCompleted = summary.phases.every(\n    (p) => p.status === \"completed\" || p.status === \"skipped\",\n  );\n\n  summary.status = hasFailures\n    ? \"failed\"\n    : hasPartial\n      ? \"partial\"\n      : allSkippedOrCompleted\n        ? \"completed\"\n        : \"partial\";\n  summary.completedAt = new Date();\n\n  return summary;\n}\n\n// ---------------------------------------------------------------------------\n// Phase writers\n// ---------------------------------------------------------------------------\n\nfunction computePhaseStatus(\n  resources: ResourceResult[],\n): PhaseResult[\"status\"] {\n  if (resources.length === 0) return \"completed\";\n  const allFailed = resources.every((r) => r.status === \"failed\");\n  if (allFailed) return \"failed\";\n  const hasFailed = resources.some((r) => r.status === \"failed\");\n  if (hasFailed) return \"partial\";\n  return \"completed\";\n}\n\nasync function writePagePhase(\n  phase: PhaseResult,\n  workspaceId: number,\n  existingPageId?: number,\n  limits?: Limits,\n): Promise<number> {\n  const resource = phase.resources[0];\n  if (!resource?.data) {\n    throw new Error(\"No page data found in phase\");\n  }\n\n  const data = resource.data as {\n    workspaceId: number;\n    title: string;\n    description: string;\n    slug: string;\n    customDomain: string;\n    published: boolean;\n    icon: string;\n  };\n\n  // Strip custom domain if not allowed by plan\n  if (limits && !limits[\"custom-domain\"]) {\n    data.customDomain = \"\";\n  }\n\n  // If a page ID was provided, verify and update it\n  if (existingPageId) {\n    const existing = await db\n      .select()\n      .from(page)\n      .where(\n        and(eq(page.id, existingPageId), eq(page.workspaceId, workspaceId)),\n      )\n      .get();\n\n    if (!existing) {\n      throw new Error(\n        \"Provided page not found or does not belong to workspace\",\n      );\n    }\n\n    await db\n      .update(page)\n      .set({ title: data.title, description: data.description })\n      .where(eq(page.id, existingPageId));\n\n    resource.openstatusId = existingPageId;\n    resource.status = \"skipped\";\n    phase.status = \"completed\";\n    return existingPageId;\n  }\n\n  // Check idempotency by slug (scoped to workspace)\n  const existingBySlug = await db\n    .select()\n    .from(page)\n    .where(and(eq(page.slug, data.slug), eq(page.workspaceId, workspaceId)))\n    .get();\n\n  if (existingBySlug) {\n    resource.openstatusId = existingBySlug.id;\n    resource.status = \"skipped\";\n    phase.status = \"completed\";\n    return existingBySlug.id;\n  }\n\n  // Insert new page\n  const [inserted] = await db\n    .insert(page)\n    .values({\n      workspaceId: data.workspaceId,\n      title: data.title,\n      description: data.description,\n      slug: data.slug,\n      customDomain: data.customDomain,\n      published: data.published,\n      icon: data.icon,\n    })\n    .returning({ id: page.id });\n\n  if (!inserted) {\n    throw new Error(\"Failed to insert page\");\n  }\n\n  resource.openstatusId = inserted.id;\n  resource.status = \"created\";\n  phase.status = \"completed\";\n  return inserted.id;\n}\n\nasync function writeComponentGroupsPhase(\n  phase: PhaseResult,\n  workspaceId: number,\n  pageId: number,\n  groupIdMap: Map<string, number>,\n): Promise<void> {\n  for (const resource of phase.resources) {\n    try {\n      const data = resource.data as {\n        workspaceId: number;\n        pageId: number;\n        name: string;\n      };\n\n      // Check idempotency by name + pageId\n      const existing = await db\n        .select()\n        .from(pageComponentGroup)\n        .where(\n          and(\n            eq(pageComponentGroup.name, data.name),\n            eq(pageComponentGroup.pageId, pageId),\n          ),\n        )\n        .get();\n\n      if (existing) {\n        groupIdMap.set(resource.sourceId, existing.id);\n        resource.openstatusId = existing.id;\n        resource.status = \"skipped\";\n        continue;\n      }\n\n      const [inserted] = await db\n        .insert(pageComponentGroup)\n        .values({\n          workspaceId,\n          pageId,\n          name: data.name,\n        })\n        .returning({ id: pageComponentGroup.id });\n\n      if (!inserted) {\n        resource.status = \"failed\";\n        resource.error = \"Insert returned no result\";\n        continue;\n      }\n\n      groupIdMap.set(resource.sourceId, inserted.id);\n      resource.openstatusId = inserted.id;\n      resource.status = \"created\";\n    } catch (err) {\n      resource.status = \"failed\";\n      resource.error = err instanceof Error ? err.message : String(err);\n    }\n  }\n\n  phase.status = computePhaseStatus(phase.resources);\n}\n\nasync function writeComponentsPhase(\n  phase: PhaseResult,\n  workspaceId: number,\n  pageId: number,\n  groupIdMap: Map<string, number>,\n  componentIdMap: Map<string, number>,\n): Promise<void> {\n  for (const resource of phase.resources) {\n    if (resource.status === \"skipped\") continue;\n\n    try {\n      const data = resource.data as {\n        workspaceId: number;\n        pageId: number;\n        type: \"static\";\n        monitorId: null;\n        name: string;\n        description: string | null;\n        order: number;\n        sourceGroupId: string | null;\n      };\n\n      // Check idempotency by name + pageId\n      const existing = await db\n        .select()\n        .from(pageComponent)\n        .where(\n          and(\n            eq(pageComponent.name, data.name),\n            eq(pageComponent.pageId, pageId),\n          ),\n        )\n        .get();\n\n      if (existing) {\n        componentIdMap.set(resource.sourceId, existing.id);\n        resource.openstatusId = existing.id;\n        resource.status = \"skipped\";\n        continue;\n      }\n\n      // Resolve group ID from source group ID\n      const resolvedGroupId = data.sourceGroupId\n        ? groupIdMap.get(data.sourceGroupId) ?? null\n        : null;\n\n      const [inserted] = await db\n        .insert(pageComponent)\n        .values({\n          workspaceId,\n          pageId,\n          type: data.type,\n          monitorId: data.monitorId,\n          name: data.name,\n          description: data.description,\n          order: data.order,\n          groupId: resolvedGroupId,\n        })\n        .returning({ id: pageComponent.id });\n\n      if (!inserted) {\n        resource.status = \"failed\";\n        resource.error = \"Insert returned no result\";\n        continue;\n      }\n\n      componentIdMap.set(resource.sourceId, inserted.id);\n      resource.openstatusId = inserted.id;\n      resource.status = \"created\";\n    } catch (err) {\n      resource.status = \"failed\";\n      resource.error = err instanceof Error ? err.message : String(err);\n    }\n  }\n\n  phase.status = computePhaseStatus(phase.resources);\n}\n\nasync function writeIncidentsPhase(\n  phase: PhaseResult,\n  workspaceId: number,\n  pageId: number,\n  componentIdMap: Map<string, number>,\n): Promise<void> {\n  for (const resource of phase.resources) {\n    try {\n      const data = resource.data as {\n        report: {\n          title: string;\n          status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\";\n          workspaceId: number;\n          pageId: number;\n        };\n        updates: Array<{\n          status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\";\n          message: string;\n          date: Date;\n        }>;\n        sourceComponentIds: string[];\n      };\n\n      // Insert status report\n      const [insertedReport] = await db\n        .insert(statusReport)\n        .values({\n          title: data.report.title,\n          status: data.report.status,\n          workspaceId,\n          pageId,\n        })\n        .returning({ id: statusReport.id });\n\n      if (!insertedReport) {\n        resource.status = \"failed\";\n        resource.error = \"Failed to insert status report\";\n        continue;\n      }\n\n      // Insert status report updates\n      if (data.updates.length > 0) {\n        await db.insert(statusReportUpdate).values(\n          data.updates.map((u) => ({\n            status: u.status,\n            message: u.message,\n            date: u.date,\n            statusReportId: insertedReport.id,\n          })),\n        );\n      }\n\n      // Link to page components\n      const componentLinks: Array<{\n        statusReportId: number;\n        pageComponentId: number;\n      }> = [];\n      for (const sourceCompId of data.sourceComponentIds) {\n        const osCompId = componentIdMap.get(sourceCompId);\n        if (osCompId) {\n          componentLinks.push({\n            statusReportId: insertedReport.id,\n            pageComponentId: osCompId,\n          });\n        }\n      }\n      if (componentLinks.length > 0) {\n        await db.insert(statusReportsToPageComponents).values(componentLinks);\n      }\n\n      resource.openstatusId = insertedReport.id;\n      resource.status = \"created\";\n    } catch (err) {\n      resource.status = \"failed\";\n      resource.error = err instanceof Error ? err.message : String(err);\n    }\n  }\n\n  phase.status = computePhaseStatus(phase.resources);\n}\n\nasync function writeMaintenancesPhase(\n  phase: PhaseResult,\n  workspaceId: number,\n  pageId: number,\n  componentIdMap: Map<string, number>,\n): Promise<void> {\n  for (const resource of phase.resources) {\n    try {\n      const data = resource.data as {\n        title: string;\n        message: string;\n        from: Date;\n        to: Date;\n        workspaceId: number;\n        pageId: number;\n        sourceComponentIds: string[];\n      };\n\n      // Insert maintenance\n      const [inserted] = await db\n        .insert(maintenance)\n        .values({\n          title: data.title,\n          message: data.message,\n          from: data.from,\n          to: data.to,\n          workspaceId,\n          pageId,\n        })\n        .returning({ id: maintenance.id });\n\n      if (!inserted) {\n        resource.status = \"failed\";\n        resource.error = \"Failed to insert maintenance\";\n        continue;\n      }\n\n      // Link to page components\n      const componentLinks: Array<{\n        maintenanceId: number;\n        pageComponentId: number;\n      }> = [];\n      for (const sourceCompId of data.sourceComponentIds) {\n        const osCompId = componentIdMap.get(sourceCompId);\n        if (osCompId) {\n          componentLinks.push({\n            maintenanceId: inserted.id,\n            pageComponentId: osCompId,\n          });\n        }\n      }\n      if (componentLinks.length > 0) {\n        await db.insert(maintenancesToPageComponents).values(componentLinks);\n      }\n\n      resource.openstatusId = inserted.id;\n      resource.status = \"created\";\n    } catch (err) {\n      resource.status = \"failed\";\n      resource.error = err instanceof Error ? err.message : String(err);\n    }\n  }\n\n  phase.status = computePhaseStatus(phase.resources);\n}\n\n// TODO: migrate to new `pageSubscription` + `pageSubscriptionToPageComponent` tables\nasync function writeSubscribersPhase(\n  phase: PhaseResult,\n  pageId: number,\n  componentIdMap: Map<string, number>,\n): Promise<void> {\n  for (const resource of phase.resources) {\n    try {\n      const data = resource.data as {\n        email: string;\n        pageId: number;\n        sourceComponentIds: string[];\n      };\n\n      // Idempotency check by email + pageId\n      const existing = await db\n        .select()\n        .from(pageSubscriber)\n        .where(\n          and(\n            eq(pageSubscriber.email, data.email),\n            eq(pageSubscriber.pageId, pageId),\n            eq(pageSubscriber.channelType, \"email\"),\n          ),\n        )\n        .get();\n\n      if (existing) {\n        resource.openstatusId = existing.id;\n        resource.status = \"skipped\";\n        continue;\n      }\n\n      const [inserted] = await db\n        .insert(pageSubscriber)\n        .values({\n          email: data.email,\n          pageId,\n          channelType: \"email\",\n        })\n        .returning({ id: pageSubscriber.id });\n\n      if (!inserted) {\n        resource.status = \"failed\";\n        resource.error = \"Insert returned no result\";\n        continue;\n      }\n\n      // Link to page components\n      const componentLinks: Array<{\n        pageSubscriberId: number;\n        pageComponentId: number;\n      }> = [];\n      for (const sourceCompId of data.sourceComponentIds) {\n        const osCompId = componentIdMap.get(sourceCompId);\n        if (osCompId) {\n          componentLinks.push({\n            pageSubscriberId: inserted.id,\n            pageComponentId: osCompId,\n          });\n        }\n      }\n      if (componentLinks.length > 0) {\n        await db.insert(pageSubscriberToPageComponent).values(componentLinks);\n      }\n\n      resource.openstatusId = inserted.id;\n      resource.status = \"created\";\n    } catch (err) {\n      resource.status = \"failed\";\n      resource.error = err instanceof Error ? err.message : String(err);\n    }\n  }\n\n  phase.status = computePhaseStatus(phase.resources);\n}\n"
  },
  {
    "path": "packages/api/src/service/telegram-updates.ts",
    "content": "import type { redis } from \"@openstatus/upstash\";\n\n// ---- Telegram API Types -----------------------------------------------------\n\ninterface TelegramUser {\n  id: number;\n  is_bot: boolean;\n  first_name: string;\n  username?: string;\n  language_code?: string;\n}\n\ninterface TelegramChat {\n  id: number;\n  type: \"private\" | \"group\" | \"supergroup\" | \"channel\";\n  title?: string;\n  first_name?: string;\n  all_members_are_administrators?: boolean;\n}\n\ninterface TelegramMessage {\n  message_id: number;\n  from: TelegramUser;\n  chat: TelegramChat;\n  date: number;\n  text?: string;\n  entities?: Array<{\n    offset: number;\n    length: number;\n    type: string;\n  }>;\n  new_chat_members?: TelegramUser[];\n  new_chat_member?: TelegramUser;\n  new_chat_participant?: TelegramUser;\n}\n\ninterface TelegramChatMember {\n  user: TelegramUser;\n  status:\n    | \"member\"\n    | \"administrator\"\n    | \"left\"\n    | \"creator\"\n    | \"restricted\"\n    | \"kicked\";\n  can_be_edited?: boolean;\n  can_manage_chat?: boolean;\n  can_change_info?: boolean;\n  can_delete_messages?: boolean;\n  can_invite_users?: boolean;\n  can_restrict_members?: boolean;\n  can_pin_messages?: boolean;\n  can_manage_topics?: boolean;\n  can_promote_members?: boolean;\n  can_manage_video_chats?: boolean;\n  can_post_stories?: boolean;\n  can_edit_stories?: boolean;\n  can_delete_stories?: boolean;\n  is_anonymous?: boolean;\n}\n\ninterface TelegramMyChatMemberUpdate {\n  chat: TelegramChat;\n  from: TelegramUser;\n  date: number;\n  old_chat_member: TelegramChatMember;\n  new_chat_member: TelegramChatMember;\n}\n\nexport interface TelegramUpdate {\n  update_id: number;\n  message?: TelegramMessage;\n  my_chat_member?: TelegramMyChatMemberUpdate;\n}\n\nexport interface TelegramGetUpdatesResponse {\n  ok: boolean;\n  result: TelegramUpdate[];\n}\n\nexport type ValidUpdate =\n  | {\n      chatId: string;\n      chatType: \"private\";\n      user: { id: number; first_name: string; username?: string };\n    }\n  | {\n      chatId: string;\n      chatType: \"group\";\n      chatTitle?: string;\n      user: { id: number; first_name: string; username?: string };\n    };\n\n// ---- Helpers ----------------------------------------------------------------\n\nfunction extractPrivateChatStart(\n  update: TelegramUpdate,\n  token: string,\n): {\n  chatId: string;\n  user: { id: number; first_name: string; username?: string };\n} | null {\n  const { message } = update;\n\n  if (\n    !message ||\n    message.chat.type !== \"private\" ||\n    !message.text?.startsWith(\"/start \") ||\n    !message.from\n  ) {\n    return null;\n  }\n\n  const receivedToken = message.text.split(\" \")[1];\n  if (!receivedToken || receivedToken !== token) return null;\n\n  return {\n    chatId: String(message.chat.id),\n    user: {\n      id: message.from.id,\n      first_name: message.from.first_name,\n      username: message.from.username,\n    },\n  };\n}\n\nfunction extractGroupBotAddition(\n  update: TelegramUpdate,\n  privateChatId: string,\n  botUsername: string,\n): {\n  chatId: string;\n  chatTitle?: string;\n  user: { id: number; first_name: string; username?: string };\n} | null {\n  // Modern Telegram Bot API (v5.0+): bot additions come as my_chat_member updates\n  if (update.my_chat_member) {\n    const { my_chat_member } = update;\n\n    const isInvalidGroupAddition =\n      (my_chat_member.chat.type !== \"group\" &&\n        my_chat_member.chat.type !== \"supergroup\") ||\n      String(my_chat_member.from.id) !== privateChatId ||\n      my_chat_member.new_chat_member.user.username !== botUsername ||\n      (my_chat_member.new_chat_member.status !== \"member\" &&\n        my_chat_member.new_chat_member.status !== \"administrator\");\n\n    if (isInvalidGroupAddition) {\n      return null;\n    }\n\n    return {\n      chatId: String(my_chat_member.chat.id),\n      chatTitle: my_chat_member.chat.title,\n      user: {\n        id: my_chat_member.from.id,\n        first_name: my_chat_member.from.first_name,\n        username: my_chat_member.from.username,\n      },\n    };\n  }\n\n  // Legacy fallback: service message with new_chat_member fields\n  const { message } = update;\n\n  const isInvalidGroupAddition =\n    !message ||\n    (message.chat.type !== \"group\" && message.chat.type !== \"supergroup\") ||\n    !message.from ||\n    String(message.from.id) !== privateChatId;\n\n  if (isInvalidGroupAddition) {\n    return null;\n  }\n\n  const isBotAdded =\n    message.new_chat_participant?.username === botUsername ||\n    message.new_chat_member?.username === botUsername ||\n    message.new_chat_members?.some((m) => m.username === botUsername);\n\n  if (!isBotAdded) return null;\n\n  return {\n    chatId: String(message.chat.id),\n    chatTitle: message.chat.title,\n    user: {\n      id: message.from.id,\n      first_name: message.from.first_name,\n      username: message.from.username,\n    },\n  };\n}\n\n// ---- Main logic -------------------------------------------------------------\n\nexport async function processTelegramUpdates(args: {\n  updates: TelegramUpdate[];\n  workspaceId: number;\n  privateChatId?: string;\n  since?: number;\n  botUsername: string;\n  redisClient: typeof redis;\n}): Promise<ValidUpdate[]> {\n  const {\n    updates,\n    workspaceId,\n    privateChatId,\n    since,\n    botUsername,\n    redisClient,\n  } = args;\n\n  const validUpdates: ValidUpdate[] = [];\n\n  // 1. Pre-filter by timestamp\n  const recentUpdates = since\n    ? updates.filter((u) => {\n        if (u.message) return u.message.date >= since;\n        if (u.my_chat_member) return u.my_chat_member.date >= since;\n        return false;\n      })\n    : updates;\n\n  // 2. Phase 1: private /start (no privateChatId filter)\n  if (!privateChatId) {\n    const tokenKey = `telegram:workspace_token:${workspaceId}`;\n    const storedToken = await redisClient.get<string>(tokenKey);\n\n    if (storedToken) {\n      for (const update of recentUpdates) {\n        const result = extractPrivateChatStart(update, storedToken);\n        if (result) {\n          // Single-use token: delete and stop\n          await redisClient.del(tokenKey);\n          validUpdates.push({ chatType: \"private\", ...result });\n          break;\n        }\n      }\n    }\n  }\n  // 3. Phase 2: group/supergroup additions\n  else {\n    for (const update of recentUpdates) {\n      const result = extractGroupBotAddition(\n        update,\n        privateChatId,\n        botUsername,\n      );\n      if (result) {\n        validUpdates.push({ chatType: \"group\", ...result });\n      }\n    }\n  }\n\n  return validUpdates;\n}\n"
  },
  {
    "path": "packages/api/src/test/preload.ts",
    "content": "import { mock } from \"bun:test\";\n\nmock.module(\"@openstatus/upstash\", () => ({\n  Redis: {\n    fromEnv() {\n      return {\n        get: () => Promise.resolve(undefined),\n        set: () => Promise.resolve([]),\n      };\n    },\n  },\n}));\n"
  },
  {
    "path": "packages/api/src/trpc.ts",
    "content": "import { TRPCError, initTRPC } from \"@trpc/server\";\nimport { type NextRequest, after } from \"next/server\";\nimport superjson from \"superjson\";\nimport { ZodError, treeifyError } from \"zod\";\n\nimport {\n  type EventProps,\n  type IdentifyProps,\n  parseInputToProps,\n  setupAnalytics,\n} from \"@openstatus/analytics\";\nimport { db, eq, schema } from \"@openstatus/db\";\nimport type { User, Workspace } from \"@openstatus/db/src/schema\";\n\n// Generic session type that works with both User and Viewer\ntype Session = {\n  user?: {\n    id?: string | null;\n    email?: string | null;\n  } | null;\n} | null;\n\n/**\n * 1. CONTEXT\n *\n * This section defines the \"contexts\" that are available in the backend API\n *\n * These allow you to access things like the database, the session, etc, when\n * processing a request\n *\n */\ntype CreateContextOptions = {\n  session: Session | null;\n  workspace?: Workspace | null;\n  user?: User | null;\n  req?: NextRequest;\n  metadata?: {\n    userAgent?: string;\n    location?: string;\n  };\n};\n\ntype Meta = {\n  track?: EventProps;\n  trackProps?: string[];\n};\n\n/**\n * This helper generates the \"internals\" for a tRPC context. If you need to use\n * it, you can export it from here\n *\n * Examples of things you may need it for:\n * - testing, so we dont have to mock Next.js' req/res\n * - trpc's `createSSGHelpers` where we don't have req/res\n * @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts\n */\nexport const createInnerTRPCContext = (opts: CreateContextOptions) => {\n  return {\n    ...opts,\n    db,\n  };\n};\n\n/**\n * This is the actual context you'll use in your router. It will be used to\n * process every request that goes through your tRPC endpoint\n * @link https://trpc.io/docs/context\n */\nexport const createTRPCContext = async (opts: {\n  req: NextRequest;\n  serverSideCall?: boolean;\n  auth?: () => Promise<Session>;\n}) => {\n  // Use provided auth function or return null session\n  const session = opts.auth ? await opts.auth() : null;\n  const workspace = null;\n  const user = null;\n\n  return createInnerTRPCContext({\n    session,\n    workspace,\n    user,\n    req: opts.req,\n    metadata: {\n      userAgent: opts.req.headers.get(\"user-agent\") ?? undefined,\n      location:\n        opts.req.headers.get(\"x-forwarded-for\") ??\n        process.env.VERCEL_REGION ??\n        undefined,\n    },\n  });\n};\n\nexport type Context = Awaited<ReturnType<typeof createTRPCContext>>;\n\n/**\n * 2. INITIALIZATION\n *\n * This is where the trpc api is initialized, connecting the context and\n * transformer\n */\nexport const t = initTRPC\n  .context<Context>()\n  .meta<Meta>()\n  .create({\n    transformer: superjson,\n    errorFormatter({ shape, error }) {\n      return {\n        ...shape,\n        data: {\n          ...shape.data,\n          zodError:\n            error.cause instanceof ZodError ? treeifyError(error.cause) : null,\n        },\n      };\n    },\n  });\n\n/**\n * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)\n *\n * These are the pieces you use to build your tRPC API. You should import these\n * a lot in the /src/server/api/routers folder\n */\n\n/**\n * This is how you create new routers and subrouters in your tRPC API\n * @see https://trpc.io/docs/router\n */\nexport const createTRPCRouter = t.router;\nexport const mergeRouters = t.mergeRouters;\n\n/**\n * Public (unauthed) procedure\n *\n * This is the base piece you use to build new queries and mutations on your\n * tRPC API. It does not guarantee that a user querying is authorized, but you\n * can still access user session data if they are logged in\n */\nexport const publicProcedure = t.procedure;\n\n/**\n * Reusable middleware that enforces users are logged in before running the\n * procedure\n */\nconst enforceUserIsAuthed = t.middleware(async (opts) => {\n  const { ctx } = opts;\n  if (!ctx.session?.user?.id) {\n    throw new TRPCError({ code: \"UNAUTHORIZED\" });\n  }\n\n  // /**\n  //  * Attach `user` and `workspace` | `activeWorkspace` infos to context by\n  //  * comparing the `user.tenantId` to clerk's `auth.userId`\n  //  */\n  const userAndWorkspace = await db.query.user.findFirst({\n    where: eq(schema.user.id, Number(ctx.session.user.id)),\n    with: {\n      usersToWorkspaces: {\n        with: {\n          workspace: true,\n        },\n      },\n    },\n  });\n\n  const { usersToWorkspaces, ...userProps } = userAndWorkspace || {};\n\n  /**\n   * We need to include the active \"workspace-slug\" cookie in the request found in the\n   * `/app/[workspaceSlug]/.../`routes. We pass them either via middleware if it's a\n   * server request or via the client cookie, set via `<WorspaceClientCookie />`\n   * if it's a client request.\n   *\n   * REMINDER: We only need the client cookie because of client side mutations.\n   */\n  const workspaceSlug = ctx.req?.cookies.get(\"workspace-slug\")?.value;\n\n  // if (!workspaceSlug) {\n  //   throw new TRPCError({\n  //     code: \"UNAUTHORIZED\",\n  //     message: \"Workspace Slug Not Found\",\n  //   });\n  // }\n\n  // NOTE: if no workspace slug fit (cookie manipulation), use the first workspace\n  const activeWorkspace =\n    usersToWorkspaces?.find(({ workspace }) => {\n      // If there is a workspace slug in the cookie, use it to find the workspace\n      if (workspaceSlug) return workspace.slug === workspaceSlug;\n      return true;\n    })?.workspace ?? usersToWorkspaces?.[0]?.workspace;\n\n  if (!activeWorkspace) {\n    throw new TRPCError({\n      code: \"UNAUTHORIZED\",\n      message: \"Workspace Not Found\",\n    });\n  }\n\n  if (activeWorkspace.slug !== workspaceSlug) {\n    // properly set the workspace slug cookie\n    ctx.req?.cookies.set(\"workspace-slug\", activeWorkspace.slug);\n  }\n\n  if (!userProps) {\n    throw new TRPCError({ code: \"UNAUTHORIZED\", message: \"User Not Found\" });\n  }\n\n  const user = schema.selectUserSchema.parse(userProps);\n  const workspace = schema.selectWorkspaceSchema.parse(activeWorkspace);\n\n  const result = await opts.next({ ctx: { ...ctx, user, workspace } });\n\n  if (process.env.NODE_ENV === \"test\") {\n    return result;\n  }\n\n  // REMINDER: We only track the event if the request was successful\n  if (!result.ok) {\n    return result;\n  }\n\n  // REMINDER: We only track the event if the request was successful\n  // REMINDER: We are not blocking the request\n  after(async () => {\n    const { ctx, meta, getRawInput } = opts;\n\n    if (meta?.track) {\n      let identify: IdentifyProps = {\n        userAgent: ctx.metadata?.userAgent,\n        location: ctx.metadata?.location,\n      };\n\n      if (user && workspace) {\n        identify = {\n          ...identify,\n          userId: `usr_${user.id}`,\n          email: user.email || undefined,\n          workspaceId: String(workspace.id),\n          plan: workspace.plan,\n        };\n      }\n\n      const analytics = await setupAnalytics(identify);\n      const rawInput = await getRawInput();\n      const additionalProps = parseInputToProps(rawInput, meta.trackProps);\n\n      await analytics.track({ ...meta.track, ...additionalProps });\n    }\n  });\n\n  return result;\n});\n\n/**\n * Middleware to parse form data and put it in the rawInput\n */\nexport const formdataMiddleware = t.middleware(async (opts) => {\n  const formData = await opts.ctx.req?.formData?.();\n  if (!formData) throw new TRPCError({ code: \"BAD_REQUEST\" });\n\n  return opts.next({\n    input: formData,\n  });\n});\n\n/**\n * Protected (authed) procedure\n *\n * If you want a query or mutation to ONLY be accessible to logged in users, use\n * this. It verifies the session is valid and guarantees ctx.session.user is not\n * null\n *\n * @see https://trpc.io/docs/procedures\n */\nexport const protectedProcedure = t.procedure.use(enforceUserIsAuthed);\n"
  },
  {
    "path": "packages/api/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"moduleResolution\": \"bundler\",\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/assertions/README.md",
    "content": "Biggest props to [@chronark\\_](https://twitter.com/chronark_/) for providing us\nthe snippets.\n"
  },
  {
    "path": "packages/assertions/package.json",
    "content": "{\n  \"name\": \"@openstatus/assertions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {},\n  \"dependencies\": {\n    \"jsonpath-plus\": \"7.2.0\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\",\n    \"zod\": \"4.1.13\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/assertions/src/dictionary.ts",
    "content": "import type { z } from \"zod\";\n\nimport type { numberCompare, recordCompare, stringCompare } from \"./v1\";\n\nexport const numberCompareDictionary: Record<\n  z.infer<typeof numberCompare>,\n  string\n> = {\n  eq: \"Equal\",\n  not_eq: \"Not Equal\",\n  gt: \"Greater than\",\n  gte: \"Greater than or equal\",\n  lt: \"Less than\",\n  lte: \"Less than or equal\",\n};\n\nexport const stringCompareDictionary: Record<\n  z.infer<typeof stringCompare>,\n  string\n> = {\n  contains: \"Contains\",\n  not_contains: \"Does not contain\",\n  eq: \"Equal\",\n  not_eq: \"Not Equal\",\n  empty: \"Empty\",\n  not_empty: \"Not Empty\",\n  gt: \"Greater than\",\n  gte: \"Greater than or equal\",\n  lt: \"Less than\",\n  lte: \"Less than or equal\",\n};\n\nexport const recordCompareDictionary: Record<\n  z.infer<typeof recordCompare>,\n  string\n> = {\n  eq: \"Equal\",\n  not_eq: \"Not Equal\",\n  contains: \"Contains\",\n  not_contains: \"Does not contain\",\n};\n"
  },
  {
    "path": "packages/assertions/src/index.ts",
    "content": "export * from \"./dictionary\";\nexport * from \"./types\";\nexport * from \"./serializing\";\nexport * from \"./v1\";\nexport * from \"./type-guards\";\n"
  },
  {
    "path": "packages/assertions/src/serializing.ts",
    "content": "import { z } from \"zod\";\n\nimport type { Assertion } from \"./types\";\nimport {\n  DnsRecordAssertion,\n  HeaderAssertion,\n  JsonBodyAssertion,\n  StatusAssertion,\n  TextBodyAssertion,\n  base,\n  headerAssertion,\n  jsonBodyAssertion,\n  recordAssertion,\n  statusAssertion,\n  textBodyAssertion,\n} from \"./v1\";\n\nexport function serialize(assertions: Assertion[]): string {\n  return JSON.stringify(assertions.map((a) => a.schema));\n}\nexport function deserialize(s: string): Assertion[] {\n  const bases = z.array(base).parse(JSON.parse(s));\n  return bases.map((b) => {\n    switch (b.type) {\n      case \"status\":\n        return new StatusAssertion(statusAssertion.parse(b));\n      case \"header\":\n        return new HeaderAssertion(headerAssertion.parse(b));\n      case \"jsonBody\":\n        return new JsonBodyAssertion(jsonBodyAssertion.parse(b));\n      case \"textBody\":\n        return new TextBodyAssertion(textBodyAssertion.parse(b));\n      case \"dnsRecord\":\n        return new DnsRecordAssertion(recordAssertion.parse(b));\n\n      default:\n        throw new Error(`unknown assertion type: ${b.type}`);\n    }\n  });\n}\n"
  },
  {
    "path": "packages/assertions/src/type-guards.ts",
    "content": "import type {\n  AssertionRequest,\n  DnsAssertionRequest,\n  HttpAssertionRequest,\n} from \"./types\";\n\nexport function isHttpAssertionRequest(\n  req: AssertionRequest,\n): req is HttpAssertionRequest {\n  return \"status\" in req && \"header\" in req && \"body\" in req;\n}\n\nexport function isDnsAssertionRequest(\n  req: AssertionRequest,\n): req is DnsAssertionRequest {\n  return \"records\" in req;\n}\n"
  },
  {
    "path": "packages/assertions/src/types.ts",
    "content": "import type { z } from \"zod\";\n\nimport type { assertion } from \"./v1\";\n\nexport type HttpAssertionRequest = {\n  body: string;\n  header: Record<string, string>;\n  status: number;\n};\n\nexport type DnsAssertionRequest = {\n  records: Partial<Record<string, string[]>>;\n};\n\nexport type AssertionRequest = HttpAssertionRequest | DnsAssertionRequest;\n\nexport type AssertionResult =\n  | {\n      success: true;\n      message?: never;\n    }\n  | {\n      success: false;\n      message: string;\n    };\nexport interface Assertion {\n  schema: z.infer<typeof assertion>;\n  assert: (req: AssertionRequest) => AssertionResult;\n}\n"
  },
  {
    "path": "packages/assertions/src/v1.ts",
    "content": "import { JSONPath } from \"jsonpath-plus\";\nimport { z } from \"zod\";\n\nimport { isDnsAssertionRequest, isHttpAssertionRequest } from \"./type-guards\";\nimport type { Assertion, AssertionRequest, AssertionResult } from \"./types\";\n\nexport const stringCompare = z.enum([\n  \"contains\",\n  \"not_contains\",\n  \"eq\",\n  \"not_eq\",\n  \"empty\",\n  \"not_empty\",\n  \"gt\",\n  \"gte\",\n  \"lt\",\n  \"lte\",\n]);\nexport const numberCompare = z.enum([\"eq\", \"not_eq\", \"gt\", \"gte\", \"lt\", \"lte\"]);\n\nexport const recordCompare = z.enum([\n  \"contains\",\n  \"not_contains\",\n  \"eq\",\n  \"not_eq\",\n]);\n\nfunction evaluateNumber(\n  value: number,\n  compare: z.infer<typeof numberCompare>,\n  target: number,\n): AssertionResult {\n  switch (compare) {\n    case \"eq\":\n      if (value !== target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be equal to ${target}`,\n        };\n      }\n      break;\n    case \"not_eq\":\n      if (value === target) {\n        return {\n          success: false,\n          message: `Expected ${value} to not be equal to ${target}`,\n        };\n      }\n      break;\n    case \"gt\":\n      if (value <= target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be greater than ${target}`,\n        };\n      }\n      break;\n    case \"gte\":\n      if (value < target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be greater than or equal to ${target}`,\n        };\n      }\n      break;\n    case \"lt\":\n      if (value >= target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be less than ${target}`,\n        };\n      }\n      break;\n    case \"lte\":\n      if (value > target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be less than or equal to ${target}`,\n        };\n      }\n      break;\n  }\n  return { success: true };\n}\n\nfunction evaluateString(\n  value: string,\n  compare: z.infer<typeof stringCompare>,\n  target: string,\n): AssertionResult {\n  switch (compare) {\n    case \"contains\":\n      if (!value.includes(target)) {\n        return {\n          success: false,\n          message: `Expected ${value} to contain ${target}`,\n        };\n      }\n      break;\n    case \"not_contains\":\n      if (value.includes(target)) {\n        return {\n          success: false,\n          message: `Expected ${value} to not contain ${target}`,\n        };\n      }\n      break;\n    case \"empty\":\n      if (value !== \"\") {\n        return { success: false, message: `Expected ${value} to be empty` };\n      }\n      break;\n    case \"not_empty\":\n      if (value === \"\") {\n        return { success: false, message: `Expected ${value} to not be empty` };\n      }\n      break;\n    case \"eq\":\n      if (value !== target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be equal to ${target}`,\n        };\n      }\n      break;\n    case \"not_eq\":\n      if (value === target) {\n        return {\n          success: false,\n          message: `Expected ${value} to not be equal to ${target}`,\n        };\n      }\n      break;\n    case \"gt\":\n      if (value <= target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be greater than ${target}`,\n        };\n      }\n      break;\n    case \"gte\":\n      if (value < target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be greater than or equal to ${target}`,\n        };\n      }\n      break;\n    case \"lt\":\n      if (value >= target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be less than ${target}`,\n        };\n      }\n      break;\n    case \"lte\":\n      if (value > target) {\n        return {\n          success: false,\n          message: `Expected ${value} to be less than or equal to ${target}`,\n        };\n      }\n      break;\n  }\n  return { success: true };\n}\n\nfunction evaluateRecord(\n  values: string[],\n  compare: z.infer<typeof recordCompare>,\n  target: string,\n): AssertionResult {\n  const valuesString = values.join(\", \");\n\n  switch (compare) {\n    case \"contains\":\n      if (!values.some((v) => v.includes(target))) {\n        return {\n          success: false,\n          message: `Expected DNS records [${valuesString}] to contain ${target}`,\n        };\n      }\n      break;\n    case \"not_contains\":\n      if (values.some((v) => v.includes(target))) {\n        return {\n          success: false,\n          message: `Expected DNS records [${valuesString}] to not contain ${target}`,\n        };\n      }\n      break;\n    case \"eq\":\n      if (!values.includes(target)) {\n        return {\n          success: false,\n          message: `Expected DNS records [${valuesString}] to equal ${target}`,\n        };\n      }\n      break;\n    case \"not_eq\":\n      if (values.includes(target)) {\n        return {\n          success: false,\n          message: `Expected DNS records [${valuesString}] to not equal ${target}`,\n        };\n      }\n      break;\n  }\n  return { success: true };\n}\n\nexport const base = z.looseObject({\n  version: z.enum([\"v1\"]).prefault(\"v1\"),\n  type: z.string(),\n});\nexport const statusAssertion = base.extend(\n  z.object({\n    type: z.literal(\"status\"),\n    compare: numberCompare,\n    target: z.int().positive(),\n  }).shape,\n);\n\nexport const headerAssertion = base.extend(\n  z.object({\n    type: z.literal(\"header\"),\n    compare: stringCompare,\n    key: z.string(),\n    target: z.string(),\n  }).shape,\n);\n\nexport const textBodyAssertion = base.extend(\n  z.object({\n    type: z.literal(\"textBody\"),\n    compare: stringCompare,\n    target: z.string(),\n  }).shape,\n);\n\nexport const jsonBodyAssertion = base.extend(\n  z.object({\n    type: z.literal(\"jsonBody\"),\n    path: z.string(), // https://www.npmjs.com/package/jsonpath-plus\n    compare: stringCompare,\n    target: z.string(),\n  }).shape,\n);\n\nexport const dnsRecords = [\"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\", \"NS\"] as const;\n\nexport const recordAssertion = base.extend(\n  z.object({\n    type: z.literal(\"dnsRecord\"),\n    key: z.enum(dnsRecords),\n    compare: recordCompare,\n    target: z.string(),\n  }).shape,\n);\n\nexport const assertion = z.discriminatedUnion(\"type\", [\n  statusAssertion,\n  headerAssertion,\n  textBodyAssertion,\n  jsonBodyAssertion,\n  recordAssertion,\n]);\n\nexport class StatusAssertion implements Assertion {\n  readonly schema: z.infer<typeof statusAssertion>;\n\n  constructor(schema: z.infer<typeof statusAssertion>) {\n    this.schema = schema;\n  }\n\n  public assert(req: AssertionRequest): AssertionResult {\n    if (!isHttpAssertionRequest(req)) {\n      return {\n        success: false,\n        message: \"Invalid request type for status assertion\",\n      };\n    }\n    const { success, message } = evaluateNumber(\n      req.status,\n      this.schema.compare,\n      this.schema.target,\n    );\n    if (success) {\n      return { success };\n    }\n    return { success, message: `Status: ${message}` };\n  }\n}\n\nexport class HeaderAssertion implements Assertion {\n  readonly schema: z.infer<typeof headerAssertion>;\n\n  constructor(schema: z.infer<typeof headerAssertion>) {\n    this.schema = schema;\n  }\n\n  public assert(req: AssertionRequest): AssertionResult {\n    if (!isHttpAssertionRequest(req)) {\n      return {\n        success: false,\n        message: \"Invalid request type for header assertion\",\n      };\n    }\n    const { success, message } = evaluateString(\n      req.header[this.schema.key],\n      this.schema.compare,\n      this.schema.target,\n    );\n    if (success) {\n      return { success };\n    }\n    return { success, message: `Header ${this.schema.key}: ${message}` };\n  }\n}\n\nexport class TextBodyAssertion implements Assertion {\n  readonly schema: z.infer<typeof textBodyAssertion>;\n\n  constructor(schema: z.infer<typeof textBodyAssertion>) {\n    this.schema = schema;\n  }\n\n  public assert(req: AssertionRequest): AssertionResult {\n    if (!isHttpAssertionRequest(req)) {\n      return {\n        success: false,\n        message: \"Invalid request type for text body assertion\",\n      };\n    }\n    const { success, message } = evaluateString(\n      req.body,\n      this.schema.compare,\n      this.schema.target,\n    );\n    if (success) {\n      return { success };\n    }\n    return { success, message: `Body: ${message}` };\n  }\n}\nexport class JsonBodyAssertion implements Assertion {\n  readonly schema: z.infer<typeof jsonBodyAssertion>;\n\n  constructor(schema: z.infer<typeof jsonBodyAssertion>) {\n    this.schema = schema;\n  }\n\n  public assert(req: AssertionRequest): AssertionResult {\n    if (!isHttpAssertionRequest(req)) {\n      return {\n        success: false,\n        message: \"Invalid request type for JSON body assertion\",\n      };\n    }\n    try {\n      const json = JSON.parse(req.body);\n      const value = JSONPath({ path: this.schema.path, json });\n      const { success, message } = evaluateString(\n        value,\n        this.schema.compare,\n        this.schema.target,\n      );\n      if (success) {\n        return { success };\n      }\n      return { success, message: `Body: ${message}` };\n    } catch (_e) {\n      console.error(\"Unable to parse json\");\n      return { success: false, message: \"Unable to parse json\" };\n    }\n  }\n}\n\nexport class DnsRecordAssertion implements Assertion {\n  readonly schema: z.infer<typeof recordAssertion>;\n\n  constructor(schema: z.infer<typeof recordAssertion>) {\n    this.schema = schema;\n  }\n\n  public assert(req: AssertionRequest): AssertionResult {\n    if (!isDnsAssertionRequest(req)) {\n      return {\n        success: false,\n        message: \"Invalid request type for DNS record assertion\",\n      };\n    }\n    const records = req.records[this.schema.key] || [];\n    const { success, message } = evaluateRecord(\n      records,\n      this.schema.compare,\n      this.schema.target,\n    );\n    if (success) {\n      return { success };\n    }\n    return { success, message: `DNS Record ${this.schema.key}: ${message}` };\n  }\n}\n"
  },
  {
    "path": "packages/assertions/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"es2015\", \"dom\"]\n  },\n  \"include\": [\"src\", \"*.ts\"]\n}\n"
  },
  {
    "path": "packages/db/README.md",
    "content": "# DB\n\nWe are using [turso](https://turso.tech) and sqlite as database to store\nuser/page/monitor settings. The timeseries data is stored in a\n[tinybird](https://tinybird.co) datasource (built on top of ClickHouse).\n\n## Local Development\n\nInstall the [Turso CLI](https://docs.turso.tech/reference/turso-cli).\n\nFor local environment, first\n[install sqld](https://github.com/tursodatabase/libsql/blob/main/docs/BUILD-RUN.md).\n\nWhen installing with Homebrew, follow:\n\n```bash\n$ brew tap libsql/sqld\n$ brew install sqld-beta\n$ sqld --help\n```\n\nIf you want to keep your database locally, run:\n\n```bash\n$ turso dev --db-file openstatus.db\n```\n\nIt will create a local database in the directory you run the command.\n\nAdd the environment variables to inside of the `.env` file in both projects, the\n`/apps/web` and `/packages/db`:\n\n```env\nDATABASE_URL=http://127.0.0.1:8080\nDATABASE_AUTH_TOKEN=any-token # no need to change token\n```\n\nStart the migration script inside of `/packages/db` to have the database schema\nup to date:\n\n```bash\n$ pnpm migrate\n```\n\nYou should be ready to go! Check out the Drizzle Studio to see if your tables\nhave been created:\n\n```$\n$ pnpm studio\n```\n"
  },
  {
    "path": "packages/db/drizzle/0000_lively_master_chief.sql",
    "content": "CREATE TABLE `incident` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`status` text(2) NOT NULL,\n\t`page_id` text NOT NULL,\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `incident_update` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`uuid` text NOT NULL,\n\t`incident_date` integer,\n\t`title` text(256),\n\t`message` text,\n\t`incident_id` integer NOT NULL,\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`incident_id`) REFERENCES `incident`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `page` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`title` text NOT NULL,\n\t`description` text NOT NULL,\n\t`icon` text(256),\n\t`slug` text(256) NOT NULL,\n\t`custom_domain` text(256) NOT NULL,\n\t`published` integer DEFAULT false,\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `monitor` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`job_type` text(3) DEFAULT 'other' NOT NULL,\n\t`periodicity` text(6) DEFAULT 'other' NOT NULL,\n\t`status` text(2) DEFAULT 'inactive' NOT NULL,\n\t`active` integer DEFAULT false,\n\t`url` text(512) NOT NULL,\n\t`name` text(256) DEFAULT '' NOT NULL,\n\t`description` text DEFAULT '' NOT NULL,\n\t`workspace_id` integer,\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE TABLE `monitors_to_pages` (\n\t`monitor_id` integer NOT NULL,\n\t`page_id` integer NOT NULL,\n\tPRIMARY KEY(`monitor_id`, `page_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `user` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`tenant_id` text(256),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now'))\n);\n--> statement-breakpoint\nCREATE TABLE `users_to_workspaces` (\n\t`user_id` integer NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\tPRIMARY KEY(`user_id`, `workspace_id`),\n\tFOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE TABLE `workspace` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`slug` text NOT NULL,\n\t`stripe_id` text(256),\n\t`name` text,\n\t`updated_at` integer DEFAULT (strftime('%s', 'now'))\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `incident_update_uuid_unique` ON `incident_update` (`uuid`);--> statement-breakpoint\nCREATE UNIQUE INDEX `page_slug_unique` ON `page` (`slug`);--> statement-breakpoint\nCREATE UNIQUE INDEX `user_tenant_id_unique` ON `user` (`tenant_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `workspace_slug_unique` ON `workspace` (`slug`);--> statement-breakpoint\nCREATE UNIQUE INDEX `workspace_stripe_id_unique` ON `workspace` (`stripe_id`);"
  },
  {
    "path": "packages/db/drizzle/0001_brainy_beast.sql",
    "content": "ALTER TABLE monitor ADD `regions` text DEFAULT '' NOT NULL;\n\n"
  },
  {
    "path": "packages/db/drizzle/0002_luxuriant_ser_duncan.sql",
    "content": "ALTER TABLE user ADD `first_name` text DEFAULT '';--> statement-breakpoint\nALTER TABLE user ADD `last_name` text DEFAULT '';--> statement-breakpoint\nALTER TABLE user ADD `email` text DEFAULT '';--> statement-breakpoint\nALTER TABLE user ADD `photo_url` text DEFAULT '';"
  },
  {
    "path": "packages/db/drizzle/0003_glamorous_living_mummy.sql",
    "content": "DROP INDEX IF EXISTS `incident_update_uuid_unique`;--> statement-breakpoint\n\nALTER TABLE `incident` RENAME TO `incident_old`;--> statement-breakpoint\nALTER TABLE `incident_update` RENAME TO `incident_update_old`;--> statement-breakpoint\n\nDROP TABLE `incident_old`;--> statement-breakpoint\nDROP TABLE `incident_update_old`;--> statement-breakpoint\n\nCREATE TABLE `incident` (\n  `id` integer PRIMARY KEY NOT NULL,\n  `status` text(4) NOT NULL,\n  `title` text(256) NOT NULL,\n  `created_at` integer DEFAULT (strftime('%s', 'now')),\n  `updated_at` integer DEFAULT (strftime('%s', 'now')),\n  `workspace_id` integer NOT NULL,\n  FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\n\nCREATE TABLE `incident_update` (\n  `id` integer PRIMARY KEY NOT NULL,\n  `status` text(4) NOT NULL,\n  `date` integer NOT NULL,\n  `message` text NOT NULL,\n  `created_at` integer DEFAULT (strftime('%s', 'now')),\n  `updated_at` integer DEFAULT (strftime('%s', 'now')),\n  `incident_id` integer NOT NULL,\n  FOREIGN KEY (`incident_id`) REFERENCES `incident`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\n\nDROP INDEX IF EXISTS `incident_update_uuid_unique`;\n--> statement-breakpoint\n\nCREATE TABLE `incidents_to_monitors` (\n\t`monitor_id` integer NOT NULL,\n\t`incident_id` integer NOT NULL,\n\tPRIMARY KEY(`incident_id`, `monitor_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`incident_id`) REFERENCES `incident`(`id`) ON UPDATE no action ON DELETE cascade\n);\n\n"
  },
  {
    "path": "packages/db/drizzle/0004_fixed_dakota_north.sql",
    "content": "ALTER TABLE page RENAME COLUMN `updated_at` to `created_at` ;--> statement-breakpoint\nALTER TABLE page ADD `updated_at` integer;--> statement-breakpoint\n\nALTER TABLE monitor RENAME COLUMN `updated_at` to `created_at` ;--> statement-breakpoint\nALTER TABLE monitor ADD `updated_at` integer;--> statement-breakpoint\n\nALTER TABLE user RENAME COLUMN `updated_at` to `created_at` ;--> statement-breakpoint\nALTER TABLE user ADD `updated_at` integer;--> statement-breakpoint\n\nALTER TABLE workspace RENAME COLUMN `updated_at` to `created_at` ;--> statement-breakpoint\nALTER TABLE workspace ADD `updated_at` integer;"
  },
  {
    "path": "packages/db/drizzle/0005_even_baron_strucker.sql",
    "content": "ALTER TABLE workspace ADD `subscription_id` text;--> statement-breakpoint\nALTER TABLE workspace ADD `plan` text(3);--> statement-breakpoint\nALTER TABLE workspace ADD `ends_at` integer;--> statement-breakpoint\nALTER TABLE workspace ADD `paid_until` integer;"
  },
  {
    "path": "packages/db/drizzle/0006_tired_anita_blake.sql",
    "content": "/*\n SQLite does not support \"Changing existing column type\" out of the box, we do not generate automatic migration for that, so it has to be done manually\n Please refer to: https://www.techonthenet.com/sqlite/tables/alter_table.php\n                  https://www.sqlite.org/lang_altertable.html\n                  https://stackoverflow.com/questions/2083543/modify-a-columns-type-in-sqlite3\n\n Due to that we don't generate migration automatically and it has to be done manually\n\n*/\nPRAGMA foreign_keys=OFF;\n--> statement-breakpoint\n\nCREATE TABLE `monitor_new` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`job_type` text(3) DEFAULT 'other' NOT NULL,\n\t`periodicity` text(6) DEFAULT 'other' NOT NULL,\n\t`status` text(2) DEFAULT 'inactive' NOT NULL,\n\t`active` integer DEFAULT false,\n\t`url` text(512) NOT NULL,\n\t`name` text(256) DEFAULT '' NOT NULL,\n\t`description` text DEFAULT '' NOT NULL,\n\t`workspace_id` integer,\n\t`headers` text DEFAULT '',\n\t`body` text DEFAULT '',\n\t`method` text(5) DEFAULT 'GET',\n\t`created_at` integer DEFAULT (strftime('%s', 'now')), `regions` text DEFAULT '' NOT NULL, `updated_at` integer,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nINSERT INTO monitor_new(`id`,`job_type`,`periodicity`,`status`,`active`,`url`,`name`,`description`,`workspace_id`,`created_at`,`regions`,`updated_at`) SELECT * FROM monitor;\n--> statement-breakpoint\n\nCREATE TABLE `monitors_to_pages_new` (\n\t`monitor_id` integer NOT NULL,\n\t`page_id` integer NOT NULL,\n\tPRIMARY KEY(`monitor_id`, `page_id`)\n);\n--> statement-breakpoint\nINSERT INTO monitors_to_pages_new(`monitor_id`,`page_id`) SELECT * FROM monitors_to_pages;\n--> statement-breakpoint\n\nCREATE TABLE `incidents_to_monitors_new` (\n\t`monitor_id` integer NOT NULL,\n\t`incident_id` integer NOT NULL,\n\tPRIMARY KEY(`incident_id`, `monitor_id`)\n);\n--> statement-breakpoint\nINSERT INTO incidents_to_monitors_new(`monitor_id`,`incident_id`) SELECT * FROM incidents_to_monitors;\n--> statement-breakpoint\nDROP TABLE monitor;\n--> statement-breakpoint\nALTER TABLE monitor_new RENAME TO monitor;\n--> statement-breakpoint\nINSERT INTO monitors_to_pages(`monitor_id`,`page_id`) SELECT * FROM monitors_to_pages_new;\n--> statement-breakpoint\nINSERT INTO incidents_to_monitors(`monitor_id`,`incident_id`) SELECT * FROM incidents_to_monitors_new;\n--> statement-breakpoint\nDROP TABLE monitors_to_pages_new;\n--> statement-breakpoint\nDROP TABLE incidents_to_monitors_new;\n"
  },
  {
    "path": "packages/db/drizzle/0007_complex_frog_thor.sql",
    "content": "CREATE TABLE `integration` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text(256) NOT NULL,\n\t`workspace_id` integer,\n\t`credential` text,\n\t`external_id` text NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\t`data` text NOT NULL,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n"
  },
  {
    "path": "packages/db/drizzle/0008_overjoyed_sunset_bain.sql",
    "content": "CREATE TABLE `notification` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`provider` text NOT NULL,\n\t`data` text DEFAULT '{}',\n\t`workspace_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE TABLE `notifications_to_monitors` (\n\t`monitor_id` integer NOT NULL,\n\t`notification_id` integer NOT NULL,\n\tPRIMARY KEY(`monitor_id`, `notification_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`notification_id`) REFERENCES `notification`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nALTER TABLE `monitor` DROP COLUMN `status`;\n--> statement-breakpoint\nALTER TABLE `monitor` ADD COLUMN `status` text(2) DEFAULT 'active' NOT NULL;\n"
  },
  {
    "path": "packages/db/drizzle/0009_small_maximus.sql",
    "content": "CREATE TABLE `incidents_to_pages` (\n\t`page_id` integer NOT NULL,\n\t`incident_id` integer NOT NULL,\n\tPRIMARY KEY(`incident_id`, `page_id`),\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`incident_id`) REFERENCES `incident`(`id`) ON UPDATE no action ON DELETE cascade\n);\n"
  },
  {
    "path": "packages/db/drizzle/0010_lame_songbird.sql",
    "content": "CREATE TABLE `monitor_status` (\n\t`monitor_id` integer NOT NULL,\n\t`region` text DEFAULT '' NOT NULL,\n\t`status` text DEFAULT 'active' NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`monitor_id`, `region`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `monitor_status_idx` ON `monitor_status` (`monitor_id`,`region`);"
  },
  {
    "path": "packages/db/drizzle/0011_bright_jazinda.sql",
    "content": "ALTER TABLE `incidents_to_monitors` RENAME TO `status_report_to_monitors`;--> statement-breakpoint\nALTER TABLE `incidents_to_pages` RENAME TO `status_reports_to_pages`;--> statement-breakpoint\nALTER TABLE `incident_update` RENAME TO `status_report_update`;--> statement-breakpoint\nALTER TABLE `incident` RENAME TO `status_report`; --> statement-breakpoint\nALTER TABLE `status_report_to_monitors` RENAME COLUMN `incident_id` TO `status_report_id`;--> statement-breakpoint\nALTER TABLE `status_reports_to_pages` RENAME COLUMN `incident_id` TO `status_report_id`;--> statement-breakpoint\nALTER TABLE `status_report_update` RENAME COLUMN `incident_id` TO `status_report_id`;"
  },
  {
    "path": "packages/db/drizzle/0012_tan_magma.sql",
    "content": "CREATE TABLE `invitation` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`email` text NOT NULL,\n\t`role` text DEFAULT 'member' NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`token` text NOT NULL,\n\t`expires_at` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`accepted_at` integer\n);\n--> statement-breakpoint\nALTER TABLE users_to_workspaces ADD `role` text DEFAULT 'owner' NOT NULL;"
  },
  {
    "path": "packages/db/drizzle/0013_tired_paladin.sql",
    "content": "CREATE TABLE `page_subscriber` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`email` text NOT NULL,\n\t`page_id` integer,\n\t`token` text,\n\t`accepted_at` integer,\n\t`expires_at` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE no action\n);\n"
  },
  {
    "path": "packages/db/drizzle/0014_adorable_skaar.sql",
    "content": "DROP TABLE IF EXISTS `page_subscriber`;\n--> statement-breakpoint\n\nCREATE TABLE `page_subscriber` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`email` text NOT NULL,\n\t`page_id` integer NOT NULL,\n\t`token` text,\n\t`accepted_at` integer,\n\t`expires_at` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE no action\n);\n"
  },
  {
    "path": "packages/db/drizzle/0015_bent_sister_grimm.sql",
    "content": "CREATE TABLE `incident` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`title` text DEFAULT '' NOT NULL,\n\t`summary` text DEFAULT '' NOT NULL,\n\t`status` text DEFAULT 'triage' NOT NULL,\n\t`monitor_id` integer,\n\t`workspace_id` integer,\n\t`started_at` integer,\n\t`acknowledged_at` integer,\n\t`acknowledged_by` integer,\n\t`resolved_at` integer,\n\t`resolved_by` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`acknowledged_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`resolved_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action\n);\n"
  },
  {
    "path": "packages/db/drizzle/0016_certain_praxagora.sql",
    "content": "CREATE TABLE `incident_new` (\n    `id` integer PRIMARY KEY NOT NULL,\n\t`title` text DEFAULT '' NOT NULL,\n\t`summary` text DEFAULT '' NOT NULL,\n\t`status` text DEFAULT 'triage' NOT NULL,\n\t`monitor_id` integer,\n\t`workspace_id` integer,\n\t`started_at` integer DEFAULT (strftime('%s', 'now')) NOT NULL,\n\t`acknowledged_at` integer,\n\t`acknowledged_by` integer,\n\t`resolved_at` integer,\n\t`resolved_by` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE set default,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`acknowledged_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`resolved_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `composite_incident_new_id_started_at_unique` ON `incident_new` (`id`,`started_at`);\n--> statement-breakpoint\nINSERT INTO incident_new(`id`,`title`,`summary`,`status`,`monitor_id`,`workspace_id`,`started_at`,`acknowledged_at`,`acknowledged_by`,`resolved_at`,`resolved_by`,`created_at`,`updated_at`) SELECT * FROM incident;\n--> statement-breakpoint\nDROP TABLE incident;\n--> statement-breakpoint\nALTER TABLE incident_new RENAME TO incident;"
  },
  {
    "path": "packages/db/drizzle/0017_loose_maggott.sql",
    "content": "ALTER TABLE status_report_to_monitors ADD `created_at` integer DEFAULT (strftime('%s', 'now')); --> statement-breakpoint\nALTER TABLE status_reports_to_pages ADD `created_at` integer DEFAULT (strftime('%s', 'now')); --> statement-breakpoint\nALTER TABLE monitors_to_pages ADD `created_at` integer DEFAULT (strftime('%s', 'now')); --> statement-breakpoint\nALTER TABLE users_to_workspaces ADD `created_at` integer DEFAULT (strftime('%s', 'now')); --> statement-breakpoint\nALTER TABLE notifications_to_monitors ADD `created_at` integer DEFAULT (strftime('%s', 'now'));"
  },
  {
    "path": "packages/db/drizzle/0018_neat_orphan.sql",
    "content": "ALTER TABLE incident ADD `auto_resolved` integer DEFAULT false;"
  },
  {
    "path": "packages/db/drizzle/0019_dashing_malcolm_colcord.sql",
    "content": "DROP INDEX IF EXISTS `incident_monitor_id_started_at_unique`;--> statement-breakpoint\nDROP INDEX IF EXISTS `composite_incident_new_id_started_at_unique`;--> statement-breakpoint\nCREATE UNIQUE INDEX `incident_id_monitor_id_started_at_unique` ON `incident` (`id`,`monitor_id`,`started_at`);"
  },
  {
    "path": "packages/db/drizzle/0020_flat_bedlam.sql",
    "content": "DROP INDEX IF EXISTS `incident_id_monitor_id_started_at_unique`;--> statement-breakpoint\nCREATE UNIQUE INDEX `incident_monitor_id_started_at_unique` ON `incident` (`monitor_id`,`started_at`);"
  },
  {
    "path": "packages/db/drizzle/0021_reflective_nico_minoru.sql",
    "content": "CREATE TABLE `monitor_tag` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`name` text NOT NULL,\n\t`color` text NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `monitor_tag_to_monitor` (\n\t`monitor_id` integer NOT NULL,\n\t`monitor_tag_id` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`monitor_id`, `monitor_tag_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`monitor_tag_id`) REFERENCES `monitor_tag`(`id`) ON UPDATE no action ON DELETE cascade\n);\n"
  },
  {
    "path": "packages/db/drizzle/0022_chunky_rockslide.sql",
    "content": "ALTER TABLE monitor ADD `assertions` text;"
  },
  {
    "path": "packages/db/drizzle/0023_dry_blink.sql",
    "content": "ALTER TABLE incident ADD `incident_screenshot_url` text;--> statement-breakpoint\nALTER TABLE incident ADD `recovery_screenshot_url` text;"
  },
  {
    "path": "packages/db/drizzle/0024_young_proudstar.sql",
    "content": "ALTER TABLE monitor ADD `deleted_at` integer;"
  },
  {
    "path": "packages/db/drizzle/0025_strong_thunderball.sql",
    "content": "ALTER TABLE monitor ADD `public` integer DEFAULT false;"
  },
  {
    "path": "packages/db/drizzle/0026_giant_absorbing_man.sql",
    "content": "ALTER TABLE workspace ADD `dsn` text;--> statement-breakpoint\nCREATE UNIQUE INDEX `workspace_id_dsn_unique` ON `workspace` (`id`,`dsn`);"
  },
  {
    "path": "packages/db/drizzle/0027_bizarre_bastion.sql",
    "content": "ALTER TABLE page ADD `password` text(256);--> statement-breakpoint\nALTER TABLE page ADD `password_protected` integer DEFAULT false;"
  },
  {
    "path": "packages/db/drizzle/0028_thin_power_pack.sql",
    "content": "CREATE TABLE `account` (\n\t`user_id` integer NOT NULL,\n\t`type` text NOT NULL,\n\t`provider` text NOT NULL,\n\t`provider_account_id` text NOT NULL,\n\t`refresh_token` text,\n\t`access_token` text,\n\t`expires_at` integer,\n\t`token_type` text,\n\t`scope` text,\n\t`id_token` text,\n\t`session_state` text,\n\tPRIMARY KEY(`provider`, `provider_account_id`),\n\tFOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `session` (\n\t`session_token` text PRIMARY KEY NOT NULL,\n\t`user_id` integer NOT NULL,\n\t`expires` integer NOT NULL,\n\tFOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `verification_token` (\n\t`identifier` text NOT NULL,\n\t`token` text NOT NULL,\n\t`expires` integer NOT NULL,\n\tPRIMARY KEY(`identifier`, `token`)\n);\n--> statement-breakpoint\nALTER TABLE user ADD `name` text;--> statement-breakpoint\nALTER TABLE user ADD `emailVerified` integer;"
  },
  {
    "path": "packages/db/drizzle/0029_regular_marrow.sql",
    "content": "ALTER TABLE `monitors_to_pages` ADD `order` integer DEFAULT 0;"
  },
  {
    "path": "packages/db/drizzle/0030_elite_barracuda.sql",
    "content": "CREATE TABLE `application` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text,\n\t`dsn` text,\n\t`workspace_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `application_dsn_unique` ON `application` (`dsn`);"
  },
  {
    "path": "packages/db/drizzle/0031_lowly_gabe_jones.sql",
    "content": "CREATE TABLE `maintenance` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`title` text(256) NOT NULL,\n\t`message` text NOT NULL,\n\t`from` integer NOT NULL,\n\t`to` integer NOT NULL,\n\t`workspace_id` integer,\n\t`page_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE TABLE `maintenance_to_monitor` (\n\t`monitor_id` integer NOT NULL,\n\t`maintenance_id` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`maintenance_id`, `monitor_id`),\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`maintenance_id`) REFERENCES `maintenance`(`id`) ON UPDATE no action ON DELETE cascade\n);\n"
  },
  {
    "path": "packages/db/drizzle/0032_hot_swordsman.sql",
    "content": "CREATE TABLE `check` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`regions` text DEFAULT '' NOT NULL,\n\t`url` text(4096) NOT NULL,\n\t`headers` text DEFAULT '',\n\t`body` text DEFAULT '',\n\t`method` text DEFAULT 'GET',\n\t`count_requests` integer DEFAULT 1,\n\t`workspace_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n"
  },
  {
    "path": "packages/db/drizzle/0033_solid_colossus.sql",
    "content": "ALTER TABLE `monitor` ADD `timeout` integer DEFAULT 45000 NOT NULL;--> statement-breakpoint\nALTER TABLE `monitor` ADD `degraded_after` integer;"
  },
  {
    "path": "packages/db/drizzle/0034_serious_shard.sql",
    "content": "ALTER TABLE `status_report` ADD `page_id` integer REFERENCES page(id);--> statement-breakpoint\n\nUPDATE `status_report` SET `page_id` = `t`.`page_id` from (select `page_id`, `status_report_id` from `status_reports_to_pages`) `t` where `t`.`status_report_id` = `id` ;"
  },
  {
    "path": "packages/db/drizzle/0035_open_the_professor.sql",
    "content": "ALTER TABLE `workspace` ADD `limits` text DEFAULT '{}' NOT NULL;"
  },
  {
    "path": "packages/db/drizzle/0036_gifted_deathbird.sql",
    "content": "UPDATE `monitor` SET `job_type` = 'http' ;"
  },
  {
    "path": "packages/db/drizzle/0037_equal_beyonder.sql",
    "content": "ALTER TABLE `page` ADD `show_monitor_values` integer DEFAULT true;"
  },
  {
    "path": "packages/db/drizzle/0038_foamy_stardust.sql",
    "content": "CREATE TABLE `monitor_run` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer,\n\t`monitor_id` integer,\n\t`runned_at` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE no action\n);\n"
  },
  {
    "path": "packages/db/drizzle/0039_lonely_jigsaw.sql",
    "content": "ALTER TABLE `monitor` ADD `otel_endpoint` text;--> statement-breakpoint\nALTER TABLE `monitor` ADD `otel_headers` text;"
  },
  {
    "path": "packages/db/drizzle/0040_narrow_anthem.sql",
    "content": "PRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_page_subscriber` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`email` text NOT NULL,\n\t`page_id` integer NOT NULL,\n\t`token` text,\n\t`accepted_at` integer,\n\t`expires_at` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nINSERT INTO `__new_page_subscriber`(\"id\", \"email\", \"page_id\", \"token\", \"accepted_at\", \"expires_at\", \"created_at\", \"updated_at\") SELECT \"id\", \"email\", \"page_id\", \"token\", \"accepted_at\", \"expires_at\", \"created_at\", \"updated_at\" FROM `page_subscriber`;--> statement-breakpoint\nDROP TABLE `page_subscriber`;--> statement-breakpoint\nALTER TABLE `__new_page_subscriber` RENAME TO `page_subscriber`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;"
  },
  {
    "path": "packages/db/drizzle/0041_nasty_jigsaw.sql",
    "content": "CREATE TABLE `notification_trigger` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`monitor_id` integer,\n\t`notification_id` integer,\n\t`cron_timestamp` integer NOT NULL,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`notification_id`) REFERENCES `notification`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `notification_id_monitor_id_crontimestampe` ON `notification_trigger` (`notification_id`,`monitor_id`,`cron_timestamp`);--> statement-breakpoint\nDROP INDEX IF EXISTS \"page_slug_unique\";--> statement-breakpoint\nDROP INDEX IF EXISTS \"workspace_slug_unique\";--> statement-breakpoint\nDROP INDEX IF EXISTS \"workspace_stripe_id_unique\";--> statement-breakpoint\nDROP INDEX IF EXISTS \"workspace_id_dsn_unique\";--> statement-breakpoint\nDROP INDEX IF EXISTS \"user_tenant_id_unique\";--> statement-breakpoint\nDROP INDEX IF EXISTS \"notification_id_monitor_id_crontimestampe\";--> statement-breakpoint\nDROP INDEX IF EXISTS \"monitor_status_idx\";--> statement-breakpoint\nDROP INDEX IF EXISTS \"incident_monitor_id_started_at_unique\";--> statement-breakpoint\nDROP INDEX IF EXISTS \"application_dsn_unique\";--> statement-breakpoint\nALTER TABLE `status_report_update` ALTER COLUMN \"status\" TO \"status\" text NOT NULL;--> statement-breakpoint\nCREATE UNIQUE INDEX `page_slug_unique` ON `page` (`slug`);--> statement-breakpoint\nCREATE UNIQUE INDEX `workspace_slug_unique` ON `workspace` (`slug`);--> statement-breakpoint\nCREATE UNIQUE INDEX `workspace_stripe_id_unique` ON `workspace` (`stripe_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `workspace_id_dsn_unique` ON `workspace` (`id`,`dsn`);--> statement-breakpoint\nCREATE UNIQUE INDEX `user_tenant_id_unique` ON `user` (`tenant_id`);--> statement-breakpoint\nCREATE INDEX `monitor_status_idx` ON `monitor_status` (`monitor_id`,`region`);--> statement-breakpoint\nCREATE UNIQUE INDEX `incident_monitor_id_started_at_unique` ON `incident` (`monitor_id`,`started_at`);--> statement-breakpoint\nCREATE UNIQUE INDEX `application_dsn_unique` ON `application` (`dsn`);"
  },
  {
    "path": "packages/db/drizzle/0042_great_epoch.sql",
    "content": "PRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_status_report` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`status` text NOT NULL,\n\t`title` text(256) NOT NULL,\n\t`workspace_id` integer,\n\t`page_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nINSERT INTO `__new_status_report`(\"id\", \"status\", \"title\", \"workspace_id\", \"page_id\", \"created_at\", \"updated_at\") SELECT \"id\", \"status\", \"title\", \"workspace_id\", \"page_id\", \"created_at\", \"updated_at\" FROM `status_report`;--> statement-breakpoint\nDROP TABLE `status_report`;--> statement-breakpoint\nALTER TABLE `__new_status_report` RENAME TO `status_report`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE TABLE `__new_notification_trigger` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`monitor_id` integer,\n\t`notification_id` integer,\n\t`cron_timestamp` integer NOT NULL,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`notification_id`) REFERENCES `notification`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nINSERT INTO `__new_notification_trigger`(\"id\", \"monitor_id\", \"notification_id\", \"cron_timestamp\") SELECT \"id\", \"monitor_id\", \"notification_id\", \"cron_timestamp\" FROM `notification_trigger`;--> statement-breakpoint\nDROP TABLE `notification_trigger`;--> statement-breakpoint\nALTER TABLE `__new_notification_trigger` RENAME TO `notification_trigger`;--> statement-breakpoint\nCREATE UNIQUE INDEX `notification_id_monitor_id_crontimestampe` ON `notification_trigger` (`notification_id`,`monitor_id`,`cron_timestamp`);--> statement-breakpoint\nCREATE TABLE `__new_maintenance` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`title` text(256) NOT NULL,\n\t`message` text NOT NULL,\n\t`from` integer NOT NULL,\n\t`to` integer NOT NULL,\n\t`workspace_id` integer,\n\t`page_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nINSERT INTO `__new_maintenance`(\"id\", \"title\", \"message\", \"from\", \"to\", \"workspace_id\", \"page_id\", \"created_at\", \"updated_at\") SELECT \"id\", \"title\", \"message\", \"from\", \"to\", \"workspace_id\", \"page_id\", \"created_at\", \"updated_at\" FROM `maintenance`;--> statement-breakpoint\nDROP TABLE `maintenance`;--> statement-breakpoint\nALTER TABLE `__new_maintenance` RENAME TO `maintenance`;"
  },
  {
    "path": "packages/db/drizzle/0043_low_lily_hollister.sql",
    "content": "ALTER TABLE `monitor` ADD `retry` integer DEFAULT 3;"
  },
  {
    "path": "packages/db/drizzle/0044_illegal_turbo.sql",
    "content": "ALTER TABLE `page` ADD `force_theme` text DEFAULT 'system' NOT NULL;"
  },
  {
    "path": "packages/db/drizzle/0045_little_paladin.sql",
    "content": "ALTER TABLE `monitor` ADD `follow_redirects` integer DEFAULT true;"
  },
  {
    "path": "packages/db/drizzle/0046_lucky_tarantula.sql",
    "content": "ALTER TABLE `page` ADD `legacy_page` integer DEFAULT true NOT NULL;--> statement-breakpoint\nALTER TABLE `page` ADD `configuration` text;"
  },
  {
    "path": "packages/db/drizzle/0047_nifty_roughhouse.sql",
    "content": "ALTER TABLE `page` ADD `homepage_url` text(256);--> statement-breakpoint\nALTER TABLE `page` ADD `contact_url` text(256);"
  },
  {
    "path": "packages/db/drizzle/0048_neat_tempest.sql",
    "content": "CREATE TABLE `private_location` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`token` text NOT NULL,\n\t`last_seen_at` integer,\n\t`workspace_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE TABLE `private_location_to_monitor` (\n\t`private_location_id` integer,\n\t`monitor_id` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`deleted_at` integer,\n\tFOREIGN KEY (`private_location_id`) REFERENCES `private_location`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade\n);\n"
  },
  {
    "path": "packages/db/drizzle/0049_sloppy_inhumans.sql",
    "content": "CREATE TABLE `monitor_group` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`page_id` integer NOT NULL,\n\t`name` text NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nALTER TABLE `monitors_to_pages` ADD `monitor_group_id` integer REFERENCES monitor_group(id);--> statement-breakpoint\nALTER TABLE `monitors_to_pages` ADD `group_order` integer DEFAULT 0;"
  },
  {
    "path": "packages/db/drizzle/0050_damp_xorn.sql",
    "content": "ALTER TABLE `monitor` ADD `external_name` text;"
  },
  {
    "path": "packages/db/drizzle/0051_fuzzy_red_hulk.sql",
    "content": "CREATE TABLE `viewer` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`name` text,\n\t`email` text,\n\t`emailVerified` integer,\n\t`image` text,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now'))\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `viewer_email_unique` ON `viewer` (`email`);--> statement-breakpoint\nCREATE TABLE `viewer_accounts` (\n\t`user_id` text NOT NULL,\n\t`type` text NOT NULL,\n\t`provider` text NOT NULL,\n\t`providerAccountId` text NOT NULL,\n\t`refresh_token` text,\n\t`access_token` text,\n\t`expires_at` integer,\n\t`token_type` text,\n\t`scope` text,\n\t`id_token` text,\n\t`session_state` text,\n\tPRIMARY KEY(`provider`, `providerAccountId`),\n\tFOREIGN KEY (`user_id`) REFERENCES `viewer`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `viewer_session` (\n\t`session_token` text PRIMARY KEY NOT NULL,\n\t`user_id` integer NOT NULL,\n\t`expires` integer NOT NULL,\n\tFOREIGN KEY (`user_id`) REFERENCES `viewer`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nALTER TABLE `page` ADD `access_type` text DEFAULT 'public';--> statement-breakpoint\nALTER TABLE `page` ADD `auth_email_domains` text;--> statement-breakpoint\n-- NOTE: manual migration to set the access type based on the password column\nUPDATE `page` SET `access_type` = 'password' WHERE `password` IS NOT NULL AND `password` != '' AND `password_protected` = 1;"
  },
  {
    "path": "packages/db/drizzle/0052_illegal_killraven.sql",
    "content": "CREATE TABLE `api_key` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`name` text NOT NULL,\n\t`description` text,\n\t`prefix` text NOT NULL,\n\t`hashed_token` text NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`created_by_id` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`expires_at` integer,\n\t`last_used_at` integer,\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`created_by_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `api_key_prefix_unique` ON `api_key` (`prefix`);--> statement-breakpoint\nCREATE UNIQUE INDEX `api_key_hashed_token_unique` ON `api_key` (`hashed_token`);--> statement-breakpoint\nCREATE INDEX `api_key_prefix_idx` ON `api_key` (`prefix`);"
  },
  {
    "path": "packages/db/drizzle/0053_dark_orphan.sql",
    "content": "ALTER TABLE `page_subscriber` ADD `unsubscribed_at` integer;"
  },
  {
    "path": "packages/db/drizzle/0054_bitter_lilandra.sql",
    "content": "CREATE TABLE `maintenance_to_page_component` (\n\t`maintenance_id` integer NOT NULL,\n\t`page_component_id` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`maintenance_id`, `page_component_id`),\n\tFOREIGN KEY (`maintenance_id`) REFERENCES `maintenance`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`page_component_id`) REFERENCES `page_component`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `page_component` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`page_id` integer NOT NULL,\n\t`type` text DEFAULT 'monitor' NOT NULL,\n\t`monitor_id` integer,\n\t`name` text NOT NULL,\n\t`description` text,\n\t`order` integer DEFAULT 0,\n\t`group_id` integer,\n\t`group_order` integer DEFAULT 0,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`group_id`) REFERENCES `page_component_groups`(`id`) ON UPDATE no action ON DELETE set null,\n\tCONSTRAINT \"page_component_type_check\" CHECK(\"page_component\".\"type\" = 'monitor' AND \"page_component\".\"monitor_id\" IS NOT NULL OR \"page_component\".\"type\" = 'external' AND \"page_component\".\"monitor_id\" IS NULL)\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `page_component_page_id_monitor_id_unique` ON `page_component` (`page_id`,`monitor_id`);--> statement-breakpoint\nCREATE TABLE `status_report_to_page_component` (\n\t`status_report_id` integer NOT NULL,\n\t`page_component_id` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`status_report_id`, `page_component_id`),\n\tFOREIGN KEY (`status_report_id`) REFERENCES `status_report`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`page_component_id`) REFERENCES `page_component`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `page_component_groups` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`page_id` integer NOT NULL,\n\t`name` text NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\n-- Data Migration: monitors_to_pages → page_component\n-- This section migrates data from the old structure to the new page_component structure\n-- Step 0: Migrate monitor_group to page_component_groups\nINSERT OR IGNORE INTO `page_component_groups` (\n\t`id`,\n\t`workspace_id`,\n\t`page_id`,\n\t`name`,\n\t`created_at`,\n\t`updated_at`\n)\nSELECT \n\tmg.id,\n\tmg.workspace_id,\n\tmg.page_id,\n\tmg.name,\n\tmg.created_at,\n\tmg.updated_at\nFROM `monitor_group` mg;\n--> statement-breakpoint\n-- Step 1: Migrate monitors_to_pages to page_component\nINSERT OR IGNORE INTO `page_component` (\n\t`workspace_id`,\n\t`page_id`,\n\t`type`,\n\t`monitor_id`,\n\t`name`,\n\t`order`,\n\t`group_id`,\n\t`group_order`,\n\t`created_at`,\n\t`updated_at`\n)\nSELECT \n\tm.workspace_id,\n\tmtp.page_id,\n\t'monitor' as type,\n\tmtp.monitor_id,\n\tCOALESCE(m.external_name, m.name, 'Unnamed Monitor') as name,\n\tCOALESCE(mtp.`order`, 0),\n\tmtp.monitor_group_id as group_id,\n\tCOALESCE(mtp.group_order, 0) as group_order,\n\tmtp.created_at,\n\tstrftime('%s', 'now') as updated_at\nFROM `monitors_to_pages` mtp\nINNER JOIN `monitor` m ON mtp.monitor_id = m.id\nWHERE m.workspace_id IS NOT NULL;\n--> statement-breakpoint\n-- Step 2: Migrate status_report_to_monitors to status_report_to_page_component\nINSERT OR IGNORE INTO `status_report_to_page_component` (\n\t`status_report_id`,\n\t`page_component_id`,\n\t`created_at`\n)\nSELECT DISTINCT\n\tsrtm.status_report_id,\n\tpc.id as page_component_id,\n\tsrtm.created_at\nFROM `status_report_to_monitors` srtm\nINNER JOIN `status_report` sr ON srtm.status_report_id = sr.id\nINNER JOIN `page_component` pc ON srtm.monitor_id = pc.monitor_id\nWHERE (sr.page_id = pc.page_id OR sr.page_id IS NULL);\n--> statement-breakpoint\n-- Step 3: Migrate maintenance_to_monitor to maintenance_to_page_component\nINSERT OR IGNORE INTO `maintenance_to_page_component` (\n\t`maintenance_id`,\n\t`page_component_id`,\n\t`created_at`\n)\nSELECT DISTINCT\n\tmtm.maintenance_id,\n\tpc.id as page_component_id,\n\tmtm.created_at\nFROM `maintenance_to_monitor` mtm\nINNER JOIN `maintenance` m ON mtm.maintenance_id = m.id\nINNER JOIN `page_component` pc ON mtm.monitor_id = pc.monitor_id\nWHERE (m.page_id = pc.page_id OR m.page_id IS NULL);"
  },
  {
    "path": "packages/db/drizzle/0055_spicy_bastion.sql",
    "content": "PRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_page_component` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`workspace_id` integer NOT NULL,\n\t`page_id` integer NOT NULL,\n\t`type` text DEFAULT 'monitor' NOT NULL,\n\t`monitor_id` integer,\n\t`name` text NOT NULL,\n\t`description` text,\n\t`order` integer DEFAULT 0,\n\t`group_id` integer,\n\t`group_order` integer DEFAULT 0,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`group_id`) REFERENCES `page_component_groups`(`id`) ON UPDATE no action ON DELETE set null,\n\tCONSTRAINT \"page_component_type_check\" CHECK(\"__new_page_component\".\"type\" = 'monitor' AND \"__new_page_component\".\"monitor_id\" IS NOT NULL OR \"__new_page_component\".\"type\" = 'static' AND \"__new_page_component\".\"monitor_id\" IS NULL)\n);\n--> statement-breakpoint\nINSERT INTO `__new_page_component`(\"id\", \"workspace_id\", \"page_id\", \"type\", \"monitor_id\", \"name\", \"description\", \"order\", \"group_id\", \"group_order\", \"created_at\", \"updated_at\") SELECT \"id\", \"workspace_id\", \"page_id\", \"type\", \"monitor_id\", \"name\", \"description\", \"order\", \"group_id\", \"group_order\", \"created_at\", \"updated_at\" FROM `page_component`;--> statement-breakpoint\nDROP TABLE `page_component`;--> statement-breakpoint\nALTER TABLE `__new_page_component` RENAME TO `page_component`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `page_component_page_id_monitor_id_unique` ON `page_component` (`page_id`,`monitor_id`);"
  },
  {
    "path": "packages/db/drizzle/0056_violet_shotgun.sql",
    "content": "CREATE TABLE `page_subscriber_to_page_component` (\n\t`page_subscriber_id` integer NOT NULL,\n\t`page_component_id` integer NOT NULL,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\tPRIMARY KEY(`page_subscriber_id`, `page_component_id`),\n\tFOREIGN KEY (`page_subscriber_id`) REFERENCES `page_subscriber`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`page_component_id`) REFERENCES `page_component`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_page_subscriber` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`email` text NOT NULL,\n\t`page_id` integer NOT NULL,\n\t`channel_type` text DEFAULT 'email' NOT NULL,\n\t`webhook_url` text,\n\t`channel_config` text,\n\t`token` text,\n\t`accepted_at` integer,\n\t`expires_at` integer,\n\t`unsubscribed_at` integer,\n\t`created_at` integer DEFAULT (strftime('%s', 'now')),\n\t`updated_at` integer DEFAULT (strftime('%s', 'now')),\n\tFOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade,\n\tCONSTRAINT \"page_subscriber_channel_check\" CHECK((\"__new_page_subscriber\".\"channel_type\" = 'email' AND \"__new_page_subscriber\".\"email\" IS NOT NULL AND \"__new_page_subscriber\".\"webhook_url\" IS NULL) OR (\"__new_page_subscriber\".\"channel_type\" = 'webhook' AND \"__new_page_subscriber\".\"webhook_url\" IS NOT NULL AND \"__new_page_subscriber\".\"email\" IS NULL))\n);\n--> statement-breakpoint\nINSERT INTO `__new_page_subscriber`(\"id\", \"email\", \"page_id\", \"channel_type\", \"webhook_url\", \"channel_config\", \"token\", \"accepted_at\", \"expires_at\", \"unsubscribed_at\", \"created_at\", \"updated_at\") SELECT \"id\", \"email\", \"page_id\", 'email', NULL, NULL, \"token\", \"accepted_at\", \"expires_at\", \"unsubscribed_at\", \"created_at\", \"updated_at\" FROM `page_subscriber`;--> statement-breakpoint\nDROP TABLE `page_subscriber`;--> statement-breakpoint\nALTER TABLE `__new_page_subscriber` RENAME TO `page_subscriber`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `idx_page_subscriber_email_page_active` ON `page_subscriber` (LOWER(\"email\"),`page_id`) WHERE \"page_subscriber\".\"unsubscribed_at\" IS NULL AND \"page_subscriber\".\"channel_type\" = 'email';--> statement-breakpoint\nCREATE UNIQUE INDEX `idx_page_subscriber_webhook_page_active` ON `page_subscriber` (`webhook_url`,`page_id`) WHERE \"page_subscriber\".\"unsubscribed_at\" IS NULL AND \"page_subscriber\".\"channel_type\" = 'webhook';"
  },
  {
    "path": "packages/db/drizzle/0057_curious_xorn.sql",
    "content": "ALTER TABLE `user` ADD `deleted_at` integer;"
  },
  {
    "path": "packages/db/drizzle/0058_absent_chameleon.sql",
    "content": "DROP TABLE `status_report_to_monitors`;--> statement-breakpoint\nDROP TABLE `monitors_to_pages`;--> statement-breakpoint\nDROP TABLE `maintenance_to_monitor`;"
  },
  {
    "path": "packages/db/drizzle/meta/0000_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_page_id_page_id_fk\": {\n          \"name\": \"incident_page_id_page_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"uuid\": {\n          \"name\": \"uuid\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_date\": {\n          \"name\": \"incident_date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_update_uuid_unique\": {\n          \"name\": \"incident_update_uuid_unique\",\n          \"columns\": [\"uuid\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'inactive'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(512)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"41c8003d-9f2a-4d3e-9e11-b3d4076fe395\",\n  \"prevId\": \"00000000-0000-0000-0000-000000000000\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0001_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_page_id_page_id_fk\": {\n          \"name\": \"incident_page_id_page_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"uuid\": {\n          \"name\": \"uuid\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_date\": {\n          \"name\": \"incident_date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_update_uuid_unique\": {\n          \"name\": \"incident_update_uuid_unique\",\n          \"columns\": [\"uuid\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'inactive'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(512)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"762acc59-e72a-4ca4-b3fa-2cfd65cdf49a\",\n  \"prevId\": \"41c8003d-9f2a-4d3e-9e11-b3d4076fe395\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0002_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_page_id_page_id_fk\": {\n          \"name\": \"incident_page_id_page_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"uuid\": {\n          \"name\": \"uuid\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_date\": {\n          \"name\": \"incident_date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_update_uuid_unique\": {\n          \"name\": \"incident_update_uuid_unique\",\n          \"columns\": [\"uuid\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'inactive'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(512)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"84c77cb9-dca1-424b-8929-c31f24ad5144\",\n  \"prevId\": \"762acc59-e72a-4ca4-b3fa-2cfd65cdf49a\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0003_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\n            \"incident_id\"\n          ],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_monitors\": {\n      \"name\": \"incidents_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"incidents_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_monitors_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_monitors_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\n            \"incident_id\"\n          ],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_monitors_monitor_id_incident_id_pk\": {\n          \"columns\": [\n            \"incident_id\",\n            \"monitor_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'inactive'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(512)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"f68fee05-786b-45d6-9e08-f793e7591577\",\n  \"prevId\": \"84c77cb9-dca1-424b-8929-c31f24ad5144\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0004_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_monitors\": {\n      \"name\": \"incidents_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"incidents_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_monitors_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_monitors_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_monitors_monitor_id_incident_id_pk\": {\n          \"columns\": [\"incident_id\", \"monitor_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'inactive'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(512)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"c7088c9f-2521-43af-884e-b7b0b81b0cb9\",\n  \"prevId\": \"f68fee05-786b-45d6-9e08-f793e7591577\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0005_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_monitors\": {\n      \"name\": \"incidents_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"incidents_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_monitors_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_monitors_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_monitors_monitor_id_incident_id_pk\": {\n          \"columns\": [\"incident_id\", \"monitor_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'inactive'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(512)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"84447f0c-af79-4add-93d7-eb6868983258\",\n  \"prevId\": \"c7088c9f-2521-43af-884e-b7b0b81b0cb9\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0006_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\n            \"incident_id\"\n          ],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_monitors\": {\n      \"name\": \"incidents_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"incidents_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_monitors_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_monitors_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\n            \"incident_id\"\n          ],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_monitors_monitor_id_incident_id_pk\": {\n          \"columns\": [\n            \"incident_id\",\n            \"monitor_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'inactive'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(512)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"fee77e21-b52b-49c0-b109-5ccc37821935\",\n  \"prevId\": \"84447f0c-af79-4add-93d7-eb6868983258\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0007_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_monitors\": {\n      \"name\": \"incidents_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"incidents_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_monitors_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_monitors_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_monitors_monitor_id_incident_id_pk\": {\n          \"columns\": [\"incident_id\", \"monitor_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'inactive'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(512)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"23bfc90d-3bb1-4077-81d8-8f2e874f9c62\",\n  \"prevId\": \"fee77e21-b52b-49c0-b109-5ccc37821935\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0008_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\n            \"incident_id\"\n          ],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_monitors\": {\n      \"name\": \"incidents_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"incidents_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_monitors_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_monitors_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\n            \"incident_id\"\n          ],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_monitors_monitor_id_incident_id_pk\": {\n          \"columns\": [\n            \"incident_id\",\n            \"monitor_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"72f58965-4d96-43e5-9101-3ceb67dac648\",\n  \"prevId\": \"23bfc90d-3bb1-4077-81d8-8f2e874f9c62\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0009_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_monitors\": {\n      \"name\": \"incidents_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"incidents_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_monitors_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_monitors_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_monitors_monitor_id_incident_id_pk\": {\n          \"columns\": [\"incident_id\", \"monitor_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_pages\": {\n      \"name\": \"incidents_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_pages_page_id_page_id_fk\": {\n          \"name\": \"incidents_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"incidents_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_pages_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_pages_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_pages\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_pages_page_id_incident_id_pk\": {\n          \"columns\": [\"incident_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text(3)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text(6)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text(2)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"0f3dddb6-09cf-46a4-b12c-c5bb82475b94\",\n  \"prevId\": \"72f58965-4d96-43e5-9101-3ceb67dac648\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0010_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident_update\": {\n      \"name\": \"incident_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_update_incident_id_incident_id_fk\": {\n          \"name\": \"incident_update_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incident_update\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_monitors\": {\n      \"name\": \"incidents_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"incidents_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_monitors_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_monitors_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_monitors\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_monitors_monitor_id_incident_id_pk\": {\n          \"columns\": [\"incident_id\", \"monitor_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"incidents_to_pages\": {\n      \"name\": \"incidents_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incidents_to_pages_page_id_page_id_fk\": {\n          \"name\": \"incidents_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"incidents_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"incidents_to_pages_incident_id_incident_id_fk\": {\n          \"name\": \"incidents_to_pages_incident_id_incident_id_fk\",\n          \"tableFrom\": \"incidents_to_pages\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"incident\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"incidents_to_pages_page_id_incident_id_pk\": {\n          \"columns\": [\"incident_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"452e7037-85cf-4d9a-b4e2-375a695d5fa8\",\n  \"prevId\": \"0f3dddb6-09cf-46a4-b12c-c5bb82475b94\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0011_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_incident_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_incident_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_incident_id_pk\": {\n          \"columns\": [\"incident_id\", \"monitor_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_incident_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_incident_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_incident_id_pk\": {\n          \"columns\": [\"incident_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"incident_id\": {\n          \"name\": \"incident_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_incident_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_incident_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"incident_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {\n      \"\\\"incidents_to_monitors\\\"\": \"\\\"status_report_to_monitors\\\"\",\n      \"\\\"incidents_to_pages\\\"\": \"\\\"status_reports_to_pages\\\"\",\n      \"\\\"incident_update\\\"\": \"\\\"status_report_update\\\"\",\n      \"\\\"incident\\\"\": \"\\\"status_report\\\"\"\n    },\n    \"columns\": {}\n  },\n  \"id\": \"21a5d3a2-6aab-4f67-8385-19d905e1a232\",\n  \"prevId\": \"452e7037-85cf-4d9a-b4e2-375a695d5fa8\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0012_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"b5c53ce9-981d-4a7c-a0da-c82e517fc0a3\",\n  \"prevId\": \"21a5d3a2-6aab-4f67-8385-19d905e1a232\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0013_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"5ac15aa5-4512-4f06-92e3-4410ce097034\",\n  \"prevId\": \"b5c53ce9-981d-4a7c-a0da-c82e517fc0a3\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0014_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"4cc8ce9b-ed70-47c1-8218-2cbc02743563\",\n  \"prevId\": \"5ac15aa5-4512-4f06-92e3-4410ce097034\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0015_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"acknowledged_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"resolved_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"f5a77857-319d-4575-b570-f0aacb9eab35\",\n  \"prevId\": \"4cc8ce9b-ed70-47c1-8218-2cbc02743563\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0016_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"d3626cd5-956f-4a64-9c70-02e5ee44cde0\",\n  \"prevId\": \"f5a77857-319d-4575-b570-f0aacb9eab35\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0017_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"]\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\"monitor_id\", \"started_at\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"acknowledged_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"resolved_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"bbc87c20-bc65-4132-913c-65154793a027\",\n  \"prevId\": \"d3626cd5-956f-4a64-9c70-02e5ee44cde0\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0018_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\"monitor_id\", \"started_at\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"acknowledged_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"resolved_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"8b74891f-e46d-4716-89a8-e2befaca5513\",\n  \"prevId\": \"bbc87c20-bc65-4132-913c-65154793a027\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0019_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_id_monitor_id_started_at_unique\": {\n          \"name\": \"incident_id_monitor_id_started_at_unique\",\n          \"columns\": [\"id\", \"monitor_id\", \"started_at\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"acknowledged_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"resolved_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"fed3b3e4-d55d-4370-b39b-c837512a6d8e\",\n  \"prevId\": \"8b74891f-e46d-4716-89a8-e2befaca5513\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0020_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\"monitor_id\", \"started_at\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"acknowledged_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"resolved_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"47000fd1-f273-4406-8380-803a0a63bc59\",\n  \"prevId\": \"fed3b3e4-d55d-4370-b39b-c837512a6d8e\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0021_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\"monitor_id\", \"started_at\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"acknowledged_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"resolved_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\"monitor_tag_id\"],\n          \"tableTo\": \"monitor_tag\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\"monitor_id\", \"monitor_tag_id\"],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"44c2bbb5-69d5-40e8-b301-f2569bc2e499\",\n  \"prevId\": \"47000fd1-f273-4406-8380-803a0a63bc59\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0022_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"tableTo\": \"monitor_tag\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"a53b01bf-cce5-411d-99cf-a0645f3bd125\",\n  \"prevId\": \"44c2bbb5-69d5-40e8-b301-f2569bc2e499\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0023_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"tableTo\": \"monitor_tag\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"a8569d87-b829-4372-9f00-1c61a956f117\",\n  \"prevId\": \"a53b01bf-cce5-411d-99cf-a0645f3bd125\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0024_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"tableTo\": \"monitor_tag\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"96f82403-94f3-49fd-871b-946bc0a687f2\",\n  \"prevId\": \"a8569d87-b829-4372-9f00-1c61a956f117\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0025_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"user_id\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\"page_id\"],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\"monitor_id\", \"started_at\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"acknowledged_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\"resolved_by\"],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\"monitor_tag_id\"],\n          \"tableTo\": \"monitor_tag\",\n          \"columnsTo\": [\"id\"],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\"monitor_id\", \"monitor_tag_id\"],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"40ddc769-9652-470e-87af-71f269131bc3\",\n  \"prevId\": \"96f82403-94f3-49fd-871b-946bc0a687f2\"\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0026_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"tableTo\": \"monitor_tag\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"0021b887-ceab-4294-a17c-34c902d14a12\",\n  \"prevId\": \"40ddc769-9652-470e-87af-71f269131bc3\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0027_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"tableTo\": \"monitor_tag\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"4488e03b-52ea-45eb-aac9-7d263c8f2e43\",\n  \"prevId\": \"0021b887-ceab-4294-a17c-34c902d14a12\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0028_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"tableTo\": \"status_report\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"tableTo\": \"page\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"tableTo\": \"notification\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set default\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"tableTo\": \"workspace\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"tableTo\": \"monitor\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"tableTo\": \"monitor_tag\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"715da58c-9c0f-42fe-920a-46f8c03c92ba\",\n  \"prevId\": \"4488e03b-52ea-45eb-aac9-7d263c8f2e43\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0029_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"45eca167-3e3b-43b0-b3b5-f25a910045f5\",\n  \"prevId\": \"715da58c-9c0f-42fe-920a-46f8c03c92ba\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0030_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"baadea64-aa8c-4b6f-b0e5-55ca15fc05b0\",\n  \"prevId\": \"45eca167-3e3b-43b0-b3b5-f25a910045f5\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0031_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"afc5bf87-9bfd-4753-af9f-edbf7b3351ba\",\n  \"prevId\": \"baadea64-aa8c-4b6f-b0e5-55ca15fc05b0\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_monitor_id_maintenance_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_monitor_id_maintenance_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0032_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"de3d58e5-d801-4b9e-a35a-39cfc09b7807\",\n  \"prevId\": \"afc5bf87-9bfd-4753-af9f-edbf7b3351ba\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\n            \"page_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_monitor_id_maintenance_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_monitor_id_maintenance_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0033_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"447f81c0-1837-4e85-bb0e-9ecce75e95d9\",\n  \"prevId\": \"de3d58e5-d801-4b9e-a35a-39cfc09b7807\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\"monitor_id\", \"status_report_id\"],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_reports_to_pages\": {\n      \"name\": \"status_reports_to_pages\",\n      \"columns\": {\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_reports_to_pages_page_id_page_id_fk\": {\n          \"name\": \"status_reports_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\"page_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_reports_to_pages_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_reports_to_pages_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_reports_to_pages\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_reports_to_pages_page_id_status_report_id_pk\": {\n          \"columns\": [\"page_id\", \"status_report_id\"],\n          \"name\": \"status_reports_to_pages_page_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\"status_report_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\"page_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\"monitor_id\", \"page_id\"],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"user_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\"provider\", \"provider_account_id\"],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"user_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\"tenant_id\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"user_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\"user_id\", \"workspace_id\"],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\"identifier\", \"token\"],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\"page_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\"slug\"],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\"stripe_id\"],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\"id\", \"dsn\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\"notification_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\"monitor_id\", \"notification_id\"],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\"monitor_id\", \"region\"],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\"monitor_id\", \"started_at\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"acknowledged_by\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"resolved_by\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\"monitor_tag_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\"monitor_id\", \"monitor_tag_id\"],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\"dsn\"],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\"page_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\"monitor_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\"maintenance_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_monitor_id_maintenance_id_pk\": {\n          \"columns\": [\"maintenance_id\", \"monitor_id\"],\n          \"name\": \"maintenance_to_monitor_monitor_id_maintenance_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\"workspace_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  }\n}\n"
  },
  {
    "path": "packages/db/drizzle/meta/0034_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"145f2782-3cb0-4066-9842-881a17636999\",\n  \"prevId\": \"447f81c0-1837-4e85-bb0e-9ecce75e95d9\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_monitor_id_maintenance_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_monitor_id_maintenance_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0035_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"d42f0027-40b6-4d43-9d76-160e0a6e35e7\",\n  \"prevId\": \"145f2782-3cb0-4066-9842-881a17636999\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_monitor_id_maintenance_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_monitor_id_maintenance_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0036_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"7321a546-31be-4313-8e3f-235db16ddee4\",\n  \"prevId\": \"d42f0027-40b6-4d43-9d76-160e0a6e35e7\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_monitor_id_maintenance_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_monitor_id_maintenance_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0037_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"09d93b56-dcc3-4148-934a-a490ab04bab9\",\n  \"prevId\": \"7321a546-31be-4313-8e3f-235db16ddee4\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_monitor_id_maintenance_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_monitor_id_maintenance_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0038_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"e284ec98-e715-4599-9f79-ecbf022ca7a2\",\n  \"prevId\": \"09d93b56-dcc3-4148-934a-a490ab04bab9\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_monitor_id_maintenance_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_monitor_id_maintenance_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0039_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"83c9c2dc-5d06-4b92-8604-86a5426a59d9\",\n  \"prevId\": \"e284ec98-e715-4599-9f79-ecbf022ca7a2\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0040_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"377a6822-a9c1-48a8-b07a-c18130fa682c\",\n  \"prevId\": \"83c9c2dc-5d06-4b92-8604-86a5426a59d9\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text(4)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0041_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"3c04b245-907b-4ae3-bd0b-73e1ecfc1526\",\n  \"prevId\": \"377a6822-a9c1-48a8-b07a-c18130fa682c\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0042_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"42f5ca77-ee56-4d55-bea0-c4767a026152\",\n  \"prevId\": \"3c04b245-907b-4ae3-bd0b-73e1ecfc1526\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0043_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"5ab3a069-5948-479e-8763-b5546564c0cc\",\n  \"prevId\": \"42f5ca77-ee56-4d55-bea0-c4767a026152\",\n  \"tables\": {\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0044_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"731c8921-4489-4e79-965c-e3d292b164dd\",\n  \"prevId\": \"5ab3a069-5948-479e-8763-b5546564c0cc\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0045_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"c5cd5994-2e75-43ff-b2e0-637f0fe87d88\",\n  \"prevId\": \"731c8921-4489-4e79-965c-e3d292b164dd\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0046_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"a46ba9e3-3ab0-4114-86d4-bf494a8c6220\",\n  \"prevId\": \"c5cd5994-2e75-43ff-b2e0-637f0fe87d88\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0047_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"686060ea-046b-414a-909f-1b087e086065\",\n  \"prevId\": \"a46ba9e3-3ab0-4114-86d4-bf494a8c6220\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0048_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"091d5fb3-ae4f-4ce2-a91f-695d054c0f79\",\n  \"prevId\": \"686060ea-046b-414a-909f-1b087e086065\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0049_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"7fd60946-e536-4fb1-863b-877d0d01dcc9\",\n  \"prevId\": \"091d5fb3-ae4f-4ce2-a91f-695d054c0f79\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"monitor_group_id\": {\n          \"name\": \"monitor_group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor_group\",\n          \"columnsFrom\": [\n            \"monitor_group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0050_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"d7863fda-51fb-4d7c-b9e6-fd1943ad5d71\",\n  \"prevId\": \"7fd60946-e536-4fb1-863b-877d0d01dcc9\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"external_name\": {\n          \"name\": \"external_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"monitor_group_id\": {\n          \"name\": \"monitor_group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor_group\",\n          \"columnsFrom\": [\n            \"monitor_group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0051_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"e8d9889c-8d28-4311-83e6-9fd28c840595\",\n  \"prevId\": \"d7863fda-51fb-4d7c-b9e6-fd1943ad5d71\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"access_type\": {\n          \"name\": \"access_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'public'\"\n        },\n        \"auth_email_domains\": {\n          \"name\": \"auth_email_domains\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"external_name\": {\n          \"name\": \"external_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"monitor_group_id\": {\n          \"name\": \"monitor_group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor_group\",\n          \"columnsFrom\": [\n            \"monitor_group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer\": {\n      \"name\": \"viewer\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"viewer_email_unique\": {\n          \"name\": \"viewer_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_accounts\": {\n      \"name\": \"viewer_accounts\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_accounts_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_accounts_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_accounts\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"viewer_accounts_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"viewer_accounts_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_session\": {\n      \"name\": \"viewer_session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_session_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_session_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_session\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0052_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"cc596587-5474-42a7-b638-1b7c771fbc5e\",\n  \"prevId\": \"e8d9889c-8d28-4311-83e6-9fd28c840595\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"access_type\": {\n          \"name\": \"access_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'public'\"\n        },\n        \"auth_email_domains\": {\n          \"name\": \"auth_email_domains\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"external_name\": {\n          \"name\": \"external_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"monitor_group_id\": {\n          \"name\": \"monitor_group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor_group\",\n          \"columnsFrom\": [\n            \"monitor_group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer\": {\n      \"name\": \"viewer\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"viewer_email_unique\": {\n          \"name\": \"viewer_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_accounts\": {\n      \"name\": \"viewer_accounts\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_accounts_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_accounts_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_accounts\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"viewer_accounts_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"viewer_accounts_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_session\": {\n      \"name\": \"viewer_session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_session_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_session_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_session\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"api_key\": {\n      \"name\": \"api_key\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"prefix\": {\n          \"name\": \"prefix\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"hashed_token\": {\n          \"name\": \"hashed_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_by_id\": {\n          \"name\": \"created_by_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"last_used_at\": {\n          \"name\": \"last_used_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"api_key_prefix_unique\": {\n          \"name\": \"api_key_prefix_unique\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_hashed_token_unique\": {\n          \"name\": \"api_key_hashed_token_unique\",\n          \"columns\": [\n            \"hashed_token\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_prefix_idx\": {\n          \"name\": \"api_key_prefix_idx\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"api_key_workspace_id_workspace_id_fk\": {\n          \"name\": \"api_key_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"api_key_created_by_id_user_id_fk\": {\n          \"name\": \"api_key_created_by_id_user_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"created_by_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0053_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"cd3deb34-e4c8-4ae2-aa4e-bc0f7d9517f5\",\n  \"prevId\": \"cc596587-5474-42a7-b638-1b7c771fbc5e\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"access_type\": {\n          \"name\": \"access_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'public'\"\n        },\n        \"auth_email_domains\": {\n          \"name\": \"auth_email_domains\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"external_name\": {\n          \"name\": \"external_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"monitor_group_id\": {\n          \"name\": \"monitor_group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor_group\",\n          \"columnsFrom\": [\n            \"monitor_group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"unsubscribed_at\": {\n          \"name\": \"unsubscribed_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer\": {\n      \"name\": \"viewer\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"viewer_email_unique\": {\n          \"name\": \"viewer_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_accounts\": {\n      \"name\": \"viewer_accounts\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_accounts_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_accounts_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_accounts\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"viewer_accounts_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"viewer_accounts_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_session\": {\n      \"name\": \"viewer_session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_session_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_session_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_session\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"api_key\": {\n      \"name\": \"api_key\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"prefix\": {\n          \"name\": \"prefix\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"hashed_token\": {\n          \"name\": \"hashed_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_by_id\": {\n          \"name\": \"created_by_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"last_used_at\": {\n          \"name\": \"last_used_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"api_key_prefix_unique\": {\n          \"name\": \"api_key_prefix_unique\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_hashed_token_unique\": {\n          \"name\": \"api_key_hashed_token_unique\",\n          \"columns\": [\n            \"hashed_token\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_prefix_idx\": {\n          \"name\": \"api_key_prefix_idx\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"api_key_workspace_id_workspace_id_fk\": {\n          \"name\": \"api_key_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"api_key_created_by_id_user_id_fk\": {\n          \"name\": \"api_key_created_by_id_user_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"created_by_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0054_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"fa90adb7-69d9-419c-baf9-3a73922e9e43\",\n  \"prevId\": \"cd3deb34-e4c8-4ae2-aa4e-bc0f7d9517f5\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"access_type\": {\n          \"name\": \"access_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'public'\"\n        },\n        \"auth_email_domains\": {\n          \"name\": \"auth_email_domains\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"external_name\": {\n          \"name\": \"external_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"monitor_group_id\": {\n          \"name\": \"monitor_group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor_group\",\n          \"columnsFrom\": [\n            \"monitor_group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"unsubscribed_at\": {\n          \"name\": \"unsubscribed_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer\": {\n      \"name\": \"viewer\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"viewer_email_unique\": {\n          \"name\": \"viewer_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_accounts\": {\n      \"name\": \"viewer_accounts\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_accounts_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_accounts_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_accounts\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"viewer_accounts_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"viewer_accounts_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_session\": {\n      \"name\": \"viewer_session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_session_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_session_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_session\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"api_key\": {\n      \"name\": \"api_key\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"prefix\": {\n          \"name\": \"prefix\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"hashed_token\": {\n          \"name\": \"hashed_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_by_id\": {\n          \"name\": \"created_by_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"last_used_at\": {\n          \"name\": \"last_used_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"api_key_prefix_unique\": {\n          \"name\": \"api_key_prefix_unique\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_hashed_token_unique\": {\n          \"name\": \"api_key_hashed_token_unique\",\n          \"columns\": [\n            \"hashed_token\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_prefix_idx\": {\n          \"name\": \"api_key_prefix_idx\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"api_key_workspace_id_workspace_id_fk\": {\n          \"name\": \"api_key_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"api_key_created_by_id_user_id_fk\": {\n          \"name\": \"api_key_created_by_id_user_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"created_by_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_page_component\": {\n      \"name\": \"maintenance_to_page_component\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"maintenance_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_page_component_maintenance_id_page_component_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"maintenance_to_page_component_maintenance_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component\": {\n      \"name\": \"page_component\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'monitor'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"group_id\": {\n          \"name\": \"group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_component_page_id_monitor_id_unique\": {\n          \"name\": \"page_component_page_id_monitor_id_unique\",\n          \"columns\": [\n            \"page_id\",\n            \"monitor_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_component_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_page_id_page_id_fk\": {\n          \"name\": \"page_component_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_monitor_id_monitor_id_fk\": {\n          \"name\": \"page_component_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_group_id_page_component_groups_id_fk\": {\n          \"name\": \"page_component_group_id_page_component_groups_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page_component_groups\",\n          \"columnsFrom\": [\n            \"group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {\n        \"page_component_type_check\": {\n          \"name\": \"page_component_type_check\",\n          \"value\": \"\\\"page_component\\\".\\\"type\\\" = 'monitor' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NOT NULL OR \\\"page_component\\\".\\\"type\\\" = 'external' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NULL\"\n        }\n      }\n    },\n    \"status_report_to_page_component\": {\n      \"name\": \"status_report_to_page_component\",\n      \"columns\": {\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_page_component_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_page_component_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"status_report_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_page_component_status_report_id_page_component_id_pk\": {\n          \"columns\": [\n            \"status_report_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"status_report_to_page_component_status_report_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component_groups\": {\n      \"name\": \"page_component_groups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_component_groups_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_groups_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_groups_page_id_page_id_fk\": {\n          \"name\": \"page_component_groups_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0055_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"4f344b53-2e8a-400d-be39-8018d77ff0a9\",\n  \"prevId\": \"fa90adb7-69d9-419c-baf9-3a73922e9e43\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"access_type\": {\n          \"name\": \"access_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'public'\"\n        },\n        \"auth_email_domains\": {\n          \"name\": \"auth_email_domains\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"external_name\": {\n          \"name\": \"external_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"monitor_group_id\": {\n          \"name\": \"monitor_group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor_group\",\n          \"columnsFrom\": [\n            \"monitor_group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"unsubscribed_at\": {\n          \"name\": \"unsubscribed_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer\": {\n      \"name\": \"viewer\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"viewer_email_unique\": {\n          \"name\": \"viewer_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_accounts\": {\n      \"name\": \"viewer_accounts\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_accounts_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_accounts_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_accounts\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"viewer_accounts_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"viewer_accounts_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_session\": {\n      \"name\": \"viewer_session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_session_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_session_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_session\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"api_key\": {\n      \"name\": \"api_key\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"prefix\": {\n          \"name\": \"prefix\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"hashed_token\": {\n          \"name\": \"hashed_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_by_id\": {\n          \"name\": \"created_by_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"last_used_at\": {\n          \"name\": \"last_used_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"api_key_prefix_unique\": {\n          \"name\": \"api_key_prefix_unique\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_hashed_token_unique\": {\n          \"name\": \"api_key_hashed_token_unique\",\n          \"columns\": [\n            \"hashed_token\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_prefix_idx\": {\n          \"name\": \"api_key_prefix_idx\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"api_key_workspace_id_workspace_id_fk\": {\n          \"name\": \"api_key_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"api_key_created_by_id_user_id_fk\": {\n          \"name\": \"api_key_created_by_id_user_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"created_by_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_page_component\": {\n      \"name\": \"maintenance_to_page_component\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"maintenance_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_page_component_maintenance_id_page_component_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"maintenance_to_page_component_maintenance_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component\": {\n      \"name\": \"page_component\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'monitor'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"group_id\": {\n          \"name\": \"group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_component_page_id_monitor_id_unique\": {\n          \"name\": \"page_component_page_id_monitor_id_unique\",\n          \"columns\": [\n            \"page_id\",\n            \"monitor_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_component_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_page_id_page_id_fk\": {\n          \"name\": \"page_component_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_monitor_id_monitor_id_fk\": {\n          \"name\": \"page_component_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_group_id_page_component_groups_id_fk\": {\n          \"name\": \"page_component_group_id_page_component_groups_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page_component_groups\",\n          \"columnsFrom\": [\n            \"group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {\n        \"page_component_type_check\": {\n          \"name\": \"page_component_type_check\",\n          \"value\": \"\\\"page_component\\\".\\\"type\\\" = 'monitor' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NOT NULL OR \\\"page_component\\\".\\\"type\\\" = 'static' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NULL\"\n        }\n      }\n    },\n    \"status_report_to_page_component\": {\n      \"name\": \"status_report_to_page_component\",\n      \"columns\": {\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_page_component_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_page_component_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"status_report_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_page_component_status_report_id_page_component_id_pk\": {\n          \"columns\": [\n            \"status_report_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"status_report_to_page_component_status_report_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component_groups\": {\n      \"name\": \"page_component_groups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_component_groups_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_groups_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_groups_page_id_page_id_fk\": {\n          \"name\": \"page_component_groups_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0056_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"b585a66a-babd-44de-85a7-b154332cd51b\",\n  \"prevId\": \"4f344b53-2e8a-400d-be39-8018d77ff0a9\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"access_type\": {\n          \"name\": \"access_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'public'\"\n        },\n        \"auth_email_domains\": {\n          \"name\": \"auth_email_domains\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"external_name\": {\n          \"name\": \"external_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"monitor_group_id\": {\n          \"name\": \"monitor_group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor_group\",\n          \"columnsFrom\": [\n            \"monitor_group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"channel_type\": {\n          \"name\": \"channel_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'email'\"\n        },\n        \"webhook_url\": {\n          \"name\": \"webhook_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"channel_config\": {\n          \"name\": \"channel_config\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"unsubscribed_at\": {\n          \"name\": \"unsubscribed_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"idx_page_subscriber_email_page_active\": {\n          \"name\": \"idx_page_subscriber_email_page_active\",\n          \"columns\": [\n            \"LOWER(\\\"email\\\")\",\n            \"page_id\"\n          ],\n          \"isUnique\": true,\n          \"where\": \"\\\"page_subscriber\\\".\\\"unsubscribed_at\\\" IS NULL AND \\\"page_subscriber\\\".\\\"channel_type\\\" = 'email'\"\n        },\n        \"idx_page_subscriber_webhook_page_active\": {\n          \"name\": \"idx_page_subscriber_webhook_page_active\",\n          \"columns\": [\n            \"webhook_url\",\n            \"page_id\"\n          ],\n          \"isUnique\": true,\n          \"where\": \"\\\"page_subscriber\\\".\\\"unsubscribed_at\\\" IS NULL AND \\\"page_subscriber\\\".\\\"channel_type\\\" = 'webhook'\"\n        }\n      },\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {\n        \"page_subscriber_channel_check\": {\n          \"name\": \"page_subscriber_channel_check\",\n          \"value\": \"(\\\"page_subscriber\\\".\\\"channel_type\\\" = 'email' AND \\\"page_subscriber\\\".\\\"email\\\" IS NOT NULL AND \\\"page_subscriber\\\".\\\"webhook_url\\\" IS NULL) OR (\\\"page_subscriber\\\".\\\"channel_type\\\" = 'webhook' AND \\\"page_subscriber\\\".\\\"webhook_url\\\" IS NOT NULL AND \\\"page_subscriber\\\".\\\"email\\\" IS NULL)\"\n        }\n      }\n    },\n    \"page_subscriber_to_page_component\": {\n      \"name\": \"page_subscriber_to_page_component\",\n      \"columns\": {\n        \"page_subscriber_id\": {\n          \"name\": \"page_subscriber_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk\": {\n          \"name\": \"page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk\",\n          \"tableFrom\": \"page_subscriber_to_page_component\",\n          \"tableTo\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_subscriber_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_subscriber_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"page_subscriber_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"page_subscriber_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk\": {\n          \"columns\": [\n            \"page_subscriber_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer\": {\n      \"name\": \"viewer\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"viewer_email_unique\": {\n          \"name\": \"viewer_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_accounts\": {\n      \"name\": \"viewer_accounts\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_accounts_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_accounts_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_accounts\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"viewer_accounts_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"viewer_accounts_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_session\": {\n      \"name\": \"viewer_session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_session_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_session_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_session\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"api_key\": {\n      \"name\": \"api_key\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"prefix\": {\n          \"name\": \"prefix\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"hashed_token\": {\n          \"name\": \"hashed_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_by_id\": {\n          \"name\": \"created_by_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"last_used_at\": {\n          \"name\": \"last_used_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"api_key_prefix_unique\": {\n          \"name\": \"api_key_prefix_unique\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_hashed_token_unique\": {\n          \"name\": \"api_key_hashed_token_unique\",\n          \"columns\": [\n            \"hashed_token\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_prefix_idx\": {\n          \"name\": \"api_key_prefix_idx\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"api_key_workspace_id_workspace_id_fk\": {\n          \"name\": \"api_key_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"api_key_created_by_id_user_id_fk\": {\n          \"name\": \"api_key_created_by_id_user_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"created_by_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_page_component\": {\n      \"name\": \"maintenance_to_page_component\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"maintenance_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_page_component_maintenance_id_page_component_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"maintenance_to_page_component_maintenance_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component\": {\n      \"name\": \"page_component\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'monitor'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"group_id\": {\n          \"name\": \"group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_component_page_id_monitor_id_unique\": {\n          \"name\": \"page_component_page_id_monitor_id_unique\",\n          \"columns\": [\n            \"page_id\",\n            \"monitor_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_component_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_page_id_page_id_fk\": {\n          \"name\": \"page_component_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_monitor_id_monitor_id_fk\": {\n          \"name\": \"page_component_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_group_id_page_component_groups_id_fk\": {\n          \"name\": \"page_component_group_id_page_component_groups_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page_component_groups\",\n          \"columnsFrom\": [\n            \"group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {\n        \"page_component_type_check\": {\n          \"name\": \"page_component_type_check\",\n          \"value\": \"\\\"page_component\\\".\\\"type\\\" = 'monitor' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NOT NULL OR \\\"page_component\\\".\\\"type\\\" = 'static' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NULL\"\n        }\n      }\n    },\n    \"status_report_to_page_component\": {\n      \"name\": \"status_report_to_page_component\",\n      \"columns\": {\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_page_component_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_page_component_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"status_report_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_page_component_status_report_id_page_component_id_pk\": {\n          \"columns\": [\n            \"status_report_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"status_report_to_page_component_status_report_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component_groups\": {\n      \"name\": \"page_component_groups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_component_groups_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_groups_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_groups_page_id_page_id_fk\": {\n          \"name\": \"page_component_groups_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {\n      \"idx_page_subscriber_email_page_active\": {\n        \"columns\": {\n          \"LOWER(\\\"email\\\")\": {\n            \"isExpression\": true\n          }\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0057_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"0ff42ba7-5824-42ff-84cf-db1a9c4ea79f\",\n  \"prevId\": \"b585a66a-babd-44de-85a7-b154332cd51b\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_to_monitors\": {\n      \"name\": \"status_report_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"status_report_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_monitors_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_monitors_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_monitors\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_monitors_monitor_id_status_report_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"status_report_id\"\n          ],\n          \"name\": \"status_report_to_monitors_monitor_id_status_report_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"access_type\": {\n          \"name\": \"access_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'public'\"\n        },\n        \"auth_email_domains\": {\n          \"name\": \"auth_email_domains\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"external_name\": {\n          \"name\": \"external_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitors_to_pages\": {\n      \"name\": \"monitors_to_pages\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"monitor_group_id\": {\n          \"name\": \"monitor_group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitors_to_pages_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_page_id_page_id_fk\": {\n          \"name\": \"monitors_to_pages_page_id_page_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\": {\n          \"name\": \"monitors_to_pages_monitor_group_id_monitor_group_id_fk\",\n          \"tableFrom\": \"monitors_to_pages\",\n          \"tableTo\": \"monitor_group\",\n          \"columnsFrom\": [\n            \"monitor_group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitors_to_pages_monitor_id_page_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"page_id\"\n          ],\n          \"name\": \"monitors_to_pages_monitor_id_page_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"channel_type\": {\n          \"name\": \"channel_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'email'\"\n        },\n        \"webhook_url\": {\n          \"name\": \"webhook_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"channel_config\": {\n          \"name\": \"channel_config\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"unsubscribed_at\": {\n          \"name\": \"unsubscribed_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"idx_page_subscriber_email_page_active\": {\n          \"name\": \"idx_page_subscriber_email_page_active\",\n          \"columns\": [\n            \"LOWER(\\\"email\\\")\",\n            \"page_id\"\n          ],\n          \"isUnique\": true,\n          \"where\": \"\\\"page_subscriber\\\".\\\"unsubscribed_at\\\" IS NULL AND \\\"page_subscriber\\\".\\\"channel_type\\\" = 'email'\"\n        },\n        \"idx_page_subscriber_webhook_page_active\": {\n          \"name\": \"idx_page_subscriber_webhook_page_active\",\n          \"columns\": [\n            \"webhook_url\",\n            \"page_id\"\n          ],\n          \"isUnique\": true,\n          \"where\": \"\\\"page_subscriber\\\".\\\"unsubscribed_at\\\" IS NULL AND \\\"page_subscriber\\\".\\\"channel_type\\\" = 'webhook'\"\n        }\n      },\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {\n        \"page_subscriber_channel_check\": {\n          \"name\": \"page_subscriber_channel_check\",\n          \"value\": \"(\\\"page_subscriber\\\".\\\"channel_type\\\" = 'email' AND \\\"page_subscriber\\\".\\\"email\\\" IS NOT NULL AND \\\"page_subscriber\\\".\\\"webhook_url\\\" IS NULL) OR (\\\"page_subscriber\\\".\\\"channel_type\\\" = 'webhook' AND \\\"page_subscriber\\\".\\\"webhook_url\\\" IS NOT NULL AND \\\"page_subscriber\\\".\\\"email\\\" IS NULL)\"\n        }\n      }\n    },\n    \"page_subscriber_to_page_component\": {\n      \"name\": \"page_subscriber_to_page_component\",\n      \"columns\": {\n        \"page_subscriber_id\": {\n          \"name\": \"page_subscriber_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk\": {\n          \"name\": \"page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk\",\n          \"tableFrom\": \"page_subscriber_to_page_component\",\n          \"tableTo\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_subscriber_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_subscriber_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"page_subscriber_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"page_subscriber_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk\": {\n          \"columns\": [\n            \"page_subscriber_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_monitor\": {\n      \"name\": \"maintenance_to_monitor\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_monitor_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"maintenance_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"maintenance_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_monitor_maintenance_id_monitor_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"monitor_id\"\n          ],\n          \"name\": \"maintenance_to_monitor_maintenance_id_monitor_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer\": {\n      \"name\": \"viewer\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"viewer_email_unique\": {\n          \"name\": \"viewer_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_accounts\": {\n      \"name\": \"viewer_accounts\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_accounts_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_accounts_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_accounts\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"viewer_accounts_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"viewer_accounts_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_session\": {\n      \"name\": \"viewer_session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_session_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_session_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_session\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"api_key\": {\n      \"name\": \"api_key\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"prefix\": {\n          \"name\": \"prefix\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"hashed_token\": {\n          \"name\": \"hashed_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_by_id\": {\n          \"name\": \"created_by_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"last_used_at\": {\n          \"name\": \"last_used_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"api_key_prefix_unique\": {\n          \"name\": \"api_key_prefix_unique\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_hashed_token_unique\": {\n          \"name\": \"api_key_hashed_token_unique\",\n          \"columns\": [\n            \"hashed_token\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_prefix_idx\": {\n          \"name\": \"api_key_prefix_idx\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"api_key_workspace_id_workspace_id_fk\": {\n          \"name\": \"api_key_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"api_key_created_by_id_user_id_fk\": {\n          \"name\": \"api_key_created_by_id_user_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"created_by_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_page_component\": {\n      \"name\": \"maintenance_to_page_component\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"maintenance_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_page_component_maintenance_id_page_component_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"maintenance_to_page_component_maintenance_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component\": {\n      \"name\": \"page_component\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'monitor'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"group_id\": {\n          \"name\": \"group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_component_page_id_monitor_id_unique\": {\n          \"name\": \"page_component_page_id_monitor_id_unique\",\n          \"columns\": [\n            \"page_id\",\n            \"monitor_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_component_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_page_id_page_id_fk\": {\n          \"name\": \"page_component_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_monitor_id_monitor_id_fk\": {\n          \"name\": \"page_component_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_group_id_page_component_groups_id_fk\": {\n          \"name\": \"page_component_group_id_page_component_groups_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page_component_groups\",\n          \"columnsFrom\": [\n            \"group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {\n        \"page_component_type_check\": {\n          \"name\": \"page_component_type_check\",\n          \"value\": \"\\\"page_component\\\".\\\"type\\\" = 'monitor' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NOT NULL OR \\\"page_component\\\".\\\"type\\\" = 'static' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NULL\"\n        }\n      }\n    },\n    \"status_report_to_page_component\": {\n      \"name\": \"status_report_to_page_component\",\n      \"columns\": {\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_page_component_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_page_component_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"status_report_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_page_component_status_report_id_page_component_id_pk\": {\n          \"columns\": [\n            \"status_report_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"status_report_to_page_component_status_report_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component_groups\": {\n      \"name\": \"page_component_groups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_component_groups_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_groups_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_groups_page_id_page_id_fk\": {\n          \"name\": \"page_component_groups_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {\n      \"idx_page_subscriber_email_page_active\": {\n        \"columns\": {\n          \"LOWER(\\\"email\\\")\": {\n            \"isExpression\": true\n          }\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0058_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"3ce96f19-7f95-45a6-8704-7c50d133b4a0\",\n  \"prevId\": \"0ff42ba7-5824-42ff-84cf-db1a9c4ea79f\",\n  \"tables\": {\n    \"workspace\": {\n      \"name\": \"workspace\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"stripe_id\": {\n          \"name\": \"stripe_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"subscription_id\": {\n          \"name\": \"subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"ends_at\": {\n          \"name\": \"ends_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"paid_until\": {\n          \"name\": \"paid_until\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"limits\": {\n          \"name\": \"limits\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"workspace_slug_unique\": {\n          \"name\": \"workspace_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_stripe_id_unique\": {\n          \"name\": \"workspace_stripe_id_unique\",\n          \"columns\": [\n            \"stripe_id\"\n          ],\n          \"isUnique\": true\n        },\n        \"workspace_id_dsn_unique\": {\n          \"name\": \"workspace_id_dsn_unique\",\n          \"columns\": [\n            \"id\",\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider_account_id\": {\n          \"name\": \"provider_account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_user_id_user_id_fk\": {\n          \"name\": \"account_user_id_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_provider_account_id_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"provider_account_id\"\n          ],\n          \"name\": \"account_provider_provider_account_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tenant_id\": {\n          \"name\": \"tenant_id\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"first_name\": {\n          \"name\": \"first_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"last_name\": {\n          \"name\": \"last_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"photo_url\": {\n          \"name\": \"photo_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_tenant_id_unique\": {\n          \"name\": \"user_tenant_id_unique\",\n          \"columns\": [\n            \"tenant_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"users_to_workspaces\": {\n      \"name\": \"users_to_workspaces\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"users_to_workspaces_user_id_user_id_fk\": {\n          \"name\": \"users_to_workspaces_user_id_user_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"users_to_workspaces_workspace_id_workspace_id_fk\": {\n          \"name\": \"users_to_workspaces_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"users_to_workspaces\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"users_to_workspaces_user_id_workspace_id_pk\": {\n          \"columns\": [\n            \"user_id\",\n            \"workspace_id\"\n          ],\n          \"name\": \"users_to_workspaces_user_id_workspace_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verification_token\": {\n      \"name\": \"verification_token\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verification_token_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verification_token_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report\": {\n      \"name\": \"status_report\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_workspace_id_workspace_id_fk\": {\n          \"name\": \"status_report_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_page_id_page_id_fk\": {\n          \"name\": \"status_report_page_id_page_id_fk\",\n          \"tableFrom\": \"status_report\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"status_report_update\": {\n      \"name\": \"status_report_update\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"date\": {\n          \"name\": \"date\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_update_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_update_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_update\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"integration\": {\n      \"name\": \"integration\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"credential\": {\n          \"name\": \"credential\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"external_id\": {\n          \"name\": \"external_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"integration_workspace_id_workspace_id_fk\": {\n          \"name\": \"integration_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"integration\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page\": {\n      \"name\": \"page\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"custom_domain\": {\n          \"name\": \"custom_domain\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"published\": {\n          \"name\": \"published\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"force_theme\": {\n          \"name\": \"force_theme\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'system'\"\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password_protected\": {\n          \"name\": \"password_protected\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"access_type\": {\n          \"name\": \"access_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'public'\"\n        },\n        \"auth_email_domains\": {\n          \"name\": \"auth_email_domains\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"homepage_url\": {\n          \"name\": \"homepage_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contact_url\": {\n          \"name\": \"contact_url\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"legacy_page\": {\n          \"name\": \"legacy_page\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"configuration\": {\n          \"name\": \"configuration\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"show_monitor_values\": {\n          \"name\": \"show_monitor_values\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_slug_unique\": {\n          \"name\": \"page_slug_unique\",\n          \"columns\": [\n            \"slug\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor\": {\n      \"name\": \"monitor\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"job_type\": {\n          \"name\": \"job_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'http'\"\n        },\n        \"periodicity\": {\n          \"name\": \"periodicity\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'other'\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"active\": {\n          \"name\": \"active\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(2048)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"external_name\": {\n          \"name\": \"external_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"timeout\": {\n          \"name\": \"timeout\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 45000\n        },\n        \"degraded_after\": {\n          \"name\": \"degraded_after\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"assertions\": {\n          \"name\": \"assertions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_endpoint\": {\n          \"name\": \"otel_endpoint\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"otel_headers\": {\n          \"name\": \"otel_headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"retry\": {\n          \"name\": \"retry\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 3\n        },\n        \"follow_redirects\": {\n          \"name\": \"follow_redirects\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_subscriber\": {\n      \"name\": \"page_subscriber\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"channel_type\": {\n          \"name\": \"channel_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'email'\"\n        },\n        \"webhook_url\": {\n          \"name\": \"webhook_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"channel_config\": {\n          \"name\": \"channel_config\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"unsubscribed_at\": {\n          \"name\": \"unsubscribed_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"idx_page_subscriber_email_page_active\": {\n          \"name\": \"idx_page_subscriber_email_page_active\",\n          \"columns\": [\n            \"LOWER(\\\"email\\\")\",\n            \"page_id\"\n          ],\n          \"isUnique\": true,\n          \"where\": \"\\\"page_subscriber\\\".\\\"unsubscribed_at\\\" IS NULL AND \\\"page_subscriber\\\".\\\"channel_type\\\" = 'email'\"\n        },\n        \"idx_page_subscriber_webhook_page_active\": {\n          \"name\": \"idx_page_subscriber_webhook_page_active\",\n          \"columns\": [\n            \"webhook_url\",\n            \"page_id\"\n          ],\n          \"isUnique\": true,\n          \"where\": \"\\\"page_subscriber\\\".\\\"unsubscribed_at\\\" IS NULL AND \\\"page_subscriber\\\".\\\"channel_type\\\" = 'webhook'\"\n        }\n      },\n      \"foreignKeys\": {\n        \"page_subscriber_page_id_page_id_fk\": {\n          \"name\": \"page_subscriber_page_id_page_id_fk\",\n          \"tableFrom\": \"page_subscriber\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {\n        \"page_subscriber_channel_check\": {\n          \"name\": \"page_subscriber_channel_check\",\n          \"value\": \"(\\\"page_subscriber\\\".\\\"channel_type\\\" = 'email' AND \\\"page_subscriber\\\".\\\"email\\\" IS NOT NULL AND \\\"page_subscriber\\\".\\\"webhook_url\\\" IS NULL) OR (\\\"page_subscriber\\\".\\\"channel_type\\\" = 'webhook' AND \\\"page_subscriber\\\".\\\"webhook_url\\\" IS NOT NULL AND \\\"page_subscriber\\\".\\\"email\\\" IS NULL)\"\n        }\n      }\n    },\n    \"page_subscriber_to_page_component\": {\n      \"name\": \"page_subscriber_to_page_component\",\n      \"columns\": {\n        \"page_subscriber_id\": {\n          \"name\": \"page_subscriber_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk\": {\n          \"name\": \"page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk\",\n          \"tableFrom\": \"page_subscriber_to_page_component\",\n          \"tableTo\": \"page_subscriber\",\n          \"columnsFrom\": [\n            \"page_subscriber_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_subscriber_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"page_subscriber_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"page_subscriber_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk\": {\n          \"columns\": [\n            \"page_subscriber_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification\": {\n      \"name\": \"notification\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"data\": {\n          \"name\": \"data\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'{}'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notification_workspace_id_workspace_id_fk\": {\n          \"name\": \"notification_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"notification\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notification_trigger\": {\n      \"name\": \"notification_trigger\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cron_timestamp\": {\n          \"name\": \"cron_timestamp\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"notification_id_monitor_id_crontimestampe\": {\n          \"name\": \"notification_id_monitor_id_crontimestampe\",\n          \"columns\": [\n            \"notification_id\",\n            \"monitor_id\",\n            \"cron_timestamp\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"notification_trigger_monitor_id_monitor_id_fk\": {\n          \"name\": \"notification_trigger_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notification_trigger_notification_id_notification_id_fk\": {\n          \"name\": \"notification_trigger_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notification_trigger\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"notifications_to_monitors\": {\n      \"name\": \"notifications_to_monitors\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"notification_id\": {\n          \"name\": \"notification_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"notifications_to_monitors_monitor_id_monitor_id_fk\": {\n          \"name\": \"notifications_to_monitors_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"notifications_to_monitors_notification_id_notification_id_fk\": {\n          \"name\": \"notifications_to_monitors_notification_id_notification_id_fk\",\n          \"tableFrom\": \"notifications_to_monitors\",\n          \"tableTo\": \"notification\",\n          \"columnsFrom\": [\n            \"notification_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"notifications_to_monitors_monitor_id_notification_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"notification_id\"\n          ],\n          \"name\": \"notifications_to_monitors_monitor_id_notification_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_status\": {\n      \"name\": \"monitor_status\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"region\": {\n          \"name\": \"region\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'active'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"monitor_status_idx\": {\n          \"name\": \"monitor_status_idx\",\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"monitor_status_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_status_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_status\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_status_monitor_id_region_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"region\"\n          ],\n          \"name\": \"monitor_status_monitor_id_region_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invitation\": {\n      \"name\": \"invitation\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'member'\"\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"incident\": {\n      \"name\": \"incident\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'triage'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"started_at\": {\n          \"name\": \"started_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"acknowledged_at\": {\n          \"name\": \"acknowledged_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"acknowledged_by\": {\n          \"name\": \"acknowledged_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_at\": {\n          \"name\": \"resolved_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resolved_by\": {\n          \"name\": \"resolved_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"incident_screenshot_url\": {\n          \"name\": \"incident_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"recovery_screenshot_url\": {\n          \"name\": \"recovery_screenshot_url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"auto_resolved\": {\n          \"name\": \"auto_resolved\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"incident_monitor_id_started_at_unique\": {\n          \"name\": \"incident_monitor_id_started_at_unique\",\n          \"columns\": [\n            \"monitor_id\",\n            \"started_at\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"incident_monitor_id_monitor_id_fk\": {\n          \"name\": \"incident_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set default\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_workspace_id_workspace_id_fk\": {\n          \"name\": \"incident_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_acknowledged_by_user_id_fk\": {\n          \"name\": \"incident_acknowledged_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"acknowledged_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"incident_resolved_by_user_id_fk\": {\n          \"name\": \"incident_resolved_by_user_id_fk\",\n          \"tableFrom\": \"incident\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"resolved_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag\": {\n      \"name\": \"monitor_tag\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_tag_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_tag\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_tag_to_monitor\": {\n      \"name\": \"monitor_tag_to_monitor\",\n      \"columns\": {\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"monitor_tag_id\": {\n          \"name\": \"monitor_tag_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\": {\n          \"name\": \"monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk\",\n          \"tableFrom\": \"monitor_tag_to_monitor\",\n          \"tableTo\": \"monitor_tag\",\n          \"columnsFrom\": [\n            \"monitor_tag_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\": {\n          \"columns\": [\n            \"monitor_id\",\n            \"monitor_tag_id\"\n          ],\n          \"name\": \"monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"application\": {\n      \"name\": \"application\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dsn\": {\n          \"name\": \"dsn\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"application_dsn_unique\": {\n          \"name\": \"application_dsn_unique\",\n          \"columns\": [\n            \"dsn\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"application_workspace_id_workspace_id_fk\": {\n          \"name\": \"application_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"application\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance\": {\n      \"name\": \"maintenance\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text(256)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"from\": {\n          \"name\": \"from\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"to\": {\n          \"name\": \"to\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_workspace_id_workspace_id_fk\": {\n          \"name\": \"maintenance_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_page_id_page_id_fk\": {\n          \"name\": \"maintenance_page_id_page_id_fk\",\n          \"tableFrom\": \"maintenance\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"check\": {\n      \"name\": \"check\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"regions\": {\n          \"name\": \"regions\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text(4096)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"headers\": {\n          \"name\": \"headers\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"body\": {\n          \"name\": \"body\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"method\": {\n          \"name\": \"method\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'GET'\"\n        },\n        \"count_requests\": {\n          \"name\": \"count_requests\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 1\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"check_workspace_id_workspace_id_fk\": {\n          \"name\": \"check_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"check\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_run\": {\n      \"name\": \"monitor_run\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"runned_at\": {\n          \"name\": \"runned_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_run_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_run_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_run_monitor_id_monitor_id_fk\": {\n          \"name\": \"monitor_run_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"monitor_run\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location\": {\n      \"name\": \"private_location\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"last_seen_at\": {\n          \"name\": \"last_seen_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_workspace_id_workspace_id_fk\": {\n          \"name\": \"private_location_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"private_location\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"private_location_to_monitor\": {\n      \"name\": \"private_location_to_monitor\",\n      \"columns\": {\n        \"private_location_id\": {\n          \"name\": \"private_location_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"private_location_to_monitor_private_location_id_private_location_id_fk\": {\n          \"name\": \"private_location_to_monitor_private_location_id_private_location_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"private_location\",\n          \"columnsFrom\": [\n            \"private_location_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"private_location_to_monitor_monitor_id_monitor_id_fk\": {\n          \"name\": \"private_location_to_monitor_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"private_location_to_monitor\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"monitor_group\": {\n      \"name\": \"monitor_group\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"monitor_group_workspace_id_workspace_id_fk\": {\n          \"name\": \"monitor_group_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"monitor_group_page_id_page_id_fk\": {\n          \"name\": \"monitor_group_page_id_page_id_fk\",\n          \"tableFrom\": \"monitor_group\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer\": {\n      \"name\": \"viewer\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"viewer_email_unique\": {\n          \"name\": \"viewer_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_accounts\": {\n      \"name\": \"viewer_accounts\",\n      \"columns\": {\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_accounts_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_accounts_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_accounts\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"viewer_accounts_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"viewer_accounts_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"viewer_session\": {\n      \"name\": \"viewer_session\",\n      \"columns\": {\n        \"session_token\": {\n          \"name\": \"session_token\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"viewer_session_user_id_viewer_id_fk\": {\n          \"name\": \"viewer_session_user_id_viewer_id_fk\",\n          \"tableFrom\": \"viewer_session\",\n          \"tableTo\": \"viewer\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"api_key\": {\n      \"name\": \"api_key\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"prefix\": {\n          \"name\": \"prefix\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"hashed_token\": {\n          \"name\": \"hashed_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_by_id\": {\n          \"name\": \"created_by_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"last_used_at\": {\n          \"name\": \"last_used_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"api_key_prefix_unique\": {\n          \"name\": \"api_key_prefix_unique\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_hashed_token_unique\": {\n          \"name\": \"api_key_hashed_token_unique\",\n          \"columns\": [\n            \"hashed_token\"\n          ],\n          \"isUnique\": true\n        },\n        \"api_key_prefix_idx\": {\n          \"name\": \"api_key_prefix_idx\",\n          \"columns\": [\n            \"prefix\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"api_key_workspace_id_workspace_id_fk\": {\n          \"name\": \"api_key_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"api_key_created_by_id_user_id_fk\": {\n          \"name\": \"api_key_created_by_id_user_id_fk\",\n          \"tableFrom\": \"api_key\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"created_by_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"maintenance_to_page_component\": {\n      \"name\": \"maintenance_to_page_component\",\n      \"columns\": {\n        \"maintenance_id\": {\n          \"name\": \"maintenance_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\": {\n          \"name\": \"maintenance_to_page_component_maintenance_id_maintenance_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"maintenance\",\n          \"columnsFrom\": [\n            \"maintenance_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"maintenance_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"maintenance_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"maintenance_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"maintenance_to_page_component_maintenance_id_page_component_id_pk\": {\n          \"columns\": [\n            \"maintenance_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"maintenance_to_page_component_maintenance_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component\": {\n      \"name\": \"page_component\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'monitor'\"\n        },\n        \"monitor_id\": {\n          \"name\": \"monitor_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"order\": {\n          \"name\": \"order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"group_id\": {\n          \"name\": \"group_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"group_order\": {\n          \"name\": \"group_order\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {\n        \"page_component_page_id_monitor_id_unique\": {\n          \"name\": \"page_component_page_id_monitor_id_unique\",\n          \"columns\": [\n            \"page_id\",\n            \"monitor_id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"page_component_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_page_id_page_id_fk\": {\n          \"name\": \"page_component_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_monitor_id_monitor_id_fk\": {\n          \"name\": \"page_component_monitor_id_monitor_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"monitor\",\n          \"columnsFrom\": [\n            \"monitor_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_group_id_page_component_groups_id_fk\": {\n          \"name\": \"page_component_group_id_page_component_groups_id_fk\",\n          \"tableFrom\": \"page_component\",\n          \"tableTo\": \"page_component_groups\",\n          \"columnsFrom\": [\n            \"group_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {\n        \"page_component_type_check\": {\n          \"name\": \"page_component_type_check\",\n          \"value\": \"\\\"page_component\\\".\\\"type\\\" = 'monitor' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NOT NULL OR \\\"page_component\\\".\\\"type\\\" = 'static' AND \\\"page_component\\\".\\\"monitor_id\\\" IS NULL\"\n        }\n      }\n    },\n    \"status_report_to_page_component\": {\n      \"name\": \"status_report_to_page_component\",\n      \"columns\": {\n        \"status_report_id\": {\n          \"name\": \"status_report_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_component_id\": {\n          \"name\": \"page_component_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"status_report_to_page_component_status_report_id_status_report_id_fk\": {\n          \"name\": \"status_report_to_page_component_status_report_id_status_report_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"status_report\",\n          \"columnsFrom\": [\n            \"status_report_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"status_report_to_page_component_page_component_id_page_component_id_fk\": {\n          \"name\": \"status_report_to_page_component_page_component_id_page_component_id_fk\",\n          \"tableFrom\": \"status_report_to_page_component\",\n          \"tableTo\": \"page_component\",\n          \"columnsFrom\": [\n            \"page_component_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"status_report_to_page_component_status_report_id_page_component_id_pk\": {\n          \"columns\": [\n            \"status_report_id\",\n            \"page_component_id\"\n          ],\n          \"name\": \"status_report_to_page_component_status_report_id_page_component_id_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"page_component_groups\": {\n      \"name\": \"page_component_groups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"integer\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"workspace_id\": {\n          \"name\": \"workspace_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"page_id\": {\n          \"name\": \"page_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"(strftime('%s', 'now'))\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"page_component_groups_workspace_id_workspace_id_fk\": {\n          \"name\": \"page_component_groups_workspace_id_workspace_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"workspace\",\n          \"columnsFrom\": [\n            \"workspace_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"page_component_groups_page_id_page_id_fk\": {\n          \"name\": \"page_component_groups_page_id_page_id_fk\",\n          \"tableFrom\": \"page_component_groups\",\n          \"tableTo\": \"page\",\n          \"columnsFrom\": [\n            \"page_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {\n      \"idx_page_subscriber_email_page_active\": {\n        \"columns\": {\n          \"LOWER(\\\"email\\\")\": {\n            \"isExpression\": true\n          }\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/_journal.json",
    "content": "{\n  \"version\": \"5\",\n  \"dialect\": \"sqlite\",\n  \"entries\": [\n    {\n      \"idx\": 0,\n      \"version\": \"5\",\n      \"when\": 1690309905039,\n      \"tag\": \"0000_lively_master_chief\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 1,\n      \"version\": \"5\",\n      \"when\": 1690892003254,\n      \"tag\": \"0001_brainy_beast\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 2,\n      \"version\": \"5\",\n      \"when\": 1691573899721,\n      \"tag\": \"0002_luxuriant_ser_duncan\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 3,\n      \"version\": \"5\",\n      \"when\": 1691614487733,\n      \"tag\": \"0003_glamorous_living_mummy\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 4,\n      \"version\": \"5\",\n      \"when\": 1691850907670,\n      \"tag\": \"0004_fixed_dakota_north\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 5,\n      \"version\": \"5\",\n      \"when\": 1691930414569,\n      \"tag\": \"0005_even_baron_strucker\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 6,\n      \"version\": \"5\",\n      \"when\": 1692646649111,\n      \"tag\": \"0006_tired_anita_blake\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 7,\n      \"version\": \"5\",\n      \"when\": 1694362217174,\n      \"tag\": \"0007_complex_frog_thor\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 8,\n      \"version\": \"5\",\n      \"when\": 1695756345957,\n      \"tag\": \"0008_overjoyed_sunset_bain\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 9,\n      \"version\": \"5\",\n      \"when\": 1697285841283,\n      \"tag\": \"0009_small_maximus\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 10,\n      \"version\": \"5\",\n      \"when\": 1700586221141,\n      \"tag\": \"0010_lame_songbird\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 11,\n      \"version\": \"5\",\n      \"when\": 1701100570578,\n      \"tag\": \"0011_bright_jazinda\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 12,\n      \"version\": \"5\",\n      \"when\": 1701713135829,\n      \"tag\": \"0012_tan_magma\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 13,\n      \"version\": \"5\",\n      \"when\": 1702144660818,\n      \"tag\": \"0013_tired_paladin\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 14,\n      \"version\": \"5\",\n      \"when\": 1702227904130,\n      \"tag\": \"0014_adorable_skaar\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 15,\n      \"version\": \"5\",\n      \"when\": 1705856545397,\n      \"tag\": \"0015_bent_sister_grimm\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 16,\n      \"version\": \"5\",\n      \"when\": 1706111184826,\n      \"tag\": \"0016_certain_praxagora\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 17,\n      \"version\": \"5\",\n      \"when\": 1707411900987,\n      \"tag\": \"0017_loose_maggott\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 18,\n      \"version\": \"5\",\n      \"when\": 1707770189561,\n      \"tag\": \"0018_neat_orphan\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 19,\n      \"version\": \"5\",\n      \"when\": 1707899175705,\n      \"tag\": \"0019_dashing_malcolm_colcord\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 20,\n      \"version\": \"5\",\n      \"when\": 1707905605592,\n      \"tag\": \"0020_flat_bedlam\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 21,\n      \"version\": \"5\",\n      \"when\": 1710677383007,\n      \"tag\": \"0021_reflective_nico_minoru\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 22,\n      \"version\": \"5\",\n      \"when\": 1711307113089,\n      \"tag\": \"0022_chunky_rockslide\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 23,\n      \"version\": \"5\",\n      \"when\": 1712311348272,\n      \"tag\": \"0023_dry_blink\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 24,\n      \"version\": \"5\",\n      \"when\": 1712354121499,\n      \"tag\": \"0024_young_proudstar\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 25,\n      \"version\": \"5\",\n      \"when\": 1713095971713,\n      \"tag\": \"0025_strong_thunderball\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 26,\n      \"version\": \"5\",\n      \"when\": 1713384976187,\n      \"tag\": \"0026_giant_absorbing_man\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 27,\n      \"version\": \"5\",\n      \"when\": 1714586658374,\n      \"tag\": \"0027_bizarre_bastion\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 28,\n      \"version\": \"5\",\n      \"when\": 1715173356076,\n      \"tag\": \"0028_thin_power_pack\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 29,\n      \"version\": \"6\",\n      \"when\": 1716215342026,\n      \"tag\": \"0029_regular_marrow\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 30,\n      \"version\": \"6\",\n      \"when\": 1716364430118,\n      \"tag\": \"0030_elite_barracuda\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 31,\n      \"version\": \"6\",\n      \"when\": 1717837961923,\n      \"tag\": \"0031_lowly_gabe_jones\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 32,\n      \"version\": \"6\",\n      \"when\": 1718027484219,\n      \"tag\": \"0032_hot_swordsman\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 33,\n      \"version\": \"6\",\n      \"when\": 1719740057514,\n      \"tag\": \"0033_solid_colossus\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 34,\n      \"version\": \"6\",\n      \"when\": 1720727898360,\n      \"tag\": \"0034_serious_shard\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 35,\n      \"version\": \"6\",\n      \"when\": 1721159796428,\n      \"tag\": \"0035_open_the_professor\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 36,\n      \"version\": \"6\",\n      \"when\": 1723459608109,\n      \"tag\": \"0036_gifted_deathbird\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 37,\n      \"version\": \"6\",\n      \"when\": 1729533101998,\n      \"tag\": \"0037_equal_beyonder\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 38,\n      \"version\": \"6\",\n      \"when\": 1729579461221,\n      \"tag\": \"0038_foamy_stardust\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 39,\n      \"version\": \"6\",\n      \"when\": 1739193014150,\n      \"tag\": \"0039_lonely_jigsaw\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 40,\n      \"version\": \"6\",\n      \"when\": 1740684132626,\n      \"tag\": \"0040_narrow_anthem\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 41,\n      \"version\": \"6\",\n      \"when\": 1741936835660,\n      \"tag\": \"0041_nasty_jigsaw\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 42,\n      \"version\": \"6\",\n      \"when\": 1747410497521,\n      \"tag\": \"0042_great_epoch\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 43,\n      \"version\": \"6\",\n      \"when\": 1747908803707,\n      \"tag\": \"0043_low_lily_hollister\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 44,\n      \"version\": \"6\",\n      \"when\": 1753730490635,\n      \"tag\": \"0044_illegal_turbo\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 45,\n      \"version\": \"6\",\n      \"when\": 1756185045968,\n      \"tag\": \"0045_little_paladin\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 46,\n      \"version\": \"6\",\n      \"when\": 1757580216081,\n      \"tag\": \"0046_lucky_tarantula\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 47,\n      \"version\": \"6\",\n      \"when\": 1757840904190,\n      \"tag\": \"0047_nifty_roughhouse\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 48,\n      \"version\": \"6\",\n      \"when\": 1760602903085,\n      \"tag\": \"0048_neat_tempest\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 49,\n      \"version\": \"6\",\n      \"when\": 1761901661043,\n      \"tag\": \"0049_sloppy_inhumans\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 50,\n      \"version\": \"6\",\n      \"when\": 1765567734101,\n      \"tag\": \"0050_damp_xorn\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 51,\n      \"version\": \"6\",\n      \"when\": 1767362130713,\n      \"tag\": \"0051_fuzzy_red_hulk\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 52,\n      \"version\": \"6\",\n      \"when\": 1767797078062,\n      \"tag\": \"0052_illegal_killraven\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 53,\n      \"version\": \"6\",\n      \"when\": 1768490525276,\n      \"tag\": \"0053_dark_orphan\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 54,\n      \"version\": \"6\",\n      \"when\": 1768564852930,\n      \"tag\": \"0054_bitter_lilandra\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 55,\n      \"version\": \"6\",\n      \"when\": 1769675142003,\n      \"tag\": \"0055_spicy_bastion\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 56,\n      \"version\": \"6\",\n      \"when\": 1772280814493,\n      \"tag\": \"0056_violet_shotgun\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 57,\n      \"version\": \"6\",\n      \"when\": 1772708421188,\n      \"tag\": \"0057_curious_xorn\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 58,\n      \"version\": \"6\",\n      \"when\": 1772982206642,\n      \"tag\": \"0058_absent_chameleon\",\n      \"breakpoints\": true\n    }\n  ]\n}"
  },
  {
    "path": "packages/db/drizzle.config.ts",
    "content": "import type { Config } from \"drizzle-kit\";\n\nimport { env } from \"./env.mjs\";\n\nexport default {\n  schema: \"./src/schema/index.ts\",\n  out: \"./drizzle\",\n  dbCredentials: {\n    url: env.DATABASE_URL,\n    authToken: env.DATABASE_AUTH_TOKEN,\n  },\n  strict: true,\n  dialect: \"turso\",\n} satisfies Config;\n"
  },
  {
    "path": "packages/db/env.mjs",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    DATABASE_URL: z.string().min(1),\n    DATABASE_AUTH_TOKEN: z.string().min(1),\n    CLICKHOUSE_URL: z.string(),\n    CLICKHOUSE_USERNAME: z.string(),\n    CLICKHOUSE_PASSWORD: z.string(),\n  },\n  runtimeEnv: {\n    DATABASE_URL:\n      // FIXME: This is a hack to get the tests to run\n      process.env.NODE_ENV === \"test\"\n        ? \"http://127.0.0.1:8080\"\n        : process.env.DATABASE_URL,\n    DATABASE_AUTH_TOKEN: process.env.DATABASE_AUTH_TOKEN,\n    CLICKHOUSE_URL: process.env.CLICKHOUSE_URL,\n    CLICKHOUSE_USERNAME: process.env.CLICKHOUSE_USERNAME,\n    CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD,\n  },\n  skipValidation: true,\n});\n"
  },
  {
    "path": "packages/db/env.ts",
    "content": "const file = Bun.file(\"./.env.example\");\nawait Bun.write(\"./.env\", file);\n"
  },
  {
    "path": "packages/db/package.json",
    "content": "{\n  \"name\": \"@openstatus/db\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"generate\": \"drizzle-kit generate\",\n    \"push\": \"drizzle-kit push\",\n    \"migrate\": \"bun src/migrate.mts\",\n    \"studio\": \"drizzle-kit studio\",\n    \"env\": \"bun env.ts\",\n    \"seed\": \"bun src/seed.mts\",\n    \"dev\": \"turso dev --db-file ../../openstatus-dev.db\"\n  },\n  \"dependencies\": {\n    \"@libsql/client\": \"0.15.15\",\n    \"@openstatus/assertions\": \"workspace:*\",\n    \"@openstatus/regions\": \"workspace:*\",\n    \"@openstatus/theme-store\": \"workspace:*\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"bcryptjs\": \"3.0.3\",\n    \"drizzle-orm\": \"0.44.4\",\n    \"drizzle-zod\": \"0.8.3\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/bcryptjs\": \"3.0.0\",\n    \"@types/node\": \"22.10.2\",\n    \"drizzle-kit\": \"0.31.4\",\n    \"next-auth\": \"5.0.0-beta.29\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"author\": \"OpenStatus\"\n}\n"
  },
  {
    "path": "packages/db/script/region-migration.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"bun:test\";\nimport type { z } from \"zod\";\nimport type { monitorRegionSchema } from \"../src/schema/constants\";\nimport { updateRegion } from \"./region-migration\";\n\n// Import the types we need\n\ndescribe(\"updateRegion\", () => {\n  let regions: z.infer<typeof monitorRegionSchema>[];\n\n  beforeEach(() => {\n    // Reset regions array before each test\n    regions = [\"ams\", \"hkg\", \"fra\", \"lax\"];\n  });\n\n  describe(\"when old region exists in array\", () => {\n    test(\"should replace old region with 'sin' when new region does not exist in array\", () => {\n      updateRegion(\"hkg\", \"sin\", regions);\n\n      expect(regions).toEqual([\"ams\", \"sin\", \"fra\", \"lax\"]);\n      expect(regions).toHaveLength(4);\n    });\n\n    test(\"should remove old region when new region already exists in array\", () => {\n      updateRegion(\"hkg\", \"ams\", regions);\n\n      expect(regions).toEqual([\"ams\", \"fra\", \"lax\"]);\n      expect(regions).toHaveLength(3);\n    });\n\n    test(\"should handle replacing first element\", () => {\n      updateRegion(\"ams\", \"sin\", regions);\n\n      expect(regions).toEqual([\"sin\", \"hkg\", \"fra\", \"lax\"]);\n    });\n\n    test(\"should handle replacing last element\", () => {\n      updateRegion(\"lax\", \"sin\", regions);\n\n      expect(regions).toEqual([\"ams\", \"hkg\", \"fra\", \"sin\"]);\n    });\n\n    test(\"should handle case where old and new region are the same\", () => {\n      updateRegion(\"hkg\", \"hkg\", regions);\n\n      // Since hkg exists, it should be removed (newRegionIndex !== -1)\n      expect(regions).toEqual([\"ams\", \"fra\", \"lax\"]);\n      expect(regions).toHaveLength(3);\n    });\n  });\n\n  describe(\"when old region does not exist in array\", () => {\n    test(\"should not modify the array when old region is not found\", () => {\n      const originalRegions = [...regions];\n      updateRegion(\"sin\", \"syd\", regions);\n\n      expect(regions).toEqual(originalRegions);\n      expect(regions).toHaveLength(4);\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    test(\"should handle empty regions array\", () => {\n      const emptyRegions: z.infer<typeof monitorRegionSchema>[] = [];\n      updateRegion(\"hkg\", \"sin\", emptyRegions);\n\n      expect(emptyRegions).toEqual([]);\n      expect(emptyRegions).toHaveLength(0);\n    });\n\n    test(\"should handle single element array - replace scenario\", () => {\n      const singleRegion: z.infer<typeof monitorRegionSchema>[] = [\"hkg\"];\n      updateRegion(\"hkg\", \"sin\", singleRegion);\n\n      expect(singleRegion).toEqual([\"sin\"]);\n    });\n\n    test(\"should handle single element array - remove scenario\", () => {\n      const singleRegion: z.infer<typeof monitorRegionSchema>[] = [\"hkg\"];\n      updateRegion(\"hkg\", \"hkg\", singleRegion);\n\n      expect(singleRegion).toEqual([]);\n    });\n\n    test(\"should handle array with duplicate regions\", () => {\n      const duplicateRegions: z.infer<typeof monitorRegionSchema>[] = [\n        \"ams\",\n        \"hkg\",\n        \"ams\",\n        \"fra\",\n      ];\n      updateRegion(\"hkg\", \"sin\", duplicateRegions);\n\n      // Should only replace the first occurrence of hkg\n      expect(duplicateRegions).toEqual([\"ams\", \"sin\", \"ams\", \"fra\"]);\n    });\n\n    test(\"should handle multiple occurrences of old region\", () => {\n      const multipleOldRegions: z.infer<typeof monitorRegionSchema>[] = [\n        \"hkg\",\n        \"ams\",\n        \"hkg\",\n        \"fra\",\n      ];\n      updateRegion(\"hkg\", \"sin\", multipleOldRegions);\n\n      // Should only replace the first occurrence\n      expect(multipleOldRegions).toEqual([\"sin\", \"ams\", \"hkg\", \"fra\"]);\n    });\n\n    describe(\"function mutates original array\", () => {\n      test(\"should modify the original regions array reference\", () => {\n        const originalReference = regions;\n        updateRegion(\"hkg\", \"sin\", regions);\n\n        // Should be the same reference (mutated)\n        expect(regions).toBe(originalReference);\n        expect(regions).toEqual([\"ams\", \"sin\", \"fra\", \"lax\"]);\n      });\n    });\n\n    describe(\"full migrations\", () => {\n      test(\"should modify the original regions array reference\", () => {\n        const newRegions = [\n          \"ams\",\n          \"arn\",\n          \"atl\",\n          \"bog\",\n          \"bom\",\n          \"bos\",\n          \"cdg\",\n          \"den\",\n          \"dfw\",\n          \"ewr\",\n          \"eze\",\n          \"fra\",\n          \"gdl\",\n          \"gig\",\n          \"gru\",\n          \"hkg\",\n          \"iad\",\n          \"jnb\",\n          \"lax\",\n          \"lhr\",\n          \"mad\",\n          \"mia\",\n          \"nrt\",\n          \"ord\",\n          \"otp\",\n          \"phx\",\n          \"qro\",\n          \"sin\",\n          \"scl\",\n          \"sjc\",\n          \"sea\",\n          \"sin\",\n          \"syd\",\n          \"waw\",\n          \"yul\",\n          \"yyz\",\n        ] as z.infer<typeof monitorRegionSchema>[];\n        // Asia Pacific\n        updateRegion(\"hkg\", \"sin\", newRegions);\n\n        // North America\n        updateRegion(\"atl\", \"dfw\", newRegions);\n        updateRegion(\"mia\", \"dfw\", newRegions);\n        updateRegion(\"gdl\", \"dfw\", newRegions);\n        updateRegion(\"qro\", \"dfw\", newRegions);\n        updateRegion(\"bos\", \"ewr\", newRegions);\n        updateRegion(\"phx\", \"lax\", newRegions);\n        updateRegion(\"sea\", \"sjc\", newRegions);\n        updateRegion(\"yul\", \"yyz\", newRegions);\n\n        // Europe\n        updateRegion(\"waw\", \"ams\", newRegions);\n        updateRegion(\"mad\", \"cdg\", newRegions);\n        updateRegion(\"otp\", \"fra\", newRegions);\n\n        // South America\n        updateRegion(\"bog\", \"gru\", newRegions);\n        updateRegion(\"gig\", \"gru\", newRegions);\n        updateRegion(\"scl\", \"gru\", newRegions);\n        updateRegion(\"eze\", \"gru\", newRegions);\n\n        // Should be the same reference (mutated)\n\n        expect(newRegions).toEqual([\n          \"ams\",\n          \"arn\",\n          \"bom\",\n          \"cdg\",\n          \"den\",\n          \"dfw\",\n          \"ewr\",\n          \"fra\",\n          \"gru\",\n          \"iad\",\n          \"jnb\",\n          \"lax\",\n          \"lhr\",\n          \"nrt\",\n          \"ord\",\n          \"sin\",\n          \"sjc\",\n          \"sin\",\n          \"syd\",\n          \"yyz\",\n        ]);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/db/script/region-migration.ts",
    "content": "import { z } from \"zod\";\nimport { db, eq, schema } from \"../src\";\nimport { selectMonitorSchema } from \"../src/schema\";\nimport type { monitorRegionSchema } from \"../src/schema/constants\";\n\nconst rawMonitors = await db.select().from(schema.monitor);\n\nconst monitors = z.array(selectMonitorSchema).parse(rawMonitors);\nfor (const monitor of monitors) {\n  const regions = monitor.regions.slice();\n  // Asia Pacific\n  updateRegion(\"hkg\", \"sin\", regions);\n\n  // North America\n  updateRegion(\"atl\", \"dfw\", regions);\n  updateRegion(\"mia\", \"dfw\", regions);\n  updateRegion(\"gdl\", \"dfw\", regions);\n  updateRegion(\"qro\", \"dfw\", regions);\n  updateRegion(\"bos\", \"ewr\", regions);\n  updateRegion(\"phx\", \"lax\", regions);\n  updateRegion(\"sea\", \"sjc\", regions);\n  updateRegion(\"yul\", \"yyz\", regions);\n  updateRegion(\"den\", \"dfw\", regions);\n\n  // Europe\n  updateRegion(\"waw\", \"ams\", regions);\n  updateRegion(\"mad\", \"cdg\", regions);\n  updateRegion(\"otp\", \"fra\", regions);\n\n  // South America\n  updateRegion(\"bog\", \"gru\", regions);\n  updateRegion(\"gig\", \"gru\", regions);\n  updateRegion(\"scl\", \"gru\", regions);\n  updateRegion(\"eze\", \"gru\", regions);\n  const newRegions = regions.join(\",\");\n  // console.log(\"new regions:\",newRegions)\n  await db\n    .update(schema.monitor)\n    .set({ regions: newRegions })\n    .where(eq(schema.monitor.id, monitor.id))\n    .execute();\n}\n\nexport function updateRegion(\n  oldRegion: z.infer<typeof monitorRegionSchema>,\n  newRegion: z.infer<typeof monitorRegionSchema>,\n  regions: z.infer<typeof monitorRegionSchema>[],\n) {\n  const regionIndex = regions.indexOf(oldRegion);\n  if (regionIndex !== -1) {\n    const newRegionIndex = regions.indexOf(newRegion);\n    if (newRegionIndex === -1) {\n      regions[regionIndex] = newRegion;\n    }\n    if (newRegionIndex !== -1) {\n      regions.splice(regionIndex, 1);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/db/src/db.ts",
    "content": "import { drizzle } from \"drizzle-orm/libsql/http\";\n\nimport { env } from \"../env.mjs\";\nimport * as schema from \"./schema\";\n\nexport const db = drizzle({\n  connection: {\n    url: env.DATABASE_URL,\n    authToken: env.DATABASE_AUTH_TOKEN,\n  },\n  schema,\n});\n"
  },
  {
    "path": "packages/db/src/index.ts",
    "content": "export * as schema from \"./schema\";\nexport * from \"drizzle-orm\";\nexport * from \"./db\";\n// doing this because the external module not working see : https://github.com/vercel/next.js/issues/43433\n// export * from \"./sync-db\";\n"
  },
  {
    "path": "packages/db/src/migrate.mts",
    "content": "import { createClient } from \"@libsql/client\";\nimport { drizzle } from \"drizzle-orm/libsql\";\nimport { migrate } from \"drizzle-orm/libsql/migrator\";\n\nimport { env } from \"../env.mjs\";\n\nasync function main() {\n  const db = drizzle(\n    createClient({ url: env.DATABASE_URL, authToken: env.DATABASE_AUTH_TOKEN }),\n  );\n  console.log(\"Running migrations\");\n\n  await migrate(db, { migrationsFolder: \"drizzle\" });\n\n  console.log(\"Migrated successfully\");\n\n  process.exit(0);\n}\n\nmain().catch((e) => {\n  console.error(\"Migration failed\");\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/db/src/schema/api-keys/api_key.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport { index, integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\nimport { user } from \"../users\";\nimport { workspace } from \"../workspaces\";\n\nexport const apiKey = sqliteTable(\n  \"api_key\",\n  {\n    id: integer(\"id\").primaryKey({ autoIncrement: true }),\n    name: text(\"name\").notNull(),\n    description: text(\"description\"),\n    prefix: text(\"prefix\").notNull().unique(),\n    hashedToken: text(\"hashed_token\").notNull().unique(),\n    workspaceId: integer(\"workspace_id\")\n      .notNull()\n      .references(() => workspace.id, { onDelete: \"cascade\" }),\n    createdById: integer(\"created_by_id\")\n      .notNull()\n      .references(() => user.id, { onDelete: \"cascade\" }),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n    expiresAt: integer(\"expires_at\", { mode: \"timestamp\" }),\n    lastUsedAt: integer(\"last_used_at\", { mode: \"timestamp\" }),\n  },\n  (table) => ({\n    prefixIdx: index(\"api_key_prefix_idx\").on(table.prefix),\n  }),\n);\n\nexport const apiKeyRelations = relations(apiKey, ({ one }) => ({\n  workspace: one(workspace, {\n    fields: [apiKey.workspaceId],\n    references: [workspace.id],\n  }),\n  createdBy: one(user, {\n    fields: [apiKey.createdById],\n    references: [user.id],\n  }),\n}));\n"
  },
  {
    "path": "packages/db/src/schema/api-keys/index.ts",
    "content": "export * from \"./api_key\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/api-keys/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\n\nimport { apiKey } from \"./api_key\";\n\nexport const insertApiKeySchema = createInsertSchema(apiKey, {\n  name: z.string().min(1, \"Name is required\"),\n  description: z.string().optional(),\n  expiresAt: z.date().optional(),\n});\n\nexport const selectApiKeySchema = createSelectSchema(apiKey);\n\nexport const createApiKeySchema = z.object({\n  name: z.string().min(1, \"Name is required\"),\n  description: z.string().optional(),\n  expiresAt: z.date().optional(),\n});\n\nexport type InsertApiKey = z.infer<typeof insertApiKeySchema>;\nexport type ApiKey = z.infer<typeof selectApiKeySchema>;\nexport type CreateApiKeyInput = z.infer<typeof createApiKeySchema>;\n"
  },
  {
    "path": "packages/db/src/schema/applications/application.ts",
    "content": "import { sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { workspace } from \"../workspaces\";\n\nexport const application = sqliteTable(\"application\", {\n  id: integer(\"id\").primaryKey(),\n  name: text(\"name\"), // friendly name for the project\n  dsn: text(\"dsn\").unique(), // dsn for the source\n\n  workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n"
  },
  {
    "path": "packages/db/src/schema/applications/index.ts",
    "content": "export * from \"./application\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/applications/validation.ts",
    "content": "import { createSelectSchema } from \"drizzle-zod\";\nimport { application } from \"./application\";\n\nexport const selectApplicationSchema = createSelectSchema(application);\n"
  },
  {
    "path": "packages/db/src/schema/check/check.ts",
    "content": "import { sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { monitorMethods } from \"../monitors/constants\";\nimport { workspace } from \"../workspaces\";\n\nexport const check = sqliteTable(\"check\", {\n  id: integer(\"id\").primaryKey({ autoIncrement: true }),\n  regions: text(\"regions\").default(\"\").notNull(),\n  url: text(\"url\", { length: 4096 }).notNull(),\n  headers: text(\"headers\").default(\"\"),\n  body: text(\"body\").default(\"\"),\n  method: text(\"method\", { enum: monitorMethods }).default(\"GET\"),\n\n  countRequests: integer(\"count_requests\").default(1),\n\n  workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n"
  },
  {
    "path": "packages/db/src/schema/check/constants.ts",
    "content": "export const flyCheckerRegions = [\n  // TODO: Add more regions\n  \"ams\",\n  \"iad\",\n  \"hkg\",\n  \"jnb\",\n  \"syd\",\n  \"gru\",\n] as const;\n"
  },
  {
    "path": "packages/db/src/schema/check/index.ts",
    "content": "export * from \"./check\";\nexport * from \"./constants\";\n"
  },
  {
    "path": "packages/db/src/schema/constants.ts",
    "content": "import {\n  ALL_REGIONS,\n  AVAILABLE_REGIONS,\n  FLY_REGIONS,\n  FREE_FLY_REGIONS,\n} from \"@openstatus/regions\";\nimport { z } from \"zod\";\n\nexport const monitorPeriodicity = [\n  \"30s\",\n  \"1m\",\n  \"5m\",\n  \"10m\",\n  \"30m\",\n  \"1h\",\n  \"other\",\n] as const;\n\nexport const availableRegions = AVAILABLE_REGIONS;\nexport const monitorRegions = ALL_REGIONS;\nexport const freeFlyRegions = FREE_FLY_REGIONS;\nexport const flyRegions = FLY_REGIONS;\nexport const monitorPeriodicitySchema = z.enum(monitorPeriodicity);\nexport const monitorRegionSchema = z.enum(ALL_REGIONS);\nexport const monitorFlyRegionSchema = z.enum(FLY_REGIONS);\n\nexport type MonitorFlyRegion = z.infer<typeof monitorFlyRegionSchema>;\nexport type Region = z.infer<typeof monitorRegionSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/incidents/incident.ts",
    "content": "import { sql } from \"drizzle-orm\";\nimport { relations } from \"drizzle-orm\";\nimport { integer, sqliteTable, text, unique } from \"drizzle-orm/sqlite-core\";\n\nimport { monitor } from \"../monitors\";\nimport { user } from \"../users/user\";\nimport { workspace } from \"../workspaces\";\n\nexport const statusIncident = [\n  \"triage\",\n  \"investigating\",\n  \"identified\",\n  \"monitoring\",\n  \"resolved\",\n  \"duplicated\",\n] as const;\n\nexport const incidentTable = sqliteTable(\n  \"incident\",\n  {\n    id: integer(\"id\").primaryKey(),\n    title: text(\"title\").default(\"\").notNull(),\n    summary: text(\"summary\").default(\"\").notNull(),\n    status: text(\"status\", { enum: statusIncident })\n      .default(\"triage\")\n      .notNull(),\n\n    // Service affected by incident\n    monitorId: integer(\"monitor_id\").references(() => monitor.id, {\n      onDelete: \"set default\",\n    }),\n\n    // Workspace where the incident happened\n    workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n    // Data related to incident timeline\n    startedAt: integer(\"started_at\", { mode: \"timestamp\" })\n      .notNull()\n      .default(sql`(strftime('%s', 'now'))`),\n    // Who has acknowledged the incident\n    acknowledgedAt: integer(\"acknowledged_at\", { mode: \"timestamp\" }),\n    acknowledgedBy: integer(\"acknowledged_by\").references(() => user.id),\n\n    // Who has resolved it\n    resolvedAt: integer(\"resolved_at\", { mode: \"timestamp\" }),\n    resolvedBy: integer(\"resolved_by\").references(() => user.id),\n\n    incidentScreenshotUrl: text(\"incident_screenshot_url\"),\n    recoveryScreenshotUrl: text(\"recovery_screenshot_url\"),\n    // If the incident was auto resolved\n    autoResolved: integer(\"auto_resolved\", { mode: \"boolean\" }).default(false),\n\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n    updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (table) => {\n    return {\n      unique: unique().on(table.monitorId, table.startedAt),\n    };\n  },\n);\n\nexport const incidentRelations = relations(incidentTable, ({ one }) => ({\n  monitor: one(monitor, {\n    fields: [incidentTable.monitorId],\n    references: [monitor.id],\n  }),\n  workspace: one(workspace, {\n    fields: [incidentTable.workspaceId],\n    references: [workspace.id],\n  }),\n  acknowledgedByUser: one(user, {\n    fields: [incidentTable.acknowledgedBy],\n    references: [user.id],\n  }),\n  resolvedByUser: one(user, {\n    fields: [incidentTable.resolvedBy],\n    references: [user.id],\n  }),\n}));\n"
  },
  {
    "path": "packages/db/src/schema/incidents/index.ts",
    "content": "export * from \"./incident\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/incidents/validation.ts",
    "content": "import { createSelectSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\n\nimport { incidentTable } from \"./incident\";\n\nexport const selectIncidentSchema = createSelectSchema(incidentTable).extend({\n  monitorName: z.string().optional(),\n});\n\nexport type Incident = z.infer<typeof selectIncidentSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/index.ts",
    "content": "export * from \"./workspaces\";\nexport * from \"./users\";\nexport * from \"./status_reports\";\nexport * from \"./integration\";\nexport * from \"./pages\";\nexport * from \"./monitors\";\nexport * from \"./page_subscribers\";\nexport * from \"./shared\";\nexport * from \"./notifications\";\nexport * from \"./monitor_status\";\nexport * from \"./invitations\";\nexport * from \"./incidents\";\nexport * from \"./monitor_tags\";\nexport * from \"./applications\";\nexport * from \"./maintenances\";\nexport * from \"./check\";\nexport * from \"./monitor_run\";\nexport * from \"./private_locations\";\nexport * from \"./monitor_groups\";\nexport * from \"./viewers\";\nexport * from \"./api-keys\";\nexport * from \"./page_components\";\nexport * from \"./page_component_groups\";\n"
  },
  {
    "path": "packages/db/src/schema/integration.ts",
    "content": "import { sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { createInsertSchema } from \"drizzle-zod\";\n\nimport { workspace } from \"./workspaces\";\n\nexport const integration = sqliteTable(\"integration\", {\n  id: integer(\"id\").primaryKey(),\n  name: text(\"name\", { length: 256 }).notNull(), // Should be vercel or other\n\n  workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n\n  // Not used yet but we might need to get store something for the integration  webhook url and or secret\n  credential: text(\"credential\", { mode: \"json\" }),\n\n  externalId: text(\"external_id\").notNull(), // the id of the integration in the external service\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n\n  data: text(\"data\", { mode: \"json\" }).notNull(),\n});\n\nexport const insertIntegrationSchema = createInsertSchema(integration);\n"
  },
  {
    "path": "packages/db/src/schema/invitations/index.ts",
    "content": "export * from \"./invitation\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/invitations/invitation.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\nimport { workspace, workspaceRole } from \"../workspaces\";\n\nexport const invitation = sqliteTable(\"invitation\", {\n  id: integer(\"id\").primaryKey(),\n  email: text(\"email\").notNull(),\n  role: text(\"role\", { enum: workspaceRole }).notNull().default(\"member\"),\n  workspaceId: integer(\"workspace_id\").notNull(),\n  token: text(\"token\").notNull(),\n  expiresAt: integer(\"expires_at\", { mode: \"timestamp\" }).notNull(),\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  acceptedAt: integer(\"accepted_at\", { mode: \"timestamp\" }),\n});\n\nexport const invitationRelations = relations(invitation, ({ one }) => ({\n  workspace: one(workspace, {\n    fields: [invitation.workspaceId],\n    references: [workspace.id],\n  }),\n}));\n"
  },
  {
    "path": "packages/db/src/schema/invitations/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\n\nimport { invitation } from \"./invitation\";\n\nexport const insertInvitationSchema = createInsertSchema(invitation, {\n  email: z.email(),\n});\n\nexport const selectInvitationSchema = createSelectSchema(invitation);\n\nexport type InsertInvitation = z.infer<typeof insertInvitationSchema>;\nexport type Invitation = z.infer<typeof selectInvitationSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/maintenances/index.ts",
    "content": "export * from \"./maintenance\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/maintenances/maintenance.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\nimport { maintenancesToPageComponents } from \"../page_components\";\nimport { page } from \"../pages\";\nimport { workspace } from \"../workspaces\";\n\nexport const maintenance = sqliteTable(\"maintenance\", {\n  id: integer(\"id\").primaryKey(),\n  title: text(\"title\", { length: 256 }).notNull(),\n  message: text(\"message\").notNull(),\n\n  from: integer(\"from\", { mode: \"timestamp\" }).notNull(),\n  to: integer(\"to\", { mode: \"timestamp\" }).notNull(),\n\n  workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n  pageId: integer(\"page_id\").references(() => page.id, { onDelete: \"cascade\" }),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n\nexport const maintenanceRelations = relations(maintenance, ({ one, many }) => ({\n  maintenancesToPageComponents: many(maintenancesToPageComponents),\n  page: one(page, {\n    fields: [maintenance.pageId],\n    references: [page.id],\n  }),\n  workspace: one(workspace, {\n    fields: [maintenance.workspaceId],\n    references: [workspace.id],\n  }),\n}));\n"
  },
  {
    "path": "packages/db/src/schema/maintenances/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\nimport { maintenance } from \"./maintenance\";\n\nexport const insertMaintenanceSchema = createInsertSchema(maintenance)\n  .extend({\n    // REMINDER: trick to make the react-hook-form controlled but not allow empty string\n    title: z.string().min(1, {\n      error: \"Required\",\n    }),\n    message: z.string().min(1, {\n      error: \"Required\",\n    }),\n\n    monitors: z.number().array().prefault([]).optional(),\n  })\n  // REMINDER: validate that `from` date is before `to` date\n  .refine((data) => data.from < data.to, {\n    path: [\"to\"],\n    error: \"End date cannot be earlier than start date.\",\n  });\n\nexport const selectMaintenanceSchema = createSelectSchema(maintenance).extend({\n  monitors: z.number().array().prefault([]).optional(),\n});\n\nexport type InsertMaintenance = z.infer<typeof insertMaintenanceSchema>;\nexport type Maintenance = z.infer<typeof selectMaintenanceSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/monitor_groups/index.ts",
    "content": "export * from \"./monitor_group\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/monitor_groups/monitor_group.ts",
    "content": "import { sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\nimport { page } from \"../pages\";\nimport { workspace } from \"../workspaces\";\n\nexport const monitorGroup = sqliteTable(\"monitor_group\", {\n  id: integer(\"id\").primaryKey(),\n  workspaceId: integer(\"workspace_id\")\n    .references(() => workspace.id, { onDelete: \"cascade\" })\n    .notNull(),\n  pageId: integer(\"page_id\")\n    .references(() => page.id, { onDelete: \"cascade\" })\n    .notNull(),\n  name: text(\"name\").notNull(),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n"
  },
  {
    "path": "packages/db/src/schema/monitor_groups/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport type { z } from \"zod\";\n\nimport { monitorGroup } from \"./monitor_group\";\n\nexport const selectMonitorGroupSchema = createSelectSchema(monitorGroup);\n\nexport const insertMonitorGroupSchema = createInsertSchema(monitorGroup);\n\nexport type InsertMonitorGroup = z.infer<typeof insertMonitorGroupSchema>;\nexport type MonitorGroup = z.infer<typeof selectMonitorGroupSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/monitor_run/index.ts",
    "content": "export * from \"./monitor_run\";\n"
  },
  {
    "path": "packages/db/src/schema/monitor_run/monitor_run.ts",
    "content": "import { sql } from \"drizzle-orm\";\nimport { integer, sqliteTable } from \"drizzle-orm/sqlite-core\";\nimport { monitor } from \"../monitors\";\nimport { workspace } from \"../workspaces/workspace\";\n\nexport const monitorRun = sqliteTable(\"monitor_run\", {\n  id: integer(\"id\").primaryKey(),\n\n  workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n  monitorId: integer(\"monitor_id\").references(() => monitor.id),\n\n  runnedAt: integer(\"runned_at\", { mode: \"timestamp_ms\" }),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n"
  },
  {
    "path": "packages/db/src/schema/monitor_status/index.ts",
    "content": "export * from \"./monitor_status\";\nexport * from \"./validation\";\nexport type * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/monitor_status/monitor_status.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport {\n  index,\n  integer,\n  primaryKey,\n  sqliteTable,\n  text,\n} from \"drizzle-orm/sqlite-core\";\n\nimport { monitor, monitorStatus as monitorStatusEnum } from \"../monitors\";\n\nexport const monitorStatusTable = sqliteTable(\n  \"monitor_status\",\n  {\n    monitorId: integer(\"monitor_id\")\n      .references(() => monitor.id, { onDelete: \"cascade\" })\n      .notNull(),\n    region: text(\"region\").default(\"\").notNull(),\n    status: text(\"status\", { enum: monitorStatusEnum })\n      .default(\"active\")\n      .notNull(),\n\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n    updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (table) => {\n    return {\n      primaryKey: primaryKey({ columns: [table.monitorId, table.region] }),\n      monitorStatusIdx: index(\"monitor_status_idx\").on(\n        table.monitorId,\n        table.region,\n      ),\n    };\n  },\n);\n\nexport const monitorStatusRelations = relations(\n  monitorStatusTable,\n  ({ one }) => ({\n    monitor: one(monitor, {\n      fields: [monitorStatusTable.monitorId],\n      references: [monitor.id],\n    }),\n  }),\n);\n"
  },
  {
    "path": "packages/db/src/schema/monitor_status/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\n\nimport { monitorRegionSchema } from \"../constants\";\nimport { monitorStatusSchema } from \"../monitors\";\nimport { monitorStatusTable } from \"./monitor_status\";\n\nexport const selectMonitorStatusSchema = createSelectSchema(\n  monitorStatusTable,\n  {\n    status: monitorStatusSchema.default(\"active\"),\n    region: monitorRegionSchema.default(\"ams\"),\n  },\n);\n\nexport const insertMonitorStatusSchema = createInsertSchema(\n  monitorStatusTable,\n  {\n    status: monitorStatusSchema.default(\"active\"),\n    region: monitorRegionSchema.default(\"ams\"),\n  },\n);\n\n// export type InsertMonitorStatus = z.infer<typeof insertMonitorStatusSchema>;\n// export type MonitorStatus = z.infer<typeof selectMonitorStatusSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/monitor_tags/index.ts",
    "content": "export * from \"./monitor_tag\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/monitor_tags/monitor_tag.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport {\n  integer,\n  primaryKey,\n  sqliteTable,\n  text,\n} from \"drizzle-orm/sqlite-core\";\n\nimport { monitor } from \"../monitors\";\nimport { workspace } from \"../workspaces\";\n\nexport const monitorTag = sqliteTable(\"monitor_tag\", {\n  id: integer(\"id\").primaryKey(),\n  workspaceId: integer(\"workspace_id\")\n    .references(() => workspace.id, { onDelete: \"cascade\" })\n    .notNull(),\n\n  name: text(\"name\").notNull(),\n  color: text(\"color\").notNull(),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n\nexport const monitorTagsToMonitors = sqliteTable(\n  \"monitor_tag_to_monitor\",\n  {\n    monitorId: integer(\"monitor_id\")\n      .notNull()\n      .references(() => monitor.id, { onDelete: \"cascade\" }),\n    monitorTagId: integer(\"monitor_tag_id\")\n      .notNull()\n      .references(() => monitorTag.id, { onDelete: \"cascade\" }),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (t) => ({\n    pk: primaryKey({ columns: [t.monitorId, t.monitorTagId] }),\n  }),\n);\n\nexport const monitorTagsToMonitorsRelation = relations(\n  monitorTagsToMonitors,\n  ({ one }) => ({\n    monitor: one(monitor, {\n      fields: [monitorTagsToMonitors.monitorId],\n      references: [monitor.id],\n    }),\n    monitorTag: one(monitorTag, {\n      fields: [monitorTagsToMonitors.monitorTagId],\n      references: [monitorTag.id],\n    }),\n  }),\n);\n\nexport const monitorTagRelations = relations(monitorTag, ({ one, many }) => ({\n  monitor: many(monitorTagsToMonitors),\n  workspace: one(workspace, {\n    fields: [monitorTag.workspaceId],\n    references: [workspace.id],\n  }),\n}));\n"
  },
  {
    "path": "packages/db/src/schema/monitor_tags/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport type { z } from \"zod\";\n\nimport { monitorTag } from \"./monitor_tag\";\n\nexport const selectMonitorTagSchema = createSelectSchema(monitorTag);\n\nexport const insertMonitorTagSchema = createInsertSchema(monitorTag);\n\nexport type InsertMonitorTag = z.infer<typeof insertMonitorTagSchema>;\nexport type MonitorTag = z.infer<typeof selectMonitorTagSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/monitors/constants.ts",
    "content": "export const monitorMethods = [\n  \"GET\",\n  \"POST\",\n  \"HEAD\",\n  \"PUT\",\n  \"PATCH\",\n  \"DELETE\",\n  \"TRACE\",\n  \"CONNECT\",\n  \"OPTIONS\",\n] as const;\nexport const monitorStatus = [\"active\", \"error\", \"degraded\"] as const;\n\nexport const monitorJobTypes = [\n  \"http\",\n  \"tcp\",\n  \"imcp\",\n  \"udp\",\n  \"dns\",\n  \"ssl\",\n] as const;\n"
  },
  {
    "path": "packages/db/src/schema/monitors/index.ts",
    "content": "export * from \"./constants\";\nexport * from \"./monitor\";\nexport * from \"./validation\";\nexport type * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/monitors/monitor.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\nimport { monitorPeriodicity } from \"../constants\";\nimport { incidentTable } from \"../incidents/incident\";\nimport { monitorStatusTable } from \"../monitor_status/monitor_status\";\nimport { monitorTagsToMonitors } from \"../monitor_tags\";\nimport { notificationsToMonitors } from \"../notifications\";\nimport { privateLocationToMonitors } from \"../private_locations\";\nimport { workspace } from \"../workspaces/workspace\";\nimport { monitorJobTypes, monitorMethods, monitorStatus } from \"./constants\";\n\nexport const monitor = sqliteTable(\"monitor\", {\n  id: integer(\"id\").primaryKey(),\n  jobType: text(\"job_type\", { enum: monitorJobTypes })\n    .default(\"http\")\n    .notNull(),\n  periodicity: text(\"periodicity\", { enum: monitorPeriodicity })\n    .default(\"other\")\n    .notNull(),\n  status: text(\"status\", { enum: monitorStatus }).default(\"active\").notNull(),\n  active: integer(\"active\", { mode: \"boolean\" }).default(false),\n\n  regions: text(\"regions\").default(\"\").notNull(),\n\n  url: text(\"url\", { length: 2048 }).notNull(), // URI\n\n  name: text(\"name\", { length: 256 }).default(\"\").notNull(),\n  externalName: text(\"external_name\"),\n  description: text(\"description\").default(\"\").notNull(),\n\n  headers: text(\"headers\").default(\"\"),\n  body: text(\"body\").default(\"\"),\n  method: text(\"method\", { enum: monitorMethods }).default(\"GET\"),\n  workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n\n  // Custom timeout for this monitor\n  timeout: integer(\"timeout\").notNull().default(45000), // in milliseconds\n\n  // Threshold for the monitor to be considered degraded\n  degradedAfter: integer(\"degraded_after\"), // in millisecond\n\n  assertions: text(\"assertions\"),\n\n  otelEndpoint: text(\"otel_endpoint\"),\n\n  otelHeaders: text(\"otel_headers\"),\n\n  public: integer(\"public\", { mode: \"boolean\" }).default(false),\n\n  retry: integer(\"retry\").default(3),\n\n  followRedirects: integer(\"follow_redirects\", { mode: \"boolean\" }).default(\n    true,\n  ),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n\n  deletedAt: integer(\"deleted_at\", { mode: \"timestamp\" }),\n});\n\nexport const monitorRelation = relations(monitor, ({ one, many }) => ({\n  monitorTagsToMonitors: many(monitorTagsToMonitors),\n  workspace: one(workspace, {\n    fields: [monitor.workspaceId],\n    references: [workspace.id],\n  }),\n  monitorsToNotifications: many(notificationsToMonitors),\n  incidents: many(incidentTable),\n  monitorStatus: many(monitorStatusTable),\n  privateLocationToMonitors: many(privateLocationToMonitors),\n}));\n"
  },
  {
    "path": "packages/db/src/schema/monitors/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\n\nimport * as assertions from \"@openstatus/assertions\";\n\nimport { monitorPeriodicitySchema, monitorRegionSchema } from \"../constants\";\nimport { monitorJobTypes, monitorMethods, monitorStatus } from \"./constants\";\nimport { monitor } from \"./monitor\";\n\nexport const monitorMethodsSchema = z.enum(monitorMethods);\nexport const monitorStatusSchema = z.enum(monitorStatus);\nexport const monitorJobTypesSchema = z.enum(monitorJobTypes);\n\n// TODO: shared function\n// biome-ignore lint/correctness/noUnusedVariables: <explanation>\nfunction stringToArrayProcess<T>(_string: T) {}\n\nconst regionsToArraySchema = z.preprocess((val) => {\n  if (String(val).length > 0) {\n    return String(val).split(\",\");\n  }\n  return [];\n}, z.array(monitorRegionSchema));\n\nconst bodyToStringSchema = z.preprocess((val) => {\n  return String(val);\n}, z.string());\n\nconst headersToArraySchema = z.preprocess(\n  (val) => {\n    // early return in case the header is already an array\n    if (Array.isArray(val)) {\n      return val;\n    }\n    if (typeof val === \"string\" && String(val).length > 0) {\n      return JSON.parse(String(val));\n    }\n    return [];\n  },\n  z.array(z.object({ key: z.string(), value: z.string() })).prefault([]),\n);\n\nexport const selectMonitorSchema = createSelectSchema(monitor, {\n  periodicity: monitorPeriodicitySchema.prefault(\"10m\"),\n  status: monitorStatusSchema.prefault(\"active\"),\n  jobType: monitorJobTypesSchema.prefault(\"http\"),\n  timeout: z.number().prefault(45),\n  followRedirects: z.boolean().prefault(true),\n  retry: z.number().prefault(3),\n  regions: regionsToArraySchema.prefault([]),\n}).extend({\n  headers: headersToArraySchema.prefault([]),\n  otelHeaders: headersToArraySchema.prefault([]),\n  body: bodyToStringSchema.prefault(\"\"),\n  // for tcp monitors the method is not needed\n  method: monitorMethodsSchema.prefault(\"GET\"),\n});\n\nconst headersSchema = z\n  .array(z.object({ key: z.string(), value: z.string() }))\n  .optional();\n\nexport const insertMonitorSchema = createInsertSchema(monitor, {\n  name: z\n    .string()\n    .min(1, \"Name must be at least 1 character long\")\n    .max(255, \"Name must be at most 255 characters long\"),\n  periodicity: monitorPeriodicitySchema.prefault(\"10m\"),\n  status: monitorStatusSchema.prefault(\"active\"),\n  regions: z.array(monitorRegionSchema).prefault([]).optional(),\n  headers: headersSchema.prefault([]),\n  otelHeaders: headersSchema.prefault([]),\n}).extend({\n  method: monitorMethodsSchema.prefault(\"GET\"),\n  notifications: z.array(z.number()).optional().prefault([]),\n  pages: z.array(z.number()).optional().prefault([]),\n  body: z.string().prefault(\"\").optional(),\n  tags: z.array(z.number()).optional().prefault([]),\n  statusAssertions: z.array(assertions.statusAssertion).optional(),\n  headerAssertions: z.array(assertions.headerAssertion).optional(),\n  textBodyAssertions: z.array(assertions.textBodyAssertion).optional(),\n  timeout: z.coerce.number().gte(0).lte(60000).prefault(45000),\n  degradedAfter: z.coerce.number().gte(0).lte(60000).nullish(),\n});\n\nexport type InsertMonitor = z.infer<typeof insertMonitorSchema>;\nexport type Monitor = z.infer<typeof selectMonitorSchema>;\nexport type MonitorStatus = z.infer<typeof monitorStatusSchema>;\nexport type MonitorPeriodicity = z.infer<typeof monitorPeriodicitySchema>;\nexport type MonitorMethod = z.infer<typeof monitorMethodsSchema>;\nexport type MonitorRegion = z.infer<typeof monitorRegionSchema>;\nexport type MonitorJobType = z.infer<typeof monitorJobTypesSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/notifications/constants.ts",
    "content": "export const notificationProvider = [\n  \"discord\",\n  \"email\",\n  \"google-chat\",\n  \"grafana-oncall\",\n  \"ntfy\",\n  \"pagerduty\",\n  \"opsgenie\",\n  \"slack\",\n  \"sms\",\n  \"telegram\",\n  \"webhook\",\n  \"whatsapp\",\n] as const;\n"
  },
  {
    "path": "packages/db/src/schema/notifications/index.ts",
    "content": "export * from \"./constants\";\nexport * from \"./notification\";\nexport * from \"./validation\";\nexport type * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/notifications/notification.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport {\n  integer,\n  primaryKey,\n  sqliteTable,\n  text,\n  uniqueIndex,\n} from \"drizzle-orm/sqlite-core\";\n\nimport { monitor } from \"../monitors\";\nimport { workspace } from \"../workspaces\";\nimport { notificationProvider } from \"./constants\";\n\nexport const notification = sqliteTable(\"notification\", {\n  id: integer(\"id\").primaryKey(),\n  name: text(\"name\").notNull(),\n  provider: text(\"provider\", { enum: notificationProvider }).notNull(),\n  data: text(\"data\").default(\"{}\"),\n  workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n\nexport const notificationTrigger = sqliteTable(\n  \"notification_trigger\",\n  {\n    id: integer(\"id\").primaryKey(),\n    monitorId: integer(\"monitor_id\").references(() => monitor.id, {\n      onDelete: \"cascade\",\n    }),\n    notificationId: integer(\"notification_id\").references(\n      () => notification.id,\n      { onDelete: \"cascade\" },\n    ),\n    cronTimestamp: integer(\"cron_timestamp\").notNull(),\n  },\n  (table) => ({\n    unique: uniqueIndex(\"notification_id_monitor_id_crontimestampe\").on(\n      table.notificationId,\n      table.monitorId,\n      table.cronTimestamp,\n    ),\n  }),\n);\n\nexport const notificationsToMonitors = sqliteTable(\n  \"notifications_to_monitors\",\n  {\n    monitorId: integer(\"monitor_id\")\n      .notNull()\n      .references(() => monitor.id, { onDelete: \"cascade\" }),\n    notificationId: integer(\"notification_id\")\n      .notNull()\n      .references(() => notification.id, { onDelete: \"cascade\" }),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (t) => ({\n    pk: primaryKey({ columns: [t.monitorId, t.notificationId] }),\n  }),\n);\n\nexport const notificationsToMonitorsRelation = relations(\n  notificationsToMonitors,\n  ({ one }) => ({\n    monitor: one(monitor, {\n      fields: [notificationsToMonitors.monitorId],\n      references: [monitor.id],\n    }),\n    notification: one(notification, {\n      fields: [notificationsToMonitors.notificationId],\n      references: [notification.id],\n    }),\n  }),\n);\n\nexport const notificationRelations = relations(\n  notification,\n  ({ one, many }) => ({\n    workspace: one(workspace, {\n      fields: [notification.workspaceId],\n      references: [workspace.id],\n    }),\n    monitor: many(notificationsToMonitors),\n  }),\n);\n"
  },
  {
    "path": "packages/db/src/schema/notifications/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport * as z from \"zod\";\n\nimport { notificationProvider } from \"./constants\";\nimport { notification } from \"./notification\";\n\nexport const notificationProviderSchema = z.enum(notificationProvider);\n\nexport const selectNotificationSchema = createSelectSchema(notification).extend(\n  {\n    data: z\n      .preprocess((val) => {\n        return String(val);\n      }, z.string())\n      .prefault(\"{}\"),\n  },\n);\n\n// we need to extend, otherwise data can be `null` or `undefined` - default is not\nexport const insertNotificationSchema = createInsertSchema(notification).extend(\n  {\n    data: z.string().prefault(\"{}\"),\n    monitors: z.array(z.number()).optional().prefault([]),\n  },\n);\n\nexport type InsertNotification = z.infer<typeof insertNotificationSchema>;\nexport type Notification = z.infer<typeof selectNotificationSchema>;\nexport type NotificationProvider = z.infer<typeof notificationProviderSchema>;\n\nconst phoneRegex = new RegExp(\n  /^([+]?[\\s0-9]+)?(\\d{3}|[(]?[0-9]+[)])?([-]?[\\s]?[0-9])+$/,\n);\n\nexport const phoneSchema = z.string().regex(phoneRegex, \"Invalid Number!\");\nexport const emailSchema = z.email();\nexport const urlSchema = z.url();\n\nexport const ntfyDataSchema = z.object({\n  ntfy: z.object({\n    topic: z.string().prefault(\"\"),\n    serverUrl: z.string().prefault(\"https://ntfy.sh\"),\n    token: z.string().optional(),\n  }),\n});\nexport const webhookDataSchema = z.object({\n  webhook: z.object({\n    endpoint: z.url(),\n    headers: z\n      .array(z.object({ key: z.string(), value: z.string() }))\n      .optional(),\n  }),\n});\nexport const emailDataSchema = z.object({ email: emailSchema });\nexport const phoneDataSchema = z.object({ sms: phoneSchema });\nexport const slackDataSchema = z.object({ slack: urlSchema });\nexport const discordDataSchema = z.object({ discord: urlSchema });\nexport const googleChatDataSchema = z.object({ \"google-chat\": urlSchema });\nexport const pagerdutyDataSchema = z.object({ pagerduty: z.string() });\nexport const opsgenieDataSchema = z.object({\n  opsgenie: z.object({\n    apiKey: z.string(),\n    region: z.enum([\"us\", \"eu\"]),\n  }),\n});\nexport const telegramDataSchema = z.object({\n  telegram: z.object({ chatId: z.string() }),\n});\n\nexport const grafanaOncallDataSchema = z.object({\n  \"grafana-oncall\": z.object({\n    webhookUrl: z.url(),\n  }),\n});\n\nexport const whatsappDataSchema = z.object({\n  whatsapp: phoneSchema,\n});\n\nexport const NotificationDataSchema = z.union([\n  discordDataSchema,\n  emailDataSchema,\n  grafanaOncallDataSchema,\n  ntfyDataSchema,\n  opsgenieDataSchema,\n  pagerdutyDataSchema,\n  phoneDataSchema,\n  telegramDataSchema,\n  slackDataSchema,\n  webhookDataSchema,\n  whatsappDataSchema,\n  googleChatDataSchema,\n]);\n\nexport const InsertNotificationWithDataSchema = z.discriminatedUnion(\n  \"provider\",\n  [\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"discord\"),\n        data: discordDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"email\"),\n        data: emailDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"google-chat\"),\n        data: googleChatDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"grafana-oncall\"),\n        data: grafanaOncallDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"ntfy\"),\n        data: ntfyDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"pagerduty\"),\n        data: pagerdutyDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"opsgenie\"),\n        data: opsgenieDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"sms\"),\n        data: phoneDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"slack\"),\n        data: slackDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"telegram\"),\n        data: telegramDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"webhook\"),\n        data: webhookDataSchema,\n      }).shape,\n    ),\n    insertNotificationSchema.extend(\n      z.object({\n        provider: z.literal(\"whatsapp\"),\n        data: whatsappDataSchema,\n      }).shape,\n    ),\n  ],\n);\n\nexport type InsertNotificationWithData = z.infer<\n  typeof InsertNotificationWithDataSchema\n>;\n"
  },
  {
    "path": "packages/db/src/schema/page_component_groups/index.ts",
    "content": "export * from \"./page_component_groups\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/page_component_groups/page_component_groups.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\nimport { pageComponent } from \"../page_components\";\nimport { page } from \"../pages\";\nimport { workspace } from \"../workspaces\";\n\nexport const pageComponentGroup = sqliteTable(\"page_component_groups\", {\n  id: integer(\"id\").primaryKey(),\n  workspaceId: integer(\"workspace_id\")\n    .references(() => workspace.id, { onDelete: \"cascade\" })\n    .notNull(),\n  pageId: integer(\"page_id\")\n    .references(() => page.id, { onDelete: \"cascade\" })\n    .notNull(),\n  name: text(\"name\").notNull(),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n\nexport const pageComponentGroupRelations = relations(\n  pageComponentGroup,\n  ({ one, many }) => ({\n    workspace: one(workspace, {\n      fields: [pageComponentGroup.workspaceId],\n      references: [workspace.id],\n    }),\n    page: one(page, {\n      fields: [pageComponentGroup.pageId],\n      references: [page.id],\n    }),\n    pageComponents: many(pageComponent),\n  }),\n);\n"
  },
  {
    "path": "packages/db/src/schema/page_component_groups/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport type { z } from \"zod\";\n\nimport { pageComponentGroup } from \"./page_component_groups\";\n\nexport const selectPageComponentGroupSchema =\n  createSelectSchema(pageComponentGroup);\n\nexport const insertPageComponentGroupSchema =\n  createInsertSchema(pageComponentGroup);\n\nexport type InsertPageComponentGroup = z.infer<\n  typeof insertPageComponentGroupSchema\n>;\nexport type PageComponentGroup = z.infer<typeof selectPageComponentGroupSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/page_components/constants.ts",
    "content": "export const pageComponentTypes = [\"static\", \"monitor\"] as const;\n\nexport type PageComponentType = (typeof pageComponentTypes)[number];\n"
  },
  {
    "path": "packages/db/src/schema/page_components/index.ts",
    "content": "export * from \"./page_components\";\nexport * from \"./validation\";\nexport * from \"./constants\";\n"
  },
  {
    "path": "packages/db/src/schema/page_components/page_components.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport {\n  check,\n  integer,\n  primaryKey,\n  sqliteTable,\n  text,\n  unique,\n} from \"drizzle-orm/sqlite-core\";\n\nimport { maintenance } from \"../maintenances\";\nimport { monitor } from \"../monitors\";\nimport { pageComponentGroup } from \"../page_component_groups\";\nimport { page } from \"../pages\";\nimport { statusReport } from \"../status_reports\";\nimport { workspace } from \"../workspaces\";\nimport { pageComponentTypes } from \"./constants\";\n\nexport const pageComponent = sqliteTable(\n  \"page_component\",\n  {\n    id: integer(\"id\").primaryKey(),\n    workspaceId: integer(\"workspace_id\")\n      .notNull()\n      .references(() => workspace.id, { onDelete: \"cascade\" }),\n    pageId: integer(\"page_id\")\n      .notNull()\n      .references(() => page.id, { onDelete: \"cascade\" }),\n    type: text(\"type\", { enum: pageComponentTypes })\n      .notNull()\n      .default(\"monitor\"),\n    monitorId: integer(\"monitor_id\").references(() => monitor.id, {\n      onDelete: \"cascade\",\n    }),\n    name: text(\"name\").notNull(),\n    description: text(\"description\"),\n    order: integer(\"order\").default(0),\n    groupId: integer(\"group_id\").references(() => pageComponentGroup.id, {\n      onDelete: \"set null\",\n    }),\n    groupOrder: integer(\"group_order\").default(0),\n\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n    updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (t) => [\n    unique(\"page_component_page_id_monitor_id_unique\").on(\n      t.pageId,\n      t.monitorId,\n    ),\n    check(\n      \"page_component_type_check\",\n      //   NOTE: This check ensures that either the component is a monitor or a static component, but not both.\n      sql`${t.type} = 'monitor' AND ${t.monitorId} IS NOT NULL OR ${t.type} = 'static' AND ${t.monitorId} IS NULL`,\n    ),\n  ],\n);\n\nexport const pageComponentRelations = relations(\n  pageComponent,\n  ({ one, many }) => ({\n    workspace: one(workspace, {\n      fields: [pageComponent.workspaceId],\n      references: [workspace.id],\n    }),\n    page: one(page, {\n      fields: [pageComponent.pageId],\n      references: [page.id],\n    }),\n    monitor: one(monitor, {\n      fields: [pageComponent.monitorId],\n      references: [monitor.id],\n    }),\n    group: one(pageComponentGroup, {\n      fields: [pageComponent.groupId],\n      references: [pageComponentGroup.id],\n    }),\n    statusReportsToPageComponents: many(statusReportsToPageComponents),\n    maintenancesToPageComponents: many(maintenancesToPageComponents),\n  }),\n);\n\nexport const maintenancesToPageComponents = sqliteTable(\n  \"maintenance_to_page_component\",\n  {\n    maintenanceId: integer(\"maintenance_id\")\n      .notNull()\n      .references(() => maintenance.id, { onDelete: \"cascade\" }),\n    pageComponentId: integer(\"page_component_id\")\n      .notNull()\n      .references(() => pageComponent.id, { onDelete: \"cascade\" }),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (t) => ({\n    pk: primaryKey({ columns: [t.maintenanceId, t.pageComponentId] }),\n  }),\n);\n\nexport const maintenancesToPageComponentsRelations = relations(\n  maintenancesToPageComponents,\n  ({ one }) => ({\n    maintenance: one(maintenance, {\n      fields: [maintenancesToPageComponents.maintenanceId],\n      references: [maintenance.id],\n    }),\n    pageComponent: one(pageComponent, {\n      fields: [maintenancesToPageComponents.pageComponentId],\n      references: [pageComponent.id],\n    }),\n  }),\n);\n\nexport const statusReportsToPageComponents = sqliteTable(\n  \"status_report_to_page_component\",\n  {\n    statusReportId: integer(\"status_report_id\")\n      .notNull()\n      .references(() => statusReport.id, { onDelete: \"cascade\" }),\n    pageComponentId: integer(\"page_component_id\")\n      .notNull()\n      .references(() => pageComponent.id, { onDelete: \"cascade\" }),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (t) => ({\n    pk: primaryKey({ columns: [t.statusReportId, t.pageComponentId] }),\n  }),\n);\n\nexport const statusReportsToPageComponentsRelations = relations(\n  statusReportsToPageComponents,\n  ({ one }) => ({\n    statusReport: one(statusReport, {\n      fields: [statusReportsToPageComponents.statusReportId],\n      references: [statusReport.id],\n    }),\n    pageComponent: one(pageComponent, {\n      fields: [statusReportsToPageComponents.pageComponentId],\n      references: [pageComponent.id],\n    }),\n  }),\n);\n"
  },
  {
    "path": "packages/db/src/schema/page_components/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport type { z } from \"zod\";\nimport { pageComponent } from \"./page_components\";\n\nexport const selectPageComponentSchema = createSelectSchema(pageComponent);\n\nexport const insertPageComponentSchema = createInsertSchema(pageComponent, {\n  name: (schema) => schema.min(1),\n}).refine(\n  (data) => {\n    // monitorId must be set when type='monitor'\n    if (data.type === \"monitor\" && !data.monitorId) {\n      return false;\n    }\n    // monitorId must be null when type='static'\n    if (data.type === \"static\" && data.monitorId) {\n      return false;\n    }\n    return true;\n  },\n  {\n    message:\n      \"monitorId must be set when type is 'monitor' and must be null when type is 'static'\",\n  },\n);\n\nexport type InsertPageComponent = z.infer<typeof insertPageComponentSchema>;\nexport type PageComponent = z.infer<typeof selectPageComponentSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/page_subscribers/index.ts",
    "content": "export * from \"./page_subscribers\";\nexport * from \"./page_subscriber_to_page_component\";\nexport * from \"./validation\";\nexport type * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/page_subscribers/page_subscriber_to_page_component.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport { integer, primaryKey, sqliteTable } from \"drizzle-orm/sqlite-core\";\n\nimport { pageComponent } from \"../page_components\";\nimport { pageSubscriber } from \"./page_subscribers\";\n\nexport const pageSubscriberToPageComponent = sqliteTable(\n  \"page_subscriber_to_page_component\",\n  {\n    pageSubscriberId: integer(\"page_subscriber_id\")\n      .notNull()\n      .references(() => pageSubscriber.id, { onDelete: \"cascade\" }),\n    pageComponentId: integer(\"page_component_id\")\n      .notNull()\n      .references(() => pageComponent.id, { onDelete: \"cascade\" }),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (t) => ({\n    pk: primaryKey({ columns: [t.pageSubscriberId, t.pageComponentId] }),\n  }),\n);\n\nexport const pageSubscriberToPageComponentRelations = relations(\n  pageSubscriberToPageComponent,\n  ({ one }) => ({\n    pageSubscriber: one(pageSubscriber, {\n      fields: [pageSubscriberToPageComponent.pageSubscriberId],\n      references: [pageSubscriber.id],\n    }),\n    pageComponent: one(pageComponent, {\n      fields: [pageSubscriberToPageComponent.pageComponentId],\n      references: [pageComponent.id],\n    }),\n  }),\n);\n"
  },
  {
    "path": "packages/db/src/schema/page_subscribers/page_subscribers.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport {\n  check,\n  integer,\n  sqliteTable,\n  text,\n  uniqueIndex,\n} from \"drizzle-orm/sqlite-core\";\n\nimport { page } from \"../pages\";\nimport { pageSubscriberToPageComponent } from \"./page_subscriber_to_page_component\";\n\nexport const pageSubscriber = sqliteTable(\n  \"page_subscriber\",\n  {\n    id: integer(\"id\").primaryKey(),\n    email: text(\"email\").notNull(),\n\n    pageId: integer(\"page_id\")\n      .notNull()\n      .references(() => page.id, { onDelete: \"cascade\" }),\n\n    // Added: channel type discriminator\n    channelType: text(\"channel_type\", {\n      enum: [\"email\", \"webhook\"],\n    })\n      .notNull()\n      .default(\"email\"),\n\n    // Added: webhook-specific fields (null for email channel)\n    webhookUrl: text(\"webhook_url\"),\n    channelConfig: text(\"channel_config\"),\n\n    token: text(\"token\"),\n    acceptedAt: integer(\"accepted_at\", { mode: \"timestamp\" }),\n    expiresAt: integer(\"expires_at\", { mode: \"timestamp\" }),\n    unsubscribedAt: integer(\"unsubscribed_at\", { mode: \"timestamp\" }),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n    updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (table) => ({\n    // Partial unique index: one active email subscription per page\n    emailPageActiveIdx: uniqueIndex(\"idx_page_subscriber_email_page_active\")\n      .on(sql`LOWER(${table.email})`, table.pageId)\n      .where(\n        sql`${table.unsubscribedAt} IS NULL AND ${table.channelType} = 'email'`,\n      ),\n\n    // Partial unique index: one active webhook subscription per page\n    webhookPageActiveIdx: uniqueIndex(\"idx_page_subscriber_webhook_page_active\")\n      .on(table.webhookUrl, table.pageId)\n      .where(\n        sql`${table.unsubscribedAt} IS NULL AND ${table.channelType} = 'webhook'`,\n      ),\n\n    // CHECK constraint: only correct identifier populated per channel type\n    channelCheck: check(\n      \"page_subscriber_channel_check\",\n      sql`(${table.channelType} = 'email' AND ${table.email} IS NOT NULL AND ${table.webhookUrl} IS NULL) OR (${table.channelType} = 'webhook' AND ${table.webhookUrl} IS NOT NULL AND ${table.email} IS NULL)`,\n    ),\n  }),\n);\n\nexport const pageSubscriberRelation = relations(\n  pageSubscriber,\n  ({ one, many }) => ({\n    page: one(page, {\n      fields: [pageSubscriber.pageId],\n      references: [page.id],\n    }),\n    components: many(pageSubscriberToPageComponent),\n  }),\n);\n"
  },
  {
    "path": "packages/db/src/schema/page_subscribers/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\n\nimport { pageSubscriber } from \"./page_subscribers\";\n\n// Base schemas (auto-generated from Drizzle schema)\nexport const selectPageSubscriberSchema = createSelectSchema(pageSubscriber);\nexport const insertPageSubscriberSchema = createInsertSchema(pageSubscriber, {\n  email: z.email().nullable(),\n});\n\n// Channel config schema for webhook\nexport const webhookChannelConfigSchema = z.object({\n  headers: z\n    .array(\n      z.object({\n        key: z.string().min(1),\n        value: z.string(),\n      }),\n    )\n    .optional(),\n  secret: z.string().optional(),\n});\n\n// Discriminated union for type-safe subscribers\nexport const pageSubscriberSchema = z.discriminatedUnion(\"channelType\", [\n  // Email channel\n  z.object({\n    id: z.number(),\n    pageId: z.number(),\n    channelType: z.literal(\"email\"),\n    email: z.email(),\n    webhookUrl: z.null(),\n    channelConfig: z.null(),\n    token: z.string().nullable(),\n    acceptedAt: z.date().nullable(),\n    expiresAt: z.date().nullable(),\n    unsubscribedAt: z.date().nullable(),\n    createdAt: z.date().nullable(),\n    updatedAt: z.date().nullable(),\n  }),\n  // Webhook channel\n  z.object({\n    id: z.number(),\n    pageId: z.number(),\n    channelType: z.literal(\"webhook\"),\n    email: z.null(),\n    webhookUrl: z.string().url(),\n    channelConfig: z\n      .string()\n      .nullable()\n      .transform((str) => {\n        if (!str) return null;\n        try {\n          return JSON.parse(str);\n        } catch {\n          return null;\n        }\n      })\n      .pipe(webhookChannelConfigSchema.nullable()),\n    token: z.string().nullable(),\n    acceptedAt: z.date().nullable(),\n    expiresAt: z.date().nullable(),\n    unsubscribedAt: z.date().nullable(),\n    createdAt: z.date().nullable(),\n    updatedAt: z.date().nullable(),\n  }),\n]);\n\nexport type InsertPageSubscriber = z.infer<typeof insertPageSubscriberSchema>;\nexport type PageSubscriber = z.infer<typeof selectPageSubscriberSchema>;\nexport type ChannelType = \"email\" | \"webhook\";\nexport type EmailSubscriber = Extract<\n  z.infer<typeof pageSubscriberSchema>,\n  { channelType: \"email\" }\n>;\nexport type WebhookSubscriber = Extract<\n  z.infer<typeof pageSubscriberSchema>,\n  { channelType: \"webhook\" }\n>;\n"
  },
  {
    "path": "packages/db/src/schema/pages/constants.ts",
    "content": "export const subdomainSafeList = [\n  \"api\",\n  \"app\",\n  \"www\",\n  \"docs\",\n  \"checker\",\n  \"time\",\n  \"help\",\n  \"data-table\",\n  \"light\",\n  \"workflows\",\n  \"template\",\n  \"ssh\",\n  \"themes\",\n];\n\nexport const pageAccessTypes = [\"public\", \"password\", \"email-domain\"] as const;\n"
  },
  {
    "path": "packages/db/src/schema/pages/index.ts",
    "content": "export * from \"./page\";\nexport * from \"./validation\";\nexport type * from \"./validation\";\nexport * from \"./constants\";\n"
  },
  {
    "path": "packages/db/src/schema/pages/page.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\nimport { maintenance } from \"../maintenances\";\nimport { pageComponentGroup } from \"../page_component_groups\";\nimport { pageComponent } from \"../page_components\";\nimport { pageSubscriber } from \"../page_subscribers\";\nimport { statusReport } from \"../status_reports\";\nimport { workspace } from \"../workspaces\";\nimport { pageAccessTypes } from \"./constants\";\n\nexport const page = sqliteTable(\"page\", {\n  id: integer(\"id\").primaryKey(),\n\n  workspaceId: integer(\"workspace_id\")\n    .notNull()\n    .references(() => workspace.id, { onDelete: \"cascade\" }),\n\n  title: text(\"title\").notNull(), // title of the page\n  description: text(\"description\").notNull(), // description of the page\n  icon: text(\"icon\", { length: 256 }).default(\"\"), // icon of the page\n  slug: text(\"slug\", { length: 256 }).notNull().unique(), // which is used for https://slug.openstatus.dev\n  customDomain: text(\"custom_domain\", { length: 256 }).notNull(),\n  published: integer(\"published\", { mode: \"boolean\" }).default(false),\n\n  forceTheme: text(\"force_theme\", { enum: [\"dark\", \"light\", \"system\"] })\n    .notNull()\n    .default(\"system\"),\n\n  // Password protecting the status page - no specific restriction on password\n  password: text(\"password\", { length: 256 }),\n  // @deprecated: instead, use accessType\n  passwordProtected: integer(\"password_protected\", { mode: \"boolean\" }).default(\n    false,\n  ),\n  accessType: text(\"access_type\", { enum: pageAccessTypes }).default(\"public\"),\n  authEmailDomains: text(\"auth_email_domains\", { mode: \"text\" }), // TODO: change to json\n\n  // links and urls\n  homepageUrl: text(\"homepage_url\", { length: 256 }),\n  contactUrl: text(\"contact_url\", { length: 256 }),\n\n  legacyPage: integer(\"legacy_page\", { mode: \"boolean\" })\n    .notNull()\n    .default(true),\n  configuration: text(\"configuration\", { mode: \"json\" }),\n\n  /**\n   * Displays the total and failed request numbers for each monitor\n   * TODO: remove this column - we moved into configuration\n   */\n  showMonitorValues: integer(\"show_monitor_values\", {\n    mode: \"boolean\",\n  }).default(true),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n\nexport const pageRelations = relations(page, ({ many, one }) => ({\n  maintenances: many(maintenance),\n  statusReports: many(statusReport),\n  workspace: one(workspace, {\n    fields: [page.workspaceId],\n    references: [workspace.id],\n  }),\n  pageSubscribers: many(pageSubscriber),\n  pageComponents: many(pageComponent),\n  pageComponentGroups: many(pageComponentGroup),\n}));\n"
  },
  {
    "path": "packages/db/src/schema/pages/validation.ts",
    "content": "import type { ThemeKey } from \"@openstatus/theme-store\";\nimport { THEME_KEYS } from \"@openstatus/theme-store\";\nimport { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\n\nimport { pageAccessTypes } from \"./constants\";\nimport { page } from \"./page\";\n\nconst slugSchema = z\n  .string()\n  .regex(\n    /^[A-Za-z0-9-]+$/,\n    \"Only use digits (0-9), hyphen (-) or characters (A-Z, a-z).\",\n  )\n  .min(3)\n  .toLowerCase();\n\nconst customDomainSchema = z\n  .string()\n  .regex(\n    /^(?!https?:\\/\\/|www.)([a-zA-Z0-9]+(.[a-zA-Z0-9]+)+.*)$/,\n    \"Should not start with http://, https:// or www.\",\n  )\n  .or(z.enum([\"\"]));\n\nconst stringToArray = z.preprocess((val) => {\n  if (val && String(val).length > 0) {\n    return String(val).split(\",\");\n  }\n  return [];\n}, z.array(z.string()));\n\nexport const insertPageSchema = createInsertSchema(page, {\n  customDomain: customDomainSchema.prefault(\"\"),\n  accessType: z.enum(pageAccessTypes).prefault(\"public\"),\n  icon: z.string().optional(),\n  slug: slugSchema,\n}).extend({\n  password: z.string().nullable().optional().prefault(\"\"),\n  monitors: z\n    .array(\n      z.object({\n        // REMINDER: has to be different from `id` in as the prop is already used by react-hook-form\n        monitorId: z.number(),\n        order: z.number().prefault(0).optional(),\n      }),\n    )\n    .optional()\n    .prefault([]),\n  authEmailDomains: z.array(z.string()).nullish(),\n});\n\nexport const pageConfigurationSchema = z.object({\n  value: z\n    .enum([\"duration\", \"requests\", \"manual\"])\n    .nullish()\n    .prefault(\"requests\"),\n  type: z.enum([\"absolute\", \"manual\"]).nullish().prefault(\"absolute\"),\n  uptime: z.coerce.boolean().nullish().prefault(true),\n  theme: z\n    .enum(THEME_KEYS as [ThemeKey, ...ThemeKey[]])\n    .nullish()\n    .prefault(\"default\"),\n});\n\nexport const selectPageSchema = createSelectSchema(page).extend({\n  password: z.string().optional().nullable().prefault(\"\"),\n  configuration: pageConfigurationSchema.nullish().prefault({}),\n  accessType: z.enum(pageAccessTypes).prefault(\"public\"),\n  authEmailDomains: stringToArray.prefault([]),\n});\n\nexport type InsertPage = z.infer<typeof insertPageSchema>;\nexport type Page = z.infer<typeof selectPageSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/plan/config.ts",
    "content": "import { AVAILABLE_REGIONS, FREE_FLY_REGIONS } from \"@openstatus/regions\";\nimport type { WorkspacePlan } from \"../workspaces/validation\";\nimport type { Addons, PlanLimits, Price } from \"./schema\";\n\ntype PlanConfig = {\n  title: \"Hobby\" | \"Starter\" | \"Pro\";\n  id: WorkspacePlan;\n  description: string;\n  price: Price;\n  addons: Partial<{\n    [K in keyof Addons]: {\n      title: string;\n      description: string;\n      price: Price;\n    };\n  }>;\n  limits: PlanLimits;\n};\n\n// TODO: rename to `planConfig`\nexport const allPlans: Record<WorkspacePlan, PlanConfig> = {\n  free: {\n    title: \"Hobby\",\n    id: \"free\",\n    description: \"Perfect for personal projects\",\n    price: {\n      USD: 0,\n      EUR: 0,\n      INR: 0,\n    },\n    addons: {},\n    limits: {\n      version: undefined,\n      monitors: 1,\n      \"synthetic-checks\": 30,\n      periodicity: [\"10m\", \"30m\", \"1h\"],\n      \"multi-region\": true,\n      \"max-regions\": 6,\n      \"data-retention\": \"14 days\",\n      \"status-pages\": 1,\n      \"page-components\": 3,\n      maintenance: true,\n      \"monitor-values-visibility\": true,\n      \"response-logs\": false,\n      screenshots: false,\n      otel: false,\n      \"status-subscribers\": false,\n      \"custom-domain\": false,\n      \"password-protection\": false,\n      \"email-domain-protection\": false,\n      \"white-label\": false,\n      notifications: true,\n      sms: false,\n      \"sms-limit\": 0,\n      pagerduty: false,\n      opsgenie: false,\n      \"grafana-oncall\": false,\n      whatsapp: false,\n      \"notification-channels\": 1,\n      members: 1,\n      \"audit-log\": false,\n      regions: [...FREE_FLY_REGIONS],\n      \"private-locations\": false,\n      \"slack-agent\": false,\n    },\n  },\n  starter: {\n    title: \"Starter\",\n    id: \"starter\",\n    description: \"Perfect for uptime monitoring\",\n    price: {\n      USD: 30,\n      EUR: 30,\n      INR: 3000,\n    },\n    addons: {\n      \"email-domain-protection\": {\n        title: \"Magic Link (Auth)\",\n        description:\n          \"Only allow user with a given email domain to access the status page.\",\n        price: {\n          USD: 100,\n          EUR: 100,\n          INR: 10_000,\n        },\n      },\n      \"white-label\": {\n        title: \"White Label\",\n        description:\n          \"Remove the 'powered by openstatus.dev' footer from your status pages.\",\n        price: {\n          USD: 300,\n          EUR: 300,\n          INR: 30_000,\n        },\n      },\n      \"status-pages\": {\n        title: \"Status Pages\",\n        description: \"Create and manage status pages for your workspace.\",\n        price: {\n          USD: 20,\n          EUR: 20,\n          INR: 2_000,\n        },\n      },\n    },\n    limits: {\n      version: undefined,\n      monitors: 20,\n      \"synthetic-checks\": 100,\n      periodicity: [\"1m\", \"5m\", \"10m\", \"30m\", \"1h\"],\n      \"multi-region\": true,\n      \"max-regions\": 6,\n      \"data-retention\": \"3 months\",\n      \"status-pages\": 1,\n      \"page-components\": 20,\n      maintenance: true,\n      \"monitor-values-visibility\": true,\n      \"response-logs\": true,\n      screenshots: true,\n      otel: false,\n      \"status-subscribers\": true,\n      \"custom-domain\": true,\n      \"password-protection\": true,\n      \"email-domain-protection\": false,\n      \"white-label\": false,\n      notifications: true,\n      pagerduty: true,\n      opsgenie: true,\n      \"grafana-oncall\": true,\n      whatsapp: true,\n      sms: true,\n      \"sms-limit\": 50,\n      \"notification-channels\": 10,\n      members: \"Unlimited\",\n      \"audit-log\": false,\n      regions: [...AVAILABLE_REGIONS],\n      \"private-locations\": false,\n      \"slack-agent\": true,\n    },\n  },\n  team: {\n    title: \"Pro\",\n    id: \"team\",\n    description: \"Perfect for global synthetic monitoring\",\n    price: {\n      USD: 100,\n      EUR: 100,\n      INR: 10_000,\n    },\n    addons: {\n      \"email-domain-protection\": {\n        title: \"Magic Link (Auth)\",\n        description:\n          \"Only allow user with a given email domain to access the status page.\",\n        price: {\n          USD: 100,\n          EUR: 100,\n          INR: 10_000,\n        },\n      },\n      \"white-label\": {\n        title: \"White Label\",\n        description:\n          \"Remove the 'powered by openstatus.dev' footer from your status pages.\",\n        price: {\n          USD: 300,\n          EUR: 300,\n          INR: 30_000,\n        },\n      },\n      \"status-pages\": {\n        title: \"Status Pages\",\n        description: \"Create and manage status pages for your workspace.\",\n        price: {\n          USD: 20,\n          EUR: 20,\n          INR: 2_000,\n        },\n      },\n    },\n    limits: {\n      version: undefined,\n      monitors: 50,\n      \"synthetic-checks\": 300,\n      periodicity: [\"30s\", \"1m\", \"5m\", \"10m\", \"30m\", \"1h\"],\n      \"multi-region\": true,\n      \"max-regions\": AVAILABLE_REGIONS.length,\n      \"data-retention\": \"12 months\",\n      \"status-pages\": 5,\n      \"page-components\": 50,\n      maintenance: true,\n      \"monitor-values-visibility\": true,\n      \"response-logs\": true,\n      screenshots: true,\n      otel: true,\n      \"status-subscribers\": true,\n      \"custom-domain\": true,\n      \"password-protection\": true,\n      \"email-domain-protection\": false,\n      \"white-label\": false,\n      notifications: true,\n      sms: true,\n      \"sms-limit\": 100,\n      pagerduty: true,\n      opsgenie: true,\n      \"grafana-oncall\": true,\n      whatsapp: true,\n      \"notification-channels\": 20,\n      members: \"Unlimited\",\n      \"audit-log\": true,\n      regions: [...AVAILABLE_REGIONS],\n      \"private-locations\": true,\n      \"slack-agent\": true,\n    },\n  },\n};\n"
  },
  {
    "path": "packages/db/src/schema/plan/schema.ts",
    "content": "import { FREE_FLY_REGIONS } from \"@openstatus/regions\";\nimport { z } from \"zod\";\nimport { monitorPeriodicitySchema, monitorRegionSchema } from \"../constants\";\n\n// REMINDER: this is not a database table but just a schema for the limits of the plan\n// default values are set to the free plan limits\n\nexport const limitsSchema = z.object({\n  version: z.undefined(),\n  /**\n   * Monitor limits\n   */\n  monitors: z.number().prefault(1),\n  \"synthetic-checks\": z.number().prefault(30), // monthly limits\n  periodicity: monitorPeriodicitySchema.array().prefault([\"10m\", \"30m\", \"1h\"]),\n  \"multi-region\": z.boolean().prefault(true),\n  \"max-regions\": z.number().prefault(6),\n  \"data-retention\": z\n    .enum([\"14 days\", \"3 months\", \"12 months\", \"24 months\"])\n    .prefault(\"14 days\"),\n  regions: monitorRegionSchema.array().prefault(FREE_FLY_REGIONS),\n  \"private-locations\": z.boolean().prefault(false),\n  screenshots: z.boolean().prefault(false),\n  \"response-logs\": z.boolean().prefault(false),\n  otel: z.boolean().prefault(false),\n  /**\n   * Status page limits\n   */\n  \"status-pages\": z.number().prefault(1),\n  \"page-components\": z.number().prefault(3),\n  maintenance: z.boolean().prefault(true),\n  \"monitor-values-visibility\": z.boolean().prefault(true),\n  \"status-subscribers\": z.boolean().prefault(false),\n  \"custom-domain\": z.boolean().prefault(false),\n  \"password-protection\": z.boolean().prefault(false),\n  \"email-domain-protection\": z.boolean().prefault(false), // add-on but required in limits\n  \"white-label\": z.boolean().prefault(false),\n  /**\n   * Notification limits\n   */\n\n  notifications: z.boolean().prefault(true),\n  pagerduty: z.boolean().prefault(false),\n  opsgenie: z.boolean().prefault(false),\n  \"grafana-oncall\": z.boolean().prefault(false),\n  whatsapp: z.boolean().prefault(false),\n  sms: z.boolean().prefault(false),\n  \"sms-limit\": z.number().prefault(0),\n  \"notification-channels\": z.number().prefault(1),\n\n  /**\n   * Collaboration limits\n   */\n  members: z.literal(\"Unlimited\").or(z.number()).prefault(1),\n  \"audit-log\": z.boolean().prefault(false),\n\n  /**\n   * Other limits\n   */\n  \"slack-agent\": z.boolean().prefault(false),\n});\n\nexport type Limits = z.infer<typeof limitsSchema>;\n\n//\n\nconst priceSchema = z.object({\n  USD: z.number(),\n  EUR: z.number(),\n  INR: z.number(),\n});\n\nexport type Price = z.infer<typeof priceSchema>;\n\nexport const addons = [\n  \"email-domain-protection\",\n  \"white-label\",\n  \"status-pages\",\n] as const satisfies Partial<keyof Limits>[];\n\nexport const addonsSchema = z.partialRecord(\n  z.enum(addons),\n  z.object({\n    price: priceSchema,\n  }),\n) satisfies z.ZodType<Partial<Record<keyof Limits, { price: Price }>>>;\n\nexport type Addons = z.infer<typeof addonsSchema>;\n\n/**\n * Enforces that addon keys in Limits must be set to false in plan configs\n * (since addons can only be enabled by purchasing them)\n */\nexport type PlanLimits = {\n  [K in keyof Limits]: K extends keyof Addons\n    ? Limits[K] extends boolean\n      ? false // Force addon boolean fields to false\n      : Limits[K] // Non-boolean fields stay as-is\n    : Limits[K]; // Non-addon fields stay as-is\n};\n"
  },
  {
    "path": "packages/db/src/schema/plan/utils.ts",
    "content": "import type { WorkspacePlan } from \"../workspaces/validation\";\nimport { allPlans } from \"./config\";\nimport { type Addons, type Limits, limitsSchema } from \"./schema\";\n\nexport function getLimit<T extends keyof Limits>(limits: Limits, limit: T) {\n  return limits[limit] || allPlans.free.limits[limit];\n}\n\nexport function getLimits(plan: WorkspacePlan | null) {\n  return allPlans[plan || \"free\"].limits;\n}\n\nexport function getPlanConfig(plan: WorkspacePlan | null) {\n  return allPlans[plan || \"free\"];\n}\n\nexport function getCurrency({\n  continent,\n  country,\n}: {\n  continent: string;\n  country: string;\n}) {\n  if (country === \"IN\") {\n    return \"INR\";\n  }\n  if (continent === \"EU\") {\n    return \"EUR\";\n  }\n  return \"USD\";\n}\n\ntype PriceObject = {\n  USD: number;\n  EUR: number;\n  INR: number;\n};\n\ntype PriceConfig = {\n  value: number;\n  locale: string;\n  currency: string;\n};\n\nfunction getLocaleForCurrency(currency: string): string {\n  return currency === \"EUR\" ? \"fr-FR\" : \"en-US\";\n}\n\nfunction resolvePriceConfig(\n  price: PriceObject,\n  currency?: string,\n): PriceConfig {\n  const effectiveCurrency = currency && currency in price ? currency : \"USD\";\n  const value = price[effectiveCurrency as keyof PriceObject];\n  const locale = getLocaleForCurrency(effectiveCurrency);\n\n  return { value, locale, currency: effectiveCurrency };\n}\n\nexport function getPriceConfig(plan: WorkspacePlan, currency?: string) {\n  const planConfig = allPlans[plan];\n  return resolvePriceConfig(planConfig.price, currency);\n}\n\nexport function getAddonPriceConfig(\n  plan: WorkspacePlan,\n  addon: keyof Addons,\n  currency?: string,\n) {\n  const addonConfig = allPlans[plan].addons[addon];\n  if (!addonConfig) {\n    return null;\n  }\n  return resolvePriceConfig(addonConfig.price, currency);\n}\n\nexport function getPlansForLimit(\n  currentPlan: WorkspacePlan,\n  limit: keyof Limits,\n): WorkspacePlan[] {\n  const currentLimitValue = allPlans[currentPlan].limits[limit];\n  const planOrder: WorkspacePlan[] = [\"free\", \"starter\", \"team\"];\n\n  // Get plans that come after the current plan\n  const availablePlans = planOrder.filter((plan) => {\n    const planIndex = planOrder.indexOf(plan);\n    const currentIndex = planOrder.indexOf(currentPlan);\n    return planIndex > currentIndex;\n  });\n\n  // Filter plans based on the limit feature value\n  return availablePlans.filter((plan) => {\n    const planLimitValue = allPlans[plan].limits[limit];\n\n    // For boolean limits, only show plans where the feature is enabled\n    if (typeof currentLimitValue === \"boolean\") {\n      return planLimitValue === true;\n    }\n\n    // For numeric limits, show plans with higher values\n    if (\n      typeof currentLimitValue === \"number\" &&\n      typeof planLimitValue === \"number\"\n    ) {\n      return planLimitValue > currentLimitValue;\n    }\n\n    // For array limits (e.g., periodicity, regions), show plans with more options\n    if (Array.isArray(currentLimitValue) && Array.isArray(planLimitValue)) {\n      return planLimitValue.length > currentLimitValue.length;\n    }\n\n    // For string limits (e.g., data-retention), check if it's \"better\"\n    // This is a simple heuristic - could be improved based on specific needs\n    if (\n      typeof currentLimitValue === \"string\" &&\n      typeof planLimitValue === \"string\"\n    ) {\n      return planLimitValue !== currentLimitValue;\n    }\n\n    // For \"Unlimited\" string literal in members\n    if (planLimitValue === \"Unlimited\") {\n      return true;\n    }\n\n    return false;\n  });\n}\n\n/**\n * Update an addon value in limits\n * @param limits - Current workspace limits\n * @param addon - Addon key to update\n * @param value - The value to set (boolean for toggle addons, number for quantity addons)\n * @returns Updated limits object\n */\nexport function updateAddonInLimits(\n  limits: Limits,\n  addon: keyof Addons,\n  value: boolean | number,\n): Limits {\n  const currentValue = limits[addon];\n  const newLimits = { ...limits };\n\n  // Infer addon type from the limit field type and set the value\n  if (typeof currentValue === \"boolean\" && typeof value === \"boolean\") {\n    // Toggle addon: set boolean value\n    (newLimits[addon] as boolean) = value;\n  } else if (typeof currentValue === \"number\" && typeof value === \"number\") {\n    // Quantity addon: set numeric value (ensure it doesn't go below 0)\n    (newLimits[addon] as number) = Math.max(0, value);\n  }\n\n  return limitsSchema.parse(newLimits);\n}\n"
  },
  {
    "path": "packages/db/src/schema/private_locations/index.ts",
    "content": "export * from \"./private_locations\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/private_locations/private_locations.ts",
    "content": "import { relations } from \"drizzle-orm\";\nimport { sql } from \"drizzle-orm/sql\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { monitor } from \"../monitors/monitor\";\nimport { workspace } from \"../workspaces\";\n\nexport const privateLocation = sqliteTable(\"private_location\", {\n  id: integer(\"id\").primaryKey(),\n  name: text(\"name\").notNull(),\n  token: text(\"token\").notNull(),\n  lastSeenAt: integer(\"last_seen_at\", { mode: \"timestamp\" }),\n  workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n\nexport const privateLocationToMonitors = sqliteTable(\n  \"private_location_to_monitor\",\n  {\n    privateLocationId: integer(\"private_location_id\").references(\n      () => privateLocation.id,\n      { onDelete: \"cascade\" },\n    ),\n    monitorId: integer(\"monitor_id\").references(() => monitor.id, {\n      onDelete: \"cascade\",\n    }),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n    deletedAt: integer(\"deleted_at\", { mode: \"timestamp\" }),\n  },\n);\n\nexport const privateLocationRelation = relations(\n  privateLocation,\n  ({ many, one }) => ({\n    privateLocationToMonitors: many(privateLocationToMonitors),\n    workspace: one(workspace, {\n      fields: [privateLocation.workspaceId],\n      references: [workspace.id],\n    }),\n  }),\n);\n\nexport const privateLocationToMonitorsRelation = relations(\n  privateLocationToMonitors,\n  ({ one }) => ({\n    privateLocation: one(privateLocation, {\n      fields: [privateLocationToMonitors.privateLocationId],\n      references: [privateLocation.id],\n    }),\n    monitor: one(monitor, {\n      fields: [privateLocationToMonitors.monitorId],\n      references: [monitor.id],\n    }),\n  }),\n);\n"
  },
  {
    "path": "packages/db/src/schema/private_locations/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport type { z } from \"zod\";\n\nimport { privateLocation } from \"./private_locations\";\n\nexport const insertPrivateLocationSchema = createInsertSchema(privateLocation);\n\nexport const selectPrivateLocationSchema = createSelectSchema(privateLocation);\n\nexport type InsertPrivateLocation = z.infer<typeof insertPrivateLocationSchema>;\nexport type PrivateLocation = z.infer<typeof selectPrivateLocationSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/shared.ts",
    "content": "import { z } from \"zod\";\n\nimport { selectIncidentSchema } from \"./incidents/validation\";\nimport { selectMaintenanceSchema } from \"./maintenances\";\nimport { selectMonitorGroupSchema } from \"./monitor_groups\";\nimport { selectMonitorSchema } from \"./monitors\";\nimport { selectPageComponentGroupSchema } from \"./page_component_groups\";\nimport { selectPageComponentSchema } from \"./page_components\";\nimport { selectPageSchema } from \"./pages\";\nimport {\n  selectStatusReportSchema,\n  selectStatusReportUpdateSchema,\n} from \"./status_reports\";\nimport { workspacePlanSchema } from \"./workspaces\";\n\n// TODO: create a 'public-status' schema with all the different types and validations\n\n// Base schema without transform so it can be extended\nconst selectPublicMonitorBaseSchema = selectMonitorSchema.omit({\n  body: true,\n  headers: true,\n  method: true,\n  otelEndpoint: true,\n  otelHeaders: true,\n});\n\nexport const selectPublicMonitorSchema =\n  selectPublicMonitorBaseSchema.transform((data) => ({\n    ...data,\n    name: data.externalName || data.name,\n  }));\n\nexport const selectStatusReportPageSchema = selectStatusReportSchema.extend({\n  statusReportUpdates: z.array(selectStatusReportUpdateSchema).prefault([]),\n  statusReportsToPageComponents: z\n    .array(\n      z.object({\n        pageComponentId: z.number(),\n        statusReportId: z.number(),\n        pageComponent: selectPageComponentSchema,\n      }),\n    )\n    .prefault([]),\n});\n\nexport const selectMaintenancePageSchema = selectMaintenanceSchema.extend({\n  maintenancesToPageComponents: z\n    .array(\n      z.object({\n        pageComponentId: z.number(),\n        maintenanceId: z.number(),\n        pageComponent: selectPageComponentSchema,\n      }),\n    )\n    .prefault([]),\n});\n\nexport const selectPageSchemaWithRelation = selectPageSchema.extend({\n  monitors: z.array(selectMonitorSchema),\n  statusReports: z.array(selectStatusReportPageSchema),\n});\n\nexport const legacy_selectPublicPageSchemaWithRelation = selectPageSchema\n  .extend({\n    monitors: z.array(selectPublicMonitorSchema).prefault([]),\n    statusReports: z.array(selectStatusReportPageSchema).prefault([]),\n    incidents: z.array(selectIncidentSchema).prefault([]),\n    maintenances: z.array(selectMaintenancePageSchema).prefault([]),\n    workspacePlan: workspacePlanSchema\n      .nullable()\n      .prefault(\"free\")\n      .transform((val) => val ?? \"free\"),\n  })\n  .omit({\n    // workspaceId: true,\n    id: true,\n  });\n\nconst selectPublicMonitorWithStatusSchema = selectPublicMonitorBaseSchema\n  .extend({\n    status: z\n      .enum([\"success\", \"degraded\", \"error\", \"info\"])\n      .prefault(\"success\"),\n    monitorGroupId: z.number().nullable().optional(),\n    order: z.number().default(0).optional(),\n    groupOrder: z.number().default(0).nullish(),\n  })\n  .transform((data) => ({\n    ...data,\n    name: data.externalName || data.name,\n  }));\n\nexport const statusPageEventSchema = z.object({\n  id: z.number(),\n  name: z.string(),\n  from: z.date(),\n  to: z.date().nullable(),\n  status: z.enum([\"success\", \"degraded\", \"error\", \"info\"]).prefault(\"success\"),\n  type: z.enum([\"maintenance\", \"incident\", \"report\"]),\n});\n\n// Page component with status - used for new tracker system\nconst selectPublicPageComponentWithStatusSchema =\n  selectPageComponentSchema.extend({\n    status: z\n      .enum([\"success\", \"degraded\", \"error\", \"info\"])\n      .prefault(\"success\"),\n    events: z.array(statusPageEventSchema).prefault([]),\n    // For monitor-type components - omit status since it's now at component level\n    monitor: selectPublicMonitorBaseSchema\n      .extend({\n        incidents: selectIncidentSchema.array().nullish(),\n      })\n      .nullish(),\n  });\n\nconst trackersSchema = z\n  .array(\n    z.discriminatedUnion(\"type\", [\n      z.object({\n        type: z.literal(\"component\"),\n        component: selectPublicPageComponentWithStatusSchema,\n        order: z.number(),\n      }),\n      z.object({\n        type: z.literal(\"group\"),\n        groupId: z.number(),\n        groupName: z.string(),\n        components: z.array(selectPublicPageComponentWithStatusSchema),\n        status: z\n          .enum([\"success\", \"degraded\", \"error\", \"info\"])\n          .prefault(\"success\"),\n        order: z.number(),\n      }),\n    ]),\n  )\n  .prefault([]);\n\nexport const selectPageComponentWithMonitorRelation =\n  selectPageComponentSchema.extend({\n    monitor: selectPublicMonitorBaseSchema\n      .extend({\n        incidents: selectIncidentSchema.array().nullish(),\n      })\n      .transform((data) => ({\n        ...data,\n        name: data.externalName || data.name,\n      }))\n      .nullish(),\n    group: selectPageComponentGroupSchema.nullish(),\n  });\n\nexport type PageComponentWithMonitorRelation = z.infer<\n  typeof selectPageComponentWithMonitorRelation\n>;\n\nexport const selectPublicPageLightSchemaWithRelation = selectPageSchema\n  .extend({\n    monitors: z.array(selectPublicMonitorSchema).prefault([]),\n    statusReports: z.array(selectStatusReportPageSchema).prefault([]),\n    incidents: z.array(selectIncidentSchema).prefault([]),\n    maintenances: z.array(selectMaintenancePageSchema).prefault([]),\n    workspacePlan: workspacePlanSchema\n      .nullable()\n      .prefault(\"free\")\n      .transform((val) => val ?? \"free\"),\n    // NEW: Include pageComponents for modern consumers\n    pageComponents: selectPageComponentWithMonitorRelation.array().prefault([]),\n    pageComponentGroups: selectPageComponentGroupSchema.array().prefault([]),\n  })\n  .omit({\n    id: true,\n  });\n\nexport const selectPublicPageSchemaWithRelation = selectPageSchema.extend({\n  monitorGroups: selectMonitorGroupSchema.array().prefault([]),\n  // TODO: include status of the monitor\n  monitors: selectPublicMonitorWithStatusSchema.array(),\n  pageComponents: selectPageComponentWithMonitorRelation.array().prefault([]),\n  pageComponentGroups: selectPageComponentGroupSchema.array().prefault([]),\n  trackers: trackersSchema,\n  lastEvents: z.array(statusPageEventSchema),\n  openEvents: z.array(statusPageEventSchema),\n  statusReports: z.array(selectStatusReportPageSchema),\n  incidents: z.array(selectIncidentSchema),\n  maintenances: z.array(selectMaintenancePageSchema),\n  status: z.enum([\"success\", \"degraded\", \"error\", \"info\"]).prefault(\"success\"),\n  workspacePlan: workspacePlanSchema\n    .nullable()\n    .prefault(\"free\")\n    .transform((val) => val ?? \"free\"),\n  whiteLabel: z.boolean().prefault(false),\n});\n\nexport type StatusReportWithUpdates = z.infer<\n  typeof selectStatusReportPageSchema\n>;\nexport type PublicMonitor = z.infer<typeof selectPublicMonitorSchema>;\nexport type PublicPage = z.infer<\n  typeof legacy_selectPublicPageSchemaWithRelation\n>;\nexport type PublicPageComponentWithStatus = z.infer<\n  typeof selectPublicPageComponentWithStatusSchema\n>;\n"
  },
  {
    "path": "packages/db/src/schema/status_reports/index.ts",
    "content": "export * from \"./status_reports\";\nexport * from \"./validation\";\nexport type * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/status_reports/status_reports.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\nimport { statusReportsToPageComponents } from \"../page_components\";\nimport { page } from \"../pages\";\nimport { workspace } from \"../workspaces\";\n\nexport const statusReportStatus = [\n  \"investigating\",\n  \"identified\",\n  \"monitoring\",\n  \"resolved\",\n] as const;\n\nexport const statusReport = sqliteTable(\"status_report\", {\n  id: integer(\"id\").primaryKey(),\n  status: text(\"status\", { enum: statusReportStatus }).notNull(),\n  title: text(\"title\", { length: 256 }).notNull(),\n\n  workspaceId: integer(\"workspace_id\").references(() => workspace.id),\n\n  pageId: integer(\"page_id\").references(() => page.id, { onDelete: \"cascade\" }),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n\nexport const statusReportUpdate = sqliteTable(\"status_report_update\", {\n  id: integer(\"id\").primaryKey(),\n\n  status: text(\"status\", { enum: statusReportStatus }).notNull(),\n  date: integer(\"date\", { mode: \"timestamp\" }).notNull(),\n  message: text(\"message\").notNull(),\n\n  statusReportId: integer(\"status_report_id\")\n    .references(() => statusReport.id, { onDelete: \"cascade\" })\n    .notNull(),\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n\nexport const StatusReportRelations = relations(\n  statusReport,\n  ({ one, many }) => ({\n    statusReportsToPageComponents: many(statusReportsToPageComponents),\n    page: one(page, {\n      fields: [statusReport.pageId],\n      references: [page.id],\n    }),\n    statusReportUpdates: many(statusReportUpdate),\n    workspace: one(workspace, {\n      fields: [statusReport.workspaceId],\n      references: [workspace.id],\n    }),\n  }),\n);\n\nexport const statusReportUpdateRelations = relations(\n  statusReportUpdate,\n  ({ one }) => ({\n    statusReport: one(statusReport, {\n      fields: [statusReportUpdate.statusReportId],\n      references: [statusReport.id],\n    }),\n  }),\n);\n"
  },
  {
    "path": "packages/db/src/schema/status_reports/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport * as z from \"zod\";\n\nimport {\n  statusReport,\n  statusReportStatus,\n  statusReportUpdate,\n} from \"./status_reports\";\n\nexport const statusReportStatusSchema = z.enum(statusReportStatus);\n\nexport const insertStatusReportUpdateSchema = createInsertSchema(\n  statusReportUpdate,\n  {\n    status: statusReportStatusSchema,\n  },\n).extend({\n  date: z.coerce.date().optional().prefault(new Date()),\n});\n\nexport const insertStatusReportSchema = createInsertSchema(statusReport, {\n  status: statusReportStatusSchema,\n})\n  .extend({\n    date: z.coerce.date().optional().prefault(new Date()),\n    /**\n     * relationship to monitors and pages\n     */\n    monitors: z.number().array().optional().prefault([]),\n  })\n  .extend({\n    /**\n     * message for the `InsertIncidentUpdate`\n     */\n    message: z.string(),\n  });\n\nexport const selectStatusReportSchema = createSelectSchema(statusReport, {\n  status: statusReportStatusSchema,\n});\n\nexport const selectStatusReportUpdateSchema = createSelectSchema(\n  statusReportUpdate,\n  {\n    status: statusReportStatusSchema,\n  },\n);\n\nexport type InsertStatusReport = z.infer<typeof insertStatusReportSchema>;\nexport type StatusReport = z.infer<typeof selectStatusReportSchema>;\nexport type InsertStatusReportUpdate = z.infer<\n  typeof insertStatusReportUpdateSchema\n>;\nexport type StatusReportUpdate = z.infer<typeof selectStatusReportUpdateSchema>;\nexport type StatusReportStatus = z.infer<typeof statusReportStatusSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/users/index.ts",
    "content": "export * from \"./user\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/users/user.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport {\n  integer,\n  primaryKey,\n  sqliteTable,\n  text,\n} from \"drizzle-orm/sqlite-core\";\nimport type { AdapterAccount } from \"next-auth/adapters\";\n\nimport { workspace, workspaceRole } from \"../workspaces\";\n\nexport const user = sqliteTable(\"user\", {\n  id: integer(\"id\").primaryKey(),\n\n  // clerk fields\n  tenantId: text(\"tenant_id\", { length: 256 }).unique(), // the clerk User Id\n  firstName: text(\"first_name\").default(\"\"),\n  lastName: text(\"last_name\").default(\"\"),\n  photoUrl: text(\"photo_url\").default(\"\"),\n\n  // next-auth fields\n  name: text(\"name\"),\n  email: text(\"email\").default(\"\"),\n  emailVerified: integer(\"emailVerified\", { mode: \"timestamp_ms\" }),\n\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  deletedAt: integer(\"deleted_at\", { mode: \"timestamp\" }),\n});\n\nexport const userRelations = relations(user, ({ many }) => ({\n  usersToWorkspaces: many(usersToWorkspaces),\n}));\n\nexport const usersToWorkspaces = sqliteTable(\n  \"users_to_workspaces\",\n  {\n    userId: integer(\"user_id\")\n      .notNull()\n      .references(() => user.id),\n    workspaceId: integer(\"workspace_id\")\n      .notNull()\n      .references(() => workspace.id),\n    role: text(\"role\", { enum: workspaceRole }).notNull().default(\"member\"),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n  },\n  (t) => ({\n    pk: primaryKey({ columns: [t.userId, t.workspaceId] }),\n  }),\n);\n\nexport const usersToWorkspaceRelations = relations(\n  usersToWorkspaces,\n  ({ one }) => ({\n    workspace: one(workspace, {\n      fields: [usersToWorkspaces.workspaceId],\n      references: [workspace.id],\n    }),\n    user: one(user, {\n      fields: [usersToWorkspaces.userId],\n      references: [user.id],\n    }),\n  }),\n);\n\n// NEXT AUTH TABLES\n\nexport const account = sqliteTable(\n  \"account\",\n  {\n    userId: integer(\"user_id\")\n      .notNull()\n      .references(() => user.id, { onDelete: \"cascade\" }),\n    type: text(\"type\").$type<AdapterAccount[\"type\"]>().notNull(),\n    provider: text(\"provider\").notNull(),\n    providerAccountId: text(\"provider_account_id\").notNull(),\n    refresh_token: text(\"refresh_token\"),\n    access_token: text(\"access_token\"),\n    expires_at: integer(\"expires_at\"),\n    token_type: text(\"token_type\"),\n    scope: text(\"scope\"),\n    id_token: text(\"id_token\"),\n    session_state: text(\"session_state\"),\n  },\n  (account) => ({\n    compoundKey: primaryKey({\n      columns: [account.provider, account.providerAccountId],\n    }),\n  }),\n);\n\nexport const session = sqliteTable(\"session\", {\n  sessionToken: text(\"session_token\").primaryKey(),\n  userId: integer(\"user_id\")\n    .notNull()\n    .references(() => user.id, { onDelete: \"cascade\" }),\n  expires: integer(\"expires\", { mode: \"timestamp_ms\" }).notNull(),\n});\n\nexport const verificationToken = sqliteTable(\n  \"verification_token\",\n  {\n    identifier: text(\"identifier\").notNull(),\n    token: text(\"token\").notNull(),\n    expires: integer(\"expires\", { mode: \"timestamp_ms\" }).notNull(),\n  },\n  (vt) => ({\n    compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),\n  }),\n);\n"
  },
  {
    "path": "packages/db/src/schema/users/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport type { z } from \"zod\";\n\nimport { user } from \"./user\";\n\nexport const insertUserSchema = createInsertSchema(user);\n\nexport const selectUserSchema = createSelectSchema(user);\n\nexport type InsertUser = z.infer<typeof insertUserSchema>;\nexport type User = z.infer<typeof selectUserSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/viewers/index.ts",
    "content": "export * from \"./viewer\";\nexport * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/viewers/validation.ts",
    "content": "import { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport type { z } from \"zod\";\n\nimport { viewer } from \"./viewer\";\n\nexport const insertViewerSchema = createInsertSchema(viewer);\n\nexport const selectViewerSchema = createSelectSchema(viewer);\n\nexport type InsertViewer = z.infer<typeof insertViewerSchema>;\nexport type Viewer = z.infer<typeof selectViewerSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/viewers/viewer.ts",
    "content": "import { sql } from \"drizzle-orm\";\nimport {\n  integer,\n  primaryKey,\n  sqliteTable,\n  text,\n} from \"drizzle-orm/sqlite-core\";\n\nimport type { AdapterAccountType } from \"next-auth/adapters\";\n\nexport const viewer = sqliteTable(\"viewer\", {\n  id: integer(\"id\").primaryKey(),\n  name: text(\"name\"),\n  email: text(\"email\").unique(),\n  emailVerified: integer(\"emailVerified\", { mode: \"timestamp\" }),\n  image: text(\"image\"),\n  createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n  updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n    sql`(strftime('%s', 'now'))`,\n  ),\n});\n\nexport const viewerSession = sqliteTable(\"viewer_session\", {\n  sessionToken: text(\"session_token\").primaryKey(),\n  userId: integer(\"user_id\")\n    .notNull()\n    .references(() => viewer.id, { onDelete: \"cascade\" }),\n  expires: integer(\"expires\", { mode: \"timestamp_ms\" }).notNull(),\n});\n\nexport const viewerAccounts = sqliteTable(\n  \"viewer_accounts\",\n  {\n    userId: text(\"user_id\")\n      .notNull()\n      .references(() => viewer.id, { onDelete: \"cascade\" }),\n    type: text(\"type\").$type<AdapterAccountType>().notNull(),\n    provider: text(\"provider\").notNull(),\n    providerAccountId: text(\"providerAccountId\").notNull(),\n    refresh_token: text(\"refresh_token\"),\n    access_token: text(\"access_token\"),\n    expires_at: integer(\"expires_at\"),\n    token_type: text(\"token_type\"),\n    scope: text(\"scope\"),\n    id_token: text(\"id_token\"),\n    session_state: text(\"session_state\"),\n  },\n  (account) => [\n    primaryKey({\n      columns: [account.provider, account.providerAccountId],\n    }),\n  ],\n);\n"
  },
  {
    "path": "packages/db/src/schema/workspaces/constants.ts",
    "content": "export const workspacePlans = [\"free\", \"starter\", \"team\"] as const;\nexport const workspaceRole = [\"owner\", \"admin\", \"member\"] as const;\n\nexport const workspacePlanHierarchy: Record<\n  (typeof workspacePlans)[number],\n  number\n> = {\n  free: 0,\n  starter: 1,\n  team: 2,\n};\n"
  },
  {
    "path": "packages/db/src/schema/workspaces/index.ts",
    "content": "export * from \"./constants\";\nexport * from \"./workspace\";\nexport * from \"./validation\";\nexport type * from \"./validation\";\n"
  },
  {
    "path": "packages/db/src/schema/workspaces/validation.ts",
    "content": "import { createSelectSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\n\nimport { allPlans } from \"../plan/config\";\nimport { limitsSchema } from \"../plan/schema\";\nimport { workspacePlans, workspaceRole } from \"./constants\";\nimport { workspace } from \"./workspace\";\n\nexport const workspacePlanSchema = z.enum(workspacePlans);\nexport const workspaceRoleSchema = z.enum(workspaceRole);\n\n/**\n * Workspace schema with limits and plan\n */\nexport const selectWorkspaceSchema = createSelectSchema(workspace)\n  .extend({\n    limits: z.string().transform((val) => {\n      try {\n        const parsed = JSON.parse(val);\n\n        // Only validate properties that are actually present in the parsed object\n        // This avoids triggering .prefault() for missing properties\n        const validated: Record<string, unknown> = {};\n        const limitsShape = limitsSchema.shape;\n\n        for (const key in parsed) {\n          if (key in limitsShape) {\n            // Validate only the properties that exist in the parsed object\n            const propertySchema = limitsShape[key as keyof typeof limitsShape];\n            const result = propertySchema.safeParse(parsed[key]);\n            if (result.success) {\n              validated[key] = result.data;\n            } else {\n              console.warn(`Invalid value for limits.${key}:`, result.error);\n              // Skip invalid properties instead of failing entirely\n            }\n          }\n          // Unknown properties are ignored\n        }\n\n        return validated;\n      } catch (error) {\n        console.error(\"Error parsing limits:\", error);\n        return {};\n      }\n    }),\n    plan: z\n      .enum(workspacePlans)\n      .nullable()\n      .prefault(\"free\")\n      .transform((val) => val ?? \"free\"),\n    // REMINDER: workspace usage\n    usage: z\n      .object({\n        monitors: z.number().prefault(0),\n        notifications: z.number().prefault(0),\n        pages: z.number().prefault(0),\n        pageComponents: z.number().prefault(0),\n        // checks: z.number().default(0),\n      })\n      .nullish(),\n  })\n  .transform((val) => {\n    return {\n      ...val,\n      limits: limitsSchema.parse({\n        ...allPlans[val.plan].limits,\n        /**\n         * override the default plan limits\n         * allows us to set custom limits for a workspace\n         */\n        ...val.limits,\n      }),\n    };\n  });\n\nexport const insertWorkspaceSchema = createSelectSchema(workspace);\n\nexport type Workspace = z.infer<typeof selectWorkspaceSchema>;\nexport type WorkspacePlan = z.infer<typeof workspacePlanSchema>;\nexport type WorkspaceRole = z.infer<typeof workspaceRoleSchema>;\n"
  },
  {
    "path": "packages/db/src/schema/workspaces/workspace.ts",
    "content": "import { relations, sql } from \"drizzle-orm\";\nimport { integer, sqliteTable, text, unique } from \"drizzle-orm/sqlite-core\";\n\nimport { monitor } from \"../monitors\";\nimport { notification } from \"../notifications\";\nimport { page } from \"../pages\";\nimport { usersToWorkspaces } from \"../users\";\nimport { workspacePlans } from \"./constants\";\n\nexport const workspace = sqliteTable(\n  \"workspace\",\n  {\n    id: integer(\"id\").primaryKey(),\n    slug: text(\"slug\").notNull().unique(), // we love random words\n    name: text(\"name\"),\n\n    stripeId: text(\"stripe_id\", { length: 256 }).unique(),\n    subscriptionId: text(\"subscription_id\"),\n    plan: text(\"plan\", { enum: workspacePlans }),\n    endsAt: integer(\"ends_at\", { mode: \"timestamp\" }),\n    paidUntil: integer(\"paid_until\", { mode: \"timestamp\" }),\n    limits: text(\"limits\").default(\"{}\").notNull(),\n    createdAt: integer(\"created_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n    updatedAt: integer(\"updated_at\", { mode: \"timestamp\" }).default(\n      sql`(strftime('%s', 'now'))`,\n    ),\n\n    dsn: text(\"dsn\"), // should be removed soon\n  },\n  (t) => ({\n    unique: unique().on(t.id, t.dsn),\n  }),\n);\n\nexport const workspaceRelations = relations(workspace, ({ many }) => ({\n  usersToWorkspaces: many(usersToWorkspaces),\n  pages: many(page),\n  monitors: many(monitor),\n  notifications: many(notification),\n  // TODO: add checks or monitorRuns\n}));\n"
  },
  {
    "path": "packages/db/src/seed.mts",
    "content": "import { createClient } from \"@libsql/client\";\nimport { drizzle } from \"drizzle-orm/libsql\";\n\nimport { env } from \"../env.mjs\";\nimport {\n  incidentTable,\n  maintenance,\n  maintenancesToPageComponents,\n  monitor,\n  notification,\n  notificationsToMonitors,\n  page,\n  pageComponent,\n  privateLocation,\n  privateLocationToMonitors,\n  statusReport,\n  statusReportUpdate,\n  statusReportsToPageComponents,\n  user,\n  usersToWorkspaces,\n  workspace,\n} from \"./schema\";\n\nasync function main() {\n  const db = drizzle(\n    createClient({ url: env.DATABASE_URL, authToken: env.DATABASE_AUTH_TOKEN }),\n  );\n  console.log(\"Seeding database \");\n  await db\n    .insert(workspace)\n    .values([\n      {\n        id: 1,\n        slug: \"love-openstatus\",\n        stripeId: \"stripeId1\",\n        name: \"test\",\n        subscriptionId: \"subscriptionId\",\n        plan: \"team\",\n        endsAt: null,\n        paidUntil: null,\n        limits:\n          '{\"monitors\":50,\"synthetic-checks\":150000,\"periodicity\":[\"30s\",\"1m\",\"5m\",\"10m\",\"30m\",\"1h\"],\"multi-region\":true,\"max-regions\":35,\"data-retention\":\"24 months\",\"status-pages\":20,\"maintenance\":true,\"status-subscribers\":true,\"custom-domain\":true,\"password-protection\":true,\"white-label\":true,\"notifications\":true,\"sms\":true,\"pagerduty\":true,\"notification-channels\":50,\"members\":\"Unlimited\",\"audit-log\":true,\"regions\":[\"ams\",\"arn\",\"atl\",\"bog\",\"bom\",\"bos\",\"cdg\",\"den\",\"dfw\",\"ewr\",\"eze\",\"fra\",\"gdl\",\"gig\",\"gru\",\"hkg\",\"iad\",\"jnb\",\"lax\",\"lhr\",\"mad\",\"mia\",\"nrt\",\"ord\",\"otp\",\"phx\",\"qro\",\"scl\",\"sea\",\"sin\",\"sjc\",\"syd\",\"waw\",\"yul\",\"yyz\"]}',\n      },\n      {\n        id: 2,\n        slug: \"test2\",\n        stripeId: \"stripeId2\",\n        name: \"test2\",\n        subscriptionId: \"subscriptionId2\",\n        plan: \"free\",\n        endsAt: null,\n        paidUntil: null,\n      },\n      {\n        id: 3,\n        slug: \"test3\",\n        stripeId: \"stripeId3\",\n        name: \"test3\",\n        subscriptionId: \"subscriptionId3\",\n        plan: \"team\",\n        endsAt: null,\n        paidUntil: null,\n      },\n    ])\n    .onConflictDoNothing()\n    .run();\n\n  await db\n    .insert(monitor)\n    .values([\n      {\n        id: 1,\n        workspaceId: 1,\n        active: true,\n        url: \"https://www.openstatus.dev\",\n        name: \"OpenStatus\",\n        description: \"OpenStatus website\",\n        method: \"POST\",\n        periodicity: \"1m\",\n        regions: \"ams\",\n        headers: '[{\"key\":\"key\", \"value\":\"value\"}]',\n        body: '{\"hello\":\"world\"}',\n      },\n      {\n        id: 2,\n        active: false,\n        workspaceId: 1,\n        periodicity: \"10m\",\n        url: \"https://www.google.com\",\n        method: \"GET\",\n        regions: \"gru\",\n        public: true,\n      },\n      {\n        id: 3,\n        workspaceId: 1,\n        active: true,\n        url: \"https://www.openstatus.dev\",\n        name: \"OpenStatus\",\n        description: \"OpenStatus website\",\n        method: \"GET\",\n        periodicity: \"1m\",\n        regions: \"ams\",\n        headers: '[{\"key\":\"key\", \"value\":\"value\"}]',\n        body: '{\"hello\":\"world\"}',\n      },\n      {\n        id: 4,\n        active: true,\n        workspaceId: 1,\n        periodicity: \"10m\",\n        url: \"https://www.google.com\",\n        method: \"GET\",\n        regions: \"gru\",\n        public: true,\n        otelEndpoint: \"https://otel.com:4337\",\n        otelHeaders: '[{\"key\":\"Authorization\",\"value\":\"Basic\"}]',\n      },\n      {\n        id: 5,\n        active: true,\n        workspaceId: 3,\n        periodicity: \"10m\",\n        url: \"https://openstat.us\",\n        method: \"GET\",\n        regions: \"ams\",\n        public: true,\n      },\n    ])\n    .onConflictDoNothing()\n    .run();\n\n  await db\n    .insert(page)\n    .values({\n      id: 1,\n      workspaceId: 1,\n      title: \"Acme Inc.\",\n      description: \"Get informed about our services.\",\n      icon: \"https://www.openstatus.dev/favicon.ico\",\n      slug: \"status\",\n      customDomain: \"\",\n      published: true,\n    })\n    .onConflictDoNothing()\n    .run();\n\n  await db\n    .insert(user)\n    .values({\n      id: 1,\n      tenantId: \"1\",\n      firstName: \"Speed\",\n      lastName: \"Matters\",\n      email: \"ping@openstatus.dev\",\n      photoUrl: \"\",\n    })\n    .onConflictDoNothing()\n    .run();\n  await db\n    .insert(usersToWorkspaces)\n    .values({ workspaceId: 1, userId: 1 })\n    .onConflictDoNothing()\n    .run();\n\n  // Page Components - representing monitors on the status page\n  await db\n    .insert(pageComponent)\n    .values([\n      {\n        id: 1,\n        workspaceId: 1,\n        pageId: 1,\n        type: \"monitor\",\n        monitorId: 1,\n        name: \"OpenStatus Monitor\",\n        description: \"Main website monitoring\",\n        order: 0,\n      },\n      {\n        id: 2,\n        workspaceId: 1,\n        pageId: 1,\n        type: \"monitor\",\n        monitorId: 2,\n        name: \"Google Monitor\",\n        description: \"Google.com monitoring\",\n        order: 1,\n      },\n    ])\n    .onConflictDoNothing()\n    .run();\n\n  await db\n    .insert(notification)\n    .values({\n      id: 1,\n      provider: \"email\",\n      name: \"sample test notification\",\n      data: '{\"email\":\"ping@openstatus.dev\"}',\n      workspaceId: 1,\n    })\n    .onConflictDoNothing()\n    .run();\n\n  await db\n    .insert(notificationsToMonitors)\n    .values({ monitorId: 1, notificationId: 1 })\n    .onConflictDoNothing()\n    .run();\n\n  // Status Report 1 - Resolved incident from 7 days ago\n  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n  const sevenDaysAgoPlus30Min = new Date(\n    sevenDaysAgo.getTime() + 30 * 60 * 1000,\n  );\n  const sevenDaysAgoPlus90Min = new Date(\n    sevenDaysAgo.getTime() + 90 * 60 * 1000,\n  );\n  const sevenDaysAgoPlus120Min = new Date(\n    sevenDaysAgo.getTime() + 120 * 60 * 1000,\n  );\n\n  await db\n    .insert(statusReport)\n    .values({\n      id: 1,\n      workspaceId: 1,\n      pageId: 1,\n      title: \"API Gateway Degraded Performance\",\n      status: \"resolved\",\n      updatedAt: sevenDaysAgoPlus120Min,\n    })\n    .onConflictDoNothing()\n    .run();\n\n  await db\n    .insert(statusReportUpdate)\n    .values([\n      {\n        id: 1,\n        statusReportId: 1,\n        status: \"investigating\",\n        message:\n          \"We are investigating reports of slow API response times affecting some customers.\",\n        date: sevenDaysAgo,\n      },\n      {\n        id: 2,\n        statusReportId: 1,\n        status: \"identified\",\n        message:\n          \"We have identified the issue as a database connection pool exhaustion and are working on a fix.\",\n        date: sevenDaysAgoPlus30Min,\n      },\n      {\n        id: 3,\n        statusReportId: 1,\n        status: \"monitoring\",\n        message:\n          \"A fix has been deployed and we are monitoring the system. Response times are returning to normal.\",\n        date: sevenDaysAgoPlus90Min,\n      },\n      {\n        id: 4,\n        statusReportId: 1,\n        status: \"resolved\",\n        message:\n          \"All systems are operating normally. The issue has been fully resolved.\",\n        date: sevenDaysAgoPlus120Min,\n      },\n    ])\n    .onConflictDoNothing()\n    .run();\n\n  // Status Report 2 - Ongoing incident from 2 hours ago\n  const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);\n  const oneHourAgo = new Date(Date.now() - 1 * 60 * 60 * 1000);\n  const twentyMinutesAgo = new Date(Date.now() - 20 * 60 * 1000);\n\n  await db\n    .insert(statusReport)\n    .values({\n      id: 2,\n      workspaceId: 1,\n      pageId: 1,\n      title: \"Increased Error Rates on Monitoring Checks\",\n      status: \"resolved\",\n      updatedAt: oneHourAgo,\n    })\n    .onConflictDoNothing()\n    .run();\n\n  await db\n    .insert(statusReportUpdate)\n    .values([\n      {\n        id: 5,\n        statusReportId: 2,\n        status: \"investigating\",\n        message:\n          \"We are seeing elevated error rates on some monitoring checks and are investigating the root cause.\",\n        date: twoHoursAgo,\n      },\n      {\n        id: 6,\n        statusReportId: 2,\n        status: \"monitoring\",\n        message:\n          \"We have applied a fix and are monitoring the situation. Error rates are decreasing.\",\n        date: oneHourAgo,\n      },\n      {\n        id: 7,\n        statusReportId: 2,\n        status: \"resolved\",\n        message:\n          \"Everything is under control, we continue to monitor the situation.\",\n        date: twentyMinutesAgo,\n      },\n    ])\n    .onConflictDoNothing()\n    .run();\n\n  // Maintenance windows spread across 30 days\n  const twentyDaysAgo = new Date(Date.now() - 20 * 24 * 60 * 60 * 1000);\n  const twentyDaysAgoPlus2Hours = new Date(\n    twentyDaysAgo.getTime() + 2 * 60 * 60 * 1000,\n  );\n\n  const fiveDaysFromNow = new Date(Date.now() + 5 * 24 * 60 * 60 * 1000);\n  const fiveDaysFromNowPlus4Hours = new Date(\n    fiveDaysFromNow.getTime() + 4 * 60 * 60 * 1000,\n  );\n\n  await db\n    .insert(maintenance)\n    .values([\n      {\n        id: 1,\n        workspaceId: 1,\n        title: \"Database Migration and Optimization\",\n        message:\n          \"We will be performing database maintenance to improve performance. Some queries may be slower during this window.\",\n        from: twentyDaysAgo,\n        to: twentyDaysAgoPlus2Hours,\n        pageId: 1,\n      },\n      {\n        id: 2,\n        workspaceId: 1,\n        title: \"Infrastructure Upgrade\",\n        message:\n          \"We will be upgrading our monitoring infrastructure to the latest version. Expect brief interruptions in data collection.\",\n        from: fiveDaysFromNow,\n        to: fiveDaysFromNowPlus4Hours,\n        pageId: 1,\n      },\n    ])\n    .onConflictDoNothing()\n    .run();\n\n  // Link status reports to page components\n  await db\n    .insert(statusReportsToPageComponents)\n    .values([\n      {\n        statusReportId: 1,\n        pageComponentId: 1,\n      },\n      {\n        statusReportId: 2,\n        pageComponentId: 1,\n      },\n    ])\n    .onConflictDoNothing()\n    .run();\n\n  // Link maintenances to page components\n  await db\n    .insert(maintenancesToPageComponents)\n    .values([\n      {\n        maintenanceId: 1,\n        pageComponentId: 1,\n      },\n    ])\n    .onConflictDoNothing()\n    .run();\n\n  // Incidents - realistic past incidents that were resolved\n  const fifteenDaysAgo = new Date(Date.now() - 15 * 24 * 60 * 60 * 1000);\n  const fifteenDaysAgoPlus2Hours = new Date(\n    fifteenDaysAgo.getTime() + 2 * 60 * 60 * 1000,\n  );\n\n  const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);\n  const threeDaysAgoPlus20Min = new Date(\n    threeDaysAgo.getTime() + 20 * 60 * 1000,\n  );\n\n  await db\n    .insert(incidentTable)\n    .values([\n      {\n        id: 1,\n        workspaceId: 1,\n        monitorId: 1,\n        createdAt: fifteenDaysAgo,\n        startedAt: fifteenDaysAgo,\n        acknowledgedAt: new Date(fifteenDaysAgo.getTime() + 5 * 60 * 1000),\n        resolvedAt: fifteenDaysAgoPlus2Hours,\n      },\n      {\n        id: 2,\n        workspaceId: 1,\n        monitorId: 1,\n        createdAt: threeDaysAgo,\n        startedAt: threeDaysAgo,\n        acknowledgedAt: new Date(threeDaysAgo.getTime() + 2 * 60 * 1000),\n        resolvedAt: threeDaysAgoPlus20Min,\n      },\n    ])\n    .onConflictDoNothing()\n    .run();\n\n  await db\n    .insert(privateLocation)\n    .values({\n      id: 1,\n      name: \"My Home\",\n      token: \"my-secret-key\",\n      workspaceId: 3,\n      createdAt: new Date(),\n    })\n    .onConflictDoNothing()\n    .run();\n  await db\n    .insert(privateLocationToMonitors)\n    .values({\n      privateLocationId: 1,\n      monitorId: 5,\n      createdAt: new Date(),\n    })\n    .onConflictDoNothing()\n    .run();\n  process.exit(0);\n}\n\nmain().catch((e) => {\n  console.error(\"Seed failed\");\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/db/src/sync-db.ts",
    "content": "import { drizzle } from \"drizzle-orm/libsql\";\n\nimport { createClient } from \"@libsql/client\";\nimport { env } from \"../env.mjs\";\nimport * as schema from \"./schema\";\n\nconst client = createClient({\n  url: \"file:///app/data/replica.db\",\n  syncUrl: env.DATABASE_URL,\n  authToken: env.DATABASE_AUTH_TOKEN,\n  syncInterval: 60,\n});\n\nexport const syncDB = drizzle({\n  client: client,\n  schema,\n});\n"
  },
  {
    "path": "packages/db/src/utils/api-key.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\";\nimport {\n  generateApiKey,\n  hashApiKey,\n  shouldUpdateLastUsed,\n  verifyApiKeyHash,\n} from \"./api-key\";\n\ndescribe(\"API Key Utilities\", () => {\n  describe(\"generateApiKey\", () => {\n    it(\"should generate a token with correct format\", async () => {\n      const { token } = await generateApiKey();\n\n      // Token should start with \"os_\" and be 35 chars total (os_ + 32 hex)\n      expect(token).toMatch(/^os_[a-f0-9]{32}$/);\n      expect(token.length).toBe(35);\n    });\n\n    it(\"should generate a prefix with correct format\", async () => {\n      const { prefix } = await generateApiKey();\n\n      // Prefix should be \"os_\" + 8 chars = 11 chars total\n      expect(prefix).toMatch(/^os_[a-f0-9]{8}$/);\n      expect(prefix.length).toBe(11);\n    });\n\n    it(\"should generate unique tokens\", async () => {\n      const key1 = await generateApiKey();\n      const key2 = await generateApiKey();\n\n      expect(key1.token).not.toBe(key2.token);\n      expect(key1.hash).not.toBe(key2.hash);\n    });\n\n    it(\"should generate prefix from token start\", async () => {\n      const { token, prefix } = await generateApiKey();\n\n      expect(token.slice(0, 11)).toBe(prefix);\n    });\n  });\n\n  describe(\"hashApiKey\", () => {\n    it(\"should generate hash that can verify the token\", async () => {\n      const { token, hash } = await generateApiKey();\n\n      expect(await verifyApiKeyHash(token, hash)).toBe(true);\n    });\n  });\n\n  describe(\"hashApiKey\", () => {\n    it(\"should generate different hashes for different tokens\", async () => {\n      const hash1 = await hashApiKey(\"os_token1\");\n      const hash2 = await hashApiKey(\"os_token2\");\n\n      expect(hash1).not.toBe(hash2);\n    });\n\n    it(\"should generate a valid bcrypt hash\", async () => {\n      const hash = await hashApiKey(\"os_test_token\");\n\n      // Bcrypt hashes start with $2a$, $2b$, or $2y$\n      expect(hash).toMatch(/^\\$2[aby]\\$/);\n    });\n\n    it(\"should generate hash that can verify the original token\", async () => {\n      const token = \"os_test_token_12345\";\n      const hash = await hashApiKey(token);\n\n      expect(await verifyApiKeyHash(token, hash)).toBe(true);\n    });\n\n    it(\"should generate different hashes for same token on multiple calls\", async () => {\n      const token = \"os_same_token\";\n      const hash1 = await hashApiKey(token);\n      const hash2 = await hashApiKey(token);\n\n      // bcrypt uses salt, so same input produces different hashes\n      expect(hash1).not.toBe(hash2);\n      // But both should verify the token\n      expect(await verifyApiKeyHash(token, hash1)).toBe(true);\n      expect(await verifyApiKeyHash(token, hash2)).toBe(true);\n    });\n  });\n\n  describe(\"verifyApiKeyHash\", () => {\n    it(\"should return true for valid bcrypt hash with correct token\", async () => {\n      const token = \"os_valid_token_12345\";\n      const hash = await hashApiKey(token);\n\n      expect(await verifyApiKeyHash(token, hash)).toBe(true);\n    });\n\n    it(\"should return false for valid bcrypt hash with wrong token\", async () => {\n      const correctToken = \"os_correct_token\";\n      const wrongToken = \"os_wrong_token\";\n      const hash = await hashApiKey(correctToken);\n\n      expect(await verifyApiKeyHash(wrongToken, hash)).toBe(false);\n    });\n\n    it(\"should return false for non-bcrypt hash format\", async () => {\n      const token = \"os_test_token\";\n      const invalidHash = \"not_a_bcrypt_hash\";\n\n      expect(await verifyApiKeyHash(token, invalidHash)).toBe(false);\n    });\n\n    it(\"should return false for SHA-256 hash format\", async () => {\n      const token = \"os_test_token\";\n      // SHA-256 hashes are 64 hex characters\n      const sha256Hash =\n        \"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8\";\n\n      expect(await verifyApiKeyHash(token, sha256Hash)).toBe(false);\n    });\n\n    it(\"should return false for empty hash\", async () => {\n      const token = \"os_test_token\";\n\n      expect(await verifyApiKeyHash(token, \"\")).toBe(false);\n    });\n\n    it(\"should return false for empty token with valid hash\", async () => {\n      const hash = await hashApiKey(\"os_some_token\");\n\n      expect(await verifyApiKeyHash(\"\", hash)).toBe(false);\n    });\n\n    it(\"should handle bcrypt hashes with different cost factors\", async () => {\n      const token = \"os_test_token\";\n      const hash = await hashApiKey(token);\n\n      // Should work regardless of the $2a$, $2b$, or $2y$ variant\n      expect(await verifyApiKeyHash(token, hash)).toBe(true);\n    });\n  });\n\n  describe(\"shouldUpdateLastUsed\", () => {\n    it(\"should return true when lastUsedAt is null\", () => {\n      expect(shouldUpdateLastUsed(null)).toBe(true);\n    });\n\n    it(\"should return true when enough time has passed\", () => {\n      const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000);\n      expect(shouldUpdateLastUsed(sixMinutesAgo, 5)).toBe(true);\n    });\n\n    it(\"should return false when not enough time has passed\", () => {\n      const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);\n      expect(shouldUpdateLastUsed(twoMinutesAgo, 5)).toBe(false);\n    });\n\n    it(\"should respect custom debounce period\", () => {\n      const threeMinutesAgo = new Date(Date.now() - 3 * 60 * 1000);\n      expect(shouldUpdateLastUsed(threeMinutesAgo, 2)).toBe(true);\n      expect(shouldUpdateLastUsed(threeMinutesAgo, 4)).toBe(false);\n    });\n\n    it(\"should return false when just updated\", () => {\n      const justNow = new Date();\n      expect(shouldUpdateLastUsed(justNow, 5)).toBe(false);\n    });\n\n    it(\"should handle boundary case at exact debounce time\", () => {\n      const exactlyFiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);\n      // At exactly the debounce time, it should not update (needs to be > not >=)\n      expect(shouldUpdateLastUsed(exactlyFiveMinutesAgo, 5)).toBe(false);\n    });\n\n    it(\"should handle boundary case just after debounce time\", () => {\n      const justOverFiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000 - 1);\n      expect(shouldUpdateLastUsed(justOverFiveMinutesAgo, 5)).toBe(true);\n    });\n\n    it(\"should use default debounce of 5 minutes when not specified\", () => {\n      const fourMinutesAgo = new Date(Date.now() - 4 * 60 * 1000);\n      const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000);\n\n      expect(shouldUpdateLastUsed(fourMinutesAgo)).toBe(false);\n      expect(shouldUpdateLastUsed(sixMinutesAgo)).toBe(true);\n    });\n\n    it(\"should handle zero debounce period\", () => {\n      const oneSecondAgo = new Date(Date.now() - 1000);\n      expect(shouldUpdateLastUsed(oneSecondAgo, 0)).toBe(true);\n    });\n\n    it(\"should handle very long debounce periods\", () => {\n      const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);\n      expect(shouldUpdateLastUsed(oneHourAgo, 120)).toBe(false); // 2 hours\n      expect(shouldUpdateLastUsed(oneHourAgo, 30)).toBe(true); // 30 minutes\n    });\n  });\n});\n"
  },
  {
    "path": "packages/db/src/utils/api-key.ts",
    "content": "// biome-ignore lint/style/useNodejsImportProtocol: <explanation>\nimport crypto from \"crypto\";\nimport bcrypt from \"bcryptjs\";\n\n/**\n * Generates a new API key with token, prefix, and hash\n * @returns Object containing the full token, prefix for lookup, and SHA-256 hash\n */\nexport async function generateApiKey(): Promise<{\n  token: string;\n  prefix: string;\n  hash: string;\n}> {\n  const randomBytes = crypto.randomBytes(16).toString(\"hex\"); // 32 hex chars\n  const token = `os_${randomBytes}`;\n  const prefix = token.slice(0, 11); // \"os_\" (3 chars) + 8 hex chars = 11 total\n  const hash = await bcrypt.hash(token, 10);\n  return { token, prefix, hash };\n}\n\n/**\n * Hashes an API key token using bcrypt\n * @param token - The API key token to hash\n * @returns The bcrypt hash of the token\n */\nexport async function hashApiKey(token: string): Promise<string> {\n  return bcrypt.hash(token, 10);\n}\n\n/**\n * Verifies an API key token against a stored hash\n * Supports both bcrypt hashes (new) and SHA-256 hashes (legacy) for migration\n * @param token - The API key token to verify\n * @param storedHash - The stored hash to verify against\n * @returns True if the token matches the hash\n */\nexport async function verifyApiKeyHash(\n  token: string,\n  storedHash: string,\n): Promise<boolean> {\n  // Check if it's a bcrypt hash (starts with $2a$, $2b$, or $2y$)\n  if (storedHash.startsWith(\"$2\")) {\n    return bcrypt.compare(token, storedHash);\n  }\n\n  // Unknown hash format\n  return false;\n}\n\n/**\n * Determines if lastUsedAt should be updated based on debounce period\n * @param lastUsedAt - The last time the key was used (or null)\n * @param debounceMinutes - Minutes to wait before updating again (default: 5)\n * @returns True if lastUsedAt should be updated\n */\nexport function shouldUpdateLastUsed(\n  lastUsedAt: Date | null,\n  debounceMinutes = 5,\n): boolean {\n  if (!lastUsedAt) return true;\n  const diffMs = Date.now() - lastUsedAt.getTime();\n  return diffMs > debounceMinutes * 60 * 1000;\n}\n"
  },
  {
    "path": "packages/db/src/utils/index.ts",
    "content": "export * from \"./api-key\";\n"
  },
  {
    "path": "packages/db/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"exclude\": [\"dist\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"target\": \"es2021\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"noUncheckedIndexedAccess\": true\n  },\n  \"include\": [\"src\", \"*.ts\", \"env.mjs\", \"**/*.ts\", \"../utils/try.ts\"]\n}\n"
  },
  {
    "path": "packages/emails/emails/_components/footer.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { Link, Section, Text } from \"@react-email/components\";\nimport { styles } from \"./styles\";\n\nexport function Footer() {\n  return (\n    <Section style={{ textAlign: \"center\" }}>\n      <Text>\n        <Link style={styles.link} href=\"https://openstatus.dev\">\n          Home Page\n        </Link>{\" \"}\n        ・{\" \"}\n        <Link style={styles.link} href=\"mailto:ping@openstatus.dev\">\n          Contact Support\n        </Link>\n      </Text>\n\n      <Text>OpenStatus ・ 122 Rue Amelot ・ 75011 Paris, France</Text>\n    </Section>\n  );\n}\n"
  },
  {
    "path": "packages/emails/emails/_components/layout.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { Container, Img, Link, Section } from \"@react-email/components\";\nimport type * as React from \"react\";\nimport { Footer } from \"./footer\";\nimport { styles } from \"./styles\";\n\ninterface LayoutProps {\n  children?: React.ReactNode;\n  img?: {\n    src: string;\n    alt: string;\n    href: string;\n  };\n}\n\nconst defaultImg = {\n  src: \"https://openstatus.dev/assets/logos/OpenStatus.png\",\n  alt: \"OpenStatus\",\n  href: \"https://openstatus.dev\",\n};\n\nexport function Layout({ children, img = defaultImg }: LayoutProps) {\n  return (\n    <Container style={styles.container}>\n      <Link href={img.href}>\n        <Img src={img.src} width=\"36\" height=\"36\" alt={img.alt} />\n      </Link>\n      <Section style={styles.section}>{children}</Section>\n      <Footer />\n    </Container>\n  );\n}\n"
  },
  {
    "path": "packages/emails/emails/_components/styles.ts",
    "content": "export const colors = {\n  success: \"#51b363\",\n  danger: \"#ec6041\",\n  warning: \"#ffd60a\",\n  info: \"#3d9eff\",\n  border: \"#dedede\",\n};\n\nexport const styles = {\n  main: {\n    backgroundColor: \"#ffffff\",\n    color: \"#24292e\",\n    fontFamily:\n      '-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"',\n  },\n  container: {\n    maxWidth: \"480px\",\n    margin: \"0 auto\",\n    padding: \"20px 0 48px\",\n  },\n  section: {\n    padding: \"24px\",\n    margin: \"24px 0\",\n    border: \"solid 1px #dedede\",\n    borderRadius: \"5px\",\n  },\n  button: {\n    backgroundColor: \"#24292e\",\n    color: \"#ffffff\",\n    padding: \"8px 16px\",\n    borderRadius: \"6px\",\n  },\n  link: {\n    textDecoration: \"underline\",\n    color: colors.info,\n  },\n  bold: {\n    fontWeight: \"bold\",\n  },\n  row: {\n    borderTop: \"1px solid #dedede\",\n  },\n} satisfies Record<string, React.CSSProperties>;\n"
  },
  {
    "path": "packages/emails/emails/feedback.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { Body, Head, Html, Preview } from \"@react-email/components\";\n\nconst FeedbackEmail = () => {\n  return (\n    <Html>\n      <Head>\n        <title>One quick question</title>\n      </Head>\n      <Preview>What's the one thing you'd change about OpenStatus?</Preview>\n      <Body>\n        Hey\n        <br />\n        <br />\n        You've been on OpenStatus for about two weeks now. One quick question:\n        <br />\n        <br />\n        What's the one thing you wish OpenStatus did differently?\n        <br />\n        <br />\n        No survey, no form — just hit reply. I read every response and it\n        genuinely shapes what we build next.\n        <br />\n        <br />\n        Thibault Le Ouay Ducasse, co-founder of OpenStatus\n        <br />\n      </Body>\n    </Html>\n  );\n};\n\nexport default FeedbackEmail;\n"
  },
  {
    "path": "packages/emails/emails/followup.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { Body, Head, Html, Preview } from \"@react-email/components\";\n\nconst FollowUpEmail = () => {\n  return (\n    <Html>\n      <Head>\n        <title>Manage incidents from Slack</title>\n      </Head>\n      <Preview>Update your status page without leaving Slack</Preview>\n      <Body>\n        Hey\n        <br />\n        <br />\n        Quick tip: connect the OpenStatus Slack app and manage incident updates\n        for your status page directly from Slack — no need to switch tabs during\n        an outage.\n        <br />\n        <br />👉{\" \"}\n        <a href=\"https://app.openstatus.dev/agents?ref=email-followup\">\n          Install the Slack app\n        </a>\n        <br />\n        <br />\n        When something goes wrong, just mention @openstatus to create or update\n        an incident.\n        <br />\n        <br />\n        Hit reply if you have questions — happy to help.\n        <br />\n        <br />\n        Thibault Le Ouay Ducasse, co-founder of OpenStatus\n        <br />\n      </Body>\n    </Html>\n  );\n};\n\nexport default FollowUpEmail;\n"
  },
  {
    "path": "packages/emails/emails/monitor-alert.tsx",
    "content": "/** @jsxImportSource react */\n\nimport {\n  Body,\n  CodeInline,\n  Column,\n  Head,\n  Heading,\n  Html,\n  Link,\n  Preview,\n  Row,\n  Text,\n} from \"@react-email/components\";\nimport { z } from \"zod\";\nimport { Layout } from \"./_components/layout\";\nimport { colors, styles } from \"./_components/styles\";\n\nconst MonitorAlertSchema = z.object({\n  type: z.enum([\"degraded\", \"alert\", \"recovery\"]),\n  name: z.string().optional(),\n  url: z.string().optional(),\n  method: z.string().optional(),\n  status: z.string().optional(),\n  latency: z.string().optional(),\n  region: z.string().optional(),\n  timestamp: z.string().optional(),\n  message: z.string().optional(),\n});\n\nexport type MonitorAlertProps = z.infer<typeof MonitorAlertSchema>;\n\nfunction getIcon(type: MonitorAlertProps[\"type\"]): {\n  src: string;\n  color: string;\n} {\n  switch (type) {\n    case \"recovery\":\n      return {\n        src: \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNoZWNrIj48cGF0aCBkPSJNMjAgNiA5IDE3bC01LTUiLz48L3N2Zz4=\",\n        color: colors.success,\n      };\n    case \"alert\":\n      return {\n        src: \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXgiPjxwYXRoIGQ9Ik0xOCA2IDYgMTgiLz48cGF0aCBkPSJtNiA2IDEyIDEyIi8+PC9zdmc+\",\n        color: colors.danger,\n      };\n    case \"degraded\":\n      return {\n        src: \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXRyaWFuZ2xlLWFsZXJ0Ij48cGF0aCBkPSJtMjEuNzMgMTgtOC0xNGEyIDIgMCAwIDAtMy40OCAwbC04IDE0QTIgMiAwIDAgMCA0IDIxaDE2YTIgMiAwIDAgMCAxLjczLTMiLz48cGF0aCBkPSJNMTIgOXY0Ii8+PHBhdGggZD0iTTEyIDE3aC4wMSIvPjwvc3ZnPg==\",\n        color: colors.warning,\n      };\n  }\n}\n\nconst MonitorAlertEmail = (props: MonitorAlertProps) => (\n  <Html>\n    <Head />\n    <Preview>Your monitor's status is: {props.type}</Preview>\n    <Body style={styles.main}>\n      <Layout>\n        <Row>\n          <Column>\n            <Heading as=\"h4\">{props.name}</Heading>\n          </Column>\n          <Column style={{ textAlign: \"right\" }}>\n            <Text\n              style={{\n                color: getIcon(props.type).color,\n                textTransform: \"uppercase\",\n              }}\n            >\n              {props.type}\n            </Text>\n          </Column>\n        </Row>\n        <Row style={styles.row}>\n          <Column>\n            <Text style={styles.bold}>Request</Text>\n          </Column>\n          <Column\n            style={{\n              textAlign: \"right\",\n              flexWrap: \"wrap\",\n              wordWrap: \"break-word\",\n              maxWidth: \"300px\",\n            }}\n          >\n            <Text>\n              <CodeInline>{props.method}</CodeInline> {props.url}\n            </Text>\n          </Column>\n        </Row>\n        {/* REMINDER: no status code for TCP monitors */}\n        {props.status ? (\n          <Row style={styles.row}>\n            <Column>\n              <Text style={styles.bold}>Status</Text>\n            </Column>\n            <Column style={{ textAlign: \"right\" }}>\n              <Text>{props.status}</Text>\n            </Column>\n          </Row>\n        ) : null}\n        <Row style={styles.row}>\n          <Column>\n            <Text style={styles.bold}>Region</Text>\n          </Column>\n          <Column style={{ textAlign: \"right\" }}>\n            <Text>{props.region}</Text>\n          </Column>\n        </Row>\n        {props.latency ? (\n          <Row style={styles.row}>\n            <Column>\n              <Text style={styles.bold}>Latency</Text>\n            </Column>\n            <Column style={{ textAlign: \"right\" }}>\n              <Text>{props.latency}</Text>\n            </Column>\n          </Row>\n        ) : null}\n        <Row style={styles.row}>\n          <Column>\n            <Text style={styles.bold}>Timestamp</Text>\n          </Column>\n          <Column style={{ textAlign: \"right\" }}>\n            <Text>{props.timestamp}</Text>\n          </Column>\n        </Row>\n        {props.message ? (\n          <Row style={styles.row}>\n            <Column>\n              <Text>\n                {props.message?.slice(0, 200)}\n                {props.message?.length > 200 ? \"...\" : \"\"}\n              </Text>\n            </Column>\n          </Row>\n        ) : null}\n        <Row style={styles.row}>\n          <Column>\n            <Text style={{ textAlign: \"center\" }}>\n              <Link style={styles.link} href=\"https://openstatus.dev/app\">\n                View details\n              </Link>\n            </Text>\n          </Column>\n        </Row>\n      </Layout>\n    </Body>\n  </Html>\n);\n\nMonitorAlertEmail.PreviewProps = {\n  type: \"alert\",\n  name: \"Ping Pong\",\n  url: \"https://openstatus.dev/ping\",\n  method: \"GET\",\n  status: \"200\",\n  latency: \"300ms\",\n  region: \"Amsterdam, Netherlands\",\n  timestamp: \"2021-10-13T17:29:00Z\",\n  message:\n    \"This is a very long test message that will be truncated. Just testing it for the preview. Veryy veryy long message.\",\n} satisfies MonitorAlertProps;\n\nexport default MonitorAlertEmail;\n"
  },
  {
    "path": "packages/emails/emails/monitor-deactivation.tsx",
    "content": "/** @jsxImportSource react */\n\nimport {\n  Body,\n  Button,\n  Head,\n  Html,\n  Preview,\n  Text,\n} from \"@react-email/components\";\nimport { z } from \"zod\";\nimport { Layout } from \"./_components/layout\";\nimport { styles } from \"./_components/styles\";\n\nexport const MonitorDeactivationSchema = z.object({\n  // lastLogin: z.coerce.date(),\n  deactivateAt: z.coerce.date(),\n});\n\nexport type MonitorDeactivationProps = z.infer<\n  typeof MonitorDeactivationSchema\n>;\n\nconst MonitorDeactivationEmail = ({\n  // lastLogin,\n  deactivateAt,\n}: MonitorDeactivationProps) => {\n  return (\n    <Html>\n      <Head />\n      <Preview>\n        Login to your OpenStatus account to keep your monitors active.\n      </Preview>\n      <Body style={styles.main}>\n        <Layout>\n          <Text>Hello 👋</Text>\n          {/* <Heading as=\"h3\">Deactivation of the your monitor(s)</Heading> */}\n          <Text>\n            To save on cloud resources and avoid having stale monitors. We are\n            deactivating monitors for free account if you have not logged in for\n            the last 2 months.\n          </Text>\n          {/* <Text>Your last login was {lastLogin.toDateString()}.</Text> */}\n          <Text>\n            Your monitor(s) will be deactivated on {deactivateAt.toDateString()}\n            .\n          </Text>\n          <Text>\n            If you would like to keep your monitor(s) active, please login to\n            your account or upgrade to a paid plan.\n          </Text>\n          <Text style={{ textAlign: \"center\" }}>\n            <Button style={styles.button} href=\"https://www.openstatus.dev/app\">\n              Login\n            </Button>\n          </Text>\n          <Text>If you have any questions, please reply to this email.</Text>\n          <Text>Thibault </Text>\n          <Text>\n            Check out our latest update{\" \"}\n            <a href=\"https://www.openstatus.dev/changelog?ref=paused-email\">\n              here\n            </a>\n          </Text>\n        </Layout>\n      </Body>\n    </Html>\n  );\n};\n\nMonitorDeactivationEmail.PreviewProps = {\n  // lastLogin: new Date(new Date().setDate(new Date().getDate() - 100)),\n  deactivateAt: new Date(new Date().setDate(new Date().getDate() + 7)),\n} satisfies MonitorDeactivationProps;\n\nexport default MonitorDeactivationEmail;\n"
  },
  {
    "path": "packages/emails/emails/monitor-paused.tsx",
    "content": "/** @jsxImportSource react */\n\nimport {\n  Body,\n  Button,\n  Head,\n  Html,\n  Preview,\n  Text,\n} from \"@react-email/components\";\nimport { Layout } from \"./_components/layout\";\nimport { styles } from \"./_components/styles\";\n\nconst MonitorPausedEmail = () => {\n  return (\n    <Html>\n      <Head />\n      <Preview>Your monitors have been paused</Preview>\n      <Body style={styles.main}>\n        <Layout>\n          <Text>Hello 👋</Text>\n          {/* <Heading as=\"h3\">Deactivation of the your monitor(s)</Heading> */}\n          <Text>\n            To save on cloud resources, your monitor(s) has been paused due to\n            inactivity.\n          </Text>\n          <Text>\n            If you would like to unpause your monitor(s), please login to your\n            account or upgrade to a paid plan.\n          </Text>\n          <Text style={{ textAlign: \"center\" }}>\n            <Button style={styles.button} href=\"https://www.openstatus.dev/app\">\n              Login\n            </Button>\n          </Text>\n\n          <Text>If you have any questions, please reply to this email.</Text>\n          <Text>Thibault</Text>\n          <Text>\n            Check out our latest update{\" \"}\n            <a href=\"https://www.openstatus.dev/changelog?ref=paused-email\">\n              here\n            </a>\n          </Text>\n        </Layout>\n      </Body>\n    </Html>\n  );\n};\n\nexport default MonitorPausedEmail;\n"
  },
  {
    "path": "packages/emails/emails/page-subscription.tsx",
    "content": "/** @jsxImportSource react */\n\nimport {\n  Body,\n  Head,\n  Heading,\n  Html,\n  Link,\n  Preview,\n  Text,\n} from \"@react-email/components\";\nimport { z } from \"zod\";\nimport { Layout } from \"./_components/layout\";\nimport { styles } from \"./_components/styles\";\n\nexport const PageSubscriptionSchema = z.object({\n  page: z.string(),\n  link: z.string(),\n  img: z\n    .object({\n      src: z.string(),\n      alt: z.string(),\n      href: z.string(),\n    })\n    .optional(),\n});\n\nexport type PageSubscriptionProps = z.infer<typeof PageSubscriptionSchema>;\n\nconst PageSubscriptionEmail = ({ page, link, img }: PageSubscriptionProps) => {\n  return (\n    <Html>\n      <Head />\n      <Preview>Confirm your subscription to \"{page}\" Status Page</Preview>\n      <Body style={styles.main}>\n        <Layout img={img}>\n          <Heading as=\"h3\">\n            Confirm your subscription to \"{page}\" Status Page\n          </Heading>\n          <Text>\n            You are receiving this email because you subscribed to receive\n            updates from \"{page}\" Status Page.\n          </Text>\n          <Text>\n            To confirm your subscription, please click the link below. The link\n            is valid for 7 days. If you believe this is a mistake, please ignore\n            this email.\n          </Text>\n          <Text>\n            <Link style={styles.link} href={link}>\n              Confirm subscription\n            </Link>\n          </Text>\n        </Layout>\n      </Body>\n    </Html>\n  );\n};\n\nPageSubscriptionEmail.PreviewProps = {\n  link: \"https://slug.openstatus.dev/verify/token\",\n  page: \"OpenStatus\",\n} satisfies PageSubscriptionProps;\n\nexport default PageSubscriptionEmail;\n"
  },
  {
    "path": "packages/emails/emails/slack-feedback.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { Body, Head, Html, Preview } from \"@react-email/components\";\n\nconst SlackFeedbackEmail = () => {\n  return (\n    <Html>\n      <Head>\n        <title>How's the Slack app working for you?</title>\n      </Head>\n      <Preview>We'd love your feedback on the OpenStatus Slack app</Preview>\n      <Body>\n        Hey\n        <br />\n        <br />I saw you installed the OpenStatus Slack app — thanks for trying\n        it out!\n        <br />\n        <br />\n        Quick question: how's the incident management from Slack going so far?\n        <br />\n        <br />\n        Anything missing or confusing? I'd love to hear what's working and what\n        we could improve.\n        <br />\n        <br />\n        Just hit reply — I read every response.\n        <br />\n        <br />\n        Thibault Le Ouay Ducasse, co-founder of OpenStatus\n        <br />\n      </Body>\n    </Html>\n  );\n};\n\nexport default SlackFeedbackEmail;\n"
  },
  {
    "path": "packages/emails/emails/status-page-magic-link.tsx",
    "content": "/** @jsxImportSource react */\n\nimport {\n  Body,\n  Head,\n  Heading,\n  Html,\n  Link,\n  Preview,\n  Text,\n} from \"@react-email/components\";\nimport { Layout } from \"./_components/layout\";\nimport { styles } from \"./_components/styles\";\n\nexport interface StatusPageMagicLinkProps {\n  page: string;\n  link: string;\n}\n\nconst StatusPageMagicLinkEmail = ({ page, link }: StatusPageMagicLinkProps) => {\n  return (\n    <Html>\n      <Head>\n        <title>Authenticate to \"{page}\" Status Page</title>\n      </Head>\n      <Preview>Authenticate to \"{page}\" Status Page</Preview>\n      <Body>\n        <Layout>\n          <Heading as=\"h3\">Access to \"{page}\" Status Page</Heading>\n          <Text>\n            You are receiving this email because you have requested access to\n            the \"{page}\" Status Page.\n          </Text>\n          <Text>\n            To authenticate, please click the link below. The link is valid for\n            24 hours. If you believe this is a mistake, please ignore this\n            email.\n          </Text>\n          <Text>\n            <Link style={styles.link} href={link}>\n              Authenticate\n            </Link>\n          </Text>\n        </Layout>\n      </Body>\n    </Html>\n  );\n};\n\nStatusPageMagicLinkEmail.PreviewProps = {\n  page: \"OpenStatus\",\n  link: \"https://slug.openstatus.dev/verify/token-xyz\",\n} satisfies StatusPageMagicLinkProps;\n\nexport default StatusPageMagicLinkEmail;\n"
  },
  {
    "path": "packages/emails/emails/status-report.tsx",
    "content": "/** @jsxImportSource react */\n\nimport {\n  Body,\n  Column,\n  Head,\n  Heading,\n  Html,\n  Link,\n  Markdown,\n  Preview,\n  Row,\n  Section,\n  Text,\n} from \"@react-email/components\";\nimport { z } from \"zod\";\nimport { Layout } from \"./_components/layout\";\nimport { colors, styles } from \"./_components/styles\";\n\nexport const StatusReportSchema = z.object({\n  pageTitle: z.string(),\n  // statusReportStatus from db\n  status: z.enum([\n    \"investigating\",\n    \"identified\",\n    \"monitoring\",\n    \"resolved\",\n    \"maintenance\",\n  ]),\n  date: z.string(),\n  message: z.string(),\n  reportTitle: z.string(),\n  pageComponents: z.array(z.string()),\n  unsubscribeUrl: z.url(),\n  manageUrl: z.url(),\n});\n\nexport type StatusReportProps = z.infer<typeof StatusReportSchema>;\n\nfunction getStatusColor(status: string) {\n  switch (status) {\n    case \"investigating\":\n      return colors.danger;\n    case \"identified\":\n      return colors.warning;\n    case \"resolved\":\n      return colors.success;\n    case \"monitoring\":\n      return colors.info;\n    case \"maintenance\":\n      return colors.info;\n    default:\n      return colors.success;\n  }\n}\n\nfunction StatusReportEmail({\n  status,\n  date,\n  message,\n  reportTitle,\n  pageTitle,\n  pageComponents,\n  unsubscribeUrl,\n  manageUrl,\n}: StatusReportProps) {\n  return (\n    <Html>\n      <Head />\n      <Preview>There are new updates on \"{pageTitle}\" page</Preview>\n      <Body style={styles.main}>\n        <Layout>\n          <Row>\n            <Column>\n              <Heading as=\"h3\">{pageTitle}</Heading>\n            </Column>\n            <Column style={{ textAlign: \"right\" }}>\n              <Text\n                style={{\n                  color: getStatusColor(status),\n                  textTransform: \"uppercase\",\n                }}\n              >\n                {status}\n              </Text>\n            </Column>\n          </Row>\n          <Row style={styles.row}>\n            <Column>\n              <Text style={styles.bold}>Title</Text>\n            </Column>\n            <Column style={{ textAlign: \"right\" }}>\n              <Text>{reportTitle}</Text>\n            </Column>\n          </Row>\n          <Row style={styles.row}>\n            <Column>\n              <Text style={styles.bold}>Date</Text>\n            </Column>\n            <Column style={{ textAlign: \"right\" }}>\n              <Text>{date}</Text>\n            </Column>\n          </Row>\n          <Row style={styles.row}>\n            <Column>\n              <Text style={styles.bold}>Affected</Text>\n            </Column>\n            <Column style={{ textAlign: \"right\" }}>\n              <Text style={{ flexWrap: \"wrap\", wordWrap: \"break-word\" }}>\n                {pageComponents.length > 0 ? pageComponents.join(\", \") : \"N/A\"}\n              </Text>\n            </Column>\n          </Row>\n          <Row style={styles.row}>\n            <Column>\n              <Markdown>{message}</Markdown>\n            </Column>\n          </Row>\n          {unsubscribeUrl && (\n            <Section style={{ marginTop: \"24px\", textAlign: \"center\" }}>\n              <Text style={{ fontSize: \"12px\", color: \"#6b7280\" }}>\n                <Link\n                  href={unsubscribeUrl}\n                  style={{ color: \"#6b7280\", textDecoration: \"underline\" }}\n                >\n                  Unsubscribe\n                </Link>{\" \"}\n                ・{\" \"}\n                <Link\n                  href={manageUrl}\n                  style={{ color: \"#6b7280\", textDecoration: \"underline\" }}\n                >\n                  Manage notifications\n                </Link>\n              </Text>\n            </Section>\n          )}\n        </Layout>\n      </Body>\n    </Html>\n  );\n}\n\nStatusReportEmail.PreviewProps = {\n  pageTitle: \"OpenStatus Status\",\n  reportTitle: \"API Unavaible\",\n  status: \"investigating\",\n  date: new Date().toISOString(),\n  message: `\n**Status**: Partial Service Restored\n\n**GitHub Runners**: Operational\n\n**Cache Action**: Degraded\n\n---\n\n### What's Changed\n\n- All queued workflows are now being picked up and completed successfully.\n- Jobs are running normally on our GitHub App. ### Current Issue: Cache Action Unavailable Attempts to re-publish our action to GitHub Marketplace are returning 500 Internal Server Errors. This prevents the updated versions from going live.\n\n### Mitigation In Progress\n\n- Collaborating with GitHub Support to resolve any upstream issues.\n\n### Next Update\n\nWe'll post another update by **19:00 UTC** today or sooner if critical developments occur. We apologize for the inconvenience and appreciate your patience as we restore full cache functionality.\n  `,\n  pageComponents: [\"OpenStatus API\", \"OpenStatus Webhook\"],\n  unsubscribeUrl:\n    \"https://status.openstatus.dev/unsubscribe/550e8400-e29b-41d4-a716-446655440000\",\n  manageUrl:\n    \"https://status.openstatus.dev/manage/550e8400-e29b-41d4-a716-446655440000\",\n};\n\nexport default StatusReportEmail;\n"
  },
  {
    "path": "packages/emails/emails/subscribe.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { Body, Head, Html, Link, Preview } from \"@react-email/components\";\n\ninterface SubscribeProps {\n  page: string;\n  link: string;\n}\n\nconst SubscribeEmail = ({ page, link }: SubscribeProps) => {\n  return (\n    <Html>\n      <Head>\n        <title>Confirm your subscription to \"{page}\" Status Page</title>\n      </Head>\n      <Preview>Confirm your subscription to \"{page}\" Status Page</Preview>\n      <Body>\n        <p>Confirm your subscription to \"{page}\" Status Page</p>\n        <p>\n          You are receiving this email because you subscribed to receive updates\n          from {page} Status Page.\n        </p>\n        <p>\n          To confirm your subscription, please click the link below. If you\n          believe this is a mistake, please ignore this email.\n        </p>\n        <p>\n          <a href={link}>Confirm subscription</a>\n        </p>\n        <br />🚀 Powered by{\" \"}\n        <Link href=\"https://www.openstatus.dev\">OpenStatus.dev</Link>\n      </Body>\n    </Html>\n  );\n};\n\nSubscribeEmail.PreviewProps = {\n  page: \"OpenStatus\",\n  link: \"https://slug.openstatus.dev/verify/token-xyz\",\n} satisfies SubscribeProps;\n\nexport default SubscribeEmail;\n"
  },
  {
    "path": "packages/emails/emails/team-invitation.tsx",
    "content": "/** @jsxImportSource react */\n\nimport {\n  Body,\n  Head,\n  Heading,\n  Html,\n  Link,\n  Preview,\n  Text,\n} from \"@react-email/components\";\nimport { z } from \"zod\";\nimport { Layout } from \"./_components/layout\";\nimport { styles } from \"./_components/styles\";\n\nconst BASE_URL = \"https://app.openstatus.dev/invite\";\n\nexport const TeamInvitationSchema = z.object({\n  invitedBy: z.string(),\n  workspaceName: z.string().optional().nullable(),\n  token: z.string(),\n  baseUrl: z.string().optional(),\n});\n\nexport type TeamInvitationProps = z.infer<typeof TeamInvitationSchema>;\n\nconst TeamInvitationEmail = ({\n  token,\n  workspaceName,\n  invitedBy,\n  baseUrl = BASE_URL,\n}: TeamInvitationProps) => {\n  return (\n    <Html>\n      <Head />\n      <Preview>You have been invited to join OpenStatus.dev</Preview>\n      <Body style={styles.main}>\n        <Layout>\n          <Heading as=\"h3\">\n            You have been invited to join{\" \"}\n            {workspaceName ? `\"${workspaceName}\" workspace` : \"OpenStatus.dev\"}{\" \"}\n            by {invitedBy}\n          </Heading>\n          <Text>\n            Click here to access the workspace:{\" \"}\n            <Link style={styles.link} href={`${baseUrl}?token=${token}`}>\n              accept invitation\n            </Link>\n          </Text>\n          <Text>\n            If you don't have an account yet, it will require you to create one.\n          </Text>\n        </Layout>\n      </Body>\n    </Html>\n  );\n};\n\nTeamInvitationEmail.PreviewProps = {\n  token: \"token\",\n  workspaceName: \"OpenStatus\",\n  invitedBy: \"max@openstatus.dev\",\n} satisfies TeamInvitationProps;\n\nexport default TeamInvitationEmail;\n"
  },
  {
    "path": "packages/emails/emails/team-invite-reminder.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { Body, Head, Html, Preview } from \"@react-email/components\";\n\nconst TeamInviteReminderEmail = () => {\n  return (\n    <Html>\n      <Head>\n        <title>Incidents are a team sport</title>\n      </Head>\n      <Preview>Invite your team so everyone can manage status updates</Preview>\n      <Body>\n        Hey\n        <br />\n        <br />\n        When something goes down at 2am, you don't want to be the only one who\n        can update the status page.\n        <br />\n        <br />👉{\" \"}\n        <a href=\"https://app.openstatus.dev/settings/general?ref=email-team-invite\">\n          Invite your team\n        </a>\n        <br />\n        <br />\n        Everyone gets access to monitors, incidents, and status pages — so\n        whoever is on-call can respond without waiting on you.\n        <br />\n        <br />\n        Hit reply if you have questions — happy to help.\n        <br />\n        <br />\n        Thibault Le Ouay Ducasse, co-founder of OpenStatus\n        <br />\n      </Body>\n    </Html>\n  );\n};\n\nexport default TeamInviteReminderEmail;\n"
  },
  {
    "path": "packages/emails/emails/welcome.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { Body, Head, Html, Preview } from \"@react-email/components\";\n\nconst WelcomeEmail = () => {\n  return (\n    <Html>\n      <Head>\n        <title>Welcome to OpenStatus</title>\n      </Head>\n      <Preview>Set up your status page in under 5 minutes</Preview>\n\n      <Body>\n        Hey 👋\n        <br />\n        <br />\n        Thanks for signing up for OpenStatus.\n        <br />\n        <br />\n        The fastest way to get started: create your status page. It takes under\n        5 minutes and your users will have a single place to check if your\n        services are up.\n        <br />\n        <br />👉{\" \"}\n        <a href=\"https://app.openstatus.dev/status-pages/create?ref=email-onboarding\">\n          Create your status page\n        </a>\n        <br />\n        <br />\n        Want full control? Use our{\" \"}\n        <a href=\"https://www.openstatus.dev/registry?ref=email-onboarding\">\n          open source\n        </a>{\" \"}\n        to build your own status page and host it anywhere.\n        <br />\n        <br />\n        Hit reply if you get stuck — I read every response.\n        <br />\n        <br />\n        Thibault Le Ouay Ducasse, co-founder of OpenStatus\n        <br />\n      </Body>\n    </Html>\n  );\n};\n\nexport default WelcomeEmail;\n"
  },
  {
    "path": "packages/emails/hotfix/monitor-alert.ts",
    "content": "interface MonitorAlertEmailProps {\n  name?: string;\n  type: \"alert\" | \"degraded\" | \"recovery\";\n  url?: string;\n  status?: string;\n  latency?: string;\n  region?: string;\n  timestamp?: string;\n  message?: string;\n}\n\nexport function monitorAlertEmail(props: MonitorAlertEmailProps) {\n  return `\n    <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html dir=\"ltr\" lang=\"en\">\n  <body>\n    <p>Your monitor is ${props.type}</p>\n    <ul>\n      <li>URL: ${props.url || \"N/A\"}</li>\n      <li>Status: ${props.status || \"N/A\"}</li>\n      <li>Timestamp: ${props.timestamp || \"N/A\"}</li>\n      <li>Message: ${props.message || \"N/A\"}</li>\n    </ul>\n  </body>\n</html>\n`;\n}\n"
  },
  {
    "path": "packages/emails/hotfix/monitor-deactivation.ts",
    "content": "interface MonitorDeactivationEmailProps {\n  date: string;\n}\n\nexport const monitorDeactivationEmail = (\n  props: MonitorDeactivationEmailProps,\n) => {\n  return `\n  <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n  <html xmlns=\"http://www.w3.org/1999/xhtml\">\n  <head>\n      <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n      <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n      <title>Login to your OpenStatus account to keep your monitors active.</title>\n      <style>\n          body {\n              font-family: Arial, sans-serif;\n          }\n      </style>\n  </head>\n  <body>\n      <p>Hello, </p><p>To save on cloud resources and avoid having stale monitors. We are deactivating monitors for free account if you have not logged in for the last 2 months.</p><p>Your monitor(s) will be deactivated on ${props.date}</p><p>If you would like to keep your monitor(s) active, please login to  your account or upgrade to a paid plan.</p><p>If you have any questions, please reply to this email.</p><p>Merci,</p><p>Thibault</p>\n  </body>\n  </html>\n`;\n};\n"
  },
  {
    "path": "packages/emails/hotfix/monitor-paused.ts",
    "content": "export const monitorPausedEmail = () => {\n  return `\n  <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n  <html xmlns=\"http://www.w3.org/1999/xhtml\">\n  <head>\n      <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n      <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n      <title>Your monitors have been paused</title>\n      <style>\n          body {\n              font-family: Arial, sans-serif;\n          }\n      </style>\n  </head>\n  <body>\n      <p>Hello, </p><p>To save on cloud resources, your monitor(s) has been paused due to inactivity.</p><p>If you would like to unpause your monitor(s), please login to  your account or upgrade to a paid plan.</p><p>If you have any questions, please reply to this email.</p><p>Merci,</p><p>Thibault</p>\n  </body>\n  </html>\n\n`;\n};\n"
  },
  {
    "path": "packages/emails/package.json",
    "content": "{\n  \"name\": \"@openstatus/emails\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"./src/index.ts\",\n  \"scripts\": {\n    \"dev:email\": \"email dev\",\n    \"test\": \"bun test\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"@react-email/components\": \"1.0.3\",\n    \"@react-email/render\": \"2.0.1\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"effect\": \"3.19.12\",\n    \"react-email\": \"5.1.1\",\n    \"resend\": \"6.6.0\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@react-email/preview-server\": \"5.1.1\",\n    \"@types/node\": \"22.10.2\",\n    \"@types/react\": \"19.2.2\",\n    \"react\": \"19.2.3\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/emails/src/client.integration.test.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { describe, expect, test } from \"bun:test\";\nimport { render } from \"@react-email/render\";\nimport StatusReportEmail from \"../emails/status-report\";\n\ndescribe(\"Status Report Email - Unsubscribe Link in Body\", () => {\n  const unsubscribeUrl =\n    \"https://openstatus.openstatus.dev/unsubscribe/test-token\";\n  const manageUrl = \"https://openstatus.openstatus.dev/manage/test-token\";\n\n  test(\"should include unsubscribe link in email body when URL is provided\", async () => {\n    const html = await render(\n      <StatusReportEmail\n        pageTitle=\"Test Page\"\n        reportTitle=\"Test Report\"\n        status=\"investigating\"\n        date={new Date().toISOString()}\n        message=\"Test message\"\n        pageComponents={[\"Monitor 1\"]}\n        unsubscribeUrl={unsubscribeUrl}\n        manageUrl={manageUrl}\n      />,\n    );\n\n    expect(html).toContain(unsubscribeUrl);\n    expect(html).toContain(\"Unsubscribe\");\n  });\n\n  test(\"should not include unsubscribe section when URL is not provided\", async () => {\n    const html = await render(\n      <StatusReportEmail\n        pageTitle=\"Test Page\"\n        reportTitle=\"Test Report\"\n        status=\"investigating\"\n        date={new Date().toISOString()}\n        message=\"Test message\"\n        pageComponents={[\"Monitor 1\"]}\n        manageUrl={manageUrl}\n      />,\n    );\n\n    // Should not contain the unsubscribe text when no URL provided\n    expect(html).not.toContain(\"from these notifications\");\n  });\n\n  test(\"should render unsubscribe link as clickable\", async () => {\n    const html = await render(\n      <StatusReportEmail\n        pageTitle=\"Test Page\"\n        reportTitle=\"Test Report\"\n        status=\"investigating\"\n        date={new Date().toISOString()}\n        message=\"Test message\"\n        pageComponents={[\"Monitor 1\"]}\n        unsubscribeUrl={unsubscribeUrl}\n        manageUrl={manageUrl}\n      />,\n    );\n\n    // Check that the URL is in an href attribute\n    expect(html).toContain(`href=\"${unsubscribeUrl}\"`);\n  });\n\n  test(\"should display unsubscribe link with proper styling\", async () => {\n    const html = await render(\n      <StatusReportEmail\n        pageTitle=\"Test Page\"\n        reportTitle=\"Test Report\"\n        status=\"investigating\"\n        date={new Date().toISOString()}\n        message=\"Test message\"\n        pageComponents={[\"Monitor 1\"]}\n        unsubscribeUrl={unsubscribeUrl}\n        manageUrl={manageUrl}\n      />,\n    );\n\n    // Check for muted styling (gray color for footer)\n    expect(html).toContain(\"#6b7280\");\n  });\n});\n\ndescribe(\"Status Report Email - Email Content Validation\", () => {\n  test(\"should include all required email fields\", async () => {\n    const props = {\n      pageTitle: \"OpenStatus\",\n      reportTitle: \"API Outage\",\n      status: \"investigating\" as const,\n      date: \"2024-01-15T10:00:00.000Z\",\n      message: \"We are investigating the issue\",\n      pageComponents: [\"API\", \"Web\"],\n      unsubscribeUrl: \"https://openstatus.openstatus.dev/unsubscribe/test\",\n      manageUrl: \"https://openstatus.openstatus.dev/manage/test\",\n    };\n\n    const html = await render(<StatusReportEmail {...props} />);\n\n    expect(html).toContain(props.pageTitle);\n    expect(html).toContain(props.reportTitle);\n    expect(html).toContain(props.message);\n    expect(html).toContain(\"API\");\n    expect(html).toContain(\"Web\");\n    expect(html).toContain(props.unsubscribeUrl);\n  });\n\n  test(\"should handle all status types correctly\", async () => {\n    const statuses = [\n      \"investigating\",\n      \"identified\",\n      \"monitoring\",\n      \"resolved\",\n      \"maintenance\",\n    ] as const;\n\n    for (const status of statuses) {\n      const html = await render(\n        <StatusReportEmail\n          pageTitle=\"Test\"\n          reportTitle=\"Test Report\"\n          status={status}\n          date={new Date().toISOString()}\n          message=\"Test\"\n          pageComponents={[]}\n          unsubscribeUrl=\"https://example.com/unsubscribe\"\n          manageUrl=\"https://example.com/manage\"\n        />,\n      );\n\n      // Should render without errors and contain the status\n      // Note: status is rendered lowercase in HTML with text-transform: uppercase CSS\n      expect(html).toContain(status);\n    }\n  });\n});\n"
  },
  {
    "path": "packages/emails/src/client.tsx",
    "content": "/** @jsxImportSource react */\n\nimport { render } from \"@react-email/render\";\nimport { Effect, Schedule } from \"effect\";\nimport { Resend } from \"resend\";\nimport FollowUpEmail from \"../emails/followup\";\nimport type { MonitorAlertProps } from \"../emails/monitor-alert\";\nimport PageSubscriptionEmail from \"../emails/page-subscription\";\nimport type { PageSubscriptionProps } from \"../emails/page-subscription\";\nimport SlackFeedbackEmail from \"../emails/slack-feedback\";\nimport StatusPageMagicLinkEmail from \"../emails/status-page-magic-link\";\nimport type { StatusPageMagicLinkProps } from \"../emails/status-page-magic-link\";\nimport StatusReportEmail from \"../emails/status-report\";\nimport type { StatusReportProps } from \"../emails/status-report\";\nimport TeamInvitationEmail from \"../emails/team-invitation\";\nimport type { TeamInvitationProps } from \"../emails/team-invitation\";\nimport { monitorAlertEmail } from \"../hotfix/monitor-alert\";\n\n// split an array into chunks of a given size.\nfunction chunk<T>(array: T[], size: number): T[][] {\n  const result: T[][] = [];\n  for (let i = 0; i < array.length; i += size) {\n    result.push(array.slice(i, i + size));\n  }\n  return result;\n}\n\nexport class EmailClient {\n  public readonly client: Resend;\n\n  constructor(opts: { apiKey: string }) {\n    this.client = new Resend(opts.apiKey);\n  }\n\n  public async sendFollowUp(req: { to: string }) {\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(`Sending follow up email to ${req.to}`);\n      return;\n    }\n\n    try {\n      const html = await render(<FollowUpEmail />);\n      const result = await this.client.emails.send({\n        from: \"Thibault Le Ouay Ducasse <welcome@openstatus.dev>\",\n        replyTo: \"Thibault Le Ouay Ducasse <thibault@openstatus.dev>\",\n        subject: \"How's it going with OpenStatus?\",\n        to: req.to,\n        html,\n      });\n\n      if (!result.error) {\n        console.log(`Sent follow up email to ${req.to}`);\n        return;\n      }\n\n      throw result.error;\n    } catch (err) {\n      console.error(`Error sending follow up email to ${req.to}: ${err}`);\n    }\n  }\n\n  public async sendFollowUpBatched(req: { to: string[] }) {\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(`Sending follow up emails to ${req.to.join(\", \")}`);\n      return;\n    }\n\n    const html = await render(<FollowUpEmail />);\n    const result = await this.client.batch.send(\n      req.to.map((subscriber) => ({\n        from: \"Thibault Le Ouay Ducasse <thibault@openstatus.dev>\",\n        subject: \"How's it going with OpenStatus?\",\n        to: subscriber,\n        html,\n      })),\n    );\n\n    if (result.error) {\n      //  We only throw the error if we are rate limited\n      if (result.error?.name === \"rate_limit_exceeded\") {\n        throw result.error;\n      }\n      //  Otherwise let's log the error and continue\n      console.error(\n        `Error sending follow up email to ${req.to}: ${result.error}`,\n      );\n      return;\n    }\n\n    console.log(`Sent follow up emails to ${req.to}`);\n  }\n\n  public async sendSlackFeedback(req: { to: string }) {\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(`Sending slack feedback email to ${req.to}`);\n      return;\n    }\n\n    try {\n      const html = await render(<SlackFeedbackEmail />);\n      const result = await this.client.emails.send({\n        from: \"Thibault Le Ouay Ducasse <thibault@openstatus.dev>\",\n        replyTo: \"Thibault Le Ouay Ducasse <thibault@openstatus.dev>\",\n        subject: \"How's the Slack app working for you?\",\n        to: req.to,\n        html,\n      });\n\n      if (!result.error) {\n        console.log(`Sent slack feedback email to ${req.to}`);\n        return;\n      }\n\n      throw result.error;\n    } catch (err) {\n      console.error(`Error sending slack feedback email to ${req.to}: ${err}`);\n    }\n  }\n\n  public async sendSlackFeedbackBatched(req: { to: string[] }) {\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(`Sending slack feedback emails to ${req.to.join(\", \")}`);\n      return;\n    }\n\n    const html = await render(<SlackFeedbackEmail />);\n    const result = await this.client.batch.send(\n      req.to.map((subscriber) => ({\n        from: \"Thibault Le Ouay Ducasse <thibault@openstatus.dev>\",\n        subject: \"How's the Slack app working for you?\",\n        to: subscriber,\n        html,\n      })),\n    );\n\n    if (result.error) {\n      if (result.error?.name === \"rate_limit_exceeded\") {\n        throw result.error;\n      }\n      console.error(\n        `Error sending slack feedback email to ${req.to}: ${result.error}`,\n      );\n      return;\n    }\n\n    console.log(`Sent slack feedback emails to ${req.to}`);\n  }\n\n  public async sendStatusReportUpdate(\n    req: Omit<StatusReportProps, \"unsubscribeUrl\" | \"manageUrl\"> & {\n      subscribers: Array<{ email: string; token: string }>;\n      pageSlug: string;\n      customDomain?: string | null;\n    },\n  ) {\n    const statusPageBaseUrl = req.customDomain\n      ? `https://${req.customDomain}`\n      : `https://${req.pageSlug}.openstatus.dev`;\n\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(\n        `Sending status report update emails to ${req.subscribers\n          .map((s) => s.email)\n          .join(\", \")}`,\n      );\n      return;\n    }\n\n    for (const recipients of chunk(req.subscribers, 100)) {\n      const sendEmail = Effect.tryPromise({\n        try: () =>\n          this.client.batch.send(\n            recipients.map((subscriber) => {\n              const unsubscribeUrl = `${statusPageBaseUrl}/unsubscribe/${subscriber.token}`;\n              const manageUrl = `${statusPageBaseUrl}/manage/${subscriber.token}`;\n              return {\n                from: `${req.pageTitle} <notifications@notifications.openstatus.dev>`,\n                subject: req.reportTitle,\n                to: subscriber.email,\n                react: (\n                  <StatusReportEmail\n                    {...req}\n                    unsubscribeUrl={unsubscribeUrl}\n                    manageUrl={manageUrl}\n                  />\n                ),\n              };\n            }),\n          ),\n        catch: (_unknown) =>\n          new Error(\n            `Error sending status report update batch to ${recipients.map(\n              (r) => r.email,\n            )}`,\n          ),\n      }).pipe(\n        Effect.andThen((result) =>\n          result.error ? Effect.fail(result.error) : Effect.succeed(result),\n        ),\n        Effect.retry({\n          times: 3,\n          schedule: Schedule.exponential(\"1000 millis\"),\n        }),\n      );\n      await Effect.runPromise(sendEmail).catch(console.error);\n    }\n\n    console.log(\n      `Sent status report update email to ${req.subscribers.length} subscribers`,\n    );\n  }\n\n  public async sendTeamInvitation(req: TeamInvitationProps & { to: string }) {\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(`Sending team invitation email to ${req.to}`);\n      return;\n    }\n\n    try {\n      const html = await render(<TeamInvitationEmail {...req} />);\n      const result = await this.client.emails.send({\n        from: `${\n          req.workspaceName ?? \"OpenStatus\"\n        } <notifications@notifications.openstatus.dev>`,\n        subject: `You've been invited to join ${\n          req.workspaceName ?? \"OpenStatus\"\n        }`,\n        to: req.to,\n        html,\n      });\n\n      if (!result.error) {\n        console.log(`Sent team invitation email to ${req.to}`);\n        return;\n      }\n\n      throw result.error;\n    } catch (err) {\n      console.error(`Error sending team invitation email to ${req.to}`, err);\n    }\n  }\n\n  public async sendMonitorAlert(req: MonitorAlertProps & { to: string }) {\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(`Sending monitor alert email to ${req.to}`);\n      return;\n    }\n\n    try {\n      // const html = await render(<MonitorAlertEmail {...req} />);\n      const html = monitorAlertEmail(req);\n      const result = await this.client.emails.send({\n        from: \"OpenStatus <notifications@notifications.openstatus.dev>\",\n        subject: `${req.name}: ${req.type.toUpperCase()}`,\n        to: req.to,\n        html,\n      });\n\n      if (!result.error) {\n        console.log(`Sent monitor alert email to ${req.to}`);\n        return;\n      }\n\n      throw result.error;\n    } catch (err) {\n      console.error(`Error sending monitor alert to ${req.to}`, err);\n      throw err;\n    }\n  }\n\n  public async sendPageSubscription(\n    req: PageSubscriptionProps & { to: string },\n  ) {\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(`Sending page subscription email to ${req.to}`);\n      return;\n    }\n\n    try {\n      const html = await render(<PageSubscriptionEmail {...req} />);\n      const result = await this.client.emails.send({\n        from: \"Status Page <notifications@notifications.openstatus.dev>\",\n        subject: `Confirm your subscription to ${req.page}`,\n        to: req.to,\n        html,\n      });\n\n      if (!result.error) {\n        console.log(`Sent page subscription email to ${req.to}`);\n        return;\n      }\n\n      throw result.error;\n    } catch (err) {\n      console.error(`Error sending page subscription to ${req.to}`, err);\n    }\n  }\n\n  public async sendStatusPageMagicLink(\n    req: StatusPageMagicLinkProps & { to: string },\n  ) {\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(`Sending status page magic link email to ${req.to}`);\n      console.log(`>>> Magic Link: ${req.link}`);\n      return;\n    }\n\n    try {\n      const html = await render(<StatusPageMagicLinkEmail {...req} />);\n      const result = await this.client.emails.send({\n        from: \"Status Page <notifications@notifications.openstatus.dev>\",\n        subject: `Authenticate to ${req.page}`,\n        to: req.to,\n        html,\n      });\n\n      if (!result.error) {\n        console.log(`Sent status page magic link email to ${req.to}`);\n        return;\n      }\n\n      throw result.error;\n    } catch (err) {\n      console.error(`Error sending status page magic link to ${req.to}`, err);\n    }\n  }\n\n  public async sendMaintenanceNotification(req: {\n    subscribers: Array<{ email: string; token: string }>;\n    pageTitle: string;\n    pageSlug: string;\n    customDomain?: string | null;\n    maintenanceTitle: string;\n    message: string;\n    from: string;\n    to: string;\n    pageComponents: string[];\n  }) {\n    const statusPageBaseUrl = req.customDomain\n      ? `https://${req.customDomain}`\n      : `https://${req.pageSlug}.openstatus.dev`;\n\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(\n        `Sending maintenance notification emails to ${req.subscribers\n          .map((s) => s.email)\n          .join(\", \")}`,\n      );\n      return;\n    }\n\n    for (const recipients of chunk(req.subscribers, 100)) {\n      const sendEmail = Effect.tryPromise({\n        try: () =>\n          this.client.batch.send(\n            recipients.map((subscriber) => {\n              const unsubscribeUrl = `${statusPageBaseUrl}/unsubscribe/${subscriber.token}`;\n              const manageUrl = `${statusPageBaseUrl}/manage/${subscriber.token}`;\n              return {\n                from: `${req.pageTitle} <notifications@notifications.openstatus.dev>`,\n                subject: `Scheduled Maintenance: ${req.maintenanceTitle}`,\n                to: subscriber.email,\n                react: (\n                  <StatusReportEmail\n                    pageTitle={req.pageTitle}\n                    reportTitle={req.maintenanceTitle}\n                    status=\"maintenance\"\n                    date={`${req.from} - ${req.to}`}\n                    message={req.message}\n                    pageComponents={req.pageComponents}\n                    unsubscribeUrl={unsubscribeUrl}\n                    manageUrl={manageUrl}\n                  />\n                ),\n              };\n            }),\n          ),\n        catch: (_unknown) =>\n          new Error(\n            `Error sending maintenance notification batch to ${recipients.map(\n              (r) => r.email,\n            )}`,\n          ),\n      }).pipe(\n        Effect.andThen((result) =>\n          result.error ? Effect.fail(result.error) : Effect.succeed(result),\n        ),\n        Effect.retry({\n          times: 3,\n          schedule: Schedule.exponential(\"1000 millis\"),\n        }),\n      );\n      await Effect.runPromise(sendEmail).catch(console.error);\n    }\n\n    console.log(\n      `Sent maintenance notification email to ${req.subscribers.length} subscribers`,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/emails/src/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    RESEND_API_KEY: z.string().min(1),\n  },\n  runtimeEnv: {\n    RESEND_API_KEY: process.env.RESEND_API_KEY,\n  },\n});\n"
  },
  {
    "path": "packages/emails/src/index.ts",
    "content": "export { default as FeedbackEmail } from \"../emails/feedback\";\nexport { default as FollowUpEmail } from \"../emails/followup\";\nexport { default as SlackFeedbackEmail } from \"../emails/slack-feedback\";\nexport { default as SubscribeEmail } from \"../emails/subscribe\";\nexport { default as TeamInviteReminderEmail } from \"../emails/team-invite-reminder\";\nexport { default as WelcomeEmail } from \"../emails/welcome\";\nexport { default as TeamInvitationEmail } from \"../emails/team-invitation\";\nexport { default as MonitorPausedEmail } from \"../emails/monitor-paused\";\nexport { default as MonitorDeactivationEmail } from \"../emails/monitor-deactivation\";\nexport { default as StatusPageMagicLinkEmail } from \"../emails/status-page-magic-link\";\n\nexport { monitorDeactivationEmail } from \"../hotfix/monitor-deactivation\";\nexport { monitorPausedEmail } from \"../hotfix/monitor-paused\";\nexport { sendEmail, sendEmailHtml, sendBatchEmailHtml } from \"./send\";\n\nexport { EmailClient } from \"./client\";\n"
  },
  {
    "path": "packages/emails/src/send.ts",
    "content": "import type React from \"react\";\nimport { Resend } from \"resend\";\n\nimport { render } from \"@react-email/render\";\nimport { env } from \"./env\";\n\nexport const resend = new Resend(env.RESEND_API_KEY);\n\nexport interface Emails {\n  react: React.JSX.Element;\n  subject: string;\n  to: string[];\n  from: string;\n  reply_to?: string;\n}\n\nexport type EmailHtml = {\n  html: string;\n  subject: string;\n  to: string;\n  from: string;\n  reply_to?: string;\n};\nexport const sendEmail = async (email: Emails) => {\n  if (process.env.NODE_ENV !== \"production\") return;\n  await resend.emails.send(email);\n};\n\nexport const sendBatchEmailHtml = async (emails: EmailHtml[]) => {\n  if (process.env.NODE_ENV !== \"production\") return;\n  await resend.batch.send(emails);\n};\n\n// TODO: delete in favor of sendBatchEmailHtml\nexport const sendEmailHtml = async (emails: EmailHtml[]) => {\n  if (process.env.NODE_ENV !== \"production\") return;\n\n  await fetch(\"https://api.resend.com/emails/batch\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${env.RESEND_API_KEY}`,\n    },\n    body: JSON.stringify(emails),\n  });\n};\n\nexport const sendWithRender = async (email: Emails) => {\n  if (process.env.NODE_ENV !== \"production\") return;\n  const html = await render(email.react);\n  await resend.emails.send({\n    ...email,\n    html,\n  });\n};\n"
  },
  {
    "path": "packages/emails/src/utils.ts",
    "content": "export const validateEmailNotDisposable = async (mailHost: string) => {\n  const response = await fetch(\n    `https://open.kickbox.com/v1/disposable/${mailHost}`,\n  );\n  const status = (await response.json()) as Record<string, unknown>;\n\n  return status.disposable;\n};\n"
  },
  {
    "path": "packages/emails/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/react-library.json\",\n  \"include\": [\".\", \"src/env.ts\"],\n  \"exclude\": [\"dist\", \"build\", \"node_modules\"]\n}\n"
  },
  {
    "path": "packages/error/index.ts",
    "content": "export * from \"./src/base-error\";\nexport * from \"./src/error-code\";\nexport * from \"./src/http-error\";\nexport * from \"./src/schema-error\";\nexport * from \"./src/utils\";\n"
  },
  {
    "path": "packages/error/package.json",
    "content": "{\n  \"name\": \"@openstatus/error\",\n  \"version\": \"0.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.ts\",\n  \"scripts\": {},\n  \"dependencies\": {\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/error/src/base-error.ts",
    "content": "import type { ErrorCode } from \"./error-code\";\n\ntype ErrorContext = Record<string, unknown>;\n\nexport abstract class BaseError<\n  TContext extends ErrorContext = ErrorContext,\n> extends Error {\n  public abstract readonly name: string;\n  /**\n   * A distinct code for the error type used to differentiate between different types of errors.\n   * Used to build the URL for the error documentation.\n   * @example 'UNAUTHENTICATED' | 'INTERNAL_SERVER_ERROR'\n   */\n  public abstract readonly code?: ErrorCode;\n  public readonly cause?: BaseError;\n  /**\n   * Additional context to help understand the error.\n   * @example { url: 'https://example.com/api', method: 'GET', statusCode: 401 }\n   */\n  public readonly context?: TContext;\n\n  constructor(opts: {\n    message: string;\n    cause?: BaseError;\n    context?: TContext;\n  }) {\n    super(opts.message);\n    this.cause = opts.cause;\n    this.context = opts.context;\n\n    // TODO: add logger here!\n  }\n\n  public toString(): string {\n    return `${this.name}(${this.code}): ${\n      this.message\n    } - caused by ${this.cause?.toString()} - with context ${JSON.stringify(\n      this.context,\n    )}`;\n  }\n\n  // get docs(): string {\n  //   if (!this.code) return \"https://example.com/docs/errors\"\n  //   return `https://example.com/docs/errors/${this.code}`;\n  // }\n}\n"
  },
  {
    "path": "packages/error/src/error-code.ts",
    "content": "import { z } from \"zod\";\n\nexport const ErrorCodes = [\n  \"BAD_REQUEST\",\n  \"FORBIDDEN\",\n  \"INTERNAL_SERVER_ERROR\",\n  \"PAYMENT_REQUIRED\",\n  \"CONFLICT\",\n  \"NOT_FOUND\",\n  \"UNAUTHORIZED\",\n  \"METHOD_NOT_ALLOWED\",\n  \"UNPROCESSABLE_ENTITY\",\n] as const;\n\nexport const ErrorCodeEnum = z.enum(ErrorCodes);\n\nexport type ErrorCode = z.infer<typeof ErrorCodeEnum>;\n"
  },
  {
    "path": "packages/error/src/http-error.ts",
    "content": "import { BaseError } from \"./base-error\";\nimport type { ErrorCode } from \"./error-code\";\nimport { statusToCode } from \"./utils\";\n\ntype Context = {\n  url?: string;\n  method?: string;\n  statusCode?: number;\n};\n\nexport class HttpError extends BaseError<Context> {\n  public readonly name = HttpError.name;\n  public readonly code: ErrorCode;\n\n  constructor(opts: {\n    code: ErrorCode;\n    message: string;\n    cause?: BaseError;\n    context?: Context;\n  }) {\n    super(opts);\n    this.code = opts.code;\n  }\n\n  public static fromRequest(request: Request, response: Response) {\n    return new HttpError({\n      code: statusToCode(response.status),\n      message: response.statusText, // can be overriden with { ...res, statusText: 'Custom message' }\n      context: {\n        url: request.url,\n        method: request.method,\n        statusCode: response.status,\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "packages/error/src/schema-error.ts",
    "content": "import type { ZodError } from \"zod\";\n\nimport { BaseError } from \"./base-error\";\nimport type { ErrorCode } from \"./error-code\";\nimport { parseZodErrorIssues } from \"./utils\";\n\ntype Context = { raw: unknown };\n\nexport class SchemaError extends BaseError<Context> {\n  public readonly name = SchemaError.name;\n  public readonly code: ErrorCode;\n\n  constructor(opts: {\n    code: ErrorCode;\n    message: string;\n    cause?: BaseError;\n    context?: Context;\n  }) {\n    super(opts);\n    this.code = opts.code;\n  }\n\n  static fromZod<T>(e: ZodError<T>, raw: unknown): SchemaError {\n    return new SchemaError({\n      code: \"UNPROCESSABLE_ENTITY\",\n      message: parseZodErrorIssues(e.issues),\n      context: { raw: JSON.stringify(raw) },\n    });\n  }\n}\n"
  },
  {
    "path": "packages/error/src/utils.ts",
    "content": "import type { ZodIssue } from \"zod\";\n\nimport type { ErrorCode } from \"./error-code\";\n\nexport function statusToCode(status: number): ErrorCode {\n  switch (status) {\n    case 400:\n      return \"BAD_REQUEST\";\n    case 401:\n      return \"UNAUTHORIZED\";\n    case 402:\n      return \"PAYMENT_REQUIRED\";\n    case 403:\n      return \"FORBIDDEN\";\n    case 404:\n      return \"NOT_FOUND\";\n    case 405:\n      return \"METHOD_NOT_ALLOWED\";\n    case 409:\n      return \"CONFLICT\";\n    case 422:\n      return \"UNPROCESSABLE_ENTITY\";\n    case 500:\n      return \"INTERNAL_SERVER_ERROR\";\n    default:\n      return \"INTERNAL_SERVER_ERROR\";\n  }\n}\n\nexport function codeToStatus(code: ErrorCode) {\n  switch (code) {\n    case \"BAD_REQUEST\":\n      return 400;\n    case \"UNAUTHORIZED\":\n      return 401;\n    case \"PAYMENT_REQUIRED\":\n      return 402;\n    case \"FORBIDDEN\":\n      return 403;\n    case \"NOT_FOUND\":\n      return 404;\n    case \"METHOD_NOT_ALLOWED\":\n      return 405;\n    case \"CONFLICT\":\n      return 409;\n    case \"UNPROCESSABLE_ENTITY\":\n      return 422;\n    case \"INTERNAL_SERVER_ERROR\":\n      return 500;\n    default:\n      return 500;\n  }\n}\n\n// Props to cal.com: https://github.com/calcom/cal.com/blob/5d325495a9c30c5a9d89fc2adfa620b8fde9346e/packages/lib/server/getServerErrorFromUnknown.ts#L17\nexport function parseZodErrorIssues(issues: ZodIssue[]): string {\n  return issues\n    .map((i) =>\n      i.code === \"invalid_union\"\n        ? i.errors.map((ue) => parseZodErrorIssues(ue)).join(\"; \")\n        : i.code === \"unrecognized_keys\"\n          ? i.message\n          : `${i.path.length ? `${i.code} in '${i.path}': ` : \"\"}${i.message}`,\n    )\n    .join(\"; \");\n}\n\nexport function redactError<TError extends Error | unknown>(err: TError) {\n  if (!(err instanceof Error)) return err;\n  console.error(`Type of Error: ${err.constructor}`);\n}\n"
  },
  {
    "path": "packages/error/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\"]\n}\n"
  },
  {
    "path": "packages/header-analysis/package.json",
    "content": "{\n  \"name\": \"@openstatus/header-analysis\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {},\n  \"dependencies\": {},\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/header-analysis/src/index.ts",
    "content": "export * from \"./parser/cache-control\";\nexport * from \"./parser/cf-cache-status\";\nexport * from \"./parser/cf-ray\";\nexport * from \"./parser/fly-request-id\";\nexport * from \"./parser/x-vercel-cache\";\nexport * from \"./parser/x-vercel-id\";\n"
  },
  {
    "path": "packages/header-analysis/src/parser/cache-control.ts",
    "content": "interface CacheControlInfo {\n  directive: string; // unparsed directive\n  description: string;\n  name: string;\n  value?: number;\n}\n\nexport function parseCacheControlHeader(header: string): CacheControlInfo[] {\n  const cacheControlDirectives = header\n    .split(\",\")\n    .map((directive) => directive.trim());\n\n  const cacheControlInfo: CacheControlInfo[] = [];\n\n  // biome-ignore lint/complexity/noForEach: <explanation>\n  cacheControlDirectives.forEach((directive) => {\n    const parts = directive.split(\"=\");\n\n    const name = parts[0].trim();\n    const value = !Number.isNaN(Number(parts[1]))\n      ? Number(parts[1])\n      : undefined;\n    const description = getDirectiveDescription(name);\n\n    cacheControlInfo.push({ description, name, value, directive });\n  });\n\n  return cacheControlInfo;\n}\n\nfunction getDirectiveDescription(key: string): string {\n  switch (key.toLowerCase()) {\n    case \"max-age\":\n      return \"Specifies the maximum amount of time in seconds that a resource is considered fresh in a cache. After this time, the cache is required to revalidate the resource with the origin server.\";\n    case \"max-stale\":\n      return \"Indicates that a cache can serve a stale response even after its freshness lifetime has expired. Optionally, a value can be specified to indicate the maximum staleness allowed.\";\n    case \"min-fresh\":\n      return \"Specifies the minimum amount of time in seconds that a resource must be considered fresh. Caches should not serve a response that is older than this value without revalidating with the origin server.\";\n    case \"s-maxage\":\n      return \"Similar to max-age, but applies specifically to shared caches, such as proxy servers. Overrides max-age when present in shared caches.\";\n    case \"no-cache\":\n      return \"Forces caches to submit the request to the origin server for validation before releasing a cached copy. The response must be validated with the origin server before it can be used.\";\n    case \"no-store\":\n      return \"Instructs caches not to store any part of either the request or response. The content must be obtained from the origin server for each request.\";\n    case \"no-transform\":\n      return \"Specifies that intermediaries should not modify the content of the resource, such as by transcoding or compressing it.\";\n    case \"only-if-cached\":\n      return \"Indicates that a cache should respond only if the resource is available in the cache. It should not contact the origin server to retrieve the resource.\";\n    case \"must-revalidate\":\n      return \"Specifies that caches must revalidate stale responses with the origin server before using them.\";\n    case \"proxy-revalidate\":\n      return \"Similar to must-revalidate, but applies specifically to shared caches. It indicates that shared caches must revalidate stale responses with the origin server before using them.\";\n    case \"private\":\n      return \"Indicates that the response is intended for a single user and should not be cached by shared caches.\";\n    case \"public\":\n      return \"Specifies that the response may be cached by any cache, including both private and shared caches.\";\n    case \"immutable\":\n      return \"Indicates that the response body will not change over time. Caches can store immutable responses indefinitely.\";\n    case \"stale-while-revalidate\":\n      return \"Allows a cache to serve stale responses while asynchronously revalidating them with the origin server in the background.\";\n    case \"stale-if-error\":\n      return \"Allows a cache to serve stale responses if the origin server is unavailable or returns an error.\";\n    default:\n      return \"-\";\n  }\n}\n"
  },
  {
    "path": "packages/header-analysis/src/parser/cf-cache-status.ts",
    "content": "interface CfCacheStatusInfo {\n  description: string;\n  value: string;\n}\n\nexport function parseCfCacheStatus(header: string): CfCacheStatusInfo {\n  const description = getCacheDescription(header);\n  return { description, value: header };\n}\n\nfunction getCacheDescription(key: string): string {\n  switch (key.toUpperCase()) {\n    case \"HIT\":\n      return \"Your resource was found in Cloudflare’s cache. This means that it has been previously accessed from your original server and loaded into Cache. It has not expired.\";\n    case \"MISS\":\n      return \"Cloudflare looked for your resource in cache but did not find it. Cloudflare went back to your origin server to retrieve the resource. The next time this resource is accessed its status should be HIT.\";\n    case \"BYPASS\":\n      return \"Cloudflare has been instructed to not cache this asset. It has been served directly from the origin. This is usually because something like an existing NO-CACHE header is being respected.\";\n    case \"EXPIRED\":\n      return \"Cloudflare has previously retrieved this resource, but its cache has expired. Cloudflare will go back to the origin to retrieve this resource again. The next time this resource is accessed its status should be HIT.\";\n    case \"DYNAMIC\":\n      return \"This resource is not cached by default and there are no explicit settings configured to cache it. You will see this frequently when Cloudflare is handling a POST request. This request will always go to the origin.\";\n    default:\n      return \"-\";\n  }\n}\n"
  },
  {
    "path": "packages/header-analysis/src/parser/cf-ray.ts",
    "content": "// List of all Cloudflare data center taken by https://www.feitsui.com/en/article/26\nimport { regions } from \"../regions/cloudflare\";\nimport type { ParserReturn, Region } from \"../types\";\n\nexport function parseCfRay(header: string): ParserReturn<Region> {\n  const regex = /\\b([A-Z]{3})\\b/g;\n  const arr = header.match(regex);\n\n  if (!arr || !Array.isArray(arr) || arr.length === 0) {\n    return { status: \"failed\", error: new Error(\"Couldn't parse the header.\") };\n  }\n\n  const region = regions[arr[0]];\n  if (region) return { status: \"success\", data: region };\n\n  return {\n    status: \"failed\",\n    error: new Error(\n      `It seems like the data center '${arr[0]}' (iata) is not listed.`,\n    ),\n  };\n}\n"
  },
  {
    "path": "packages/header-analysis/src/parser/fly-request-id.ts",
    "content": "import { regions } from \"../regions/fly\";\nimport type { ParserReturn, Region } from \"../types\";\n\nexport function parseFlyRequestId(header: string): ParserReturn<Region> {\n  const regex = /\\b([a-z]{3})\\b/g;\n  const arr = header.match(regex);\n\n  if (!arr || arr.length === 0) {\n    return { status: \"failed\", error: new Error(\"Couldn't parse the header.\") };\n  }\n\n  const region = regions[arr[0]];\n  if (region) return { status: \"success\", data: region };\n\n  return {\n    status: \"failed\",\n    error: new Error(\n      `It seems like the data center '${arr[0]}' (iata) is not listed.`,\n    ),\n  };\n}\n"
  },
  {
    "path": "packages/header-analysis/src/parser/x-vercel-cache.ts",
    "content": "interface VercelCacheInfo {\n  description: string;\n  value: string;\n}\n\nexport function parseXVercelCache(header: string): VercelCacheInfo {\n  const description = getCacheDescription(header);\n  return { description, value: header };\n}\n\nfunction getCacheDescription(key: string): string {\n  switch (key.toUpperCase()) {\n    case \"MISS\":\n      return \"The response was not found in the edge cache and was fetched from the origin server.\";\n    case \"HIT\":\n      return \"The response was served from the edge cache.\";\n    case \"STALE\":\n      return \"The response was served from the edge cache. A background request to the origin server was made to update the content.\";\n    case \"PRERENDER\":\n      return \"The response was served from static storage.\";\n    case \"REVLIDATED\":\n      return \"The response was served from the origin server and the cache was refreshed due to an authorization from the user in the incoming request.\";\n    default:\n      return \"-\";\n  }\n}\n"
  },
  {
    "path": "packages/header-analysis/src/parser/x-vercel-id.ts",
    "content": "import { regions } from \"../regions/vercel\";\nimport type { ParserReturn, Region } from \"../types\";\n\nexport function parseXVercelId(header: string): ParserReturn<Region[]> {\n  const regex = /([a-z]{3}[0-9])+:+/g;\n\n  const arr = header.match(regex);\n  if (!arr || !arr.length) {\n    return { status: \"failed\", error: new Error(\"Couldn't parse the header.\") };\n  }\n\n  const data = arr.map((r) => {\n    const regionId = r.replace(/:+/, \"\");\n    return regions[regionId];\n  });\n\n  return { status: \"success\", data };\n}\n"
  },
  {
    "path": "packages/header-analysis/src/regions/cloudflare.ts",
    "content": "import type { Region } from \"../types\";\n\n// REMINDER: nono-official data center list\n// https://www.feitsui.com/en/article/26\nexport const regions: Record<string, Region> = {\n  CGB: { code: \"CGB\", location: \"Cuiabá, Brazil\", flag: \"🇧🇷\" },\n  COR: { code: \"COR\", location: \"Córdoba, Argentina\", flag: \"🇦🇷\" },\n  BTS: { code: \"BTS\", location: \"Bratislava, Slovakia\", flag: \"🇸🇰\" },\n  KHH: { code: \"KHH\", location: \"Kaohsiung, Taiwan\", flag: \"🇹🇼\" },\n  GND: { code: \"GND\", location: \"Grenada\", flag: \"🇬🇩\" },\n  NVT: { code: \"NVT\", location: \"Navegantes, Brazil\", flag: \"🇧🇷\" },\n  ISU: { code: \"ISU\", location: \"Sulaimaniyah, Iraq\", flag: \"🇮🇶\" },\n  PAP: { code: \"PAP\", location: \"Port-au-Prince, Haiti\", flag: \"🇭🇹\" },\n  KLD: { code: \"KLD\", location: \"Tver, Russia\", flag: \"🇷🇺\" },\n  REC: { code: \"REC\", location: \"Recife, Brazil\", flag: \"🇧🇷\" },\n  FSD: { code: \"FSD\", location: \"Sioux Falls, USA\", flag: \"🇺🇸\" },\n  ADB: { code: \"ADB\", location: \"Izmir, Turkey\", flag: \"🇹🇷\" },\n  CHC: { code: \"CHC\", location: \"Christchurch, New Zealand\", flag: \"🇳🇿\" },\n  UDI: { code: \"UDI\", location: \"Uberlândia, Brazil\", flag: \"🇧🇷\" },\n  PPT: { code: \"PPT\", location: \"Papeete, French Polynesia\", flag: \"🇵🇫\" },\n  KIN: { code: \"KIN\", location: \"Kingston, Jamaica\", flag: \"🇯🇲\" },\n  STR: { code: \"STR\", location: \"Stuttgart, Germany\", flag: \"🇩🇪\" },\n  COK: { code: \"COK\", location: \"Kochi, India\", flag: \"🇮🇳\" },\n  KJA: { code: \"KJA\", location: \"Krasnoyarsk, Russia\", flag: \"🇷🇺\" },\n  AUS: { code: \"AUS\", location: \"Austin, USA\", flag: \"🇺🇸\" },\n  ORN: { code: \"ORN\", location: \"Oran, Algeria\", flag: \"🇩🇿\" },\n  ACC: { code: \"ACC\", location: \"Accra, Ghana\", flag: \"🇬🇭\" },\n  GDL: { code: \"GDL\", location: \"Guadalajara, Mexico\", flag: \"🇲🇽\" },\n  SJK: { code: \"SJK\", location: \"São José dos Campos, Brazil\", flag: \"🇧🇷\" },\n  AAE: { code: \"AAE\", location: \"Annaba, Algeria\", flag: \"🇩🇿\" },\n  MDL: { code: \"MDL\", location: \"Mandalay, Myanmar\", flag: \"🇲🇲\" },\n  DPS: { code: \"DPS\", location: \"Denpasar, Indonesia\", flag: \"🇮🇩\" },\n  VIX: { code: \"VIX\", location: \"Vitória, Brazil\", flag: \"🇧🇷\" },\n  FUK: { code: \"FUK\", location: \"Fukuoka, Japan\", flag: \"🇯🇵\" },\n  CNN: { code: \"CNN\", location: \"Chengdu, China\", flag: \"🇨🇳\" },\n  LYS: { code: \"LYS\", location: \"Lyon, France\", flag: \"🇫🇷\" },\n  XAP: { code: \"XAP\", location: \"Chapecó, Brazil\", flag: \"🇧🇷\" },\n  CAI: { code: \"CAI\", location: \"Cairo, Egypt\", flag: \"🇪🇬\" },\n  RDU: { code: \"RDU\", location: \"Raleigh-Durham, USA\", flag: \"🇺🇸\" },\n  CAW: { code: \"CAW\", location: \"Campos dos Goytacazes, Brazil\", flag: \"🇧🇷\" },\n  OKC: { code: \"OKC\", location: \"Oklahoma City, USA\", flag: \"🇺🇸\" },\n  SDQ: {\n    code: \"SDQ\",\n    location: \"Santo Domingo, Dominican Republic\",\n    flag: \"🇩🇴\",\n  },\n  OUA: { code: \"OUA\", location: \"Ouagadougou, Burkina Faso\", flag: \"🇧🇫\" },\n  YHZ: { code: \"YHZ\", location: \"Halifax, Canada\", flag: \"🇨🇦\" },\n  ANC: { code: \"ANC\", location: \"Anchorage, USA\", flag: \"🇺🇸\" },\n  LPB: { code: \"LPB\", location: \"La Paz, Bolivia\", flag: \"🇧🇴\" },\n  SUV: { code: \"SUV\", location: \"Suva, Fiji\", flag: \"🇫🇯\" },\n  BGR: { code: \"BGR\", location: \"Bangor, USA\", flag: \"🇺🇸\" },\n  SJU: { code: \"SJU\", location: \"San Juan, Puerto Rico\", flag: \"🇵🇷\" },\n  BBI: { code: \"BBI\", location: \"Bhubaneswar, India\", flag: \"🇮🇳\" },\n  ALG: { code: \"ALG\", location: \"Algiers, Algeria\", flag: \"🇩🇿\" },\n  LAD: { code: \"LAD\", location: \"Luanda, Angola\", flag: \"🇦🇴\" },\n  EZE: { code: \"EZE\", location: \"Buenos Aires, Argentina\", flag: \"🇦🇷\" },\n  NQN: { code: \"NQN\", location: \"Neuquén, Argentina\", flag: \"🇦🇷\" },\n  EVN: { code: \"EVN\", location: \"Yerevan, Armenia\", flag: \"🇦🇲\" },\n  ADL: { code: \"ADL\", location: \"Adelaide, Australia\", flag: \"🇦🇺\" },\n  BNE: { code: \"BNE\", location: \"Brisbane, Australia\", flag: \"🇦🇺\" },\n  CBR: { code: \"CBR\", location: \"Canberra, Australia\", flag: \"🇦🇺\" },\n  HBA: { code: \"HBA\", location: \"Hobart, Australia\", flag: \"🇦🇺\" },\n  MEL: { code: \"MEL\", location: \"Melbourne, Australia\", flag: \"🇦🇺\" },\n  PER: { code: \"PER\", location: \"Perth, Australia\", flag: \"🇦🇺\" },\n  SYD: { code: \"SYD\", location: \"Sydney, Australia\", flag: \"🇦🇺\" },\n  VIE: { code: \"VIE\", location: \"Vienna, Austria\", flag: \"🇦🇹\" },\n  LLK: { code: \"LLK\", location: \"Luleå, Sweden\", flag: \"🇸🇪\" },\n  GYD: { code: \"GYD\", location: \"Baku, Azerbaijan\", flag: \"🇦🇿\" },\n  BAH: { code: \"BAH\", location: \"Manama, Bahrain\", flag: \"🇧🇭\" },\n  CGP: { code: \"CGP\", location: \"Chittagong, Bangladesh\", flag: \"🇧🇩\" },\n  DAC: { code: \"DAC\", location: \"Dhaka, Bangladesh\", flag: \"🇧🇩\" },\n  JSR: { code: \"JSR\", location: \"Jessore, Bangladesh\", flag: \"🇧🇩\" },\n  MSQ: { code: \"MSQ\", location: \"Minsk, Belarus\", flag: \"🇧🇾\" },\n  BRU: { code: \"BRU\", location: \"Brussels, Belgium\", flag: \"🇧🇪\" },\n  PBH: { code: \"PBH\", location: \"Paro, Bhutan\", flag: \"🇧🇹\" },\n  GBE: { code: \"GBE\", location: \"Gaborone, Botswana\", flag: \"🇧🇼\" },\n  QWJ: { code: \"QWJ\", location: \"Bouake, Ivory Coast\", flag: \"🇨🇮\" },\n  CNF: { code: \"CNF\", location: \"Belo Horizonte, Brazil\", flag: \"🇧🇷\" },\n  BEL: { code: \"BEL\", location: \"Belém, Brazil\", flag: \"🇧🇷\" },\n  BNU: { code: \"BNU\", location: \"Blumenau, Brazil\", flag: \"🇧🇷\" },\n  BSB: { code: \"BSB\", location: \"Brasília, Brazil\", flag: \"🇧🇷\" },\n  VCP: { code: \"VCP\", location: \"Campinas, Brazil\", flag: \"🇧🇷\" },\n  CFC: { code: \"CFC\", location: \"Cafelândia, Brazil\", flag: \"🇧🇷\" },\n  CWB: { code: \"CWB\", location: \"Curitiba, Brazil\", flag: \"🇧🇷\" },\n  FLN: { code: \"FLN\", location: \"Florianópolis, Brazil\", flag: \"🇧🇷\" },\n  FOR: { code: \"FOR\", location: \"Fortaleza, Brazil\", flag: \"🇧🇷\" },\n  GYN: { code: \"GYN\", location: \"Goiânia, Brazil\", flag: \"🇧🇷\" },\n  ITJ: { code: \"ITJ\", location: \"Itajaí, Brazil\", flag: \"🇧🇷\" },\n  JOI: { code: \"JOI\", location: \"Joinville, Brazil\", flag: \"🇧🇷\" },\n  JDO: { code: \"JDO\", location: \"Juazeiro do Norte, Brazil\", flag: \"🇧🇷\" },\n  MAO: { code: \"MAO\", location: \"Manaus, Brazil\", flag: \"🇧🇷\" },\n  POA: { code: \"POA\", location: \"Porto Alegre, Brazil\", flag: \"🇧🇷\" },\n  RAO: { code: \"RAO\", location: \"Ribeirão Preto, Brazil\", flag: \"🇧🇷\" },\n  GIG: { code: \"GIG\", location: \"Rio de Janeiro, Brazil\", flag: \"🇧🇷\" },\n  SSA: { code: \"SSA\", location: \"Salvador, Brazil\", flag: \"🇧🇷\" },\n  SOD: { code: \"SOD\", location: \"Sorocaba, Brazil\", flag: \"🇧🇷\" },\n  SJP: { code: \"SJP\", location: \"São José do Rio Preto, Brazil\", flag: \"🇧🇷\" },\n  GRU: { code: \"GRU\", location: \"São Paulo, Brazil\", flag: \"🇧🇷\" },\n  BWN: { code: \"BWN\", location: \"Bandar Seri Begawan, Brunei\", flag: \"🇧🇳\" },\n  SOF: { code: \"SOF\", location: \"Sofia, Bulgaria\", flag: \"🇧🇬\" },\n  PNH: { code: \"PNH\", location: \"Phnom Penh, Cambodia\", flag: \"🇰🇭\" },\n  YYC: { code: \"YYC\", location: \"Calgary, Canada\", flag: \"🇨🇦\" },\n  YUL: { code: \"YUL\", location: \"Montreal, Canada\", flag: \"🇨🇦\" },\n  YOW: { code: \"YOW\", location: \"Ottawa, Canada\", flag: \"🇨🇦\" },\n  YXE: { code: \"YXE\", location: \"Saskatoon, Canada\", flag: \"🇨🇦\" },\n  YYZ: { code: \"YYZ\", location: \"Toronto, Canada\", flag: \"🇨🇦\" },\n  YVR: { code: \"YVR\", location: \"Vancouver, Canada\", flag: \"🇨🇦\" },\n  YWG: { code: \"YWG\", location: \"Winnipeg, Canada\", flag: \"🇨🇦\" },\n  ARI: { code: \"ARI\", location: \"Arica, Chile\", flag: \"🇨🇱\" },\n  SCL: { code: \"SCL\", location: \"Santiago, Chile\", flag: \"🇨🇱\" },\n  HKG: { code: \"HKG\", location: \"Hong Kong, Hong Kong\", flag: \"🇭🇰\" },\n  MFM: { code: \"MFM\", location: \"Macau, Macau\", flag: \"🇲🇴\" },\n  TPE: { code: \"TPE\", location: \"Taipei, Taiwan\", flag: \"🇹🇼\" },\n  BOG: { code: \"BOG\", location: \"Bogotá, Colombia\", flag: \"🇨🇴\" },\n  MDE: { code: \"MDE\", location: \"Medellín, Colombia\", flag: \"🇨🇴\" },\n  SJO: { code: \"SJO\", location: \"San José, Costa Rica\", flag: \"🇨🇷\" },\n  ZAG: { code: \"ZAG\", location: \"Zagreb, Croatia\", flag: \"🇭🇷\" },\n  LCA: { code: \"LCA\", location: \"Larnaca, Cyprus\", flag: \"🇨🇾\" },\n  PRG: { code: \"PRG\", location: \"Prague, Czech Republic\", flag: \"🇨🇿\" },\n  CPH: { code: \"CPH\", location: \"Copenhagen, Denmark\", flag: \"🇩🇰\" },\n  JIB: { code: \"JIB\", location: \"Djibouti\", flag: \"🇩🇯\" },\n  GYE: { code: \"GYE\", location: \"Guayaquil, Ecuador\", flag: \"🇪🇨\" },\n  UIO: { code: \"UIO\", location: \"Quito, Ecuador\", flag: \"🇪🇨\" },\n  TLL: { code: \"TLL\", location: \"Tallinn, Estonia\", flag: \"🇪🇪\" },\n  HEL: { code: \"HEL\", location: \"Helsinki, Finland\", flag: \"🇫🇮\" },\n  MRS: { code: \"MRS\", location: \"Marseille, France\", flag: \"🇫🇷\" },\n  CDG: { code: \"CDG\", location: \"Paris, France\", flag: \"🇫🇷\" },\n  RUN: { code: \"RUN\", location: \"Saint-Denis, Réunion\", flag: \"🇷🇪\" },\n  TBS: { code: \"TBS\", location: \"Tbilisi, Georgia\", flag: \"🇬🇪\" },\n  TXL: { code: \"TXL\", location: \"Berlin, Germany\", flag: \"🇩🇪\" },\n  DUS: { code: \"DUS\", location: \"Düsseldorf, Germany\", flag: \"🇩🇪\" },\n  FRA: { code: \"FRA\", location: \"Frankfurt, Germany\", flag: \"🇩🇪\" },\n  HAM: { code: \"HAM\", location: \"Hamburg, Germany\", flag: \"🇩🇪\" },\n  MUC: { code: \"MUC\", location: \"Munich, Germany\", flag: \"🇩🇪\" },\n  ATH: { code: \"ATH\", location: \"Athens, Greece\", flag: \"🇬🇷\" },\n  SKG: { code: \"SKG\", location: \"Thessaloniki, Greece\", flag: \"🇬🇷\" },\n  GUM: { code: \"GUM\", location: \"Hagåtña, Guam\", flag: \"🇬🇺\" },\n  GUA: { code: \"GUA\", location: \"Guatemala City, Guatemala\", flag: \"🇬🇹\" },\n  GEO: { code: \"GEO\", location: \"Georgetown, Guyana\", flag: \"🇬🇾\" },\n  TGU: { code: \"TGU\", location: \"Tegucigalpa, Honduras\", flag: \"🇭🇳\" },\n  BUD: { code: \"BUD\", location: \"Budapest, Hungary\", flag: \"🇭🇺\" },\n  KEF: { code: \"KEF\", location: \"Reykjavík, Iceland\", flag: \"🇮🇸\" },\n  AMD: { code: \"AMD\", location: \"Ahmedabad, India\", flag: \"🇮🇳\" },\n  BLR: { code: \"BLR\", location: \"Bengaluru, India\", flag: \"🇮🇳\" },\n  IXC: { code: \"IXC\", location: \"Chandigarh, India\", flag: \"🇮🇳\" },\n  MAA: { code: \"MAA\", location: \"Chennai, India\", flag: \"🇮🇳\" },\n  HYD: { code: \"HYD\", location: \"Hyderabad, India\", flag: \"🇮🇳\" },\n  KNU: { code: \"KNU\", location: \"Kanpur, India\", flag: \"🇮🇳\" },\n  CCU: { code: \"CCU\", location: \"Kolkata, India\", flag: \"🇮🇳\" },\n  BOM: { code: \"BOM\", location: \"Mumbai, India\", flag: \"🇮🇳\" },\n  NAG: { code: \"NAG\", location: \"Nagpur, India\", flag: \"🇮🇳\" },\n  DEL: { code: \"DEL\", location: \"New Delhi, India\", flag: \"🇮🇳\" },\n  PAT: { code: \"PAT\", location: \"Patna, India\", flag: \"🇮🇳\" },\n  CGK: { code: \"CGK\", location: \"Jakarta, Indonesia\", flag: \"🇮🇩\" },\n  JOG: { code: \"JOG\", location: \"Yogyakarta, Indonesia\", flag: \"🇮🇩\" },\n  BGW: { code: \"BGW\", location: \"Baghdad, Iraq\", flag: \"🇮🇶\" },\n  BSR: { code: \"BSR\", location: \"Basra, Iraq\", flag: \"🇮🇶\" },\n  EBL: { code: \"EBL\", location: \"Erbil, Iraq\", flag: \"🇮🇶\" },\n  NJF: { code: \"NJF\", location: \"Najaf, Iraq\", flag: \"🇮🇶\" },\n  XNH: { code: \"XNH\", location: \"Nouadhibou, Mauritania\", flag: \"🇲🇷\" },\n  ORK: { code: \"ORK\", location: \"Cork, Ireland\", flag: \"🇮🇪\" },\n  DUB: { code: \"DUB\", location: \"Dublin, Ireland\", flag: \"🇮🇪\" },\n  HFA: { code: \"HFA\", location: \"Haifa, Israel\", flag: \"🇮🇱\" },\n  TLV: { code: \"TLV\", location: \"Tel-Aviv, Israel\", flag: \"🇮🇱\" },\n  MXP: { code: \"MXP\", location: \"Milan, Italy\", flag: \"🇮🇹\" },\n  PMO: { code: \"PMO\", location: \"Palermo, Italy\", flag: \"🇮🇹\" },\n  FCO: { code: \"FCO\", location: \"Rome, Italy\", flag: \"🇮🇹\" },\n  OKA: { code: \"OKA\", location: \"Okinawa, Japan\", flag: \"🇯🇵\" },\n  KIX: { code: \"KIX\", location: \"Osaka, Japan\", flag: \"🇯🇵\" },\n  NRT: { code: \"NRT\", location: \"Tokyo, Japan\", flag: \"🇯🇵\" },\n  AMM: { code: \"AMM\", location: \"Amman, Jordan\", flag: \"🇯🇴\" },\n  ALA: { code: \"ALA\", location: \"Almaty, Kazakhstan\", flag: \"🇰🇿\" },\n  MBA: { code: \"MBA\", location: \"Mombasa, Kenya\", flag: \"🇰🇪\" },\n  NBO: { code: \"NBO\", location: \"Nairobi, Kenya\", flag: \"🇰🇪\" },\n  KWI: { code: \"KWI\", location: \"Kuwait City, Kuwait\", flag: \"🇰🇼\" },\n  VTE: { code: \"VTE\", location: \"Vientiane, Laos\", flag: \"🇱🇦\" },\n  RIX: { code: \"RIX\", location: \"Riga, Latvia\", flag: \"🇱🇻\" },\n  VNO: { code: \"VNO\", location: \"Vilnius, Lithuania\", flag: \"🇱🇹\" },\n  LUX: { code: \"LUX\", location: \"Luxembourg\", flag: \"🇱🇺\" },\n  TNR: { code: \"TNR\", location: \"Antananarivo, Madagascar\", flag: \"🇲🇬\" },\n  JHB: { code: \"JHB\", location: \"Johor Bahru, Malaysia\", flag: \"🇲🇾\" },\n  KUL: { code: \"KUL\", location: \"Kuala Lumpur, Malaysia\", flag: \"🇲🇾\" },\n  MLE: { code: \"MLE\", location: \"Malé, Maldives\", flag: \"🇲🇻\" },\n  MRU: { code: \"MRU\", location: \"Port Louis, Mauritius\", flag: \"🇲🇺\" },\n  MEX: { code: \"MEX\", location: \"Mexico City, Mexico\", flag: \"🇲🇽\" },\n  QRO: { code: \"QRO\", location: \"Querétaro, Mexico\", flag: \"🇲🇽\" },\n  KIV: { code: \"KIV\", location: \"Chișinău, Moldova\", flag: \"🇲🇩\" },\n  ULN: { code: \"ULN\", location: \"Ulaanbaatar, Mongolia\", flag: \"🇲🇳\" },\n  CMN: { code: \"CMN\", location: \"Casablanca, Morocco\", flag: \"🇲🇦\" },\n  MPM: { code: \"MPM\", location: \"Maputo, Mozambique\", flag: \"🇲🇿\" },\n  RGN: { code: \"RGN\", location: \"Yangon, Myanmar\", flag: \"🇲🇲\" },\n  KTM: { code: \"KTM\", location: \"Kathmandu, Nepal\", flag: \"🇳🇵\" },\n  AMS: { code: \"AMS\", location: \"Amsterdam, Netherlands\", flag: \"🇳🇱\" },\n  NOU: { code: \"NOU\", location: \"Nouméa, New Caledonia\", flag: \"🇳🇨\" },\n  AKL: { code: \"AKL\", location: \"Auckland, New Zealand\", flag: \"🇳🇿\" },\n  LOS: { code: \"LOS\", location: \"Lagos, Nigeria\", flag: \"🇳🇬\" },\n  OSL: { code: \"OSL\", location: \"Oslo, Norway\", flag: \"🇳🇴\" },\n  MCT: { code: \"MCT\", location: \"Muscat, Oman\", flag: \"🇴🇲\" },\n  ISB: { code: \"ISB\", location: \"Islamabad, Pakistan\", flag: \"🇵🇰\" },\n  KHI: { code: \"KHI\", location: \"Karachi, Pakistan\", flag: \"🇵🇰\" },\n  LHE: { code: \"LHE\", location: \"Lahore, Pakistan\", flag: \"🇵🇰\" },\n  ZDM: { code: \"ZDM\", location: \"Zahedan, Iran\", flag: \"🇮🇷\" },\n  PTY: { code: \"PTY\", location: \"Panama City, Panama\", flag: \"🇵🇦\" },\n  ASU: { code: \"ASU\", location: \"Asunción, Paraguay\", flag: \"🇵🇾\" },\n  LIM: { code: \"LIM\", location: \"Lima, Peru\", flag: \"🇵🇪\" },\n  CGY: { code: \"CGY\", location: \"Cagayan de Oro, Philippines\", flag: \"🇵🇭\" },\n  CEB: { code: \"CEB\", location: \"Cebu, Philippines\", flag: \"🇵🇭\" },\n  MNL: { code: \"MNL\", location: \"Manila, Philippines\", flag: \"🇵🇭\" },\n  WAW: { code: \"WAW\", location: \"Warsaw, Poland\", flag: \"🇵🇱\" },\n  LIS: { code: \"LIS\", location: \"Lisbon, Portugal\", flag: \"🇵🇹\" },\n  DOH: { code: \"DOH\", location: \"Doha, Qatar\", flag: \"🇶🇦\" },\n  OTP: { code: \"OTP\", location: \"Bucharest, Romania\", flag: \"🇷🇴\" },\n  LED: { code: \"LED\", location: \"Saint Petersburg, Russia\", flag: \"🇷🇺\" },\n  DME: { code: \"DME\", location: \"Moscow, Russia\", flag: \"🇷🇺\" },\n  KZN: { code: \"KZN\", location: \"Kazan, Russia\", flag: \"🇷🇺\" },\n  KRR: { code: \"KRR\", location: \"Krasnodar, Russia\", flag: \"🇷🇺\" },\n  SVX: { code: \"SVX\", location: \"Yekaterinburg, Russia\", flag: \"🇷🇺\" },\n  KGL: { code: \"KGL\", location: \"Kigali, Rwanda\", flag: \"🇷🇼\" },\n  JED: { code: \"JED\", location: \"Jeddah, Saudi Arabia\", flag: \"🇸🇦\" },\n  RUH: { code: \"RUH\", location: \"Riyadh, Saudi Arabia\", flag: \"🇸🇦\" },\n  DKR: { code: \"DKR\", location: \"Dakar, Senegal\", flag: \"🇸🇳\" },\n  BEG: { code: \"BEG\", location: \"Belgrade, Serbia\", flag: \"🇷🇸\" },\n  SGP: { code: \"SGP\", location: \"Singapore\", flag: \"🇸🇬\" },\n  LJU: { code: \"LJU\", location: \"Ljubljana, Slovenia\", flag: \"🇸🇮\" },\n  CPT: { code: \"CPT\", location: \"Cape Town, South Africa\", flag: \"🇿🇦\" },\n  DUR: { code: \"DUR\", location: \"Durban, South Africa\", flag: \"🇿🇦\" },\n  JNB: { code: \"JNB\", location: \"Johannesburg, South Africa\", flag: \"🇿🇦\" },\n  ICN: { code: \"ICN\", location: \"Seoul, South Korea\", flag: \"🇰🇷\" },\n  MAD: { code: \"MAD\", location: \"Madrid, Spain\", flag: \"🇪🇸\" },\n  BCN: { code: \"BCN\", location: \"Barcelona, Spain\", flag: \"🇪🇸\" },\n  GOT: { code: \"GOT\", location: \"Gothenburg, Sweden\", flag: \"🇸🇪\" },\n  ARN: { code: \"ARN\", location: \"Stockholm, Sweden\", flag: \"🇸🇪\" },\n  BSL: { code: \"BSL\", location: \"Basel, Switzerland\", flag: \"🇨🇭\" },\n  GVA: { code: \"GVA\", location: \"Geneva, Switzerland\", flag: \"🇨🇭\" },\n  ZRH: { code: \"ZRH\", location: \"Zürich, Switzerland\", flag: \"🇨🇭\" },\n  DAR: { code: \"DAR\", location: \"Dar es Salaam, Tanzania\", flag: \"🇹🇿\" },\n  BKK: { code: \"BKK\", location: \"Bangkok, Thailand\", flag: \"🇹🇭\" },\n  IST: { code: \"IST\", location: \"Istanbul, Turkey\", flag: \"🇹🇷\" },\n  TUN: { code: \"TUN\", location: \"Tunis, Tunisia\", flag: \"🇹🇳\" },\n  KBP: { code: \"KBP\", location: \"Kyiv, Ukraine\", flag: \"🇺🇦\" },\n  DXB: { code: \"DXB\", location: \"Dubai, United Arab Emirates\", flag: \"🇦🇪\" },\n  EDI: { code: \"EDI\", location: \"Edinburgh, United Kingdom\", flag: \"🇬🇧\" },\n  LHR: { code: \"LHR\", location: \"London, United Kingdom\", flag: \"🇬🇧\" },\n  MAN: { code: \"MAN\", location: \"Manchester, United Kingdom\", flag: \"🇬🇧\" },\n  IAD: { code: \"IAD\", location: \"Washington, USA\", flag: \"🇺🇸\" },\n  ATL: { code: \"ATL\", location: \"Atlanta, USA\", flag: \"🇺🇸\" },\n  BOS: { code: \"BOS\", location: \"Boston, USA\", flag: \"🇺🇸\" },\n  BUF: { code: \"BUF\", location: \"Buffalo, USA\", flag: \"🇺🇸\" },\n  CLT: { code: \"CLT\", location: \"Charlotte, USA\", flag: \"🇺🇸\" },\n  ORD: { code: \"ORD\", location: \"Chicago, USA\", flag: \"🇺🇸\" },\n  CMH: { code: \"CMH\", location: \"Columbus, USA\", flag: \"🇺🇸\" },\n  DFW: { code: \"DFW\", location: \"Dallas, USA\", flag: \"🇺🇸\" },\n  DEN: { code: \"DEN\", location: \"Denver, USA\", flag: \"🇺🇸\" },\n  DTW: { code: \"DTW\", location: \"Detroit, USA\", flag: \"🇺🇸\" },\n  HNL: { code: \"HNL\", location: \"Honolulu, USA\", flag: \"🇺🇸\" },\n  IAH: { code: \"IAH\", location: \"Houston, USA\", flag: \"🇺🇸\" },\n  JAX: { code: \"JAX\", location: \"Jacksonville, USA\", flag: \"🇺🇸\" },\n  MCI: { code: \"MCI\", location: \"Kansas City, USA\", flag: \"🇺🇸\" },\n  LAS: { code: \"LAS\", location: \"Las Vegas, USA\", flag: \"🇺🇸\" },\n  LAX: { code: \"LAX\", location: \"Los Angeles, USA\", flag: \"🇺🇸\" },\n  MFE: { code: \"MFE\", location: \"McAllen, USA\", flag: \"🇺🇸\" },\n  MEM: { code: \"MEM\", location: \"Memphis, USA\", flag: \"🇺🇸\" },\n  MIA: { code: \"MIA\", location: \"Miami, USA\", flag: \"🇺🇸\" },\n  MSP: { code: \"MSP\", location: \"Minneapolis, USA\", flag: \"🇺🇸\" },\n  MGM: { code: \"MGM\", location: \"Montgomery, USA\", flag: \"🇺🇸\" },\n  BNA: { code: \"BNA\", location: \"Nashville, USA\", flag: \"🇺🇸\" },\n  EWR: { code: \"EWR\", location: \"Newark, USA\", flag: \"🇺🇸\" },\n  OMA: { code: \"OMA\", location: \"Omaha, USA\", flag: \"🇺🇸\" },\n  PHL: { code: \"PHL\", location: \"Philadelphia, USA\", flag: \"🇺🇸\" },\n  PHX: { code: \"PHX\", location: \"Phoenix, USA\", flag: \"🇺🇸\" },\n  PIT: { code: \"PIT\", location: \"Pittsburgh, USA\", flag: \"🇺🇸\" },\n  PDX: { code: \"PDX\", location: \"Portland, USA\", flag: \"🇺🇸\" },\n  RIC: { code: \"RIC\", location: \"Richmond, USA\", flag: \"🇺🇸\" },\n  SMF: { code: \"SMF\", location: \"Sacramento, USA\", flag: \"🇺🇸\" },\n  SLC: { code: \"SLC\", location: \"Salt Lake City, USA\", flag: \"🇺🇸\" },\n  SAN: { code: \"SAN\", location: \"San Diego, USA\", flag: \"🇺🇸\" },\n  SFO: { code: \"SFO\", location: \"San Francisco, USA\", flag: \"🇺🇸\" },\n  SJC: { code: \"SJC\", location: \"San Jose, USA\", flag: \"🇺🇸\" },\n  SEA: { code: \"SEA\", location: \"Seattle, USA\", flag: \"🇺🇸\" },\n  IND: { code: \"IND\", location: \"Indianapolis, USA\", flag: \"🇺🇸\" },\n  STL: { code: \"STL\", location: \"St. Louis, USA\", flag: \"🇺🇸\" },\n  TLH: { code: \"TLH\", location: \"Tallahassee, USA\", flag: \"🇺🇸\" },\n  TPA: { code: \"TPA\", location: \"Tampa, USA\", flag: \"🇺🇸\" },\n  TAS: { code: \"TAS\", location: \"Tashkent, Uzbekistan\", flag: \"🇺🇿\" },\n  HAN: { code: \"HAN\", location: \"Hanoi, Vietnam\", flag: \"🇻🇳\" },\n  SGN: { code: \"SGN\", location: \"Ho Chi Minh City, Vietnam\", flag: \"🇻🇳\" },\n  HRE: { code: \"HRE\", location: \"Harare, Zimbabwe\", flag: \"🇿🇼\" },\n};\n"
  },
  {
    "path": "packages/header-analysis/src/regions/fly.ts",
    "content": "import type { Region } from \"../types\";\n\n// https://fly.io/docs/reference/regions/\nexport const regions: Record<string, Region> = {\n  ams: { code: \"ams\", location: \"Amsterdam, Netherlands\", flag: \"🇳🇱\" },\n  arn: { code: \"arn\", location: \"Stockholm, Sweden\", flag: \"🇸🇪\" },\n  atl: { code: \"atl\", location: \"Atlanta, Georgia (US)\", flag: \"🇺🇸\" },\n  bog: { code: \"bog\", location: \"Bogotá, Colombia\", flag: \"🇨🇴\" },\n  bom: { code: \"bom\", location: \"Mumbai, India\", flag: \"🇮🇳\" },\n  bos: { code: \"bos\", location: \"Boston, Massachusetts (US)\", flag: \"🇺🇸\" },\n  cdg: { code: \"cdg\", location: \"Paris, France\", flag: \"🇫🇷\" },\n  den: { code: \"den\", location: \"Denver, Colorado (US)\", flag: \"🇺🇸\" },\n  dfw: { code: \"dfw\", location: \"Dallas, Texas (US)\", flag: \"🇺🇸\" },\n  ewr: { code: \"ewr\", location: \"Secaucus, NJ (US)\", flag: \"🇺🇸\" },\n  eze: { code: \"eze\", location: \"Ezeiza, Argentina\", flag: \"🇦🇷\" },\n  fra: { code: \"fra\", location: \"Frankfurt, Germany\", flag: \"🇩🇪\" },\n  gdl: { code: \"gdl\", location: \"Guadalajara, Mexico\", flag: \"🇲🇽\" },\n  gig: { code: \"gig\", location: \"Rio de Janeiro, Brazil\", flag: \"🇧🇷\" },\n  gru: { code: \"gru\", location: \"Sao Paulo, Brazil\", flag: \"🇧🇷\" },\n  hkg: { code: \"hkg\", location: \"Hong Kong, Hong Kong\", flag: \"🇭🇰\" },\n  iad: { code: \"iad\", location: \"Ashburn, Virginia (US)\", flag: \"🇺🇸\" },\n  jnb: { code: \"jnb\", location: \"Johannesburg, South Africa\", flag: \"🇿🇦\" },\n  lax: { code: \"lax\", location: \"Los Angeles, California (US)\", flag: \"🇺🇸\" },\n  lhr: { code: \"lhr\", location: \"London, United Kingdom\", flag: \"🇬🇧\" },\n  mad: { code: \"mad\", location: \"Madrid, Spain\", flag: \"🇪🇸\" },\n  mia: { code: \"mia\", location: \"Miami, Florida (US)\", flag: \"🇺🇸\" },\n  nrt: { code: \"nrt\", location: \"Tokyo, Japan\", flag: \"🇯🇵\" },\n  ord: { code: \"ord\", location: \"Chicago, Illinois (US)\", flag: \"🇺🇸\" },\n  otp: { code: \"otp\", location: \"Bucharest, Romania\", flag: \"🇷🇴\" },\n  phx: { code: \"phx\", location: \"Phoenix, Arizona (US)\", flag: \"🇺🇸\" },\n  qro: { code: \"qro\", location: \"Querétaro, Mexico\", flag: \"🇲🇽\" },\n  scl: { code: \"scl\", location: \"Santiago, Chile\", flag: \"🇨🇱\" },\n  sea: { code: \"sea\", location: \"Seattle, Washington (US)\", flag: \"🇺🇸\" },\n  sin: { code: \"sin\", location: \"Singapore, Singapore\", flag: \"🇸🇬\" },\n  sjc: { code: \"sjc\", location: \"San Jose, California (US)\", flag: \"🇺🇸\" },\n  syd: { code: \"syd\", location: \"Sydney, Australia\", flag: \"🇦🇺\" },\n  waw: { code: \"waw\", location: \"Warsaw, Poland\", flag: \"🇵🇱\" },\n  yul: { code: \"yul\", location: \"Montreal, Canada\", flag: \"🇨🇦\" },\n  yyz: { code: \"yyz\", location: \"Toronto, Canada\", flag: \"🇨🇦\" },\n};\n"
  },
  {
    "path": "packages/header-analysis/src/regions/vercel.ts",
    "content": "import type { Region } from \"../types\";\n\n// https://vercel.com/docs/edge-network/regions\nexport const regions: Record<string, Region> = {\n  arn1: { code: \"arn1\", location: \"Stockholm, Sweden\", flag: \"🇸🇪\" },\n  bom1: { code: \"bom1\", location: \"Mumbai, India\", flag: \"🇮🇳\" },\n  cdg1: { code: \"cdg1\", location: \"Paris, France\", flag: \"🇫🇷\" },\n  cle1: { code: \"cle1\", location: \"Cleveland, USA\", flag: \"🇺🇸\" },\n  cpt1: { code: \"cpt1\", location: \"Cape Town, South Africa\", flag: \"🇿🇦\" },\n  dub1: { code: \"dub1\", location: \"Dublin, Ireland\", flag: \"🇮🇪\" },\n  fra1: { code: \"fra1\", location: \"Frankfurt, Germany\", flag: \"🇩🇪\" },\n  gru1: { code: \"gru1\", location: \"São Paulo, Brazil\", flag: \"🇧🇷\" },\n  hkg1: { code: \"hkg1\", location: \"Hong Kong\", flag: \"🇭🇰\" },\n  hnd1: { code: \"hnd1\", location: \"Tokyo, Japan\", flag: \"🇯🇵\" },\n  iad1: { code: \"iad1\", location: \"Washington, D.C., USA\", flag: \"🇺🇸\" },\n  icn1: { code: \"icn1\", location: \"Seoul, South Korea\", flag: \"🇰🇷\" },\n  kix1: { code: \"kix1\", location: \"Osaka, Japan\", flag: \"🇯🇵\" },\n  lhr1: { code: \"lhr1\", location: \"London, United Kingdom\", flag: \"🇬🇧\" },\n  pdx1: { code: \"pdx1\", location: \"Portland, USA\", flag: \"🇺🇸\" },\n  sfo1: { code: \"sfo1\", location: \"San Francisco, USA\", flag: \"🇺🇸\" },\n  sin1: { code: \"sin1\", location: \"Singapore\", flag: \"🇸🇬\" },\n  syd1: { code: \"syd1\", location: \"Sydney, Australia\", flag: \"🇦🇺\" },\n};\n"
  },
  {
    "path": "packages/header-analysis/src/types/index.ts",
    "content": "export type Region = {\n  code: string;\n  location: string;\n  flag: string;\n};\n\nexport type ParserReturn<T> =\n  | { status: \"success\"; data: T }\n  | { status: \"failed\"; error: Error };\n"
  },
  {
    "path": "packages/header-analysis/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"resolveJsonModule\": true\n  }\n}\n"
  },
  {
    "path": "packages/icons/README.md",
    "content": "# Lucide React Icons Used in Dashboard\n\nThis document lists all the lucide-react icons currently used in the OpenStatus dashboard application.\n\n## Complete Icon List\n\n- Activity\n- AlertCircle\n- ArrowUpRight\n- AudioLines\n- Bell\n- Book\n- Bookmark\n- Braces\n- CalendarClock\n- CalendarIcon\n- Check\n- CheckCircle2\n- CheckIcon\n- ChevronDown\n- ChevronDownIcon\n- ChevronLeft\n- ChevronRight\n- ChevronRightIcon\n- ChevronUp\n- ChevronUpIcon\n- ChevronsUpDown\n- CircleCheck\n- CircleDashed\n- CircleIcon\n- Clock\n- ClockIcon\n- Cog\n- Copy\n- CopyPlus\n- CreditCard\n- ExternalLink\n- Eye\n- FileDown\n- FileJson\n- Filter\n- Globe\n- GripVertical\n- Hammer\n- HelpCircle\n- Info\n- Inbox\n- Key\n- Laptop\n- LayoutGrid\n- LifeBuoy\n- List\n- Loader\n- LoaderCircle\n- Lock\n- LogOut\n- Logs\n- Megaphone\n- Mic\n- Moon\n- MoreHorizontal\n- Network\n- PanelLeftIcon\n- PanelRight\n- PanelTop\n- Pencil\n- Plus\n- PlusCircle\n- Rocket\n- Search\n- SearchCheck\n- SearchIcon\n- Settings\n- Settings2\n- Siren\n- Sparkles\n- Sun\n- TableProperties\n- Terminal\n- Trash2\n- User\n- Users\n- Workflow\n- X\n- XCircle\n- XIcon\n\n## Usage Statistics\n\n- **Total Icons**: 75 unique icons\n- **Most Common**: Check, Plus, X, ChevronRight, ChevronDown, ChevronUp\n- **Categories**: \n  - Navigation (Chevron*, Arrow*, MoreHorizontal)\n  - Actions (Plus, X, Trash2, Copy, Pencil)\n  - Status (Check, AlertCircle, CheckCircle2, XCircle)\n  - UI Elements (Settings, Search, Bell, Lock)\n  - Content (Book, Braces, TableProperties, Globe)\n\n## Icon Categories\n\n### Navigation & Direction\n- ArrowUpRight\n- ChevronDown, ChevronDownIcon\n- ChevronLeft\n- ChevronRight, ChevronRightIcon\n- ChevronUp, ChevronUpIcon\n- ChevronsUpDown\n\n### Actions & Operations\n- Check, CheckIcon\n- Copy, CopyPlus\n- Eye\n- Pencil\n- Plus, PlusCircle\n- Trash2\n- X, XIcon\n\n### Status & Feedback\n- AlertCircle\n- CheckCircle2\n- CircleCheck\n- CircleDashed\n- Loader, LoaderCircle\n- XCircle\n\n### UI & Interface\n- Bell\n- Lock\n- MoreHorizontal\n- PanelLeftIcon, PanelRight, PanelTop\n- Search, SearchIcon\n- Settings, Settings2\n\n### Content & Data\n- Book\n- Braces\n- TableProperties\n- Globe\n- Network\n- Terminal\n\n### Time & Calendar\n- CalendarClock\n- CalendarIcon\n- Clock, ClockIcon\n\n### User & Account\n- CreditCard\n- LogOut\n- User\n- Users\n\n### System & Technical\n- Activity\n- Cog\n- FileDown, FileJson\n- Key\n- Logs\n- Workflow\n\n### Theme & Appearance\n- Laptop\n- Moon\n- Sun\n\n### Communication\n- AudioLines\n- Inbox\n- Mic\n- Megaphone\n\n### Miscellaneous\n- Bookmark\n- ExternalLink\n- Filter\n- GripVertical\n- Hammer\n- HelpCircle\n- Info\n- LayoutGrid\n- LifeBuoy\n- List\n- Rocket\n- SearchCheck\n- Siren\n- Sparkles\n"
  },
  {
    "path": "packages/icons/package.json",
    "content": "{\n  \"name\": \"@openstatus/icons\",\n  \"version\": \"0.0.0\",\n  \"main\": \"./src/index.tsx\",\n  \"types\": \"./src/index.tsx\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"lint\": \"biome lint \\\"**/*.ts*\\\"\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"1.9.4\",\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"dependencies\": {\n    \"react\": \"19.2.3\"\n  }\n}\n"
  },
  {
    "path": "packages/icons/src/discord.tsx",
    "content": "export function DiscordIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Discord</title>\n      <path d=\"M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/fly.tsx",
    "content": "export function Fly(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      viewBox=\"0 0 167 151\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      strokeLinejoin=\"round\"\n      strokeMiterlimit=\"2\"\n      {...props}\n    >\n      <title>Fly</title>\n      <path\n        d=\"M116.78 20.613h19.23c17.104 0 30.99 13.886 30.99 30.99v67.618c0 17.104-13.886 30.99-30.99 30.99h-1.516c-8.803-1.377-12.621-4.017-15.57-6.248L94.475 123.86a3.453 3.453 0 00-4.329 0l-7.943 6.532-22.37-18.394a3.443 3.443 0 00-4.326 0l-31.078 27.339c-6.255 5.087-10.392 4.148-13.075 3.853C4.424 137.503 0 128.874 0 119.221V51.603c0-17.104 13.886-30.99 30.993-30.99H50.18l-.035.077-.647 1.886-.201.647-.871 3.862-.12.678-.382 3.868-.051 1.062-.008.372.036 1.774.088 1.039.215 1.628.275 1.464.326 1.349.423 1.46 1.098 3.092.362.927 1.912 4.04.675 1.241 2.211 3.795.846 1.369 3.086 4.544.446.602 4.015 5.226 1.297 1.608 4.585 5.36.942 1.031 3.779 4.066 1.497 1.55 2.474 2.457-.497.415-.309.279a30.309 30.309 0 00-2.384 2.49c-.359.423-.701.86-1.025 1.31-.495.687-.938 1.41-1.324 2.164-.198.391-.375.792-.531 1.202a11.098 11.098 0 00-.718 3.267l-.014.966c.035 1.362.312 2.707.819 3.972a11.06 11.06 0 002.209 3.464 11.274 11.274 0 002.329 1.896c.731.447 1.51.816 2.319 1.096 1.76.597 3.627.809 5.476.623h.01a12.347 12.347 0 004.516-1.341 11.647 11.647 0 001.724-1.116 11.067 11.067 0 003.479-4.626c.569-1.422.848-2.941.823-4.471l-.044-.799a11.305 11.305 0 00-.749-3.078c-.17-.429-.364-.848-.58-1.257-.4-.752-.856-1.473-1.362-2.158-.232-.313-.472-.62-.72-.921a29.81 29.81 0 00-2.661-2.787l-.669-.569 1.133-1.119 4.869-5.085 1.684-1.849 2.618-2.945 1.703-1.992 2.428-2.957 1.644-2.067 2.414-3.228 1.219-1.67 1.729-2.585 1.44-2.203 2.713-4.725 1.552-3.1.045-.095 1.188-2.876c.015-.037.029-.075.04-.114l1.28-3.991.134-.582.555-3.177.108-.86.033-.527.038-1.989-.01-.371-.102-1.781-.126-1.383-.63-3.989a1.521 1.521 0 00-.037-.159l-.809-2.949-.279-.82-.364-.907zm9.141 84.321c-4.007.056-7.287 3.336-7.343 7.342.059 4.006 3.337 7.284 7.343 7.341 4.005-.058 7.284-3.335 7.345-7.341-.058-4.006-3.338-7.286-7.345-7.342z\"\n        fill=\"url(#_Radial1)\"\n      />\n      <path\n        d=\"M72.499 147.571l-1.296 1.09a6.802 6.802 0 01-4.253 1.55H30.993a30.867 30.867 0 01-19.639-7.021c2.683.295 6.82 1.234 13.075-3.853l31.078-27.339a3.443 3.443 0 014.326 0l22.37 18.394 7.943-6.532a3.453 3.453 0 014.329 0l24.449 20.103c2.949 2.231 6.767 4.871 15.57 6.248H118.23a6.919 6.919 0 01-3.993-1.33l-.285-.22-1.207-1.003a2.377 2.377 0 00-.32-.323 21845.256 21845.256 0 00-18.689-15.497 2.035 2.035 0 00-2.606.006s.044.052-18.386 15.491c-.09.075-.172.154-.245.236zm53.422-42.637c-4.007.056-7.287 3.336-7.343 7.342.059 4.006 3.337 7.284 7.343 7.341 4.005-.058 7.284-3.335 7.345-7.341-.058-4.006-3.338-7.286-7.345-7.342zM78.453 82.687l-2.474-2.457-1.497-1.55-3.779-4.066-.942-1.031-4.585-5.36-1.297-1.609-4.015-5.225-.446-.602-3.086-4.544-.846-1.369-2.211-3.795-.675-1.241-1.912-4.04-.362-.927-1.098-3.092-.423-1.46-.326-1.349-.275-1.465-.215-1.627-.088-1.039-.036-1.774.008-.372.051-1.062.382-3.869.12-.677.871-3.862.201-.647.647-1.886.207-.488 1.03-2.262.714-1.346.994-1.64.991-1.46.706-.928.813-.98.895-.985.767-.771 1.867-1.643 1.365-1.117c.033-.028.067-.053.102-.077l1.615-1.092 1.283-.818L65.931 3.8c.037-.023.079-.041.118-.059l3.456-1.434.319-.12 3.072-.899 1.297-.291 1.754-.352L77.11.468l1.784-.222L80.11.138 82.525.01l.946-.01 1.791.037.466.026 2.596.216 3.433.484.397.083 3.393.844.996.297 1.107.383 1.348.51 1.066.452 1.566.738.987.507 1.774 1.041.661.407 2.418 1.765.694.602 1.686 1.536.083.083 1.43 1.534.492.555 1.678 2.23.342.533 1.332 2.249.401.771.751 1.678.785 1.959.279.82.809 2.949c.015.052.027.105.037.159l.63 3.988.126 1.384.102 1.781.01.371-.038 1.989-.033.527-.108.86-.555 3.177-.134.582-1.28 3.991a1.186 1.186 0 01-.04.114l-1.188 2.876-.045.095-1.552 3.1-2.713 4.725-1.44 2.203-1.729 2.585-1.219 1.67-2.414 3.228-1.644 2.067-2.428 2.957-1.703 1.992-2.618 2.945-1.684 1.849-4.869 5.085-1.133 1.119.669.569c.946.871 1.835 1.8 2.661 2.787.248.301.488.608.72.921.506.685.962 1.406 1.362 2.158.216.407.409.828.58 1.257.389.985.651 2.026.749 3.078l.044.799c.025 1.53-.255 3.05-.823 4.471a11.057 11.057 0 01-3.479 4.625c-.541.424-1.118.796-1.724 1.117a12.347 12.347 0 01-4.516 1.341h-.01a12.996 12.996 0 01-5.476-.623 11.933 11.933 0 01-2.319-1.096 11.268 11.268 0 01-2.329-1.896 11.06 11.06 0 01-2.209-3.464 11.468 11.468 0 01-.819-3.972l.014-.966c.073-1.119.315-2.221.718-3.267.157-.411.334-.812.531-1.202.386-.755.83-1.477 1.324-2.164.323-.45.667-.887 1.025-1.31a30.309 30.309 0 012.384-2.49l.309-.279.497-.415z\"\n        fill=\"#24175b\"\n      />\n      <path\n        d=\"M71.203 148.661l19.927-16.817a2.035 2.035 0 012.606-.006l20.216 16.823a6.906 6.906 0 004.351 1.55H66.877a6.805 6.805 0 004.326-1.55zm12.404-60.034l.195.057c.063.03.116.075.173.114l.163.144c.402.37.793.759 1.169 1.157.265.283.523.574.771.875.315.38.61.779.879 1.194.116.183.224.368.325.561.088.167.167.34.236.515.122.305.214.627.242.954l-.006.614a3.507 3.507 0 01-1.662 2.732 4.747 4.747 0 01-2.021.665l-.759.022-.641-.056a4.964 4.964 0 01-.881-.214 4.17 4.17 0 01-.834-.391l-.5-.366a3.431 3.431 0 01-1.139-1.952 5.016 5.016 0 01-.059-.387l-.018-.586c.01-.158.034-.315.069-.472.087-.341.213-.673.372-.988.205-.396.439-.776.7-1.137.433-.586.903-1.143 1.405-1.67.324-.342.655-.673 1.001-.993l.246-.221c.171-.114.173-.114.368-.171h.206zM82.348 6.956l.079-.006v68.484l-.171-.315a191.264 191.264 0 01-6.291-12.75 136.318 136.318 0 01-4.269-10.688 84.358 84.358 0 01-2.574-8.802c-.541-2.365-.956-4.765-1.126-7.19a35.028 35.028 0 01-.059-3.108c.016-.903.053-1.804.109-2.705.09-1.418.234-2.832.442-4.235.165-1.104.368-2.205.62-3.293.2-.865.431-1.723.696-2.567.382-1.22.84-2.412 1.373-3.576.195-.419.405-.836.624-1.245 1.322-2.449 3.116-4.704 5.466-6.214a11.422 11.422 0 015.081-1.79zm8.88.173l4.607 1.314a28.193 28.193 0 016.076 3.096 24.387 24.387 0 016.533 6.517 24.618 24.618 0 012.531 4.878 28.586 28.586 0 011.761 7.898c.061.708.096 1.418.11 2.127.016.659.012 1.321-.041 1.98a22.306 22.306 0 01-.828 4.352 34.281 34.281 0 01-1.194 3.426 49.43 49.43 0 01-1.895 4.094c-1.536 2.966-3.304 5.803-5.195 8.547a133.118 133.118 0 01-7.491 9.776 185.466 185.466 0 01-8.987 9.96c2.114-3.963 4.087-8 5.915-12.102a149.96 149.96 0 002.876-6.93 108.799 108.799 0 002.679-7.792 76.327 76.327 0 001.54-5.976c.368-1.727.657-3.472.836-5.228.15-1.464.205-2.937.169-4.406a62.154 62.154 0 00-.1-2.695c-.216-3.612-.765-7.212-1.818-10.676a31.255 31.255 0 00-1.453-3.849c-1.348-2.937-3.23-5.683-5.776-7.686l-.855-.625z\"\n        fill=\"#fff\"\n      />\n      <defs>\n        <radialGradient\n          id=\"_Radial1\"\n          cx=\"0\"\n          cy=\"0\"\n          r=\"1\"\n          gradientUnits=\"userSpaceOnUse\"\n          gradientTransform=\"translate(88.67 84.848) scale(120.977)\"\n        >\n          <stop offset=\"0\" stopColor=\"#ba7bf0\" />\n          <stop offset=\".45\" stopColor=\"#996bec\" />\n          <stop offset=\"1\" stopColor=\"#5046e4\" />\n        </radialGradient>\n      </defs>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/github.tsx",
    "content": "export function GitHubIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>GitHub</title>\n      <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/google.tsx",
    "content": "export function GoogleIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Google</title>\n      <path d=\"M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/grafana.tsx",
    "content": "export function GrafanaIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Grafana</title>\n      <path d=\"M23.02 10.59a8.578 8.578 0 0 0-.862-3.034 8.911 8.911 0 0 0-1.789-2.445c.337-1.342-.413-2.505-.413-2.505-1.292-.08-2.113.4-2.416.62-.052-.02-.102-.044-.154-.064-.22-.089-.446-.172-.677-.247-.231-.073-.47-.14-.711-.197a9.867 9.867 0 0 0-.875-.161C14.557.753 12.94 0 12.94 0c-1.804 1.145-2.147 2.744-2.147 2.744l-.018.093c-.098.029-.2.057-.298.088-.138.042-.275.094-.413.143-.138.055-.275.107-.41.166a8.869 8.869 0 0 0-1.557.87l-.063-.029c-2.497-.955-4.716.195-4.716.195-.203 2.658.996 4.33 1.235 4.636a11.608 11.608 0 0 0-.607 2.635C1.636 12.677.953 15.014.953 15.014c1.926 2.214 4.171 2.351 4.171 2.351.003-.002.006-.002.006-.005.285.509.615.994.986 1.446.156.19.32.371.488.548-.704 2.009.099 3.68.099 3.68 2.144.08 3.553-.937 3.849-1.173a9.784 9.784 0 0 0 3.164.501h.08l.055-.003.107-.002.103-.005.003.002c1.01 1.44 2.788 1.646 2.788 1.646 1.264-1.332 1.337-2.653 1.337-2.94v-.058c0-.02-.003-.039-.003-.06.265-.187.52-.387.758-.6a7.875 7.875 0 0 0 1.415-1.7c1.43.083 2.437-.885 2.437-.885-.236-1.49-1.085-2.216-1.264-2.354l-.018-.013-.016-.013a.217.217 0 0 1-.031-.02c.008-.092.016-.18.02-.27.011-.162.016-.323.016-.48v-.253l-.005-.098-.008-.135a1.891 1.891 0 0 0-.01-.13c-.003-.042-.008-.083-.013-.125l-.016-.124-.018-.122a6.215 6.215 0 0 0-2.032-3.73 6.015 6.015 0 0 0-3.222-1.46 6.292 6.292 0 0 0-.85-.048l-.107.002h-.063l-.044.003-.104.008a4.777 4.777 0 0 0-3.335 1.695c-.332.4-.592.84-.768 1.297a4.594 4.594 0 0 0-.312 1.817l.003.091c.005.055.007.11.013.164a3.615 3.615 0 0 0 .698 1.82 3.53 3.53 0 0 0 1.827 1.282c.33.098.66.14.971.137.039 0 .078 0 .114-.002l.063-.003c.02 0 .041-.003.062-.003.034-.002.065-.007.099-.01.007 0 .018-.003.028-.003l.031-.005.06-.008a1.18 1.18 0 0 0 .112-.02c.036-.008.072-.013.109-.024a2.634 2.634 0 0 0 .914-.415c.028-.02.056-.041.085-.065a.248.248 0 0 0 .039-.35.244.244 0 0 0-.309-.06l-.078.042c-.09.044-.184.083-.283.116a2.476 2.476 0 0 1-.475.096c-.028.003-.054.006-.083.006l-.083.002c-.026 0-.054 0-.08-.002l-.102-.006h-.012l-.024.006c-.016-.003-.031-.003-.044-.006-.031-.002-.06-.007-.091-.01a2.59 2.59 0 0 1-.724-.213 2.557 2.557 0 0 1-.667-.438 2.52 2.52 0 0 1-.805-1.475 2.306 2.306 0 0 1-.029-.444l.006-.122v-.023l.002-.031c.003-.021.003-.04.005-.06a3.163 3.163 0 0 1 1.352-2.29 3.12 3.12 0 0 1 .937-.43 2.946 2.946 0 0 1 .776-.101h.06l.07.002.045.003h.026l.07.005a4.041 4.041 0 0 1 1.635.49 3.94 3.94 0 0 1 1.602 1.662 3.77 3.77 0 0 1 .397 1.414l.005.076.003.075c.002.026.002.05.002.075 0 .024.003.052 0 .07v.065l-.002.073-.008.174a6.195 6.195 0 0 1-.08.639 5.1 5.1 0 0 1-.267.927 5.31 5.31 0 0 1-.624 1.13 5.052 5.052 0 0 1-3.237 2.014 4.82 4.82 0 0 1-.649.066l-.039.003h-.287a6.607 6.607 0 0 1-1.716-.265 6.776 6.776 0 0 1-3.4-2.274 6.75 6.75 0 0 1-.746-1.15 6.616 6.616 0 0 1-.714-2.596l-.005-.083-.002-.02v-.056l-.003-.073v-.096l-.003-.104v-.07l.003-.163c.008-.22.026-.45.054-.678a8.707 8.707 0 0 1 .28-1.355c.128-.444.286-.872.473-1.277a7.04 7.04 0 0 1 1.456-2.1 5.925 5.925 0 0 1 .953-.763c.169-.111.343-.213.524-.306.089-.05.182-.091.273-.135.047-.02.093-.042.138-.062a7.177 7.177 0 0 1 .714-.267l.145-.045c.049-.015.098-.026.148-.041.098-.029.197-.052.296-.076.049-.013.1-.02.15-.033l.15-.032.151-.028.076-.013.075-.01.153-.024c.057-.01.114-.013.171-.023l.169-.021c.036-.003.073-.008.106-.01l.073-.008.036-.003.042-.002c.057-.003.114-.008.171-.01l.086-.006h.023l.037-.003.145-.007a7.999 7.999 0 0 1 1.708.125 7.917 7.917 0 0 1 2.048.68 8.253 8.253 0 0 1 1.672 1.09l.09.077.089.078c.06.052.114.107.171.159.057.052.112.106.166.16.052.055.107.107.159.164a8.671 8.671 0 0 1 1.41 1.978c.012.026.028.052.04.078l.04.078.075.156c.023.051.05.1.07.153l.065.15a8.848 8.848 0 0 1 .45 1.34.19.19 0 0 0 .201.142.186.186 0 0 0 .172-.184c.01-.246.002-.532-.024-.856z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/index.tsx",
    "content": "export * from \"./discord\";\nexport * from \"./github\";\nexport * from \"./google\";\nexport * from \"./grafana\";\nexport * from \"./pagerduty\";\nexport * from \"./slack\";\nexport * from \"./opsgenie\";\nexport * from \"./fly\";\nexport * from \"./railway\";\nexport * from \"./koyeb\";\nexport * from \"./telegram\";\nexport * from \"./whatsapp\";\nexport * from \"./markdown\";\nexport * from \"./statuspage\";\n"
  },
  {
    "path": "packages/icons/src/koyeb.tsx",
    "content": "export function Koyeb(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      width=\"485\"\n      height=\"480\"\n      viewBox=\"0 0 485 480\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <title>Koyeb</title>\n      <path\n        d=\"M242.171 116.666L484.653 256.284V139.774L242.016 0L0 139.929V256.594L242.171 116.666Z\"\n        fill=\"currentColor\"\n        className=\"fill-[#100F13] dark:fill-white\"\n      />\n      <path\n        d=\"M445.867 396.691L484.653 373.88V302.359L242.016 162.43L0 302.359V374.191L38.785 396.691L242.016 279.095L445.867 396.691Z\"\n        fill=\"currentColor\"\n        className=\"fill-[#100F13] dark:fill-white\"\n      />\n      <path\n        d=\"M242.17 444.471L303.753 479.999L404.91 421.815L242.015 327.805L79.7402 421.815L180.884 479.999L242.17 444.471Z\"\n        fill=\"currentColor\"\n        className=\"fill-[#100F13] dark:fill-white\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/markdown.tsx",
    "content": "export function Markdown(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <title>Markdown</title>\n      <path d=\"M22.27 19.385H1.73A1.73 1.73 0 010 17.655V6.345a1.73 1.73 0 011.73-1.73h20.54A1.73 1.73 0 0124 6.345v11.308a1.73 1.73 0 01-1.73 1.731zM5.769 15.923v-4.5l2.308 2.885 2.307-2.885v4.5h2.308V8.078h-2.308l-2.307 2.885-2.308-2.885H3.46v7.847zM21.232 12h-2.309V8.077h-2.307V12h-2.308l3.461 4.039z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/opsgenie.tsx",
    "content": "export function OpsGenieIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Opsgenie</title>\n      <path d=\"M12.002 0a5.988 5.988 0 1 1 0 11.975 5.988 5.988 0 0 1 0-11.975zm9.723 13.026h-.03l-4.527-2.242a.671.671 0 0 0-.876.268 22.408 22.408 0 0 1-4.306 5.217 22.407 22.407 0 0 1-4.286-5.2.671.671 0 0 0-.876-.269l-4.535 2.226h-.03a.671.671 0 0 0-.248.902 28.85 28.85 0 0 0 4.55 5.933l-.002.001c.024.025.05.048.075.072.335.335.676.664 1.027.981.081.074.165.144.247.217.315.278.632.555.96.82.144.117.295.227.441.341.277.216.552.434.837.639.44.318.888.625 1.346.917a.963.963 0 0 0 1.007.017c.487-.312.962-.64 1.428-.98.068-.05.132-.103.2-.153.358-.266.713-.537 1.06-.82.234-.19.46-.39.688-.588.17-.147.34-.291.506-.442.295-.268.58-.545.864-.825.061-.06.127-.118.188-.179l-.004-.002a28.852 28.852 0 0 0 4.565-5.949.671.671 0 0 0-.269-.902z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/pagerduty.tsx",
    "content": "export function PagerDutyIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>PagerDuty</title>\n      <path d=\"M16.965 1.18C15.085.164 13.769 0 10.683 0H3.73v14.55h6.926c2.743 0 4.8-.164 6.61-1.37 1.975-1.303 3.004-3.484 3.004-6.007 0-2.716-1.262-4.896-3.305-5.994zm-5.5 10.326h-4.21V3.113l3.977-.027c3.62-.028 5.43 1.234 5.43 4.128 0 3.113-2.248 4.292-5.197 4.292zM3.73 17.61h3.525V24H3.73Z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/railway.tsx",
    "content": "export function Railway(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      width=\"1024\"\n      height=\"1024\"\n      viewBox=\"0 0 1024 1024\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <title>Railway</title>\n      <path\n        d=\"M4.756 438.175A520.713 520.713 0 0 0 0 489.735h777.799c-2.716-5.306-6.365-10.09-10.045-14.772-132.97-171.791-204.498-156.896-306.819-161.26-34.114-1.403-57.249-1.967-193.037-1.967-72.677 0-151.688.185-228.628.39-9.96 26.884-19.566 52.942-24.243 74.14h398.571v51.909H4.756ZM783.93 541.696H.399c.82 13.851 2.112 27.517 3.978 40.999h723.39c32.248 0 50.299-18.297 56.162-40.999ZM45.017 724.306S164.941 1018.77 511.46 1024c207.112 0 385.071-123.006 465.907-299.694H45.017Z\"\n        fill=\"currentColor\"\n        className=\"fill-[#100F13] dark:fill-white\"\n      />\n      <path\n        d=\"M511.454 0C319.953 0 153.311 105.16 65.31 260.612c68.771-.144 202.704-.226 202.704-.226h.031v-.051c158.309 0 164.193.707 195.118 1.998l19.149.706c66.7 2.224 148.683 9.384 213.19 58.19 35.015 26.471 85.571 84.896 115.708 126.52 27.861 38.499 35.876 82.756 16.933 125.158-17.436 38.97-54.952 62.215-100.383 62.215H16.69s4.233 17.944 10.58 37.751h970.632A510.385 510.385 0 0 0 1024 512.218C1024.01 229.355 794.532 0 511.454 0Z\"\n        fill=\"currentColor\"\n        className=\"fill-[#100F13] dark:fill-white\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/slack.tsx",
    "content": "export function SlackIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Slack</title>\n      <path d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/statuspage.tsx",
    "content": "export function StatuspageIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Statuspage</title>\n      <path d=\"M12.008 9.597a5.623 5.623 0 1 1 0 11.245 5.623 5.623 0 0 1 0-11.245zM.154 8.717l3.02 3.574a.639.639 0 0 0 .913.068c4.885-4.379 10.97-4.379 15.84 0a.642.642 0 0 0 .916-.068l3.006-3.574a.646.646 0 0 0-.075-.906c-7.1-6.204-16.462-6.204-23.555 0a.65.65 0 0 0-.065.906z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/telegram.tsx",
    "content": "export function TelegramIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>Telegram</title>\n      <path d=\"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/src/whatsapp.tsx",
    "content": "export function WhatsappIcon(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      role=\"img\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      stroke=\"currentColor\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <title>WhatsApp</title>\n      <path d=\"M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/icons/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/react-library.json\",\n  \"include\": [\".\"],\n  \"exclude\": [\"dist\", \"build\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "packages/importers/package.json",
    "content": "{\n  \"name\": \"@openstatus/importers\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./types\": \"./src/types.ts\",\n    \"./statuspage\": \"./src/providers/statuspage/index.ts\",\n    \"./statuspage/fixtures\": \"./src/providers/statuspage/fixtures.ts\"\n  },\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"test:watch\": \"bun test --watch\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/importers/src/index.ts",
    "content": "export type {\n  ImportConfig,\n  ImportProvider,\n  ImportSummary,\n  PhaseResult,\n  ResourceResult,\n} from \"./types\";\n\nexport { createStatuspageProvider } from \"./providers/statuspage\";\nexport type { StatuspageImportConfig } from \"./providers/statuspage\";\n\n/**\n * Registry of all available import providers.\n * Add new providers here as they are implemented.\n */\nexport const IMPORT_PROVIDERS = [\"statuspage\"] as const;\nexport type ImportProviderName = (typeof IMPORT_PROVIDERS)[number];\n"
  },
  {
    "path": "packages/importers/src/providers/statuspage/api-types.ts",
    "content": "import { z } from \"zod\";\n\nexport const StatuspageComponentSchema = z.object({\n  id: z.string(),\n  page_id: z.string(),\n  group_id: z.string().nullable(),\n  name: z.string(),\n  description: z.string().nullable(),\n  position: z.number(),\n  status: z.enum([\n    \"operational\",\n    \"degraded_performance\",\n    \"partial_outage\",\n    \"major_outage\",\n    \"under_maintenance\",\n  ]),\n  showcase: z.boolean(),\n  only_show_if_degraded: z.boolean(),\n  group: z.boolean(),\n  start_date: z.string().nullable(),\n  created_at: z.string(),\n  updated_at: z.string(),\n});\n\nexport type StatuspageComponent = z.infer<typeof StatuspageComponentSchema>;\n\nexport const StatuspageGroupComponentSchema = z.object({\n  id: z.string(),\n  page_id: z.string(),\n  name: z.string(),\n  description: z.string().nullable(),\n  components: z.array(z.string()),\n  position: z.number(),\n  created_at: z.string(),\n  updated_at: z.string(),\n});\n\nexport type StatuspageGroupComponent = z.infer<\n  typeof StatuspageGroupComponentSchema\n>;\n\nexport const StatuspageIncidentUpdateSchema = z.object({\n  id: z.string(),\n  incident_id: z.string(),\n  status: z.enum([\n    \"investigating\",\n    \"identified\",\n    \"monitoring\",\n    \"resolved\",\n    \"scheduled\",\n    \"in_progress\",\n    \"verifying\",\n    \"completed\",\n  ]),\n  body: z.string().nullable(),\n  display_at: z.string().nullable(),\n  deliver_notifications: z.boolean(),\n  affected_components: z\n    .array(\n      z.object({\n        code: z.string(),\n        name: z.string(),\n        old_status: z.string(),\n        new_status: z.string(),\n      }),\n    )\n    .nullable(),\n  created_at: z.string(),\n  updated_at: z.string(),\n});\n\nexport type StatuspageIncidentUpdate = z.infer<\n  typeof StatuspageIncidentUpdateSchema\n>;\n\nexport const StatuspageIncidentSchema = z.object({\n  id: z.string(),\n  page_id: z.string(),\n  name: z.string(),\n  status: z.enum([\n    \"investigating\",\n    \"identified\",\n    \"monitoring\",\n    \"resolved\",\n    \"scheduled\",\n    \"in_progress\",\n    \"verifying\",\n    \"completed\",\n  ]),\n  impact: z.enum([\"none\", \"minor\", \"major\", \"critical\"]).nullable(),\n  shortlink: z.string().nullable(),\n  scheduled_for: z.string().nullable(),\n  scheduled_until: z.string().nullable(),\n  resolved_at: z.string().nullable(),\n  monitoring_at: z.string().nullable(),\n  created_at: z.string(),\n  updated_at: z.string(),\n  incident_updates: z.array(StatuspageIncidentUpdateSchema).optional(),\n  components: z.array(StatuspageComponentSchema).optional(),\n  postmortem_body: z.string().nullable(),\n  metadata: z.unknown().nullable(),\n});\n\nexport type StatuspageIncident = z.infer<typeof StatuspageIncidentSchema>;\n\nexport const StatuspageSubscriberSchema = z.object({\n  id: z.string(),\n  page_id: z.string(),\n  mode: z.enum([\"email\", \"sms\", \"slack\", \"webhook\", \"integration_partner\"]),\n  email: z.string().nullable(),\n  endpoint: z.string().nullable(),\n  phone_number: z.string().nullable(),\n  phone_country: z.string().nullable(),\n  display_phone_number: z.string().nullable(),\n  obfuscated_channel_name: z.string().nullable(),\n  workspace_name: z.string().nullable(),\n  components: z.array(z.string()).nullable(),\n  quarantined_at: z.string().nullable(),\n  created_at: z.string(),\n});\n\nexport type StatuspageSubscriber = z.infer<typeof StatuspageSubscriberSchema>;\n\nexport const StatuspagePageSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  page_description: z.string().nullable(),\n  subdomain: z.string(),\n  domain: z.string().nullable(),\n  url: z.string().nullable(),\n  support_url: z.string().nullable(),\n  time_zone: z.string().nullable(),\n  allow_page_subscribers: z.boolean(),\n  allow_email_subscribers: z.boolean(),\n  allow_sms_subscribers: z.boolean(),\n  allow_webhook_subscribers: z.boolean(),\n  created_at: z.string(),\n  updated_at: z.string(),\n});\n\nexport type StatuspagePage = z.infer<typeof StatuspagePageSchema>;\n"
  },
  {
    "path": "packages/importers/src/providers/statuspage/client.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, mock, test } from \"bun:test\";\nimport { createStatuspageClient } from \"./client\";\nimport {\n  MOCK_COMPONENTS,\n  MOCK_COMPONENT_GROUPS,\n  MOCK_INCIDENTS,\n  MOCK_PAGES,\n  MOCK_SUBSCRIBERS,\n} from \"./fixtures\";\n\nconst originalFetch = globalThis.fetch;\n\nfunction mockFetch(data: unknown, status = 200) {\n  globalThis.fetch = mock(() =>\n    Promise.resolve(\n      new Response(JSON.stringify(data), {\n        status,\n        statusText: status === 200 ? \"OK\" : \"Unauthorized\",\n        headers: { \"Content-Type\": \"application/json\" },\n      }),\n    ),\n  ) as typeof globalThis.fetch;\n}\n\n/**\n * Mock fetch for paginated endpoints: returns `data` on first call, `[]` on subsequent calls.\n */\nfunction mockFetchPaginated(data: unknown, status = 200) {\n  let callCount = 0;\n  globalThis.fetch = mock(() => {\n    callCount++;\n    const body = callCount === 1 ? data : [];\n    return Promise.resolve(\n      new Response(JSON.stringify(body), {\n        status,\n        statusText: status === 200 ? \"OK\" : \"Unauthorized\",\n        headers: { \"Content-Type\": \"application/json\" },\n      }),\n    );\n  }) as typeof globalThis.fetch;\n}\n\ndescribe(\"StatuspageClient\", () => {\n  let client: ReturnType<typeof createStatuspageClient>;\n\n  beforeEach(() => {\n    client = createStatuspageClient(\"test-api-key\");\n  });\n\n  afterEach(() => {\n    globalThis.fetch = originalFetch;\n  });\n\n  test(\"getPages returns parsed pages\", async () => {\n    mockFetch(MOCK_PAGES);\n    const pages = await client.getPages();\n    expect(pages).toEqual(MOCK_PAGES);\n    expect(pages).toHaveLength(1);\n    expect(pages[0].name).toBe(\"Acme Corp Status\");\n  });\n\n  test(\"getComponents returns parsed components\", async () => {\n    mockFetchPaginated(MOCK_COMPONENTS);\n    const components = await client.getComponents(\"sp_page_001\");\n    expect(components).toEqual(MOCK_COMPONENTS);\n    expect(components).toHaveLength(4);\n    expect(components[2].status).toBe(\"degraded_performance\");\n  });\n\n  test(\"getComponentGroups returns parsed groups\", async () => {\n    mockFetchPaginated(MOCK_COMPONENT_GROUPS);\n    const groups = await client.getComponentGroups(\"sp_page_001\");\n    expect(groups).toEqual(MOCK_COMPONENT_GROUPS);\n    expect(groups).toHaveLength(1);\n    expect(groups[0].name).toBe(\"Core Services\");\n    expect(groups[0].components).toEqual([\"sp_comp_001\", \"sp_comp_002\"]);\n  });\n\n  test(\"getIncidents returns parsed incidents with updates\", async () => {\n    mockFetchPaginated(MOCK_INCIDENTS);\n    const incidents = await client.getIncidents(\"sp_page_001\");\n    expect(incidents).toEqual(MOCK_INCIDENTS);\n    expect(incidents).toHaveLength(3);\n    expect(incidents[0].incident_updates).toHaveLength(4);\n    expect(incidents[0].postmortem_body).toBeTruthy();\n    expect(incidents[1].status).toBe(\"identified\");\n    expect(incidents[2].scheduled_for).toBe(\"2024-06-20T02:00:00.000Z\");\n  });\n\n  test(\"getSubscribers returns parsed subscribers\", async () => {\n    mockFetchPaginated(MOCK_SUBSCRIBERS);\n    const subscribers = await client.getSubscribers(\"sp_page_001\");\n    expect(subscribers).toEqual(MOCK_SUBSCRIBERS);\n    expect(subscribers).toHaveLength(5);\n    expect(subscribers[0].mode).toBe(\"email\");\n    expect(subscribers[2].mode).toBe(\"webhook\");\n    expect(subscribers[3].mode).toBe(\"sms\");\n    expect(subscribers[4].mode).toBe(\"slack\");\n  });\n\n  test(\"throws on API error (401)\", async () => {\n    mockFetch({ error: \"Unauthorized\" }, 401);\n    await expect(client.getPages()).rejects.toThrow(\n      \"Statuspage API error: 401 Unauthorized for /pages\",\n    );\n  });\n\n  test(\"getPage returns a single parsed page\", async () => {\n    mockFetch(MOCK_PAGES[0]);\n    const singlePage = await client.getPage(\"sp_page_001\");\n    expect(singlePage).toEqual(MOCK_PAGES[0]);\n    expect(singlePage.id).toBe(\"sp_page_001\");\n    expect(singlePage.name).toBe(\"Acme Corp Status\");\n  });\n\n  test(\"getPage calls correct URL path\", async () => {\n    mockFetch(MOCK_PAGES[0]);\n    await client.getPage(\"sp_page_001\");\n    const fetchMock = globalThis.fetch as ReturnType<typeof mock>;\n    const [url] = fetchMock.mock.calls[0] as [string, RequestInit];\n    expect(url).toBe(\"https://api.statuspage.io/v1/pages/sp_page_001\");\n  });\n\n  test(\"getPage throws on non-200 response\", async () => {\n    mockFetch({ error: \"Not Found\" }, 404);\n    await expect(client.getPage(\"nonexistent\")).rejects.toThrow(\n      \"Statuspage API error: 404\",\n    );\n  });\n\n  test(\"getPage throws on schema mismatch\", async () => {\n    mockFetch({ id: 123, unexpected: true });\n    await expect(client.getPage(\"sp_page_001\")).rejects.toThrow();\n  });\n\n  test(\"sends correct auth header\", async () => {\n    mockFetch(MOCK_PAGES);\n    await client.getPages();\n    const fetchMock = globalThis.fetch as ReturnType<typeof mock>;\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const [url, options] = fetchMock.mock.calls[0] as [string, RequestInit];\n    expect(url).toBe(\"https://api.statuspage.io/v1/pages\");\n    expect((options.headers as Record<string, string>).Authorization).toBe(\n      \"OAuth test-api-key\",\n    );\n  });\n});\n"
  },
  {
    "path": "packages/importers/src/providers/statuspage/client.ts",
    "content": "import { z } from \"zod\";\nimport {\n  type StatuspageComponent,\n  StatuspageComponentSchema,\n  type StatuspageGroupComponent,\n  StatuspageGroupComponentSchema,\n  type StatuspageIncident,\n  StatuspageIncidentSchema,\n  type StatuspagePage,\n  StatuspagePageSchema,\n  type StatuspageSubscriber,\n  StatuspageSubscriberSchema,\n} from \"./api-types\";\n\nexport type StatuspageClient = {\n  getPages: () => Promise<StatuspagePage[]>;\n  getPage: (pageId: string) => Promise<StatuspagePage>;\n  getComponents: (pageId: string) => Promise<StatuspageComponent[]>;\n  getComponentGroups: (pageId: string) => Promise<StatuspageGroupComponent[]>;\n  getIncidents: (pageId: string) => Promise<StatuspageIncident[]>;\n  // NOTE: could add getScheduledStatusReports if we need the dedicated /incidents/scheduled endpoint\n  getSubscribers: (pageId: string) => Promise<StatuspageSubscriber[]>;\n};\n\nexport function createStatuspageClient(\n  apiKey: string,\n  baseUrl = \"https://api.statuspage.io/v1\",\n): StatuspageClient {\n  async function request<T>(path: string, schema: z.ZodType<T>): Promise<T> {\n    const url = `${baseUrl}${path}`;\n    const response = await fetch(url, {\n      headers: {\n        Authorization: `OAuth ${apiKey}`,\n        \"Content-Type\": \"application/json\",\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(\n        `Statuspage API error: ${response.status} ${response.statusText} for ${path}`,\n      );\n    }\n\n    const data = await response.json();\n    return schema.parse(data);\n  }\n\n  async function requestAllPages<T>(\n    path: string,\n    itemSchema: z.ZodType<T>,\n    perPage = 100,\n  ): Promise<T[]> {\n    const all: T[] = [];\n    let page = 1;\n    while (true) {\n      const separator = path.includes(\"?\") ? \"&\" : \"?\";\n      const items = await request(\n        `${path}${separator}page=${page}&per_page=${perPage}`,\n        z.array(itemSchema),\n      );\n      all.push(...items);\n      if (items.length < perPage) break;\n      page++;\n    }\n    return all;\n  }\n\n  return {\n    getPages: () => request(\"/pages\", z.array(StatuspagePageSchema)),\n    getPage: (pageId) => request(`/pages/${pageId}`, StatuspagePageSchema),\n    getComponents: (pageId) =>\n      requestAllPages(`/pages/${pageId}/components`, StatuspageComponentSchema),\n    getComponentGroups: (pageId) =>\n      requestAllPages(\n        `/pages/${pageId}/component-groups`,\n        StatuspageGroupComponentSchema,\n      ),\n    getIncidents: (pageId) =>\n      requestAllPages(`/pages/${pageId}/incidents`, StatuspageIncidentSchema),\n    getSubscribers: (pageId) =>\n      requestAllPages(\n        `/pages/${pageId}/subscribers`,\n        StatuspageSubscriberSchema,\n      ),\n  };\n}\n"
  },
  {
    "path": "packages/importers/src/providers/statuspage/fixtures.ts",
    "content": "import type {\n  StatuspageComponent,\n  StatuspageGroupComponent,\n  StatuspageIncident,\n  StatuspagePage,\n  StatuspageSubscriber,\n} from \"./api-types\";\n\nexport const MOCK_PAGES: StatuspagePage[] = [\n  {\n    id: \"sp_page_001\",\n    name: \"Acme Corp Status\",\n    page_description: \"Current status and incidents for Acme Corp services\",\n    subdomain: \"acmecorp\",\n    domain: \"status.acmecorp.com\",\n    url: \"https://status.acmecorp.com\",\n    support_url: \"https://support.acmecorp.com\",\n    time_zone: \"America/New_York\",\n    allow_page_subscribers: true,\n    allow_email_subscribers: true,\n    allow_sms_subscribers: true,\n    allow_webhook_subscribers: true,\n    created_at: \"2024-01-15T10:00:00.000Z\",\n    updated_at: \"2024-06-01T12:00:00.000Z\",\n  },\n];\n\nexport const MOCK_COMPONENTS: StatuspageComponent[] = [\n  {\n    id: \"sp_comp_001\",\n    page_id: \"sp_page_001\",\n    group_id: \"sp_group_001\",\n    name: \"API Gateway\",\n    description: \"Main API gateway for all services\",\n    position: 1,\n    status: \"operational\",\n    showcase: true,\n    only_show_if_degraded: false,\n    group: false,\n    start_date: \"2024-01-15\",\n    created_at: \"2024-01-15T10:00:00.000Z\",\n    updated_at: \"2024-06-01T12:00:00.000Z\",\n  },\n  {\n    id: \"sp_comp_002\",\n    page_id: \"sp_page_001\",\n    group_id: \"sp_group_001\",\n    name: \"Authentication Service\",\n    description: \"Handles user authentication and authorization\",\n    position: 2,\n    status: \"operational\",\n    showcase: true,\n    only_show_if_degraded: false,\n    group: false,\n    start_date: \"2024-01-15\",\n    created_at: \"2024-01-15T10:05:00.000Z\",\n    updated_at: \"2024-06-01T12:00:00.000Z\",\n  },\n  {\n    id: \"sp_comp_003\",\n    page_id: \"sp_page_001\",\n    group_id: null,\n    name: \"Dashboard\",\n    description: \"Web dashboard application\",\n    position: 3,\n    status: \"degraded_performance\",\n    showcase: true,\n    only_show_if_degraded: false,\n    group: false,\n    start_date: \"2024-02-01\",\n    created_at: \"2024-02-01T08:00:00.000Z\",\n    updated_at: \"2024-06-10T15:30:00.000Z\",\n  },\n  {\n    id: \"sp_comp_004\",\n    page_id: \"sp_page_001\",\n    group_id: null,\n    name: \"CDN\",\n    description: null,\n    position: 4,\n    status: \"operational\",\n    showcase: true,\n    only_show_if_degraded: true,\n    group: false,\n    start_date: null,\n    created_at: \"2024-03-10T09:00:00.000Z\",\n    updated_at: \"2024-06-01T12:00:00.000Z\",\n  },\n];\n\nexport const MOCK_COMPONENT_GROUPS: StatuspageGroupComponent[] = [\n  {\n    id: \"sp_group_001\",\n    page_id: \"sp_page_001\",\n    name: \"Core Services\",\n    description: \"Core infrastructure services\",\n    components: [\"sp_comp_001\", \"sp_comp_002\"],\n    position: 1,\n    created_at: \"2024-01-15T10:00:00.000Z\",\n    updated_at: \"2024-06-01T12:00:00.000Z\",\n  },\n];\n\nexport const MOCK_INCIDENTS: StatuspageIncident[] = [\n  {\n    id: \"sp_incident_001\",\n    page_id: \"sp_page_001\",\n    name: \"API Gateway Elevated Error Rates\",\n    status: \"resolved\",\n    impact: \"major\",\n    shortlink: \"https://stspg.io/abc123\",\n    scheduled_for: null,\n    scheduled_until: null,\n    resolved_at: \"2024-06-10T16:45:00.000Z\",\n    monitoring_at: \"2024-06-10T16:00:00.000Z\",\n    created_at: \"2024-06-10T14:00:00.000Z\",\n    updated_at: \"2024-06-10T16:45:00.000Z\",\n    incident_updates: [\n      {\n        id: \"sp_update_001\",\n        incident_id: \"sp_incident_001\",\n        status: \"investigating\",\n        body: \"We are investigating elevated error rates on the API Gateway.\",\n        display_at: \"2024-06-10T14:00:00.000Z\",\n        deliver_notifications: true,\n        affected_components: [\n          {\n            code: \"sp_comp_001\",\n            name: \"API Gateway\",\n            old_status: \"operational\",\n            new_status: \"major_outage\",\n          },\n        ],\n        created_at: \"2024-06-10T14:00:00.000Z\",\n        updated_at: \"2024-06-10T14:00:00.000Z\",\n      },\n      {\n        id: \"sp_update_002\",\n        incident_id: \"sp_incident_001\",\n        status: \"identified\",\n        body: \"The issue has been identified as a misconfigured load balancer rule.\",\n        display_at: \"2024-06-10T14:30:00.000Z\",\n        deliver_notifications: true,\n        affected_components: [\n          {\n            code: \"sp_comp_001\",\n            name: \"API Gateway\",\n            old_status: \"major_outage\",\n            new_status: \"major_outage\",\n          },\n        ],\n        created_at: \"2024-06-10T14:30:00.000Z\",\n        updated_at: \"2024-06-10T14:30:00.000Z\",\n      },\n      {\n        id: \"sp_update_003\",\n        incident_id: \"sp_incident_001\",\n        status: \"monitoring\",\n        body: \"A fix has been deployed and we are monitoring the results.\",\n        display_at: \"2024-06-10T16:00:00.000Z\",\n        deliver_notifications: true,\n        affected_components: [\n          {\n            code: \"sp_comp_001\",\n            name: \"API Gateway\",\n            old_status: \"major_outage\",\n            new_status: \"degraded_performance\",\n          },\n        ],\n        created_at: \"2024-06-10T16:00:00.000Z\",\n        updated_at: \"2024-06-10T16:00:00.000Z\",\n      },\n      {\n        id: \"sp_update_004\",\n        incident_id: \"sp_incident_001\",\n        status: \"resolved\",\n        body: \"The issue has been fully resolved. Error rates are back to normal.\",\n        display_at: \"2024-06-10T16:45:00.000Z\",\n        deliver_notifications: true,\n        affected_components: [\n          {\n            code: \"sp_comp_001\",\n            name: \"API Gateway\",\n            old_status: \"degraded_performance\",\n            new_status: \"operational\",\n          },\n        ],\n        created_at: \"2024-06-10T16:45:00.000Z\",\n        updated_at: \"2024-06-10T16:45:00.000Z\",\n      },\n    ],\n    components: [MOCK_COMPONENTS[0]],\n    postmortem_body:\n      \"## Summary\\n\\nOn June 10, 2024, our API Gateway experienced elevated error rates due to a misconfigured load balancer rule. The issue was resolved within 2 hours and 45 minutes.\\n\\n## Root Cause\\n\\nA deployment at 13:55 UTC introduced an incorrect routing rule that caused 30% of requests to fail.\\n\\n## Resolution\\n\\nThe faulty rule was reverted and a fix was deployed.\",\n    metadata: null,\n  },\n  {\n    id: \"sp_incident_002\",\n    page_id: \"sp_page_001\",\n    name: \"Dashboard Slow Response Times\",\n    status: \"identified\",\n    impact: \"minor\",\n    shortlink: \"https://stspg.io/def456\",\n    scheduled_for: null,\n    scheduled_until: null,\n    resolved_at: null,\n    monitoring_at: null,\n    created_at: \"2024-06-11T09:00:00.000Z\",\n    updated_at: \"2024-06-11T09:45:00.000Z\",\n    incident_updates: [\n      {\n        id: \"sp_update_005\",\n        incident_id: \"sp_incident_002\",\n        status: \"investigating\",\n        body: \"We are investigating reports of slow response times on the Dashboard.\",\n        display_at: \"2024-06-11T09:00:00.000Z\",\n        deliver_notifications: true,\n        affected_components: [\n          {\n            code: \"sp_comp_003\",\n            name: \"Dashboard\",\n            old_status: \"operational\",\n            new_status: \"degraded_performance\",\n          },\n        ],\n        created_at: \"2024-06-11T09:00:00.000Z\",\n        updated_at: \"2024-06-11T09:00:00.000Z\",\n      },\n      {\n        id: \"sp_update_006\",\n        incident_id: \"sp_incident_002\",\n        status: \"identified\",\n        body: \"The issue has been traced to a slow database query affecting the main dashboard view.\",\n        display_at: \"2024-06-11T09:45:00.000Z\",\n        deliver_notifications: true,\n        affected_components: [\n          {\n            code: \"sp_comp_003\",\n            name: \"Dashboard\",\n            old_status: \"degraded_performance\",\n            new_status: \"degraded_performance\",\n          },\n        ],\n        created_at: \"2024-06-11T09:45:00.000Z\",\n        updated_at: \"2024-06-11T09:45:00.000Z\",\n      },\n    ],\n    components: [MOCK_COMPONENTS[2]],\n    postmortem_body: null,\n    metadata: null,\n  },\n  {\n    id: \"sp_incident_003\",\n    page_id: \"sp_page_001\",\n    name: \"Scheduled Database Maintenance\",\n    status: \"scheduled\",\n    impact: \"none\",\n    shortlink: \"https://stspg.io/ghi789\",\n    scheduled_for: \"2024-06-20T02:00:00.000Z\",\n    scheduled_until: \"2024-06-20T06:00:00.000Z\",\n    resolved_at: null,\n    monitoring_at: null,\n    created_at: \"2024-06-12T10:00:00.000Z\",\n    updated_at: \"2024-06-12T10:00:00.000Z\",\n    incident_updates: [\n      {\n        id: \"sp_update_007\",\n        incident_id: \"sp_incident_003\",\n        status: \"scheduled\",\n        body: \"We will be performing scheduled database maintenance. Some services may experience brief interruptions.\",\n        display_at: \"2024-06-12T10:00:00.000Z\",\n        deliver_notifications: true,\n        affected_components: [\n          {\n            code: \"sp_comp_001\",\n            name: \"API Gateway\",\n            old_status: \"operational\",\n            new_status: \"under_maintenance\",\n          },\n          {\n            code: \"sp_comp_002\",\n            name: \"Authentication Service\",\n            old_status: \"operational\",\n            new_status: \"under_maintenance\",\n          },\n        ],\n        created_at: \"2024-06-12T10:00:00.000Z\",\n        updated_at: \"2024-06-12T10:00:00.000Z\",\n      },\n    ],\n    components: [MOCK_COMPONENTS[0], MOCK_COMPONENTS[1]],\n    postmortem_body: null,\n    metadata: null,\n  },\n];\n\nexport const MOCK_SUBSCRIBERS: StatuspageSubscriber[] = [\n  {\n    id: \"sp_sub_001\",\n    page_id: \"sp_page_001\",\n    mode: \"email\",\n    email: \"alice@acmecorp.com\",\n    endpoint: null,\n    phone_number: null,\n    phone_country: null,\n    display_phone_number: null,\n    obfuscated_channel_name: null,\n    workspace_name: null,\n    components: null,\n    quarantined_at: null,\n    created_at: \"2024-02-01T10:00:00.000Z\",\n  },\n  {\n    id: \"sp_sub_002\",\n    page_id: \"sp_page_001\",\n    mode: \"email\",\n    email: \"bob@acmecorp.com\",\n    endpoint: null,\n    phone_number: null,\n    phone_country: null,\n    display_phone_number: null,\n    obfuscated_channel_name: null,\n    workspace_name: null,\n    components: [\"sp_comp_001\", \"sp_comp_003\"],\n    quarantined_at: null,\n    created_at: \"2024-02-15T14:30:00.000Z\",\n  },\n  {\n    id: \"sp_sub_003\",\n    page_id: \"sp_page_001\",\n    mode: \"webhook\",\n    email: null,\n    endpoint: \"https://hooks.acmecorp.com/statuspage\",\n    phone_number: null,\n    phone_country: null,\n    display_phone_number: null,\n    obfuscated_channel_name: null,\n    workspace_name: null,\n    components: null,\n    quarantined_at: null,\n    created_at: \"2024-03-01T09:00:00.000Z\",\n  },\n  {\n    id: \"sp_sub_004\",\n    page_id: \"sp_page_001\",\n    mode: \"sms\",\n    email: null,\n    endpoint: null,\n    phone_number: \"+15551234567\",\n    phone_country: \"US\",\n    display_phone_number: \"+1 (555) 123-4567\",\n    obfuscated_channel_name: null,\n    workspace_name: null,\n    components: [\"sp_comp_001\"],\n    quarantined_at: null,\n    created_at: \"2024-03-10T11:00:00.000Z\",\n  },\n  {\n    id: \"sp_sub_005\",\n    page_id: \"sp_page_001\",\n    mode: \"slack\",\n    email: null,\n    endpoint: null,\n    phone_number: null,\n    phone_country: null,\n    display_phone_number: null,\n    obfuscated_channel_name: \"#ops-alerts\",\n    workspace_name: \"Acme Corp\",\n    components: null,\n    quarantined_at: null,\n    created_at: \"2024-04-01T08:00:00.000Z\",\n  },\n];\n"
  },
  {
    "path": "packages/importers/src/providers/statuspage/index.ts",
    "content": "export { createStatuspageClient } from \"./client\";\nexport type { StatuspageClient } from \"./client\";\nexport type {\n  StatuspageComponent,\n  StatuspageGroupComponent,\n  StatuspageIncident,\n  StatuspageIncidentUpdate,\n  StatuspagePage,\n  StatuspageSubscriber,\n} from \"./api-types\";\nexport {\n  StatuspageComponentSchema,\n  StatuspageGroupComponentSchema,\n  StatuspageIncidentSchema,\n  StatuspageIncidentUpdateSchema,\n  StatuspagePageSchema,\n  StatuspageSubscriberSchema,\n} from \"./api-types\";\nexport { createStatuspageProvider } from \"./provider\";\nexport type { StatuspageImportConfig } from \"./provider\";\n"
  },
  {
    "path": "packages/importers/src/providers/statuspage/mapper.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\";\nimport {\n  MOCK_COMPONENTS,\n  MOCK_COMPONENT_GROUPS,\n  MOCK_INCIDENTS,\n  MOCK_PAGES,\n  MOCK_SUBSCRIBERS,\n} from \"./fixtures\";\nimport {\n  isScheduledIncident,\n  mapComponent,\n  mapComponentGroup,\n  mapIncidentToMaintenance,\n  mapIncidentToStatusReport,\n  mapIncidentUpdateStatus,\n  mapPage,\n  mapSubscriber,\n} from \"./mapper\";\n\ndescribe(\"mapPage\", () => {\n  it(\"maps a page with all fields\", () => {\n    const result = mapPage(MOCK_PAGES[0], 1);\n    expect(result).toEqual({\n      workspaceId: 1,\n      title: \"Acme Corp Status\",\n      description: \"Current status and incidents for Acme Corp services\",\n      slug: \"acmecorp\",\n      customDomain: \"status.acmecorp.com\",\n      published: true,\n      icon: \"\",\n    });\n  });\n\n  it(\"handles null domain\", () => {\n    const page = { ...MOCK_PAGES[0], domain: null };\n    const result = mapPage(page, 1);\n    expect(result.customDomain).toBe(\"\");\n  });\n});\n\ndescribe(\"mapComponent\", () => {\n  it(\"maps a component with all fields\", () => {\n    const result = mapComponent(MOCK_COMPONENTS[0], 1, 10);\n    expect(result).toEqual({\n      workspaceId: 1,\n      pageId: 10,\n      type: \"static\",\n      monitorId: null,\n      name: \"API Gateway\",\n      description: \"Main API gateway for all services\",\n      order: 1,\n    });\n  });\n\n  it(\"maps position to order\", () => {\n    const result = mapComponent(MOCK_COMPONENTS[2], 1, 10);\n    expect(result.order).toBe(3);\n  });\n});\n\ndescribe(\"mapComponentGroup\", () => {\n  it(\"maps a component group\", () => {\n    const result = mapComponentGroup(MOCK_COMPONENT_GROUPS[0], 1, 10);\n    expect(result).toEqual({\n      workspaceId: 1,\n      pageId: 10,\n      name: \"Core Services\",\n    });\n  });\n});\n\ndescribe(\"isScheduledIncident\", () => {\n  it(\"returns true for scheduled incidents\", () => {\n    expect(isScheduledIncident(MOCK_INCIDENTS[2])).toBe(true);\n  });\n\n  it(\"returns false for realtime incidents\", () => {\n    expect(isScheduledIncident(MOCK_INCIDENTS[0])).toBe(false);\n    expect(isScheduledIncident(MOCK_INCIDENTS[1])).toBe(false);\n  });\n});\n\ndescribe(\"mapIncidentToStatusReport\", () => {\n  it(\"maps a resolved incident with 4 updates\", () => {\n    const result = mapIncidentToStatusReport(MOCK_INCIDENTS[0], 1, 10);\n    expect(result.report.title).toBe(\"API Gateway Elevated Error Rates\");\n    expect(result.report.status).toBe(\"resolved\");\n    expect(result.report.workspaceId).toBe(1);\n    expect(result.report.pageId).toBe(10);\n    expect(result.updates).toHaveLength(4);\n    expect(result.updates[0].status).toBe(\"investigating\");\n    expect(result.updates[1].status).toBe(\"identified\");\n    expect(result.updates[2].status).toBe(\"monitoring\");\n    expect(result.updates[3].status).toBe(\"resolved\");\n    expect(result.sourceComponentIds).toEqual([\"sp_comp_001\"]);\n  });\n\n  it(\"maps an ongoing incident\", () => {\n    const result = mapIncidentToStatusReport(MOCK_INCIDENTS[1], 1, 10);\n    expect(result.report.status).toBe(\"identified\");\n    expect(result.updates).toHaveLength(2);\n    expect(result.sourceComponentIds).toEqual([\"sp_comp_003\"]);\n  });\n\n  it(\"appends postmortem to last update\", () => {\n    const result = mapIncidentToStatusReport(MOCK_INCIDENTS[0], 1, 10);\n    const lastUpdate = result.updates[result.updates.length - 1];\n    expect(lastUpdate.message).toContain(\"---\\n\\n**Postmortem**\\n\\n\");\n    expect(lastUpdate.message).toContain(\"## Summary\");\n  });\n});\n\ndescribe(\"mapIncidentUpdateStatus\", () => {\n  it.each([\n    [\"investigating\", \"investigating\"],\n    [\"identified\", \"identified\"],\n    [\"monitoring\", \"monitoring\"],\n    [\"resolved\", \"resolved\"],\n    [\"scheduled\", \"investigating\"],\n    [\"in_progress\", \"investigating\"],\n    [\"verifying\", \"monitoring\"],\n    [\"completed\", \"resolved\"],\n  ] as const)(\"maps %s to %s\", (input, expected) => {\n    expect(mapIncidentUpdateStatus(input)).toBe(expected);\n  });\n});\n\ndescribe(\"mapIncidentToMaintenance\", () => {\n  it(\"maps a scheduled incident to maintenance\", () => {\n    const result = mapIncidentToMaintenance(MOCK_INCIDENTS[2], 1, 10);\n    expect(result.title).toBe(\"Scheduled Database Maintenance\");\n    expect(result.workspaceId).toBe(1);\n    expect(result.pageId).toBe(10);\n    expect(result.from).toEqual(new Date(\"2024-06-20T02:00:00.000Z\"));\n    expect(result.to).toEqual(new Date(\"2024-06-20T06:00:00.000Z\"));\n    expect(result.message).toContain(\"scheduled database maintenance\");\n  });\n});\n\ndescribe(\"mapSubscriber\", () => {\n  it(\"maps email subscriber\", () => {\n    const result = mapSubscriber(MOCK_SUBSCRIBERS[0], 10);\n    expect(result).toEqual({\n      email: \"alice@acmecorp.com\",\n      pageId: 10,\n      sourceComponentIds: [],\n    });\n  });\n\n  it(\"maps email subscriber with component subscriptions\", () => {\n    const result = mapSubscriber(MOCK_SUBSCRIBERS[1], 10);\n    expect(result).toEqual({\n      email: \"bob@acmecorp.com\",\n      pageId: 10,\n      sourceComponentIds: [\"sp_comp_001\", \"sp_comp_003\"],\n    });\n  });\n\n  it(\"returns null for webhook subscriber\", () => {\n    expect(mapSubscriber(MOCK_SUBSCRIBERS[2], 10)).toBeNull();\n  });\n\n  it(\"returns null for sms subscriber\", () => {\n    expect(mapSubscriber(MOCK_SUBSCRIBERS[3], 10)).toBeNull();\n  });\n\n  it(\"returns null for slack subscriber\", () => {\n    expect(mapSubscriber(MOCK_SUBSCRIBERS[4], 10)).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/importers/src/providers/statuspage/mapper.ts",
    "content": "import type {\n  StatuspageComponent,\n  StatuspageGroupComponent,\n  StatuspageIncident,\n  StatuspagePage,\n  StatuspageSubscriber,\n} from \"./api-types\";\n\nexport type StatusReportStatus =\n  | \"investigating\"\n  | \"identified\"\n  | \"monitoring\"\n  | \"resolved\";\n\nexport function mapPage(page: StatuspagePage, workspaceId: number) {\n  return {\n    workspaceId,\n    title: page.name,\n    description: page.page_description ?? \"\",\n    slug: page.subdomain,\n    customDomain: page.domain ?? \"\",\n    published: true,\n    icon: \"\",\n  };\n}\n\nexport function mapComponent(\n  component: StatuspageComponent,\n  workspaceId: number,\n  pageId?: number,\n) {\n  return {\n    workspaceId,\n    pageId,\n    type: \"static\" as const,\n    monitorId: null,\n    name: component.name,\n    description: component.description ?? null,\n    order: component.position ?? 0,\n  };\n}\n\nexport function mapComponentGroup(\n  group: StatuspageGroupComponent,\n  workspaceId: number,\n  pageId?: number,\n) {\n  return {\n    workspaceId,\n    pageId,\n    name: group.name,\n  };\n}\n\nconst INCIDENT_UPDATE_STATUS_MAP: Record<string, StatusReportStatus> = {\n  investigating: \"investigating\",\n  identified: \"identified\",\n  monitoring: \"monitoring\",\n  resolved: \"resolved\",\n  scheduled: \"investigating\",\n  in_progress: \"investigating\",\n  verifying: \"monitoring\",\n  completed: \"resolved\",\n};\n\nexport function mapIncidentUpdateStatus(status: string): StatusReportStatus {\n  return INCIDENT_UPDATE_STATUS_MAP[status] ?? \"investigating\";\n}\n\nexport function isScheduledIncident(incident: StatuspageIncident): boolean {\n  return incident.scheduled_for != null;\n}\n\nexport function mapIncidentToStatusReport(\n  incident: StatuspageIncident,\n  workspaceId: number,\n  pageId?: number,\n) {\n  const updates = [...(incident.incident_updates ?? [])].sort(\n    (a, b) =>\n      new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),\n  );\n\n  const mappedUpdates = updates.map((u) => ({\n    status: mapIncidentUpdateStatus(u.status),\n    message: u.body ?? \"\",\n    date: new Date(u.created_at),\n  }));\n\n  if (incident.postmortem_body && mappedUpdates.length > 0) {\n    const last = mappedUpdates[mappedUpdates.length - 1];\n    last.message = `${last.message}\\n\\n---\\n\\n**Postmortem**\\n\\n${incident.postmortem_body}`;\n  }\n\n  const lastUpdate = updates[updates.length - 1];\n  const reportStatus = lastUpdate\n    ? mapIncidentUpdateStatus(lastUpdate.status)\n    : \"investigating\";\n\n  const sourceComponentIds: string[] = [];\n  if (incident.components) {\n    for (const comp of incident.components) {\n      sourceComponentIds.push(comp.id);\n    }\n  }\n\n  return {\n    report: {\n      title: incident.name,\n      status: reportStatus,\n      workspaceId,\n      pageId,\n    },\n    updates: mappedUpdates,\n    sourceComponentIds,\n  };\n}\n\nexport function mapIncidentToMaintenance(\n  incident: StatuspageIncident,\n  workspaceId: number,\n  pageId?: number,\n) {\n  const updates = incident.incident_updates ?? [];\n  const message = updates.map((u) => u.body ?? \"\").join(\"\\n\");\n\n  if (!incident.scheduled_for) {\n    throw new Error(\n      `Incident \"${incident.name}\" is missing scheduled_for date`,\n    );\n  }\n\n  return {\n    title: incident.name,\n    message,\n    from: new Date(incident.scheduled_for),\n    to: new Date(incident.scheduled_until ?? incident.scheduled_for),\n    workspaceId,\n    pageId,\n  };\n}\n\nexport function mapSubscriber(\n  subscriber: StatuspageSubscriber,\n  pageId?: number,\n): {\n  email: string;\n  pageId?: number;\n  sourceComponentIds: string[];\n} | null {\n  if (subscriber.mode !== \"email\") return null;\n\n  return {\n    email: subscriber.email ?? \"\",\n    pageId,\n    sourceComponentIds: subscriber.components ?? [],\n  };\n}\n"
  },
  {
    "path": "packages/importers/src/providers/statuspage/provider.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, it } from \"bun:test\";\nimport {\n  MOCK_COMPONENTS,\n  MOCK_COMPONENT_GROUPS,\n  MOCK_INCIDENTS,\n  MOCK_PAGES,\n  MOCK_SUBSCRIBERS,\n} from \"./fixtures\";\nimport {\n  type StatuspageImportConfig,\n  createStatuspageProvider,\n} from \"./provider\";\n\nfunction createMockFetch(options?: { failAuth?: boolean }) {\n  return (input: string | URL | Request): Promise<Response> => {\n    const url = typeof input === \"string\" ? input : input.toString();\n\n    if (options?.failAuth) {\n      return Promise.resolve(\n        new Response(JSON.stringify({ error: \"Unauthorized\" }), {\n          status: 401,\n          statusText: \"Unauthorized\",\n        }),\n      );\n    }\n\n    if (url.endsWith(\"/pages\")) {\n      return Promise.resolve(Response.json(MOCK_PAGES));\n    }\n    if (url.match(/\\/pages\\/[^/]+\\/components(\\?|$)/)) {\n      const pageParam = new URL(url).searchParams.get(\"page\");\n      return Promise.resolve(\n        Response.json(pageParam === \"1\" || !pageParam ? MOCK_COMPONENTS : []),\n      );\n    }\n    if (url.match(/\\/pages\\/[^/]+\\/component-groups(\\?|$)/)) {\n      const pageParam = new URL(url).searchParams.get(\"page\");\n      return Promise.resolve(\n        Response.json(\n          pageParam === \"1\" || !pageParam ? MOCK_COMPONENT_GROUPS : [],\n        ),\n      );\n    }\n    if (url.match(/\\/pages\\/[^/]+\\/incidents(\\?|$)/)) {\n      const pageParam = new URL(url).searchParams.get(\"page\");\n      return Promise.resolve(\n        Response.json(pageParam === \"1\" || !pageParam ? MOCK_INCIDENTS : []),\n      );\n    }\n    if (url.match(/\\/pages\\/[^/]+\\/subscribers(\\?|$)/)) {\n      const pageParam = new URL(url).searchParams.get(\"page\");\n      return Promise.resolve(\n        Response.json(pageParam === \"1\" || !pageParam ? MOCK_SUBSCRIBERS : []),\n      );\n    }\n\n    return Promise.resolve(\n      new Response(\"Not Found\", { status: 404, statusText: \"Not Found\" }),\n    );\n  };\n}\n\ndescribe(\"StatuspageImportProvider\", () => {\n  const originalFetch = globalThis.fetch;\n\n  beforeAll(() => {\n    globalThis.fetch = createMockFetch() as typeof fetch;\n  });\n\n  afterAll(() => {\n    globalThis.fetch = originalFetch;\n  });\n\n  it(\"has name 'statuspage'\", () => {\n    const provider = createStatuspageProvider();\n    expect(provider.name).toBe(\"statuspage\");\n  });\n\n  it(\"validate returns valid for working key\", async () => {\n    const provider = createStatuspageProvider();\n    const result = await provider.validate({\n      apiKey: \"test-key\",\n      workspaceId: 1,\n    });\n    expect(result).toEqual({ valid: true });\n  });\n\n  it(\"validate returns error for 401\", async () => {\n    const prev = globalThis.fetch;\n    globalThis.fetch = createMockFetch({ failAuth: true }) as typeof fetch;\n\n    const provider = createStatuspageProvider();\n    const result = await provider.validate({\n      apiKey: \"bad-key\",\n      workspaceId: 1,\n    });\n    expect(result.valid).toBe(false);\n    expect(result.error).toBeDefined();\n\n    globalThis.fetch = prev;\n  });\n\n  it(\"run with dryRun returns correct phase counts\", async () => {\n    const provider = createStatuspageProvider();\n    const config: StatuspageImportConfig = {\n      apiKey: \"test-key\",\n      workspaceId: 1,\n      dryRun: true,\n    };\n\n    const summary = await provider.run(config);\n\n    expect(summary.provider).toBe(\"statuspage\");\n    expect(summary.status).toBe(\"completed\");\n\n    const findPhase = (name: string) =>\n      summary.phases.find((p) => p.phase === name);\n\n    // 1 page\n    expect(findPhase(\"page\")?.resources).toHaveLength(1);\n\n    // 1 component group\n    expect(findPhase(\"componentGroups\")?.resources).toHaveLength(1);\n\n    // 4 components\n    expect(findPhase(\"components\")?.resources).toHaveLength(4);\n\n    // 2 realtime incidents (incident_001 and incident_002; incident_003 is scheduled)\n    expect(findPhase(\"incidents\")?.resources).toHaveLength(2);\n\n    // 1 maintenance (incident_003)\n    expect(findPhase(\"maintenances\")?.resources).toHaveLength(1);\n\n    // 2 subscribers: 2 email only (webhook + sms + slack skipped)\n    expect(findPhase(\"subscribers\")?.resources).toHaveLength(2);\n  });\n});\n"
  },
  {
    "path": "packages/importers/src/providers/statuspage/provider.ts",
    "content": "import type {\n  ImportConfig,\n  ImportProvider,\n  PhaseResult,\n  ResourceResult,\n} from \"../../types\";\nimport type { StatuspageIncident } from \"./api-types\";\nimport { createStatuspageClient } from \"./client\";\nimport {\n  isScheduledIncident,\n  mapComponent,\n  mapComponentGroup,\n  mapIncidentToMaintenance,\n  mapIncidentToStatusReport,\n  mapPage,\n  mapSubscriber,\n} from \"./mapper\";\n\nexport interface StatuspageImportConfig extends ImportConfig {\n  statuspagePageId?: string;\n}\n\nexport function createStatuspageProvider(): ImportProvider<StatuspageImportConfig> {\n  return {\n    name: \"statuspage\",\n\n    validate: async (config) => {\n      try {\n        const client = createStatuspageClient(config.apiKey);\n        await client.getPages();\n        return { valid: true };\n      } catch (err) {\n        return {\n          valid: false,\n          error: err instanceof Error ? err.message : String(err),\n        };\n      }\n    },\n\n    run: async (config) => {\n      const startedAt = new Date();\n      const client = createStatuspageClient(config.apiKey);\n      const phases: PhaseResult[] = [];\n\n      let pages = await client.getPages();\n      if (config.statuspagePageId) {\n        pages = pages.filter((p) => p.id === config.statuspagePageId);\n      }\n\n      let skippedSubscribers = 0;\n\n      for (const page of pages) {\n        const [components, groups, incidents, subscribers] = await Promise.all([\n          client.getComponents(page.id),\n          client.getComponentGroups(page.id),\n          client.getIncidents(page.id),\n          client.getSubscribers(page.id),\n        ]);\n\n        // Page phase\n        const mappedPage = mapPage(page, config.workspaceId);\n        phases.push({\n          phase: \"page\",\n          status: \"completed\",\n          resources: [\n            {\n              sourceId: page.id,\n              name: page.name,\n              status: \"created\",\n              data: mappedPage,\n            },\n          ],\n        });\n\n        const pageId = config.pageId;\n\n        // Component groups phase\n        const groupResources: ResourceResult[] = groups.map((g) => ({\n          sourceId: g.id,\n          name: g.name,\n          status: \"created\" as const,\n          data: mapComponentGroup(g, config.workspaceId, pageId),\n        }));\n        phases.push({\n          phase: \"componentGroups\",\n          status: \"completed\",\n          resources: groupResources,\n        });\n\n        // Components phase\n        const componentResources: ResourceResult[] = components.map((c) => ({\n          sourceId: c.id,\n          name: c.name,\n          status: \"created\" as const,\n          data: {\n            ...mapComponent(c, config.workspaceId, pageId),\n            sourceGroupId: c.group_id,\n          },\n        }));\n        phases.push({\n          phase: \"components\",\n          status: \"completed\",\n          resources: componentResources,\n        });\n\n        // Split incidents\n        const realtimeIncidents: StatuspageIncident[] = [];\n        const scheduledIncidents: StatuspageIncident[] = [];\n        for (const inc of incidents) {\n          if (isScheduledIncident(inc)) {\n            scheduledIncidents.push(inc);\n          } else {\n            realtimeIncidents.push(inc);\n          }\n        }\n\n        // Incidents phase (status reports)\n        const incidentResources: ResourceResult[] = realtimeIncidents.map(\n          (inc) => ({\n            sourceId: inc.id,\n            name: inc.name,\n            status: \"created\" as const,\n            data: mapIncidentToStatusReport(inc, config.workspaceId, pageId),\n          }),\n        );\n        phases.push({\n          phase: \"incidents\",\n          status: \"completed\",\n          resources: incidentResources,\n        });\n\n        // Maintenances phase\n        const maintenanceResources: ResourceResult[] = scheduledIncidents.map(\n          (inc) => ({\n            sourceId: inc.id,\n            name: inc.name,\n            status: \"created\" as const,\n            data: {\n              ...mapIncidentToMaintenance(inc, config.workspaceId, pageId),\n              sourceComponentIds: (inc.components ?? []).map((c) => c.id),\n            },\n          }),\n        );\n        phases.push({\n          phase: \"maintenances\",\n          status: \"completed\",\n          resources: maintenanceResources,\n        });\n\n        // Subscribers phase\n        const subscriberResources: ResourceResult[] = [];\n        for (const sub of subscribers) {\n          const mapped = mapSubscriber(sub, pageId);\n          if (mapped) {\n            subscriberResources.push({\n              sourceId: sub.id,\n              name: sub.email ?? sub.endpoint ?? sub.id,\n              status: \"created\",\n              data: mapped,\n            });\n          } else {\n            skippedSubscribers++;\n          }\n        }\n        phases.push({\n          phase: \"subscribers\",\n          status: \"completed\",\n          resources: subscriberResources,\n        });\n      }\n\n      const errors: string[] = [];\n      if (skippedSubscribers > 0) {\n        errors.push(\n          `Only email subscribers are supported. ${skippedSubscribers} non-email subscriber${skippedSubscribers === 1 ? \" was\" : \"s were\"} skipped.`,\n        );\n      }\n\n      return {\n        provider: \"statuspage\",\n        status: \"completed\",\n        startedAt,\n        completedAt: new Date(),\n        phases,\n        errors,\n      };\n    },\n  };\n}\n"
  },
  {
    "path": "packages/importers/src/types.ts",
    "content": "/**\n * Result of importing a single resource.\n */\nexport type ResourceResult = {\n  sourceId: string;\n  openstatusId?: number;\n  name: string;\n  status: \"created\" | \"skipped\" | \"failed\";\n  error?: string;\n  data?: unknown;\n};\n\n/**\n * Result of a single import phase (e.g., \"components\", \"incidents\").\n */\nexport type PhaseResult = {\n  phase: string;\n  status: \"completed\" | \"partial\" | \"failed\" | \"skipped\";\n  resources: ResourceResult[];\n};\n\n/**\n * Summary returned after a full import completes.\n */\nexport type ImportSummary = {\n  provider: string;\n  status: \"completed\" | \"partial\" | \"failed\";\n  startedAt: Date;\n  completedAt: Date;\n  phases: PhaseResult[];\n  errors: string[];\n};\n\n/**\n * Configuration passed to any import provider.\n */\nexport type ImportConfig = {\n  apiKey: string;\n  workspaceId: number;\n  pageId?: number;\n  dryRun?: boolean;\n};\n\n/**\n * Every import provider must conform to this type.\n */\nexport type ImportProvider<TConfig extends ImportConfig = ImportConfig> = {\n  name: string;\n  validate: (config: TConfig) => Promise<{ valid: boolean; error?: string }>;\n  run: (config: TConfig) => Promise<ImportSummary>;\n};\n"
  },
  {
    "path": "packages/importers/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/notifications/base/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-base\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Shared types and utilities for OpenStatus notification providers\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/regions\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/notifications/base/src/index.ts",
    "content": "// Types\nexport type {\n  NotificationContext,\n  FormattedMessageData,\n  NotificationType,\n} from \"./types\";\n\n// Utilities\nexport { formatDuration, calculateDuration } from \"./utils/duration\";\nexport { formatTimestamp } from \"./utils/timestamp\";\nexport { getIncidentDuration } from \"./utils/incident\";\nexport { formatStatusCode, buildCommonMessageData } from \"./utils/message\";\nexport { COLORS, COLOR_DECIMALS } from \"./utils/colors\";\n"
  },
  {
    "path": "packages/notifications/base/src/types.ts",
    "content": "import type {\n  Incident,\n  Monitor,\n  Notification,\n} from \"@openstatus/db/src/schema\";\n\n/**\n * Common context passed to all notification providers\n */\nexport interface NotificationContext {\n  monitor: Monitor;\n  notification: Notification;\n  statusCode?: number;\n  message?: string;\n  cronTimestamp: number;\n  regions?: string[];\n  latency?: number;\n  incident?: Incident;\n}\n\n/**\n * Formatted common message data ready for rendering\n */\nexport interface FormattedMessageData {\n  monitorName: string;\n  monitorUrl: string;\n  monitorMethod?: string;\n  monitorJobType: string;\n  statusCodeFormatted: string;\n  errorMessage: string;\n  timestampFormatted: string;\n  regionsDisplay: string;\n  latencyDisplay: string;\n  dashboardUrl: string;\n  incidentDuration?: string;\n}\n\n/**\n * Notification type discriminator\n */\nexport type NotificationType = \"alert\" | \"recovery\" | \"degraded\";\n"
  },
  {
    "path": "packages/notifications/base/src/utils/colors.ts",
    "content": "type Color = \"red\" | \"yellow\" | \"green\" | \"blue\";\n\nexport const COLORS = {\n  red: \"#e7000b\", // Alert/Error - red left border\n  yellow: \"#f49f1e\", // Degraded/Warning - yellow/orange left border\n  green: \"#20c45f\", // Recovery/Success - green left border\n  blue: \"#3a81f6\", // Monitoring - blue left border\n} as const satisfies Record<Color, string>;\n\nexport const COLOR_DECIMALS = {\n  red: 15138827, // Alert/Error - red left border\n  yellow: 16031518, // Degraded/Warning - yellow/orange left border\n  green: 2147423, // Recovery/Success - green left border\n  blue: 3834358, // Monitoring - blue left border\n} as const satisfies Record<Color, number>;\n"
  },
  {
    "path": "packages/notifications/base/src/utils/duration.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\";\nimport { calculateDuration, formatDuration } from \"./duration\";\n\ndescribe(\"formatDuration\", () => {\n  it(\"formats short durations (seconds only)\", () => {\n    expect(formatDuration(30000)).toBe(\"30s\");\n  });\n\n  it(\"formats medium durations (minutes and seconds)\", () => {\n    expect(formatDuration(135000)).toBe(\"2m 15s\");\n  });\n\n  it(\"formats long durations (hours, minutes, seconds)\", () => {\n    expect(formatDuration(8130000)).toBe(\"2h 15m 30s\");\n  });\n\n  it(\"formats very long durations (days and hours)\", () => {\n    expect(formatDuration(90000000)).toBe(\"1d 1h\");\n  });\n\n  it(\"handles 0ms\", () => {\n    expect(formatDuration(0)).toBe(\"0s\");\n  });\n\n  it(\"handles negative values\", () => {\n    expect(formatDuration(-1000)).toBe(\"0s\");\n  });\n\n  it(\"handles sub-second durations\", () => {\n    expect(formatDuration(500)).toBe(\"0s\");\n  });\n\n  it(\"respects maxUnits option\", () => {\n    expect(formatDuration(90061000, { maxUnits: 2 })).toBe(\"1d 1h\");\n    expect(formatDuration(90061000, { maxUnits: 1 })).toBe(\"1d\");\n  });\n\n  it(\"only shows non-zero units\", () => {\n    expect(formatDuration(3600000)).toBe(\"1h\");\n    expect(formatDuration(60000)).toBe(\"1m\");\n    expect(formatDuration(86400000)).toBe(\"1d\");\n  });\n});\n\ndescribe(\"calculateDuration\", () => {\n  it(\"calculates duration between two dates\", () => {\n    const start = new Date(\"2026-01-22T10:00:00Z\");\n    const end = new Date(\"2026-01-22T12:15:30Z\");\n    expect(calculateDuration(start, end)).toBe(8130000);\n  });\n\n  it(\"handles same start and end\", () => {\n    const date = new Date(\"2026-01-22T10:00:00Z\");\n    expect(calculateDuration(date, date)).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/notifications/base/src/utils/duration.ts",
    "content": "/**\n * Format milliseconds to human-readable duration\n *\n * @example\n * formatDuration(30000) // \"30s\"\n * formatDuration(135000) // \"2m 15s\"\n * formatDuration(8130000) // \"2h 15m 30s\"\n * formatDuration(90000000) // \"1d 1h\"\n *\n * @param durationMs - Duration in milliseconds\n * @param options - Formatting options\n * @param options.maxUnits - Max number of units to show (default: 3)\n * @returns Formatted duration string\n */\nexport function formatDuration(\n  durationMs: number,\n  options?: {\n    maxUnits?: number;\n  },\n): string {\n  const maxUnits = options?.maxUnits ?? 3;\n\n  if (durationMs < 0) {\n    return \"0s\";\n  }\n\n  const totalSeconds = Math.floor(durationMs / 1000);\n\n  if (totalSeconds === 0) {\n    return \"0s\";\n  }\n\n  const days = Math.floor(totalSeconds / 86400);\n  const hours = Math.floor((totalSeconds % 86400) / 3600);\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\n  const seconds = totalSeconds % 60;\n\n  const parts: string[] = [];\n\n  if (days > 0) {\n    parts.push(`${days}d`);\n  }\n  if (hours > 0) {\n    parts.push(`${hours}h`);\n  }\n  if (minutes > 0) {\n    parts.push(`${minutes}m`);\n  }\n  if (seconds > 0) {\n    parts.push(`${seconds}s`);\n  }\n\n  // Take only the first maxUnits parts\n  return parts.slice(0, maxUnits).join(\" \");\n}\n\n/**\n * Calculate duration between two timestamps\n *\n * @param start - Start date\n * @param end - End date\n * @returns Duration in milliseconds\n */\nexport function calculateDuration(start: Date, end: Date): number {\n  return end.getTime() - start.getTime();\n}\n"
  },
  {
    "path": "packages/notifications/base/src/utils/incident.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\";\nimport type { Incident } from \"@openstatus/db/src/schema\";\nimport { getIncidentDuration } from \"./incident\";\n\n// Helper to create a partial incident object for testing\nfunction createIncident(overrides: Partial<Incident>): Incident {\n  return {\n    id: 1,\n    monitorId: 1,\n    workspaceId: 1,\n    startedAt: null,\n    resolvedAt: null,\n    autoResolved: false,\n    acknowledgedAt: null,\n    acknowledgedBy: null,\n    ...overrides,\n  } as Incident;\n}\n\ndescribe(\"getIncidentDuration\", () => {\n  it(\"returns null when incident.startedAt is missing\", () => {\n    const incident = createIncident({\n      startedAt: null,\n      resolvedAt: new Date(\"2026-01-22T12:00:00Z\"),\n    });\n    expect(getIncidentDuration(incident)).toBe(null);\n  });\n\n  it(\"returns null when incident.resolvedAt is null (ongoing incident)\", () => {\n    const incident = createIncident({\n      startedAt: new Date(\"2026-01-22T10:00:00Z\"),\n      resolvedAt: null,\n    });\n    expect(getIncidentDuration(incident)).toBe(null);\n  });\n\n  it(\"calculates correct duration for resolved incident\", () => {\n    // 2h 15m 30s = 8130000ms\n    const incident = createIncident({\n      startedAt: new Date(\"2026-01-22T10:00:00Z\"),\n      resolvedAt: new Date(\"2026-01-22T12:15:30Z\"),\n    });\n    expect(getIncidentDuration(incident)).toBe(\"2h 15m 30s\");\n  });\n\n  it(\"calculates duration with Date objects\", () => {\n    const incident = createIncident({\n      startedAt: new Date(\"2026-01-22T10:00:00Z\"),\n      resolvedAt: new Date(\"2026-01-22T10:30:00Z\"),\n    });\n    expect(getIncidentDuration(incident)).toBe(\"30m\");\n  });\n\n  it(\"calculates duration with timestamp numbers\", () => {\n    // 5 minutes = 300000ms\n    const startTime = new Date(\"2026-01-22T10:00:00Z\").getTime();\n    const endTime = new Date(\"2026-01-22T10:05:00Z\").getTime();\n    const incident = createIncident({\n      startedAt: startTime as unknown as Date,\n      resolvedAt: endTime as unknown as Date,\n    });\n    expect(getIncidentDuration(incident)).toBe(\"5m\");\n  });\n\n  it(\"handles very short durations (less than a minute)\", () => {\n    const incident = createIncident({\n      startedAt: new Date(\"2026-01-22T10:00:00Z\"),\n      resolvedAt: new Date(\"2026-01-22T10:00:45Z\"),\n    });\n    expect(getIncidentDuration(incident)).toBe(\"45s\");\n  });\n\n  it(\"handles very long durations (over a day)\", () => {\n    // 1 day, 2 hours = 26 hours = 93600000ms\n    const incident = createIncident({\n      startedAt: new Date(\"2026-01-21T10:00:00Z\"),\n      resolvedAt: new Date(\"2026-01-22T12:00:00Z\"),\n    });\n    expect(getIncidentDuration(incident)).toBe(\"1d 2h\");\n  });\n\n  it(\"handles same start and end time (0 duration)\", () => {\n    const timestamp = new Date(\"2026-01-22T10:00:00Z\");\n    const incident = createIncident({\n      startedAt: timestamp,\n      resolvedAt: timestamp,\n    });\n    expect(getIncidentDuration(incident)).toBe(\"0s\");\n  });\n\n  it(\"returns null when resolvedAt is before startedAt (negative duration)\", () => {\n    const incident = createIncident({\n      startedAt: new Date(\"2026-01-22T12:00:00Z\"),\n      resolvedAt: new Date(\"2026-01-22T10:00:00Z\"),\n    });\n    expect(getIncidentDuration(incident)).toBe(null);\n  });\n});\n"
  },
  {
    "path": "packages/notifications/base/src/utils/incident.ts",
    "content": "import type { Incident } from \"@openstatus/db/src/schema\";\nimport { formatDuration } from \"./duration\";\n\n/**\n * Get formatted incident duration\n *\n * Returns duration string only if incident is resolved (has resolvedAt timestamp).\n * Returns null for ongoing incidents or if startedAt is missing.\n *\n * @example\n * // Resolved incident\n * getIncidentDuration({\n *   startedAt: new Date('2026-01-22T10:00:00Z'),\n *   resolvedAt: new Date('2026-01-22T12:15:30Z')\n * }) // \"2h 15m 30s\"\n *\n * // Ongoing incident\n * getIncidentDuration({\n *   startedAt: new Date('2026-01-22T10:00:00Z'),\n *   resolvedAt: null\n * }) // null\n *\n * @param incident - The incident object\n * @returns Formatted duration string or null if incident is not resolved\n */\nexport function getIncidentDuration(incident: Incident): string | null {\n  if (!incident.startedAt) {\n    return null;\n  }\n\n  // Only calculate duration for resolved incidents\n  if (!incident.resolvedAt) {\n    return null;\n  }\n\n  const startTime =\n    incident.startedAt instanceof Date\n      ? incident.startedAt.getTime()\n      : incident.startedAt;\n\n  const endTime =\n    incident.resolvedAt instanceof Date\n      ? incident.resolvedAt.getTime()\n      : incident.resolvedAt;\n\n  const durationMs = endTime - startTime;\n\n  if (durationMs < 0) {\n    return null;\n  }\n\n  return formatDuration(durationMs);\n}\n"
  },
  {
    "path": "packages/notifications/base/src/utils/message.ts",
    "content": "import type { Incident } from \"@openstatus/db/src/schema\";\nimport { getRegionInfo } from \"@openstatus/regions\";\nimport type { FormattedMessageData, NotificationContext } from \"../types\";\nimport { getIncidentDuration } from \"./incident\";\nimport { formatTimestamp } from \"./timestamp\";\n\n/**\n * Common HTTP status descriptions\n */\nconst statusDescriptions: Record<number, string> = {\n  200: \"OK\",\n  201: \"Created\",\n  204: \"No Content\",\n  301: \"Moved Permanently\",\n  302: \"Found\",\n  304: \"Not Modified\",\n  400: \"Bad Request\",\n  401: \"Unauthorized\",\n  403: \"Forbidden\",\n  404: \"Not Found\",\n  405: \"Method Not Allowed\",\n  408: \"Request Timeout\",\n  429: \"Too Many Requests\",\n  500: \"Internal Server Error\",\n  502: \"Bad Gateway\",\n  503: \"Service Unavailable\",\n  504: \"Gateway Timeout\",\n};\n\n/**\n * Format status code for display with human-readable description\n *\n * @example\n * formatStatusCode(503) // \"503 Service Unavailable\"\n * formatStatusCode(404) // \"404 Not Found\"\n * formatStatusCode(418) // \"418\" (no description available)\n * formatStatusCode(undefined) // \"Unknown\"\n *\n * @param statusCode - HTTP status code\n * @returns Formatted status code string\n */\nexport function formatStatusCode(statusCode?: number): string {\n  if (!statusCode) {\n    return \"Unknown\";\n  }\n\n  const description = statusDescriptions[statusCode];\n  return description ? `${statusCode} ${description}` : `${statusCode}`;\n}\n\n/**\n * Build common formatted message data from notification context\n *\n * Centralizes formatting logic used by all providers to ensure consistency.\n *\n * @example\n * const data = buildCommonMessageData(context);\n * // Returns: {\n * //   monitorName: \"My API\",\n * //   monitorUrl: \"https://api.example.com\",\n * //   monitorMethod: \"GET\",\n * //   monitorJobType: \"http\",\n * //   statusCodeFormatted: \"503 Service Unavailable\",\n * //   errorMessage: \"Connection timeout\",\n * //   timestampFormatted: \"Jan 22, 2026 at 14:30 UTC\",\n * //   regionsDisplay: \"ams, fra, syd\",\n * //   latencyDisplay: \"2,450ms\",\n * //   dashboardUrl: \"https://app.openstatus.dev/monitors/123\",\n * //   incidentDuration: undefined\n * // }\n *\n * @param context - Notification context with monitor and event data\n * @param options - Optional configuration\n * @param options.incident - Include incident data for duration calculation\n * @returns Formatted message data ready for rendering\n */\nexport function buildCommonMessageData(\n  context: NotificationContext,\n  options?: {\n    incident?: Incident;\n  },\n): FormattedMessageData {\n  const { monitor, statusCode, message, cronTimestamp, regions, latency } =\n    context;\n\n  // Format multiple regions as comma-separated list\n  let regionsDisplay = \"Unknown\";\n  if (regions && regions.length > 0) {\n    if (regions.length === 1) {\n      // Single region: show code and location\n      const regionInfo = getRegionInfo(regions[0]);\n      regionsDisplay = regionInfo\n        ? `${regionInfo.code} (${regionInfo.location})`\n        : regions[0];\n    } else {\n      // Multiple regions: show comma-separated codes\n      regionsDisplay = regions.join(\", \");\n    }\n  }\n\n  // Calculate incident duration only if incident is resolved\n  let incidentDuration: string | undefined;\n  if (options?.incident?.resolvedAt) {\n    const duration = getIncidentDuration(options.incident);\n    incidentDuration = duration ?? undefined;\n  }\n\n  return {\n    monitorName: monitor.name,\n    monitorUrl: monitor.url,\n    monitorMethod: monitor.method ?? undefined,\n    monitorJobType: monitor.jobType,\n    statusCodeFormatted: formatStatusCode(statusCode),\n    errorMessage: message || \"No error message available\",\n    timestampFormatted: formatTimestamp(cronTimestamp),\n    regionsDisplay,\n    latencyDisplay:\n      typeof latency === \"number\" ? `${latency.toLocaleString()}ms` : \"N/A\",\n    dashboardUrl: `https://app.openstatus.dev/monitors/${monitor.id}`,\n    incidentDuration,\n  };\n}\n"
  },
  {
    "path": "packages/notifications/base/src/utils/timestamp.ts",
    "content": "/**\n * Format cron timestamp (epoch ms) to ISO string\n *\n * @example\n * formatTimestamp(1737553800000) // \"2026-01-22T14:30:00.000Z\"\n *\n * @param cronTimestamp - Epoch timestamp in milliseconds\n * @returns Formatted timestamp string to ISO string\n */\nexport function formatTimestamp(cronTimestamp: number): string {\n  if (!cronTimestamp || !Number.isFinite(cronTimestamp)) {\n    return \"Unknown\";\n  }\n\n  const date = new Date(cronTimestamp);\n\n  if (Number.isNaN(date.getTime())) {\n    return \"Unknown\";\n  }\n\n  return date.toISOString();\n}\n"
  },
  {
    "path": "packages/notifications/base/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"exclude\": [\"**/*.test.ts\"]\n}\n"
  },
  {
    "path": "packages/notifications/discord/.gitignore",
    "content": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\n\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# Runtime data\n\npids\n_.pid\n_.seed\n\\*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\n\nlib-cov\n\n# Coverage directory used by tools like istanbul\n\ncoverage\n\\*.lcov\n\n# nyc test coverage\n\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n\n.grunt\n\n# Bower dependency directory (https://bower.io/)\n\nbower_components\n\n# node-waf configuration\n\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\n\nbuild/Release\n\n# Dependency directories\n\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\n\nweb_modules/\n\n# TypeScript cache\n\n\\*.tsbuildinfo\n\n# Optional npm cache directory\n\n.npm\n\n# Optional eslint cache\n\n.eslintcache\n\n# Optional stylelint cache\n\n.stylelintcache\n\n# Microbundle cache\n\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n\n.node_repl_history\n\n# Output of 'npm pack'\n\n\\*.tgz\n\n# Yarn Integrity file\n\n.yarn-integrity\n\n# dotenv environment variable files\n\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n\n.cache\n.parcel-cache\n\n# Next.js build output\n\n.next\nout\n\n# Nuxt.js build / generate output\n\n.nuxt\ndist\n\n# Gatsby files\n\n.cache/\n\n# Comment in the public line in if your project uses Gatsby and not Next.js\n\n# https://nextjs.org/blog/next-9-1#public-directory-support\n\n# public\n\n# vuepress build output\n\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n\n.temp\n.cache\n\n# Docusaurus cache and generated files\n\n.docusaurus\n\n# Serverless directories\n\n.serverless/\n\n# FuseBox cache\n\n.fusebox/\n\n# DynamoDB Local files\n\n.dynamodb/\n\n# TernJS port file\n\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.\\*\n"
  },
  {
    "path": "packages/notifications/discord/README.md",
    "content": "# @openstatus/notifications-discord\n\nTo install dependencies:\n\n```bash\nbun install\n```\n\nTo run:\n\n```bash\nbun run src/index.ts\n```\n\nThis project was created using `bun init` in bun v1.0.0. [Bun](https://bun.sh)\nis a fast all-in-one JavaScript runtime.\n"
  },
  {
    "path": "packages/notifications/discord/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-discord\",\n  \"version\": \"1.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/discord/src/embeds.ts",
    "content": "import {\n  COLOR_DECIMALS,\n  type FormattedMessageData,\n} from \"@openstatus/notification-base\";\n\n/**\n * Discord Embed structure for webhook messages\n * Reference: https://birdie0.github.io/discord-webhooks-guide/structure/embeds.html\n */\n\ninterface DiscordEmbedField {\n  name: string;\n  value: string;\n  inline?: boolean;\n}\n\ninterface DiscordEmbedFooter {\n  text: string;\n}\n\nexport interface DiscordEmbed {\n  title: string;\n  description: string;\n  color: number;\n  fields: DiscordEmbedField[];\n  timestamp: string;\n  footer: DiscordEmbedFooter;\n  url: string;\n}\n\n/**\n * Builds Discord embed for alert notifications\n *\n * Layout:\n * - Title: \"{monitor.name} is failing\"\n * - Description: \"METHOD URL\" in code format (e.g., `GET https://api.example.com`)\n * - Color: Red (#ED4245 / 15548997)\n * - Fields: Status Code, Regions, Latency, Cron Timestamp (inline), Error Message in code block (full width)\n * - Timestamp: ISO 8601 format\n * - Footer: \"openstatus\"\n * - URL: Dashboard link\n *\n * @param data - Formatted message data from buildCommonMessageData\n * @returns DiscordEmbed object ready for webhook payload\n *\n * @example\n * const embed = buildAlertEmbed({\n *   monitorName: \"API Health\",\n *   monitorUrl: \"https://api.example.com\",\n *   statusCodeFormatted: \"503 Service Unavailable\",\n *   errorMessage: \"Connection timeout\",\n *   timestampFormatted: \"Jan 22, 2026 at 14:30 UTC\",\n *   regionDisplay: \"iad (Virginia)\",\n *   latencyDisplay: \"1,234 ms\",\n *   dashboardUrl: \"https://app.openstatus.dev/monitors/123\"\n * });\n */\nexport function buildAlertEmbed(data: FormattedMessageData): DiscordEmbed {\n  // Format description as \"METHOD URL\" or just \"URL\" for non-HTTP\n  const description =\n    data.monitorMethod && data.monitorJobType === \"http\"\n      ? `${data.monitorMethod} ${data.monitorUrl}`\n      : data.monitorUrl;\n\n  return {\n    title: `${data.monitorName} is failing`,\n    description: `\\`${description}\\``,\n    color: COLOR_DECIMALS.red,\n    fields: [\n      {\n        name: \"Status Code\",\n        value: data.statusCodeFormatted,\n        inline: true,\n      },\n      {\n        name: \"Regions\",\n        value: data.regionsDisplay,\n        inline: true,\n      },\n      {\n        name: \"Latency\",\n        value: data.latencyDisplay,\n        inline: true,\n      },\n      {\n        name: \"Cron Timestamp\",\n        value: data.timestampFormatted,\n        inline: true,\n      },\n      {\n        name: \"Error Message\",\n        value: `\\`\\`\\`${data.errorMessage}\\`\\`\\``,\n        inline: false,\n      },\n    ],\n    timestamp: new Date().toISOString(),\n    footer: {\n      text: \"openstatus\",\n    },\n    url: data.dashboardUrl,\n  };\n}\n\n/**\n * Builds Discord embed for recovery notifications\n *\n * Layout:\n * - Title: \"{monitor.name} is recovered\"\n * - Description: \"METHOD URL\" in code format (e.g., `GET https://api.example.com`)\n * - Color: Green (#57F287 / 5763719)\n * - Fields: Optional Downtime field (if incidentDuration exists), Status Code, Regions, Latency, Cron Timestamp (inline)\n * - Timestamp: ISO 8601 format\n * - Footer: \"openstatus\"\n * - URL: Dashboard link\n *\n * @param data - Formatted message data from buildCommonMessageData\n * @returns DiscordEmbed object ready for webhook payload\n *\n * @example\n * const embed = buildRecoveryEmbed({\n *   monitorName: \"API Health\",\n *   monitorUrl: \"https://api.example.com\",\n *   statusCodeFormatted: \"200 OK\",\n *   errorMessage: \"\",\n *   timestampFormatted: \"Jan 22, 2026 at 14:35 UTC\",\n *   regionDisplay: \"iad (Virginia)\",\n *   latencyDisplay: \"156 ms\",\n *   dashboardUrl: \"https://app.openstatus.dev/monitors/123\",\n *   incidentDuration: \"5m 30s\"\n * });\n */\nexport function buildRecoveryEmbed(data: FormattedMessageData): DiscordEmbed {\n  const fields: DiscordEmbedField[] = [];\n\n  // Format description as \"METHOD URL\" or just \"URL\" for non-HTTP\n  const description =\n    data.monitorMethod && data.monitorJobType === \"http\"\n      ? `${data.monitorMethod} ${data.monitorUrl}`\n      : data.monitorUrl;\n\n  // Add downtime field if incident duration is available\n  if (data.incidentDuration) {\n    fields.push({\n      name: \"⏱️ Downtime\",\n      value: data.incidentDuration,\n      inline: false,\n    });\n  }\n\n  // Add status fields (inline for 3-column layout)\n  fields.push(\n    {\n      name: \"Status Code\",\n      value: data.statusCodeFormatted,\n      inline: true,\n    },\n    {\n      name: \"Regions\",\n      value: data.regionsDisplay,\n      inline: true,\n    },\n    {\n      name: \"Latency\",\n      value: data.latencyDisplay,\n      inline: true,\n    },\n    {\n      name: \"Cron Timestamp\",\n      value: data.timestampFormatted,\n      inline: true,\n    },\n  );\n\n  return {\n    title: `${data.monitorName} is recovered`,\n    description: `\\`${description}\\``,\n    color: COLOR_DECIMALS.green,\n    fields,\n    timestamp: new Date().toISOString(),\n    footer: {\n      text: \"openstatus\",\n    },\n    url: data.dashboardUrl,\n  };\n}\n\n/**\n * Builds Discord embed for degraded notifications\n *\n * Layout:\n * - Title: \"{monitor.name} is degraded\"\n * - Description: \"METHOD URL\" in code format (e.g., `GET https://api.example.com`)\n * - Color: Yellow (#FEE75C / 16705372)\n * - Fields: Optional Previous Incident Duration field (if incidentDuration exists), Status Code, Regions, Latency, Cron Timestamp (inline)\n * - Timestamp: ISO 8601 format\n * - Footer: \"openstatus\"\n * - URL: Dashboard link\n *\n * @param data - Formatted message data from buildCommonMessageData\n * @returns DiscordEmbed object ready for webhook payload\n *\n * @example\n * const embed = buildDegradedEmbed({\n *   monitorName: \"API Health\",\n *   monitorUrl: \"https://api.example.com\",\n *   statusCodeFormatted: \"504 Gateway Timeout\",\n *   errorMessage: \"Slow response\",\n *   timestampFormatted: \"Jan 22, 2026 at 14:40 UTC\",\n *   regionDisplay: \"iad (Virginia)\",\n *   latencyDisplay: \"5,234 ms\",\n *   dashboardUrl: \"https://app.openstatus.dev/monitors/123\",\n *   incidentDuration: \"2h 15m\"\n * });\n */\nexport function buildDegradedEmbed(data: FormattedMessageData): DiscordEmbed {\n  const fields: DiscordEmbedField[] = [];\n\n  // Format description as \"METHOD URL\" or just \"URL\" for non-HTTP\n  const description =\n    data.monitorMethod && data.monitorJobType === \"http\"\n      ? `${data.monitorMethod} ${data.monitorUrl}`\n      : data.monitorUrl;\n\n  // Add previous incident duration field if available\n  if (data.incidentDuration) {\n    fields.push({\n      name: \"⏱️ Previous Incident Duration\",\n      value: data.incidentDuration,\n      inline: false,\n    });\n  }\n\n  // Add status fields (inline for 3-column layout)\n  fields.push(\n    {\n      name: \"Status Code\",\n      value: data.statusCodeFormatted,\n      inline: true,\n    },\n    {\n      name: \"Regions\",\n      value: data.regionsDisplay,\n      inline: true,\n    },\n    {\n      name: \"Latency\",\n      value: data.latencyDisplay,\n      inline: true,\n    },\n    {\n      name: \"Cron Timestamp\",\n      value: data.timestampFormatted,\n      inline: true,\n    },\n  );\n\n  return {\n    title: `${data.monitorName} is degraded`,\n    description: `\\`${description}\\``,\n    color: COLOR_DECIMALS.yellow,\n    fields,\n    timestamp: new Date().toISOString(),\n    footer: {\n      text: \"openstatus\",\n    },\n    url: data.dashboardUrl,\n  };\n}\n"
  },
  {
    "path": "packages/notifications/discord/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { COLOR_DECIMALS } from \"@openstatus/notification-base\";\nimport {\n  sendAlert,\n  sendDegraded,\n  sendRecovery,\n  sendTestDiscordMessage,\n} from \"./index\";\n\ndescribe(\"Discord Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n\n  beforeEach(() => {\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockRestore();\n    }\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"iad\",\n  });\n\n  const createMockNotification = () => ({\n    id: 1,\n    name: \"Discord Notification\",\n    provider: \"discord\",\n    workspaceId: 1,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    data: '{\"discord\":\"https://discord.com/api/webhooks/123456789/abcdefgh\"}',\n  });\n\n  test(\"Send Alert\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\n      \"https://discord.com/api/webhooks/123456789/abcdefgh\",\n    );\n    expect(callArgs[1].method).toBe(\"POST\");\n    expect(callArgs[1].headers[\"Content-Type\"]).toBe(\"application/json\");\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.embeds).toBeDefined();\n    expect(body.embeds[0].title).toContain(\"is failing\");\n    expect(body.embeds[0].title).toContain(\"API Health Check\");\n    expect(body.embeds[0].color).toBe(COLOR_DECIMALS.red);\n    expect(body.username).toBe(\"OpenStatus Notifications\");\n    expect(body.avatar_url).toBeDefined();\n  });\n\n  test(\"Send Recovery\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 200,\n      message: \"Service recovered\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.embeds).toBeDefined();\n    expect(body.embeds[0].title).toContain(\"is recovered\");\n    expect(body.embeds[0].title).toContain(\"API Health Check\");\n    expect(body.embeds[0].color).toBe(COLOR_DECIMALS.green);\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      message: \"Service degraded\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.embeds).toBeDefined();\n    expect(body.embeds[0].title).toContain(\"is degraded\");\n    expect(body.embeds[0].title).toContain(\"API Health Check\");\n    expect(body.embeds[0].color).toBe(COLOR_DECIMALS.yellow); // Yellow color\n  });\n\n  test(\"Send Test Discord Message\", async () => {\n    const webhookUrl = \"https://discord.com/api/webhooks/123456789/abcdefgh\";\n    await sendTestDiscordMessage(webhookUrl);\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(webhookUrl);\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.embeds).toBeDefined();\n    expect(body.embeds[0].title).toContain(\"Test Notification\");\n  });\n\n  test(\"Send Test Discord Message with empty webhookUrl\", async () => {\n    expect(sendTestDiscordMessage(\"\")).rejects.toThrow();\n    expect(fetchMock).not.toHaveBeenCalled();\n  });\n\n  test(\"Handle fetch error gracefully\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/notifications/discord/src/index.ts",
    "content": "import { discordDataSchema } from \"@openstatus/db/src/schema\";\nimport {\n  COLOR_DECIMALS,\n  type NotificationContext,\n  buildCommonMessageData,\n} from \"@openstatus/notification-base\";\nimport {\n  type DiscordEmbed,\n  buildAlertEmbed,\n  buildDegradedEmbed,\n  buildRecoveryEmbed,\n} from \"./embeds\";\n\nconst postToWebhook = async (embeds: DiscordEmbed[], webhookUrl: string) => {\n  if (!webhookUrl || webhookUrl.trim() === \"\") {\n    throw new Error(\"Discord webhook URL is required\");\n  }\n\n  const res = await fetch(webhookUrl, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      embeds,\n      avatar_url:\n        \"https://img.stackshare.io/service/104872/default_dc6948366d9bae553adbb8f51252eafbc5d2043a.jpg\",\n      username: \"OpenStatus Notifications\",\n    }),\n  });\n  if (!res.ok) {\n    throw new Error(\n      `Failed to send Discord webhook: ${res.status} ${res.statusText}`,\n    );\n  }\n};\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n  cronTimestamp,\n  latency,\n  regions,\n}: NotificationContext) => {\n  const notificationData = discordDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { discord: webhookUrl } = notificationData;\n\n  const context = {\n    monitor,\n    notification,\n    statusCode,\n    message,\n    cronTimestamp,\n    latency,\n    regions,\n  };\n\n  const data = buildCommonMessageData(context);\n  const embed = buildAlertEmbed(data);\n\n  await postToWebhook([embed], webhookUrl);\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n  incident,\n  cronTimestamp,\n  latency,\n  regions,\n}: NotificationContext) => {\n  const notificationData = discordDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { discord: webhookUrl } = notificationData;\n\n  const context = {\n    monitor,\n    notification,\n    statusCode,\n    message,\n    cronTimestamp,\n    latency,\n    regions,\n  };\n\n  const data = buildCommonMessageData(context, { incident });\n  const embed = buildRecoveryEmbed(data);\n\n  await postToWebhook([embed], webhookUrl);\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n  incident,\n  cronTimestamp,\n  latency,\n  regions,\n}: NotificationContext) => {\n  const notificationData = discordDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { discord: webhookUrl } = notificationData;\n\n  const context = {\n    monitor,\n    notification,\n    statusCode,\n    message,\n    cronTimestamp,\n    latency,\n    regions,\n  };\n\n  const data = buildCommonMessageData(context, { incident });\n  const embed = buildDegradedEmbed(data);\n\n  await postToWebhook([embed], webhookUrl);\n};\n\nexport const sendTestDiscordMessage = async (webhookUrl: string) => {\n  const testEmbed: DiscordEmbed = {\n    title: \"Test Notification\",\n    description: \"🧪 Your Discord webhook is configured correctly!\",\n    color: COLOR_DECIMALS.green,\n    fields: [\n      {\n        name: \"Status\",\n        value: \"Webhook Connected\",\n        inline: true,\n      },\n      {\n        name: \"Type\",\n        value: \"Test Notification\",\n        inline: true,\n      },\n      {\n        name: \"Next Steps\",\n        value:\n          \"You will receive notifications here when your monitors trigger fail, recover, or degrades.\",\n        inline: false,\n      },\n    ],\n    timestamp: new Date().toISOString(),\n    footer: {\n      text: \"openstatus\",\n    },\n    url: \"https://www.openstatus.dev/app/\",\n  };\n  await postToWebhook([testEmbed], webhookUrl);\n};\n"
  },
  {
    "path": "packages/notifications/discord/src/mock.ts",
    "content": "import type { Monitor, Notification } from \"@openstatus/db/src/schema\";\nimport {\n  sendAlert,\n  sendDegraded,\n  sendRecovery,\n  sendTestDiscordMessage,\n} from \"./index\";\n\nconst monitor: Monitor = {\n  id: 1,\n  name: \"OpenStatus Docs\",\n  url: \"https://docs.openstatus.dev\",\n  periodicity: \"10m\",\n  jobType: \"http\",\n  active: true,\n  public: true,\n  createdAt: null,\n  updatedAt: null,\n  regions: [\"ams\", \"fra\"],\n  description: \"Monitor Description\",\n  headers: [],\n  body: \"\",\n  workspaceId: 1,\n  timeout: 45000,\n  degradedAfter: null,\n  assertions: null,\n  status: \"active\",\n  method: \"GET\",\n  deletedAt: null,\n  externalName: null,\n  otelEndpoint: null,\n  otelHeaders: [],\n  retry: 3,\n  followRedirects: false,\n};\n\nconst notification: Notification = {\n  id: 1,\n  name: \"Discord\",\n  data: `{ \"discord\": \"${process.env.DISCORD_WEBHOOK}\" }`,\n  createdAt: null,\n  updatedAt: null,\n  workspaceId: 1,\n  provider: \"discord\",\n};\n\nconst cronTimestamp = Date.now();\n\nif (process.env.NODE_ENV === \"development\") {\n  await sendDegraded({\n    monitor,\n    notification,\n    cronTimestamp,\n  });\n\n  await sendAlert({\n    monitor,\n    notification,\n    statusCode: 500,\n    cronTimestamp,\n  });\n\n  await sendRecovery({\n    monitor,\n    notification,\n    cronTimestamp,\n  });\n\n  await sendTestDiscordMessage(`${process.env.DISCORD_WEBHOOK}`);\n}\n"
  },
  {
    "path": "packages/notifications/discord/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/email/README.md",
    "content": "#\n"
  },
  {
    "path": "packages/notifications/email/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    RESEND_API_KEY: z.string().min(1),\n  },\n  runtimeEnv: {\n    RESEND_API_KEY: process.env.RESEND_API_KEY,\n  },\n  skipValidation: process.env.NODE_ENV === \"test\",\n});\n"
  },
  {
    "path": "packages/notifications/email/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-emails\",\n  \"version\": \"0.0.0\",\n  \"main\": \"src/index.ts\",\n  \"description\": \"Log drains Vercel integration.\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/emails\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"@openstatus/regions\": \"workspace:*\",\n    \"@openstatus/tinybird\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:*\",\n    \"@react-email/components\": \"1.0.3\",\n    \"@react-email/render\": \"2.0.1\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"react-dom\": \"19.2.3\",\n    \"resend\": \"6.6.0\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/email/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, mock, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery } from \"./index\";\n\nconst sendMonitorAlertMock = mock(async (_callArgs) => {});\n\nmock.module(\"@openstatus/emails/src/client\", () => ({\n  EmailClient: mock((_args: { apiKey: string }) => {\n    return {\n      sendMonitorAlert: sendMonitorAlertMock,\n    };\n  }),\n}));\n\ndescribe(\"Email Notifications\", () => {\n  beforeEach(() => {\n    sendMonitorAlertMock.mockClear();\n  });\n\n  afterEach(() => {\n    sendMonitorAlertMock.mockClear();\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"iad\",\n  });\n\n  const createMockNotification = () => ({\n    id: 1,\n    name: \"Email Notification\",\n    provider: \"email\",\n    workspaceId: 1,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    data: '{\"email\":\"ping@openstatus.dev\"}',\n  });\n\n  test(\"Send Alert\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      latency: 1500,\n      region: \"iad\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(sendMonitorAlertMock).toHaveBeenCalledTimes(1);\n    const callArgs = sendMonitorAlertMock.mock.calls[0][0];\n    expect(callArgs.name).toBe(\"API Health Check\");\n    expect(callArgs.type).toBe(\"alert\");\n    expect(callArgs.to).toBe(\"ping@openstatus.dev\");\n    expect(callArgs.url).toBe(\"https://api.example.com/health\");\n    expect(callArgs.status).toBe(\"500\");\n    expect(callArgs.latency).toBe(\"1500ms\");\n    expect(callArgs.message).toBe(\"Something went wrong\");\n    expect(callArgs.timestamp).toBeDefined();\n  });\n\n  test(\"Send Alert without optional fields\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      cronTimestamp: Date.now(),\n    });\n\n    expect(sendMonitorAlertMock).toHaveBeenCalledTimes(1);\n    const callArgs = sendMonitorAlertMock.mock.calls[0][0];\n    expect(callArgs.status).toBeUndefined();\n    expect(callArgs.latency).toBe(\"N/A\");\n    expect(callArgs.region).toBe(\"N/A\");\n  });\n\n  test(\"Send Recovery\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 200,\n      latency: 100,\n      region: \"ams\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(sendMonitorAlertMock).toHaveBeenCalledTimes(1);\n    const callArgs = sendMonitorAlertMock.mock.calls[0][0];\n    expect(callArgs.type).toBe(\"recovery\");\n    expect(callArgs.name).toBe(\"API Health Check\");\n    expect(callArgs.to).toBe(\"ping@openstatus.dev\");\n    expect(callArgs.status).toBe(\"200\");\n    expect(callArgs.latency).toBe(\"100ms\");\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      latency: 2000,\n      region: \"lax\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(sendMonitorAlertMock).toHaveBeenCalledTimes(1);\n    const callArgs = sendMonitorAlertMock.mock.calls[0][0];\n    expect(callArgs.type).toBe(\"degraded\");\n    expect(callArgs.name).toBe(\"API Health Check\");\n    expect(callArgs.status).toBe(\"503\");\n    expect(callArgs.latency).toBe(\"2000ms\");\n  });\n\n  test(\"Handles invalid notification data gracefully\", async () => {\n    const monitor = createMockMonitor();\n    const invalidNotification = selectNotificationSchema.parse({\n      id: 1,\n      name: \"Email Notification\",\n      provider: \"email\",\n      workspaceId: 1,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      data: '{\"invalid\":\"data\"}',\n    });\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification: invalidNotification,\n      cronTimestamp: Date.now(),\n    });\n\n    // Should not call sendMonitorAlert when data is invalid\n    expect(sendMonitorAlertMock).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/notifications/email/src/index.ts",
    "content": "import { emailDataSchema } from \"@openstatus/db/src/schema\";\nimport type { Region } from \"@openstatus/db/src/schema/constants\";\nimport { EmailClient } from \"@openstatus/emails/src/client\";\nimport type { NotificationContext } from \"@openstatus/notification-base\";\nimport { regionDict } from \"@openstatus/regions\";\nimport { env } from \"../env\";\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n  cronTimestamp,\n  latency,\n  regions,\n}: NotificationContext) => {\n  // Convert regions array to single region for backwards compatibility\n  const region = regions?.[0] as Region | undefined;\n  const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY });\n\n  const config = emailDataSchema.safeParse(JSON.parse(notification.data));\n\n  if (!config.success) return;\n\n  await emailClient.sendMonitorAlert({\n    name: monitor.name,\n    type: \"alert\",\n    to: config.data.email,\n    url: monitor.url,\n    status: statusCode?.toString(),\n    latency: latency ? `${latency}ms` : \"N/A\",\n    region: region ? regionDict[region].location : \"N/A\",\n    timestamp: new Date(cronTimestamp).toISOString(),\n    message,\n  });\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n  statusCode,\n  cronTimestamp,\n  regions,\n  latency,\n}: NotificationContext) => {\n  // Convert regions array to single region for backwards compatibility\n  const region = regions?.[0] as Region | undefined;\n  const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY });\n\n  const config = emailDataSchema.safeParse(JSON.parse(notification.data));\n\n  if (!config.success) return;\n\n  await emailClient.sendMonitorAlert({\n    name: monitor.name,\n    type: \"recovery\",\n    to: config.data.email,\n    url: monitor.url,\n    status: statusCode?.toString(),\n    latency: latency ? `${latency}ms` : \"N/A\",\n    region: region ?? \"N/A\",\n    timestamp: new Date(cronTimestamp).toISOString(),\n  });\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n  statusCode,\n  cronTimestamp,\n  regions,\n  latency,\n}: NotificationContext) => {\n  // Convert regions array to single region for backwards compatibility\n  const region = regions?.[0] as Region | undefined;\n  const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY });\n\n  const config = emailDataSchema.safeParse(JSON.parse(notification.data));\n\n  if (!config.success) return;\n\n  await emailClient.sendMonitorAlert({\n    name: monitor.name,\n    type: \"degraded\",\n    to: config.data.email,\n    url: monitor.url,\n    status: statusCode?.toString(),\n    latency: latency ? `${latency}ms` : \"N/A\",\n    region: region ?? \"N/A\",\n    timestamp: new Date(cronTimestamp).toISOString(),\n  });\n};\n"
  },
  {
    "path": "packages/notifications/email/src/mock.ts",
    "content": "import type { Monitor, Notification } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery } from \"./index\";\n\nconst monitor: Monitor = {\n  id: 1,\n  name: \"OpenStatus Docs\",\n  url: \"https://docs.openstatus.dev\",\n  periodicity: \"10m\",\n  jobType: \"http\",\n  active: true,\n  public: true,\n  createdAt: null,\n  updatedAt: null,\n  regions: [\"ams\", \"fra\"],\n  description: \"Monitor Description\",\n  headers: [],\n  body: \"\",\n  workspaceId: 1,\n  timeout: 45000,\n  degradedAfter: null,\n  assertions: null,\n  status: \"active\",\n  method: \"GET\",\n  deletedAt: null,\n  otelEndpoint: null,\n  otelHeaders: [],\n  followRedirects: false,\n  retry: 3,\n  externalName: null,\n};\n\nconst notification: Notification = {\n  id: 1,\n  name: \"Email\",\n  data: '{ \"email\": \"max@openstatus.dev\" }',\n  createdAt: null,\n  updatedAt: null,\n  workspaceId: 1,\n  provider: \"email\",\n};\n\nconst cronTimestamp = Date.now();\n\nif (process.env.NODE_ENV === \"development\") {\n  await sendDegraded({\n    monitor,\n    notification,\n    cronTimestamp,\n  });\n\n  await sendAlert({\n    monitor,\n    notification,\n    statusCode: 500,\n    cronTimestamp,\n  });\n\n  await sendRecovery({\n    monitor,\n    notification,\n    cronTimestamp,\n  });\n}\n"
  },
  {
    "path": "packages/notifications/email/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/google-chat/.gitignore",
    "content": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\n\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# Runtime data\n\npids\n_.pid\n_.seed\n\\*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\n\nlib-cov\n\n# Coverage directory used by tools like istanbul\n\ncoverage\n\\*.lcov\n\n# nyc test coverage\n\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n\n.grunt\n\n# Bower dependency directory (https://bower.io/)\n\nbower_components\n\n# node-waf configuration\n\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\n\nbuild/Release\n\n# Dependency directories\n\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\n\nweb_modules/\n\n# TypeScript cache\n\n\\*.tsbuildinfo\n\n# Optional npm cache directory\n\n.npm\n\n# Optional eslint cache\n\n.eslintcache\n\n# Optional stylelint cache\n\n.stylelintcache\n\n# Microbundle cache\n\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n\n.node_repl_history\n\n# Output of 'npm pack'\n\n\\*.tgz\n\n# Yarn Integrity file\n\n.yarn-integrity\n\n# dotenv environment variable files\n\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n\n.cache\n.parcel-cache\n\n# Next.js build output\n\n.next\nout\n\n# Nuxt.js build / generate output\n\n.nuxt\ndist\n\n# Gatsby files\n\n.cache/\n\n# Comment in the public line in if your project uses Gatsby and not Next.js\n\n# https://nextjs.org/blog/next-9-1#public-directory-support\n\n# public\n\n# vuepress build output\n\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n\n.temp\n.cache\n\n# Docusaurus cache and generated files\n\n.docusaurus\n\n# Serverless directories\n\n.serverless/\n\n# FuseBox cache\n\n.fusebox/\n\n# DynamoDB Local files\n\n.dynamodb/\n\n# TernJS port file\n\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.\\*\n"
  },
  {
    "path": "packages/notifications/google-chat/README.md",
    "content": "# @openstatus/notifications-discord\n\nTo install dependencies:\n\n```bash\nbun install\n```\n\nTo run:\n\n```bash\nbun run src/index.ts\n```\n\nThis project was created using `bun init` in bun v1.0.0. [Bun](https://bun.sh)\nis a fast all-in-one JavaScript runtime.\n"
  },
  {
    "path": "packages/notifications/google-chat/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-google-chat\",\n  \"version\": \"1.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/google-chat/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery, sendTest } from \"./index\";\n\ndescribe(\"Google Chat Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n\n  beforeEach(() => {\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockRestore();\n    }\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"iad\",\n  });\n\n  const createMockNotification = () => ({\n    id: 1,\n    name: \"Google Chat Notification\",\n    provider: \"google-chat\",\n    workspaceId: 1,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    data: '{\"google-chat\":\"https://google.com/api/webhooks/123456789/abcdefgh\"}',\n  });\n\n  test(\"Send Alert\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\n      \"https://google.com/api/webhooks/123456789/abcdefgh\",\n    );\n    expect(callArgs[1].method).toBe(\"POST\");\n    expect(callArgs[1].headers[\"Content-Type\"]).toBe(\"application/json\");\n\n    const body = JSON.parse(callArgs[1].body);\n    console.log(body);\n    expect(body.text).toContain(\"🚨 Alert\");\n    expect(body.text).toContain(\"API Health Check\");\n    expect(body.text).toContain(\"Something went wrong\");\n  });\n\n  test(\"Send Recovery\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 200,\n      message: \"Service recovered\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.text).toContain(\"✅ Recovered\");\n    expect(body.text).toContain(\"API Health Check\");\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      message: \"Service degraded\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.text).toContain(\"⚠️ Degraded\");\n    expect(body.text).toContain(\"API Health Check\");\n  });\n\n  test(\"Send Test Google Chat Message\", async () => {\n    const webhookUrl = \"https://google.com/api/webhooks/123456789/abcdefgh\";\n\n    const result = await sendTest(webhookUrl);\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(webhookUrl);\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.text).toContain(\"🧪 Test\");\n    expect(body.text).toContain(\"OpenStatus\");\n  });\n\n  test(\"Send Test Google Chat Message with empty webhookUrl\", async () => {\n    const result = await sendTest(\"\");\n\n    expect(result).toBe(false);\n    expect(fetchMock).not.toHaveBeenCalled();\n  });\n\n  test(\"Handle fetch error gracefully\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/notifications/google-chat/src/index.ts",
    "content": "import { googleChatDataSchema } from \"@openstatus/db/src/schema\";\nimport type { NotificationContext } from \"@openstatus/notification-base\";\n\nconst postToWebhook = async (content: string, webhookUrl: string) => {\n  const res = await fetch(webhookUrl, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      text: content,\n    }),\n  });\n  if (!res.ok) {\n    throw new Error(\n      `Failed to send Google Chat webhook: ${res.status} ${res.statusText}`,\n    );\n  }\n};\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n  cronTimestamp,\n}: NotificationContext) => {\n  const notificationData = googleChatDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { \"google-chat\": webhookUrl } = notificationData; // webhook url\n  const { name } = monitor;\n\n  try {\n    await postToWebhook(\n      `*🚨 Alert <${monitor.url}|${name}>*\\nStatus Code: ${\n        statusCode || \"_empty_\"\n      }\\nMessage: ${\n        message || \"_empty_\"\n      }\\nCron Timestamp: ${cronTimestamp} (${new Date(\n        cronTimestamp,\n      ).toISOString()})\\n> Check your <https://www.openstatus.dev/app/|Dashboard>.\\n`,\n      webhookUrl,\n    );\n  } catch (err) {\n    console.error(err);\n    throw err;\n  }\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = googleChatDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { \"google-chat\": webhookUrl } = notificationData; // webhook url\n  const { name } = monitor;\n\n  try {\n    await postToWebhook(\n      `*✅ Recovered <${monitor.url}|${name}>*\\n> Check your <https://www.openstatus.dev/app/|Dashboard>.\\n`,\n      webhookUrl,\n    );\n  } catch (err) {\n    console.error(err);\n    throw err;\n  }\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = googleChatDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { \"google-chat\": webhookUrl } = notificationData; // webhook url\n  const { name } = monitor;\n\n  try {\n    await postToWebhook(\n      `*⚠️ Degraded <${monitor.url}|${name}>*\\n> Check your <https://www.openstatus.dev/app/|Dashboard>.\\n`,\n      webhookUrl,\n    );\n  } catch (err) {\n    console.error(err);\n    throw err;\n  }\n};\n\nexport const sendTest = async (webhookUrl: string) => {\n  if (!webhookUrl) {\n    return false;\n  }\n  console.log(webhookUrl);\n  try {\n    await postToWebhook(\n      \"*🧪 Test <https://www.openstatus.dev/|OpenStatus>*\\nIf you can read this, your Google Chat webhook is functioning correctly!\\n> Check your <https://www.openstatus.dev/app/|Dashboard>.\\n\",\n      webhookUrl,\n    );\n    return true;\n  } catch (_err) {\n    console.log(_err);\n    return false;\n  }\n};\n"
  },
  {
    "path": "packages/notifications/google-chat/src/mock.ts",
    "content": "import type { Monitor, Notification } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery, sendTest } from \"./index\";\n\nconst monitor: Monitor = {\n  id: 1,\n  name: \"OpenStatus Docs\",\n  url: \"https://docs.openstatus.dev\",\n  periodicity: \"10m\",\n  jobType: \"http\",\n  active: true,\n  public: true,\n  createdAt: null,\n  updatedAt: null,\n  regions: [\"ams\", \"fra\"],\n  description: \"Monitor Description\",\n  headers: [],\n  body: \"\",\n  workspaceId: 1,\n  timeout: 45000,\n  degradedAfter: null,\n  assertions: null,\n  status: \"active\",\n  method: \"GET\",\n  deletedAt: null,\n  otelEndpoint: null,\n  otelHeaders: [],\n  followRedirects: true,\n  retry: 3,\n  externalName: null,\n};\n\nconst notification: Notification = {\n  id: 1,\n  name: \"Discord\",\n  data: `{ \"discord\": \"${process.env.DISCORD_WEBHOOK}\" }`,\n  createdAt: null,\n  updatedAt: null,\n  workspaceId: 1,\n  provider: \"discord\",\n};\n\nconst cronTimestamp = Date.now();\n\nif (process.env.NODE_ENV === \"development\") {\n  await sendDegraded({\n    monitor,\n    notification,\n    cronTimestamp,\n  });\n\n  await sendAlert({\n    monitor,\n    notification,\n    statusCode: 500,\n    cronTimestamp,\n  });\n\n  await sendRecovery({\n    monitor,\n    notification,\n    cronTimestamp,\n  });\n\n  await sendTest(\"\");\n}\n"
  },
  {
    "path": "packages/notifications/google-chat/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/grafana-oncall/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-grafana-oncall\",\n  \"version\": \"0.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/grafana-oncall/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery, sendTest } from \"./index\";\n\ndescribe(\"Grafana OnCall Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n\n  beforeEach(() => {\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockRestore();\n    }\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"iad\",\n  });\n\n  const createMockNotification = () => ({\n    id: 1,\n    name: \"Grafana OnCall Notification\",\n    provider: \"grafana-oncall\",\n    workspaceId: 1,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    data: '{\"grafana-oncall\":{\"webhookUrl\":\"https://oncall.example.com/integrations/v1/webhook/abc123\"}}',\n  });\n\n  test(\"Send Alert\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\n      \"https://oncall.example.com/integrations/v1/webhook/abc123\",\n    );\n    expect(callArgs[1].method).toBe(\"POST\");\n    expect(callArgs[1].headers[\"Content-Type\"]).toBe(\"application/json\");\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.alert_uid).toBe(\"openstatus-monitor-monitor-1\");\n    expect(body.title).toBe(\"API Health Check is down\");\n    expect(body.message).toBe(\"Something went wrong\");\n    expect(body.state).toBe(\"alerting\");\n    expect(body.link_to_upstream_details).toBe(\n      \"https://www.openstatus.dev/app/monitor-1/overview\",\n    );\n  });\n\n  test(\"Send Alert uses status code as message when no message provided\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.message).toBe(\"Status code: 503\");\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      message: \"Service degraded\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\n      \"https://oncall.example.com/integrations/v1/webhook/abc123\",\n    );\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.alert_uid).toBe(\"openstatus-monitor-monitor-1\");\n    expect(body.title).toBe(\"API Health Check is degraded\");\n    expect(body.message).toBe(\"Service degraded\");\n    expect(body.state).toBe(\"alerting\");\n  });\n\n  test(\"Send Recovery\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 200,\n      message: \"Service recovered\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\n      \"https://oncall.example.com/integrations/v1/webhook/abc123\",\n    );\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.alert_uid).toBe(\"openstatus-monitor-monitor-1\");\n    expect(body.title).toBe(\"API Health Check has recovered\");\n    expect(body.message).toBe(\"Service recovered\");\n    expect(body.state).toBe(\"ok\");\n  });\n\n  test(\"Send Test\", async () => {\n    const webhookUrl =\n      \"https://oncall.example.com/integrations/v1/webhook/test123\";\n\n    const result = await sendTest({ webhookUrl });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(webhookUrl);\n    expect(callArgs[1].method).toBe(\"POST\");\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.alert_uid).toBe(\"openstatus-test\");\n    expect(body.title).toBe(\"Test Alert <OpenStatus>\");\n    expect(body.message).toContain(\"Grafana OnCall integration is functioning\");\n    expect(body.state).toBe(\"alerting\");\n    expect(body.link_to_upstream_details).toBe(\"https://www.openstatus.dev\");\n  });\n\n  test(\"Send Test returns false on network error\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const result = await sendTest({\n      webhookUrl: \"https://oncall.example.com/integrations/v1/webhook/test123\",\n    });\n\n    expect(result).toBe(false);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"Send Test returns false on non-ok response\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 401 })),\n    );\n\n    const result = await sendTest({\n      webhookUrl: \"https://oncall.example.com/integrations/v1/webhook/test123\",\n    });\n\n    expect(result).toBe(false);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"Send Alert throws on non-ok response\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.resolve(\n        new Response(null, {\n          status: 500,\n          statusText: \"Internal Server Error\",\n        }),\n      ),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow(\"Failed to send Grafana OnCall alert\");\n  });\n\n  test(\"Handle fetch error gracefully\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/notifications/grafana-oncall/src/index.ts",
    "content": "import type { NotificationContext } from \"@openstatus/notification-base\";\nimport { GrafanaOncallPayload, GrafanaOncallSchema } from \"./schema\";\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const { \"grafana-oncall\": config } = GrafanaOncallSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { name } = monitor;\n\n  const event = GrafanaOncallPayload.parse({\n    alert_uid: `openstatus-monitor-${monitor.id}`,\n    title: `${name} is down`,\n    message: message || `Status code: ${statusCode}`,\n    state: \"alerting\",\n    link_to_upstream_details: `https://www.openstatus.dev/app/${monitor.id}/overview`,\n  });\n\n  const res = await fetch(config.webhookUrl, {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n  });\n\n  if (!res.ok) {\n    throw new Error(\n      `Failed to send Grafana OnCall alert: ${res.status} ${res.statusText}`,\n    );\n  }\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const { \"grafana-oncall\": config } = GrafanaOncallSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { name } = monitor;\n\n  const event = GrafanaOncallPayload.parse({\n    alert_uid: `openstatus-monitor-${monitor.id}`,\n    title: `${name} is degraded`,\n    message: message || `Status code: ${statusCode}`,\n    state: \"alerting\",\n    link_to_upstream_details: `https://www.openstatus.dev/app/${monitor.id}/overview`,\n  });\n\n  const res = await fetch(config.webhookUrl, {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n  });\n\n  if (!res.ok) {\n    throw new Error(\n      `Failed to send Grafana OnCall degraded alert: ${res.status} ${res.statusText}`,\n    );\n  }\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const { \"grafana-oncall\": config } = GrafanaOncallSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { name } = monitor;\n\n  const event = GrafanaOncallPayload.parse({\n    alert_uid: `openstatus-monitor-${monitor.id}`,\n    title: `${name} has recovered`,\n    message: message || `Status code: ${statusCode}`,\n    state: \"ok\",\n    link_to_upstream_details: `https://www.openstatus.dev/app/${monitor.id}/overview`,\n  });\n\n  const res = await fetch(config.webhookUrl, {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n  });\n\n  if (!res.ok) {\n    throw new Error(\n      `Failed to send Grafana OnCall recovery: ${res.status} ${res.statusText}`,\n    );\n  }\n};\n\nexport const sendTest = async (props: { webhookUrl: string }) => {\n  const { webhookUrl } = props;\n\n  const event = GrafanaOncallPayload.parse({\n    alert_uid: \"openstatus-test\",\n    title: \"Test Alert <OpenStatus>\",\n    message:\n      \"If you can read this, your Grafana OnCall integration is functioning correctly! Please ignore this alert.\",\n    state: \"alerting\",\n    link_to_upstream_details: \"https://www.openstatus.dev\",\n  });\n\n  try {\n    const res = await fetch(webhookUrl, {\n      method: \"POST\",\n      body: JSON.stringify(event),\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n    });\n\n    if (!res.ok) {\n      throw new Error(`Failed to send test: ${res.status} ${res.statusText}`);\n    }\n\n    return true;\n  } catch (err) {\n    console.log(err);\n    return false;\n  }\n};\n\nexport { GrafanaOncallSchema };\n"
  },
  {
    "path": "packages/notifications/grafana-oncall/src/schema.ts",
    "content": "import { grafanaOncallDataSchema } from \"@openstatus/db/src/schema\";\nimport { z } from \"zod\";\n\nexport const GrafanaOncallSchema = grafanaOncallDataSchema;\n\nexport const GrafanaOncallPayload = z.object({\n  alert_uid: z.string(),\n  title: z.string(),\n  message: z.string().optional(),\n  state: z.enum([\"alerting\", \"ok\"]),\n  link_to_upstream_details: z.string().optional(),\n});\n"
  },
  {
    "path": "packages/notifications/grafana-oncall/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/ntfy/.gitignore",
    "content": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\n\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# Runtime data\n\npids\n_.pid\n_.seed\n\\*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\n\nlib-cov\n\n# Coverage directory used by tools like istanbul\n\ncoverage\n\\*.lcov\n\n# nyc test coverage\n\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n\n.grunt\n\n# Bower dependency directory (https://bower.io/)\n\nbower_components\n\n# node-waf configuration\n\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\n\nbuild/Release\n\n# Dependency directories\n\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\n\nweb_modules/\n\n# TypeScript cache\n\n\\*.tsbuildinfo\n\n# Optional npm cache directory\n\n.npm\n\n# Optional eslint cache\n\n.eslintcache\n\n# Optional stylelint cache\n\n.stylelintcache\n\n# Microbundle cache\n\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n\n.node_repl_history\n\n# Output of 'npm pack'\n\n\\*.tgz\n\n# Yarn Integrity file\n\n.yarn-integrity\n\n# dotenv environment variable files\n\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n\n.cache\n.parcel-cache\n\n# Next.js build output\n\n.next\nout\n\n# Nuxt.js build / generate output\n\n.nuxt\ndist\n\n# Gatsby files\n\n.cache/\n\n# Comment in the public line in if your project uses Gatsby and not Next.js\n\n# https://nextjs.org/blog/next-9-1#public-directory-support\n\n# public\n\n# vuepress build output\n\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n\n.temp\n.cache\n\n# Docusaurus cache and generated files\n\n.docusaurus\n\n# Serverless directories\n\n.serverless/\n\n# FuseBox cache\n\n.fusebox/\n\n# DynamoDB Local files\n\n.dynamodb/\n\n# TernJS port file\n\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.\\*\n"
  },
  {
    "path": "packages/notifications/ntfy/README.md",
    "content": "# @openstatus/notification-ntfy\nTo install dependencies:\n\n```bash\nbun install\n```\n\nTo run:\n\n```bash\nbun run src/index.ts\n```\n\nThis project was created using `bun init` in bun v1.0.0. [Bun](https://bun.sh)\nis a fast all-in-one JavaScript runtime.\n"
  },
  {
    "path": "packages/notifications/ntfy/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-ntfy\",\n  \"version\": \"1.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/ntfy/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery, sendTest } from \"./index\";\n\ndescribe(\"Ntfy Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n\n  beforeEach(() => {\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockRestore();\n    }\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"us-east-1\",\n  });\n\n  const createMockNotification = (withToken = false, withServerUrl = false) => {\n    const data: {\n      ntfy: { topic: string; token?: string; serverUrl?: string };\n    } = {\n      ntfy: {\n        topic: \"my-topic\",\n      },\n    };\n    if (withToken) {\n      data.ntfy.token = \"test-token-123\";\n    }\n    if (withServerUrl) {\n      data.ntfy.serverUrl = \"https://ntfy.example.com\";\n    }\n    return {\n      id: 1,\n      name: \"Ntfy Notification\",\n      provider: \"ntfy\",\n      workspaceId: 1,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      data: JSON.stringify(data),\n    };\n  };\n\n  test(\"Send Alert with default server\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://ntfy.sh/my-topic\");\n    expect(callArgs[1].method).toBe(\"post\");\n    expect(callArgs[1].body).toContain(\"API Health Check\");\n    expect(callArgs[1].body).toContain(\"status code 500\");\n    expect(callArgs[1].headers).not.toHaveProperty(\"Authorization\");\n  });\n\n  test(\"Send Alert with custom server\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(false, true),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Error\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://ntfy.example.com/my-topic\");\n  });\n\n  test(\"Send Alert with token\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(true),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Error\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[1].headers.Authorization).toBe(\"Bearer test-token-123\");\n  });\n\n  test(\"Send Alert without statusCode\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      message: \"Connection timeout\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[1].body).toContain(\"error: Connection timeout\");\n  });\n\n  test(\"Send Recovery\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 200,\n      message: \"Service recovered\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[1].body).toContain(\"is up again\");\n  });\n\n  test(\"Send Recovery with token\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(true),\n    );\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[1].headers.Authorization).toBe(\"Bearer test-token-123\");\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      message: \"Service degraded\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[1].body).toContain(\"is degraded\");\n  });\n\n  test(\"Send Test with default server\", async () => {\n    const result = await sendTest({\n      topic: \"test-topic\",\n    });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://ntfy.sh/test-topic\");\n    expect(callArgs[1].body).toBe(\"This is a test message from OpenStatus\");\n    expect(callArgs[1].headers).not.toHaveProperty(\"Authorization\");\n  });\n\n  test(\"Send Test with custom server\", async () => {\n    const result = await sendTest({\n      topic: \"test-topic\",\n      serverUrl: \"https://ntfy.example.com\",\n    });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://ntfy.example.com/test-topic\");\n  });\n\n  test(\"Send Test with token\", async () => {\n    const result = await sendTest({\n      topic: \"test-topic\",\n      token: \"test-token\",\n    });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[1].headers.Authorization).toBe(\"Bearer test-token\");\n  });\n\n  test(\"Handle fetch error gracefully\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    // Should not throw - function catches errors internally\n    await expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"Send Test returns false on error\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const result = await sendTest({\n      topic: \"test-topic\",\n    });\n\n    expect(result).toBe(false);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/notifications/ntfy/src/index.ts",
    "content": "import { ntfyDataSchema } from \"@openstatus/db/src/schema\";\nimport type { NotificationContext } from \"@openstatus/notification-base\";\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const notificationData = ntfyDataSchema.parse(JSON.parse(notification.data));\n  const { name } = monitor;\n\n  const body = `Your monitor ${name} / ${monitor.url} is down with ${\n    statusCode ? `status code ${statusCode}` : `error: ${message}`\n  }`;\n\n  const authorization = notificationData.ntfy.token\n    ? { Authorization: `Bearer ${notificationData.ntfy.token}` }\n    : undefined;\n\n  const url = notificationData.ntfy.serverUrl\n    ? `${notificationData.ntfy.serverUrl}/${notificationData.ntfy.topic}`\n    : `https://ntfy.sh/${notificationData.ntfy.topic}`;\n\n  const res = await fetch(url, {\n    method: \"post\",\n    body,\n    headers: {\n      ...authorization,\n    },\n  });\n  if (!res.ok) {\n    throw new Error(`Failed to send alert notification: ${res.statusText}`);\n  }\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = ntfyDataSchema.parse(JSON.parse(notification.data));\n  const { name } = monitor;\n\n  const body = `Your monitor ${name} / ${monitor.url} is up again`;\n  const authorization = notificationData.ntfy.token\n    ? { Authorization: `Bearer ${notificationData.ntfy.token}` }\n    : undefined;\n\n  const url = notificationData.ntfy.serverUrl\n    ? `${notificationData.ntfy.serverUrl}/${notificationData.ntfy.topic}`\n    : `https://ntfy.sh/${notificationData.ntfy.topic}`;\n\n  const res = await fetch(url, {\n    method: \"post\",\n    body,\n    headers: {\n      ...authorization,\n    },\n  });\n  if (!res.ok) {\n    throw new Error(`Failed to send recovery notification: ${res.statusText}`);\n  }\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = ntfyDataSchema.parse(JSON.parse(notification.data));\n  const { name } = monitor;\n\n  const body = `Your monitor ${name} / ${monitor.url} is degraded `;\n\n  const authorization = notificationData.ntfy\n    ? { Authorization: `Bearer ${notificationData.ntfy.token}` }\n    : undefined;\n\n  const url = notificationData.ntfy.serverUrl\n    ? `${notificationData.ntfy.serverUrl}/${notificationData.ntfy.topic}`\n    : `https://ntfy.sh/${notificationData.ntfy.topic}`;\n\n  const res = await fetch(url, {\n    method: \"post\",\n    body,\n    headers: {\n      ...authorization,\n    },\n  });\n  if (!res.ok) {\n    throw new Error(`Failed to send degraded notification: ${res.statusText}`);\n  }\n};\n\nexport const sendTest = async ({\n  serverUrl,\n  topic,\n  token,\n}: {\n  topic: string;\n  serverUrl?: string;\n  token?: string;\n}) => {\n  const authorization = token\n    ? { Authorization: `Bearer ${token}` }\n    : undefined;\n  const url = serverUrl ? `${serverUrl}/${topic}` : `https://ntfy.sh/${topic}`;\n  try {\n    await fetch(url, {\n      method: \"post\",\n      body: \"This is a test message from OpenStatus\",\n      headers: {\n        ...authorization,\n      },\n    });\n  } catch (err) {\n    console.log(err);\n    return false;\n  }\n  return true;\n};\n"
  },
  {
    "path": "packages/notifications/ntfy/src/schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const NtfySchema = z.object({\n  ntfy: z.object({\n    topic: z.string(),\n    serverUrl: z.string().prefault(\"https://ntfy.sh\"),\n    token: z.string().optional(),\n  }),\n});\n"
  },
  {
    "path": "packages/notifications/ntfy/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/opsgenie/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-opsgenie\",\n  \"version\": \"0.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"@types/validator\": \"13.12.0\",\n    \"validator\": \"13.12.0\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/opsgenie/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendTest } from \"./index\";\n\ndescribe(\"OpsGenie Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n\n  beforeEach(() => {\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockRestore();\n    }\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"us-east-1\",\n  });\n\n  const createMockNotification = (region: \"eu\" | \"us\" = \"us\") => ({\n    id: 1,\n    name: \"OpsGenie Notification\",\n    provider: \"opsgenie\",\n    workspaceId: 1,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    data: JSON.stringify({\n      opsgenie: {\n        apiKey: \"test-api-key-123\",\n        region,\n      },\n    }),\n  });\n\n  const createMockIncident = () => ({\n    id: 1,\n    title: \"API Health Check is down\",\n    summary: \"API Health Check is down\",\n    status: \"triage\" as const,\n    monitorId: \"monitor-1\",\n    workspaceId: 1,\n    startedAt: Date.now(),\n  });\n\n  test(\"Send Alert with US region\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(\"us\"),\n    );\n    const incident = createMockIncident();\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      // @ts-expect-error\n      incident,\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://api.opsgenie.com/v2/alerts\");\n    expect(callArgs[1].method).toBe(\"POST\");\n    expect(callArgs[1].headers[\"Content-Type\"]).toBe(\"application/json\");\n    expect(callArgs[1].headers.Authorization).toBe(\"GenieKey test-api-key-123\");\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.message).toBe(\"API Health Check is down\");\n    expect(body.alias).toBe(\"monitor-1\");\n    expect(body.details.severity).toBe(\"down\");\n    expect(body.details.status).toBe(500);\n    expect(body.details.message).toBe(\"Something went wrong\");\n  });\n\n  test(\"Send Alert with EU region\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(\"eu\"),\n    );\n    const incident = createMockIncident();\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Error\",\n      // @ts-expect-error\n      incident,\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://api.eu.opsgenie.com/v2/alerts\");\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n    const incident = createMockIncident();\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      message: \"Service degraded\",\n      // @ts-expect-error\n      incident,\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.details.severity).toBe(\"degraded\");\n    expect(body.message).toBe(\"API Health Check is degraded\");\n  });\n\n  test(\"Handle fetch error gracefully\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n    const incident = createMockIncident();\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        // @ts-expect-error\n        incident,\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"Send Test returns false on error\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const result = await sendTest({\n      apiKey: \"test-api-key\",\n      region: \"us\",\n    });\n\n    expect(result).toBe(false);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/notifications/opsgenie/src/index.ts",
    "content": "import type { NotificationContext } from \"@openstatus/notification-base\";\nimport { OpsGeniePayloadAlert, OpsGenieSchema } from \"./schema\";\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const { opsgenie } = OpsGenieSchema.parse(JSON.parse(notification.data));\n  const { name } = monitor;\n\n  const event = OpsGeniePayloadAlert.parse({\n    alias: `${monitor.id}`,\n    message: `${name} is down`,\n    description: message,\n    details: {\n      message,\n      status: statusCode,\n      severity: \"down\",\n    },\n  });\n\n  const url =\n    opsgenie.region === \"eu\"\n      ? \"https://api.eu.opsgenie.com/v2/alerts\"\n      : \"https://api.opsgenie.com/v2/alerts\";\n\n  const res = await fetch(url, {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `GenieKey ${opsgenie.apiKey}`,\n    },\n  });\n  if (!res.ok) {\n    throw new Error(\n      `Failed to send OpsGenie alert: ${res.status} ${res.statusText}`,\n    );\n  }\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const { opsgenie } = OpsGenieSchema.parse(JSON.parse(notification.data));\n  const { name } = monitor;\n\n  const event = OpsGeniePayloadAlert.parse({\n    alias: `${monitor.id}`,\n    message: `${name} is degraded`,\n    description: message,\n    details: {\n      message,\n      status: statusCode,\n      severity: \"degraded\",\n    },\n  });\n\n  const url =\n    opsgenie.region === \"eu\"\n      ? \"https://api.eu.opsgenie.com/v2/alerts\"\n      : \"https://api.opsgenie.com/v2/alerts\";\n  const res = await fetch(url, {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `GenieKey ${opsgenie.apiKey}`,\n    },\n  });\n  if (!res.ok) {\n    throw new Error(\n      `Failed to send OpsGenie degraded alert: ${res.status} ${res.statusText}`,\n    );\n  }\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const { opsgenie } = OpsGenieSchema.parse(JSON.parse(notification.data));\n\n  const url =\n    opsgenie.region === \"eu\"\n      ? `https://api.eu.opsgenie.com/v2/alerts/${monitor.id}/close`\n      : `https://api.opsgenie.com/v2/alerts/${monitor.id}/close`;\n\n  const event = OpsGeniePayloadAlert.parse({\n    alias: `${monitor.id}`,\n    message: `${monitor.name} has recovered`,\n    description: message,\n    details: {\n      message,\n      status: statusCode,\n    },\n  });\n  try {\n    await fetch(url, {\n      method: \"POST\",\n      body: JSON.stringify(event),\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `GenieKey ${opsgenie.apiKey}`,\n      },\n    });\n  } catch (err) {\n    console.log(err);\n    // Do something\n  }\n};\nexport const sendTest = async (props: {\n  apiKey: string;\n  region: \"eu\" | \"us\";\n}) => {\n  const { apiKey, region } = props;\n\n  const url =\n    region === \"eu\"\n      ? \"https://api.eu.opsgenie.com/v2/alerts\"\n      : \"https://api.opsgenie.com/v2/alerts\";\n\n  const alert = OpsGeniePayloadAlert.parse({\n    alias: \"test-openstatus\",\n    message: \"Test Alert <OpenStatus>\",\n    description:\n      \"If you can read this, your OpsGenie integration is functioning correctly! Please ignore this alert and delete it.\",\n  });\n\n  try {\n    const res = await fetch(url, {\n      method: \"POST\",\n      body: JSON.stringify(alert),\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `GenieKey ${apiKey}`,\n        \"Access-Control-Allow-Origin\": \"*\",\n      },\n    });\n    console.log(await res.json());\n    return true;\n  } catch (err) {\n    console.log(err);\n    return false;\n  }\n};\n\nexport { OpsGenieSchema };\n"
  },
  {
    "path": "packages/notifications/opsgenie/src/schema.ts",
    "content": "import { opsgenieDataSchema } from \"@openstatus/db/src/schema\";\nimport { z } from \"zod\";\n\nexport const OpsGenieSchema = opsgenieDataSchema;\n\nexport const OpsGeniePayloadAlert = z.object({\n  message: z.string(),\n  alias: z.string(),\n  description: z.string(),\n  source: z.string().prefault(\"OpenStatus\"),\n  details: z\n    .object({\n      message: z.string(),\n      status: z.number().optional(),\n      severity: z.enum([\"degraded\", \"down\"]),\n    })\n    .optional(),\n});\n\nexport const OpsGenieCloseAlert = z.object({\n  source: z.string().prefault(\"OpenStatus\"),\n});\n"
  },
  {
    "path": "packages/notifications/opsgenie/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/pagerduty/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-pagerduty\",\n  \"version\": \"0.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"@types/validator\": \"13.12.0\",\n    \"validator\": \"13.12.0\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/pagerduty/src/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    PAGERDUTY_APP_ID: z.string(),\n  },\n\n  /**\n   * What object holds the environment variables at runtime. This is usually\n   * `process.env` or `import.meta.env`.\n   */\n  runtimeEnv: process.env,\n\n  /**\n   * By default, this library will feed the environment variables directly to\n   * the Zod validator.\n   *\n   * This means that if you have an empty string for a value that is supposed\n   * to be a number (e.g. `PORT=` in a \".env\" file), Zod will incorrectly flag\n   * it as a type mismatch violation. Additionally, if you have an empty string\n   * for a value that is supposed to be a string with a default value (e.g.\n   * `DOMAIN=` in an \".env\" file), the default value will never be applied.\n   *\n   * In order to solve these issues, we recommend that all new projects\n   * explicitly specify this option as true.\n   */\n  skipValidation: true,\n});\n"
  },
  {
    "path": "packages/notifications/pagerduty/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery, sendTest } from \"./index\";\n\ndescribe(\"PagerDuty Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n\n  beforeEach(() => {\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockRestore();\n    }\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"us-east-1\",\n  });\n\n  const createMockIncident = () => ({\n    id: 1,\n    title: \"API Health Check is down\",\n    summary: \"API Health Check is down\",\n    status: \"triage\" as const,\n    monitorId: \"monitor-1\",\n    workspaceId: 1,\n    startedAt: Date.now(),\n  });\n\n  const createMockNotification = () => ({\n    id: 1,\n    name: \"PagerDuty Notification\",\n    provider: \"pagerduty\",\n    workspaceId: 1,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    data: '{\"pagerduty\":\"{\\\\\"integration_keys\\\\\":[{\\\\\"integration_key\\\\\":\\\\\"my_key\\\\\",\\\\\"name\\\\\":\\\\\"Default Service\\\\\",\\\\\"id\\\\\":\\\\\"ABCD\\\\\",\\\\\"type\\\\\":\\\\\"service\\\\\"}],\\\\\"account\\\\\":{\\\\\"subdomain\\\\\":\\\\\"test\\\\\",\\\\\"name\\\\\":\\\\\"test\\\\\"}}\"}',\n  });\n\n  test(\"Send Alert\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n    const incident = createMockIncident();\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      // @ts-expect-error\n      incident,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://events.pagerduty.com/v2/enqueue\");\n    expect(callArgs[1].method).toBe(\"POST\");\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.routing_key).toBe(\"my_key\");\n    expect(body.dedup_key).toBe(\"monitor-1\");\n    expect(body.event_action).toBe(\"trigger\");\n    expect(body.payload.summary).toBe(\"API Health Check is down\");\n    expect(body.payload.severity).toBe(\"error\");\n    expect(body.payload.custom_details.statusCode).toBe(500);\n    expect(body.payload.custom_details.message).toBe(\"Something went wrong\");\n  });\n\n  test(\"Send Alert with multiple integration keys\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse({\n      id: 1,\n      name: \"PagerDuty Notification\",\n      provider: \"pagerduty\",\n      workspaceId: 1,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      data: '{\"pagerduty\":\"{\\\\\"integration_keys\\\\\":[{\\\\\"integration_key\\\\\":\\\\\"key1\\\\\",\\\\\"name\\\\\":\\\\\"Service 1\\\\\",\\\\\"id\\\\\":\\\\\"ABCD\\\\\",\\\\\"type\\\\\":\\\\\"service\\\\\"},{\\\\\"integration_key\\\\\":\\\\\"key2\\\\\",\\\\\"name\\\\\":\\\\\"Service 2\\\\\",\\\\\"id\\\\\":\\\\\"EFGH\\\\\",\\\\\"type\\\\\":\\\\\"service\\\\\"}],\\\\\"account\\\\\":{\\\\\"subdomain\\\\\":\\\\\"test\\\\\",\\\\\"name\\\\\":\\\\\"test\\\\\"}}\"}',\n    });\n    const incident = createMockIncident();\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Error\",\n      // @ts-expect-error\n      incident,\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(2);\n    expect(fetchMock.mock.calls[0][1].body).toContain(\"key1\");\n    expect(fetchMock.mock.calls[1][1].body).toContain(\"key2\");\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n    const incident = createMockIncident();\n\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      message: \"Service degraded\",\n      // @ts-expect-error\n      incident,\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.payload.summary).toBe(\"API Health Check is degraded\");\n    expect(body.payload.severity).toBe(\"warning\");\n    expect(body.dedup_key).toBe(\"monitor-1\");\n  });\n\n  test(\"Send Recovery\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n    const incident = createMockIncident();\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 200,\n      message: \"Service recovered\",\n      // @ts-expect-error\n      incident,\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://events.pagerduty.com/v2/enqueue\");\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.routing_key).toBe(\"my_key\");\n    expect(body.dedup_key).toBe(\"monitor-1\");\n    expect(body.event_action).toBe(\"resolve\");\n  });\n\n  test(\"Send Test\", async () => {\n    const result = await sendTest({\n      integrationKey: \"test-integration-key\",\n    });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://events.pagerduty.com/v2/enqueue\");\n    expect(callArgs[1].method).toBe(\"POST\");\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.routing_key).toBe(\"test-integration-key\");\n    expect(body.dedup_key).toBe(\"openstatus-test\");\n    expect(body.event_action).toBe(\"trigger\");\n    expect(body.payload.summary).toBe(\"This is a test from OpenStatus\");\n    expect(body.payload.severity).toBe(\"error\");\n    expect(body.payload.custom_details.statusCode).toBe(418);\n  });\n\n  test(\"Send Test returns false on error\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const result = await sendTest({\n      integrationKey: \"test-key\",\n    });\n\n    expect(result).toBe(false);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"Handle fetch error gracefully\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n    const incident = createMockIncident();\n\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        // @ts-expect-error\n        incident,\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/notifications/pagerduty/src/index.ts",
    "content": "import { pagerdutyDataSchema } from \"@openstatus/db/src/schema\";\nimport type { NotificationContext } from \"@openstatus/notification-base\";\nimport {\n  PagerDutySchema,\n  resolveEventPayloadSchema,\n  triggerEventPayloadSchema,\n} from \"./schema/config\";\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n  cronTimestamp,\n}: NotificationContext) => {\n  const data = pagerdutyDataSchema.parse(JSON.parse(notification.data));\n\n  const notificationData = PagerDutySchema.parse(JSON.parse(data.pagerduty));\n\n  const { name } = monitor;\n\n  for (const integrationKey of notificationData.integration_keys) {\n    const { integration_key } = integrationKey;\n    const event = triggerEventPayloadSchema.parse({\n      routing_key: integration_key,\n      dedup_key: `${monitor.id}`,\n      event_action: \"trigger\",\n      payload: {\n        summary: `${name} is down`,\n        source: \"Open Status\",\n        severity: \"error\",\n        timestamp: new Date(cronTimestamp).toISOString(),\n        custom_details: {\n          statusCode,\n          message,\n        },\n      },\n    });\n    const res = await fetch(\"https://events.pagerduty.com/v2/enqueue\", {\n      method: \"POST\",\n      body: JSON.stringify(event),\n    });\n    if (!res.ok) {\n      console.log(`Failed to send alert notification: ${res.statusText}`);\n      throw new Error(\"Failed to send alert notification\");\n    }\n  }\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const data = pagerdutyDataSchema.parse(JSON.parse(notification.data));\n\n  const notificationData = PagerDutySchema.parse(JSON.parse(data.pagerduty));\n  const { name } = monitor;\n\n  for (const integrationKey of notificationData.integration_keys) {\n    const { integration_key } = integrationKey;\n\n    const event = triggerEventPayloadSchema.parse({\n      routing_key: integration_key,\n      dedup_key: `${monitor.id}`,\n      event_action: \"trigger\",\n      payload: {\n        summary: `${name} is degraded`,\n        source: \"Open Status\",\n        severity: \"warning\",\n        timestamp: new Date().toISOString(),\n        custom_details: {\n          statusCode,\n          message,\n        },\n      },\n    });\n\n    const res = await fetch(\"https://events.pagerduty.com/v2/enqueue\", {\n      method: \"POST\",\n      body: JSON.stringify(event),\n    });\n    if (!res.ok) {\n      console.log(`Failed to send alert notification: ${res.statusText}`);\n      throw new Error(\"Failed to send alert notification\");\n    }\n  }\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const data = pagerdutyDataSchema.parse(JSON.parse(notification.data));\n\n  const notificationData = PagerDutySchema.parse(JSON.parse(data.pagerduty));\n\n  for (const integrationKey of notificationData.integration_keys) {\n    const event = resolveEventPayloadSchema.parse({\n      routing_key: integrationKey.integration_key,\n      dedup_key: `${monitor.id}`,\n      event_action: \"resolve\",\n    });\n    const res = await fetch(\"https://events.pagerduty.com/v2/enqueue\", {\n      method: \"POST\",\n      body: JSON.stringify(event),\n    });\n    if (!res.ok) {\n      console.log(`Failed to send alert notification: ${res.statusText}`);\n      throw new Error(\"Failed to send alert notification\");\n    }\n  }\n};\n\nexport const sendTest = async ({\n  integrationKey,\n}: {\n  integrationKey: string;\n}) => {\n  console.log(\"Sending test alert to PagerDuty\");\n  try {\n    const event = triggerEventPayloadSchema.parse({\n      routing_key: integrationKey,\n      dedup_key: \"openstatus-test\",\n      event_action: \"trigger\",\n      payload: {\n        summary: \"This is a test from OpenStatus\",\n        source: \"Open Status\",\n        severity: \"error\",\n        timestamp: new Date().toISOString(),\n        custom_details: {\n          statusCode: 418,\n          message: 'I\"m a teapot',\n        },\n      },\n    });\n\n    const _res = await fetch(\"https://events.pagerduty.com/v2/enqueue\", {\n      method: \"POST\",\n      body: JSON.stringify(event),\n    });\n  } catch (err) {\n    console.log(err);\n    return false;\n  }\n  return true;\n};\nexport { PagerDutySchema };\n"
  },
  {
    "path": "packages/notifications/pagerduty/src/schema/config.ts",
    "content": "import { z } from \"zod\";\n\nexport const PagerDutySchema = z.object({\n  integration_keys: z.array(\n    z.object({\n      integration_key: z.string(),\n      name: z.string(),\n      id: z.string(),\n      type: z.string(),\n    }),\n  ),\n  account: z.object({ subdomain: z.string(), name: z.string() }),\n});\n\nexport const actionSchema = z.union([\n  z.literal(\"trigger\"),\n  z.literal(\"acknowledge\"),\n  z.literal(\"resolve\"),\n]);\n\nexport const severitySchema = z.union([\n  z.literal(\"critical\"),\n  z.literal(\"error\"),\n  z.literal(\"warning\"),\n  z.literal(\"info\"),\n]);\n\nexport const imageSchema = z.object({\n  src: z.string(),\n  href: z.string().optional(),\n  alt: z.string().optional(),\n});\n\nexport const linkSchema = z.object({\n  href: z.string(),\n  text: z.string().optional(),\n});\n\nconst baseEventPayloadSchema = z.object({\n  routing_key: z.string(),\n  dedup_key: z.string(),\n});\n\nexport const triggerEventPayloadSchema = baseEventPayloadSchema.extend(\n  z.object({\n    event_action: z.literal(\"trigger\"),\n    payload: z.object({\n      summary: z.string(),\n      source: z.string(),\n      severity: severitySchema,\n      timestamp: z.string().optional(),\n      component: z.string().optional(),\n      group: z.string().optional(),\n      class: z.string().optional(),\n      custom_details: z.any().optional(),\n    }),\n    images: z.array(imageSchema).optional(),\n    links: z.array(linkSchema).optional(),\n  }).shape,\n);\n\nexport const acknowledgeEventPayloadSchema = baseEventPayloadSchema.extend(\n  z.object({\n    event_action: z.literal(\"acknowledge\"),\n  }).shape,\n);\n\nexport const resolveEventPayloadSchema = baseEventPayloadSchema.extend(\n  z.object({\n    event_action: z.literal(\"resolve\"),\n  }).shape,\n);\n\nexport const eventPayloadV2Schema = z.discriminatedUnion(\"event_action\", [\n  triggerEventPayloadSchema,\n  acknowledgeEventPayloadSchema,\n  resolveEventPayloadSchema,\n]);\n"
  },
  {
    "path": "packages/notifications/pagerduty/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/slack/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-slack\",\n  \"version\": \"0.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/slack/src/blocks.ts",
    "content": "import type { FormattedMessageData } from \"@openstatus/notification-base\";\n\n/**\n * Slack Block types for rich message formatting\n * Reference: https://docs.slack.dev/messaging/formatting-message-text\n */\n\ninterface SlackTextObject {\n  type: \"plain_text\" | \"mrkdwn\";\n  text: string;\n  emoji?: boolean;\n}\n\ninterface SlackHeaderBlock {\n  type: \"header\";\n  text: SlackTextObject;\n}\n\ninterface SlackSectionBlock {\n  type: \"section\";\n  text?: SlackTextObject;\n  fields?: SlackTextObject[];\n  accessory?: SlackButtonElement;\n}\n\ninterface SlackDividerBlock {\n  type: \"divider\";\n}\n\ninterface SlackActionsBlock {\n  type: \"actions\";\n  elements: SlackButtonElement[];\n}\n\ninterface SlackButtonElement {\n  type: \"button\";\n  text: SlackTextObject;\n  url?: string;\n  action_id?: string;\n}\n\ntype SlackBlock =\n  | SlackHeaderBlock\n  | SlackSectionBlock\n  | SlackDividerBlock\n  | SlackActionsBlock;\n\n/**\n * Escapes special characters for Slack mrkdwn format\n * Reference: https://api.slack.com/reference/surfaces/formatting#escaping\n *\n * @param text - Text to escape\n * @returns Escaped text safe for Slack mrkdwn\n *\n * @example\n * escapeSlackText(\"Hello & <world>\") // \"Hello &amp; &lt;world&gt;\"\n */\nexport function escapeSlackText(text: string): string {\n  return text\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\");\n}\n\n/**\n * Builds Slack blocks for alert notifications\n *\n * Layout:\n * - Header: \"{monitor.name} is failing\"\n * - Section: \"METHOD URL\" in code format (e.g., `GET https://api.example.com`)\n * - Divider\n * - Section: 4 fields in 2x2 grid (Status, Regions, Latency, Cron Timestamp)\n * - Section: Error message in code block\n * - Actions: Dashboard button\n *\n * @param data - Formatted message data from buildCommonMessageData\n * @returns Array of Slack blocks\n *\n * @example\n * const blocks = buildAlertBlocks({\n *   monitorName: \"API Health\",\n *   monitorUrl: \"https://api.example.com\",\n *   monitorMethod: \"GET\",\n *   monitorJobType: \"http\",\n *   statusCodeFormatted: \"503 Service Unavailable\",\n *   errorMessage: \"Connection timeout\",\n *   timestampFormatted: \"Jan 22, 2026 at 14:30 UTC\",\n *   regionsDisplay: \"iad, fra, syd\",\n *   latencyDisplay: \"1,234 ms\",\n *   dashboardUrl: \"https://app.openstatus.dev/monitors/123\"\n * });\n */\nexport function buildAlertBlocks(data: FormattedMessageData): SlackBlock[] {\n  const escapedName = escapeSlackText(data.monitorName);\n  const escapedError = escapeSlackText(data.errorMessage);\n\n  // Format description as \"METHOD URL\" or just \"URL\" for non-HTTP\n  const description =\n    data.monitorMethod && data.monitorJobType === \"http\"\n      ? `${data.monitorMethod} <${data.monitorUrl}|${data.monitorUrl}>`\n      : `<${data.monitorUrl}|${data.monitorUrl}>`;\n\n  return [\n    {\n      type: \"header\",\n      text: {\n        type: \"plain_text\",\n        text: `${escapedName} is failing`,\n        emoji: false,\n      },\n    },\n    {\n      type: \"section\",\n      text: {\n        type: \"mrkdwn\",\n        text: `\\`${description}\\``,\n      },\n    },\n    {\n      type: \"divider\",\n    },\n    {\n      type: \"section\",\n      fields: [\n        {\n          type: \"mrkdwn\",\n          text: `*Status*\\n${data.statusCodeFormatted}`,\n        },\n        {\n          type: \"mrkdwn\",\n          text: `*Regions*\\n${data.regionsDisplay}`,\n        },\n        {\n          type: \"mrkdwn\",\n          text: `*Latency*\\n${data.latencyDisplay}`,\n        },\n        {\n          type: \"mrkdwn\",\n          text: `*Cron Timestamp*\\n${data.timestampFormatted}`,\n        },\n      ],\n    },\n    {\n      type: \"section\",\n      text: {\n        type: \"mrkdwn\",\n        text: `*Error*\\n\\`\\`\\`${escapedError}\\`\\`\\``,\n      },\n    },\n    {\n      type: \"actions\",\n      elements: [\n        {\n          type: \"button\",\n          text: {\n            type: \"plain_text\",\n            text: \"View Dashboard\",\n            emoji: true,\n          },\n          url: data.dashboardUrl,\n          action_id: \"view_dashboard\",\n        },\n      ],\n    },\n  ];\n}\n\n/**\n * Builds Slack blocks for recovery notifications\n *\n * Layout:\n * - Header: \"{monitor.name} is recovered\"\n * - Section: \"METHOD URL\" in code format (e.g., `GET https://api.example.com`)\n * - Section: Downtime duration (optional, only if data.incidentDuration exists)\n * - Divider\n * - Section: 4 fields in 2x2 grid (Status, Regions, Latency, Cron Timestamp)\n * - Actions: Dashboard button\n *\n * @param data - Formatted message data from buildCommonMessageData\n * @returns Array of Slack blocks\n *\n * @example\n * const blocks = buildRecoveryBlocks({\n *   monitorName: \"API Health\",\n *   monitorUrl: \"https://api.example.com\",\n *   monitorMethod: \"GET\",\n *   monitorJobType: \"http\",\n *   statusCodeFormatted: \"200 OK\",\n *   errorMessage: \"\",\n *   timestampFormatted: \"Jan 22, 2026 at 14:35 UTC\",\n *   regionsDisplay: \"iad, fra, syd\",\n *   latencyDisplay: \"156 ms\",\n *   dashboardUrl: \"https://app.openstatus.dev/monitors/123\",\n *   incidentDuration: \"5m 30s\"\n * });\n */\nexport function buildRecoveryBlocks(data: FormattedMessageData): SlackBlock[] {\n  const escapedName = escapeSlackText(data.monitorName);\n\n  // Format description as \"METHOD URL\" or just \"URL\" for non-HTTP\n  const description =\n    data.monitorMethod && data.monitorJobType === \"http\"\n      ? `${data.monitorMethod} <${data.monitorUrl}|${data.monitorUrl}>`\n      : `<${data.monitorUrl}|${data.monitorUrl}>`;\n\n  const blocks: SlackBlock[] = [\n    {\n      type: \"header\",\n      text: {\n        type: \"plain_text\",\n        text: `${escapedName} is recovered`,\n        emoji: false,\n      },\n    },\n    {\n      type: \"section\",\n      text: {\n        type: \"mrkdwn\",\n        text: `\\`${description}\\``,\n      },\n    },\n  ];\n\n  // Only include downtime if incident duration is available\n  if (data.incidentDuration) {\n    blocks.push({\n      type: \"section\",\n      text: {\n        type: \"mrkdwn\",\n        text: `⏱️ *Downtime:* ${data.incidentDuration}`,\n      },\n    });\n  }\n\n  blocks.push(\n    {\n      type: \"divider\",\n    },\n    {\n      type: \"section\",\n      fields: [\n        {\n          type: \"mrkdwn\",\n          text: `*Status*\\n${data.statusCodeFormatted}`,\n        },\n        {\n          type: \"mrkdwn\",\n          text: `*Regions*\\n${data.regionsDisplay}`,\n        },\n        {\n          type: \"mrkdwn\",\n          text: `*Latency*\\n${data.latencyDisplay}`,\n        },\n        {\n          type: \"mrkdwn\",\n          text: `*Cron Timestamp*\\n${data.timestampFormatted}`,\n        },\n      ],\n    },\n    {\n      type: \"actions\",\n      elements: [\n        {\n          type: \"button\",\n          text: {\n            type: \"plain_text\",\n            text: \"View Dashboard\",\n            emoji: true,\n          },\n          url: data.dashboardUrl,\n          action_id: \"view_dashboard\",\n        },\n      ],\n    },\n  );\n\n  return blocks;\n}\n\n/**\n * Builds Slack blocks for degraded notifications\n *\n * Layout:\n * - Header: \"{monitor.name} is degraded\"\n * - Section: \"METHOD URL\" in code format (e.g., `GET https://api.example.com`)\n * - Section: Previous incident duration (optional, only if data.incidentDuration exists)\n * - Divider\n * - Section: 4 fields in 2x2 grid (Status, Regions, Latency, Cron Timestamp)\n * - Actions: Dashboard button\n *\n * @param data - Formatted message data from buildCommonMessageData\n * @returns Array of Slack blocks\n *\n * @example\n * const blocks = buildDegradedBlocks({\n *   monitorName: \"API Health\",\n *   monitorUrl: \"https://api.example.com\",\n *   monitorMethod: \"GET\",\n *   monitorJobType: \"http\",\n *   statusCodeFormatted: \"504 Gateway Timeout\",\n *   errorMessage: \"Slow response\",\n *   timestampFormatted: \"Jan 22, 2026 at 14:40 UTC\",\n *   regionsDisplay: \"iad, fra, syd\",\n *   latencyDisplay: \"5,234 ms\",\n *   dashboardUrl: \"https://app.openstatus.dev/monitors/123\",\n *   incidentDuration: \"2h 15m\"\n * });\n */\nexport function buildDegradedBlocks(data: FormattedMessageData): SlackBlock[] {\n  const escapedName = escapeSlackText(data.monitorName);\n\n  // Format description as \"METHOD URL\" or just \"URL\" for non-HTTP\n  const description =\n    data.monitorMethod && data.monitorJobType === \"http\"\n      ? `${data.monitorMethod} <${data.monitorUrl}|${data.monitorUrl}>`\n      : `<${data.monitorUrl}|${data.monitorUrl}>`;\n\n  const blocks: SlackBlock[] = [\n    {\n      type: \"header\",\n      text: {\n        type: \"plain_text\",\n        text: `${escapedName} is degraded`,\n        emoji: false,\n      },\n    },\n    {\n      type: \"section\",\n      text: {\n        type: \"mrkdwn\",\n        text: `\\`${description}\\``,\n      },\n    },\n  ];\n\n  // Only include previous incident duration if available\n  if (data.incidentDuration) {\n    blocks.push({\n      type: \"section\",\n      text: {\n        type: \"mrkdwn\",\n        text: `⏱️ *Previous Incident Duration:* ${data.incidentDuration}`,\n      },\n    });\n  }\n\n  blocks.push(\n    {\n      type: \"divider\",\n    },\n    {\n      type: \"section\",\n      fields: [\n        {\n          type: \"mrkdwn\",\n          text: `*Status*\\n${data.statusCodeFormatted}`,\n        },\n        {\n          type: \"mrkdwn\",\n          text: `*Regions*\\n${data.regionsDisplay}`,\n        },\n        {\n          type: \"mrkdwn\",\n          text: `*Latency*\\n${data.latencyDisplay}`,\n        },\n        {\n          type: \"mrkdwn\",\n          text: `*Cron Timestamp*\\n${data.timestampFormatted}`,\n        },\n      ],\n    },\n    {\n      type: \"actions\",\n      elements: [\n        {\n          type: \"button\",\n          text: {\n            type: \"plain_text\",\n            text: \"View Dashboard\",\n            emoji: true,\n          },\n          url: data.dashboardUrl,\n          action_id: \"view_dashboard\",\n        },\n      ],\n    },\n  );\n\n  return blocks;\n}\n"
  },
  {
    "path": "packages/notifications/slack/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { COLORS } from \"@openstatus/notification-base\";\nimport {\n  sendAlert,\n  sendDegraded,\n  sendRecovery,\n  sendTestSlackMessage,\n} from \"./index\";\n\ndescribe(\"Slack Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n\n  beforeEach(() => {\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockRestore();\n    }\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"us-east-1\",\n  });\n\n  const createMockNotification = () => ({\n    id: 1,\n    name: \"Slack Notification\",\n    provider: \"slack\",\n    workspaceId: 1,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    data: '{\"slack\":\"https://hooks.slack.com/services/url\"}',\n  });\n\n  test(\"Send Alert\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\"https://hooks.slack.com/services/url\");\n    expect(callArgs[1].method).toBe(\"POST\");\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.attachments).toBeDefined();\n    expect(body.attachments[0].color).toBe(COLORS.red);\n    expect(body.attachments[0].blocks).toBeDefined();\n    expect(body.attachments[0].blocks.length).toBeGreaterThan(0);\n    expect(body.attachments[0].blocks[0].text.text).toContain(\"is failing\");\n  });\n\n  test(\"Send Alert without statusCode\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      message: \"Connection timeout\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.attachments[0].color).toBe(COLORS.red);\n    expect(body.attachments[0].blocks[3].fields[0].text).toContain(\"Unknown\");\n  });\n\n  test(\"Send Recovery\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 200,\n      message: \"Service recovered\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.attachments).toBeDefined();\n    expect(body.attachments[0].color).toBe(COLORS.green);\n    expect(body.attachments[0].blocks[0].text.text).toContain(\"is recovered\");\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      message: \"Service degraded\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.attachments).toBeDefined();\n    expect(body.attachments[0].color).toBe(COLORS.yellow);\n    expect(body.attachments[0].blocks[0].text.text).toContain(\"is degraded\");\n  });\n\n  test(\"Send Test Slack Message\", async () => {\n    const webhookUrl = \"https://hooks.slack.com/services/test/url\";\n\n    await sendTestSlackMessage(webhookUrl);\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(webhookUrl);\n\n    const body = JSON.parse(callArgs[1].body);\n    expect(body.attachments[0].blocks[0].text.text).toContain(\n      \"Test Notification\",\n    );\n  });\n\n  test(\"Send Test Slack Message throws error on empty webhookUrl\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    expect(sendTestSlackMessage(\"\")).rejects.toThrow();\n    expect(fetchMock).toHaveBeenCalledTimes(0);\n  });\n\n  test(\"Handle fetch error gracefully\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    // Should not throw - function catches errors internally\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/notifications/slack/src/index.ts",
    "content": "import { slackDataSchema } from \"@openstatus/db/src/schema\";\nimport {\n  COLORS,\n  type NotificationContext,\n  buildCommonMessageData,\n} from \"@openstatus/notification-base\";\nimport {\n  buildAlertBlocks,\n  buildDegradedBlocks,\n  buildRecoveryBlocks,\n} from \"./blocks\";\n\n// biome-ignore lint/suspicious/noExplicitAny: <explanation>\nconst postToWebhook = async (body: any, webhookUrl: string) => {\n  if (!webhookUrl || webhookUrl.trim() === \"\") {\n    throw new Error(\"Slack webhook URL is required\");\n  }\n\n  const res = await fetch(webhookUrl, {\n    method: \"POST\",\n    body: JSON.stringify(body),\n  });\n  if (!res.ok) {\n    throw new Error(`Failed to send Slack notification: ${res.statusText}`);\n  }\n};\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n  cronTimestamp,\n  latency,\n  regions,\n}: NotificationContext) => {\n  const notificationData = slackDataSchema.parse(JSON.parse(notification.data));\n  const { slack: webhookUrl } = notificationData;\n\n  const context = {\n    monitor,\n    notification,\n    statusCode,\n    message,\n    cronTimestamp,\n    latency,\n    regions,\n  };\n\n  const data = buildCommonMessageData(context);\n  const blocks = buildAlertBlocks(data);\n\n  await postToWebhook(\n    {\n      attachments: [\n        {\n          color: COLORS.red,\n          blocks,\n        },\n      ],\n    },\n    webhookUrl,\n  );\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n  incident,\n  cronTimestamp,\n  regions,\n  latency,\n}: NotificationContext) => {\n  const notificationData = slackDataSchema.parse(JSON.parse(notification.data));\n  const { slack: webhookUrl } = notificationData;\n\n  const context = {\n    monitor,\n    notification,\n    statusCode,\n    message,\n    cronTimestamp,\n    latency,\n    regions,\n  };\n\n  const data = buildCommonMessageData(context, { incident });\n  const blocks = buildRecoveryBlocks(data);\n\n  await postToWebhook(\n    {\n      attachments: [\n        {\n          color: COLORS.green,\n          blocks,\n        },\n      ],\n    },\n    webhookUrl,\n  );\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n  incident,\n  cronTimestamp,\n  regions,\n  latency,\n}: NotificationContext) => {\n  const notificationData = slackDataSchema.parse(JSON.parse(notification.data));\n  const { slack: webhookUrl } = notificationData;\n\n  const context = {\n    monitor,\n    notification,\n    statusCode,\n    message,\n    cronTimestamp,\n    latency,\n    regions,\n  };\n\n  const data = buildCommonMessageData(context, { incident });\n  const blocks = buildDegradedBlocks(data);\n\n  await postToWebhook(\n    {\n      attachments: [\n        {\n          color: COLORS.yellow,\n          blocks,\n        },\n      ],\n    },\n    webhookUrl,\n  );\n};\n\nexport const sendTestSlackMessage = async (webhookUrl: string) => {\n  await postToWebhook(\n    {\n      attachments: [\n        {\n          color: COLORS.green,\n          blocks: [\n            {\n              type: \"header\",\n              text: {\n                type: \"plain_text\",\n                text: \"Test Notification\",\n                emoji: false,\n              },\n            },\n            {\n              type: \"section\",\n              text: {\n                type: \"mrkdwn\",\n                text: \"`🧪 Your Slack webhook is configured correctly!`\",\n              },\n            },\n            {\n              type: \"divider\",\n            },\n            {\n              type: \"section\",\n              fields: [\n                {\n                  type: \"mrkdwn\",\n                  text: \"*Status*\\nWebhook Connected\",\n                },\n                {\n                  type: \"mrkdwn\",\n                  text: \"*Type*\\nTest Notification\",\n                },\n              ],\n            },\n            {\n              type: \"section\",\n              text: {\n                type: \"mrkdwn\",\n                text: \"*Next Steps*\\nYou will receive notifications here when your monitors trigger fail, recover, or degrades.\",\n              },\n            },\n            {\n              type: \"actions\",\n              elements: [\n                {\n                  type: \"button\",\n                  text: {\n                    type: \"plain_text\",\n                    text: \"View Dashboard\",\n                    emoji: true,\n                  },\n                  url: \"https://app.openstatus.dev\",\n                  action_id: \"view_dashboard\",\n                },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n    webhookUrl,\n  );\n};\n"
  },
  {
    "path": "packages/notifications/slack/src/mock.ts",
    "content": "import type { Monitor, Notification } from \"@openstatus/db/src/schema\";\nimport {\n  sendAlert,\n  sendDegraded,\n  sendRecovery,\n  sendTestSlackMessage,\n} from \"./index\";\n\nconst monitor: Monitor = {\n  id: 1,\n  name: \"OpenStatus Docs\",\n  url: \"https://docs.openstatus.dev\",\n  periodicity: \"10m\",\n  jobType: \"http\",\n  active: true,\n  public: true,\n  createdAt: null,\n  updatedAt: null,\n  regions: [\"ams\", \"fra\"],\n  description: \"Monitor Description\",\n  headers: [],\n  body: \"\",\n  workspaceId: 1,\n  timeout: 45000,\n  degradedAfter: null,\n  assertions: null,\n  status: \"active\",\n  method: \"GET\",\n  deletedAt: null,\n  otelEndpoint: null,\n  otelHeaders: [],\n  retry: 3,\n  followRedirects: false,\n  externalName: null,\n};\n\nconst notification: Notification = {\n  id: 1,\n  name: \"Slack\",\n  data: `{ \"slack\": \"${process.env.SLACK_WEBHOOK}\" }`,\n  createdAt: null,\n  updatedAt: null,\n  workspaceId: 1,\n  provider: \"slack\",\n};\n\nconst cronTimestamp = Date.now();\n\nif (process.env.NODE_ENV === \"development\") {\n  await sendDegraded({\n    monitor,\n    notification,\n    cronTimestamp,\n  });\n\n  await sendAlert({\n    monitor,\n    notification,\n    statusCode: 500,\n    cronTimestamp,\n  });\n\n  await sendRecovery({\n    monitor,\n    notification,\n    cronTimestamp,\n  });\n\n  await sendTestSlackMessage(`${process.env.SLACK_WEBHOOK}`);\n}\n"
  },
  {
    "path": "packages/notifications/slack/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/telegram/.gitignore",
    "content": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\n\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# Runtime data\n\npids\n_.pid\n_.seed\n\\*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\n\nlib-cov\n\n# Coverage directory used by tools like istanbul\n\ncoverage\n\\*.lcov\n\n# nyc test coverage\n\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n\n.grunt\n\n# Bower dependency directory (https://bower.io/)\n\nbower_components\n\n# node-waf configuration\n\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\n\nbuild/Release\n\n# Dependency directories\n\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\n\nweb_modules/\n\n# TypeScript cache\n\n\\*.tsbuildinfo\n\n# Optional npm cache directory\n\n.npm\n\n# Optional eslint cache\n\n.eslintcache\n\n# Optional stylelint cache\n\n.stylelintcache\n\n# Microbundle cache\n\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n\n.node_repl_history\n\n# Output of 'npm pack'\n\n\\*.tgz\n\n# Yarn Integrity file\n\n.yarn-integrity\n\n# dotenv environment variable files\n\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n\n.cache\n.parcel-cache\n\n# Next.js build output\n\n.next\nout\n\n# Nuxt.js build / generate output\n\n.nuxt\ndist\n\n# Gatsby files\n\n.cache/\n\n# Comment in the public line in if your project uses Gatsby and not Next.js\n\n# https://nextjs.org/blog/next-9-1#public-directory-support\n\n# public\n\n# vuepress build output\n\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n\n.temp\n.cache\n\n# Docusaurus cache and generated files\n\n.docusaurus\n\n# Serverless directories\n\n.serverless/\n\n# FuseBox cache\n\n.fusebox/\n\n# DynamoDB Local files\n\n.dynamodb/\n\n# TernJS port file\n\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.\\*\n"
  },
  {
    "path": "packages/notifications/telegram/README.md",
    "content": "# @openstatus/notification-telegram\nTo install dependencies:\n\n```bash\nbun install\n```\n\nTo run:\n\n```bash\nbun run src/index.ts\n```\n\nThis project was created using `bun init` in bun v1.0.0. [Bun](https://bun.sh)\nis a fast all-in-one JavaScript runtime.\n\n\n---\n\nImportant `@BotFather` commands to update the bot.\n\n```\nChange profile image\n/setuserpic\nChange display name\n/setname\nChange bot description\n/setdescription\nShort about text shown in bio preview\n/setabouttext\n```\n\n---\n\n#### TODO\n\nThis integration is very \"raw\". We need to let the user search for their chat id (e.g. by asking RawDataBot `@raw_info_bot`).\n\nIn the future, we could work with `/setcommands` behaviors to send the value to our `/getUpdates` endpoint and access the chat id. We will need to forward a specific unique identifier with it to match the chat id with the proper notification.\n\nAn improved UX option:\n\n```\n1. user enters `/connect` command\n2. we listen to the `/getUpdates` messages and generate an id, store it for a few days in redis, and send it back to the user's chat id\n3. user enters generated id into openstatus dashboard to connect with chat id - we validate it from the `/getUpdates`\n```\n\n"
  },
  {
    "path": "packages/notifications/telegram/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-telegram\",\n  \"version\": \"1.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/telegram/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery, sendTest } from \"./index\";\n\ndescribe(\"Telegram Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n  const originalEnv = process.env.TELEGRAM_BOT_TOKEN;\n\n  beforeEach(() => {\n    process.env.TELEGRAM_BOT_TOKEN = \"test-bot-token-123\";\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockRestore();\n    }\n    if (originalEnv) {\n      process.env.TELEGRAM_BOT_TOKEN = originalEnv;\n    } else {\n      process.env.TELEGRAM_BOT_TOKEN = undefined;\n    }\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"us-east-1\",\n  });\n\n  const createMockNotification = () => ({\n    id: 1,\n    name: \"Telegram Notification\",\n    provider: \"telegram\",\n    workspaceId: 1,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    data: JSON.stringify({\n      telegram: {\n        chatId: \"123456789\",\n      },\n    }),\n  });\n\n  test(\"Send Alert\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toContain(\n      \"https://api.telegram.org/bottest-bot-token-123/sendMessage\",\n    );\n    expect(callArgs[0]).toContain(\"chat_id=123456789\");\n    expect(callArgs[0]).toContain(\"API Health Check\");\n  });\n\n  test(\"Send Alert without statusCode\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      message: \"Connection timeout\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toContain(\"error: Connection timeout\");\n  });\n\n  test(\"Send Recovery\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 200,\n      message: \"Service recovered\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toContain(\"is up again\");\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      message: \"Service degraded\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toContain(\"is degraded\");\n  });\n\n  test(\"Send Test\", async () => {\n    const result = await sendTest({\n      chatId: \"123456789\",\n    });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toContain(\n      \"https://api.telegram.org/bottest-bot-token-123/sendMessage\",\n    );\n    expect(callArgs[0]).toContain(\"chat_id=123456789\");\n    expect(callArgs[0]).toContain(\"This is a test message from OpenStatus\");\n  });\n\n  test(\"Send Test returns false on error\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const result = await sendTest({\n      chatId: \"123456789\",\n    });\n\n    expect(result).toBe(false);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"Handle fetch error gracefully\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"fetch not called when TELEGRAM_BOT_TOKEN is not set\", async () => {\n    process.env.TELEGRAM_BOT_TOKEN = undefined;\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n    // Should not call fetch when token is missing\n    expect(fetchMock).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/notifications/telegram/src/index.ts",
    "content": "import { telegramDataSchema } from \"@openstatus/db/src/schema\";\nimport type { NotificationContext } from \"@openstatus/notification-base\";\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const notificationData = telegramDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { name } = monitor;\n\n  const body = `Your monitor ${name} / ${monitor.url} is down with ${\n    statusCode ? `status code ${statusCode}` : `error: ${message}`\n  }`;\n\n  await sendMessage({\n    chatId: notificationData.telegram.chatId,\n    message: body,\n  });\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = telegramDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { name } = monitor;\n\n  const body = `Your monitor ${name} / ${monitor.url} is up again`;\n  await sendMessage({\n    chatId: notificationData.telegram.chatId,\n    message: body,\n  });\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = telegramDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const { name } = monitor;\n\n  const body = `Your monitor ${name} / ${monitor.url} is degraded `;\n\n  await sendMessage({\n    chatId: notificationData.telegram.chatId,\n    message: body,\n  });\n};\n\nexport const sendTest = async ({ chatId }: { chatId: string }) => {\n  try {\n    await sendMessage({\n      chatId,\n      message: \"This is a test message from OpenStatus. You are good to go!\",\n    });\n  } catch (err) {\n    console.log(err);\n    return false;\n  }\n  return true;\n};\n\nexport async function sendMessage({\n  chatId,\n  message,\n}: {\n  chatId: string;\n  message: string;\n}) {\n  if (!process.env.TELEGRAM_BOT_TOKEN) {\n    throw new Error(\"TELEGRAM_BOT_TOKEN is not set\");\n  }\n  const res = await fetch(\n    `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage?chat_id=${chatId}&text=${message}`,\n  );\n  if (!res.ok) {\n    throw new Error(`Failed to send telegram message: ${res.statusText}`);\n  }\n  return res;\n}\n"
  },
  {
    "path": "packages/notifications/telegram/src/schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const TelegramSchema = z.object({\n  telegram: z.object({\n    chatId: z.string(),\n  }),\n});\n"
  },
  {
    "path": "packages/notifications/telegram/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/twillio-sms/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-twillio-sms\",\n  \"version\": \"0.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"validator\": \"13.12.0\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"@types/validator\": \"13.12.0\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/twillio-sms/src/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    TWILLIO_AUTH_TOKEN: z.string(),\n    TWILLIO_ACCOUNT_ID: z.string(),\n  },\n\n  /**\n   * What object holds the environment variables at runtime. This is usually\n   * `process.env` or `import.meta.env`.\n   */\n  runtimeEnv: process.env,\n\n  /**\n   * By default, this library will feed the environment variables directly to\n   * the Zod validator.\n   *\n   * This means that if you have an empty string for a value that is supposed\n   * to be a number (e.g. `PORT=` in a \".env\" file), Zod will incorrectly flag\n   * it as a type mismatch violation. Additionally, if you have an empty string\n   * for a value that is supposed to be a string with a default value (e.g.\n   * `DOMAIN=` in an \".env\" file), the default value will never be applied.\n   *\n   * In order to solve these issues, we recommend that all new projects\n   * explicitly specify this option as true.\n   */\n  skipValidation: true,\n});\n"
  },
  {
    "path": "packages/notifications/twillio-sms/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery } from \"./index\";\n\ndescribe(\"Twilio SMS Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n  const originalEnv = {\n    TWILLIO_ACCOUNT_ID: process.env.TWILLIO_ACCOUNT_ID,\n    TWILLIO_AUTH_TOKEN: process.env.TWILLIO_AUTH_TOKEN,\n  };\n\n  beforeEach(() => {\n    process.env.TWILLIO_ACCOUNT_ID = \"test-account-id\";\n    process.env.TWILLIO_AUTH_TOKEN = \"test-auth-token\";\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockRestore();\n    }\n    if (originalEnv.TWILLIO_ACCOUNT_ID) {\n      process.env.TWILLIO_ACCOUNT_ID = originalEnv.TWILLIO_ACCOUNT_ID;\n    } else {\n      process.env.TWILLIO_ACCOUNT_ID = undefined;\n    }\n    if (originalEnv.TWILLIO_AUTH_TOKEN) {\n      process.env.TWILLIO_AUTH_TOKEN = originalEnv.TWILLIO_AUTH_TOKEN;\n    } else {\n      process.env.TWILLIO_AUTH_TOKEN = undefined;\n    }\n  });\n\n  const createMockMonitor = () => ({\n    id: \"monitor-1\",\n    name: \"API Health Check\",\n    url: \"https://api.example.com/health\",\n    jobType: \"http\" as const,\n    periodicity: \"5m\" as const,\n    status: \"active\" as const,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    region: \"us-east-1\",\n  });\n\n  const createMockNotification = () => ({\n    id: 1,\n    name: \"Twilio SMS Notification\",\n    provider: \"sms\",\n    workspaceId: 1,\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    data: '{\"sms\":\"+33623456789\"}',\n  });\n\n  test(\"Send Alert\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    expect(callArgs[0]).toBe(\n      \"https://api.twilio.com/2010-04-01/Accounts/test-account-id/Messages.json\",\n    );\n    expect(callArgs[1].method).toBe(\"post\");\n    expect(callArgs[1].headers.Authorization).toBe(\n      `Basic ${btoa(\"test-account-id:test-auth-token\")}`,\n    );\n\n    const formData = callArgs[1].body as FormData;\n    expect(formData.get(\"To\")).toBe(\"+33623456789\");\n    expect(formData.get(\"From\")).toBe(\"+14807252613\");\n    expect(formData.get(\"Body\")).toContain(\"API Health Check\");\n    expect(formData.get(\"Body\")).toContain(\"status code 500\");\n  });\n\n  test(\"Send Alert without statusCode\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification,\n      message: \"Connection timeout\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const formData = callArgs[1].body as FormData;\n    expect(formData.get(\"Body\")).toContain(\"error: Connection timeout\");\n  });\n\n  test(\"Send Recovery\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 200,\n      message: \"Service recovered\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const formData = callArgs[1].body as FormData;\n    expect(formData.get(\"Body\")).toContain(\"is up again\");\n    expect(formData.get(\"Body\")).toContain(\"API Health Check\");\n  });\n\n  test(\"Send Degraded\", async () => {\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification,\n      statusCode: 503,\n      message: \"Service degraded\",\n      cronTimestamp: Date.now(),\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    const formData = callArgs[1].body as FormData;\n    expect(formData.get(\"Body\")).toContain(\"is degraded\");\n    expect(formData.get(\"Body\")).toContain(\"API Health Check\");\n  });\n\n  test(\"Handle fetch error gracefully\", async () => {\n    fetchMock.mockImplementation(() =>\n      Promise.reject(new Error(\"Network error\")),\n    );\n\n    const monitor = createMockMonitor();\n    const notification = selectNotificationSchema.parse(\n      createMockNotification(),\n    );\n\n    expect(\n      sendAlert({\n        // @ts-expect-error\n        monitor,\n        notification,\n        statusCode: 500,\n        message: \"Error\",\n        cronTimestamp: Date.now(),\n      }),\n    ).rejects.toThrow();\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/notifications/twillio-sms/src/index.ts",
    "content": "import { phoneDataSchema } from \"@openstatus/db/src/schema\";\nimport type { NotificationContext } from \"@openstatus/notification-base\";\nimport { env } from \"./env\";\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const notificationData = phoneDataSchema.parse(JSON.parse(notification.data));\n  const { name } = monitor;\n\n  const body = new FormData();\n  body.set(\"To\", notificationData.sms);\n  body.set(\"From\", \"+14807252613\");\n  body.set(\n    \"Body\",\n    `Your monitor ${name} / ${monitor.url} is down with ${\n      statusCode ? `status code ${statusCode}` : `error: ${message}`\n    }`,\n  );\n\n  const res = await fetch(\n    `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`,\n    {\n      method: \"post\",\n      body,\n      headers: {\n        Authorization: `Basic ${btoa(\n          `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`,\n        )}`,\n      },\n    },\n  );\n  if (!res.ok) {\n    throw new Error(`Failed to send SMS: ${res.statusText}`);\n  }\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = phoneDataSchema.parse(JSON.parse(notification.data));\n  const { name } = monitor;\n\n  const body = new FormData();\n  body.set(\"To\", notificationData.sms);\n  body.set(\"From\", \"+14807252613\");\n  body.set(\"Body\", `Your monitor ${name} / ${monitor.url} is up again`);\n\n  const res = await fetch(\n    `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`,\n    {\n      method: \"post\",\n      body,\n      headers: {\n        Authorization: `Basic ${btoa(\n          `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`,\n        )}`,\n      },\n    },\n  );\n  if (!res.ok) {\n    throw new Error(`Failed to send SMS: ${res.statusText}`);\n  }\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = phoneDataSchema.parse(JSON.parse(notification.data));\n  const { name } = monitor;\n\n  const body = new FormData();\n  body.set(\"To\", notificationData.sms);\n  body.set(\"From\", \"+14807252613\");\n  body.set(\"Body\", `Your monitor ${name} / ${monitor.url} is degraded `);\n\n  const res = await fetch(\n    `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`,\n    {\n      method: \"post\",\n      body,\n      headers: {\n        Authorization: `Basic ${btoa(\n          `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`,\n        )}`,\n      },\n    },\n  );\n  if (!res.ok) {\n    throw new Error(`Failed to send SMS: ${res.statusText}`);\n  }\n};\n"
  },
  {
    "path": "packages/notifications/twillio-sms/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/twillio-whatsapp/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-twillio-whatsapp\",\n  \"version\": \"0.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"@t3-oss/env-core\": \"0.13.10\",\n    \"validator\": \"13.12.0\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"@types/validator\": \"13.12.0\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/twillio-whatsapp/src/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    TWILLIO_AUTH_TOKEN: z.string(),\n    TWILLIO_ACCOUNT_ID: z.string(),\n  },\n\n  /**\n   * What object holds the environment variables at runtime. This is usually\n   * `process.env` or `import.meta.env`.\n   */\n  runtimeEnv: process.env,\n\n  /**\n   * By default, this library will feed the environment variables directly to\n   * the Zod validator.\n   *\n   * This means that if you have an empty string for a value that is supposed\n   * to be a number (e.g. `PORT=` in a \".env\" file), Zod will incorrectly flag\n   * it as a type mismatch violation. Additionally, if you have an empty string\n   * for a value that is supposed to be a string with a default value (e.g.\n   * `DOMAIN=` in an \".env\" file), the default value will never be applied.\n   *\n   * In order to solve these issues, we recommend that all new projects\n   * explicitly specify this option as true.\n   */\n  skipValidation: true,\n});\n"
  },
  {
    "path": "packages/notifications/twillio-whatsapp/src/index.test.ts",
    "content": "import {\n  afterEach,\n  beforeEach,\n  describe,\n  expect,\n  jest,\n  spyOn,\n  test,\n} from \"bun:test\";\nimport { selectNotificationSchema } from \"@openstatus/db/src/schema\";\nimport { sendAlert, sendDegraded, sendRecovery } from \"./index\";\n\ndescribe(\"whatsapp Notifications\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n\n  beforeEach(() => {\n    // @ts-expect-error\n    fetchMock = spyOn(global, \"fetch\").mockImplementation(() =>\n      Promise.resolve(new Response(null, { status: 200 })),\n    );\n  });\n\n  afterEach(() => {\n    jest.resetAllMocks();\n  });\n\n  test(\"Send degraded\", async () => {\n    const monitor = {\n      id: \"monitor-1\",\n      name: \"API Health Check\",\n      url: \"https://api.example.com/health\",\n      jobType: \"http\" as const,\n      periodicity: \"5m\" as const,\n      status: \"active\" as const, // or \"down\", \"degraded\"\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      region: \"us-east-1\",\n    };\n\n    const a = {\n      id: 1,\n      name: \"slack Notification\",\n      provider: \"slack\",\n      workspaceId: 1,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      data: '{\"whatsapp\":\"+33623456789\"}',\n    };\n\n    const n = selectNotificationSchema.parse(a);\n    await sendDegraded({\n      // @ts-expect-error\n      monitor,\n      notification: n,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n    expect(fetchMock).toHaveBeenCalled();\n  });\n\n  test(\"Send Recovered\", async () => {\n    const monitor = {\n      id: \"monitor-1\",\n      name: \"API Health Check\",\n      url: \"https://api.example.com/health\",\n      jobType: \"http\" as const,\n      periodicity: \"5m\" as const,\n      status: \"active\" as const, // or \"down\", \"degraded\"\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      region: \"us-east-1\",\n    };\n\n    const a = {\n      id: 1,\n      name: \"slack Notification\",\n      provider: \"slack\",\n      workspaceId: 1,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      data: '{\"whatsapp\":\"+33623456789\"}',\n    };\n\n    const n = selectNotificationSchema.parse(a);\n    await sendRecovery({\n      // @ts-expect-error\n      monitor,\n      notification: n,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n    expect(fetchMock).toHaveBeenCalled();\n  });\n\n  test(\"Send Alert\", async () => {\n    const monitor = {\n      id: \"monitor-1\",\n      name: \"API Health Check\",\n      url: \"https://api.example.com/health\",\n      jobType: \"http\" as const,\n      periodicity: \"5m\" as const,\n      status: \"active\" as const, // or \"down\", \"degraded\"\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      region: \"us-east-1\",\n    };\n    const a = {\n      id: 1,\n      name: \"slack Notification\",\n      provider: \"slack\",\n      workspaceId: 1,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      data: '{\"whatsapp\":\"+33623456789\"}',\n    };\n\n    const n = selectNotificationSchema.parse(a);\n\n    await sendAlert({\n      // @ts-expect-error\n      monitor,\n      notification: n,\n      statusCode: 500,\n      message: \"Something went wrong\",\n      cronTimestamp: Date.now(),\n    });\n    expect(fetchMock).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/notifications/twillio-whatsapp/src/index.ts",
    "content": "import { whatsappDataSchema } from \"@openstatus/db/src/schema\";\nimport type { NotificationContext } from \"@openstatus/notification-base\";\nimport { env } from \"./env\";\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = whatsappDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const contentVariables = JSON.stringify({ url: monitor.url });\n\n  const body = new FormData();\n  body.set(\"To\", `whatsapp:${notificationData.whatsapp}`);\n  body.set(\"From\", \"whatsapp:+14807252613\");\n  body.set(\"ContentSid\", \"HX8282106bfaecb7939e69f9c5564babe5\");\n  body.set(\"ContentVariables\", contentVariables);\n\n  const res = await fetch(\n    `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`,\n    {\n      method: \"post\",\n      body,\n      headers: {\n        Authorization: `Basic ${btoa(\n          `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`,\n        )}`,\n      },\n    },\n  );\n  if (!res.ok) {\n    throw new Error(`Failed to send WhatsApp message: ${res.statusText}`);\n  }\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = whatsappDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const contentVariables = JSON.stringify({ url: monitor.url });\n\n  const body = new FormData();\n  body.set(\"To\", `whatsapp:${notificationData.whatsapp}`);\n  body.set(\"From\", \"whatsapp:+14807252613\");\n  body.set(\"ContentSid\", \"HX8fdeb4201bed18ac8838b3c0135bbf28\");\n  body.set(\"ContentVariables\", contentVariables);\n\n  const res = await fetch(\n    `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`,\n    {\n      method: \"post\",\n      body,\n      headers: {\n        Authorization: `Basic ${btoa(\n          `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`,\n        )}`,\n      },\n    },\n  );\n  if (!res.ok) {\n    throw new Error(`Failed to send SMS: ${res.statusText}`);\n  }\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n}: NotificationContext) => {\n  const notificationData = whatsappDataSchema.parse(\n    JSON.parse(notification.data),\n  );\n  const contentVariables = JSON.stringify({ url: monitor.url });\n\n  const body = new FormData();\n  body.set(\"To\", `whatsapp:${notificationData.whatsapp}`);\n  body.set(\"From\", \"whatsapp:+14807252613\");\n  body.set(\"ContentSid\", \"HX35589f2e7ac8b8be63f4bd62a60e435f\");\n  body.set(\"ContentVariables\", contentVariables);\n\n  const res = await fetch(\n    `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`,\n    {\n      method: \"post\",\n      body,\n      headers: {\n        Authorization: `Basic ${btoa(\n          `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`,\n        )}`,\n      },\n    },\n  );\n  if (!res.ok) {\n    throw new Error(`Failed to send SMS: ${res.statusText}`);\n  }\n};\n\nexport const sendTest = async ({ phoneNumber }: { phoneNumber: string }) => {\n  const contentVariables = JSON.stringify({ url: \"https://openstat.us\" });\n  const body = new FormData();\n  body.set(\"To\", `whatsapp:${phoneNumber}`);\n  body.set(\"ContentSid\", \"HX36ac9074ebda4376c7d6ddd1690b5291\");\n  body.set(\"From\", \"whatsapp:+14807252613\");\n  body.set(\"ContentVariables\", contentVariables);\n\n  console.log(\"send data\");\n  try {\n    await fetch(\n      `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`,\n      {\n        method: \"post\",\n        body,\n        headers: {\n          Authorization: `Basic ${btoa(\n            `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`,\n          )}`,\n        },\n      },\n    );\n  } catch (err) {\n    console.log(err);\n    // Do something\n  }\n};\n"
  },
  {
    "path": "packages/notifications/twillio-whatsapp/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/nextjs.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"bun-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/notifications/webhook/.gitignore",
    "content": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\n\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# Runtime data\n\npids\n_.pid\n_.seed\n\\*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\n\nlib-cov\n\n# Coverage directory used by tools like istanbul\n\ncoverage\n\\*.lcov\n\n# nyc test coverage\n\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n\n.grunt\n\n# Bower dependency directory (https://bower.io/)\n\nbower_components\n\n# node-waf configuration\n\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\n\nbuild/Release\n\n# Dependency directories\n\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\n\nweb_modules/\n\n# TypeScript cache\n\n\\*.tsbuildinfo\n\n# Optional npm cache directory\n\n.npm\n\n# Optional eslint cache\n\n.eslintcache\n\n# Optional stylelint cache\n\n.stylelintcache\n\n# Microbundle cache\n\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n\n.node_repl_history\n\n# Output of 'npm pack'\n\n\\*.tgz\n\n# Yarn Integrity file\n\n.yarn-integrity\n\n# dotenv environment variable files\n\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n\n.cache\n.parcel-cache\n\n# Next.js build output\n\n.next\nout\n\n# Nuxt.js build / generate output\n\n.nuxt\ndist\n\n# Gatsby files\n\n.cache/\n\n# Comment in the public line in if your project uses Gatsby and not Next.js\n\n# https://nextjs.org/blog/next-9-1#public-directory-support\n\n# public\n\n# vuepress build output\n\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n\n.temp\n.cache\n\n# Docusaurus cache and generated files\n\n.docusaurus\n\n# Serverless directories\n\n.serverless/\n\n# FuseBox cache\n\n.fusebox/\n\n# DynamoDB Local files\n\n.dynamodb/\n\n# TernJS port file\n\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.\\*\n"
  },
  {
    "path": "packages/notifications/webhook/README.md",
    "content": "# @openstatus/notification-webhook\nTo install dependencies:\n"
  },
  {
    "path": "packages/notifications/webhook/package.json",
    "content": "{\n  \"name\": \"@openstatus/notification-webhook\",\n  \"version\": \"1.0.0\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"tsc\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/notification-base\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"24.0.8\",\n    \"bun-types\": \"1.3.1\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/notifications/webhook/src/index.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { sendTest } from \"./index\";\n\ndescribe(\"Webhook sendTest\", () => {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  let fetchMock: any = undefined;\n\n  beforeEach(() => {\n    fetchMock = spyOn(global, \"fetch\");\n  });\n\n  afterEach(() => {\n    if (fetchMock) {\n      fetchMock.mockClear();\n      fetchMock.mockRestore();\n    }\n  });\n\n  test(\"should send test webhook successfully\", async () => {\n    const url = \"https://example.com/webhook\";\n    fetchMock.mockResolvedValue(new Response(null, { status: 200 }));\n\n    const result = await sendTest({ url });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    expect(fetchMock).toHaveBeenCalledWith(\n      url,\n      expect.objectContaining({\n        method: \"post\",\n        body: expect.any(String),\n        headers: { \"Content-Type\": \"application/json\" },\n      }),\n    );\n\n    const callArgs = fetchMock.mock.calls[0];\n    const body = JSON.parse(callArgs[1].body);\n    expect(body).toMatchObject({\n      monitor: {\n        id: 1,\n        name: \"test\",\n        url: \"http://openstat.us\",\n      },\n      status: \"recovered\",\n      statusCode: 200,\n      latency: 1337,\n    });\n    expect(body.cronTimestamp).toBeTypeOf(\"number\");\n  });\n\n  test(\"should send test webhook with headers\", async () => {\n    const url = \"https://example.com/webhook\";\n    const headers = [\n      { key: \"Authorization\", value: \"Bearer token123\" },\n      { key: \"Content-Type\", value: \"application/json\" },\n    ];\n    fetchMock.mockResolvedValue(new Response(null, { status: 200 }));\n\n    const result = await sendTest({ url, headers });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    expect(fetchMock).toHaveBeenCalledWith(\n      url,\n      expect.objectContaining({\n        method: \"post\",\n        body: expect.any(String),\n        headers: {\n          Authorization: \"Bearer token123\",\n          \"Content-Type\": \"application/json\",\n        },\n      }),\n    );\n  });\n\n  test(\"should throw error when response is not ok\", async () => {\n    const url = \"https://example.com/webhook\";\n    fetchMock.mockResolvedValue(new Response(null, { status: 400 }));\n\n    await expect(sendTest({ url })).rejects.toThrow(\"Failed to send test\");\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"should throw error when fetch fails\", async () => {\n    const url = \"https://example.com/webhook\";\n    const networkError = new Error(\"Network error\");\n    fetchMock.mockRejectedValue(networkError);\n\n    await expect(sendTest({ url })).rejects.toThrow(\"Failed to send test\");\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"should send test webhook with empty headers array\", async () => {\n    const url = \"https://example.com/webhook\";\n    const headers: { key: string; value: string }[] = [];\n    fetchMock.mockResolvedValue(new Response(null, { status: 200 }));\n\n    const result = await sendTest({ url, headers });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const callArgs = fetchMock.mock.calls[0];\n    // Empty headers array should result in empty object after transformHeaders\n    expect(callArgs[1].headers).toEqual({});\n  });\n\n  test(\"should send test webhook with 500 status code\", async () => {\n    const url = \"https://example.com/webhook\";\n    fetchMock.mockResolvedValue(new Response(null, { status: 500 }));\n\n    await expect(sendTest({ url })).rejects.toThrow(\"Failed to send test\");\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"should send test webhook with 201 status code (success)\", async () => {\n    const url = \"https://example.com/webhook\";\n    fetchMock.mockResolvedValue(new Response(null, { status: 201 }));\n\n    const result = await sendTest({ url });\n\n    expect(result).toBe(true);\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/notifications/webhook/src/index.ts",
    "content": "import type { NotificationContext } from \"@openstatus/notification-base\";\nimport { transformHeaders } from \"@openstatus/utils\";\nimport { PayloadSchema, WebhookSchema } from \"./schema\";\n\nexport const sendAlert = async ({\n  monitor,\n  notification,\n  cronTimestamp,\n  statusCode,\n  latency,\n  message,\n}: NotificationContext) => {\n  const notificationData = WebhookSchema.parse(JSON.parse(notification.data));\n\n  const body = PayloadSchema.parse({\n    monitor: monitor,\n    cronTimestamp,\n    status: \"error\",\n    statusCode,\n    latency,\n    errorMessage: message,\n  });\n\n  const res = await fetch(notificationData.webhook.endpoint, {\n    method: \"post\",\n    body: JSON.stringify(body),\n    headers: notificationData.webhook.headers\n      ? transformHeaders(notificationData.webhook.headers)\n      : {\n          \"Content-Type\": \"application/json\",\n        },\n  });\n  if (!res.ok) {\n    throw new Error(`Failed to send webhook notification: ${res.statusText}`);\n  }\n};\n\nexport const sendRecovery = async ({\n  monitor,\n  notification,\n  cronTimestamp,\n  latency,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const notificationData = WebhookSchema.parse(JSON.parse(notification.data));\n\n  const body = PayloadSchema.parse({\n    monitor: monitor,\n    cronTimestamp,\n    status: \"recovered\",\n    statusCode,\n    latency,\n    errorMessage: message,\n  });\n  const url = notificationData.webhook.endpoint;\n  const res = await fetch(url, {\n    method: \"post\",\n    body: JSON.stringify(body),\n    headers: notificationData.webhook.headers\n      ? transformHeaders(notificationData.webhook.headers)\n      : {\n          \"Content-Type\": \"application/json\",\n        },\n  });\n  if (!res.ok) {\n    throw new Error(`Failed to send webhook notification: ${res.statusText}`);\n  }\n};\n\nexport const sendDegraded = async ({\n  monitor,\n  notification,\n  cronTimestamp,\n  latency,\n  statusCode,\n  message,\n}: NotificationContext) => {\n  const notificationData = WebhookSchema.parse(JSON.parse(notification.data));\n\n  const body = PayloadSchema.parse({\n    monitor: monitor,\n    cronTimestamp,\n    status: \"degraded\",\n    statusCode,\n    latency,\n    errorMessage: message,\n  });\n\n  const res = await fetch(notificationData.webhook.endpoint, {\n    method: \"post\",\n    body: JSON.stringify(body),\n    headers: notificationData.webhook.headers\n      ? transformHeaders(notificationData.webhook.headers)\n      : {\n          \"Content-Type\": \"application/json\",\n        },\n  });\n  if (!res.ok) {\n    throw new Error(`Failed to send webhook notification: ${res.statusText}`);\n  }\n};\n\nexport const sendTest = async ({\n  url,\n  headers,\n}: {\n  url: string;\n  headers?: { key: string; value: string }[];\n}) => {\n  const body = PayloadSchema.parse({\n    monitor: {\n      id: 1,\n      name: \"test\",\n      url: \"http://openstat.us\",\n    },\n    cronTimestamp: Date.now(),\n    status: \"recovered\",\n    statusCode: 200,\n    latency: 1337,\n  });\n  try {\n    const response = await fetch(url, {\n      method: \"post\",\n      body: JSON.stringify(body),\n      headers: headers\n        ? transformHeaders(headers)\n        : {\n            \"Content-Type\": \"application/json\",\n          },\n    });\n    if (!response.ok) {\n      throw new Error(\"Failed to send test\");\n    }\n    return true;\n  } catch (err) {\n    console.log(err);\n    throw new Error(\"Failed to send test\");\n  }\n};\n"
  },
  {
    "path": "packages/notifications/webhook/src/schema.ts",
    "content": "import { webhookDataSchema } from \"@openstatus/db/src/schema\";\nimport { z } from \"zod\";\n\nexport const WebhookSchema = webhookDataSchema;\n\nexport const PayloadSchema = z.object({\n  monitor: z.object({\n    id: z.number(),\n    name: z.string(),\n    url: z.string(),\n  }),\n  cronTimestamp: z.number(),\n  status: z.enum([\"degraded\", \"error\", \"recovered\"]),\n  statusCode: z.number().optional(),\n  latency: z.number().optional(),\n  errorMessage: z.string().optional(),\n});\n"
  },
  {
    "path": "packages/notifications/webhook/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"exclude\": [\"**/*.test.ts\", \"**/*.spec.ts\"]\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/health/v1/health.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.health.v1;\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/health/v1;healthv1\";\n\n// HealthService provides health check endpoints for load balancer probes.\nservice HealthService {\n  // Check returns the current serving status of the service.\n  rpc Check(CheckRequest) returns (CheckResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n}\n\n// CheckRequest is the request message for health checks.\nmessage CheckRequest {\n  // Optional service name to check. If empty, checks overall service health.\n  string service = 1;\n}\n\n// CheckResponse is the response message for health checks.\nmessage CheckResponse {\n  // ServingStatus represents the health status of the service.\n  enum ServingStatus {\n    // SERVING_STATUS_UNSPECIFIED indicates an unknown status.\n    SERVING_STATUS_UNSPECIFIED = 0;\n    // SERVING_STATUS_SERVING indicates the service is healthy and serving.\n    SERVING_STATUS_SERVING = 1;\n    // SERVING_STATUS_NOT_SERVING indicates the service is not healthy.\n    SERVING_STATUS_NOT_SERVING = 2;\n  }\n\n  // The serving status of the service.\n  ServingStatus status = 1;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/maintenance/v1/maintenance.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.maintenance.v1;\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/maintenance/v1;maintenancev1\";\n\n// MaintenanceSummary represents metadata for a maintenance window (used in list responses).\nmessage MaintenanceSummary {\n  // Unique identifier for the maintenance.\n  string id = 1;\n\n  // Title of the maintenance.\n  string title = 2;\n\n  // Message describing the maintenance.\n  string message = 3;\n\n  // Start time of the maintenance window (RFC 3339 format).\n  string from = 4;\n\n  // End time of the maintenance window (RFC 3339 format).\n  string to = 5;\n\n  // ID of the page this maintenance is associated with.\n  string page_id = 6;\n\n  // IDs of affected page components.\n  repeated string page_component_ids = 7;\n\n  // Timestamp when the maintenance was created (RFC 3339 format).\n  string created_at = 8;\n\n  // Timestamp when the maintenance was last updated (RFC 3339 format).\n  string updated_at = 9;\n}\n\n// Maintenance represents a maintenance window with full details.\nmessage Maintenance {\n  // Unique identifier for the maintenance.\n  string id = 1;\n\n  // Title of the maintenance.\n  string title = 2;\n\n  // Message describing the maintenance.\n  string message = 3;\n\n  // Start time of the maintenance window (RFC 3339 format).\n  string from = 4;\n\n  // End time of the maintenance window (RFC 3339 format).\n  string to = 5;\n\n  // ID of the page this maintenance is associated with.\n  string page_id = 6;\n\n  // IDs of affected page components.\n  repeated string page_component_ids = 7;\n\n  // Timestamp when the maintenance was created (RFC 3339 format).\n  string created_at = 8;\n\n  // Timestamp when the maintenance was last updated (RFC 3339 format).\n  string updated_at = 9;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/maintenance/v1/service.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.maintenance.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"gnostic/openapi/v3/annotations.proto\";\nimport \"openstatus/maintenance/v1/maintenance.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/maintenance/v1;maintenancev1\";\n\n// MaintenanceService provides CRUD operations for maintenance windows.\nservice MaintenanceService {\n  // CreateMaintenance creates a new maintenance window.\n  rpc CreateMaintenance(CreateMaintenanceRequest) returns (CreateMaintenanceResponse);\n\n  // GetMaintenance retrieves a specific maintenance window by ID.\n  rpc GetMaintenance(GetMaintenanceRequest) returns (GetMaintenanceResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // ListMaintenances returns all maintenance windows for the workspace.\n  rpc ListMaintenances(ListMaintenancesRequest) returns (ListMaintenancesResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // UpdateMaintenance updates a maintenance window.\n  rpc UpdateMaintenance(UpdateMaintenanceRequest) returns (UpdateMaintenanceResponse);\n\n  // DeleteMaintenance removes a maintenance window.\n  rpc DeleteMaintenance(DeleteMaintenanceRequest) returns (DeleteMaintenanceResponse);\n}\n\n// CreateMaintenanceRequest is the request to create a new maintenance window.\nmessage CreateMaintenanceRequest {\n  // Title of the maintenance (required, 1-256 characters).\n  string title = 1 [\n    (buf.validate.field).string = {\n      min_len: 1\n      max_len: 256\n    },\n    (gnostic.openapi.v3.property) = {example: {yaml: \"Database Migration\"}}\n  ];\n\n  // Message describing the maintenance (required).\n  string message = 2 [(buf.validate.field).string.min_len = 1];\n\n  // Start time of the maintenance window (RFC 3339 format, required).\n  string from = 3 [\n    (buf.validate.field).string.pattern = \"^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d{1,9})?(Z|[+-]\\\\d{2}:\\\\d{2})$\",\n    (gnostic.openapi.v3.property) = {example: {yaml: \"2024-03-01T02:00:00Z\"}}\n  ];\n\n  // End time of the maintenance window (RFC 3339 format, required).\n  string to = 4 [\n    (buf.validate.field).string.pattern = \"^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d{1,9})?(Z|[+-]\\\\d{2}:\\\\d{2})$\",\n    (gnostic.openapi.v3.property) = {example: {yaml: \"2024-03-01T06:00:00Z\"}}\n  ];\n\n  // Page ID to associate with this maintenance (required).\n  string page_id = 5 [(buf.validate.field).string.min_len = 1];\n\n  // Page component IDs to associate with this maintenance (optional).\n  repeated string page_component_ids = 6;\n\n  // Whether to notify subscribers about this maintenance (optional, defaults to false).\n  optional bool notify = 7;\n}\n\n// CreateMaintenanceResponse is the response after creating a maintenance window.\nmessage CreateMaintenanceResponse {\n  // The created maintenance.\n  Maintenance maintenance = 1;\n}\n\n// GetMaintenanceRequest is the request to get a maintenance window by ID.\nmessage GetMaintenanceRequest {\n  // ID of the maintenance to retrieve (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// GetMaintenanceResponse is the response containing the maintenance window.\nmessage GetMaintenanceResponse {\n  // The requested maintenance.\n  Maintenance maintenance = 1;\n}\n\n// ListMaintenancesRequest is the request to list maintenance windows.\nmessage ListMaintenancesRequest {\n  // Maximum number of maintenances to return (1-100, defaults to 50).\n  optional int32 limit = 1 [(buf.validate.field).int32 = {\n    gte: 1\n    lte: 100\n  }];\n\n  // Number of maintenances to skip for pagination (defaults to 0).\n  optional int32 offset = 2 [(buf.validate.field).int32.gte = 0];\n\n  // Filter by page ID (optional).\n  optional string page_id = 3;\n}\n\n// ListMaintenancesResponse is the response containing maintenance window summaries.\nmessage ListMaintenancesResponse {\n  // List of maintenances.\n  repeated MaintenanceSummary maintenances = 1;\n\n  // Total number of maintenances matching the filter.\n  int32 total_size = 2;\n}\n\n// UpdateMaintenanceRequest is the request to update a maintenance window.\nmessage UpdateMaintenanceRequest {\n  // ID of the maintenance to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // New title for the maintenance (optional).\n  optional string title = 2 [(buf.validate.field).string = {\n    min_len: 1\n    max_len: 256\n  }];\n\n  // New message for the maintenance (optional).\n  optional string message = 3;\n\n  // New start time (RFC 3339 format, optional).\n  optional string from = 4 [(buf.validate.field).string.pattern = \"^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d{1,9})?(Z|[+-]\\\\d{2}:\\\\d{2})$\"];\n\n  // New end time (RFC 3339 format, optional).\n  optional string to = 5 [(buf.validate.field).string.pattern = \"^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d{1,9})?(Z|[+-]\\\\d{2}:\\\\d{2})$\"];\n\n  // New page ID (optional).\n  optional string page_id = 6;\n\n  // New list of page component IDs (optional, replaces existing list).\n  repeated string page_component_ids = 7;\n}\n\n// UpdateMaintenanceResponse is the response after updating a maintenance window.\nmessage UpdateMaintenanceResponse {\n  // The updated maintenance.\n  Maintenance maintenance = 1;\n}\n\n// DeleteMaintenanceRequest is the request to delete a maintenance window.\nmessage DeleteMaintenanceRequest {\n  // ID of the maintenance to delete (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// DeleteMaintenanceResponse is the response after deleting a maintenance window.\nmessage DeleteMaintenanceResponse {\n  // Whether the deletion was successful.\n  bool success = 1;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/monitor/v1/assertions.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.monitor.v1;\n\nimport \"buf/validate/validate.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/monitor/v1;monitorv1\";\n\n// NumberComparator defines comparison operations for numeric values.\nenum NumberComparator {\n  NUMBER_COMPARATOR_UNSPECIFIED = 0;\n  NUMBER_COMPARATOR_EQUAL = 1;\n  NUMBER_COMPARATOR_NOT_EQUAL = 2;\n  NUMBER_COMPARATOR_GREATER_THAN = 3;\n  NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL = 4;\n  NUMBER_COMPARATOR_LESS_THAN = 5;\n  NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL = 6;\n}\n\n// StringComparator defines comparison operations for string values.\nenum StringComparator {\n  STRING_COMPARATOR_UNSPECIFIED = 0;\n  STRING_COMPARATOR_CONTAINS = 1;\n  STRING_COMPARATOR_NOT_CONTAINS = 2;\n  STRING_COMPARATOR_EQUAL = 3;\n  STRING_COMPARATOR_NOT_EQUAL = 4;\n  STRING_COMPARATOR_EMPTY = 5;\n  STRING_COMPARATOR_NOT_EMPTY = 6;\n  STRING_COMPARATOR_GREATER_THAN = 7;\n  STRING_COMPARATOR_GREATER_THAN_OR_EQUAL = 8;\n  STRING_COMPARATOR_LESS_THAN = 9;\n  STRING_COMPARATOR_LESS_THAN_OR_EQUAL = 10;\n}\n\n// RecordComparator defines comparison operations for DNS records.\nenum RecordComparator {\n  RECORD_COMPARATOR_UNSPECIFIED = 0;\n  RECORD_COMPARATOR_EQUAL = 1;\n  RECORD_COMPARATOR_NOT_EQUAL = 2;\n  RECORD_COMPARATOR_CONTAINS = 3;\n  RECORD_COMPARATOR_NOT_CONTAINS = 4;\n}\n\n// StatusCodeAssertion defines an assertion for HTTP status codes.\nmessage StatusCodeAssertion {\n  // Target status code to compare against (100-599).\n  int64 target = 1 [(buf.validate.field).int64 = {\n    gte: 100\n    lte: 599\n  }];\n\n  // Comparison operation (required, must not be UNSPECIFIED).\n  NumberComparator comparator = 2 [(buf.validate.field).enum = {\n    not_in: [0]\n  }];\n}\n\n// BodyAssertion defines an assertion for response body content.\nmessage BodyAssertion {\n  // Target value to compare against.\n  string target = 1;\n\n  // Comparison operation (required, must not be UNSPECIFIED).\n  StringComparator comparator = 2 [(buf.validate.field).enum = {\n    not_in: [0]\n  }];\n}\n\n// HeaderAssertion defines an assertion for response headers.\nmessage HeaderAssertion {\n  // Target value to compare against.\n  string target = 1;\n\n  // Comparison operation (required, must not be UNSPECIFIED).\n  StringComparator comparator = 2 [(buf.validate.field).enum = {\n    not_in: [0]\n  }];\n\n  // Header key to check (required).\n  string key = 3 [(buf.validate.field).string.min_len = 1];\n}\n\n// RecordAssertion defines an assertion for DNS records.\nmessage RecordAssertion {\n  // DNS record type (e.g., \"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\").\n  string record = 1 [(buf.validate.field).string = {\n    in: [\"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\"]\n  }];\n\n  // Comparison operation (required, must not be UNSPECIFIED).\n  RecordComparator comparator = 2 [(buf.validate.field).enum = {\n    not_in: [0]\n  }];\n\n  // Target value to compare against.\n  string target = 3;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/monitor/v1/dns_monitor.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.monitor.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"gnostic/openapi/v3/annotations.proto\";\nimport \"openstatus/monitor/v1/assertions.proto\";\nimport \"openstatus/monitor/v1/http_monitor.proto\";\nimport \"openstatus/monitor/v1/monitor.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/monitor/v1;monitorv1\";\n\n// DNSMonitor defines the configuration for a DNS monitor.\nmessage DNSMonitor {\n  // Unique identifier for the monitor (output only for create requests).\n  string id = 1;\n\n  // Name of the monitor (required, max 256 characters).\n  string name = 2 [\n    (buf.validate.field).string = {\n      min_len: 1\n      max_len: 256\n    },\n    (gnostic.openapi.v3.property) = {example: {yaml: \"DNS Resolution Check\"}}\n  ];\n\n  // Domain to resolve (required, max 2048 characters).\n  string uri = 3 [\n    (buf.validate.field).string = {\n      min_len: 1\n      max_len: 2048\n    },\n    (gnostic.openapi.v3.property) = {example: {yaml: \"example.com\"}}\n  ];\n\n  // Check periodicity (required).\n  Periodicity periodicity = 4 [(buf.validate.field).enum = {\n    not_in: [0]\n  }];\n\n  // Timeout in milliseconds (0-120000, defaults to 45000).\n  int64 timeout = 5 [(buf.validate.field).int64 = {\n    gte: 0\n    lte: 120000\n  }];\n\n  // Latency threshold for degraded status in milliseconds (optional, 0-120000).\n  optional int64 degraded_at = 6 [(buf.validate.field).int64 = {\n    gte: 0\n    lte: 120000\n  }];\n\n  // Number of retry attempts (0-10, defaults to 3).\n  int64 retry = 7 [(buf.validate.field).int64 = {\n    gte: 0\n    lte: 10\n  }];\n\n  // DNS record assertions for validation.\n  repeated RecordAssertion record_assertions = 8 [(buf.validate.field).repeated.max_items = 10];\n\n  // Description of the monitor (optional).\n  string description = 9 [(buf.validate.field).string.max_len = 1024];\n\n  // Whether the monitor is active (defaults to false).\n  bool active = 10;\n\n  // Whether the monitor is publicly visible (defaults to false).\n  bool public = 11;\n\n  // Geographic regions to run checks from.\n  repeated Region regions = 12 [(buf.validate.field).repeated = {\n    max_items: 28\n    items: {\n      enum: {\n        not_in: [0]\n      }\n    }\n  }];\n\n  // OpenTelemetry configuration for exporting metrics.\n  OpenTelemetryConfig open_telemetry = 13;\n\n  // Current operational status of the monitor.\n  MonitorStatus status = 14;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/monitor/v1/http_monitor.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.monitor.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"gnostic/openapi/v3/annotations.proto\";\nimport \"openstatus/monitor/v1/assertions.proto\";\nimport \"openstatus/monitor/v1/monitor.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/monitor/v1;monitorv1\";\n\n// HTTP methods supported for monitors.\nenum HTTPMethod {\n  HTTP_METHOD_UNSPECIFIED = 0;\n  HTTP_METHOD_GET = 1;\n  HTTP_METHOD_POST = 2;\n  HTTP_METHOD_HEAD = 3;\n  HTTP_METHOD_PUT = 4;\n  HTTP_METHOD_PATCH = 5;\n  HTTP_METHOD_DELETE = 6;\n  HTTP_METHOD_TRACE = 7;\n  HTTP_METHOD_CONNECT = 8;\n  HTTP_METHOD_OPTIONS = 9;\n}\n\n// Headers represents a key-value pair for HTTP headers.\nmessage Headers {\n  // Header name.\n  string key = 1 [\n    (buf.validate.field).string.min_len = 1,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"Authorization\"}}\n  ];\n  // Header value.\n  string value = 2 [\n    (gnostic.openapi.v3.property) = {example: {yaml: \"Bearer token123\"}}\n  ];\n}\n\n// OpenTelemetry configuration for exporting metrics.\nmessage OpenTelemetryConfig {\n  // OTEL endpoint URL.\n  string endpoint = 1 [(buf.validate.field).string.max_len = 2048];\n\n  // Custom headers for OTEL requests.\n  repeated Headers headers = 2 [(buf.validate.field).repeated.max_items = 20];\n}\n\n// HTTPMonitor defines the configuration for an HTTP monitor.\nmessage HTTPMonitor {\n  // Unique identifier for the monitor (output only for create requests).\n  string id = 1;\n\n  // Name of the monitor (required, max 256 characters).\n  string name = 2 [\n    (buf.validate.field).string = {\n      min_len: 1\n      max_len: 256\n    },\n    (gnostic.openapi.v3.property) = {example: {yaml: \"Production API Health Check\"}}\n  ];\n\n  // URL to monitor (required, max 2048 characters).\n  string url = 3 [\n    (buf.validate.field).string = {\n      min_len: 1\n      max_len: 2048\n      uri: true\n    },\n    (gnostic.openapi.v3.property) = {example: {yaml: \"https://api.example.com/health\"}}\n  ];\n\n  // Check periodicity (required).\n  Periodicity periodicity = 4 [(buf.validate.field).enum = {\n    not_in: [0]\n  }];\n\n  // HTTP method to use (defaults to GET).\n  HTTPMethod method = 5 [(buf.validate.field).enum = {\n      not_in: [0]\n  }];\n\n  // Request body (optional).\n  string body = 6 [\n    (gnostic.openapi.v3.property) = {example: {yaml: \"{\\\"key\\\": \\\"value\\\"}\"}}\n  ];\n\n  // Timeout in milliseconds (0-120000, defaults to 45000).\n  int64 timeout = 7 [(buf.validate.field).int64 = {\n    gte: 0\n    lte: 120000\n  }];\n\n  // Latency threshold for degraded status in milliseconds (optional, 0-120000).\n  optional int64 degraded_at = 8 [(buf.validate.field).int64 = {\n    gte: 0\n    lte: 120000\n  }];\n\n  // Number of retry attempts (0-10, defaults to 3).\n  int64 retry = 9 [(buf.validate.field).int64 = {\n    gte: 0\n    lte: 10\n  }];\n\n  // Whether to follow HTTP redirects (defaults to true when not specified).\n  optional bool follow_redirects = 10;\n\n  // Custom headers for the request.\n  repeated Headers headers = 11 [(buf.validate.field).repeated.max_items = 20];\n\n  // Status code assertions for the response.\n  repeated StatusCodeAssertion status_code_assertions = 12 [(buf.validate.field).repeated.max_items = 10];\n\n  // Body content assertions for the response.\n  repeated BodyAssertion body_assertions = 13 [(buf.validate.field).repeated.max_items = 10];\n\n  // Header assertions for the response.\n  repeated HeaderAssertion header_assertions = 14 [(buf.validate.field).repeated.max_items = 10];\n\n  // Description of the monitor (optional).\n  string description = 15 [(buf.validate.field).string.max_len = 1024];\n\n  // Whether the monitor is active (defaults to false).\n  bool active = 16;\n\n  // Whether the monitor is publicly visible (defaults to false).\n  bool public = 17;\n\n  // Geographic regions to run checks from.\n  repeated Region regions = 18 [(buf.validate.field).repeated = {\n    max_items: 28\n    items: {\n      enum: {\n        not_in: [0]\n      }\n    }\n  }];\n\n  // OpenTelemetry configuration for exporting metrics.\n  OpenTelemetryConfig open_telemetry = 19;\n\n  // Current operational status of the monitor.\n  MonitorStatus status = 20;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/monitor/v1/monitor.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.monitor.v1;\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/monitor/v1;monitorv1\";\n\n// MonitorStatus represents the operational status of a monitor.\nenum MonitorStatus {\n  // MONITOR_STATUS_UNSPECIFIED indicates an unknown status.\n  MONITOR_STATUS_UNSPECIFIED = 0;\n  // MONITOR_STATUS_ACTIVE indicates the monitor is actively checking.\n  MONITOR_STATUS_ACTIVE = 1;\n  // MONITOR_STATUS_DEGRADED indicates the monitor is degraded.\n  MONITOR_STATUS_DEGRADED = 2;\n  // MONITOR_STATUS_ERROR indicates the monitor is in an error state.\n  MONITOR_STATUS_ERROR = 3;\n}\n\n// Monitor periodicity options.\nenum Periodicity {\n  PERIODICITY_UNSPECIFIED = 0;\n  PERIODICITY_30S = 1;\n  PERIODICITY_1M = 2;\n  PERIODICITY_5M = 3;\n  PERIODICITY_10M = 4;\n  PERIODICITY_30M = 5;\n  PERIODICITY_1H = 6;\n}\n\n// Geographic regions where monitors can run checks from.\nenum Region {\n  REGION_UNSPECIFIED = 0;\n  // Fly.io regions\n  REGION_FLY_AMS = 1;   // Amsterdam, Netherlands\n  REGION_FLY_ARN = 2;   // Stockholm, Sweden\n  REGION_FLY_BOM = 3;   // Mumbai, India\n  REGION_FLY_CDG = 4;   // Paris, France\n  REGION_FLY_DFW = 5;   // Dallas, USA\n  REGION_FLY_EWR = 6;   // Newark, USA\n  REGION_FLY_FRA = 7;   // Frankfurt, Germany\n  REGION_FLY_GRU = 8;   // São Paulo, Brazil\n  REGION_FLY_IAD = 9;   // Ashburn, USA\n  REGION_FLY_JNB = 10;  // Johannesburg, South Africa\n  REGION_FLY_LAX = 11;  // Los Angeles, USA\n  REGION_FLY_LHR = 12;  // London, UK\n  REGION_FLY_NRT = 13;  // Tokyo, Japan\n  REGION_FLY_ORD = 14;  // Chicago, USA\n  REGION_FLY_SJC = 15;  // San Jose, USA\n  REGION_FLY_SIN = 16;  // Singapore\n  REGION_FLY_SYD = 17;  // Sydney, Australia\n  REGION_FLY_YYZ = 18;  // Toronto, Canada\n  // Koyeb regions\n  REGION_KOYEB_FRA = 19;  // Koyeb Frankfurt\n  REGION_KOYEB_PAR = 20;  // Koyeb Paris\n  REGION_KOYEB_SFO = 21;  // Koyeb San Francisco\n  REGION_KOYEB_SIN = 22;  // Koyeb Singapore\n  REGION_KOYEB_TYO = 23;  // Koyeb Tokyo\n  REGION_KOYEB_WAS = 24;  // Koyeb Washington\n  // Railway regions\n  REGION_RAILWAY_US_WEST2 = 25;           // Railway US West\n  REGION_RAILWAY_US_EAST4 = 26;           // Railway US East\n  REGION_RAILWAY_EUROPE_WEST4 = 27;       // Railway Europe West\n  REGION_RAILWAY_ASIA_SOUTHEAST1 = 28;    // Railway Asia Southeast\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/monitor/v1/service.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.monitor.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"gnostic/openapi/v3/annotations.proto\";\nimport \"openstatus/monitor/v1/dns_monitor.proto\";\nimport \"openstatus/monitor/v1/http_monitor.proto\";\nimport \"openstatus/monitor/v1/monitor.proto\";\nimport \"openstatus/monitor/v1/tcp_monitor.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/monitor/v1;monitorv1\";\n\n// TimeRange represents the time period for metrics aggregation.\nenum TimeRange {\n  // Unspecified time range.\n  TIME_RANGE_UNSPECIFIED = 0;\n  // Last 24 hours.\n  TIME_RANGE_1D = 1;\n  // Last 7 days.\n  TIME_RANGE_7D = 2;\n  // Last 14 days.\n  TIME_RANGE_14D = 3;\n}\n\n// MonitorService provides CRUD and operational commands for monitors.\nservice MonitorService {\n  // CreateHTTPMonitor creates a new HTTP monitor with URL, method, headers, assertions, and optional OpenTelemetry configuration.\n  rpc CreateHTTPMonitor(CreateHTTPMonitorRequest) returns (CreateHTTPMonitorResponse) {\n    option (gnostic.openapi.v3.operation) = {\n      description: \"Creates a new HTTP monitor in the authenticated workspace. Configure the target URL, HTTP method, request headers and body, response assertions (status code, body content, headers), check periodicity, geographic regions, and optional OpenTelemetry export. The monitor starts checking immediately if set to active.\"\n    };\n  }\n\n  // CreateTCPMonitor creates a new TCP monitor.\n  rpc CreateTCPMonitor(CreateTCPMonitorRequest) returns (CreateTCPMonitorResponse);\n\n  // CreateDNSMonitor creates a new DNS monitor.\n  rpc CreateDNSMonitor(CreateDNSMonitorRequest) returns (CreateDNSMonitorResponse);\n\n  // UpdateHTTPMonitor updates an existing HTTP monitor.\n  rpc UpdateHTTPMonitor(UpdateHTTPMonitorRequest) returns (UpdateHTTPMonitorResponse);\n\n  // UpdateTCPMonitor updates an existing TCP monitor.\n  rpc UpdateTCPMonitor(UpdateTCPMonitorRequest) returns (UpdateTCPMonitorResponse);\n\n  // UpdateDNSMonitor updates an existing DNS monitor.\n  rpc UpdateDNSMonitor(UpdateDNSMonitorRequest) returns (UpdateDNSMonitorResponse);\n\n  // TriggerMonitor initiates an immediate check for a monitor across all configured regions.\n  rpc TriggerMonitor(TriggerMonitorRequest) returns (TriggerMonitorResponse) {\n    option (gnostic.openapi.v3.operation) = {\n      description: \"Manually triggers an immediate check for the specified monitor across all configured regions. This operation is rate-limited under the synthetic-checks quota. A monitor run record is created and the check is dispatched to the checker service.\"\n    };\n  }\n\n  // DeleteMonitor removes a monitor.\n  rpc DeleteMonitor(DeleteMonitorRequest) returns (DeleteMonitorResponse);\n\n  // ListMonitors returns a paginated list of all monitors in the workspace.\n  rpc ListMonitors(ListMonitorsRequest) returns (ListMonitorsResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // GetMonitorStatus returns the current status of all regions for a monitor.\n  rpc GetMonitorStatus(GetMonitorStatusRequest) returns (GetMonitorStatusResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // GetMonitorSummary returns aggregated metrics and statistics for a monitor over a configurable time range.\n  rpc GetMonitorSummary(GetMonitorSummaryRequest) returns (GetMonitorSummaryResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n    option (gnostic.openapi.v3.operation) = {\n      description: \"Returns aggregated metrics for a monitor including latency percentiles (p50, p75, p90, p95, p99), request counts by status (successful, degraded, failed), and the timestamp of the last check. Metrics can be scoped to a time range (1 day, 7 days, or 14 days) and filtered by specific regions.\"\n    };\n  }\n\n  // GetMonitor returns a single monitor by ID within the authenticated workspace.\n  // Returns the monitor configuration (HTTP, TCP, or DNS) using the MonitorConfig oneof type.\n  rpc GetMonitor(GetMonitorRequest) returns (GetMonitorResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n}\n\n// CreateHTTPMonitorRequest is the request to create a new HTTP monitor.\nmessage CreateHTTPMonitorRequest {\n  // Monitor configuration (required).\n  HTTPMonitor monitor = 1 [(buf.validate.field).required = true];\n}\n\n// CreateHTTPMonitorResponse is the response after creating an HTTP monitor.\nmessage CreateHTTPMonitorResponse {\n  // The created monitor with assigned ID.\n  HTTPMonitor monitor = 1;\n}\n\n// CreateTCPMonitorRequest is the request to create a new TCP monitor.\nmessage CreateTCPMonitorRequest {\n  // Monitor configuration (required).\n  TCPMonitor monitor = 1 [(buf.validate.field).required = true];\n}\n\n// CreateTCPMonitorResponse is the response after creating a TCP monitor.\nmessage CreateTCPMonitorResponse {\n  // The created monitor with assigned ID.\n  TCPMonitor monitor = 1;\n}\n\n// CreateDNSMonitorRequest is the request to create a new DNS monitor.\nmessage CreateDNSMonitorRequest {\n  // Monitor configuration (required).\n  DNSMonitor monitor = 1 [(buf.validate.field).required = true];\n}\n\n// CreateDNSMonitorResponse is the response after creating a DNS monitor.\nmessage CreateDNSMonitorResponse {\n  // The created monitor with assigned ID.\n  DNSMonitor monitor = 1;\n}\n\n// UpdateHTTPMonitorRequest is the request to update an existing HTTP monitor.\nmessage UpdateHTTPMonitorRequest {\n  // Monitor ID to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Updated monitor configuration (all fields optional for partial updates).\n  optional HTTPMonitor monitor = 2;\n}\n\n// UpdateHTTPMonitorResponse is the response after updating an HTTP monitor.\nmessage UpdateHTTPMonitorResponse {\n  // The updated monitor.\n  HTTPMonitor monitor = 1;\n}\n\n// UpdateTCPMonitorRequest is the request to update an existing TCP monitor.\nmessage UpdateTCPMonitorRequest {\n  // Monitor ID to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Updated monitor configuration (all fields optional for partial updates).\n  optional TCPMonitor monitor = 2;\n}\n\n// UpdateTCPMonitorResponse is the response after updating a TCP monitor.\nmessage UpdateTCPMonitorResponse {\n  // The updated monitor.\n  TCPMonitor monitor = 1;\n}\n\n// UpdateDNSMonitorRequest is the request to update an existing DNS monitor.\nmessage UpdateDNSMonitorRequest {\n  // Monitor ID to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Updated monitor configuration (all fields optional for partial updates).\n  optional DNSMonitor monitor = 2;\n}\n\n// UpdateDNSMonitorResponse is the response after updating a DNS monitor.\nmessage UpdateDNSMonitorResponse {\n  // The updated monitor.\n  DNSMonitor monitor = 1;\n}\n\n// TriggerMonitorRequest is the request to trigger a monitor check.\nmessage TriggerMonitorRequest {\n  // Monitor ID to trigger (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// TriggerMonitorResponse is the response after triggering a monitor.\nmessage TriggerMonitorResponse {\n  // Whether the trigger was successful.\n  bool success = 1;\n}\n\n// DeleteMonitorRequest is the request to delete a monitor.\nmessage DeleteMonitorRequest {\n  // Monitor ID to delete (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// DeleteMonitorResponse is the response after deleting a monitor.\nmessage DeleteMonitorResponse {\n  // Whether the deletion was successful.\n  bool success = 1;\n}\n\n// ListMonitorsRequest is the request to list monitors.\nmessage ListMonitorsRequest {\n  // Maximum number of monitors to return (1-100, defaults to 50).\n  optional int32 limit = 1 [(buf.validate.field).int32 = {\n    gte: 1\n    lte: 100\n  }];\n\n  // Number of monitors to skip for pagination (defaults to 0).\n  optional int32 offset = 2 [(buf.validate.field).int32.gte = 0];\n}\n\n// ListMonitorsResponse is the response containing a list of monitors.\nmessage ListMonitorsResponse {\n  // HTTP monitors in the workspace.\n  repeated HTTPMonitor http_monitors = 1;\n\n  // TCP monitors in the workspace.\n  repeated TCPMonitor tcp_monitors = 2;\n\n  // DNS monitors in the workspace.\n  repeated DNSMonitor dns_monitors = 3;\n\n  // Total number of monitors across all types.\n  int32 total_size = 4;\n}\n\n// GetMonitorStatusRequest is the request to get the status of all regions for a monitor.\nmessage GetMonitorStatusRequest {\n  // Monitor ID to get status for (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// RegionStatus represents the status of a monitor in a specific region.\nmessage RegionStatus {\n  // The region identifier.\n  Region region = 1;\n\n  // The status of the monitor in this region.\n  MonitorStatus status = 2;\n}\n\n// GetMonitorStatusResponse is the response containing the status of all regions for a monitor.\nmessage GetMonitorStatusResponse {\n  // Monitor ID.\n  string id = 1;\n\n  // Status for each region.\n  repeated RegionStatus regions = 2;\n}\n\n// MonitorConfig represents the type-specific configuration for a monitor.\nmessage MonitorConfig {\n  oneof config {\n    // HTTP monitor configuration.\n    HTTPMonitor http = 1;\n    // TCP monitor configuration.\n    TCPMonitor tcp = 2;\n    // DNS monitor configuration.\n    DNSMonitor dns = 3;\n  }\n}\n\n// GetMonitorSummaryRequest is the request to get aggregated metrics for a monitor.\nmessage GetMonitorSummaryRequest {\n  // Monitor ID to get summary for (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Time range for metrics aggregation (defaults to 1 day if unspecified).\n  TimeRange time_range = 2;\n\n  // Optional filter by regions. If empty, returns metrics for all regions.\n  repeated Region regions = 3 [(buf.validate.field).repeated.max_items = 28];\n}\n\n// GetMonitorSummaryResponse is the response containing aggregated metrics for a monitor.\nmessage GetMonitorSummaryResponse {\n  // Monitor ID.\n  string id = 1;\n\n  // Timestamp of the last check in RFC 3339 format.\n  string last_ping_at = 2;\n\n  // Total number of successful requests.\n  int64 total_successful = 3;\n\n  // Total number of degraded requests.\n  int64 total_degraded = 4;\n\n  // Total number of failed requests.\n  int64 total_failed = 5;\n\n  // 50th percentile (median) latency in milliseconds.\n  int64 p50 = 6;\n\n  // 75th percentile latency in milliseconds.\n  int64 p75 = 7;\n\n  // 90th percentile latency in milliseconds.\n  int64 p90 = 8;\n\n  // 95th percentile latency in milliseconds.\n  int64 p95 = 9;\n\n  // 99th percentile latency in milliseconds.\n  int64 p99 = 10;\n\n  // Time range used for the metrics.\n  TimeRange time_range = 11;\n\n  // Regions included in the metrics.\n  repeated Region regions = 12;\n}\n\n// GetMonitorRequest is the request to get a single monitor by ID.\nmessage GetMonitorRequest {\n  // Monitor ID to retrieve (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// GetMonitorResponse is the response containing the monitor.\nmessage GetMonitorResponse {\n  // The monitor configuration (one of HTTP, TCP, or DNS).\n  MonitorConfig monitor = 1;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/monitor/v1/tcp_monitor.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.monitor.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"gnostic/openapi/v3/annotations.proto\";\nimport \"openstatus/monitor/v1/http_monitor.proto\";\nimport \"openstatus/monitor/v1/monitor.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/monitor/v1;monitorv1\";\n\n// TCPMonitor defines the configuration for a TCP monitor.\nmessage TCPMonitor {\n  // Unique identifier for the monitor (output only for create requests).\n  string id = 1;\n\n  // Name of the monitor (required, max 256 characters).\n  string name = 2 [\n    (buf.validate.field).string = {\n      min_len: 1\n      max_len: 256\n    },\n    (gnostic.openapi.v3.property) = {example: {yaml: \"Database Connection Check\"}}\n  ];\n\n  // URI to monitor in format \"host:port\" (required, max 2048 characters).\n  string uri = 3 [\n    (buf.validate.field).string = {\n      min_len: 1\n      max_len: 2048\n    },\n    (gnostic.openapi.v3.property) = {example: {yaml: \"tcp://db.example.com:5432\"}}\n  ];\n\n  // Check periodicity (required).\n  Periodicity periodicity = 4 [(buf.validate.field).enum = {\n    not_in: [0]\n  }];\n\n  // Timeout in milliseconds (0-120000, defaults to 45000).\n  int64 timeout = 5 [(buf.validate.field).int64 = {\n    gte: 0\n    lte: 120000\n  }];\n\n  // Latency threshold for degraded status in milliseconds (optional, 0-120000).\n  optional int64 degraded_at = 6 [(buf.validate.field).int64 = {\n    gte: 0\n    lte: 120000\n  }];\n\n  // Number of retry attempts (0-10, defaults to 3).\n  int64 retry = 7 [(buf.validate.field).int64 = {\n    gte: 0\n    lte: 10\n  }];\n\n  // Description of the monitor (optional).\n  string description = 8 [(buf.validate.field).string.max_len = 1024];\n\n  // Whether the monitor is active (defaults to false).\n  bool active = 9;\n\n  // Whether the monitor is publicly visible (defaults to false).\n  bool public = 10;\n\n  // Geographic regions to run checks from.\n  repeated Region regions = 11 [(buf.validate.field).repeated = {\n    max_items: 28\n    items: {\n      enum: {\n        not_in: [0]\n      }\n    }\n  }];\n\n  // OpenTelemetry configuration for exporting metrics.\n  OpenTelemetryConfig open_telemetry = 12;\n\n  // Current operational status of the monitor.\n  MonitorStatus status = 13;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/notification/v1/notification.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.notification.v1;\n\nimport \"openstatus/notification/v1/providers.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/notification/v1;notificationv1\";\n\n// Notification represents a notification channel with full details.\nmessage Notification {\n  // Unique identifier for the notification.\n  string id = 1;\n  // Display name for the notification channel.\n  string name = 2;\n  // Provider type.\n  NotificationProvider provider = 3;\n  // Provider-specific configuration.\n  NotificationData data = 4;\n  // IDs of monitors associated with this notification.\n  repeated string monitor_ids = 5;\n  // Timestamp when the notification was created (RFC 3339).\n  string created_at = 6;\n  // Timestamp when the notification was last updated (RFC 3339).\n  string updated_at = 7;\n}\n\n// NotificationSummary represents a notification channel summary for list responses.\nmessage NotificationSummary {\n  // Unique identifier for the notification.\n  string id = 1;\n  // Display name for the notification channel.\n  string name = 2;\n  // Provider type.\n  NotificationProvider provider = 3;\n  // Number of monitors associated with this notification.\n  int32 monitor_count = 4;\n  // Timestamp when the notification was created (RFC 3339).\n  string created_at = 5;\n  // Timestamp when the notification was last updated (RFC 3339).\n  string updated_at = 6;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/notification/v1/providers.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.notification.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"gnostic/openapi/v3/annotations.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/notification/v1;notificationv1\";\n\n// NotificationProvider represents the supported notification channel types.\nenum NotificationProvider {\n  // Unspecified provider.\n  NOTIFICATION_PROVIDER_UNSPECIFIED = 0;\n  // Discord webhook.\n  NOTIFICATION_PROVIDER_DISCORD = 1;\n  // Email notification.\n  NOTIFICATION_PROVIDER_EMAIL = 2;\n  // Google Chat webhook.\n  NOTIFICATION_PROVIDER_GOOGLE_CHAT = 3;\n  // Grafana OnCall webhook.\n  NOTIFICATION_PROVIDER_GRAFANA_ONCALL = 4;\n  // Ntfy notification service.\n  NOTIFICATION_PROVIDER_NTFY = 5;\n  // PagerDuty integration.\n  NOTIFICATION_PROVIDER_PAGERDUTY = 6;\n  // Opsgenie integration.\n  NOTIFICATION_PROVIDER_OPSGENIE = 7;\n  // Slack webhook.\n  NOTIFICATION_PROVIDER_SLACK = 8;\n  // SMS notification.\n  NOTIFICATION_PROVIDER_SMS = 9;\n  // Telegram bot.\n  NOTIFICATION_PROVIDER_TELEGRAM = 10;\n  // Custom webhook.\n  NOTIFICATION_PROVIDER_WEBHOOK = 11;\n  // WhatsApp notification.\n  NOTIFICATION_PROVIDER_WHATSAPP = 12;\n}\n\n// DiscordData contains configuration for Discord notifications.\nmessage DiscordData {\n  // Discord webhook URL.\n  string webhook_url = 1 [\n    (buf.validate.field).string.uri = true,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"https://discord.com/api/webhooks/123/abc\"}}\n  ];\n}\n\n// EmailData contains configuration for email notifications.\nmessage EmailData {\n  // Email address to send notifications to.\n  string email = 1 [\n    (buf.validate.field).string.email = true,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"ops-team@example.com\"}}\n  ];\n}\n\n// GoogleChatData contains configuration for Google Chat notifications.\nmessage GoogleChatData {\n  // Google Chat webhook URL.\n  string webhook_url = 1 [(buf.validate.field).string.uri = true];\n}\n\n// GrafanaOncallData contains configuration for Grafana OnCall notifications.\nmessage GrafanaOncallData {\n  // Grafana OnCall webhook URL.\n  string webhook_url = 1 [(buf.validate.field).string.uri = true];\n}\n\n// NtfyData contains configuration for Ntfy notifications.\nmessage NtfyData {\n  // Ntfy topic to publish to.\n  string topic = 1 [\n    (buf.validate.field).string.min_len = 1,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"openstatus-alerts\"}}\n  ];\n  // Ntfy server URL (defaults to https://ntfy.sh).\n  string server_url = 2;\n  // Optional authentication token.\n  optional string token = 3;\n}\n\n// PagerDutyData contains configuration for PagerDuty notifications.\nmessage PagerDutyData {\n  // PagerDuty integration key.\n  string integration_key = 1 [\n    (buf.validate.field).string.min_len = 1,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"a1b2c3d4e5f6g7h8i9j0\"}}\n  ];\n}\n\n// OpsgenieRegion represents the Opsgenie API region.\nenum OpsgenieRegion {\n  // Unspecified region.\n  OPSGENIE_REGION_UNSPECIFIED = 0;\n  // US region.\n  OPSGENIE_REGION_US = 1;\n  // EU region.\n  OPSGENIE_REGION_EU = 2;\n}\n\n// OpsgenieData contains configuration for Opsgenie notifications.\nmessage OpsgenieData {\n  // Opsgenie API key.\n  string api_key = 1 [(buf.validate.field).string.min_len = 1];\n  // Opsgenie region.\n  OpsgenieRegion region = 2 [(buf.validate.field).enum.defined_only = true];\n}\n\n// SlackData contains configuration for Slack notifications.\nmessage SlackData {\n  // Slack webhook URL.\n  string webhook_url = 1 [\n    (buf.validate.field).string.uri = true,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"https://hooks.slack.com/services/T00/B00/xxx\"}}\n  ];\n}\n\n// SmsData contains configuration for SMS notifications.\nmessage SmsData {\n  // Phone number to send SMS to.\n  string phone_number = 1 [\n    (buf.validate.field).string.min_len = 1,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"+14155551234\"}}\n  ];\n}\n\n// TelegramData contains configuration for Telegram notifications.\nmessage TelegramData {\n  // Telegram chat ID.\n  string chat_id = 1 [\n    (buf.validate.field).string.min_len = 1,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"-1001234567890\"}}\n  ];\n}\n\n// WebhookHeader represents a custom header for webhook requests.\nmessage WebhookHeader {\n  // Header name.\n  string key = 1 [(buf.validate.field).string.min_len = 1];\n  // Header value.\n  string value = 2;\n}\n\n// WebhookData contains configuration for custom webhook notifications.\nmessage WebhookData {\n  // Webhook endpoint URL.\n  string endpoint = 1 [\n    (buf.validate.field).string.uri = true,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"https://api.example.com/webhooks/openstatus\"}}\n  ];\n  // Optional custom headers.\n  repeated WebhookHeader headers = 2;\n}\n\n// WhatsappData contains configuration for WhatsApp notifications.\nmessage WhatsappData {\n  // Phone number to send WhatsApp messages to.\n  string phone_number = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// NotificationData is a union of provider-specific configuration.\nmessage NotificationData {\n  oneof data {\n    // Discord configuration.\n    DiscordData discord = 1;\n    // Email configuration.\n    EmailData email = 2;\n    // Google Chat configuration.\n    GoogleChatData google_chat = 3;\n    // Grafana OnCall configuration.\n    GrafanaOncallData grafana_oncall = 4;\n    // Ntfy configuration.\n    NtfyData ntfy = 5;\n    // PagerDuty configuration.\n    PagerDutyData pagerduty = 6;\n    // Opsgenie configuration.\n    OpsgenieData opsgenie = 7;\n    // Slack configuration.\n    SlackData slack = 8;\n    // SMS configuration.\n    SmsData sms = 9;\n    // Telegram configuration.\n    TelegramData telegram = 10;\n    // Webhook configuration.\n    WebhookData webhook = 11;\n    // WhatsApp configuration.\n    WhatsappData whatsapp = 12;\n  }\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/notification/v1/service.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.notification.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"gnostic/openapi/v3/annotations.proto\";\nimport \"openstatus/notification/v1/notification.proto\";\nimport \"openstatus/notification/v1/providers.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/notification/v1;notificationv1\";\n\n// NotificationService provides CRUD operations for notification channels.\nservice NotificationService {\n  // CreateNotification creates a new notification channel.\n  rpc CreateNotification(CreateNotificationRequest) returns (CreateNotificationResponse);\n\n  // GetNotification retrieves a notification channel by ID.\n  rpc GetNotification(GetNotificationRequest) returns (GetNotificationResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // ListNotifications returns a list of notification channels.\n  rpc ListNotifications(ListNotificationsRequest) returns (ListNotificationsResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // UpdateNotification updates an existing notification channel.\n  rpc UpdateNotification(UpdateNotificationRequest) returns (UpdateNotificationResponse);\n\n  // DeleteNotification removes a notification channel.\n  rpc DeleteNotification(DeleteNotificationRequest) returns (DeleteNotificationResponse);\n\n  // SendTestNotification sends a test notification to verify the provider configuration without requiring an existing notification channel.\n  rpc SendTestNotification(SendTestNotificationRequest) returns (SendTestNotificationResponse) {\n    option (gnostic.openapi.v3.operation) = {\n      description: \"Sends a test notification to the specified provider to verify that the configuration is correct. This does not require an existing notification channel - just provide the provider type and its configuration data. Returns success status and an error message if the test failed.\"\n    };\n  }\n\n  // CheckNotificationLimit checks if the workspace has reached its notification limit.\n  rpc CheckNotificationLimit(CheckNotificationLimitRequest) returns (CheckNotificationLimitResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n}\n\n// CreateNotificationRequest is the request to create a new notification channel.\nmessage CreateNotificationRequest {\n  // Display name for the notification channel.\n  string name = 1 [\n    (buf.validate.field).string.min_len = 1,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"Slack Ops Channel\"}}\n  ];\n  // Provider type.\n  NotificationProvider provider = 2 [(buf.validate.field).enum = {\n    defined_only: true\n    not_in: [0]\n  }];\n  // Provider-specific configuration.\n  NotificationData data = 3 [(buf.validate.field).required = true];\n  // IDs of monitors to associate with this notification.\n  repeated string monitor_ids = 4;\n}\n\n// CreateNotificationResponse is the response after creating a notification channel.\nmessage CreateNotificationResponse {\n  // The created notification channel.\n  Notification notification = 1;\n}\n\n// GetNotificationRequest is the request to get a notification channel.\nmessage GetNotificationRequest {\n  // Notification ID to retrieve (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// GetNotificationResponse is the response containing the notification channel.\nmessage GetNotificationResponse {\n  // The notification channel.\n  Notification notification = 1;\n}\n\n// ListNotificationsRequest is the request to list notification channels.\nmessage ListNotificationsRequest {\n  // Maximum number of notifications to return (1-100, defaults to 50).\n  optional int32 limit = 1 [(buf.validate.field).int32 = {\n    gte: 1\n    lte: 100\n  }];\n  // Number of notifications to skip for pagination (defaults to 0).\n  optional int32 offset = 2 [(buf.validate.field).int32.gte = 0];\n}\n\n// ListNotificationsResponse is the response containing notification channels.\nmessage ListNotificationsResponse {\n  // Notification channel summaries.\n  repeated NotificationSummary notifications = 1;\n  // Total number of notification channels.\n  int32 total_size = 2;\n}\n\n// UpdateNotificationRequest is the request to update a notification channel.\nmessage UpdateNotificationRequest {\n  // Notification ID to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n  // Updated display name.\n  optional string name = 2;\n  // Updated provider-specific configuration.\n  optional NotificationData data = 3;\n  // Updated monitor IDs to associate.\n  repeated string monitor_ids = 4;\n}\n\n// UpdateNotificationResponse is the response after updating a notification channel.\nmessage UpdateNotificationResponse {\n  // The updated notification channel.\n  Notification notification = 1;\n}\n\n// DeleteNotificationRequest is the request to delete a notification channel.\nmessage DeleteNotificationRequest {\n  // Notification ID to delete (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// DeleteNotificationResponse is the response after deleting a notification channel.\nmessage DeleteNotificationResponse {\n  // Whether the deletion was successful.\n  bool success = 1;\n}\n\n// SendTestNotificationRequest is the request to send a test notification.\nmessage SendTestNotificationRequest {\n  // Provider type.\n  NotificationProvider provider = 1 [(buf.validate.field).enum = {\n    defined_only: true\n    not_in: [0]\n  }];\n  // Provider-specific configuration.\n  NotificationData data = 2 [(buf.validate.field).required = true];\n}\n\n// SendTestNotificationResponse is the response after sending a test notification.\nmessage SendTestNotificationResponse {\n  // Whether the test was successful.\n  bool success = 1;\n  // Optional error message if the test failed.\n  optional string error_message = 2;\n}\n\n// CheckNotificationLimitRequest is the request to check notification limits.\nmessage CheckNotificationLimitRequest {}\n\n// CheckNotificationLimitResponse is the response containing limit information.\nmessage CheckNotificationLimitResponse {\n  // Whether the workspace has reached its notification limit.\n  bool limit_reached = 1;\n  // Current number of notification channels.\n  int32 current_count = 2;\n  // Maximum allowed notification channels.\n  int32 max_count = 3;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/status_page/v1/page_component.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.status_page.v1;\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/status_page/v1;statuspagev1\";\n\n// PageComponentType defines the type of a component on a status page.\nenum PageComponentType {\n  PAGE_COMPONENT_TYPE_UNSPECIFIED = 0;\n  PAGE_COMPONENT_TYPE_MONITOR = 1;\n  PAGE_COMPONENT_TYPE_STATIC = 2;\n}\n\n// PageComponent represents a component displayed on a status page.\nmessage PageComponent {\n  // Unique identifier for the component.\n  string id = 1;\n\n  // ID of the status page this component belongs to.\n  string page_id = 2;\n\n  // Display name of the component.\n  string name = 3;\n\n  // Description of the component (optional).\n  string description = 4;\n\n  // Type of the component (monitor or static).\n  PageComponentType type = 5;\n\n  // ID of the monitor if type is MONITOR (optional).\n  string monitor_id = 6;\n\n  // Display order of the component.\n  int32 order = 7;\n\n  // ID of the group this component belongs to (optional).\n  string group_id = 8;\n\n  // Order within the group if grouped.\n  int32 group_order = 9;\n\n  // Timestamp when the component was created (RFC 3339 format).\n  string created_at = 10;\n\n  // Timestamp when the component was last updated (RFC 3339 format).\n  string updated_at = 11;\n}\n\n// PageComponentGroup represents a group of components on a status page.\nmessage PageComponentGroup {\n  // Unique identifier for the group.\n  string id = 1;\n\n  // ID of the status page this group belongs to.\n  string page_id = 2;\n\n  // Display name of the group.\n  string name = 3;\n\n  // Timestamp when the group was created (RFC 3339 format).\n  string created_at = 4;\n\n  // Timestamp when the group was last updated (RFC 3339 format).\n  string updated_at = 5;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/status_page/v1/page_subscriber.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.status_page.v1;\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/status_page/v1;statuspagev1\";\n\n// PageSubscriber represents a subscriber to a status page.\nmessage PageSubscriber {\n  // Unique identifier for the subscriber.\n  string id = 1;\n\n  // ID of the status page the user is subscribed to.\n  string page_id = 2;\n\n  // Email address of the subscriber.\n  string email = 3;\n\n  // Timestamp when the subscription was accepted/confirmed (RFC 3339 format, optional).\n  string accepted_at = 4;\n\n  // Timestamp when the user unsubscribed (RFC 3339 format, optional).\n  string unsubscribed_at = 5;\n\n  // Timestamp when the subscription was created (RFC 3339 format).\n  string created_at = 6;\n\n  // Timestamp when the subscription was last updated (RFC 3339 format).\n  string updated_at = 7;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/status_page/v1/service.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.status_page.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"gnostic/openapi/v3/annotations.proto\";\nimport \"openstatus/maintenance/v1/maintenance.proto\";\nimport \"openstatus/status_page/v1/page_component.proto\";\nimport \"openstatus/status_page/v1/page_subscriber.proto\";\nimport \"openstatus/status_page/v1/status_page.proto\";\nimport \"openstatus/status_report/v1/status_report.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/status_page/v1;statuspagev1\";\n\n// StatusPageService provides CRUD and management operations for status pages,\n// including component management, component grouping, subscriber handling, and aggregated status queries.\nservice StatusPageService {\n  // CreateStatusPage creates a new status page.\n  rpc CreateStatusPage(CreateStatusPageRequest) returns (CreateStatusPageResponse);\n\n  // GetStatusPage retrieves a specific status page by ID.\n  rpc GetStatusPage(GetStatusPageRequest) returns (GetStatusPageResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // ListStatusPages returns all status pages for the workspace.\n  rpc ListStatusPages(ListStatusPagesRequest) returns (ListStatusPagesResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // UpdateStatusPage updates an existing status page.\n  rpc UpdateStatusPage(UpdateStatusPageRequest) returns (UpdateStatusPageResponse);\n\n  // DeleteStatusPage removes a status page.\n  rpc DeleteStatusPage(DeleteStatusPageRequest) returns (DeleteStatusPageResponse);\n\n  // AddMonitorComponent adds a monitor-based component to a status page.\n  rpc AddMonitorComponent(AddMonitorComponentRequest) returns (AddMonitorComponentResponse);\n\n  // AddStaticComponent adds a static component to a status page.\n  rpc AddStaticComponent(AddStaticComponentRequest) returns (AddStaticComponentResponse);\n\n  // RemoveComponent removes a component from a status page.\n  rpc RemoveComponent(RemoveComponentRequest) returns (RemoveComponentResponse);\n\n  // UpdateComponent updates an existing component.\n  rpc UpdateComponent(UpdateComponentRequest) returns (UpdateComponentResponse);\n\n  // CreateComponentGroup creates a new component group.\n  rpc CreateComponentGroup(CreateComponentGroupRequest) returns (CreateComponentGroupResponse);\n\n  // DeleteComponentGroup removes a component group.\n  rpc DeleteComponentGroup(DeleteComponentGroupRequest) returns (DeleteComponentGroupResponse);\n\n  // UpdateComponentGroup updates an existing component group.\n  rpc UpdateComponentGroup(UpdateComponentGroupRequest) returns (UpdateComponentGroupResponse);\n\n  // SubscribeToPage subscribes an email to a status page. If the email was previously unsubscribed, the subscription is reactivated.\n  rpc SubscribeToPage(SubscribeToPageRequest) returns (SubscribeToPageResponse) {\n    option (gnostic.openapi.v3.operation) = {\n      description: \"Subscribes an email address to receive notifications from a status page. If the email was previously unsubscribed, the subscription is reactivated instead of creating a duplicate.\"\n    };\n  }\n\n  // UnsubscribeFromPage removes a subscription from a status page.\n  rpc UnsubscribeFromPage(UnsubscribeFromPageRequest) returns (UnsubscribeFromPageResponse);\n\n  // ListSubscribers returns all subscribers for a status page.\n  rpc ListSubscribers(ListSubscribersRequest) returns (ListSubscribersResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // GetStatusPageContent retrieves the full content of a status page including components, groups, active reports, and maintenances.\n  rpc GetStatusPageContent(GetStatusPageContentRequest) returns (GetStatusPageContentResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n    option (gnostic.openapi.v3.operation) = {\n      description: \"Returns the full content of a status page including its components, component groups, active status reports, and scheduled maintenances. Supports two access paths: by ID (requires authentication, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).\"\n    };\n  }\n\n  // GetOverallStatus returns the aggregated status of a status page and its individual components.\n  rpc GetOverallStatus(GetOverallStatusRequest) returns (GetOverallStatusResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n    option (gnostic.openapi.v3.operation) = {\n      description: \"Returns the overall status of a status page along with individual component statuses. The overall status is computed from active status reports and maintenances with the following priority: degraded (from active status reports) > maintenance (from active maintenance windows) > operational.\"\n    };\n  }\n}\n\n// =============================================================================\n// Page CRUD Messages\n// =============================================================================\n\n// CreateStatusPageRequest is the request to create a new status page.\nmessage CreateStatusPageRequest {\n  // Title of the status page (required).\n  string title = 1 [\n    (buf.validate.field).string.min_len = 1,\n    (buf.validate.field).string.max_len = 256,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"Acme Corp Status\"}}\n  ];\n\n  // Description of the status page (optional).\n  optional string description = 2 [(buf.validate.field).string.max_len = 1024];\n\n  // URL-friendly slug for the status page (required). Must be lowercase alphanumeric with hyphens.\n  string slug = 3 [\n    (buf.validate.field).string.min_len = 1,\n    (buf.validate.field).string.max_len = 256,\n    (buf.validate.field).string.pattern = \"^[a-z0-9]+(?:-[a-z0-9]+)*$\",\n    (gnostic.openapi.v3.property) = {example: {yaml: \"my-status-page\"}}\n  ];\n\n  // URL to the homepage (optional).\n  optional string homepage_url = 4 [\n    (gnostic.openapi.v3.property) = {example: {yaml: \"https://www.example.com\"}}\n  ];\n\n  // URL to the contact page (optional).\n  optional string contact_url = 5;\n}\n\n// CreateStatusPageResponse is the response after creating a status page.\nmessage CreateStatusPageResponse {\n  // The created status page.\n  StatusPage status_page = 1;\n}\n\n// GetStatusPageRequest is the request to get a status page by ID.\nmessage GetStatusPageRequest {\n  // ID of the status page to retrieve (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// GetStatusPageResponse is the response containing the status page.\nmessage GetStatusPageResponse {\n  // The requested status page.\n  StatusPage status_page = 1;\n}\n\n// ListStatusPagesRequest is the request to list status pages.\nmessage ListStatusPagesRequest {\n  // Maximum number of pages to return (1-100, defaults to 50).\n  optional int32 limit = 1 [(buf.validate.field).int32 = {\n    gte: 1\n    lte: 100\n  }];\n\n  // Number of pages to skip for pagination (defaults to 0).\n  optional int32 offset = 2 [(buf.validate.field).int32.gte = 0];\n}\n\n// ListStatusPagesResponse is the response containing status page summaries.\nmessage ListStatusPagesResponse {\n  // List of status pages (metadata only).\n  repeated StatusPageSummary status_pages = 1;\n\n  // Total number of status pages.\n  int32 total_size = 2;\n}\n\n// UpdateStatusPageRequest is the request to update a status page.\nmessage UpdateStatusPageRequest {\n  // ID of the status page to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // New title for the status page (optional).\n  optional string title = 2 [(buf.validate.field).string = {\n    min_len: 1\n    max_len: 256\n  }];\n\n  // New description for the status page (optional).\n  optional string description = 3 [(buf.validate.field).string.max_len = 1024];\n\n  // New slug for the status page (optional).\n  optional string slug = 4 [(buf.validate.field).string = {\n    min_len: 1\n    max_len: 256\n    pattern: \"^[a-z0-9]+(?:-[a-z0-9]+)*$\"\n  }];\n\n  // New homepage URL (optional).\n  optional string homepage_url = 5;\n\n  // New contact URL (optional).\n  optional string contact_url = 6;\n}\n\n// UpdateStatusPageResponse is the response after updating a status page.\nmessage UpdateStatusPageResponse {\n  // The updated status page.\n  StatusPage status_page = 1;\n}\n\n// DeleteStatusPageRequest is the request to delete a status page.\nmessage DeleteStatusPageRequest {\n  // ID of the status page to delete (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// DeleteStatusPageResponse is the response after deleting a status page.\nmessage DeleteStatusPageResponse {\n  // Whether the deletion was successful.\n  bool success = 1;\n}\n\n// =============================================================================\n// Component Management Messages\n// =============================================================================\n\n// AddMonitorComponentRequest is the request to add a monitor-based component to a status page.\nmessage AddMonitorComponentRequest {\n  // ID of the status page to add the component to (required).\n  string page_id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // ID of the monitor to associate with this component (required).\n  string monitor_id = 2 [(buf.validate.field).string.min_len = 1];\n\n  // Display name for the component (optional, defaults to monitor name).\n  optional string name = 3 [(buf.validate.field).string.max_len = 256];\n\n  // Description of the component (optional).\n  optional string description = 4 [(buf.validate.field).string.max_len = 1024];\n\n  // Display order of the component (optional).\n  optional int32 order = 5;\n\n  // ID of the group to add this component to (optional).\n  optional string group_id = 6;\n}\n\n// AddMonitorComponentResponse is the response after adding a monitor component.\nmessage AddMonitorComponentResponse {\n  // The created component.\n  PageComponent component = 1;\n}\n\n// AddStaticComponentRequest is the request to add a static component to a status page.\nmessage AddStaticComponentRequest {\n  // ID of the status page to add the component to (required).\n  string page_id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Display name for the component (required).\n  string name = 2 [\n    (buf.validate.field).string.min_len = 1,\n    (buf.validate.field).string.max_len = 256\n  ];\n\n  // Description of the component (optional).\n  optional string description = 3 [(buf.validate.field).string.max_len = 1024];\n\n  // Display order of the component (optional).\n  optional int32 order = 4;\n\n  // ID of the group to add this component to (optional).\n  optional string group_id = 5;\n}\n\n// AddStaticComponentResponse is the response after adding a static component.\nmessage AddStaticComponentResponse {\n  // The created component.\n  PageComponent component = 1;\n}\n\n// RemoveComponentRequest is the request to remove a component from a status page.\nmessage RemoveComponentRequest {\n  // ID of the component to remove (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// RemoveComponentResponse is the response after removing a component.\nmessage RemoveComponentResponse {\n  // Whether the removal was successful.\n  bool success = 1;\n}\n\n// UpdateComponentRequest is the request to update a component.\nmessage UpdateComponentRequest {\n  // ID of the component to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // New display name for the component (optional).\n  optional string name = 2 [(buf.validate.field).string.max_len = 256];\n\n  // New description for the component (optional).\n  optional string description = 3 [(buf.validate.field).string.max_len = 1024];\n\n  // New display order (optional).\n  optional int32 order = 4;\n\n  // New group ID (optional, set to empty string to remove from group).\n  optional string group_id = 5;\n\n  // New order within the group (optional).\n  optional int32 group_order = 6;\n}\n\n// UpdateComponentResponse is the response after updating a component.\nmessage UpdateComponentResponse {\n  // The updated component.\n  PageComponent component = 1;\n}\n\n// =============================================================================\n// Component Group Messages\n// =============================================================================\n\n// CreateComponentGroupRequest is the request to create a new component group.\nmessage CreateComponentGroupRequest {\n  // ID of the status page to create the group in (required).\n  string page_id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Display name for the group (required).\n  string name = 2 [\n    (buf.validate.field).string.min_len = 1,\n    (buf.validate.field).string.max_len = 256\n  ];\n}\n\n// CreateComponentGroupResponse is the response after creating a component group.\nmessage CreateComponentGroupResponse {\n  // The created component group.\n  PageComponentGroup group = 1;\n}\n\n// DeleteComponentGroupRequest is the request to delete a component group.\nmessage DeleteComponentGroupRequest {\n  // ID of the component group to delete (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// DeleteComponentGroupResponse is the response after deleting a component group.\nmessage DeleteComponentGroupResponse {\n  // Whether the deletion was successful.\n  bool success = 1;\n}\n\n// UpdateComponentGroupRequest is the request to update a component group.\nmessage UpdateComponentGroupRequest {\n  // ID of the component group to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // New display name for the group (optional).\n  optional string name = 2 [(buf.validate.field).string = {\n    min_len: 1\n    max_len: 256\n  }];\n}\n\n// UpdateComponentGroupResponse is the response after updating a component group.\nmessage UpdateComponentGroupResponse {\n  // The updated component group.\n  PageComponentGroup group = 1;\n}\n\n// =============================================================================\n// Subscriber Messages\n// =============================================================================\n\n// SubscribeToPageRequest is the request to subscribe an email to a status page.\nmessage SubscribeToPageRequest {\n  // ID of the status page to subscribe to (required).\n  string page_id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Email address to subscribe (required).\n  string email = 2 [\n    (buf.validate.field).string.email = true,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"user@example.com\"}}\n  ];\n}\n\n// SubscribeToPageResponse is the response after subscribing to a status page.\nmessage SubscribeToPageResponse {\n  // The created subscriber.\n  PageSubscriber subscriber = 1;\n}\n\n// UnsubscribeFromPageRequest is the request to unsubscribe from a status page.\nmessage UnsubscribeFromPageRequest {\n  // ID of the status page to unsubscribe from (required).\n  string page_id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Identifier for the subscription (either email or id).\n  oneof identifier {\n    // Email address to unsubscribe.\n    string email = 2;\n    // Subscriber ID.\n    string id = 3;\n  }\n}\n\n// UnsubscribeFromPageResponse is the response after unsubscribing from a status page.\nmessage UnsubscribeFromPageResponse {\n  // Whether the unsubscription was successful.\n  bool success = 1;\n}\n\n// ListSubscribersRequest is the request to list subscribers of a status page.\nmessage ListSubscribersRequest {\n  // ID of the status page to list subscribers for (required).\n  string page_id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Maximum number of subscribers to return (1-100, defaults to 50).\n  optional int32 limit = 2 [(buf.validate.field).int32 = {\n    gte: 1\n    lte: 100\n  }];\n\n  // Number of subscribers to skip for pagination (defaults to 0).\n  optional int32 offset = 3 [(buf.validate.field).int32.gte = 0];\n\n  // Whether to include unsubscribed users (defaults to false).\n  optional bool include_unsubscribed = 4;\n}\n\n// ListSubscribersResponse is the response containing status page subscribers.\nmessage ListSubscribersResponse {\n  // List of subscribers.\n  repeated PageSubscriber subscribers = 1;\n\n  // Total number of subscribers matching the filter.\n  int32 total_size = 2;\n}\n\n// =============================================================================\n// Full Content & Status Messages\n// =============================================================================\n\n// GetStatusPageContentRequest is the request to get the full content of a status page.\nmessage GetStatusPageContentRequest {\n  // Identifier for the status page (either id or slug).\n  oneof identifier {\n    // ID of the status page.\n    string id = 1;\n    // Slug of the status page.\n    string slug = 2;\n  }\n}\n\n// GetStatusPageContentResponse is the response containing the full status page content.\nmessage GetStatusPageContentResponse {\n  // The status page details.\n  StatusPage status_page = 1;\n\n  // Components on the status page.\n  repeated PageComponent components = 2;\n\n  // Component groups on the status page.\n  repeated PageComponentGroup groups = 3;\n\n  // Active and recent status reports.\n  repeated openstatus.status_report.v1.StatusReport status_reports = 4;\n\n  // Scheduled maintenances.\n  repeated openstatus.maintenance.v1.MaintenanceSummary maintenances = 5;\n}\n\n// GetOverallStatusRequest is the request to get the overall status of a status page.\nmessage GetOverallStatusRequest {\n  // Identifier for the status page (either id or slug).\n  oneof identifier {\n    // ID of the status page.\n    string id = 1;\n    // Slug of the status page.\n    string slug = 2;\n  }\n}\n\n// ComponentStatus represents the status of a single component.\nmessage ComponentStatus {\n  // ID of the component.\n  string component_id = 1;\n\n  // Current status of the component.\n  OverallStatus status = 2;\n}\n\n// GetOverallStatusResponse is the response containing the overall status and individual component statuses.\nmessage GetOverallStatusResponse {\n  // Aggregated status across all components.\n  OverallStatus overall_status = 1;\n\n  // Status of individual components.\n  repeated ComponentStatus component_statuses = 2;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/status_page/v1/status_page.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.status_page.v1;\n\nimport \"gnostic/openapi/v3/annotations.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/status_page/v1;statuspagev1\";\n\n// PageAccessType defines who can access the status page.\nenum PageAccessType {\n  PAGE_ACCESS_TYPE_UNSPECIFIED = 0;\n  PAGE_ACCESS_TYPE_PUBLIC = 1;\n  PAGE_ACCESS_TYPE_PASSWORD_PROTECTED = 2;\n  PAGE_ACCESS_TYPE_AUTHENTICATED = 3;\n}\n\n// PageTheme defines the visual theme of the status page.\nenum PageTheme {\n  PAGE_THEME_UNSPECIFIED = 0;\n  PAGE_THEME_SYSTEM = 1;\n  PAGE_THEME_LIGHT = 2;\n  PAGE_THEME_DARK = 3;\n}\n\n// OverallStatus represents the aggregated status of all components on a page.\nenum OverallStatus {\n  OVERALL_STATUS_UNSPECIFIED = 0;\n  OVERALL_STATUS_OPERATIONAL = 1;\n  OVERALL_STATUS_DEGRADED = 2;\n  OVERALL_STATUS_PARTIAL_OUTAGE = 3;\n  OVERALL_STATUS_MAJOR_OUTAGE = 4;\n  OVERALL_STATUS_MAINTENANCE = 5;\n  OVERALL_STATUS_UNKNOWN = 6;\n}\n\n// StatusPage represents a full status page with all details.\nmessage StatusPage {\n  // Unique identifier for the status page.\n  string id = 1;\n\n  // Title of the status page.\n  string title = 2;\n\n  // Description of the status page.\n  string description = 3;\n\n  // URL-friendly slug for the status page.\n  string slug = 4 [\n    (gnostic.openapi.v3.property) = {example: {yaml: \"acme-corp\"}}\n  ];\n\n  // Custom domain for the status page (optional).\n  string custom_domain = 5 [\n    (gnostic.openapi.v3.property) = {example: {yaml: \"status.example.com\"}}\n  ];\n\n  // Whether the status page is published and visible.\n  bool published = 6;\n\n  // Access type for the status page.\n  PageAccessType access_type = 7;\n\n  // Visual theme for the status page.\n  PageTheme theme = 8;\n\n  // URL to the homepage (optional).\n  string homepage_url = 9;\n\n  // URL to the contact page (optional).\n  string contact_url = 10;\n\n  // Icon URL for the status page (optional).\n  string icon = 11;\n\n  // Timestamp when the page was created (RFC 3339 format).\n  string created_at = 12 [\n    (gnostic.openapi.v3.property) = {example: {yaml: \"2024-01-15T09:00:00Z\"}}\n  ];\n\n  // Timestamp when the page was last updated (RFC 3339 format).\n  string updated_at = 13 [\n    (gnostic.openapi.v3.property) = {example: {yaml: \"2024-06-20T14:30:00Z\"}}\n  ];\n}\n\n// StatusPageSummary represents metadata for a status page (used in list responses).\nmessage StatusPageSummary {\n  // Unique identifier for the status page.\n  string id = 1;\n\n  // Title of the status page.\n  string title = 2;\n\n  // URL-friendly slug for the status page.\n  string slug = 3;\n\n  // Whether the status page is published and visible.\n  bool published = 4;\n\n  // Timestamp when the page was created (RFC 3339 format).\n  string created_at = 5;\n\n  // Timestamp when the page was last updated (RFC 3339 format).\n  string updated_at = 6;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/status_report/v1/service.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.status_report.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"gnostic/openapi/v3/annotations.proto\";\nimport \"openstatus/status_report/v1/status_report.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/status_report/v1;statusreportv1\";\n\n// StatusReportService provides CRUD operations for status reports.\nservice StatusReportService {\n  // CreateStatusReport creates a new status report with an initial update entry and optionally notifies subscribers.\n  rpc CreateStatusReport(CreateStatusReportRequest) returns (CreateStatusReportResponse) {\n    option (gnostic.openapi.v3.operation) = {\n      description: \"Creates a new status report with an initial update entry. The report is associated with a status page and optionally specific page components. An initial StatusReportUpdate is created automatically with the provided status, message, and date. If notify is true, subscribers of the associated page are notified by email.\"\n    };\n  }\n\n  // GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n  rpc GetStatusReport(GetStatusReportRequest) returns (GetStatusReportResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // ListStatusReports returns all status reports for the workspace (metadata only).\n  rpc ListStatusReports(ListStatusReportsRequest) returns (ListStatusReportsResponse) {\n    option idempotency_level = NO_SIDE_EFFECTS;\n  }\n\n  // UpdateStatusReport updates the metadata of a status report (title, page components).\n  rpc UpdateStatusReport(UpdateStatusReportRequest) returns (UpdateStatusReportResponse);\n\n  // DeleteStatusReport removes a status report and all its updates.\n  rpc DeleteStatusReport(DeleteStatusReportRequest) returns (DeleteStatusReportResponse);\n\n  // AddStatusReportUpdate adds a new update to an existing status report timeline and transitions the report status.\n  rpc AddStatusReportUpdate(AddStatusReportUpdateRequest) returns (AddStatusReportUpdateResponse) {\n    option (gnostic.openapi.v3.operation) = {\n      description: \"Adds a new update entry to an existing status report and transitions the report to the specified status. Status reports follow a lifecycle: investigating -> identified -> monitoring -> resolved. If notify is true, subscribers of the associated page are notified by email about the update.\"\n    };\n  }\n}\n\n// CreateStatusReportRequest is the request to create a new status report.\nmessage CreateStatusReportRequest {\n  // Title of the status report (required).\n  string title = 1 [\n    (buf.validate.field).string.min_len = 1,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"API Degradation Investigation\"}}\n  ];\n\n  // Initial status (required).\n  StatusReportStatus status = 2 [(buf.validate.field).enum.defined_only = true];\n\n  // Initial message describing the incident (required).\n  string message = 3 [\n    (buf.validate.field).string.min_len = 1,\n    (gnostic.openapi.v3.property) = {example: {yaml: \"We are investigating reports of increased API latency.\"}}\n  ];\n\n  // Date when the event occurred (RFC 3339 format, required).\n  string date = 4 [\n    (buf.validate.field).string.pattern = \"^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d{1,9})?(Z|[+-]\\\\d{2}:\\\\d{2})$\",\n    (gnostic.openapi.v3.property) = {example: {yaml: \"2024-03-15T10:30:00Z\"}}\n  ];\n\n  // Page ID to associate with this report (required).\n  string page_id = 5 [(buf.validate.field).string.min_len = 1];\n\n  // Page component IDs to associate with this report (optional).\n  repeated string page_component_ids = 6;\n\n  // Whether to notify subscribers about this status report (optional, defaults to false).\n  optional bool notify = 7;\n}\n\n// CreateStatusReportResponse is the response after creating a status report.\nmessage CreateStatusReportResponse {\n  // The created status report.\n  StatusReport status_report = 1;\n}\n\n// GetStatusReportRequest is the request to get a status report by ID.\nmessage GetStatusReportRequest {\n  // ID of the status report to retrieve (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// GetStatusReportResponse is the response containing the status report with its full update timeline.\nmessage GetStatusReportResponse {\n  // The requested status report.\n  StatusReport status_report = 1;\n}\n\n// ListStatusReportsRequest is the request to list status reports.\nmessage ListStatusReportsRequest {\n  // Maximum number of reports to return (1-100, defaults to 50).\n  optional int32 limit = 1 [(buf.validate.field).int32 = {\n    gte: 1\n    lte: 100\n  }];\n\n  // Number of reports to skip for pagination (defaults to 0).\n  optional int32 offset = 2 [(buf.validate.field).int32.gte = 0];\n\n  // Filter by status (optional). If empty, returns all statuses.\n  repeated StatusReportStatus statuses = 3;\n}\n\n// ListStatusReportsResponse is the response containing status report summaries.\nmessage ListStatusReportsResponse {\n  // List of status reports (metadata only, use GetStatusReport for full details).\n  repeated StatusReportSummary status_reports = 1;\n\n  // Total number of reports matching the filter.\n  int32 total_size = 2;\n}\n\n// UpdateStatusReportRequest is the request to update a status report's metadata.\nmessage UpdateStatusReportRequest {\n  // ID of the status report to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // New title for the report (optional).\n  optional string title = 2;\n\n  // New list of page component IDs (optional, replaces existing list).\n  repeated string page_component_ids = 3;\n}\n\n// UpdateStatusReportResponse is the response after updating a status report.\nmessage UpdateStatusReportResponse {\n  // The updated status report.\n  StatusReport status_report = 1;\n}\n\n// DeleteStatusReportRequest is the request to delete a status report.\nmessage DeleteStatusReportRequest {\n  // ID of the status report to delete (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\n// DeleteStatusReportResponse is the response after deleting a status report.\nmessage DeleteStatusReportResponse {\n  // Whether the deletion was successful.\n  bool success = 1;\n}\n\n// AddStatusReportUpdateRequest is the request to add a new update to a status report.\nmessage AddStatusReportUpdateRequest {\n  // ID of the status report to update (required).\n  string status_report_id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // New status for the report (required).\n  StatusReportStatus status = 2 [(buf.validate.field).enum.defined_only = true];\n\n  // Message describing what changed (required).\n  string message = 3 [(buf.validate.field).string.min_len = 1];\n\n  // Optional date for the update (RFC 3339 format). Defaults to current time if not provided.\n  optional string date = 4 [(buf.validate.field).string.pattern = \"^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d{1,9})?(Z|[+-]\\\\d{2}:\\\\d{2})$\"];\n\n  // Whether to notify subscribers about this update (optional, defaults to false).\n  optional bool notify = 5;\n}\n\n// AddStatusReportUpdateResponse is the response after adding an update to a status report.\nmessage AddStatusReportUpdateResponse {\n  // The updated status report with the new update included.\n  StatusReport status_report = 1;\n}\n"
  },
  {
    "path": "packages/proto/api/openstatus/status_report/v1/status_report.proto",
    "content": "syntax = \"proto3\";\n\npackage openstatus.status_report.v1;\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/status_report/v1;statusreportv1\";\n\n// StatusReportStatus represents the current state of a status report.\nenum StatusReportStatus {\n  STATUS_REPORT_STATUS_UNSPECIFIED = 0;\n  STATUS_REPORT_STATUS_INVESTIGATING = 1;\n  STATUS_REPORT_STATUS_IDENTIFIED = 2;\n  STATUS_REPORT_STATUS_MONITORING = 3;\n  STATUS_REPORT_STATUS_RESOLVED = 4;\n}\n\n// StatusReportUpdate represents a single update entry in a status report timeline.\nmessage StatusReportUpdate {\n  // Unique identifier for the update.\n  string id = 1;\n\n  // Status at the time of this update.\n  StatusReportStatus status = 2;\n\n  // Timestamp when this update occurred (RFC 3339 format).\n  string date = 3;\n\n  // Message describing the update.\n  string message = 4;\n\n  // Timestamp when the update was created (RFC 3339 format).\n  string created_at = 5;\n}\n\n// StatusReportSummary represents metadata for a status report (used in list responses).\nmessage StatusReportSummary {\n  // Unique identifier for the status report.\n  string id = 1;\n\n  // Current status of the report.\n  StatusReportStatus status = 2;\n\n  // Title of the status report.\n  string title = 3;\n\n  // IDs of affected page components.\n  repeated string page_component_ids = 4;\n\n  // Timestamp when the report was created (RFC 3339 format).\n  string created_at = 5;\n\n  // Timestamp when the report was last updated (RFC 3339 format).\n  string updated_at = 6;\n}\n\n// StatusReport represents an incident or maintenance report with full details.\nmessage StatusReport {\n  // Unique identifier for the status report.\n  string id = 1;\n\n  // Current status of the report.\n  StatusReportStatus status = 2;\n\n  // Title of the status report.\n  string title = 3;\n\n  // IDs of affected page components.\n  repeated string page_component_ids = 4;\n\n  // Timeline of updates for this report (only included in GetStatusReport).\n  repeated StatusReportUpdate updates = 5;\n\n  // Timestamp when the report was created (RFC 3339 format).\n  string created_at = 6;\n\n  // Timestamp when the report was last updated (RFC 3339 format).\n  string updated_at = 7;\n}\n"
  },
  {
    "path": "packages/proto/base.openapi.yaml",
    "content": "openapi: 3.1.0\ninfo:\n  description: OpenStatus is a open-source status page platform with global uptime monitoring.\n    The OpenStatus API allows you to interact with the OpenStatus platform programmatically.\n    To get started you need to create an account on https://www.openstatus.dev/ and create an api token in your settings.\n  title: OpenStatus API\n  version: v2.0.0\n  contact:\n    email: ping@openstatus.dev\n    url: https://www.openstatus.dev\nexternalDocs:\n  description: OpenStatus Documentation\n  url: https://docs.openstatus.dev\ncomponents:\n  securitySchemes:\n    ApiKeyAuth:\n      type: apiKey\n      in: header\n      name: x-openstatus-key\nsecurity:\n  - ApiKeyAuth: []\ntags:\n  - name: MonitorService\n    description: |\n      Create, update, delete, and query monitors. Supports HTTP, TCP, and DNS monitor types\n      with configurable check intervals, regions, assertions, and alerting thresholds.\n  - name: StatusPageService\n    description: |\n      Manage public status pages with components, component groups, and email subscribers.\n      Includes endpoints for retrieving full page content and aggregated status.\n  - name: NotificationService\n    description: |\n      Configure notification channels (Slack, Discord, PagerDuty, email, webhooks, etc.)\n      and associate them with monitors. Supports 12 notification providers.\n  - name: StatusReportService\n    description: |\n      Create and manage incident reports with status updates. Reports follow a lifecycle:\n      investigating -> identified -> monitoring -> resolved.\n  - name: MaintenanceService\n    description: |\n      Schedule maintenance windows for status page components. Subscribers can be\n      notified automatically when maintenance is created.\n  - name: HealthService\n    description: Health check endpoint for load balancer probes. No authentication required.\n"
  },
  {
    "path": "packages/proto/buf.gen.go.yaml",
    "content": "# buf.gen.yaml defines a local generation template.\n# For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml\nversion: v2\n\nplugins:\n  - local: protoc-gen-go\n    out: ../../apps/private-location/proto\n    opt:\n      - paths=source_relative\n  - local: protoc-gen-connect-go\n    out: ../../apps/private-location/proto\n    opt:\n      - paths=source_relative\n      - package_suffix\n      # - input\n  # Go generation for checker\n  - local: protoc-gen-go\n    out: ../../apps/checker/proto\n    opt:\n      - paths=source_relative\n  - local: protoc-gen-connect-go\n    out: ../../apps/checker/proto\n    opt:\n      - paths=source_relative\n      - package_suffix\n"
  },
  {
    "path": "packages/proto/buf.gen.openapi.yaml",
    "content": "version: v2\nplugins:\n  - local: protoc-gen-connect-openapi\n    out: gen\n    strategy: all\n    opt:\n      - path=openapi.yaml\n      - base=base.openapi.yaml\n      - short-operation-ids\n      - short-service-tags\n      - allow-get\n      - trim-unused-types\n      - path-prefix=/rpc\n"
  },
  {
    "path": "packages/proto/buf.gen.ts.yaml",
    "content": "# buf.gen.yaml defines a local generation template.\n# For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml\nversion: v2\n\nplugins:\n  - local: protoc-gen-es\n    out: gen/ts\n    opt:\n      - target=ts\n      - import_extension=.ts\n    include_imports: true\n"
  },
  {
    "path": "packages/proto/buf.yaml",
    "content": "version: v2\n\nmodules:\n  - path: ./api/\n    name: buf.build/openstatus/api\n  - path: ./internal/\n    name: buf.build/openstatus/private-location\n\ndeps:\n  - buf.build/bufbuild/protovalidate\n  - buf.build/gnostic/gnostic\nlint:\n  use:\n    - STANDARD\n  except:\n    - PACKAGE_VERSION_SUFFIX\nbreaking:\n  use:\n    - FILE\n"
  },
  {
    "path": "packages/proto/gen/openapi.yaml",
    "content": "openapi: 3.1.0\ninfo:\n  description: OpenStatus is a open-source status page platform with global uptime monitoring. The OpenStatus API allows you to interact with the OpenStatus platform programmatically. To get started you need to create an account on https://www.openstatus.dev/ and create an api token in your settings.\n  title: OpenStatus API\n  version: v2.0.0\n  contact:\n    email: ping@openstatus.dev\n    url: https://www.openstatus.dev\nexternalDocs:\n  description: OpenStatus Documentation\n  url: https://docs.openstatus.dev\ncomponents:\n  securitySchemes:\n    ApiKeyAuth:\n      type: apiKey\n      in: header\n      name: x-openstatus-key\n  schemas:\n    connect.error:\n      type: object\n      properties:\n        code:\n          type: string\n          examples:\n            - not_found\n          enum:\n            - canceled\n            - unknown\n            - invalid_argument\n            - deadline_exceeded\n            - not_found\n            - already_exists\n            - permission_denied\n            - resource_exhausted\n            - failed_precondition\n            - aborted\n            - out_of_range\n            - unimplemented\n            - internal\n            - unavailable\n            - data_loss\n            - unauthenticated\n          description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].\n        message:\n          type: string\n          description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.\n        details:\n          type: array\n          items:\n            $ref: '#/components/schemas/connect.error_details.Any'\n          description: A list of messages that carry the error details. There is no limit on the number of messages.\n      title: Connect Error\n      additionalProperties: true\n      description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation'\n    connect.error_details.Any:\n      type: object\n      properties:\n        type:\n          type: string\n          description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.'\n        value:\n          type: string\n          format: binary\n          description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field.\n        debug:\n          oneOf:\n            - type: object\n              title: Any\n              additionalProperties: true\n              description: Detailed error information.\n          discriminator:\n            propertyName: type\n          title: Debug\n          description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic.\n      additionalProperties: true\n      description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details.\n    openstatus.health.v1.CheckRequest:\n      type: object\n      properties:\n        service:\n          type: string\n          title: service\n          description: Optional service name to check. If empty, checks overall service health.\n      title: CheckRequest\n      additionalProperties: false\n      description: CheckRequest is the request message for health checks.\n    openstatus.health.v1.CheckResponse:\n      type: object\n      properties:\n        status:\n          title: status\n          description: The serving status of the service.\n          $ref: '#/components/schemas/openstatus.health.v1.CheckResponse.ServingStatus'\n      title: CheckResponse\n      additionalProperties: false\n      description: CheckResponse is the response message for health checks.\n    openstatus.health.v1.CheckResponse.ServingStatus:\n      type: string\n      title: ServingStatus\n      enum:\n        - SERVING_STATUS_UNSPECIFIED\n        - SERVING_STATUS_SERVING\n        - SERVING_STATUS_NOT_SERVING\n      description: ServingStatus represents the health status of the service.\n    openstatus.maintenance.v1.CreateMaintenanceRequest:\n      type: object\n      properties:\n        title:\n          type: string\n          examples:\n            - Database Migration\n          title: title\n          maxLength: 256\n          minLength: 1\n          description: Title of the maintenance (required, 1-256 characters).\n        message:\n          type: string\n          title: message\n          minLength: 1\n          description: Message describing the maintenance (required).\n        from:\n          type: string\n          examples:\n            - \"2024-03-01T02:00:00Z\"\n          title: from\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: Start time of the maintenance window (RFC 3339 format, required).\n        to:\n          type: string\n          examples:\n            - \"2024-03-01T06:00:00Z\"\n          title: to\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: End time of the maintenance window (RFC 3339 format, required).\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: Page ID to associate with this maintenance (required).\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: Page component IDs to associate with this maintenance (optional).\n        notify:\n          type:\n            - boolean\n            - \"null\"\n          title: notify\n          description: Whether to notify subscribers about this maintenance (optional, defaults to false).\n      title: CreateMaintenanceRequest\n      additionalProperties: false\n      description: CreateMaintenanceRequest is the request to create a new maintenance window.\n    openstatus.maintenance.v1.CreateMaintenanceResponse:\n      type: object\n      properties:\n        maintenance:\n          title: maintenance\n          description: The created maintenance.\n          $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n      title: CreateMaintenanceResponse\n      additionalProperties: false\n      description: CreateMaintenanceResponse is the response after creating a maintenance window.\n    openstatus.maintenance.v1.DeleteMaintenanceRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the maintenance to delete (required).\n      title: DeleteMaintenanceRequest\n      additionalProperties: false\n      description: DeleteMaintenanceRequest is the request to delete a maintenance window.\n    openstatus.maintenance.v1.DeleteMaintenanceResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteMaintenanceResponse\n      additionalProperties: false\n      description: DeleteMaintenanceResponse is the response after deleting a maintenance window.\n    openstatus.maintenance.v1.GetMaintenanceRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the maintenance to retrieve (required).\n      title: GetMaintenanceRequest\n      additionalProperties: false\n      description: GetMaintenanceRequest is the request to get a maintenance window by ID.\n    openstatus.maintenance.v1.GetMaintenanceResponse:\n      type: object\n      properties:\n        maintenance:\n          title: maintenance\n          description: The requested maintenance.\n          $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n      title: GetMaintenanceResponse\n      additionalProperties: false\n      description: GetMaintenanceResponse is the response containing the maintenance window.\n    openstatus.maintenance.v1.ListMaintenancesRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of maintenances to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of maintenances to skip for pagination (defaults to 0).\n        pageId:\n          type:\n            - string\n            - \"null\"\n          title: page_id\n          description: Filter by page ID (optional).\n      title: ListMaintenancesRequest\n      additionalProperties: false\n      description: ListMaintenancesRequest is the request to list maintenance windows.\n    openstatus.maintenance.v1.ListMaintenancesResponse:\n      type: object\n      properties:\n        maintenances:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n          title: maintenances\n          description: List of maintenances.\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of maintenances matching the filter.\n      title: ListMaintenancesResponse\n      additionalProperties: false\n      description: ListMaintenancesResponse is the response containing maintenance window summaries.\n    openstatus.maintenance.v1.Maintenance:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the maintenance.\n        title:\n          type: string\n          title: title\n          description: Title of the maintenance.\n        message:\n          type: string\n          title: message\n          description: Message describing the maintenance.\n        from:\n          type: string\n          title: from\n          description: Start time of the maintenance window (RFC 3339 format).\n        to:\n          type: string\n          title: to\n          description: End time of the maintenance window (RFC 3339 format).\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the page this maintenance is associated with.\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: IDs of affected page components.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the maintenance was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the maintenance was last updated (RFC 3339 format).\n      title: Maintenance\n      additionalProperties: false\n      description: Maintenance represents a maintenance window with full details.\n    openstatus.maintenance.v1.MaintenanceSummary:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the maintenance.\n        title:\n          type: string\n          title: title\n          description: Title of the maintenance.\n        message:\n          type: string\n          title: message\n          description: Message describing the maintenance.\n        from:\n          type: string\n          title: from\n          description: Start time of the maintenance window (RFC 3339 format).\n        to:\n          type: string\n          title: to\n          description: End time of the maintenance window (RFC 3339 format).\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the page this maintenance is associated with.\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: IDs of affected page components.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the maintenance was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the maintenance was last updated (RFC 3339 format).\n      title: MaintenanceSummary\n      additionalProperties: false\n      description: MaintenanceSummary represents metadata for a maintenance window (used in list responses).\n    openstatus.maintenance.v1.UpdateMaintenanceRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the maintenance to update (required).\n        title:\n          type:\n            - string\n            - \"null\"\n          title: title\n          maxLength: 256\n          minLength: 1\n          description: New title for the maintenance (optional).\n        message:\n          type:\n            - string\n            - \"null\"\n          title: message\n          description: New message for the maintenance (optional).\n        from:\n          type:\n            - string\n            - \"null\"\n          title: from\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: New start time (RFC 3339 format, optional).\n        to:\n          type:\n            - string\n            - \"null\"\n          title: to\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: New end time (RFC 3339 format, optional).\n        pageId:\n          type:\n            - string\n            - \"null\"\n          title: page_id\n          description: New page ID (optional).\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: New list of page component IDs (optional, replaces existing list).\n      title: UpdateMaintenanceRequest\n      additionalProperties: false\n      description: UpdateMaintenanceRequest is the request to update a maintenance window.\n    openstatus.maintenance.v1.UpdateMaintenanceResponse:\n      type: object\n      properties:\n        maintenance:\n          title: maintenance\n          description: The updated maintenance.\n          $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n      title: UpdateMaintenanceResponse\n      additionalProperties: false\n      description: UpdateMaintenanceResponse is the response after updating a maintenance window.\n    openstatus.monitor.v1.BodyAssertion:\n      type: object\n      properties:\n        target:\n          type: string\n          title: target\n          description: Target value to compare against.\n        comparator:\n          not:\n            enum:\n              - STRING_COMPARATOR_UNSPECIFIED\n          title: comparator\n          description: Comparison operation (required, must not be UNSPECIFIED).\n          $ref: '#/components/schemas/openstatus.monitor.v1.StringComparator'\n      title: BodyAssertion\n      additionalProperties: false\n      description: BodyAssertion defines an assertion for response body content.\n    openstatus.monitor.v1.CreateDNSMonitorRequest:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: Monitor configuration (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n      title: CreateDNSMonitorRequest\n      required:\n        - monitor\n      additionalProperties: false\n      description: CreateDNSMonitorRequest is the request to create a new DNS monitor.\n    openstatus.monitor.v1.CreateDNSMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The created monitor with assigned ID.\n          $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n      title: CreateDNSMonitorResponse\n      additionalProperties: false\n      description: CreateDNSMonitorResponse is the response after creating a DNS monitor.\n    openstatus.monitor.v1.CreateHTTPMonitorRequest:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: Monitor configuration (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n      title: CreateHTTPMonitorRequest\n      required:\n        - monitor\n      additionalProperties: false\n      description: CreateHTTPMonitorRequest is the request to create a new HTTP monitor.\n    openstatus.monitor.v1.CreateHTTPMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The created monitor with assigned ID.\n          $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n      title: CreateHTTPMonitorResponse\n      additionalProperties: false\n      description: CreateHTTPMonitorResponse is the response after creating an HTTP monitor.\n    openstatus.monitor.v1.CreateTCPMonitorRequest:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: Monitor configuration (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n      title: CreateTCPMonitorRequest\n      required:\n        - monitor\n      additionalProperties: false\n      description: CreateTCPMonitorRequest is the request to create a new TCP monitor.\n    openstatus.monitor.v1.CreateTCPMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The created monitor with assigned ID.\n          $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n      title: CreateTCPMonitorResponse\n      additionalProperties: false\n      description: CreateTCPMonitorResponse is the response after creating a TCP monitor.\n    openstatus.monitor.v1.DNSMonitor:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the monitor (output only for create requests).\n        name:\n          type: string\n          examples:\n            - DNS Resolution Check\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Name of the monitor (required, max 256 characters).\n        uri:\n          type: string\n          examples:\n            - example.com\n          title: uri\n          maxLength: 2048\n          minLength: 1\n          description: Domain to resolve (required, max 2048 characters).\n        periodicity:\n          not:\n            enum:\n              - PERIODICITY_UNSPECIFIED\n          title: periodicity\n          description: Check periodicity (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n        timeout:\n          type:\n            - integer\n            - string\n          title: timeout\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Timeout in milliseconds (0-120000, defaults to 45000).\n        degradedAt:\n          type:\n            - integer\n            - string\n            - \"null\"\n          title: degraded_at\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n        retry:\n          type:\n            - integer\n            - string\n          title: retry\n          maximum: 10\n          minimum: 0\n          format: int64\n          description: Number of retry attempts (0-10, defaults to 3).\n        recordAssertions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.RecordAssertion'\n          title: record_assertions\n          maxItems: 10\n          description: DNS record assertions for validation.\n        description:\n          type: string\n          title: description\n          maxLength: 1024\n          description: Description of the monitor (optional).\n        active:\n          type: boolean\n          title: active\n          description: Whether the monitor is active (defaults to false).\n        public:\n          type: boolean\n          title: public\n          description: Whether the monitor is publicly visible (defaults to false).\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          maxItems: 28\n          description: Geographic regions to run checks from.\n        openTelemetry:\n          title: open_telemetry\n          description: OpenTelemetry configuration for exporting metrics.\n          $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n        status:\n          title: status\n          description: Current operational status of the monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n      title: DNSMonitor\n      additionalProperties: false\n      description: DNSMonitor defines the configuration for a DNS monitor.\n    openstatus.monitor.v1.DeleteMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to delete (required).\n      title: DeleteMonitorRequest\n      additionalProperties: false\n      description: DeleteMonitorRequest is the request to delete a monitor.\n    openstatus.monitor.v1.DeleteMonitorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteMonitorResponse\n      additionalProperties: false\n      description: DeleteMonitorResponse is the response after deleting a monitor.\n    openstatus.monitor.v1.GetMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to retrieve (required).\n      title: GetMonitorRequest\n      additionalProperties: false\n      description: GetMonitorRequest is the request to get a single monitor by ID.\n    openstatus.monitor.v1.GetMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The monitor configuration (one of HTTP, TCP, or DNS).\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorConfig'\n      title: GetMonitorResponse\n      additionalProperties: false\n      description: GetMonitorResponse is the response containing the monitor.\n    openstatus.monitor.v1.GetMonitorStatusRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to get status for (required).\n      title: GetMonitorStatusRequest\n      additionalProperties: false\n      description: GetMonitorStatusRequest is the request to get the status of all regions for a monitor.\n    openstatus.monitor.v1.GetMonitorStatusResponse:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Monitor ID.\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.RegionStatus'\n          title: regions\n          description: Status for each region.\n      title: GetMonitorStatusResponse\n      additionalProperties: false\n      description: GetMonitorStatusResponse is the response containing the status of all regions for a monitor.\n    openstatus.monitor.v1.GetMonitorSummaryRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to get summary for (required).\n        timeRange:\n          title: time_range\n          description: Time range for metrics aggregation (defaults to 1 day if unspecified).\n          $ref: '#/components/schemas/openstatus.monitor.v1.TimeRange'\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          maxItems: 28\n          description: Optional filter by regions. If empty, returns metrics for all regions.\n      title: GetMonitorSummaryRequest\n      additionalProperties: false\n      description: GetMonitorSummaryRequest is the request to get aggregated metrics for a monitor.\n    openstatus.monitor.v1.GetMonitorSummaryResponse:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Monitor ID.\n        lastPingAt:\n          type: string\n          title: last_ping_at\n          description: Timestamp of the last check in RFC 3339 format.\n        totalSuccessful:\n          type:\n            - integer\n            - string\n          title: total_successful\n          format: int64\n          description: Total number of successful requests.\n        totalDegraded:\n          type:\n            - integer\n            - string\n          title: total_degraded\n          format: int64\n          description: Total number of degraded requests.\n        totalFailed:\n          type:\n            - integer\n            - string\n          title: total_failed\n          format: int64\n          description: Total number of failed requests.\n        p50:\n          type:\n            - integer\n            - string\n          title: p50\n          format: int64\n          description: 50th percentile (median) latency in milliseconds.\n        p75:\n          type:\n            - integer\n            - string\n          title: p75\n          format: int64\n          description: 75th percentile latency in milliseconds.\n        p90:\n          type:\n            - integer\n            - string\n          title: p90\n          format: int64\n          description: 90th percentile latency in milliseconds.\n        p95:\n          type:\n            - integer\n            - string\n          title: p95\n          format: int64\n          description: 95th percentile latency in milliseconds.\n        p99:\n          type:\n            - integer\n            - string\n          title: p99\n          format: int64\n          description: 99th percentile latency in milliseconds.\n        timeRange:\n          title: time_range\n          description: Time range used for the metrics.\n          $ref: '#/components/schemas/openstatus.monitor.v1.TimeRange'\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          description: Regions included in the metrics.\n      title: GetMonitorSummaryResponse\n      additionalProperties: false\n      description: GetMonitorSummaryResponse is the response containing aggregated metrics for a monitor.\n    openstatus.monitor.v1.HTTPMethod:\n      type: string\n      title: HTTPMethod\n      enum:\n        - HTTP_METHOD_UNSPECIFIED\n        - HTTP_METHOD_GET\n        - HTTP_METHOD_POST\n        - HTTP_METHOD_HEAD\n        - HTTP_METHOD_PUT\n        - HTTP_METHOD_PATCH\n        - HTTP_METHOD_DELETE\n        - HTTP_METHOD_TRACE\n        - HTTP_METHOD_CONNECT\n        - HTTP_METHOD_OPTIONS\n      description: HTTP methods supported for monitors.\n    openstatus.monitor.v1.HTTPMonitor:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the monitor (output only for create requests).\n        name:\n          type: string\n          examples:\n            - Production API Health Check\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Name of the monitor (required, max 256 characters).\n        url:\n          type: string\n          examples:\n            - https://api.example.com/health\n          title: url\n          maxLength: 2048\n          minLength: 1\n          format: uri\n          description: URL to monitor (required, max 2048 characters).\n        periodicity:\n          not:\n            enum:\n              - PERIODICITY_UNSPECIFIED\n          title: periodicity\n          description: Check periodicity (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n        method:\n          not:\n            enum:\n              - HTTP_METHOD_UNSPECIFIED\n          title: method\n          description: HTTP method to use (defaults to GET).\n          $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMethod'\n        body:\n          type: string\n          examples:\n            - map[key:value]\n          title: body\n          description: Request body (optional).\n        timeout:\n          type:\n            - integer\n            - string\n          title: timeout\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Timeout in milliseconds (0-120000, defaults to 45000).\n        degradedAt:\n          type:\n            - integer\n            - string\n            - \"null\"\n          title: degraded_at\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n        retry:\n          type:\n            - integer\n            - string\n          title: retry\n          maximum: 10\n          minimum: 0\n          format: int64\n          description: Number of retry attempts (0-10, defaults to 3).\n        followRedirects:\n          type:\n            - boolean\n            - \"null\"\n          title: follow_redirects\n          description: Whether to follow HTTP redirects (defaults to true when not specified).\n        headers:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n          title: headers\n          maxItems: 20\n          description: Custom headers for the request.\n        statusCodeAssertions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.StatusCodeAssertion'\n          title: status_code_assertions\n          maxItems: 10\n          description: Status code assertions for the response.\n        bodyAssertions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.BodyAssertion'\n          title: body_assertions\n          maxItems: 10\n          description: Body content assertions for the response.\n        headerAssertions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.HeaderAssertion'\n          title: header_assertions\n          maxItems: 10\n          description: Header assertions for the response.\n        description:\n          type: string\n          title: description\n          maxLength: 1024\n          description: Description of the monitor (optional).\n        active:\n          type: boolean\n          title: active\n          description: Whether the monitor is active (defaults to false).\n        public:\n          type: boolean\n          title: public\n          description: Whether the monitor is publicly visible (defaults to false).\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          maxItems: 28\n          description: Geographic regions to run checks from.\n        openTelemetry:\n          title: open_telemetry\n          description: OpenTelemetry configuration for exporting metrics.\n          $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n        status:\n          title: status\n          description: Current operational status of the monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n      title: HTTPMonitor\n      additionalProperties: false\n      description: HTTPMonitor defines the configuration for an HTTP monitor.\n    openstatus.monitor.v1.HeaderAssertion:\n      type: object\n      properties:\n        target:\n          type: string\n          title: target\n          description: Target value to compare against.\n        comparator:\n          not:\n            enum:\n              - STRING_COMPARATOR_UNSPECIFIED\n          title: comparator\n          description: Comparison operation (required, must not be UNSPECIFIED).\n          $ref: '#/components/schemas/openstatus.monitor.v1.StringComparator'\n        key:\n          type: string\n          title: key\n          minLength: 1\n          description: Header key to check (required).\n      title: HeaderAssertion\n      additionalProperties: false\n      description: HeaderAssertion defines an assertion for response headers.\n    openstatus.monitor.v1.Headers:\n      type: object\n      properties:\n        key:\n          type: string\n          examples:\n            - Authorization\n          title: key\n          minLength: 1\n          description: Header name.\n        value:\n          type: string\n          examples:\n            - Bearer token123\n          title: value\n          description: Header value.\n      title: Headers\n      additionalProperties: false\n      description: Headers represents a key-value pair for HTTP headers.\n    openstatus.monitor.v1.ListMonitorsRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of monitors to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of monitors to skip for pagination (defaults to 0).\n      title: ListMonitorsRequest\n      additionalProperties: false\n      description: ListMonitorsRequest is the request to list monitors.\n    openstatus.monitor.v1.ListMonitorsResponse:\n      type: object\n      properties:\n        httpMonitors:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n          title: http_monitors\n          description: HTTP monitors in the workspace.\n        tcpMonitors:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n          title: tcp_monitors\n          description: TCP monitors in the workspace.\n        dnsMonitors:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n          title: dns_monitors\n          description: DNS monitors in the workspace.\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of monitors across all types.\n      title: ListMonitorsResponse\n      additionalProperties: false\n      description: ListMonitorsResponse is the response containing a list of monitors.\n    openstatus.monitor.v1.MonitorConfig:\n      type: object\n      oneOf:\n        - properties:\n            dns:\n              title: dns\n              description: DNS monitor configuration.\n              $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n          title: dns\n          required:\n            - dns\n        - properties:\n            http:\n              title: http\n              description: HTTP monitor configuration.\n              $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n          title: http\n          required:\n            - http\n        - properties:\n            tcp:\n              title: tcp\n              description: TCP monitor configuration.\n              $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n          title: tcp\n          required:\n            - tcp\n      title: MonitorConfig\n      additionalProperties: false\n      description: MonitorConfig represents the type-specific configuration for a monitor.\n    openstatus.monitor.v1.MonitorStatus:\n      type: string\n      title: MonitorStatus\n      enum:\n        - MONITOR_STATUS_UNSPECIFIED\n        - MONITOR_STATUS_ACTIVE\n        - MONITOR_STATUS_DEGRADED\n        - MONITOR_STATUS_ERROR\n      description: MonitorStatus represents the operational status of a monitor.\n    openstatus.monitor.v1.NumberComparator:\n      type: string\n      title: NumberComparator\n      enum:\n        - NUMBER_COMPARATOR_UNSPECIFIED\n        - NUMBER_COMPARATOR_EQUAL\n        - NUMBER_COMPARATOR_NOT_EQUAL\n        - NUMBER_COMPARATOR_GREATER_THAN\n        - NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\n        - NUMBER_COMPARATOR_LESS_THAN\n        - NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\n      description: NumberComparator defines comparison operations for numeric values.\n    openstatus.monitor.v1.OpenTelemetryConfig:\n      type: object\n      properties:\n        endpoint:\n          type: string\n          title: endpoint\n          maxLength: 2048\n          description: OTEL endpoint URL.\n        headers:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n          title: headers\n          maxItems: 20\n          description: Custom headers for OTEL requests.\n      title: OpenTelemetryConfig\n      additionalProperties: false\n      description: OpenTelemetry configuration for exporting metrics.\n    openstatus.monitor.v1.Periodicity:\n      type: string\n      title: Periodicity\n      enum:\n        - PERIODICITY_UNSPECIFIED\n        - PERIODICITY_30S\n        - PERIODICITY_1M\n        - PERIODICITY_5M\n        - PERIODICITY_10M\n        - PERIODICITY_30M\n        - PERIODICITY_1H\n      description: Monitor periodicity options.\n    openstatus.monitor.v1.RecordAssertion:\n      type: object\n      properties:\n        record:\n          type: string\n          title: record\n          enum:\n            - A\n            - AAAA\n            - CNAME\n            - MX\n            - TXT\n          description: DNS record type (e.g., \"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\").\n        comparator:\n          not:\n            enum:\n              - RECORD_COMPARATOR_UNSPECIFIED\n          title: comparator\n          description: Comparison operation (required, must not be UNSPECIFIED).\n          $ref: '#/components/schemas/openstatus.monitor.v1.RecordComparator'\n        target:\n          type: string\n          title: target\n          description: Target value to compare against.\n      title: RecordAssertion\n      additionalProperties: false\n      description: RecordAssertion defines an assertion for DNS records.\n    openstatus.monitor.v1.RecordComparator:\n      type: string\n      title: RecordComparator\n      enum:\n        - RECORD_COMPARATOR_UNSPECIFIED\n        - RECORD_COMPARATOR_EQUAL\n        - RECORD_COMPARATOR_NOT_EQUAL\n        - RECORD_COMPARATOR_CONTAINS\n        - RECORD_COMPARATOR_NOT_CONTAINS\n      description: RecordComparator defines comparison operations for DNS records.\n    openstatus.monitor.v1.Region:\n      type: string\n      title: Region\n      enum:\n        - REGION_UNSPECIFIED\n        - REGION_FLY_AMS\n        - REGION_FLY_ARN\n        - REGION_FLY_BOM\n        - REGION_FLY_CDG\n        - REGION_FLY_DFW\n        - REGION_FLY_EWR\n        - REGION_FLY_FRA\n        - REGION_FLY_GRU\n        - REGION_FLY_IAD\n        - REGION_FLY_JNB\n        - REGION_FLY_LAX\n        - REGION_FLY_LHR\n        - REGION_FLY_NRT\n        - REGION_FLY_ORD\n        - REGION_FLY_SJC\n        - REGION_FLY_SIN\n        - REGION_FLY_SYD\n        - REGION_FLY_YYZ\n        - REGION_KOYEB_FRA\n        - REGION_KOYEB_PAR\n        - REGION_KOYEB_SFO\n        - REGION_KOYEB_SIN\n        - REGION_KOYEB_TYO\n        - REGION_KOYEB_WAS\n        - REGION_RAILWAY_US_WEST2\n        - REGION_RAILWAY_US_EAST4\n        - REGION_RAILWAY_EUROPE_WEST4\n        - REGION_RAILWAY_ASIA_SOUTHEAST1\n      description: Geographic regions where monitors can run checks from.\n    openstatus.monitor.v1.RegionStatus:\n      type: object\n      properties:\n        region:\n          title: region\n          description: The region identifier.\n          $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n        status:\n          title: status\n          description: The status of the monitor in this region.\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n      title: RegionStatus\n      additionalProperties: false\n      description: RegionStatus represents the status of a monitor in a specific region.\n    openstatus.monitor.v1.StatusCodeAssertion:\n      type: object\n      properties:\n        target:\n          type:\n            - integer\n            - string\n          title: target\n          maximum: 599\n          minimum: 100\n          format: int64\n          description: Target status code to compare against (100-599).\n        comparator:\n          not:\n            enum:\n              - NUMBER_COMPARATOR_UNSPECIFIED\n          title: comparator\n          description: Comparison operation (required, must not be UNSPECIFIED).\n          $ref: '#/components/schemas/openstatus.monitor.v1.NumberComparator'\n      title: StatusCodeAssertion\n      additionalProperties: false\n      description: StatusCodeAssertion defines an assertion for HTTP status codes.\n    openstatus.monitor.v1.StringComparator:\n      type: string\n      title: StringComparator\n      enum:\n        - STRING_COMPARATOR_UNSPECIFIED\n        - STRING_COMPARATOR_CONTAINS\n        - STRING_COMPARATOR_NOT_CONTAINS\n        - STRING_COMPARATOR_EQUAL\n        - STRING_COMPARATOR_NOT_EQUAL\n        - STRING_COMPARATOR_EMPTY\n        - STRING_COMPARATOR_NOT_EMPTY\n        - STRING_COMPARATOR_GREATER_THAN\n        - STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\n        - STRING_COMPARATOR_LESS_THAN\n        - STRING_COMPARATOR_LESS_THAN_OR_EQUAL\n      description: StringComparator defines comparison operations for string values.\n    openstatus.monitor.v1.TCPMonitor:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the monitor (output only for create requests).\n        name:\n          type: string\n          examples:\n            - Database Connection Check\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Name of the monitor (required, max 256 characters).\n        uri:\n          type: string\n          examples:\n            - tcp://db.example.com:5432\n          title: uri\n          maxLength: 2048\n          minLength: 1\n          description: URI to monitor in format \"host:port\" (required, max 2048 characters).\n        periodicity:\n          not:\n            enum:\n              - PERIODICITY_UNSPECIFIED\n          title: periodicity\n          description: Check periodicity (required).\n          $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n        timeout:\n          type:\n            - integer\n            - string\n          title: timeout\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Timeout in milliseconds (0-120000, defaults to 45000).\n        degradedAt:\n          type:\n            - integer\n            - string\n            - \"null\"\n          title: degraded_at\n          maximum: 120000\n          minimum: 0\n          format: int64\n          description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n        retry:\n          type:\n            - integer\n            - string\n          title: retry\n          maximum: 10\n          minimum: 0\n          format: int64\n          description: Number of retry attempts (0-10, defaults to 3).\n        description:\n          type: string\n          title: description\n          maxLength: 1024\n          description: Description of the monitor (optional).\n        active:\n          type: boolean\n          title: active\n          description: Whether the monitor is active (defaults to false).\n        public:\n          type: boolean\n          title: public\n          description: Whether the monitor is publicly visible (defaults to false).\n        regions:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n          title: regions\n          maxItems: 28\n          description: Geographic regions to run checks from.\n        openTelemetry:\n          title: open_telemetry\n          description: OpenTelemetry configuration for exporting metrics.\n          $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n        status:\n          title: status\n          description: Current operational status of the monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n      title: TCPMonitor\n      additionalProperties: false\n      description: TCPMonitor defines the configuration for a TCP monitor.\n    openstatus.monitor.v1.TimeRange:\n      type: string\n      title: TimeRange\n      enum:\n        - TIME_RANGE_UNSPECIFIED\n        - TIME_RANGE_1D\n        - TIME_RANGE_7D\n        - TIME_RANGE_14D\n      description: TimeRange represents the time period for metrics aggregation.\n    openstatus.monitor.v1.TriggerMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to trigger (required).\n      title: TriggerMonitorRequest\n      additionalProperties: false\n      description: TriggerMonitorRequest is the request to trigger a monitor check.\n    openstatus.monitor.v1.TriggerMonitorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the trigger was successful.\n      title: TriggerMonitorResponse\n      additionalProperties: false\n      description: TriggerMonitorResponse is the response after triggering a monitor.\n    openstatus.monitor.v1.UpdateDNSMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to update (required).\n        monitor:\n          oneOf:\n            - $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n            - type: \"null\"\n          title: monitor\n          description: Updated monitor configuration (all fields optional for partial updates).\n      title: UpdateDNSMonitorRequest\n      additionalProperties: false\n      description: UpdateDNSMonitorRequest is the request to update an existing DNS monitor.\n    openstatus.monitor.v1.UpdateDNSMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The updated monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n      title: UpdateDNSMonitorResponse\n      additionalProperties: false\n      description: UpdateDNSMonitorResponse is the response after updating a DNS monitor.\n    openstatus.monitor.v1.UpdateHTTPMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to update (required).\n        monitor:\n          oneOf:\n            - $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n            - type: \"null\"\n          title: monitor\n          description: Updated monitor configuration (all fields optional for partial updates).\n      title: UpdateHTTPMonitorRequest\n      additionalProperties: false\n      description: UpdateHTTPMonitorRequest is the request to update an existing HTTP monitor.\n    openstatus.monitor.v1.UpdateHTTPMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The updated monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n      title: UpdateHTTPMonitorResponse\n      additionalProperties: false\n      description: UpdateHTTPMonitorResponse is the response after updating an HTTP monitor.\n    openstatus.monitor.v1.UpdateTCPMonitorRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Monitor ID to update (required).\n        monitor:\n          oneOf:\n            - $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n            - type: \"null\"\n          title: monitor\n          description: Updated monitor configuration (all fields optional for partial updates).\n      title: UpdateTCPMonitorRequest\n      additionalProperties: false\n      description: UpdateTCPMonitorRequest is the request to update an existing TCP monitor.\n    openstatus.monitor.v1.UpdateTCPMonitorResponse:\n      type: object\n      properties:\n        monitor:\n          title: monitor\n          description: The updated monitor.\n          $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n      title: UpdateTCPMonitorResponse\n      additionalProperties: false\n      description: UpdateTCPMonitorResponse is the response after updating a TCP monitor.\n    openstatus.notification.v1.CheckNotificationLimitRequest:\n      type: object\n      title: CheckNotificationLimitRequest\n      additionalProperties: false\n      description: CheckNotificationLimitRequest is the request to check notification limits.\n    openstatus.notification.v1.CheckNotificationLimitResponse:\n      type: object\n      properties:\n        limitReached:\n          type: boolean\n          title: limit_reached\n          description: Whether the workspace has reached its notification limit.\n        currentCount:\n          type: integer\n          title: current_count\n          format: int32\n          description: Current number of notification channels.\n        maxCount:\n          type: integer\n          title: max_count\n          format: int32\n          description: Maximum allowed notification channels.\n      title: CheckNotificationLimitResponse\n      additionalProperties: false\n      description: CheckNotificationLimitResponse is the response containing limit information.\n    openstatus.notification.v1.CreateNotificationRequest:\n      type: object\n      properties:\n        name:\n          type: string\n          examples:\n            - Slack Ops Channel\n          title: name\n          minLength: 1\n          description: Display name for the notification channel.\n        provider:\n          not:\n            enum:\n              - NOTIFICATION_PROVIDER_UNSPECIFIED\n          title: provider\n          description: Provider type.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n        data:\n          title: data\n          description: Provider-specific configuration.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n        monitorIds:\n          type: array\n          items:\n            type: string\n          title: monitor_ids\n          description: IDs of monitors to associate with this notification.\n      title: CreateNotificationRequest\n      required:\n        - data\n      additionalProperties: false\n      description: CreateNotificationRequest is the request to create a new notification channel.\n    openstatus.notification.v1.CreateNotificationResponse:\n      type: object\n      properties:\n        notification:\n          title: notification\n          description: The created notification channel.\n          $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n      title: CreateNotificationResponse\n      additionalProperties: false\n      description: CreateNotificationResponse is the response after creating a notification channel.\n    openstatus.notification.v1.DeleteNotificationRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Notification ID to delete (required).\n      title: DeleteNotificationRequest\n      additionalProperties: false\n      description: DeleteNotificationRequest is the request to delete a notification channel.\n    openstatus.notification.v1.DeleteNotificationResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteNotificationResponse\n      additionalProperties: false\n      description: DeleteNotificationResponse is the response after deleting a notification channel.\n    openstatus.notification.v1.DiscordData:\n      type: object\n      properties:\n        webhookUrl:\n          type: string\n          examples:\n            - https://discord.com/api/webhooks/123/abc\n          title: webhook_url\n          format: uri\n          description: Discord webhook URL.\n      title: DiscordData\n      additionalProperties: false\n      description: DiscordData contains configuration for Discord notifications.\n    openstatus.notification.v1.EmailData:\n      type: object\n      properties:\n        email:\n          type: string\n          examples:\n            - ops-team@example.com\n          title: email\n          format: email\n          description: Email address to send notifications to.\n      title: EmailData\n      additionalProperties: false\n      description: EmailData contains configuration for email notifications.\n    openstatus.notification.v1.GetNotificationRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Notification ID to retrieve (required).\n      title: GetNotificationRequest\n      additionalProperties: false\n      description: GetNotificationRequest is the request to get a notification channel.\n    openstatus.notification.v1.GetNotificationResponse:\n      type: object\n      properties:\n        notification:\n          title: notification\n          description: The notification channel.\n          $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n      title: GetNotificationResponse\n      additionalProperties: false\n      description: GetNotificationResponse is the response containing the notification channel.\n    openstatus.notification.v1.GoogleChatData:\n      type: object\n      properties:\n        webhookUrl:\n          type: string\n          title: webhook_url\n          format: uri\n          description: Google Chat webhook URL.\n      title: GoogleChatData\n      additionalProperties: false\n      description: GoogleChatData contains configuration for Google Chat notifications.\n    openstatus.notification.v1.GrafanaOncallData:\n      type: object\n      properties:\n        webhookUrl:\n          type: string\n          title: webhook_url\n          format: uri\n          description: Grafana OnCall webhook URL.\n      title: GrafanaOncallData\n      additionalProperties: false\n      description: GrafanaOncallData contains configuration for Grafana OnCall notifications.\n    openstatus.notification.v1.ListNotificationsRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of notifications to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of notifications to skip for pagination (defaults to 0).\n      title: ListNotificationsRequest\n      additionalProperties: false\n      description: ListNotificationsRequest is the request to list notification channels.\n    openstatus.notification.v1.ListNotificationsResponse:\n      type: object\n      properties:\n        notifications:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.notification.v1.NotificationSummary'\n          title: notifications\n          description: Notification channel summaries.\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of notification channels.\n      title: ListNotificationsResponse\n      additionalProperties: false\n      description: ListNotificationsResponse is the response containing notification channels.\n    openstatus.notification.v1.Notification:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the notification.\n        name:\n          type: string\n          title: name\n          description: Display name for the notification channel.\n        provider:\n          title: provider\n          description: Provider type.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n        data:\n          title: data\n          description: Provider-specific configuration.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n        monitorIds:\n          type: array\n          items:\n            type: string\n          title: monitor_ids\n          description: IDs of monitors associated with this notification.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the notification was created (RFC 3339).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the notification was last updated (RFC 3339).\n      title: Notification\n      additionalProperties: false\n      description: Notification represents a notification channel with full details.\n    openstatus.notification.v1.NotificationData:\n      type: object\n      oneOf:\n        - properties:\n            discord:\n              title: discord\n              description: Discord configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.DiscordData'\n          title: discord\n          required:\n            - discord\n        - properties:\n            email:\n              title: email\n              description: Email configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.EmailData'\n          title: email\n          required:\n            - email\n        - properties:\n            googleChat:\n              title: google_chat\n              description: Google Chat configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.GoogleChatData'\n          title: google_chat\n          required:\n            - googleChat\n        - properties:\n            grafanaOncall:\n              title: grafana_oncall\n              description: Grafana OnCall configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.GrafanaOncallData'\n          title: grafana_oncall\n          required:\n            - grafanaOncall\n        - properties:\n            ntfy:\n              title: ntfy\n              description: Ntfy configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.NtfyData'\n          title: ntfy\n          required:\n            - ntfy\n        - properties:\n            opsgenie:\n              title: opsgenie\n              description: Opsgenie configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.OpsgenieData'\n          title: opsgenie\n          required:\n            - opsgenie\n        - properties:\n            pagerduty:\n              title: pagerduty\n              description: PagerDuty configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.PagerDutyData'\n          title: pagerduty\n          required:\n            - pagerduty\n        - properties:\n            slack:\n              title: slack\n              description: Slack configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.SlackData'\n          title: slack\n          required:\n            - slack\n        - properties:\n            sms:\n              title: sms\n              description: SMS configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.SmsData'\n          title: sms\n          required:\n            - sms\n        - properties:\n            telegram:\n              title: telegram\n              description: Telegram configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.TelegramData'\n          title: telegram\n          required:\n            - telegram\n        - properties:\n            webhook:\n              title: webhook\n              description: Webhook configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.WebhookData'\n          title: webhook\n          required:\n            - webhook\n        - properties:\n            whatsapp:\n              title: whatsapp\n              description: WhatsApp configuration.\n              $ref: '#/components/schemas/openstatus.notification.v1.WhatsappData'\n          title: whatsapp\n          required:\n            - whatsapp\n      title: NotificationData\n      additionalProperties: false\n      description: NotificationData is a union of provider-specific configuration.\n    openstatus.notification.v1.NotificationProvider:\n      type: string\n      title: NotificationProvider\n      enum:\n        - NOTIFICATION_PROVIDER_UNSPECIFIED\n        - NOTIFICATION_PROVIDER_DISCORD\n        - NOTIFICATION_PROVIDER_EMAIL\n        - NOTIFICATION_PROVIDER_GOOGLE_CHAT\n        - NOTIFICATION_PROVIDER_GRAFANA_ONCALL\n        - NOTIFICATION_PROVIDER_NTFY\n        - NOTIFICATION_PROVIDER_PAGERDUTY\n        - NOTIFICATION_PROVIDER_OPSGENIE\n        - NOTIFICATION_PROVIDER_SLACK\n        - NOTIFICATION_PROVIDER_SMS\n        - NOTIFICATION_PROVIDER_TELEGRAM\n        - NOTIFICATION_PROVIDER_WEBHOOK\n        - NOTIFICATION_PROVIDER_WHATSAPP\n      description: NotificationProvider represents the supported notification channel types.\n    openstatus.notification.v1.NotificationSummary:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the notification.\n        name:\n          type: string\n          title: name\n          description: Display name for the notification channel.\n        provider:\n          title: provider\n          description: Provider type.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n        monitorCount:\n          type: integer\n          title: monitor_count\n          format: int32\n          description: Number of monitors associated with this notification.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the notification was created (RFC 3339).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the notification was last updated (RFC 3339).\n      title: NotificationSummary\n      additionalProperties: false\n      description: NotificationSummary represents a notification channel summary for list responses.\n    openstatus.notification.v1.NtfyData:\n      type: object\n      properties:\n        topic:\n          type: string\n          examples:\n            - openstatus-alerts\n          title: topic\n          minLength: 1\n          description: Ntfy topic to publish to.\n        serverUrl:\n          type: string\n          title: server_url\n          description: Ntfy server URL (defaults to https://ntfy.sh).\n        token:\n          type:\n            - string\n            - \"null\"\n          title: token\n          description: Optional authentication token.\n      title: NtfyData\n      additionalProperties: false\n      description: NtfyData contains configuration for Ntfy notifications.\n    openstatus.notification.v1.OpsgenieData:\n      type: object\n      properties:\n        apiKey:\n          type: string\n          title: api_key\n          minLength: 1\n          description: Opsgenie API key.\n        region:\n          title: region\n          description: Opsgenie region.\n          $ref: '#/components/schemas/openstatus.notification.v1.OpsgenieRegion'\n      title: OpsgenieData\n      additionalProperties: false\n      description: OpsgenieData contains configuration for Opsgenie notifications.\n    openstatus.notification.v1.OpsgenieRegion:\n      type: string\n      title: OpsgenieRegion\n      enum:\n        - OPSGENIE_REGION_UNSPECIFIED\n        - OPSGENIE_REGION_US\n        - OPSGENIE_REGION_EU\n      description: OpsgenieRegion represents the Opsgenie API region.\n    openstatus.notification.v1.PagerDutyData:\n      type: object\n      properties:\n        integrationKey:\n          type: string\n          examples:\n            - a1b2c3d4e5f6g7h8i9j0\n          title: integration_key\n          minLength: 1\n          description: PagerDuty integration key.\n      title: PagerDutyData\n      additionalProperties: false\n      description: PagerDutyData contains configuration for PagerDuty notifications.\n    openstatus.notification.v1.SendTestNotificationRequest:\n      type: object\n      properties:\n        provider:\n          not:\n            enum:\n              - NOTIFICATION_PROVIDER_UNSPECIFIED\n          title: provider\n          description: Provider type.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n        data:\n          title: data\n          description: Provider-specific configuration.\n          $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n      title: SendTestNotificationRequest\n      required:\n        - data\n      additionalProperties: false\n      description: SendTestNotificationRequest is the request to send a test notification.\n    openstatus.notification.v1.SendTestNotificationResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the test was successful.\n        errorMessage:\n          type:\n            - string\n            - \"null\"\n          title: error_message\n          description: Optional error message if the test failed.\n      title: SendTestNotificationResponse\n      additionalProperties: false\n      description: SendTestNotificationResponse is the response after sending a test notification.\n    openstatus.notification.v1.SlackData:\n      type: object\n      properties:\n        webhookUrl:\n          type: string\n          examples:\n            - https://hooks.slack.com/services/T00/B00/xxx\n          title: webhook_url\n          format: uri\n          description: Slack webhook URL.\n      title: SlackData\n      additionalProperties: false\n      description: SlackData contains configuration for Slack notifications.\n    openstatus.notification.v1.SmsData:\n      type: object\n      properties:\n        phoneNumber:\n          type: string\n          examples:\n            - \"+14155551234\"\n          title: phone_number\n          minLength: 1\n          description: Phone number to send SMS to.\n      title: SmsData\n      additionalProperties: false\n      description: SmsData contains configuration for SMS notifications.\n    openstatus.notification.v1.TelegramData:\n      type: object\n      properties:\n        chatId:\n          type: string\n          examples:\n            - \"-1001234567890\"\n          title: chat_id\n          minLength: 1\n          description: Telegram chat ID.\n      title: TelegramData\n      additionalProperties: false\n      description: TelegramData contains configuration for Telegram notifications.\n    openstatus.notification.v1.UpdateNotificationRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: Notification ID to update (required).\n        name:\n          type:\n            - string\n            - \"null\"\n          title: name\n          description: Updated display name.\n        data:\n          oneOf:\n            - $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n            - type: \"null\"\n          title: data\n          description: Updated provider-specific configuration.\n        monitorIds:\n          type: array\n          items:\n            type: string\n          title: monitor_ids\n          description: Updated monitor IDs to associate.\n      title: UpdateNotificationRequest\n      additionalProperties: false\n      description: UpdateNotificationRequest is the request to update a notification channel.\n    openstatus.notification.v1.UpdateNotificationResponse:\n      type: object\n      properties:\n        notification:\n          title: notification\n          description: The updated notification channel.\n          $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n      title: UpdateNotificationResponse\n      additionalProperties: false\n      description: UpdateNotificationResponse is the response after updating a notification channel.\n    openstatus.notification.v1.WebhookData:\n      type: object\n      properties:\n        endpoint:\n          type: string\n          examples:\n            - https://api.example.com/webhooks/openstatus\n          title: endpoint\n          format: uri\n          description: Webhook endpoint URL.\n        headers:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.notification.v1.WebhookHeader'\n          title: headers\n          description: Optional custom headers.\n      title: WebhookData\n      additionalProperties: false\n      description: WebhookData contains configuration for custom webhook notifications.\n    openstatus.notification.v1.WebhookHeader:\n      type: object\n      properties:\n        key:\n          type: string\n          title: key\n          minLength: 1\n          description: Header name.\n        value:\n          type: string\n          title: value\n          description: Header value.\n      title: WebhookHeader\n      additionalProperties: false\n      description: WebhookHeader represents a custom header for webhook requests.\n    openstatus.notification.v1.WhatsappData:\n      type: object\n      properties:\n        phoneNumber:\n          type: string\n          title: phone_number\n          minLength: 1\n          description: Phone number to send WhatsApp messages to.\n      title: WhatsappData\n      additionalProperties: false\n      description: WhatsappData contains configuration for WhatsApp notifications.\n    openstatus.status_page.v1.AddMonitorComponentRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to add the component to (required).\n        monitorId:\n          type: string\n          title: monitor_id\n          minLength: 1\n          description: ID of the monitor to associate with this component (required).\n        name:\n          type:\n            - string\n            - \"null\"\n          title: name\n          maxLength: 256\n          description: Display name for the component (optional, defaults to monitor name).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: Description of the component (optional).\n        order:\n          type:\n            - integer\n            - \"null\"\n          title: order\n          format: int32\n          description: Display order of the component (optional).\n        groupId:\n          type:\n            - string\n            - \"null\"\n          title: group_id\n          description: ID of the group to add this component to (optional).\n      title: AddMonitorComponentRequest\n      additionalProperties: false\n      description: AddMonitorComponentRequest is the request to add a monitor-based component to a status page.\n    openstatus.status_page.v1.AddMonitorComponentResponse:\n      type: object\n      properties:\n        component:\n          title: component\n          description: The created component.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n      title: AddMonitorComponentResponse\n      additionalProperties: false\n      description: AddMonitorComponentResponse is the response after adding a monitor component.\n    openstatus.status_page.v1.AddStaticComponentRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to add the component to (required).\n        name:\n          type: string\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Display name for the component (required).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: Description of the component (optional).\n        order:\n          type:\n            - integer\n            - \"null\"\n          title: order\n          format: int32\n          description: Display order of the component (optional).\n        groupId:\n          type:\n            - string\n            - \"null\"\n          title: group_id\n          description: ID of the group to add this component to (optional).\n      title: AddStaticComponentRequest\n      additionalProperties: false\n      description: AddStaticComponentRequest is the request to add a static component to a status page.\n    openstatus.status_page.v1.AddStaticComponentResponse:\n      type: object\n      properties:\n        component:\n          title: component\n          description: The created component.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n      title: AddStaticComponentResponse\n      additionalProperties: false\n      description: AddStaticComponentResponse is the response after adding a static component.\n    openstatus.status_page.v1.ComponentStatus:\n      type: object\n      properties:\n        componentId:\n          type: string\n          title: component_id\n          description: ID of the component.\n        status:\n          title: status\n          description: Current status of the component.\n          $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n      title: ComponentStatus\n      additionalProperties: false\n      description: ComponentStatus represents the status of a single component.\n    openstatus.status_page.v1.CreateComponentGroupRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to create the group in (required).\n        name:\n          type: string\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: Display name for the group (required).\n      title: CreateComponentGroupRequest\n      additionalProperties: false\n      description: CreateComponentGroupRequest is the request to create a new component group.\n    openstatus.status_page.v1.CreateComponentGroupResponse:\n      type: object\n      properties:\n        group:\n          title: group\n          description: The created component group.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n      title: CreateComponentGroupResponse\n      additionalProperties: false\n      description: CreateComponentGroupResponse is the response after creating a component group.\n    openstatus.status_page.v1.CreateStatusPageRequest:\n      type: object\n      properties:\n        title:\n          type: string\n          examples:\n            - Acme Corp Status\n          title: title\n          maxLength: 256\n          minLength: 1\n          description: Title of the status page (required).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: Description of the status page (optional).\n        slug:\n          type: string\n          examples:\n            - my-status-page\n          title: slug\n          maxLength: 256\n          minLength: 1\n          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$\n          description: URL-friendly slug for the status page (required). Must be lowercase alphanumeric with hyphens.\n        homepageUrl:\n          type:\n            - string\n            - \"null\"\n          examples:\n            - https://www.example.com\n          title: homepage_url\n          description: URL to the homepage (optional).\n        contactUrl:\n          type:\n            - string\n            - \"null\"\n          title: contact_url\n          description: URL to the contact page (optional).\n      title: CreateStatusPageRequest\n      additionalProperties: false\n      description: CreateStatusPageRequest is the request to create a new status page.\n    openstatus.status_page.v1.CreateStatusPageResponse:\n      type: object\n      properties:\n        statusPage:\n          title: status_page\n          description: The created status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n      title: CreateStatusPageResponse\n      additionalProperties: false\n      description: CreateStatusPageResponse is the response after creating a status page.\n    openstatus.status_page.v1.DeleteComponentGroupRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the component group to delete (required).\n      title: DeleteComponentGroupRequest\n      additionalProperties: false\n      description: DeleteComponentGroupRequest is the request to delete a component group.\n    openstatus.status_page.v1.DeleteComponentGroupResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteComponentGroupResponse\n      additionalProperties: false\n      description: DeleteComponentGroupResponse is the response after deleting a component group.\n    openstatus.status_page.v1.DeleteStatusPageRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status page to delete (required).\n      title: DeleteStatusPageRequest\n      additionalProperties: false\n      description: DeleteStatusPageRequest is the request to delete a status page.\n    openstatus.status_page.v1.DeleteStatusPageResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteStatusPageResponse\n      additionalProperties: false\n      description: DeleteStatusPageResponse is the response after deleting a status page.\n    openstatus.status_page.v1.GetOverallStatusRequest:\n      type: object\n      oneOf:\n        - properties:\n            id:\n              type: string\n              title: id\n              description: ID of the status page.\n          title: id\n          required:\n            - id\n        - properties:\n            slug:\n              type: string\n              title: slug\n              description: Slug of the status page.\n          title: slug\n          required:\n            - slug\n      title: GetOverallStatusRequest\n      additionalProperties: false\n      description: GetOverallStatusRequest is the request to get the overall status of a status page.\n    openstatus.status_page.v1.GetOverallStatusResponse:\n      type: object\n      properties:\n        overallStatus:\n          title: overall_status\n          description: Aggregated status across all components.\n          $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n        componentStatuses:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.ComponentStatus'\n          title: component_statuses\n          description: Status of individual components.\n      title: GetOverallStatusResponse\n      additionalProperties: false\n      description: GetOverallStatusResponse is the response containing the overall status and individual component statuses.\n    openstatus.status_page.v1.GetStatusPageContentRequest:\n      type: object\n      oneOf:\n        - properties:\n            id:\n              type: string\n              title: id\n              description: ID of the status page.\n          title: id\n          required:\n            - id\n        - properties:\n            slug:\n              type: string\n              title: slug\n              description: Slug of the status page.\n          title: slug\n          required:\n            - slug\n      title: GetStatusPageContentRequest\n      additionalProperties: false\n      description: GetStatusPageContentRequest is the request to get the full content of a status page.\n    openstatus.status_page.v1.GetStatusPageContentResponse:\n      type: object\n      properties:\n        statusPage:\n          title: status_page\n          description: The status page details.\n          $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n        components:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n          title: components\n          description: Components on the status page.\n        groups:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n          title: groups\n          description: Component groups on the status page.\n        statusReports:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n          title: status_reports\n          description: Active and recent status reports.\n        maintenances:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n          title: maintenances\n          description: Scheduled maintenances.\n      title: GetStatusPageContentResponse\n      additionalProperties: false\n      description: GetStatusPageContentResponse is the response containing the full status page content.\n    openstatus.status_page.v1.GetStatusPageRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status page to retrieve (required).\n      title: GetStatusPageRequest\n      additionalProperties: false\n      description: GetStatusPageRequest is the request to get a status page by ID.\n    openstatus.status_page.v1.GetStatusPageResponse:\n      type: object\n      properties:\n        statusPage:\n          title: status_page\n          description: The requested status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n      title: GetStatusPageResponse\n      additionalProperties: false\n      description: GetStatusPageResponse is the response containing the status page.\n    openstatus.status_page.v1.ListStatusPagesRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of pages to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of pages to skip for pagination (defaults to 0).\n      title: ListStatusPagesRequest\n      additionalProperties: false\n      description: ListStatusPagesRequest is the request to list status pages.\n    openstatus.status_page.v1.ListStatusPagesResponse:\n      type: object\n      properties:\n        statusPages:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.StatusPageSummary'\n          title: status_pages\n          description: List of status pages (metadata only).\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of status pages.\n      title: ListStatusPagesResponse\n      additionalProperties: false\n      description: ListStatusPagesResponse is the response containing status page summaries.\n    openstatus.status_page.v1.ListSubscribersRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to list subscribers for (required).\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of subscribers to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of subscribers to skip for pagination (defaults to 0).\n        includeUnsubscribed:\n          type:\n            - boolean\n            - \"null\"\n          title: include_unsubscribed\n          description: Whether to include unsubscribed users (defaults to false).\n      title: ListSubscribersRequest\n      additionalProperties: false\n      description: ListSubscribersRequest is the request to list subscribers of a status page.\n    openstatus.status_page.v1.ListSubscribersResponse:\n      type: object\n      properties:\n        subscribers:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n          title: subscribers\n          description: List of subscribers.\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of subscribers matching the filter.\n      title: ListSubscribersResponse\n      additionalProperties: false\n      description: ListSubscribersResponse is the response containing status page subscribers.\n    openstatus.status_page.v1.OverallStatus:\n      type: string\n      title: OverallStatus\n      enum:\n        - OVERALL_STATUS_UNSPECIFIED\n        - OVERALL_STATUS_OPERATIONAL\n        - OVERALL_STATUS_DEGRADED\n        - OVERALL_STATUS_PARTIAL_OUTAGE\n        - OVERALL_STATUS_MAJOR_OUTAGE\n        - OVERALL_STATUS_MAINTENANCE\n        - OVERALL_STATUS_UNKNOWN\n      description: OverallStatus represents the aggregated status of all components on a page.\n    openstatus.status_page.v1.PageAccessType:\n      type: string\n      title: PageAccessType\n      enum:\n        - PAGE_ACCESS_TYPE_UNSPECIFIED\n        - PAGE_ACCESS_TYPE_PUBLIC\n        - PAGE_ACCESS_TYPE_PASSWORD_PROTECTED\n        - PAGE_ACCESS_TYPE_AUTHENTICATED\n      description: PageAccessType defines who can access the status page.\n    openstatus.status_page.v1.PageComponent:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the component.\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the status page this component belongs to.\n        name:\n          type: string\n          title: name\n          description: Display name of the component.\n        description:\n          type: string\n          title: description\n          description: Description of the component (optional).\n        type:\n          title: type\n          description: Type of the component (monitor or static).\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentType'\n        monitorId:\n          type: string\n          title: monitor_id\n          description: ID of the monitor if type is MONITOR (optional).\n        order:\n          type: integer\n          title: order\n          format: int32\n          description: Display order of the component.\n        groupId:\n          type: string\n          title: group_id\n          description: ID of the group this component belongs to (optional).\n        groupOrder:\n          type: integer\n          title: group_order\n          format: int32\n          description: Order within the group if grouped.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the component was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the component was last updated (RFC 3339 format).\n      title: PageComponent\n      additionalProperties: false\n      description: PageComponent represents a component displayed on a status page.\n    openstatus.status_page.v1.PageComponentGroup:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the group.\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the status page this group belongs to.\n        name:\n          type: string\n          title: name\n          description: Display name of the group.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the group was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the group was last updated (RFC 3339 format).\n      title: PageComponentGroup\n      additionalProperties: false\n      description: PageComponentGroup represents a group of components on a status page.\n    openstatus.status_page.v1.PageComponentType:\n      type: string\n      title: PageComponentType\n      enum:\n        - PAGE_COMPONENT_TYPE_UNSPECIFIED\n        - PAGE_COMPONENT_TYPE_MONITOR\n        - PAGE_COMPONENT_TYPE_STATIC\n      description: PageComponentType defines the type of a component on a status page.\n    openstatus.status_page.v1.PageSubscriber:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the subscriber.\n        pageId:\n          type: string\n          title: page_id\n          description: ID of the status page the user is subscribed to.\n        email:\n          type: string\n          title: email\n          description: Email address of the subscriber.\n        acceptedAt:\n          type: string\n          title: accepted_at\n          description: Timestamp when the subscription was accepted/confirmed (RFC 3339 format, optional).\n        unsubscribedAt:\n          type: string\n          title: unsubscribed_at\n          description: Timestamp when the user unsubscribed (RFC 3339 format, optional).\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the subscription was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the subscription was last updated (RFC 3339 format).\n      title: PageSubscriber\n      additionalProperties: false\n      description: PageSubscriber represents a subscriber to a status page.\n    openstatus.status_page.v1.PageTheme:\n      type: string\n      title: PageTheme\n      enum:\n        - PAGE_THEME_UNSPECIFIED\n        - PAGE_THEME_SYSTEM\n        - PAGE_THEME_LIGHT\n        - PAGE_THEME_DARK\n      description: PageTheme defines the visual theme of the status page.\n    openstatus.status_page.v1.RemoveComponentRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the component to remove (required).\n      title: RemoveComponentRequest\n      additionalProperties: false\n      description: RemoveComponentRequest is the request to remove a component from a status page.\n    openstatus.status_page.v1.RemoveComponentResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the removal was successful.\n      title: RemoveComponentResponse\n      additionalProperties: false\n      description: RemoveComponentResponse is the response after removing a component.\n    openstatus.status_page.v1.StatusPage:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the status page.\n        title:\n          type: string\n          title: title\n          description: Title of the status page.\n        description:\n          type: string\n          title: description\n          description: Description of the status page.\n        slug:\n          type: string\n          examples:\n            - acme-corp\n          title: slug\n          description: URL-friendly slug for the status page.\n        customDomain:\n          type: string\n          examples:\n            - status.example.com\n          title: custom_domain\n          description: Custom domain for the status page (optional).\n        published:\n          type: boolean\n          title: published\n          description: Whether the status page is published and visible.\n        accessType:\n          title: access_type\n          description: Access type for the status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageAccessType'\n        theme:\n          title: theme\n          description: Visual theme for the status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageTheme'\n        homepageUrl:\n          type: string\n          title: homepage_url\n          description: URL to the homepage (optional).\n        contactUrl:\n          type: string\n          title: contact_url\n          description: URL to the contact page (optional).\n        icon:\n          type: string\n          title: icon\n          description: Icon URL for the status page (optional).\n        createdAt:\n          type: string\n          examples:\n            - \"2024-01-15T09:00:00Z\"\n          title: created_at\n          description: Timestamp when the page was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          examples:\n            - \"2024-06-20T14:30:00Z\"\n          title: updated_at\n          description: Timestamp when the page was last updated (RFC 3339 format).\n      title: StatusPage\n      additionalProperties: false\n      description: StatusPage represents a full status page with all details.\n    openstatus.status_page.v1.StatusPageSummary:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the status page.\n        title:\n          type: string\n          title: title\n          description: Title of the status page.\n        slug:\n          type: string\n          title: slug\n          description: URL-friendly slug for the status page.\n        published:\n          type: boolean\n          title: published\n          description: Whether the status page is published and visible.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the page was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the page was last updated (RFC 3339 format).\n      title: StatusPageSummary\n      additionalProperties: false\n      description: StatusPageSummary represents metadata for a status page (used in list responses).\n    openstatus.status_page.v1.SubscribeToPageRequest:\n      type: object\n      properties:\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: ID of the status page to subscribe to (required).\n        email:\n          type: string\n          examples:\n            - user@example.com\n          title: email\n          format: email\n          description: Email address to subscribe (required).\n      title: SubscribeToPageRequest\n      additionalProperties: false\n      description: SubscribeToPageRequest is the request to subscribe an email to a status page.\n    openstatus.status_page.v1.SubscribeToPageResponse:\n      type: object\n      properties:\n        subscriber:\n          title: subscriber\n          description: The created subscriber.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n      title: SubscribeToPageResponse\n      additionalProperties: false\n      description: SubscribeToPageResponse is the response after subscribing to a status page.\n    openstatus.status_page.v1.UnsubscribeFromPageRequest:\n      type: object\n      allOf:\n        - properties:\n            pageId:\n              type: string\n              title: page_id\n              minLength: 1\n              description: ID of the status page to unsubscribe from (required).\n        - oneOf:\n            - properties:\n                email:\n                  type: string\n                  title: email\n                  description: Email address to unsubscribe.\n              title: email\n              required:\n                - email\n            - properties:\n                id:\n                  type: string\n                  title: id\n                  description: Subscriber ID.\n              title: id\n              required:\n                - id\n      title: UnsubscribeFromPageRequest\n      additionalProperties: false\n      description: UnsubscribeFromPageRequest is the request to unsubscribe from a status page.\n    openstatus.status_page.v1.UnsubscribeFromPageResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the unsubscription was successful.\n      title: UnsubscribeFromPageResponse\n      additionalProperties: false\n      description: UnsubscribeFromPageResponse is the response after unsubscribing from a status page.\n    openstatus.status_page.v1.UpdateComponentGroupRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the component group to update (required).\n        name:\n          type:\n            - string\n            - \"null\"\n          title: name\n          maxLength: 256\n          minLength: 1\n          description: New display name for the group (optional).\n      title: UpdateComponentGroupRequest\n      additionalProperties: false\n      description: UpdateComponentGroupRequest is the request to update a component group.\n    openstatus.status_page.v1.UpdateComponentGroupResponse:\n      type: object\n      properties:\n        group:\n          title: group\n          description: The updated component group.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n      title: UpdateComponentGroupResponse\n      additionalProperties: false\n      description: UpdateComponentGroupResponse is the response after updating a component group.\n    openstatus.status_page.v1.UpdateComponentRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the component to update (required).\n        name:\n          type:\n            - string\n            - \"null\"\n          title: name\n          maxLength: 256\n          description: New display name for the component (optional).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: New description for the component (optional).\n        order:\n          type:\n            - integer\n            - \"null\"\n          title: order\n          format: int32\n          description: New display order (optional).\n        groupId:\n          type:\n            - string\n            - \"null\"\n          title: group_id\n          description: New group ID (optional, set to empty string to remove from group).\n        groupOrder:\n          type:\n            - integer\n            - \"null\"\n          title: group_order\n          format: int32\n          description: New order within the group (optional).\n      title: UpdateComponentRequest\n      additionalProperties: false\n      description: UpdateComponentRequest is the request to update a component.\n    openstatus.status_page.v1.UpdateComponentResponse:\n      type: object\n      properties:\n        component:\n          title: component\n          description: The updated component.\n          $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n      title: UpdateComponentResponse\n      additionalProperties: false\n      description: UpdateComponentResponse is the response after updating a component.\n    openstatus.status_page.v1.UpdateStatusPageRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status page to update (required).\n        title:\n          type:\n            - string\n            - \"null\"\n          title: title\n          maxLength: 256\n          minLength: 1\n          description: New title for the status page (optional).\n        description:\n          type:\n            - string\n            - \"null\"\n          title: description\n          maxLength: 1024\n          description: New description for the status page (optional).\n        slug:\n          type:\n            - string\n            - \"null\"\n          title: slug\n          maxLength: 256\n          minLength: 1\n          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$\n          description: New slug for the status page (optional).\n        homepageUrl:\n          type:\n            - string\n            - \"null\"\n          title: homepage_url\n          description: New homepage URL (optional).\n        contactUrl:\n          type:\n            - string\n            - \"null\"\n          title: contact_url\n          description: New contact URL (optional).\n      title: UpdateStatusPageRequest\n      additionalProperties: false\n      description: UpdateStatusPageRequest is the request to update a status page.\n    openstatus.status_page.v1.UpdateStatusPageResponse:\n      type: object\n      properties:\n        statusPage:\n          title: status_page\n          description: The updated status page.\n          $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n      title: UpdateStatusPageResponse\n      additionalProperties: false\n      description: UpdateStatusPageResponse is the response after updating a status page.\n    openstatus.status_report.v1.AddStatusReportUpdateRequest:\n      type: object\n      properties:\n        statusReportId:\n          type: string\n          title: status_report_id\n          minLength: 1\n          description: ID of the status report to update (required).\n        status:\n          title: status\n          description: New status for the report (required).\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        message:\n          type: string\n          title: message\n          minLength: 1\n          description: Message describing what changed (required).\n        date:\n          type:\n            - string\n            - \"null\"\n          title: date\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: Optional date for the update (RFC 3339 format). Defaults to current time if not provided.\n        notify:\n          type:\n            - boolean\n            - \"null\"\n          title: notify\n          description: Whether to notify subscribers about this update (optional, defaults to false).\n      title: AddStatusReportUpdateRequest\n      additionalProperties: false\n      description: AddStatusReportUpdateRequest is the request to add a new update to a status report.\n    openstatus.status_report.v1.AddStatusReportUpdateResponse:\n      type: object\n      properties:\n        statusReport:\n          title: status_report\n          description: The updated status report with the new update included.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n      title: AddStatusReportUpdateResponse\n      additionalProperties: false\n      description: AddStatusReportUpdateResponse is the response after adding an update to a status report.\n    openstatus.status_report.v1.CreateStatusReportRequest:\n      type: object\n      properties:\n        title:\n          type: string\n          examples:\n            - API Degradation Investigation\n          title: title\n          minLength: 1\n          description: Title of the status report (required).\n        status:\n          title: status\n          description: Initial status (required).\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        message:\n          type: string\n          examples:\n            - We are investigating reports of increased API latency.\n          title: message\n          minLength: 1\n          description: Initial message describing the incident (required).\n        date:\n          type: string\n          examples:\n            - \"2024-03-15T10:30:00Z\"\n          title: date\n          pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n          description: Date when the event occurred (RFC 3339 format, required).\n        pageId:\n          type: string\n          title: page_id\n          minLength: 1\n          description: Page ID to associate with this report (required).\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: Page component IDs to associate with this report (optional).\n        notify:\n          type:\n            - boolean\n            - \"null\"\n          title: notify\n          description: Whether to notify subscribers about this status report (optional, defaults to false).\n      title: CreateStatusReportRequest\n      additionalProperties: false\n      description: CreateStatusReportRequest is the request to create a new status report.\n    openstatus.status_report.v1.CreateStatusReportResponse:\n      type: object\n      properties:\n        statusReport:\n          title: status_report\n          description: The created status report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n      title: CreateStatusReportResponse\n      additionalProperties: false\n      description: CreateStatusReportResponse is the response after creating a status report.\n    openstatus.status_report.v1.DeleteStatusReportRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status report to delete (required).\n      title: DeleteStatusReportRequest\n      additionalProperties: false\n      description: DeleteStatusReportRequest is the request to delete a status report.\n    openstatus.status_report.v1.DeleteStatusReportResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          title: success\n          description: Whether the deletion was successful.\n      title: DeleteStatusReportResponse\n      additionalProperties: false\n      description: DeleteStatusReportResponse is the response after deleting a status report.\n    openstatus.status_report.v1.GetStatusReportRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status report to retrieve (required).\n      title: GetStatusReportRequest\n      additionalProperties: false\n      description: GetStatusReportRequest is the request to get a status report by ID.\n    openstatus.status_report.v1.GetStatusReportResponse:\n      type: object\n      properties:\n        statusReport:\n          title: status_report\n          description: The requested status report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n      title: GetStatusReportResponse\n      additionalProperties: false\n      description: GetStatusReportResponse is the response containing the status report with its full update timeline.\n    openstatus.status_report.v1.ListStatusReportsRequest:\n      type: object\n      properties:\n        limit:\n          type:\n            - integer\n            - \"null\"\n          title: limit\n          maximum: 100\n          minimum: 1\n          format: int32\n          description: Maximum number of reports to return (1-100, defaults to 50).\n        offset:\n          type:\n            - integer\n            - \"null\"\n          title: offset\n          minimum: 0\n          format: int32\n          description: Number of reports to skip for pagination (defaults to 0).\n        statuses:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n          title: statuses\n          description: Filter by status (optional). If empty, returns all statuses.\n      title: ListStatusReportsRequest\n      additionalProperties: false\n      description: ListStatusReportsRequest is the request to list status reports.\n    openstatus.status_report.v1.ListStatusReportsResponse:\n      type: object\n      properties:\n        statusReports:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportSummary'\n          title: status_reports\n          description: List of status reports (metadata only, use GetStatusReport for full details).\n        totalSize:\n          type: integer\n          title: total_size\n          format: int32\n          description: Total number of reports matching the filter.\n      title: ListStatusReportsResponse\n      additionalProperties: false\n      description: ListStatusReportsResponse is the response containing status report summaries.\n    openstatus.status_report.v1.StatusReport:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the status report.\n        status:\n          title: status\n          description: Current status of the report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        title:\n          type: string\n          title: title\n          description: Title of the status report.\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: IDs of affected page components.\n        updates:\n          type: array\n          items:\n            $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportUpdate'\n          title: updates\n          description: Timeline of updates for this report (only included in GetStatusReport).\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the report was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the report was last updated (RFC 3339 format).\n      title: StatusReport\n      additionalProperties: false\n      description: StatusReport represents an incident or maintenance report with full details.\n    openstatus.status_report.v1.StatusReportStatus:\n      type: string\n      title: StatusReportStatus\n      enum:\n        - STATUS_REPORT_STATUS_UNSPECIFIED\n        - STATUS_REPORT_STATUS_INVESTIGATING\n        - STATUS_REPORT_STATUS_IDENTIFIED\n        - STATUS_REPORT_STATUS_MONITORING\n        - STATUS_REPORT_STATUS_RESOLVED\n      description: StatusReportStatus represents the current state of a status report.\n    openstatus.status_report.v1.StatusReportSummary:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the status report.\n        status:\n          title: status\n          description: Current status of the report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        title:\n          type: string\n          title: title\n          description: Title of the status report.\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: IDs of affected page components.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the report was created (RFC 3339 format).\n        updatedAt:\n          type: string\n          title: updated_at\n          description: Timestamp when the report was last updated (RFC 3339 format).\n      title: StatusReportSummary\n      additionalProperties: false\n      description: StatusReportSummary represents metadata for a status report (used in list responses).\n    openstatus.status_report.v1.StatusReportUpdate:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          description: Unique identifier for the update.\n        status:\n          title: status\n          description: Status at the time of this update.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n        date:\n          type: string\n          title: date\n          description: Timestamp when this update occurred (RFC 3339 format).\n        message:\n          type: string\n          title: message\n          description: Message describing the update.\n        createdAt:\n          type: string\n          title: created_at\n          description: Timestamp when the update was created (RFC 3339 format).\n      title: StatusReportUpdate\n      additionalProperties: false\n      description: StatusReportUpdate represents a single update entry in a status report timeline.\n    openstatus.status_report.v1.UpdateStatusReportRequest:\n      type: object\n      properties:\n        id:\n          type: string\n          title: id\n          minLength: 1\n          description: ID of the status report to update (required).\n        title:\n          type:\n            - string\n            - \"null\"\n          title: title\n          description: New title for the report (optional).\n        pageComponentIds:\n          type: array\n          items:\n            type: string\n          title: page_component_ids\n          description: New list of page component IDs (optional, replaces existing list).\n      title: UpdateStatusReportRequest\n      additionalProperties: false\n      description: UpdateStatusReportRequest is the request to update a status report's metadata.\n    openstatus.status_report.v1.UpdateStatusReportResponse:\n      type: object\n      properties:\n        statusReport:\n          title: status_report\n          description: The updated status report.\n          $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n      title: UpdateStatusReportResponse\n      additionalProperties: false\n      description: UpdateStatusReportResponse is the response after updating a status report.\nsecurity:\n  - ApiKeyAuth: []\ntags:\n  - name: MonitorService\n    description: |\n      Create, update, delete, and query monitors. Supports HTTP, TCP, and DNS monitor types\n      with configurable check intervals, regions, assertions, and alerting thresholds.\n  - name: StatusPageService\n    description: |\n      Manage public status pages with components, component groups, and email subscribers.\n      Includes endpoints for retrieving full page content and aggregated status.\n  - name: NotificationService\n    description: |\n      Configure notification channels (Slack, Discord, PagerDuty, email, webhooks, etc.)\n      and associate them with monitors. Supports 12 notification providers.\n  - name: StatusReportService\n    description: |\n      Create and manage incident reports with status updates. Reports follow a lifecycle:\n      investigating -> identified -> monitoring -> resolved.\n  - name: MaintenanceService\n    description: |\n      Schedule maintenance windows for status page components. Subscribers can be\n      notified automatically when maintenance is created.\n  - name: HealthService\n    description: Health check endpoint for load balancer probes. No authentication required.\npaths:\n  /rpc/openstatus.health.v1.HealthService/Check:\n    get:\n      tags:\n        - HealthService\n      summary: Check\n      description: Check returns the current serving status of the service.\n      operationId: HealthService_Check.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.health.v1.CheckRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.health.v1.CheckResponse'\n    post:\n      tags:\n        - HealthService\n      summary: Check\n      description: Check returns the current serving status of the service.\n      operationId: HealthService_Check\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.health.v1.CheckRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.health.v1.CheckResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/CreateMaintenance:\n    post:\n      tags:\n        - MaintenanceService\n      summary: CreateMaintenance\n      description: CreateMaintenance creates a new maintenance window.\n      operationId: MaintenanceService_CreateMaintenance\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.CreateMaintenanceRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.CreateMaintenanceResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/DeleteMaintenance:\n    post:\n      tags:\n        - MaintenanceService\n      summary: DeleteMaintenance\n      description: DeleteMaintenance removes a maintenance window.\n      operationId: MaintenanceService_DeleteMaintenance\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/GetMaintenance:\n    get:\n      tags:\n        - MaintenanceService\n      summary: GetMaintenance\n      description: GetMaintenance retrieves a specific maintenance window by ID.\n      operationId: MaintenanceService_GetMaintenance.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceResponse'\n    post:\n      tags:\n        - MaintenanceService\n      summary: GetMaintenance\n      description: GetMaintenance retrieves a specific maintenance window by ID.\n      operationId: MaintenanceService_GetMaintenance\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/ListMaintenances:\n    get:\n      tags:\n        - MaintenanceService\n      summary: ListMaintenances\n      description: ListMaintenances returns all maintenance windows for the workspace.\n      operationId: MaintenanceService_ListMaintenances.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesResponse'\n    post:\n      tags:\n        - MaintenanceService\n      summary: ListMaintenances\n      description: ListMaintenances returns all maintenance windows for the workspace.\n      operationId: MaintenanceService_ListMaintenances\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesResponse'\n  /rpc/openstatus.maintenance.v1.MaintenanceService/UpdateMaintenance:\n    post:\n      tags:\n        - MaintenanceService\n      summary: UpdateMaintenance\n      description: UpdateMaintenance updates a maintenance window.\n      operationId: MaintenanceService_UpdateMaintenance\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/CreateDNSMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: CreateDNSMonitor\n      description: CreateDNSMonitor creates a new DNS monitor.\n      operationId: MonitorService_CreateDNSMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.CreateDNSMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.CreateDNSMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/CreateHTTPMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: CreateHTTPMonitor\n      description: Creates a new HTTP monitor in the authenticated workspace. Configure the target URL, HTTP method, request headers and body, response assertions (status code, body content, headers), check periodicity, geographic regions, and optional OpenTelemetry export. The monitor starts checking immediately if set to active.\n      operationId: MonitorService_CreateHTTPMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.CreateHTTPMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.CreateHTTPMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/CreateTCPMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: CreateTCPMonitor\n      description: CreateTCPMonitor creates a new TCP monitor.\n      operationId: MonitorService_CreateTCPMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.CreateTCPMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.CreateTCPMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/DeleteMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: DeleteMonitor\n      description: DeleteMonitor removes a monitor.\n      operationId: MonitorService_DeleteMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.DeleteMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.DeleteMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/GetMonitor:\n    get:\n      tags:\n        - MonitorService\n      summary: GetMonitor\n      description: |-\n        GetMonitor returns a single monitor by ID within the authenticated workspace.\n         Returns the monitor configuration (HTTP, TCP, or DNS) using the MonitorConfig oneof type.\n      operationId: MonitorService_GetMonitor.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorResponse'\n    post:\n      tags:\n        - MonitorService\n      summary: GetMonitor\n      description: |-\n        GetMonitor returns a single monitor by ID within the authenticated workspace.\n         Returns the monitor configuration (HTTP, TCP, or DNS) using the MonitorConfig oneof type.\n      operationId: MonitorService_GetMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/GetMonitorStatus:\n    get:\n      tags:\n        - MonitorService\n      summary: GetMonitorStatus\n      description: GetMonitorStatus returns the current status of all regions for a monitor.\n      operationId: MonitorService_GetMonitorStatus.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusResponse'\n    post:\n      tags:\n        - MonitorService\n      summary: GetMonitorStatus\n      description: GetMonitorStatus returns the current status of all regions for a monitor.\n      operationId: MonitorService_GetMonitorStatus\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/GetMonitorSummary:\n    get:\n      tags:\n        - MonitorService\n      summary: GetMonitorSummary\n      description: Returns aggregated metrics for a monitor including latency percentiles (p50, p75, p90, p95, p99), request counts by status (successful, degraded, failed), and the timestamp of the last check. Metrics can be scoped to a time range (1 day, 7 days, or 14 days) and filtered by specific regions.\n      operationId: MonitorService_GetMonitorSummary.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryResponse'\n    post:\n      tags:\n        - MonitorService\n      summary: GetMonitorSummary\n      description: Returns aggregated metrics for a monitor including latency percentiles (p50, p75, p90, p95, p99), request counts by status (successful, degraded, failed), and the timestamp of the last check. Metrics can be scoped to a time range (1 day, 7 days, or 14 days) and filtered by specific regions.\n      operationId: MonitorService_GetMonitorSummary\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/ListMonitors:\n    get:\n      tags:\n        - MonitorService\n      summary: ListMonitors\n      description: ListMonitors returns a paginated list of all monitors in the workspace.\n      operationId: MonitorService_ListMonitors.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsResponse'\n    post:\n      tags:\n        - MonitorService\n      summary: ListMonitors\n      description: ListMonitors returns a paginated list of all monitors in the workspace.\n      operationId: MonitorService_ListMonitors\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/TriggerMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: TriggerMonitor\n      description: Manually triggers an immediate check for the specified monitor across all configured regions. This operation is rate-limited under the synthetic-checks quota. A monitor run record is created and the check is dispatched to the checker service.\n      operationId: MonitorService_TriggerMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.TriggerMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.TriggerMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/UpdateDNSMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: UpdateDNSMonitor\n      description: UpdateDNSMonitor updates an existing DNS monitor.\n      operationId: MonitorService_UpdateDNSMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.UpdateDNSMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.UpdateDNSMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/UpdateHTTPMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: UpdateHTTPMonitor\n      description: UpdateHTTPMonitor updates an existing HTTP monitor.\n      operationId: MonitorService_UpdateHTTPMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.UpdateHTTPMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.UpdateHTTPMonitorResponse'\n  /rpc/openstatus.monitor.v1.MonitorService/UpdateTCPMonitor:\n    post:\n      tags:\n        - MonitorService\n      summary: UpdateTCPMonitor\n      description: UpdateTCPMonitor updates an existing TCP monitor.\n      operationId: MonitorService_UpdateTCPMonitor\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.monitor.v1.UpdateTCPMonitorRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.monitor.v1.UpdateTCPMonitorResponse'\n  /rpc/openstatus.notification.v1.NotificationService/CheckNotificationLimit:\n    get:\n      tags:\n        - NotificationService\n      summary: CheckNotificationLimit\n      description: CheckNotificationLimit checks if the workspace has reached its notification limit.\n      operationId: NotificationService_CheckNotificationLimit.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitResponse'\n    post:\n      tags:\n        - NotificationService\n      summary: CheckNotificationLimit\n      description: CheckNotificationLimit checks if the workspace has reached its notification limit.\n      operationId: NotificationService_CheckNotificationLimit\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitResponse'\n  /rpc/openstatus.notification.v1.NotificationService/CreateNotification:\n    post:\n      tags:\n        - NotificationService\n      summary: CreateNotification\n      description: CreateNotification creates a new notification channel.\n      operationId: NotificationService_CreateNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.CreateNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.CreateNotificationResponse'\n  /rpc/openstatus.notification.v1.NotificationService/DeleteNotification:\n    post:\n      tags:\n        - NotificationService\n      summary: DeleteNotification\n      description: DeleteNotification removes a notification channel.\n      operationId: NotificationService_DeleteNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.DeleteNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.DeleteNotificationResponse'\n  /rpc/openstatus.notification.v1.NotificationService/GetNotification:\n    get:\n      tags:\n        - NotificationService\n      summary: GetNotification\n      description: GetNotification retrieves a notification channel by ID.\n      operationId: NotificationService_GetNotification.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationResponse'\n    post:\n      tags:\n        - NotificationService\n      summary: GetNotification\n      description: GetNotification retrieves a notification channel by ID.\n      operationId: NotificationService_GetNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationResponse'\n  /rpc/openstatus.notification.v1.NotificationService/ListNotifications:\n    get:\n      tags:\n        - NotificationService\n      summary: ListNotifications\n      description: ListNotifications returns a list of notification channels.\n      operationId: NotificationService_ListNotifications.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsResponse'\n    post:\n      tags:\n        - NotificationService\n      summary: ListNotifications\n      description: ListNotifications returns a list of notification channels.\n      operationId: NotificationService_ListNotifications\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsResponse'\n  /rpc/openstatus.notification.v1.NotificationService/SendTestNotification:\n    post:\n      tags:\n        - NotificationService\n      summary: SendTestNotification\n      description: Sends a test notification to the specified provider to verify that the configuration is correct. This does not require an existing notification channel - just provide the provider type and its configuration data. Returns success status and an error message if the test failed.\n      operationId: NotificationService_SendTestNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.SendTestNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.SendTestNotificationResponse'\n  /rpc/openstatus.notification.v1.NotificationService/UpdateNotification:\n    post:\n      tags:\n        - NotificationService\n      summary: UpdateNotification\n      description: UpdateNotification updates an existing notification channel.\n      operationId: NotificationService_UpdateNotification\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.notification.v1.UpdateNotificationRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.notification.v1.UpdateNotificationResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/AddMonitorComponent:\n    post:\n      tags:\n        - StatusPageService\n      summary: AddMonitorComponent\n      description: AddMonitorComponent adds a monitor-based component to a status page.\n      operationId: StatusPageService_AddMonitorComponent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.AddMonitorComponentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.AddMonitorComponentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/AddStaticComponent:\n    post:\n      tags:\n        - StatusPageService\n      summary: AddStaticComponent\n      description: AddStaticComponent adds a static component to a status page.\n      operationId: StatusPageService_AddStaticComponent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.AddStaticComponentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.AddStaticComponentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/CreateComponentGroup:\n    post:\n      tags:\n        - StatusPageService\n      summary: CreateComponentGroup\n      description: CreateComponentGroup creates a new component group.\n      operationId: StatusPageService_CreateComponentGroup\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.CreateComponentGroupRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.CreateComponentGroupResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/CreateStatusPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: CreateStatusPage\n      description: CreateStatusPage creates a new status page.\n      operationId: StatusPageService_CreateStatusPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.CreateStatusPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.CreateStatusPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/DeleteComponentGroup:\n    post:\n      tags:\n        - StatusPageService\n      summary: DeleteComponentGroup\n      description: DeleteComponentGroup removes a component group.\n      operationId: StatusPageService_DeleteComponentGroup\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.DeleteComponentGroupRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.DeleteComponentGroupResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/DeleteStatusPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: DeleteStatusPage\n      description: DeleteStatusPage removes a status page.\n      operationId: StatusPageService_DeleteStatusPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.DeleteStatusPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.DeleteStatusPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/GetOverallStatus:\n    get:\n      tags:\n        - StatusPageService\n      summary: GetOverallStatus\n      description: 'Returns the overall status of a status page along with individual component statuses. The overall status is computed from active status reports and maintenances with the following priority: degraded (from active status reports) > maintenance (from active maintenance windows) > operational.'\n      operationId: StatusPageService_GetOverallStatus.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: GetOverallStatus\n      description: 'Returns the overall status of a status page along with individual component statuses. The overall status is computed from active status reports and maintenances with the following priority: degraded (from active status reports) > maintenance (from active maintenance windows) > operational.'\n      operationId: StatusPageService_GetOverallStatus\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPage:\n    get:\n      tags:\n        - StatusPageService\n      summary: GetStatusPage\n      description: GetStatusPage retrieves a specific status page by ID.\n      operationId: StatusPageService_GetStatusPage.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: GetStatusPage\n      description: GetStatusPage retrieves a specific status page by ID.\n      operationId: StatusPageService_GetStatusPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPageContent:\n    get:\n      tags:\n        - StatusPageService\n      summary: GetStatusPageContent\n      description: 'Returns the full content of a status page including its components, component groups, active status reports, and scheduled maintenances. Supports two access paths: by ID (requires authentication, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n      operationId: StatusPageService_GetStatusPageContent.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: GetStatusPageContent\n      description: 'Returns the full content of a status page including its components, component groups, active status reports, and scheduled maintenances. Supports two access paths: by ID (requires authentication, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n      operationId: StatusPageService_GetStatusPageContent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/ListStatusPages:\n    get:\n      tags:\n        - StatusPageService\n      summary: ListStatusPages\n      description: ListStatusPages returns all status pages for the workspace.\n      operationId: StatusPageService_ListStatusPages.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: ListStatusPages\n      description: ListStatusPages returns all status pages for the workspace.\n      operationId: StatusPageService_ListStatusPages\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/ListSubscribers:\n    get:\n      tags:\n        - StatusPageService\n      summary: ListSubscribers\n      description: ListSubscribers returns all subscribers for a status page.\n      operationId: StatusPageService_ListSubscribers.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersResponse'\n    post:\n      tags:\n        - StatusPageService\n      summary: ListSubscribers\n      description: ListSubscribers returns all subscribers for a status page.\n      operationId: StatusPageService_ListSubscribers\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/RemoveComponent:\n    post:\n      tags:\n        - StatusPageService\n      summary: RemoveComponent\n      description: RemoveComponent removes a component from a status page.\n      operationId: StatusPageService_RemoveComponent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.RemoveComponentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.RemoveComponentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/SubscribeToPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: SubscribeToPage\n      description: Subscribes an email address to receive notifications from a status page. If the email was previously unsubscribed, the subscription is reactivated instead of creating a duplicate.\n      operationId: StatusPageService_SubscribeToPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.SubscribeToPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.SubscribeToPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/UnsubscribeFromPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: UnsubscribeFromPage\n      description: UnsubscribeFromPage removes a subscription from a status page.\n      operationId: StatusPageService_UnsubscribeFromPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.UnsubscribeFromPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.UnsubscribeFromPageResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/UpdateComponent:\n    post:\n      tags:\n        - StatusPageService\n      summary: UpdateComponent\n      description: UpdateComponent updates an existing component.\n      operationId: StatusPageService_UpdateComponent\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/UpdateComponentGroup:\n    post:\n      tags:\n        - StatusPageService\n      summary: UpdateComponentGroup\n      description: UpdateComponentGroup updates an existing component group.\n      operationId: StatusPageService_UpdateComponentGroup\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentGroupRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentGroupResponse'\n  /rpc/openstatus.status_page.v1.StatusPageService/UpdateStatusPage:\n    post:\n      tags:\n        - StatusPageService\n      summary: UpdateStatusPage\n      description: UpdateStatusPage updates an existing status page.\n      operationId: StatusPageService_UpdateStatusPage\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_page.v1.UpdateStatusPageRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_page.v1.UpdateStatusPageResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/AddStatusReportUpdate:\n    post:\n      tags:\n        - StatusReportService\n      summary: AddStatusReportUpdate\n      description: 'Adds a new update entry to an existing status report and transitions the report to the specified status. Status reports follow a lifecycle: investigating -> identified -> monitoring -> resolved. If notify is true, subscribers of the associated page are notified by email about the update.'\n      operationId: StatusReportService_AddStatusReportUpdate\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.AddStatusReportUpdateRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.AddStatusReportUpdateResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/CreateStatusReport:\n    post:\n      tags:\n        - StatusReportService\n      summary: CreateStatusReport\n      description: Creates a new status report with an initial update entry. The report is associated with a status page and optionally specific page components. An initial StatusReportUpdate is created automatically with the provided status, message, and date. If notify is true, subscribers of the associated page are notified by email.\n      operationId: StatusReportService_CreateStatusReport\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.CreateStatusReportRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.CreateStatusReportResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/DeleteStatusReport:\n    post:\n      tags:\n        - StatusReportService\n      summary: DeleteStatusReport\n      description: DeleteStatusReport removes a status report and all its updates.\n      operationId: StatusReportService_DeleteStatusReport\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/GetStatusReport:\n    get:\n      tags:\n        - StatusReportService\n      summary: GetStatusReport\n      description: GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n      operationId: StatusReportService_GetStatusReport.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportResponse'\n    post:\n      tags:\n        - StatusReportService\n      summary: GetStatusReport\n      description: GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n      operationId: StatusReportService_GetStatusReport\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/ListStatusReports:\n    get:\n      tags:\n        - StatusReportService\n      summary: ListStatusReports\n      description: ListStatusReports returns all status reports for the workspace (metadata only).\n      operationId: StatusReportService_ListStatusReports.get\n      parameters:\n        - name: message\n          in: query\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsRequest'\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsResponse'\n    post:\n      tags:\n        - StatusReportService\n      summary: ListStatusReports\n      description: ListStatusReports returns all status reports for the workspace (metadata only).\n      operationId: StatusReportService_ListStatusReports\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsResponse'\n  /rpc/openstatus.status_report.v1.StatusReportService/UpdateStatusReport:\n    post:\n      tags:\n        - StatusReportService\n      summary: UpdateStatusReport\n      description: UpdateStatusReport updates the metadata of a status report (title, page components).\n      operationId: StatusReportService_UpdateStatusReport\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportRequest'\n        required: true\n      responses:\n        default:\n          description: Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/connect.error'\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportResponse'\n"
  },
  {
    "path": "packages/proto/gen/ts/buf/validate/validate_pb.ts",
    "content": "// Copyright 2023-2025 Buf Technologies, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file buf/validate/validate.proto (package buf.validate, syntax proto2)\n/* eslint-disable */\n\nimport type { GenEnum, GenExtension, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, extDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Duration, FieldDescriptorProto_Type, FieldMask, FieldOptions, MessageOptions, OneofOptions, Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_field_mask, file_google_protobuf_timestamp } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file buf/validate/validate.proto.\n */\nexport const file_buf_validate_validate: GenFile = /*@__PURE__*/\n  fileDesc(\"ChtidWYvdmFsaWRhdGUvdmFsaWRhdGUucHJvdG8SDGJ1Zi52YWxpZGF0ZSI3CgRSdWxlEgoKAmlkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSEgoKZXhwcmVzc2lvbhgDIAEoCSKGAQoMTWVzc2FnZVJ1bGVzEhYKDmNlbF9leHByZXNzaW9uGAUgAygJEh8KA2NlbBgDIAMoCzISLmJ1Zi52YWxpZGF0ZS5SdWxlEi0KBW9uZW9mGAQgAygLMh4uYnVmLnZhbGlkYXRlLk1lc3NhZ2VPbmVvZlJ1bGVKBAgBEAJSCGRpc2FibGVkIjQKEE1lc3NhZ2VPbmVvZlJ1bGUSDgoGZmllbGRzGAEgAygJEhAKCHJlcXVpcmVkGAIgASgIIh4KCk9uZW9mUnVsZXMSEAoIcmVxdWlyZWQYASABKAgiiwkKCkZpZWxkUnVsZXMSFgoOY2VsX2V4cHJlc3Npb24YHSADKAkSHwoDY2VsGBcgAygLMhIuYnVmLnZhbGlkYXRlLlJ1bGUSEAoIcmVxdWlyZWQYGSABKAgSJAoGaWdub3JlGBsgASgOMhQuYnVmLnZhbGlkYXRlLklnbm9yZRIpCgVmbG9hdBgBIAEoCzIYLmJ1Zi52YWxpZGF0ZS5GbG9hdFJ1bGVzSAASKwoGZG91YmxlGAIgASgLMhkuYnVmLnZhbGlkYXRlLkRvdWJsZVJ1bGVzSAASKQoFaW50MzIYAyABKAsyGC5idWYudmFsaWRhdGUuSW50MzJSdWxlc0gAEikKBWludDY0GAQgASgLMhguYnVmLnZhbGlkYXRlLkludDY0UnVsZXNIABIrCgZ1aW50MzIYBSABKAsyGS5idWYudmFsaWRhdGUuVUludDMyUnVsZXNIABIrCgZ1aW50NjQYBiABKAsyGS5idWYudmFsaWRhdGUuVUludDY0UnVsZXNIABIrCgZzaW50MzIYByABKAsyGS5idWYudmFsaWRhdGUuU0ludDMyUnVsZXNIABIrCgZzaW50NjQYCCABKAsyGS5idWYudmFsaWRhdGUuU0ludDY0UnVsZXNIABItCgdmaXhlZDMyGAkgASgLMhouYnVmLnZhbGlkYXRlLkZpeGVkMzJSdWxlc0gAEi0KB2ZpeGVkNjQYCiABKAsyGi5idWYudmFsaWRhdGUuRml4ZWQ2NFJ1bGVzSAASLwoIc2ZpeGVkMzIYCyABKAsyGy5idWYudmFsaWRhdGUuU0ZpeGVkMzJSdWxlc0gAEi8KCHNmaXhlZDY0GAwgASgLMhsuYnVmLnZhbGlkYXRlLlNGaXhlZDY0UnVsZXNIABInCgRib29sGA0gASgLMhcuYnVmLnZhbGlkYXRlLkJvb2xSdWxlc0gAEisKBnN0cmluZxgOIAEoCzIZLmJ1Zi52YWxpZGF0ZS5TdHJpbmdSdWxlc0gAEikKBWJ5dGVzGA8gASgLMhguYnVmLnZhbGlkYXRlLkJ5dGVzUnVsZXNIABInCgRlbnVtGBAgASgLMhcuYnVmLnZhbGlkYXRlLkVudW1SdWxlc0gAEi8KCHJlcGVhdGVkGBIgASgLMhsuYnVmLnZhbGlkYXRlLlJlcGVhdGVkUnVsZXNIABIlCgNtYXAYEyABKAsyFi5idWYudmFsaWRhdGUuTWFwUnVsZXNIABIlCgNhbnkYFCABKAsyFi5idWYudmFsaWRhdGUuQW55UnVsZXNIABIvCghkdXJhdGlvbhgVIAEoCzIbLmJ1Zi52YWxpZGF0ZS5EdXJhdGlvblJ1bGVzSAASMgoKZmllbGRfbWFzaxgcIAEoCzIcLmJ1Zi52YWxpZGF0ZS5GaWVsZE1hc2tSdWxlc0gAEjEKCXRpbWVzdGFtcBgWIAEoCzIcLmJ1Zi52YWxpZGF0ZS5UaW1lc3RhbXBSdWxlc0gAQgYKBHR5cGVKBAgYEBlKBAgaEBtSB3NraXBwZWRSDGlnbm9yZV9lbXB0eSJVCg9QcmVkZWZpbmVkUnVsZXMSHwoDY2VsGAEgAygLMhIuYnVmLnZhbGlkYXRlLlJ1bGVKBAgYEBlKBAgaEBtSB3NraXBwZWRSDGlnbm9yZV9lbXB0eSLaFwoKRmxvYXRSdWxlcxKDAQoFY29uc3QYASABKAJCdMJIcQpvCgtmbG9hdC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEp8BCgJsdBgCIAEoAkKQAcJIjAEKiQEKCGZsb2F0Lmx0Gn0haGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID49IHJ1bGVzLmx0KT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEq8BCgNsdGUYAyABKAJCnwHCSJsBCpgBCglmbG9hdC5sdGUaigEhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlKT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABLvBwoCZ3QYBCABKAJC4AfCSNwHCo0BCghmbG9hdC5ndBqAASFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCsMBCgtmbG9hdC5ndF9sdBqzAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCs0BChVmbG9hdC5ndF9sdF9leGNsdXNpdmUaswFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrTAQoMZmxvYXQuZ3RfbHRlGsIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycK3QEKFmZsb2F0Lmd0X2x0ZV9leGNsdXNpdmUawgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEroICgNndGUYBSABKAJCqgjCSKYICpsBCglmbG9hdC5ndGUajQEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycK0gEKDGZsb2F0Lmd0ZV9sdBrBAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK3AEKFmZsb2F0Lmd0ZV9sdF9leGNsdXNpdmUawQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCuIBCg1mbG9hdC5ndGVfbHRlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrsAQoXZmxvYXQuZ3RlX2x0ZV9leGNsdXNpdmUa0AFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESfwoCaW4YBiADKAJCc8JIcApuCghmbG9hdC5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdgoGbm90X2luGAcgAygCQmbCSGMKYQoMZmxvYXQubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSdQoGZmluaXRlGAggASgIQmXCSGIKYAoMZmxvYXQuZmluaXRlGlBydWxlcy5maW5pdGUgPyAodGhpcy5pc05hbigpIHx8IHRoaXMuaXNJbmYoKSA/ICd2YWx1ZSBtdXN0IGJlIGZpbml0ZScgOiAnJykgOiAnJxIrCgdleGFtcGxlGAkgAygCQhrCSBcKFQoNZmxvYXQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4i7RcKC0RvdWJsZVJ1bGVzEoQBCgVjb25zdBgBIAEoAUJ1wkhyCnAKDGRvdWJsZS5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEqABCgJsdBgCIAEoAUKRAcJIjQEKigEKCWRvdWJsZS5sdBp9IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCk/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKwAQoDbHRlGAMgASgBQqABwkicAQqZAQoKZG91YmxlLmx0ZRqKASFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUpPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEvQHCgJndBgEIAEoAULlB8JI4QcKjgEKCWRvdWJsZS5ndBqAASFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCsQBCgxkb3VibGUuZ3RfbHQaswFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrOAQoWZG91YmxlLmd0X2x0X2V4Y2x1c2l2ZRqzAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCtQBCg1kb3VibGUuZ3RfbHRlGsIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycK3gEKF2RvdWJsZS5ndF9sdGVfZXhjbHVzaXZlGsIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARK/CAoDZ3RlGAUgASgBQq8IwkirCAqcAQoKZG91YmxlLmd0ZRqNASFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrTAQoNZG91YmxlLmd0ZV9sdBrBAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK3QEKF2RvdWJsZS5ndGVfbHRfZXhjbHVzaXZlGsEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrjAQoOZG91YmxlLmd0ZV9sdGUa0AFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCu0BChhkb3VibGUuZ3RlX2x0ZV9leGNsdXNpdmUa0AFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESgAEKAmluGAYgAygBQnTCSHEKbwoJZG91YmxlLmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ3CgZub3RfaW4YByADKAFCZ8JIZApiCg1kb3VibGUubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSdgoGZmluaXRlGAggASgIQmbCSGMKYQoNZG91YmxlLmZpbml0ZRpQcnVsZXMuZmluaXRlID8gKHRoaXMuaXNOYW4oKSB8fCB0aGlzLmlzSW5mKCkgPyAndmFsdWUgbXVzdCBiZSBmaW5pdGUnIDogJycpIDogJycSLAoHZXhhbXBsZRgJIAMoAUIbwkgYChYKDmRvdWJsZS5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKMFQoKSW50MzJSdWxlcxKDAQoFY29uc3QYASABKAVCdMJIcQpvCgtpbnQzMi5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEooBCgJsdBgCIAEoBUJ8wkh5CncKCGludDMyLmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEpwBCgNsdGUYAyABKAVCjAHCSIgBCoUBCglpbnQzMi5sdGUaeCFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEpcHCgJndBgEIAEoBUKIB8JIhAcKegoIaW50MzIuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrMBCgtpbnQzMi5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKuwEKFWludDMyLmd0X2x0X2V4Y2x1c2l2ZRqhAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCsMBCgxpbnQzMi5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCssBChZpbnQzMi5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLjBwoDZ3RlGAUgASgFQtMHwkjPBwqIAQoJaW50MzIuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKwgEKDGludDMyLmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrKAQoWaW50MzIuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK0gEKDWludDMyLmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK2gEKF2ludDMyLmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEn8KAmluGAYgAygFQnPCSHAKbgoIaW50MzIuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnYKBm5vdF9pbhgHIAMoBUJmwkhjCmEKDGludDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEisKB2V4YW1wbGUYCCADKAVCGsJIFwoVCg1pbnQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKMFQoKSW50NjRSdWxlcxKDAQoFY29uc3QYASABKANCdMJIcQpvCgtpbnQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEooBCgJsdBgCIAEoA0J8wkh5CncKCGludDY0Lmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEpwBCgNsdGUYAyABKANCjAHCSIgBCoUBCglpbnQ2NC5sdGUaeCFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEpcHCgJndBgEIAEoA0KIB8JIhAcKegoIaW50NjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrMBCgtpbnQ2NC5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKuwEKFWludDY0Lmd0X2x0X2V4Y2x1c2l2ZRqhAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCsMBCgxpbnQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCssBChZpbnQ2NC5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLjBwoDZ3RlGAUgASgDQtMHwkjPBwqIAQoJaW50NjQuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKwgEKDGludDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrKAQoWaW50NjQuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK0gEKDWludDY0Lmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK2gEKF2ludDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEn8KAmluGAYgAygDQnPCSHAKbgoIaW50NjQuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnYKBm5vdF9pbhgHIAMoA0JmwkhjCmEKDGludDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEisKB2V4YW1wbGUYCSADKANCGsJIFwoVCg1pbnQ2NC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKeFQoLVUludDMyUnVsZXMShAEKBWNvbnN0GAEgASgNQnXCSHIKcAoMdWludDMyLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSiwEKAmx0GAIgASgNQn3CSHoKeAoJdWludDMyLmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp0BCgNsdGUYAyABKA1CjQHCSIkBCoYBCgp1aW50MzIubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKcBwoCZ3QYBCABKA1CjQfCSIkHCnsKCXVpbnQzMi5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtAEKDHVpbnQzMi5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvAEKFnVpbnQzMi5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrEAQoNdWludDMyLmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzAEKF3VpbnQzMi5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLoBwoDZ3RlGAUgASgNQtgHwkjUBwqJAQoKdWludDMyLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsMBCg11aW50MzIuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCssBChd1aW50MzIuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK0wEKDnVpbnQzMi5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtsBChh1aW50MzIuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESgAEKAmluGAYgAygNQnTCSHEKbwoJdWludDMyLmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ3CgZub3RfaW4YByADKA1CZ8JIZApiCg11aW50MzIubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSLAoHZXhhbXBsZRgIIAMoDUIbwkgYChYKDnVpbnQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKeFQoLVUludDY0UnVsZXMShAEKBWNvbnN0GAEgASgEQnXCSHIKcAoMdWludDY0LmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSiwEKAmx0GAIgASgEQn3CSHoKeAoJdWludDY0Lmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp0BCgNsdGUYAyABKARCjQHCSIkBCoYBCgp1aW50NjQubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKcBwoCZ3QYBCABKARCjQfCSIkHCnsKCXVpbnQ2NC5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtAEKDHVpbnQ2NC5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvAEKFnVpbnQ2NC5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrEAQoNdWludDY0Lmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzAEKF3VpbnQ2NC5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLoBwoDZ3RlGAUgASgEQtgHwkjUBwqJAQoKdWludDY0Lmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsMBCg11aW50NjQuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCssBChd1aW50NjQuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK0wEKDnVpbnQ2NC5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtsBChh1aW50NjQuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESgAEKAmluGAYgAygEQnTCSHEKbwoJdWludDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ3CgZub3RfaW4YByADKARCZ8JIZApiCg11aW50NjQubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSLAoHZXhhbXBsZRgIIAMoBEIbwkgYChYKDnVpbnQ2NC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKeFQoLU0ludDMyUnVsZXMShAEKBWNvbnN0GAEgASgRQnXCSHIKcAoMc2ludDMyLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSiwEKAmx0GAIgASgRQn3CSHoKeAoJc2ludDMyLmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp0BCgNsdGUYAyABKBFCjQHCSIkBCoYBCgpzaW50MzIubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKcBwoCZ3QYBCABKBFCjQfCSIkHCnsKCXNpbnQzMi5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtAEKDHNpbnQzMi5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvAEKFnNpbnQzMi5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrEAQoNc2ludDMyLmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzAEKF3NpbnQzMi5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLoBwoDZ3RlGAUgASgRQtgHwkjUBwqJAQoKc2ludDMyLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsMBCg1zaW50MzIuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCssBChdzaW50MzIuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK0wEKDnNpbnQzMi5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtsBChhzaW50MzIuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESgAEKAmluGAYgAygRQnTCSHEKbwoJc2ludDMyLmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ3CgZub3RfaW4YByADKBFCZ8JIZApiCg1zaW50MzIubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSLAoHZXhhbXBsZRgIIAMoEUIbwkgYChYKDnNpbnQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKeFQoLU0ludDY0UnVsZXMShAEKBWNvbnN0GAEgASgSQnXCSHIKcAoMc2ludDY0LmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSiwEKAmx0GAIgASgSQn3CSHoKeAoJc2ludDY0Lmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp0BCgNsdGUYAyABKBJCjQHCSIkBCoYBCgpzaW50NjQubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKcBwoCZ3QYBCABKBJCjQfCSIkHCnsKCXNpbnQ2NC5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtAEKDHNpbnQ2NC5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvAEKFnNpbnQ2NC5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrEAQoNc2ludDY0Lmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzAEKF3NpbnQ2NC5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLoBwoDZ3RlGAUgASgSQtgHwkjUBwqJAQoKc2ludDY0Lmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsMBCg1zaW50NjQuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCssBChdzaW50NjQuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK0wEKDnNpbnQ2NC5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtsBChhzaW50NjQuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESgAEKAmluGAYgAygSQnTCSHEKbwoJc2ludDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ3CgZub3RfaW4YByADKBJCZ8JIZApiCg1zaW50NjQubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSLAoHZXhhbXBsZRgIIAMoEkIbwkgYChYKDnNpbnQ2NC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKvFQoMRml4ZWQzMlJ1bGVzEoUBCgVjb25zdBgBIAEoB0J2wkhzCnEKDWZpeGVkMzIuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKMAQoCbHQYAiABKAdCfsJIewp5CgpmaXhlZDMyLmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp4BCgNsdGUYAyABKAdCjgHCSIoBCocBCgtmaXhlZDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASoQcKAmd0GAQgASgHQpIHwkiOBwp8CgpmaXhlZDMyLmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq1AQoNZml4ZWQzMi5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvQEKF2ZpeGVkMzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxQEKDmZpeGVkMzIuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrNAQoYZml4ZWQzMi5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLtBwoDZ3RlGAUgASgHQt0HwkjZBwqKAQoLZml4ZWQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrEAQoOZml4ZWQzMi5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzAEKGGZpeGVkMzIuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK1AEKD2ZpeGVkMzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrcAQoZZml4ZWQzMi5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARKBAQoCaW4YBiADKAdCdcJIcgpwCgpmaXhlZDMyLmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ4CgZub3RfaW4YByADKAdCaMJIZQpjCg5maXhlZDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEi0KB2V4YW1wbGUYCCADKAdCHMJIGQoXCg9maXhlZDMyLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIq8VCgxGaXhlZDY0UnVsZXMShQEKBWNvbnN0GAEgASgGQnbCSHMKcQoNZml4ZWQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEowBCgJsdBgCIAEoBkJ+wkh7CnkKCmZpeGVkNjQubHQaayFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID49IHJ1bGVzLmx0PyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASngEKA2x0ZRgDIAEoBkKOAcJIigEKhwEKC2ZpeGVkNjQubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKhBwoCZ3QYBCABKAZCkgfCSI4HCnwKCmZpeGVkNjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrUBCg1maXhlZDY0Lmd0X2x0GqMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq9AQoXZml4ZWQ2NC5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrFAQoOZml4ZWQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCs0BChhmaXhlZDY0Lmd0X2x0ZV9leGNsdXNpdmUasAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEu0HCgNndGUYBSABKAZC3QfCSNkHCooBCgtmaXhlZDY0Lmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsQBCg5maXhlZDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrMAQoYZml4ZWQ2NC5ndGVfbHRfZXhjbHVzaXZlGq8BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrUAQoPZml4ZWQ2NC5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtwBChlmaXhlZDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoEBCgJpbhgGIAMoBkJ1wkhyCnAKCmZpeGVkNjQuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEngKBm5vdF9pbhgHIAMoBkJowkhlCmMKDmZpeGVkNjQubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSLQoHZXhhbXBsZRgIIAMoBkIcwkgZChcKD2ZpeGVkNjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4iwBUKDVNGaXhlZDMyUnVsZXMShgEKBWNvbnN0GAEgASgPQnfCSHQKcgoOc2ZpeGVkMzIuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKNAQoCbHQYAiABKA9Cf8JIfAp6CgtzZml4ZWQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKfAQoDbHRlGAMgASgPQo8BwkiLAQqIAQoMc2ZpeGVkMzIubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKmBwoCZ3QYBCABKA9ClwfCSJMHCn0KC3NmaXhlZDMyLmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq2AQoOc2ZpeGVkMzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr4BChhzZml4ZWQzMi5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrGAQoPc2ZpeGVkMzIuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrOAQoZc2ZpeGVkMzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES8gcKA2d0ZRgFIAEoD0LiB8JI3gcKiwEKDHNmaXhlZDMyLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsUBCg9zZml4ZWQzMi5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzQEKGXNmaXhlZDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtUBChBzZml4ZWQzMi5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCt0BChpzZml4ZWQzMi5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARKCAQoCaW4YBiADKA9CdsJIcwpxCgtzZml4ZWQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSeQoGbm90X2luGAcgAygPQmnCSGYKZAoPc2ZpeGVkMzIubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSLgoHZXhhbXBsZRgIIAMoD0IdwkgaChgKEHNmaXhlZDMyLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIsAVCg1TRml4ZWQ2NFJ1bGVzEoYBCgVjb25zdBgBIAEoEEJ3wkh0CnIKDnNmaXhlZDY0LmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSjQEKAmx0GAIgASgQQn/CSHwKegoLc2ZpeGVkNjQubHQaayFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID49IHJ1bGVzLmx0PyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASnwEKA2x0ZRgDIAEoEEKPAcJIiwEKiAEKDHNmaXhlZDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASpgcKAmd0GAQgASgQQpcHwkiTBwp9CgtzZml4ZWQ2NC5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtgEKDnNmaXhlZDY0Lmd0X2x0GqMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq+AQoYc2ZpeGVkNjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxgEKD3NmaXhlZDY0Lmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzgEKGXNmaXhlZDY0Lmd0X2x0ZV9leGNsdXNpdmUasAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEvIHCgNndGUYBSABKBBC4gfCSN4HCosBCgxzZml4ZWQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrFAQoPc2ZpeGVkNjQuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCs0BChlzZml4ZWQ2NC5ndGVfbHRfZXhjbHVzaXZlGq8BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrVAQoQc2ZpeGVkNjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrdAQoac2ZpeGVkNjQuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESggEKAmluGAYgAygQQnbCSHMKcQoLc2ZpeGVkNjQuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnkKBm5vdF9pbhgHIAMoEEJpwkhmCmQKD3NmaXhlZDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEi4KB2V4YW1wbGUYCCADKBBCHcJIGgoYChBzZml4ZWQ2NC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiLHAQoJQm9vbFJ1bGVzEoIBCgVjb25zdBgBIAEoCEJzwkhwCm4KCmJvb2wuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxIqCgdleGFtcGxlGAIgAygIQhnCSBYKFAoMYm9vbC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAIiiDkKC1N0cmluZ1J1bGVzEoYBCgVjb25zdBgBIAEoCUJ3wkh0CnIKDHN0cmluZy5jb25zdBpidGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCBgJXNgJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSfgoDbGVuGBMgASgEQnHCSG4KbAoKc3RyaW5nLmxlbhpedWludCh0aGlzLnNpemUoKSkgIT0gcnVsZXMubGVuID8gJ3ZhbHVlIGxlbmd0aCBtdXN0IGJlICVzIGNoYXJhY3RlcnMnLmZvcm1hdChbcnVsZXMubGVuXSkgOiAnJxKZAQoHbWluX2xlbhgCIAEoBEKHAcJIgwEKgAEKDnN0cmluZy5taW5fbGVuGm51aW50KHRoaXMuc2l6ZSgpKSA8IHJ1bGVzLm1pbl9sZW4gPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgYXQgbGVhc3QgJXMgY2hhcmFjdGVycycuZm9ybWF0KFtydWxlcy5taW5fbGVuXSkgOiAnJxKXAQoHbWF4X2xlbhgDIAEoBEKFAcJIgQEKfwoOc3RyaW5nLm1heF9sZW4abXVpbnQodGhpcy5zaXplKCkpID4gcnVsZXMubWF4X2xlbiA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSBhdCBtb3N0ICVzIGNoYXJhY3RlcnMnLmZvcm1hdChbcnVsZXMubWF4X2xlbl0pIDogJycSmwEKCWxlbl9ieXRlcxgUIAEoBEKHAcJIgwEKgAEKEHN0cmluZy5sZW5fYnl0ZXMabHVpbnQoYnl0ZXModGhpcykuc2l6ZSgpKSAhPSBydWxlcy5sZW5fYnl0ZXMgPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubGVuX2J5dGVzXSkgOiAnJxKjAQoJbWluX2J5dGVzGAQgASgEQo8BwkiLAQqIAQoQc3RyaW5nLm1pbl9ieXRlcxp0dWludChieXRlcyh0aGlzKS5zaXplKCkpIDwgcnVsZXMubWluX2J5dGVzID8gJ3ZhbHVlIGxlbmd0aCBtdXN0IGJlIGF0IGxlYXN0ICVzIGJ5dGVzJy5mb3JtYXQoW3J1bGVzLm1pbl9ieXRlc10pIDogJycSogEKCW1heF9ieXRlcxgFIAEoBEKOAcJIigEKhwEKEHN0cmluZy5tYXhfYnl0ZXMac3VpbnQoYnl0ZXModGhpcykuc2l6ZSgpKSA+IHJ1bGVzLm1heF9ieXRlcyA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSBhdCBtb3N0ICVzIGJ5dGVzJy5mb3JtYXQoW3J1bGVzLm1heF9ieXRlc10pIDogJycSjQEKB3BhdHRlcm4YBiABKAlCfMJIeQp3Cg5zdHJpbmcucGF0dGVybhplIXRoaXMubWF0Y2hlcyhydWxlcy5wYXR0ZXJuKSA/ICd2YWx1ZSBkb2VzIG5vdCBtYXRjaCByZWdleCBwYXR0ZXJuIGAlc2AnLmZvcm1hdChbcnVsZXMucGF0dGVybl0pIDogJycShAEKBnByZWZpeBgHIAEoCUJ0wkhxCm8KDXN0cmluZy5wcmVmaXgaXiF0aGlzLnN0YXJ0c1dpdGgocnVsZXMucHJlZml4KSA/ICd2YWx1ZSBkb2VzIG5vdCBoYXZlIHByZWZpeCBgJXNgJy5mb3JtYXQoW3J1bGVzLnByZWZpeF0pIDogJycSggEKBnN1ZmZpeBgIIAEoCUJywkhvCm0KDXN0cmluZy5zdWZmaXgaXCF0aGlzLmVuZHNXaXRoKHJ1bGVzLnN1ZmZpeCkgPyAndmFsdWUgZG9lcyBub3QgaGF2ZSBzdWZmaXggYCVzYCcuZm9ybWF0KFtydWxlcy5zdWZmaXhdKSA6ICcnEpABCghjb250YWlucxgJIAEoCUJ+wkh7CnkKD3N0cmluZy5jb250YWlucxpmIXRoaXMuY29udGFpbnMocnVsZXMuY29udGFpbnMpID8gJ3ZhbHVlIGRvZXMgbm90IGNvbnRhaW4gc3Vic3RyaW5nIGAlc2AnLmZvcm1hdChbcnVsZXMuY29udGFpbnNdKSA6ICcnEpgBCgxub3RfY29udGFpbnMYFyABKAlCgQHCSH4KfAoTc3RyaW5nLm5vdF9jb250YWlucxpldGhpcy5jb250YWlucyhydWxlcy5ub3RfY29udGFpbnMpID8gJ3ZhbHVlIGNvbnRhaW5zIHN1YnN0cmluZyBgJXNgJy5mb3JtYXQoW3J1bGVzLm5vdF9jb250YWluc10pIDogJycSgAEKAmluGAogAygJQnTCSHEKbwoJc3RyaW5nLmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ3CgZub3RfaW4YCyADKAlCZ8JIZApiCg1zdHJpbmcubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycS3wEKBWVtYWlsGAwgASgIQs0BwkjJAQphCgxzdHJpbmcuZW1haWwSI3ZhbHVlIG11c3QgYmUgYSB2YWxpZCBlbWFpbCBhZGRyZXNzGiwhcnVsZXMuZW1haWwgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzRW1haWwoKQpkChJzdHJpbmcuZW1haWxfZW1wdHkSMnZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBlbWFpbCBhZGRyZXNzGhohcnVsZXMuZW1haWwgfHwgdGhpcyAhPSAnJ0gAEucBCghob3N0bmFtZRgNIAEoCELSAcJIzgEKZQoPc3RyaW5nLmhvc3RuYW1lEh52YWx1ZSBtdXN0IGJlIGEgdmFsaWQgaG9zdG5hbWUaMiFydWxlcy5ob3N0bmFtZSB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNIb3N0bmFtZSgpCmUKFXN0cmluZy5ob3N0bmFtZV9lbXB0eRItdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIGhvc3RuYW1lGh0hcnVsZXMuaG9zdG5hbWUgfHwgdGhpcyAhPSAnJ0gAEscBCgJpcBgOIAEoCEK4AcJItAEKVQoJc3RyaW5nLmlwEiB2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVAgYWRkcmVzcxomIXJ1bGVzLmlwIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwKCkKWwoPc3RyaW5nLmlwX2VtcHR5Ei92YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVAgYWRkcmVzcxoXIXJ1bGVzLmlwIHx8IHRoaXMgIT0gJydIABLWAQoEaXB2NBgPIAEoCELFAcJIwQEKXAoLc3RyaW5nLmlwdjQSInZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUHY0IGFkZHJlc3MaKSFydWxlcy5pcHY0IHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwKDQpCmEKEXN0cmluZy5pcHY0X2VtcHR5EjF2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NCBhZGRyZXNzGhkhcnVsZXMuaXB2NCB8fCB0aGlzICE9ICcnSAAS1gEKBGlwdjYYECABKAhCxQHCSMEBClwKC3N0cmluZy5pcHY2EiJ2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NiBhZGRyZXNzGikhcnVsZXMuaXB2NiB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcCg2KQphChFzdHJpbmcuaXB2Nl9lbXB0eRIxdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjYgYWRkcmVzcxoZIXJ1bGVzLmlwdjYgfHwgdGhpcyAhPSAnJ0gAEr8BCgN1cmkYESABKAhCrwHCSKsBClEKCnN0cmluZy51cmkSGXZhbHVlIG11c3QgYmUgYSB2YWxpZCBVUkkaKCFydWxlcy51cmkgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzVXJpKCkKVgoQc3RyaW5nLnVyaV9lbXB0eRIodmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIFVSSRoYIXJ1bGVzLnVyaSB8fCB0aGlzICE9ICcnSAAScAoHdXJpX3JlZhgSIAEoCEJdwkhaClgKDnN0cmluZy51cmlfcmVmEiN2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgVVJJIFJlZmVyZW5jZRohIXJ1bGVzLnVyaV9yZWYgfHwgdGhpcy5pc1VyaVJlZigpSAASkAIKB2FkZHJlc3MYFSABKAhC/AHCSPgBCoEBCg5zdHJpbmcuYWRkcmVzcxItdmFsdWUgbXVzdCBiZSBhIHZhbGlkIGhvc3RuYW1lLCBvciBpcCBhZGRyZXNzGkAhcnVsZXMuYWRkcmVzcyB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNIb3N0bmFtZSgpIHx8IHRoaXMuaXNJcCgpCnIKFHN0cmluZy5hZGRyZXNzX2VtcHR5Ejx2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgaG9zdG5hbWUsIG9yIGlwIGFkZHJlc3MaHCFydWxlcy5hZGRyZXNzIHx8IHRoaXMgIT0gJydIABKYAgoEdXVpZBgWIAEoCEKHAsJIgwIKpQEKC3N0cmluZy51dWlkEhp2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgVVVJRBp6IXJ1bGVzLnV1aWQgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLm1hdGNoZXMoJ15bMC05YS1mQS1GXXs4fS1bMC05YS1mQS1GXXs0fS1bMC05YS1mQS1GXXs0fS1bMC05YS1mQS1GXXs0fS1bMC05YS1mQS1GXXsxMn0kJykKWQoRc3RyaW5nLnV1aWRfZW1wdHkSKXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBVVUlEGhkhcnVsZXMudXVpZCB8fCB0aGlzICE9ICcnSAAS8AEKBXR1dWlkGCEgASgIQt4BwkjaAQpzCgxzdHJpbmcudHV1aWQSInZhbHVlIG11c3QgYmUgYSB2YWxpZCB0cmltbWVkIFVVSUQaPyFydWxlcy50dXVpZCB8fCB0aGlzID09ICcnIHx8IHRoaXMubWF0Y2hlcygnXlswLTlhLWZBLUZdezMyfSQnKQpjChJzdHJpbmcudHV1aWRfZW1wdHkSMXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCB0cmltbWVkIFVVSUQaGiFydWxlcy50dXVpZCB8fCB0aGlzICE9ICcnSAASlgIKEWlwX3dpdGhfcHJlZml4bGVuGBogASgIQvgBwkj0AQp4ChhzdHJpbmcuaXBfd2l0aF9wcmVmaXhsZW4SH3ZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUCBwcmVmaXgaOyFydWxlcy5pcF93aXRoX3ByZWZpeGxlbiB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcFByZWZpeCgpCngKHnN0cmluZy5pcF93aXRoX3ByZWZpeGxlbl9lbXB0eRIudmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIHByZWZpeBomIXJ1bGVzLmlwX3dpdGhfcHJlZml4bGVuIHx8IHRoaXMgIT0gJydIABLPAgoTaXB2NF93aXRoX3ByZWZpeGxlbhgbIAEoCEKvAsJIqwIKkwEKGnN0cmluZy5pcHY0X3dpdGhfcHJlZml4bGVuEjV2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NCBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBo+IXJ1bGVzLmlwdjRfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoNCkKkgEKIHN0cmluZy5pcHY0X3dpdGhfcHJlZml4bGVuX2VtcHR5EkR2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NCBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBooIXJ1bGVzLmlwdjRfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyAhPSAnJ0gAEs8CChNpcHY2X3dpdGhfcHJlZml4bGVuGBwgASgIQq8CwkirAgqTAQoac3RyaW5nLmlwdjZfd2l0aF9wcmVmaXhsZW4SNXZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUHY2IGFkZHJlc3Mgd2l0aCBwcmVmaXggbGVuZ3RoGj4hcnVsZXMuaXB2Nl93aXRoX3ByZWZpeGxlbiB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcFByZWZpeCg2KQqSAQogc3RyaW5nLmlwdjZfd2l0aF9wcmVmaXhsZW5fZW1wdHkSRHZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY2IGFkZHJlc3Mgd2l0aCBwcmVmaXggbGVuZ3RoGighcnVsZXMuaXB2Nl93aXRoX3ByZWZpeGxlbiB8fCB0aGlzICE9ICcnSAAS8gEKCWlwX3ByZWZpeBgdIAEoCELcAcJI2AEKbAoQc3RyaW5nLmlwX3ByZWZpeBIfdmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQIHByZWZpeBo3IXJ1bGVzLmlwX3ByZWZpeCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcFByZWZpeCh0cnVlKQpoChZzdHJpbmcuaXBfcHJlZml4X2VtcHR5Ei52YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVAgcHJlZml4Gh4hcnVsZXMuaXBfcHJlZml4IHx8IHRoaXMgIT0gJydIABKDAgoLaXB2NF9wcmVmaXgYHiABKAhC6wHCSOcBCnUKEnN0cmluZy5pcHY0X3ByZWZpeBIhdmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjQgcHJlZml4GjwhcnVsZXMuaXB2NF9wcmVmaXggfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoNCwgdHJ1ZSkKbgoYc3RyaW5nLmlwdjRfcHJlZml4X2VtcHR5EjB2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NCBwcmVmaXgaICFydWxlcy5pcHY0X3ByZWZpeCB8fCB0aGlzICE9ICcnSAASgwIKC2lwdjZfcHJlZml4GB8gASgIQusBwkjnAQp1ChJzdHJpbmcuaXB2Nl9wcmVmaXgSIXZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUHY2IHByZWZpeBo8IXJ1bGVzLmlwdjZfcHJlZml4IHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwUHJlZml4KDYsIHRydWUpCm4KGHN0cmluZy5pcHY2X3ByZWZpeF9lbXB0eRIwdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjYgcHJlZml4GiAhcnVsZXMuaXB2Nl9wcmVmaXggfHwgdGhpcyAhPSAnJ0gAErUCCg1ob3N0X2FuZF9wb3J0GCAgASgIQpsCwkiXAgqZAQoUc3RyaW5nLmhvc3RfYW5kX3BvcnQSQXZhbHVlIG11c3QgYmUgYSB2YWxpZCBob3N0IChob3N0bmFtZSBvciBJUCBhZGRyZXNzKSBhbmQgcG9ydCBwYWlyGj4hcnVsZXMuaG9zdF9hbmRfcG9ydCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNIb3N0QW5kUG9ydCh0cnVlKQp5ChpzdHJpbmcuaG9zdF9hbmRfcG9ydF9lbXB0eRI3dmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIGhvc3QgYW5kIHBvcnQgcGFpchoiIXJ1bGVzLmhvc3RfYW5kX3BvcnQgfHwgdGhpcyAhPSAnJ0gAEvUBCgR1bGlkGCMgASgIQuQBwkjgAQqCAQoLc3RyaW5nLnVsaWQSGnZhbHVlIG11c3QgYmUgYSB2YWxpZCBVTElEGlchcnVsZXMudWxpZCB8fCB0aGlzID09ICcnIHx8IHRoaXMubWF0Y2hlcygnXlswLTddWzAtOUEtSEpLTU5QLVRWLVphLWhqa21ucC10di16XXsyNX0kJykKWQoRc3RyaW5nLnVsaWRfZW1wdHkSKXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBVTElEGhkhcnVsZXMudWxpZCB8fCB0aGlzICE9ICcnSAASqAUKEHdlbGxfa25vd25fcmVnZXgYGCABKA4yGC5idWYudmFsaWRhdGUuS25vd25SZWdleELxBMJI7QQK8AEKI3N0cmluZy53ZWxsX2tub3duX3JlZ2V4LmhlYWRlcl9uYW1lEiZ2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSFRUUCBoZWFkZXIgbmFtZRqgAXJ1bGVzLndlbGxfa25vd25fcmVnZXggIT0gMSB8fCB0aGlzID09ICcnIHx8IHRoaXMubWF0Y2hlcyghaGFzKHJ1bGVzLnN0cmljdCkgfHwgcnVsZXMuc3RyaWN0ID8nXjo/WzAtOWEtekEtWiEjJCUmXCcqKy0uXl98flx4NjBdKyQnIDonXlteXHUwMDAwXHUwMDBBXHUwMDBEXSskJykKjQEKKXN0cmluZy53ZWxsX2tub3duX3JlZ2V4LmhlYWRlcl9uYW1lX2VtcHR5EjV2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSFRUUCBoZWFkZXIgbmFtZRopcnVsZXMud2VsbF9rbm93bl9yZWdleCAhPSAxIHx8IHRoaXMgIT0gJycK5wEKJHN0cmluZy53ZWxsX2tub3duX3JlZ2V4LmhlYWRlcl92YWx1ZRIndmFsdWUgbXVzdCBiZSBhIHZhbGlkIEhUVFAgaGVhZGVyIHZhbHVlGpUBcnVsZXMud2VsbF9rbm93bl9yZWdleCAhPSAyIHx8IHRoaXMubWF0Y2hlcyghaGFzKHJ1bGVzLnN0cmljdCkgfHwgcnVsZXMuc3RyaWN0ID8nXlteXHUwMDAwLVx1MDAwOFx1MDAwQS1cdTAwMUZcdTAwN0ZdKiQnIDonXlteXHUwMDAwXHUwMDBBXHUwMDBEXSokJylIABIOCgZzdHJpY3QYGSABKAgSLAoHZXhhbXBsZRgiIAMoCUIbwkgYChYKDnN0cmluZy5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCDAoKd2VsbF9rbm93biLCEgoKQnl0ZXNSdWxlcxKAAQoFY29uc3QYASABKAxCccJIbgpsCgtieXRlcy5jb25zdBpddGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBiZSAleCcuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEngKA2xlbhgNIAEoBEJrwkhoCmYKCWJ5dGVzLmxlbhpZdWludCh0aGlzLnNpemUoKSkgIT0gcnVsZXMubGVuID8gJ3ZhbHVlIGxlbmd0aCBtdXN0IGJlICVzIGJ5dGVzJy5mb3JtYXQoW3J1bGVzLmxlbl0pIDogJycSkAEKB21pbl9sZW4YAiABKARCf8JIfAp6Cg1ieXRlcy5taW5fbGVuGml1aW50KHRoaXMuc2l6ZSgpKSA8IHJ1bGVzLm1pbl9sZW4gPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgYXQgbGVhc3QgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubWluX2xlbl0pIDogJycSiAEKB21heF9sZW4YAyABKARCd8JIdApyCg1ieXRlcy5tYXhfbGVuGmF1aW50KHRoaXMuc2l6ZSgpKSA+IHJ1bGVzLm1heF9sZW4gPyAndmFsdWUgbXVzdCBiZSBhdCBtb3N0ICVzIGJ5dGVzJy5mb3JtYXQoW3J1bGVzLm1heF9sZW5dKSA6ICcnEpABCgdwYXR0ZXJuGAQgASgJQn/CSHwKegoNYnl0ZXMucGF0dGVybhppIXN0cmluZyh0aGlzKS5tYXRjaGVzKHJ1bGVzLnBhdHRlcm4pID8gJ3ZhbHVlIG11c3QgbWF0Y2ggcmVnZXggcGF0dGVybiBgJXNgJy5mb3JtYXQoW3J1bGVzLnBhdHRlcm5dKSA6ICcnEoEBCgZwcmVmaXgYBSABKAxCccJIbgpsCgxieXRlcy5wcmVmaXgaXCF0aGlzLnN0YXJ0c1dpdGgocnVsZXMucHJlZml4KSA/ICd2YWx1ZSBkb2VzIG5vdCBoYXZlIHByZWZpeCAleCcuZm9ybWF0KFtydWxlcy5wcmVmaXhdKSA6ICcnEn8KBnN1ZmZpeBgGIAEoDEJvwkhsCmoKDGJ5dGVzLnN1ZmZpeBpaIXRoaXMuZW5kc1dpdGgocnVsZXMuc3VmZml4KSA/ICd2YWx1ZSBkb2VzIG5vdCBoYXZlIHN1ZmZpeCAleCcuZm9ybWF0KFtydWxlcy5zdWZmaXhdKSA6ICcnEoMBCghjb250YWlucxgHIAEoDEJxwkhuCmwKDmJ5dGVzLmNvbnRhaW5zGlohdGhpcy5jb250YWlucyhydWxlcy5jb250YWlucykgPyAndmFsdWUgZG9lcyBub3QgY29udGFpbiAleCcuZm9ybWF0KFtydWxlcy5jb250YWluc10pIDogJycSpwEKAmluGAggAygMQpoBwkiWAQqTAQoIYnl0ZXMuaW4ahgFnZXRGaWVsZChydWxlcywgJ2luJykuc2l6ZSgpID4gMCAmJiAhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ2CgZub3RfaW4YCSADKAxCZsJIYwphCgxieXRlcy5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxLrAQoCaXAYCiABKAhC3AHCSNgBCnQKCGJ5dGVzLmlwEiB2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVAgYWRkcmVzcxpGIXJ1bGVzLmlwIHx8IHRoaXMuc2l6ZSgpID09IDAgfHwgdGhpcy5zaXplKCkgPT0gNCB8fCB0aGlzLnNpemUoKSA9PSAxNgpgCg5ieXRlcy5pcF9lbXB0eRIvdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIGFkZHJlc3MaHSFydWxlcy5pcCB8fCB0aGlzLnNpemUoKSAhPSAwSAAS5AEKBGlwdjQYCyABKAhC0wHCSM8BCmUKCmJ5dGVzLmlwdjQSInZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUHY0IGFkZHJlc3MaMyFydWxlcy5pcHY0IHx8IHRoaXMuc2l6ZSgpID09IDAgfHwgdGhpcy5zaXplKCkgPT0gNApmChBieXRlcy5pcHY0X2VtcHR5EjF2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NCBhZGRyZXNzGh8hcnVsZXMuaXB2NCB8fCB0aGlzLnNpemUoKSAhPSAwSAAS5QEKBGlwdjYYDCABKAhC1AHCSNABCmYKCmJ5dGVzLmlwdjYSInZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUHY2IGFkZHJlc3MaNCFydWxlcy5pcHY2IHx8IHRoaXMuc2l6ZSgpID09IDAgfHwgdGhpcy5zaXplKCkgPT0gMTYKZgoQYnl0ZXMuaXB2Nl9lbXB0eRIxdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjYgYWRkcmVzcxofIXJ1bGVzLmlwdjYgfHwgdGhpcy5zaXplKCkgIT0gMEgAEtUBCgR1dWlkGA8gASgIQsQBwkjAAQpeCgpieXRlcy51dWlkEhp2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgVVVJRBo0IXJ1bGVzLnV1aWQgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSAxNgpeChBieXRlcy51dWlkX2VtcHR5Eil2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgVVVJRBofIXJ1bGVzLnV1aWQgfHwgdGhpcy5zaXplKCkgIT0gMEgAEisKB2V4YW1wbGUYDiADKAxCGsJIFwoVCg1ieXRlcy5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCDAoKd2VsbF9rbm93biLUAwoJRW51bVJ1bGVzEoIBCgVjb25zdBgBIAEoBUJzwkhwCm4KCmVudW0uY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxIUCgxkZWZpbmVkX29ubHkYAiABKAgSfgoCaW4YAyADKAVCcsJIbwptCgdlbnVtLmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ1CgZub3RfaW4YBCADKAVCZcJIYgpgCgtlbnVtLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEioKB2V4YW1wbGUYBSADKAVCGcJIFgoUCgxlbnVtLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAiL7AwoNUmVwZWF0ZWRSdWxlcxKeAQoJbWluX2l0ZW1zGAEgASgEQooBwkiGAQqDAQoScmVwZWF0ZWQubWluX2l0ZW1zGm11aW50KHRoaXMuc2l6ZSgpKSA8IHJ1bGVzLm1pbl9pdGVtcyA/ICd2YWx1ZSBtdXN0IGNvbnRhaW4gYXQgbGVhc3QgJWQgaXRlbShzKScuZm9ybWF0KFtydWxlcy5taW5faXRlbXNdKSA6ICcnEqIBCgltYXhfaXRlbXMYAiABKARCjgHCSIoBCocBChJyZXBlYXRlZC5tYXhfaXRlbXMacXVpbnQodGhpcy5zaXplKCkpID4gcnVsZXMubWF4X2l0ZW1zID8gJ3ZhbHVlIG11c3QgY29udGFpbiBubyBtb3JlIHRoYW4gJXMgaXRlbShzKScuZm9ybWF0KFtydWxlcy5tYXhfaXRlbXNdKSA6ICcnEnAKBnVuaXF1ZRgDIAEoCEJgwkhdClsKD3JlcGVhdGVkLnVuaXF1ZRIocmVwZWF0ZWQgdmFsdWUgbXVzdCBjb250YWluIHVuaXF1ZSBpdGVtcxoeIXJ1bGVzLnVuaXF1ZSB8fCB0aGlzLnVuaXF1ZSgpEicKBWl0ZW1zGAQgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXMqCQjoBxCAgICAAiKKAwoITWFwUnVsZXMSjwEKCW1pbl9wYWlycxgBIAEoBEJ8wkh5CncKDW1hcC5taW5fcGFpcnMaZnVpbnQodGhpcy5zaXplKCkpIDwgcnVsZXMubWluX3BhaXJzID8gJ21hcCBtdXN0IGJlIGF0IGxlYXN0ICVkIGVudHJpZXMnLmZvcm1hdChbcnVsZXMubWluX3BhaXJzXSkgOiAnJxKOAQoJbWF4X3BhaXJzGAIgASgEQnvCSHgKdgoNbWFwLm1heF9wYWlycxpldWludCh0aGlzLnNpemUoKSkgPiBydWxlcy5tYXhfcGFpcnMgPyAnbWFwIG11c3QgYmUgYXQgbW9zdCAlZCBlbnRyaWVzJy5mb3JtYXQoW3J1bGVzLm1heF9wYWlyc10pIDogJycSJgoEa2V5cxgEIAEoCzIYLmJ1Zi52YWxpZGF0ZS5GaWVsZFJ1bGVzEigKBnZhbHVlcxgFIAEoCzIYLmJ1Zi52YWxpZGF0ZS5GaWVsZFJ1bGVzKgkI6AcQgICAgAIiJgoIQW55UnVsZXMSCgoCaW4YAiADKAkSDgoGbm90X2luGAMgAygJIpkXCg1EdXJhdGlvblJ1bGVzEqEBCgVjb25zdBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkJ3wkh0CnIKDmR1cmF0aW9uLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSqAEKAmx0GAMgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQn/CSHwKegoLZHVyYXRpb24ubHQaayFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID49IHJ1bGVzLmx0PyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASugEKA2x0ZRgEIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkKPAcJIiwEKiAEKDGR1cmF0aW9uLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASwQcKAmd0GAUgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQpcHwkiTBwp9CgtkdXJhdGlvbi5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtgEKDmR1cmF0aW9uLmd0X2x0GqMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq+AQoYZHVyYXRpb24uZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxgEKD2R1cmF0aW9uLmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzgEKGWR1cmF0aW9uLmd0X2x0ZV9leGNsdXNpdmUasAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEo0ICgNndGUYBiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25C4gfCSN4HCosBCgxkdXJhdGlvbi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrFAQoPZHVyYXRpb24uZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCs0BChlkdXJhdGlvbi5ndGVfbHRfZXhjbHVzaXZlGq8BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrVAQoQZHVyYXRpb24uZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrdAQoaZHVyYXRpb24uZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESnQEKAmluGAcgAygLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQnbCSHMKcQoLZHVyYXRpb24uaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEpQBCgZub3RfaW4YCCADKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CacJIZgpkCg9kdXJhdGlvbi5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxJJCgdleGFtcGxlGAkgAygLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQh3CSBoKGAoQZHVyYXRpb24uZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4i/QUKDkZpZWxkTWFza1J1bGVzEr8BCgVjb25zdBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCkwHCSI8BCowBChBmaWVsZF9tYXNrLmNvbnN0Gnh0aGlzLnBhdGhzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKS5wYXRocyA/ICd2YWx1ZSBtdXN0IGVxdWFsIHBhdGhzICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKS5wYXRoc10pIDogJycS2QEKAmluGAIgAygJQswBwkjIAQrFAQoNZmllbGRfbWFzay5pbhqzASF0aGlzLnBhdGhzLmFsbChwLCBwIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSB8fCBnZXRGaWVsZChydWxlcywgJ2luJykuZXhpc3RzKGYsIHAuc3RhcnRzV2l0aChmKycuJykpKSA/ICd2YWx1ZSBtdXN0IG9ubHkgY29udGFpbiBwYXRocyBpbiAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEvMBCgZub3RfaW4YAyADKAlC4gHCSN4BCtsBChFmaWVsZF9tYXNrLm5vdF9pbhrFASF0aGlzLnBhdGhzLmFsbChwLCAhKHAgaW4gZ2V0RmllbGQocnVsZXMsICdub3RfaW4nKSB8fCBnZXRGaWVsZChydWxlcywgJ25vdF9pbicpLmV4aXN0cyhmLCBwLnN0YXJ0c1dpdGgoZisnLicpKSkpID8gJ3ZhbHVlIG11c3Qgbm90IGNvbnRhaW4gYW55IHBhdGhzIGluICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnbm90X2luJyldKSA6ICcnEkwKB2V4YW1wbGUYBCADKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQh/CSBwKGgoSZmllbGRfbWFzay5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAIikhgKDlRpbWVzdGFtcFJ1bGVzEqMBCgVjb25zdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCeMJIdQpzCg90aW1lc3RhbXAuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKrAQoCbHQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQoABwkh9CnsKDHRpbWVzdGFtcC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABK8AQoDbHRlGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEKQAcJIjAEKiQEKDXRpbWVzdGFtcC5sdGUaeCFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEmwKBmx0X25vdxgHIAEoCEJawkhXClUKEHRpbWVzdGFtcC5sdF9ub3caQShydWxlcy5sdF9ub3cgJiYgdGhpcyA+IG5vdykgPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gbm93JyA6ICcnSAASxwcKAmd0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEKcB8JImAcKfgoMdGltZXN0YW1wLmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq3AQoPdGltZXN0YW1wLmd0X2x0GqMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq/AQoZdGltZXN0YW1wLmd0X2x0X2V4Y2x1c2l2ZRqhAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCscBChB0aW1lc3RhbXAuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrPAQoadGltZXN0YW1wLmd0X2x0ZV9leGNsdXNpdmUasAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEpMICgNndGUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQucHwkjjBwqMAQoNdGltZXN0YW1wLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsYBChB0aW1lc3RhbXAuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCs4BChp0aW1lc3RhbXAuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK1gEKEXRpbWVzdGFtcC5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCt4BCht0aW1lc3RhbXAuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESbwoGZ3Rfbm93GAggASgIQl3CSFoKWAoQdGltZXN0YW1wLmd0X25vdxpEKHJ1bGVzLmd0X25vdyAmJiB0aGlzIDwgbm93KSA/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBub3cnIDogJydIARK4AQoGd2l0aGluGAkgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQowBwkiIAQqFAQoQdGltZXN0YW1wLndpdGhpbhpxdGhpcyA8IG5vdy1ydWxlcy53aXRoaW4gfHwgdGhpcyA+IG5vdytydWxlcy53aXRoaW4gPyAndmFsdWUgbXVzdCBiZSB3aXRoaW4gJXMgb2Ygbm93Jy5mb3JtYXQoW3J1bGVzLndpdGhpbl0pIDogJycSSwoHZXhhbXBsZRgKIAMoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCHsJIGwoZChF0aW1lc3RhbXAuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4iOQoKVmlvbGF0aW9ucxIrCgp2aW9sYXRpb25zGAEgAygLMhcuYnVmLnZhbGlkYXRlLlZpb2xhdGlvbiKfAQoJVmlvbGF0aW9uEiYKBWZpZWxkGAUgASgLMhcuYnVmLnZhbGlkYXRlLkZpZWxkUGF0aBIlCgRydWxlGAYgASgLMhcuYnVmLnZhbGlkYXRlLkZpZWxkUGF0aBIPCgdydWxlX2lkGAIgASgJEg8KB21lc3NhZ2UYAyABKAkSDwoHZm9yX2tleRgEIAEoCEoECAEQAlIKZmllbGRfcGF0aCI9CglGaWVsZFBhdGgSMAoIZWxlbWVudHMYASADKAsyHi5idWYudmFsaWRhdGUuRmllbGRQYXRoRWxlbWVudCLpAgoQRmllbGRQYXRoRWxlbWVudBIUCgxmaWVsZF9udW1iZXIYASABKAUSEgoKZmllbGRfbmFtZRgCIAEoCRI+CgpmaWVsZF90eXBlGAMgASgOMiouZ29vZ2xlLnByb3RvYnVmLkZpZWxkRGVzY3JpcHRvclByb3RvLlR5cGUSPAoIa2V5X3R5cGUYBCABKA4yKi5nb29nbGUucHJvdG9idWYuRmllbGREZXNjcmlwdG9yUHJvdG8uVHlwZRI+Cgp2YWx1ZV90eXBlGAUgASgOMiouZ29vZ2xlLnByb3RvYnVmLkZpZWxkRGVzY3JpcHRvclByb3RvLlR5cGUSDwoFaW5kZXgYBiABKARIABISCghib29sX2tleRgHIAEoCEgAEhEKB2ludF9rZXkYCCABKANIABISCgh1aW50X2tleRgJIAEoBEgAEhQKCnN0cmluZ19rZXkYCiABKAlIAEILCglzdWJzY3JpcHQqoQEKBklnbm9yZRIWChJJR05PUkVfVU5TUEVDSUZJRUQQABIYChRJR05PUkVfSUZfWkVST19WQUxVRRABEhEKDUlHTk9SRV9BTFdBWVMQAyIECAIQAioMSUdOT1JFX0VNUFRZKg5JR05PUkVfREVGQVVMVCoXSUdOT1JFX0lGX0RFRkFVTFRfVkFMVUUqFUlHTk9SRV9JRl9VTlBPUFVMQVRFRCpuCgpLbm93blJlZ2V4EhsKF0tOT1dOX1JFR0VYX1VOU1BFQ0lGSUVEEAASIAocS05PV05fUkVHRVhfSFRUUF9IRUFERVJfTkFNRRABEiEKHUtOT1dOX1JFR0VYX0hUVFBfSEVBREVSX1ZBTFVFEAI6VgoHbWVzc2FnZRIfLmdvb2dsZS5wcm90b2J1Zi5NZXNzYWdlT3B0aW9ucxiHCSABKAsyGi5idWYudmFsaWRhdGUuTWVzc2FnZVJ1bGVzUgdtZXNzYWdlOk4KBW9uZW9mEh0uZ29vZ2xlLnByb3RvYnVmLk9uZW9mT3B0aW9ucxiHCSABKAsyGC5idWYudmFsaWRhdGUuT25lb2ZSdWxlc1IFb25lb2Y6TgoFZmllbGQSHS5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zGIcJIAEoCzIYLmJ1Zi52YWxpZGF0ZS5GaWVsZFJ1bGVzUgVmaWVsZDpdCgpwcmVkZWZpbmVkEh0uZ29vZ2xlLnByb3RvYnVmLkZpZWxkT3B0aW9ucxiICSABKAsyHS5idWYudmFsaWRhdGUuUHJlZGVmaW5lZFJ1bGVzUgpwcmVkZWZpbmVkQm4KEmJ1aWxkLmJ1Zi52YWxpZGF0ZUINVmFsaWRhdGVQcm90b1ABWkdidWYuYnVpbGQvZ2VuL2dvL2J1ZmJ1aWxkL3Byb3RvdmFsaWRhdGUvcHJvdG9jb2xidWZmZXJzL2dvL2J1Zi92YWxpZGF0ZQ\", [file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_field_mask, file_google_protobuf_timestamp]);\n\n/**\n * `Rule` represents a validation rule written in the Common Expression\n * Language (CEL) syntax. Each Rule includes a unique identifier, an\n * optional error message, and the CEL expression to evaluate. For more\n * information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n *\n * ```proto\n * message Foo {\n *   option (buf.validate.message).cel = {\n *     id: \"foo.bar\"\n *     message: \"bar must be greater than 0\"\n *     expression: \"this.bar > 0\"\n *   };\n *   int32 bar = 1;\n * }\n * ```\n *\n * @generated from message buf.validate.Rule\n */\nexport type Rule = Message<\"buf.validate.Rule\"> & {\n  /**\n   * `id` is a string that serves as a machine-readable name for this Rule.\n   * It should be unique within its scope, which could be either a message or a field.\n   *\n   * @generated from field: optional string id = 1;\n   */\n  id: string;\n\n  /**\n   * `message` is an optional field that provides a human-readable error message\n   * for this Rule when the CEL expression evaluates to false. If a\n   * non-empty message is provided, any strings resulting from the CEL\n   * expression evaluation are ignored.\n   *\n   * @generated from field: optional string message = 2;\n   */\n  message: string;\n\n  /**\n   * `expression` is the actual CEL expression that will be evaluated for\n   * validation. This string must resolve to either a boolean or a string\n   * value. If the expression evaluates to false or a non-empty string, the\n   * validation is considered failed, and the message is rejected.\n   *\n   * @generated from field: optional string expression = 3;\n   */\n  expression: string;\n};\n\n/**\n * Describes the message buf.validate.Rule.\n * Use `create(RuleSchema)` to create a new message.\n */\nexport const RuleSchema: GenMessage<Rule> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 0);\n\n/**\n * MessageRules represents validation rules that are applied to the entire message.\n * It includes disabling options and a list of Rule messages representing Common Expression Language (CEL) validation rules.\n *\n * @generated from message buf.validate.MessageRules\n */\nexport type MessageRules = Message<\"buf.validate.MessageRules\"> & {\n  /**\n   * `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation\n   * rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax.\n   *\n   * This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for\n   * simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will\n   * be same as the `expression`.\n   *\n   * For more information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `foo` must be greater than 42.\n   *   option (buf.validate.message).cel_expression = \"this.foo > 42\";\n   *   // The field `foo` must be less than 84.\n   *   option (buf.validate.message).cel_expression = \"this.foo < 84\";\n   *   optional int32 foo = 1;\n   * }\n   * ```\n   *\n   * @generated from field: repeated string cel_expression = 5;\n   */\n  celExpression: string[];\n\n  /**\n   * `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message.\n   * These rules are written in Common Expression Language (CEL) syntax. For more information,\n   * [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n   *\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `foo` must be greater than 42.\n   *   option (buf.validate.message).cel = {\n   *     id: \"my_message.value\",\n   *     message: \"value must be greater than 42\",\n   *     expression: \"this.foo > 42\",\n   *   };\n   *   optional int32 foo = 1;\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.Rule cel = 3;\n   */\n  cel: Rule[];\n\n  /**\n   * `oneof` is a repeated field of type MessageOneofRule that specifies a list of fields\n   * of which at most one can be present. If `required` is also specified, then exactly one\n   * of the specified fields _must_ be present.\n   *\n   * This will enforce oneof-like constraints with a few features not provided by\n   * actual Protobuf oneof declarations:\n   *   1. Repeated and map fields are allowed in this validation. In a Protobuf oneof,\n   *      only scalar fields are allowed.\n   *   2. Fields with implicit presence are allowed. In a Protobuf oneof, all member\n   *      fields have explicit presence. This means that, for the purpose of determining\n   *      how many fields are set, explicitly setting such a field to its zero value is\n   *      effectively the same as not setting it at all.\n   *   3. This will always generate validation errors for a message unmarshalled from\n   *      serialized data that sets more than one field. With a Protobuf oneof, when\n   *      multiple fields are present in the serialized form, earlier values are usually\n   *      silently ignored when unmarshalling, with only the last field being set when\n   *      unmarshalling completes.\n   *\n   * Note that adding a field to a `oneof` will also set the IGNORE_IF_ZERO_VALUE on the fields. This means\n   * only the field that is set will be validated and the unset fields are not validated according to the field rules.\n   * This behavior can be overridden by setting `ignore` against a field.\n   *\n   * ```proto\n   * message MyMessage {\n   *   // Only one of `field1` or `field2` _can_ be present in this message.\n   *   option (buf.validate.message).oneof = { fields: [\"field1\", \"field2\"] };\n   *   // Exactly one of `field3` or `field4` _must_ be present in this message.\n   *   option (buf.validate.message).oneof = { fields: [\"field3\", \"field4\"], required: true };\n   *   string field1 = 1;\n   *   bytes field2 = 2;\n   *   bool field3 = 3;\n   *   int32 field4 = 4;\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.MessageOneofRule oneof = 4;\n   */\n  oneof: MessageOneofRule[];\n};\n\n/**\n * Describes the message buf.validate.MessageRules.\n * Use `create(MessageRulesSchema)` to create a new message.\n */\nexport const MessageRulesSchema: GenMessage<MessageRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 1);\n\n/**\n * @generated from message buf.validate.MessageOneofRule\n */\nexport type MessageOneofRule = Message<\"buf.validate.MessageOneofRule\"> & {\n  /**\n   * A list of field names to include in the oneof. All field names must be\n   * defined in the message. At least one field must be specified, and\n   * duplicates are not permitted.\n   *\n   * @generated from field: repeated string fields = 1;\n   */\n  fields: string[];\n\n  /**\n   * If true, one of the fields specified _must_ be set.\n   *\n   * @generated from field: optional bool required = 2;\n   */\n  required: boolean;\n};\n\n/**\n * Describes the message buf.validate.MessageOneofRule.\n * Use `create(MessageOneofRuleSchema)` to create a new message.\n */\nexport const MessageOneofRuleSchema: GenMessage<MessageOneofRule> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 2);\n\n/**\n * The `OneofRules` message type enables you to manage rules for\n * oneof fields in your protobuf messages.\n *\n * @generated from message buf.validate.OneofRules\n */\nexport type OneofRules = Message<\"buf.validate.OneofRules\"> & {\n  /**\n   * If `required` is true, exactly one field of the oneof must be set. A\n   * validation error is returned if no fields in the oneof are set. Further rules\n   * should be placed on the fields themselves to ensure they are valid values,\n   * such as `min_len` or `gt`.\n   *\n   * ```proto\n   * message MyMessage {\n   *   oneof value {\n   *     // Either `a` or `b` must be set. If `a` is set, it must also be\n   *     // non-empty; whereas if `b` is set, it can still be an empty string.\n   *     option (buf.validate.oneof).required = true;\n   *     string a = 1 [(buf.validate.field).string.min_len = 1];\n   *     string b = 2;\n   *   }\n   * }\n   * ```\n   *\n   * @generated from field: optional bool required = 1;\n   */\n  required: boolean;\n};\n\n/**\n * Describes the message buf.validate.OneofRules.\n * Use `create(OneofRulesSchema)` to create a new message.\n */\nexport const OneofRulesSchema: GenMessage<OneofRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 3);\n\n/**\n * FieldRules encapsulates the rules for each type of field. Depending on\n * the field, the correct set should be used to ensure proper validations.\n *\n * @generated from message buf.validate.FieldRules\n */\nexport type FieldRules = Message<\"buf.validate.FieldRules\"> & {\n  /**\n   * `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation\n   * rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax.\n   *\n   * This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for\n   * simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will\n   * be same as the `expression`.\n   *\n   * For more information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `value` must be greater than 42.\n   *   optional int32 value = 1 [(buf.validate.field).cel_expression = \"this > 42\"];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string cel_expression = 29;\n   */\n  celExpression: string[];\n\n  /**\n   * `cel` is a repeated field used to represent a textual expression\n   * in the Common Expression Language (CEL) syntax. For more information,\n   * [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `value` must be greater than 42.\n   *   optional int32 value = 1 [(buf.validate.field).cel = {\n   *     id: \"my_message.value\",\n   *     message: \"value must be greater than 42\",\n   *     expression: \"this > 42\",\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.Rule cel = 23;\n   */\n  cel: Rule[];\n\n  /**\n   * If `required` is true, the field must be set. A validation error is returned\n   * if the field is not set.\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message FieldsWithPresence {\n   *   // Requires any string to be set, including the empty string.\n   *   optional string link = 1 [\n   *     (buf.validate.field).required = true\n   *   ];\n   *   // Requires true or false to be set.\n   *   optional bool disabled = 2 [\n   *     (buf.validate.field).required = true\n   *   ];\n   *   // Requires a message to be set, including the empty message.\n   *   SomeMessage msg = 4 [\n   *     (buf.validate.field).required = true\n   *   ];\n   * }\n   * ```\n   *\n   * All fields in the example above track presence. By default, Protovalidate\n   * ignores rules on those fields if no value is set. `required` ensures that\n   * the fields are set and valid.\n   *\n   * Fields that don't track presence are always validated by Protovalidate,\n   * whether they are set or not. It is not necessary to add `required`. It\n   * can be added to indicate that the field cannot be the zero value.\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message FieldsWithoutPresence {\n   *   // `string.email` always applies, even to an empty string.\n   *   string link = 1 [\n   *     (buf.validate.field).string.email = true\n   *   ];\n   *   // `repeated.min_items` always applies, even to an empty list.\n   *   repeated string labels = 2 [\n   *     (buf.validate.field).repeated.min_items = 1\n   *   ];\n   *   // `required`, for fields that don't track presence, indicates\n   *   // the value of the field can't be the zero value.\n   *   int32 zero_value_not_allowed = 3 [\n   *     (buf.validate.field).required = true\n   *   ];\n   * }\n   * ```\n   *\n   * To learn which fields track presence, see the\n   * [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat).\n   *\n   * Note: While field rules can be applied to repeated items, map keys, and map\n   * values, the elements are always considered to be set. Consequently,\n   * specifying `repeated.items.required` is redundant.\n   *\n   * @generated from field: optional bool required = 25;\n   */\n  required: boolean;\n\n  /**\n   * Ignore validation rules on the field if its value matches the specified\n   * criteria. See the `Ignore` enum for details.\n   *\n   * ```proto\n   * message UpdateRequest {\n   *   // The uri rule only applies if the field is not an empty string.\n   *   string url = 1 [\n   *     (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,\n   *     (buf.validate.field).string.uri = true\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: optional buf.validate.Ignore ignore = 27;\n   */\n  ignore: Ignore;\n\n  /**\n   * @generated from oneof buf.validate.FieldRules.type\n   */\n  type: {\n    /**\n     * Scalar Field Types\n     *\n     * @generated from field: buf.validate.FloatRules float = 1;\n     */\n    value: FloatRules;\n    case: \"float\";\n  } | {\n    /**\n     * @generated from field: buf.validate.DoubleRules double = 2;\n     */\n    value: DoubleRules;\n    case: \"double\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Int32Rules int32 = 3;\n     */\n    value: Int32Rules;\n    case: \"int32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Int64Rules int64 = 4;\n     */\n    value: Int64Rules;\n    case: \"int64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.UInt32Rules uint32 = 5;\n     */\n    value: UInt32Rules;\n    case: \"uint32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.UInt64Rules uint64 = 6;\n     */\n    value: UInt64Rules;\n    case: \"uint64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SInt32Rules sint32 = 7;\n     */\n    value: SInt32Rules;\n    case: \"sint32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SInt64Rules sint64 = 8;\n     */\n    value: SInt64Rules;\n    case: \"sint64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Fixed32Rules fixed32 = 9;\n     */\n    value: Fixed32Rules;\n    case: \"fixed32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Fixed64Rules fixed64 = 10;\n     */\n    value: Fixed64Rules;\n    case: \"fixed64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SFixed32Rules sfixed32 = 11;\n     */\n    value: SFixed32Rules;\n    case: \"sfixed32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SFixed64Rules sfixed64 = 12;\n     */\n    value: SFixed64Rules;\n    case: \"sfixed64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.BoolRules bool = 13;\n     */\n    value: BoolRules;\n    case: \"bool\";\n  } | {\n    /**\n     * @generated from field: buf.validate.StringRules string = 14;\n     */\n    value: StringRules;\n    case: \"string\";\n  } | {\n    /**\n     * @generated from field: buf.validate.BytesRules bytes = 15;\n     */\n    value: BytesRules;\n    case: \"bytes\";\n  } | {\n    /**\n     * Complex Field Types\n     *\n     * @generated from field: buf.validate.EnumRules enum = 16;\n     */\n    value: EnumRules;\n    case: \"enum\";\n  } | {\n    /**\n     * @generated from field: buf.validate.RepeatedRules repeated = 18;\n     */\n    value: RepeatedRules;\n    case: \"repeated\";\n  } | {\n    /**\n     * @generated from field: buf.validate.MapRules map = 19;\n     */\n    value: MapRules;\n    case: \"map\";\n  } | {\n    /**\n     * Well-Known Field Types\n     *\n     * @generated from field: buf.validate.AnyRules any = 20;\n     */\n    value: AnyRules;\n    case: \"any\";\n  } | {\n    /**\n     * @generated from field: buf.validate.DurationRules duration = 21;\n     */\n    value: DurationRules;\n    case: \"duration\";\n  } | {\n    /**\n     * @generated from field: buf.validate.FieldMaskRules field_mask = 28;\n     */\n    value: FieldMaskRules;\n    case: \"fieldMask\";\n  } | {\n    /**\n     * @generated from field: buf.validate.TimestampRules timestamp = 22;\n     */\n    value: TimestampRules;\n    case: \"timestamp\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message buf.validate.FieldRules.\n * Use `create(FieldRulesSchema)` to create a new message.\n */\nexport const FieldRulesSchema: GenMessage<FieldRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 4);\n\n/**\n * PredefinedRules are custom rules that can be re-used with\n * multiple fields.\n *\n * @generated from message buf.validate.PredefinedRules\n */\nexport type PredefinedRules = Message<\"buf.validate.PredefinedRules\"> & {\n  /**\n   * `cel` is a repeated field used to represent a textual expression\n   * in the Common Expression Language (CEL) syntax. For more information,\n   * [see our documentation](https://buf.build/docs/protovalidate/schemas/predefined-rules/).\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `value` must be greater than 42.\n   *   optional int32 value = 1 [(buf.validate.predefined).cel = {\n   *     id: \"my_message.value\",\n   *     message: \"value must be greater than 42\",\n   *     expression: \"this > 42\",\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.Rule cel = 1;\n   */\n  cel: Rule[];\n};\n\n/**\n * Describes the message buf.validate.PredefinedRules.\n * Use `create(PredefinedRulesSchema)` to create a new message.\n */\nexport const PredefinedRulesSchema: GenMessage<PredefinedRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 5);\n\n/**\n * FloatRules describes the rules applied to `float` values. These\n * rules may also be applied to the `google.protobuf.FloatValue` Well-Known-Type.\n *\n * @generated from message buf.validate.FloatRules\n */\nexport type FloatRules = Message<\"buf.validate.FloatRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyFloat {\n   *   // value must equal 42.0\n   *   float value = 1 [(buf.validate.field).float.const = 42.0];\n   * }\n   * ```\n   *\n   * @generated from field: optional float const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.FloatRules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be less than 10.0\n     *   float value = 1 [(buf.validate.field).float.lt = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: float lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be less than or equal to 10.0\n     *   float value = 1 [(buf.validate.field).float.lte = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: float lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.FloatRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be greater than 5.0 [float.gt]\n     *   float value = 1 [(buf.validate.field).float.gt = 5.0];\n     *\n     *   // value must be greater than 5 and less than 10.0 [float.gt_lt]\n     *   float other_value = 2 [(buf.validate.field).float = { gt: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than 10 or less than 5.0 [float.gt_lt_exclusive]\n     *   float another_value = 3 [(buf.validate.field).float = { gt: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: float gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be greater than or equal to 5.0 [float.gte]\n     *   float value = 1 [(buf.validate.field).float.gte = 5.0];\n     *\n     *   // value must be greater than or equal to 5.0 and less than 10.0 [float.gte_lt]\n     *   float other_value = 2 [(buf.validate.field).float = { gte: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than or equal to 10.0 or less than 5.0 [float.gte_lt_exclusive]\n     *   float another_value = 3 [(buf.validate.field).float = { gte: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: float gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MyFloat {\n   *   // value must be in list [1.0, 2.0, 3.0]\n   *   float value = 1 [(buf.validate.field).float = { in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated float in = 6;\n   */\n  in: number[];\n\n  /**\n   * `in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyFloat {\n   *   // value must not be in list [1.0, 2.0, 3.0]\n   *   float value = 1 [(buf.validate.field).float = { not_in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated float not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `finite` requires the field value to be finite. If the field value is\n   * infinite or NaN, an error message is generated.\n   *\n   * @generated from field: optional bool finite = 8;\n   */\n  finite: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFloat {\n   *   float value = 1 [\n   *     (buf.validate.field).float.example = 1.0,\n   *     (buf.validate.field).float.example = inf\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated float example = 9;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.FloatRules.\n * Use `create(FloatRulesSchema)` to create a new message.\n */\nexport const FloatRulesSchema: GenMessage<FloatRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 6);\n\n/**\n * DoubleRules describes the rules applied to `double` values. These\n * rules may also be applied to the `google.protobuf.DoubleValue` Well-Known-Type.\n *\n * @generated from message buf.validate.DoubleRules\n */\nexport type DoubleRules = Message<\"buf.validate.DoubleRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyDouble {\n   *   // value must equal 42.0\n   *   double value = 1 [(buf.validate.field).double.const = 42.0];\n   * }\n   * ```\n   *\n   * @generated from field: optional double const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.DoubleRules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be less than 10.0\n     *   double value = 1 [(buf.validate.field).double.lt = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: double lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified value\n     * (field <= value). If the field value is greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be less than or equal to 10.0\n     *   double value = 1 [(buf.validate.field).double.lte = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: double lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.DoubleRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or `lte`,\n     * the range is reversed, and the field value must be outside the specified\n     * range. If the field value doesn't meet the required conditions, an error\n     * message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be greater than 5.0 [double.gt]\n     *   double value = 1 [(buf.validate.field).double.gt = 5.0];\n     *\n     *   // value must be greater than 5 and less than 10.0 [double.gt_lt]\n     *   double other_value = 2 [(buf.validate.field).double = { gt: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than 10 or less than 5.0 [double.gt_lt_exclusive]\n     *   double another_value = 3 [(buf.validate.field).double = { gt: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: double gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be greater than or equal to 5.0 [double.gte]\n     *   double value = 1 [(buf.validate.field).double.gte = 5.0];\n     *\n     *   // value must be greater than or equal to 5.0 and less than 10.0 [double.gte_lt]\n     *   double other_value = 2 [(buf.validate.field).double = { gte: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than or equal to 10.0 or less than 5.0 [double.gte_lt_exclusive]\n     *   double another_value = 3 [(buf.validate.field).double = { gte: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: double gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyDouble {\n   *   // value must be in list [1.0, 2.0, 3.0]\n   *   double value = 1 [(buf.validate.field).double = { in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated double in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyDouble {\n   *   // value must not be in list [1.0, 2.0, 3.0]\n   *   double value = 1 [(buf.validate.field).double = { not_in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated double not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `finite` requires the field value to be finite. If the field value is\n   * infinite or NaN, an error message is generated.\n   *\n   * @generated from field: optional bool finite = 8;\n   */\n  finite: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyDouble {\n   *   double value = 1 [\n   *     (buf.validate.field).double.example = 1.0,\n   *     (buf.validate.field).double.example = inf\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated double example = 9;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.DoubleRules.\n * Use `create(DoubleRulesSchema)` to create a new message.\n */\nexport const DoubleRulesSchema: GenMessage<DoubleRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 7);\n\n/**\n * Int32Rules describes the rules applied to `int32` values. These\n * rules may also be applied to the `google.protobuf.Int32Value` Well-Known-Type.\n *\n * @generated from message buf.validate.Int32Rules\n */\nexport type Int32Rules = Message<\"buf.validate.Int32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   // value must equal 42\n   *   int32 value = 1 [(buf.validate.field).int32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional int32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.Int32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field\n     * < value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be less than 10\n     *   int32 value = 1 [(buf.validate.field).int32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be less than or equal to 10\n     *   int32 value = 1 [(buf.validate.field).int32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Int32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be greater than 5 [int32.gt]\n     *   int32 value = 1 [(buf.validate.field).int32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [int32.gt_lt]\n     *   int32 other_value = 2 [(buf.validate.field).int32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [int32.gt_lt_exclusive]\n     *   int32 another_value = 3 [(buf.validate.field).int32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified value\n     * (exclusive). If the value of `gte` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be greater than or equal to 5 [int32.gte]\n     *   int32 value = 1 [(buf.validate.field).int32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [int32.gte_lt]\n     *   int32 other_value = 2 [(buf.validate.field).int32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [int32.gte_lt_exclusive]\n     *   int32 another_value = 3 [(buf.validate.field).int32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   // value must be in list [1, 2, 3]\n   *   int32 value = 1 [(buf.validate.field).int32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   // value must not be in list [1, 2, 3]\n   *   int32 value = 1 [(buf.validate.field).int32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   int32 value = 1 [\n   *     (buf.validate.field).int32.example = 1,\n   *     (buf.validate.field).int32.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.Int32Rules.\n * Use `create(Int32RulesSchema)` to create a new message.\n */\nexport const Int32RulesSchema: GenMessage<Int32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 8);\n\n/**\n * Int64Rules describes the rules applied to `int64` values. These\n * rules may also be applied to the `google.protobuf.Int64Value` Well-Known-Type.\n *\n * @generated from message buf.validate.Int64Rules\n */\nexport type Int64Rules = Message<\"buf.validate.Int64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   // value must equal 42\n   *   int64 value = 1 [(buf.validate.field).int64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional int64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.Int64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be less than 10\n     *   int64 value = 1 [(buf.validate.field).int64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be less than or equal to 10\n     *   int64 value = 1 [(buf.validate.field).int64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Int64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be greater than 5 [int64.gt]\n     *   int64 value = 1 [(buf.validate.field).int64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [int64.gt_lt]\n     *   int64 other_value = 2 [(buf.validate.field).int64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [int64.gt_lt_exclusive]\n     *   int64 another_value = 3 [(buf.validate.field).int64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be greater than or equal to 5 [int64.gte]\n     *   int64 value = 1 [(buf.validate.field).int64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [int64.gte_lt]\n     *   int64 other_value = 2 [(buf.validate.field).int64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [int64.gte_lt_exclusive]\n     *   int64 another_value = 3 [(buf.validate.field).int64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   // value must be in list [1, 2, 3]\n   *   int64 value = 1 [(buf.validate.field).int64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   // value must not be in list [1, 2, 3]\n   *   int64 value = 1 [(buf.validate.field).int64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   int64 value = 1 [\n   *     (buf.validate.field).int64.example = 1,\n   *     (buf.validate.field).int64.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int64 example = 9;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.Int64Rules.\n * Use `create(Int64RulesSchema)` to create a new message.\n */\nexport const Int64RulesSchema: GenMessage<Int64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 9);\n\n/**\n * UInt32Rules describes the rules applied to `uint32` values. These\n * rules may also be applied to the `google.protobuf.UInt32Value` Well-Known-Type.\n *\n * @generated from message buf.validate.UInt32Rules\n */\nexport type UInt32Rules = Message<\"buf.validate.UInt32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   // value must equal 42\n   *   uint32 value = 1 [(buf.validate.field).uint32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.UInt32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be less than 10\n     *   uint32 value = 1 [(buf.validate.field).uint32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be less than or equal to 10\n     *   uint32 value = 1 [(buf.validate.field).uint32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.UInt32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be greater than 5 [uint32.gt]\n     *   uint32 value = 1 [(buf.validate.field).uint32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [uint32.gt_lt]\n     *   uint32 other_value = 2 [(buf.validate.field).uint32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [uint32.gt_lt_exclusive]\n     *   uint32 another_value = 3 [(buf.validate.field).uint32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be greater than or equal to 5 [uint32.gte]\n     *   uint32 value = 1 [(buf.validate.field).uint32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [uint32.gte_lt]\n     *   uint32 other_value = 2 [(buf.validate.field).uint32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [uint32.gte_lt_exclusive]\n     *   uint32 another_value = 3 [(buf.validate.field).uint32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   // value must be in list [1, 2, 3]\n   *   uint32 value = 1 [(buf.validate.field).uint32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   // value must not be in list [1, 2, 3]\n   *   uint32 value = 1 [(buf.validate.field).uint32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   uint32 value = 1 [\n   *     (buf.validate.field).uint32.example = 1,\n   *     (buf.validate.field).uint32.example = 10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.UInt32Rules.\n * Use `create(UInt32RulesSchema)` to create a new message.\n */\nexport const UInt32RulesSchema: GenMessage<UInt32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 10);\n\n/**\n * UInt64Rules describes the rules applied to `uint64` values. These\n * rules may also be applied to the `google.protobuf.UInt64Value` Well-Known-Type.\n *\n * @generated from message buf.validate.UInt64Rules\n */\nexport type UInt64Rules = Message<\"buf.validate.UInt64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   // value must equal 42\n   *   uint64 value = 1 [(buf.validate.field).uint64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.UInt64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be less than 10\n     *   uint64 value = 1 [(buf.validate.field).uint64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be less than or equal to 10\n     *   uint64 value = 1 [(buf.validate.field).uint64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.UInt64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be greater than 5 [uint64.gt]\n     *   uint64 value = 1 [(buf.validate.field).uint64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [uint64.gt_lt]\n     *   uint64 other_value = 2 [(buf.validate.field).uint64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [uint64.gt_lt_exclusive]\n     *   uint64 another_value = 3 [(buf.validate.field).uint64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be greater than or equal to 5 [uint64.gte]\n     *   uint64 value = 1 [(buf.validate.field).uint64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [uint64.gte_lt]\n     *   uint64 other_value = 2 [(buf.validate.field).uint64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [uint64.gte_lt_exclusive]\n     *   uint64 another_value = 3 [(buf.validate.field).uint64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   // value must be in list [1, 2, 3]\n   *   uint64 value = 1 [(buf.validate.field).uint64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   // value must not be in list [1, 2, 3]\n   *   uint64 value = 1 [(buf.validate.field).uint64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   uint64 value = 1 [\n   *     (buf.validate.field).uint64.example = 1,\n   *     (buf.validate.field).uint64.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.UInt64Rules.\n * Use `create(UInt64RulesSchema)` to create a new message.\n */\nexport const UInt64RulesSchema: GenMessage<UInt64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 11);\n\n/**\n * SInt32Rules describes the rules applied to `sint32` values.\n *\n * @generated from message buf.validate.SInt32Rules\n */\nexport type SInt32Rules = Message<\"buf.validate.SInt32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   // value must equal 42\n   *   sint32 value = 1 [(buf.validate.field).sint32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sint32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.SInt32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field\n     * < value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *   // value must be less than 10\n     *   sint32 value = 1 [(buf.validate.field).sint32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *   // value must be less than or equal to 10\n     *   sint32 value = 1 [(buf.validate.field).sint32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SInt32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *   // value must be greater than 5 [sint32.gt]\n     *   sint32 value = 1 [(buf.validate.field).sint32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sint32.gt_lt]\n     *   sint32 other_value = 2 [(buf.validate.field).sint32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sint32.gt_lt_exclusive]\n     *   sint32 another_value = 3 [(buf.validate.field).sint32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *  // value must be greater than or equal to 5 [sint32.gte]\n     *  sint32 value = 1 [(buf.validate.field).sint32.gte = 5];\n     *\n     *  // value must be greater than or equal to 5 and less than 10 [sint32.gte_lt]\n     *  sint32 other_value = 2 [(buf.validate.field).sint32 = { gte: 5, lt: 10 }];\n     *\n     *  // value must be greater than or equal to 10 or less than 5 [sint32.gte_lt_exclusive]\n     *  sint32 another_value = 3 [(buf.validate.field).sint32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   // value must be in list [1, 2, 3]\n   *   sint32 value = 1 [(buf.validate.field).sint32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   // value must not be in list [1, 2, 3]\n   *   sint32 value = 1 [(buf.validate.field).sint32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   sint32 value = 1 [\n   *     (buf.validate.field).sint32.example = 1,\n   *     (buf.validate.field).sint32.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.SInt32Rules.\n * Use `create(SInt32RulesSchema)` to create a new message.\n */\nexport const SInt32RulesSchema: GenMessage<SInt32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 12);\n\n/**\n * SInt64Rules describes the rules applied to `sint64` values.\n *\n * @generated from message buf.validate.SInt64Rules\n */\nexport type SInt64Rules = Message<\"buf.validate.SInt64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   // value must equal 42\n   *   sint64 value = 1 [(buf.validate.field).sint64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sint64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.SInt64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field\n     * < value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be less than 10\n     *   sint64 value = 1 [(buf.validate.field).sint64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be less than or equal to 10\n     *   sint64 value = 1 [(buf.validate.field).sint64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SInt64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be greater than 5 [sint64.gt]\n     *   sint64 value = 1 [(buf.validate.field).sint64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sint64.gt_lt]\n     *   sint64 other_value = 2 [(buf.validate.field).sint64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sint64.gt_lt_exclusive]\n     *   sint64 another_value = 3 [(buf.validate.field).sint64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be greater than or equal to 5 [sint64.gte]\n     *   sint64 value = 1 [(buf.validate.field).sint64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [sint64.gte_lt]\n     *   sint64 other_value = 2 [(buf.validate.field).sint64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [sint64.gte_lt_exclusive]\n     *   sint64 another_value = 3 [(buf.validate.field).sint64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   // value must be in list [1, 2, 3]\n   *   sint64 value = 1 [(buf.validate.field).sint64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   // value must not be in list [1, 2, 3]\n   *   sint64 value = 1 [(buf.validate.field).sint64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   sint64 value = 1 [\n   *     (buf.validate.field).sint64.example = 1,\n   *     (buf.validate.field).sint64.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.SInt64Rules.\n * Use `create(SInt64RulesSchema)` to create a new message.\n */\nexport const SInt64RulesSchema: GenMessage<SInt64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 13);\n\n/**\n * Fixed32Rules describes the rules applied to `fixed32` values.\n *\n * @generated from message buf.validate.Fixed32Rules\n */\nexport type Fixed32Rules = Message<\"buf.validate.Fixed32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   // value must equal 42\n   *   fixed32 value = 1 [(buf.validate.field).fixed32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional fixed32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.Fixed32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be less than 10\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be less than or equal to 10\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Fixed32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be greater than 5 [fixed32.gt]\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [fixed32.gt_lt]\n     *   fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [fixed32.gt_lt_exclusive]\n     *   fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be greater than or equal to 5 [fixed32.gte]\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [fixed32.gte_lt]\n     *   fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [fixed32.gte_lt_exclusive]\n     *   fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   // value must be in list [1, 2, 3]\n   *   fixed32 value = 1 [(buf.validate.field).fixed32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   // value must not be in list [1, 2, 3]\n   *   fixed32 value = 1 [(buf.validate.field).fixed32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   fixed32 value = 1 [\n   *     (buf.validate.field).fixed32.example = 1,\n   *     (buf.validate.field).fixed32.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.Fixed32Rules.\n * Use `create(Fixed32RulesSchema)` to create a new message.\n */\nexport const Fixed32RulesSchema: GenMessage<Fixed32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 14);\n\n/**\n * Fixed64Rules describes the rules applied to `fixed64` values.\n *\n * @generated from message buf.validate.Fixed64Rules\n */\nexport type Fixed64Rules = Message<\"buf.validate.Fixed64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   // value must equal 42\n   *   fixed64 value = 1 [(buf.validate.field).fixed64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional fixed64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.Fixed64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be less than 10\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be less than or equal to 10\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Fixed64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be greater than 5 [fixed64.gt]\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [fixed64.gt_lt]\n     *   fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [fixed64.gt_lt_exclusive]\n     *   fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be greater than or equal to 5 [fixed64.gte]\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [fixed64.gte_lt]\n     *   fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [fixed64.gte_lt_exclusive]\n     *   fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   // value must be in list [1, 2, 3]\n   *   fixed64 value = 1 [(buf.validate.field).fixed64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   // value must not be in list [1, 2, 3]\n   *   fixed64 value = 1 [(buf.validate.field).fixed64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   fixed64 value = 1 [\n   *     (buf.validate.field).fixed64.example = 1,\n   *     (buf.validate.field).fixed64.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.Fixed64Rules.\n * Use `create(Fixed64RulesSchema)` to create a new message.\n */\nexport const Fixed64RulesSchema: GenMessage<Fixed64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 15);\n\n/**\n * SFixed32Rules describes the rules applied to `fixed32` values.\n *\n * @generated from message buf.validate.SFixed32Rules\n */\nexport type SFixed32Rules = Message<\"buf.validate.SFixed32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   // value must equal 42\n   *   sfixed32 value = 1 [(buf.validate.field).sfixed32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sfixed32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.SFixed32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be less than 10\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be less than or equal to 10\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SFixed32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be greater than 5 [sfixed32.gt]\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sfixed32.gt_lt]\n     *   sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sfixed32.gt_lt_exclusive]\n     *   sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be greater than or equal to 5 [sfixed32.gte]\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [sfixed32.gte_lt]\n     *   sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [sfixed32.gte_lt_exclusive]\n     *   sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   // value must be in list [1, 2, 3]\n   *   sfixed32 value = 1 [(buf.validate.field).sfixed32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   // value must not be in list [1, 2, 3]\n   *   sfixed32 value = 1 [(buf.validate.field).sfixed32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   sfixed32 value = 1 [\n   *     (buf.validate.field).sfixed32.example = 1,\n   *     (buf.validate.field).sfixed32.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.SFixed32Rules.\n * Use `create(SFixed32RulesSchema)` to create a new message.\n */\nexport const SFixed32RulesSchema: GenMessage<SFixed32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 16);\n\n/**\n * SFixed64Rules describes the rules applied to `fixed64` values.\n *\n * @generated from message buf.validate.SFixed64Rules\n */\nexport type SFixed64Rules = Message<\"buf.validate.SFixed64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   // value must equal 42\n   *   sfixed64 value = 1 [(buf.validate.field).sfixed64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sfixed64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.SFixed64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be less than 10\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be less than or equal to 10\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SFixed64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be greater than 5 [sfixed64.gt]\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sfixed64.gt_lt]\n     *   sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sfixed64.gt_lt_exclusive]\n     *   sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be greater than or equal to 5 [sfixed64.gte]\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [sfixed64.gte_lt]\n     *   sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [sfixed64.gte_lt_exclusive]\n     *   sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   // value must be in list [1, 2, 3]\n   *   sfixed64 value = 1 [(buf.validate.field).sfixed64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   // value must not be in list [1, 2, 3]\n   *   sfixed64 value = 1 [(buf.validate.field).sfixed64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   sfixed64 value = 1 [\n   *     (buf.validate.field).sfixed64.example = 1,\n   *     (buf.validate.field).sfixed64.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.SFixed64Rules.\n * Use `create(SFixed64RulesSchema)` to create a new message.\n */\nexport const SFixed64RulesSchema: GenMessage<SFixed64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 17);\n\n/**\n * BoolRules describes the rules applied to `bool` values. These rules\n * may also be applied to the `google.protobuf.BoolValue` Well-Known-Type.\n *\n * @generated from message buf.validate.BoolRules\n */\nexport type BoolRules = Message<\"buf.validate.BoolRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified boolean value.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyBool {\n   *   // value must equal true\n   *   bool value = 1 [(buf.validate.field).bool.const = true];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool const = 1;\n   */\n  const: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyBool {\n   *   bool value = 1 [\n   *     (buf.validate.field).bool.example = 1,\n   *     (buf.validate.field).bool.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bool example = 2;\n   */\n  example: boolean[];\n};\n\n/**\n * Describes the message buf.validate.BoolRules.\n * Use `create(BoolRulesSchema)` to create a new message.\n */\nexport const BoolRulesSchema: GenMessage<BoolRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 18);\n\n/**\n * StringRules describes the rules applied to `string` values These\n * rules may also be applied to the `google.protobuf.StringValue` Well-Known-Type.\n *\n * @generated from message buf.validate.StringRules\n */\nexport type StringRules = Message<\"buf.validate.StringRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value must equal `hello`\n   *   string value = 1 [(buf.validate.field).string.const = \"hello\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string const = 1;\n   */\n  const: string;\n\n  /**\n   * `len` dictates that the field value must have the specified\n   * number of characters (Unicode code points), which may differ from the number\n   * of bytes in the string. If the field value does not meet the specified\n   * length, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be 5 characters\n   *   string value = 1 [(buf.validate.field).string.len = 5];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 len = 19;\n   */\n  len: bigint;\n\n  /**\n   * `min_len` specifies that the field value must have at least the specified\n   * number of characters (Unicode code points), which may differ from the number\n   * of bytes in the string. If the field value contains fewer characters, an error\n   * message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at least 3 characters\n   *   string value = 1 [(buf.validate.field).string.min_len = 3];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_len = 2;\n   */\n  minLen: bigint;\n\n  /**\n   * `max_len` specifies that the field value must have no more than the specified\n   * number of characters (Unicode code points), which may differ from the\n   * number of bytes in the string. If the field value contains more characters,\n   * an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at most 10 characters\n   *   string value = 1 [(buf.validate.field).string.max_len = 10];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_len = 3;\n   */\n  maxLen: bigint;\n\n  /**\n   * `len_bytes` dictates that the field value must have the specified number of\n   * bytes. If the field value does not match the specified length in bytes,\n   * an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be 6 bytes\n   *   string value = 1 [(buf.validate.field).string.len_bytes = 6];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 len_bytes = 20;\n   */\n  lenBytes: bigint;\n\n  /**\n   * `min_bytes` specifies that the field value must have at least the specified\n   * number of bytes. If the field value contains fewer bytes, an error message\n   * will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at least 4 bytes\n   *   string value = 1 [(buf.validate.field).string.min_bytes = 4];\n   * }\n   *\n   * ```\n   *\n   * @generated from field: optional uint64 min_bytes = 4;\n   */\n  minBytes: bigint;\n\n  /**\n   * `max_bytes` specifies that the field value must have no more than the\n   * specified number of bytes. If the field value contains more bytes, an\n   * error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at most 8 bytes\n   *   string value = 1 [(buf.validate.field).string.max_bytes = 8];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_bytes = 5;\n   */\n  maxBytes: bigint;\n\n  /**\n   * `pattern` specifies that the field value must match the specified\n   * regular expression (RE2 syntax), with the expression provided without any\n   * delimiters. If the field value doesn't match the regular expression, an\n   * error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not match regex pattern `^[a-zA-Z]//$`\n   *   string value = 1 [(buf.validate.field).string.pattern = \"^[a-zA-Z]//$\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string pattern = 6;\n   */\n  pattern: string;\n\n  /**\n   * `prefix` specifies that the field value must have the\n   * specified substring at the beginning of the string. If the field value\n   * doesn't start with the specified prefix, an error message will be\n   * generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not have prefix `pre`\n   *   string value = 1 [(buf.validate.field).string.prefix = \"pre\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string prefix = 7;\n   */\n  prefix: string;\n\n  /**\n   * `suffix` specifies that the field value must have the\n   * specified substring at the end of the string. If the field value doesn't\n   * end with the specified suffix, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not have suffix `post`\n   *   string value = 1 [(buf.validate.field).string.suffix = \"post\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string suffix = 8;\n   */\n  suffix: string;\n\n  /**\n   * `contains` specifies that the field value must have the\n   * specified substring anywhere in the string. If the field value doesn't\n   * contain the specified substring, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not contain substring `inside`.\n   *   string value = 1 [(buf.validate.field).string.contains = \"inside\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string contains = 9;\n   */\n  contains: string;\n\n  /**\n   * `not_contains` specifies that the field value must not have the\n   * specified substring anywhere in the string. If the field value contains\n   * the specified substring, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value contains substring `inside`.\n   *   string value = 1 [(buf.validate.field).string.not_contains = \"inside\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string not_contains = 23;\n   */\n  notContains: string;\n\n  /**\n   * `in` specifies that the field value must be equal to one of the specified\n   * values. If the field value isn't one of the specified values, an error\n   * message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value must be in list [\"apple\", \"banana\"]\n   *   string value = 1 [(buf.validate.field).string.in = \"apple\", (buf.validate.field).string.in = \"banana\"];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string in = 10;\n   */\n  in: string[];\n\n  /**\n   * `not_in` specifies that the field value cannot be equal to any\n   * of the specified values. If the field value is one of the specified values,\n   * an error message will be generated.\n   * ```proto\n   * message MyString {\n   *   // value must not be in list [\"orange\", \"grape\"]\n   *   string value = 1 [(buf.validate.field).string.not_in = \"orange\", (buf.validate.field).string.not_in = \"grape\"];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string not_in = 11;\n   */\n  notIn: string[];\n\n  /**\n   * `WellKnown` rules provide advanced rules against common string\n   * patterns.\n   *\n   * @generated from oneof buf.validate.StringRules.well_known\n   */\n  wellKnown: {\n    /**\n     * `email` specifies that the field value must be a valid email address, for\n     * example \"foo@example.com\".\n     *\n     * Conforms to the definition for a valid email address from the [HTML standard](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address).\n     * Note that this standard willfully deviates from [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322),\n     * which allows many unexpected forms of email addresses and will easily match\n     * a typographical error.\n     *\n     * If the field value isn't a valid email address, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid email address\n     *   string value = 1 [(buf.validate.field).string.email = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool email = 12;\n     */\n    value: boolean;\n    case: \"email\";\n  } | {\n    /**\n     * `hostname` specifies that the field value must be a valid hostname, for\n     * example \"foo.example.com\".\n     *\n     * A valid hostname follows the rules below:\n     * - The name consists of one or more labels, separated by a dot (\".\").\n     * - Each label can be 1 to 63 alphanumeric characters.\n     * - A label can contain hyphens (\"-\"), but must not start or end with a hyphen.\n     * - The right-most label must not be digits only.\n     * - The name can have a trailing dot—for example, \"foo.example.com.\".\n     * - The name can be 253 characters at most, excluding the optional trailing dot.\n     *\n     * If the field value isn't a valid hostname, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid hostname\n     *   string value = 1 [(buf.validate.field).string.hostname = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool hostname = 13;\n     */\n    value: boolean;\n    case: \"hostname\";\n  } | {\n    /**\n     * `ip` specifies that the field value must be a valid IP (v4 or v6) address.\n     *\n     * IPv4 addresses are expected in the dotted decimal format—for example, \"192.168.5.21\".\n     * IPv6 addresses are expected in their text representation—for example, \"::1\",\n     * or \"2001:0DB8:ABCD:0012::0\".\n     *\n     * Both formats are well-defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).\n     * Zone identifiers for IPv6 addresses (for example, \"fe80::a%en1\") are supported.\n     *\n     * If the field value isn't a valid IP address, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IP address\n     *   string value = 1 [(buf.validate.field).string.ip = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip = 14;\n     */\n    value: boolean;\n    case: \"ip\";\n  } | {\n    /**\n     * `ipv4` specifies that the field value must be a valid IPv4 address—for\n     * example \"192.168.5.21\". If the field value isn't a valid IPv4 address, an\n     * error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv4 address\n     *   string value = 1 [(buf.validate.field).string.ipv4 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4 = 15;\n     */\n    value: boolean;\n    case: \"ipv4\";\n  } | {\n    /**\n     * `ipv6` specifies that the field value must be a valid IPv6 address—for\n     * example \"::1\", or \"d7a:115c:a1e0:ab12:4843:cd96:626b:430b\". If the field\n     * value is not a valid IPv6 address, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv6 address\n     *   string value = 1 [(buf.validate.field).string.ipv6 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6 = 16;\n     */\n    value: boolean;\n    case: \"ipv6\";\n  } | {\n    /**\n     * `uri` specifies that the field value must be a valid URI, for example\n     * \"https://example.com/foo/bar?baz=quux#frag\".\n     *\n     * URI is defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).\n     * Zone Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)).\n     *\n     * If the field value isn't a valid URI, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid URI\n     *   string value = 1 [(buf.validate.field).string.uri = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uri = 17;\n     */\n    value: boolean;\n    case: \"uri\";\n  } | {\n    /**\n     * `uri_ref` specifies that the field value must be a valid URI Reference—either\n     * a URI such as \"https://example.com/foo/bar?baz=quux#frag\", or a Relative\n     * Reference such as \"./foo/bar?query\".\n     *\n     * URI, URI Reference, and Relative Reference are defined in the internet\n     * standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). Zone\n     * Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)).\n     *\n     * If the field value isn't a valid URI Reference, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid URI Reference\n     *   string value = 1 [(buf.validate.field).string.uri_ref = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uri_ref = 18;\n     */\n    value: boolean;\n    case: \"uriRef\";\n  } | {\n    /**\n     * `address` specifies that the field value must be either a valid hostname\n     * (for example, \"example.com\"), or a valid IP (v4 or v6) address (for example,\n     * \"192.168.0.1\", or \"::1\"). If the field value isn't a valid hostname or IP,\n     * an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid hostname, or ip address\n     *   string value = 1 [(buf.validate.field).string.address = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool address = 21;\n     */\n    value: boolean;\n    case: \"address\";\n  } | {\n    /**\n     * `uuid` specifies that the field value must be a valid UUID as defined by\n     * [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2). If the\n     * field value isn't a valid UUID, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid UUID\n     *   string value = 1 [(buf.validate.field).string.uuid = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uuid = 22;\n     */\n    value: boolean;\n    case: \"uuid\";\n  } | {\n    /**\n     * `tuuid` (trimmed UUID) specifies that the field value must be a valid UUID as\n     * defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2) with all dashes\n     * omitted. If the field value isn't a valid UUID without dashes, an error message\n     * will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid trimmed UUID\n     *   string value = 1 [(buf.validate.field).string.tuuid = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool tuuid = 33;\n     */\n    value: boolean;\n    case: \"tuuid\";\n  } | {\n    /**\n     * `ip_with_prefixlen` specifies that the field value must be a valid IP\n     * (v4 or v6) address with prefix length—for example, \"192.168.5.21/16\" or\n     * \"2001:0DB8:ABCD:0012::F1/64\". If the field value isn't a valid IP with\n     * prefix length, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IP with prefix length\n     *    string value = 1 [(buf.validate.field).string.ip_with_prefixlen = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip_with_prefixlen = 26;\n     */\n    value: boolean;\n    case: \"ipWithPrefixlen\";\n  } | {\n    /**\n     * `ipv4_with_prefixlen` specifies that the field value must be a valid\n     * IPv4 address with prefix length—for example, \"192.168.5.21/16\". If the\n     * field value isn't a valid IPv4 address with prefix length, an error\n     * message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv4 address with prefix length\n     *    string value = 1 [(buf.validate.field).string.ipv4_with_prefixlen = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4_with_prefixlen = 27;\n     */\n    value: boolean;\n    case: \"ipv4WithPrefixlen\";\n  } | {\n    /**\n     * `ipv6_with_prefixlen` specifies that the field value must be a valid\n     * IPv6 address with prefix length—for example, \"2001:0DB8:ABCD:0012::F1/64\".\n     * If the field value is not a valid IPv6 address with prefix length,\n     * an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv6 address prefix length\n     *    string value = 1 [(buf.validate.field).string.ipv6_with_prefixlen = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6_with_prefixlen = 28;\n     */\n    value: boolean;\n    case: \"ipv6WithPrefixlen\";\n  } | {\n    /**\n     * `ip_prefix` specifies that the field value must be a valid IP (v4 or v6)\n     * prefix—for example, \"192.168.0.0/16\" or \"2001:0DB8:ABCD:0012::0/64\".\n     *\n     * The prefix must have all zeros for the unmasked bits. For example,\n     * \"2001:0DB8:ABCD:0012::0/64\" designates the left-most 64 bits for the\n     * prefix, and the remaining 64 bits must be zero.\n     *\n     * If the field value isn't a valid IP prefix, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IP prefix\n     *    string value = 1 [(buf.validate.field).string.ip_prefix = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip_prefix = 29;\n     */\n    value: boolean;\n    case: \"ipPrefix\";\n  } | {\n    /**\n     * `ipv4_prefix` specifies that the field value must be a valid IPv4\n     * prefix, for example \"192.168.0.0/16\".\n     *\n     * The prefix must have all zeros for the unmasked bits. For example,\n     * \"192.168.0.0/16\" designates the left-most 16 bits for the prefix,\n     * and the remaining 16 bits must be zero.\n     *\n     * If the field value isn't a valid IPv4 prefix, an error message\n     * will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv4 prefix\n     *    string value = 1 [(buf.validate.field).string.ipv4_prefix = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4_prefix = 30;\n     */\n    value: boolean;\n    case: \"ipv4Prefix\";\n  } | {\n    /**\n     * `ipv6_prefix` specifies that the field value must be a valid IPv6 prefix—for\n     * example, \"2001:0DB8:ABCD:0012::0/64\".\n     *\n     * The prefix must have all zeros for the unmasked bits. For example,\n     * \"2001:0DB8:ABCD:0012::0/64\" designates the left-most 64 bits for the\n     * prefix, and the remaining 64 bits must be zero.\n     *\n     * If the field value is not a valid IPv6 prefix, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv6 prefix\n     *    string value = 1 [(buf.validate.field).string.ipv6_prefix = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6_prefix = 31;\n     */\n    value: boolean;\n    case: \"ipv6Prefix\";\n  } | {\n    /**\n     * `host_and_port` specifies that the field value must be valid host/port\n     * pair—for example, \"example.com:8080\".\n     *\n     * The host can be one of:\n     * - An IPv4 address in dotted decimal format—for example, \"192.168.5.21\".\n     * - An IPv6 address enclosed in square brackets—for example, \"[2001:0DB8:ABCD:0012::F1]\".\n     * - A hostname—for example, \"example.com\".\n     *\n     * The port is separated by a colon. It must be non-empty, with a decimal number\n     * in the range of 0-65535, inclusive.\n     *\n     * @generated from field: bool host_and_port = 32;\n     */\n    value: boolean;\n    case: \"hostAndPort\";\n  } | {\n    /**\n     * `ulid` specifies that the field value must be a valid ULID (Universally Unique\n     * Lexicographically Sortable Identifier) as defined by the [ULID specification](https://github.com/ulid/spec).\n     * If the field value isn't a valid ULID, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid ULID\n     *   string value = 1 [(buf.validate.field).string.ulid = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ulid = 35;\n     */\n    value: boolean;\n    case: \"ulid\";\n  } | {\n    /**\n     * `well_known_regex` specifies a common well-known pattern\n     * defined as a regex. If the field value doesn't match the well-known\n     * regex, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid HTTP header value\n     *   string value = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_VALUE];\n     * }\n     * ```\n     *\n     * #### KnownRegex\n     *\n     * `well_known_regex` contains some well-known patterns.\n     *\n     * | Name                          | Number | Description                               |\n     * |-------------------------------|--------|-------------------------------------------|\n     * | KNOWN_REGEX_UNSPECIFIED       | 0      |                                           |\n     * | KNOWN_REGEX_HTTP_HEADER_NAME  | 1      | HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2)  |\n     * | KNOWN_REGEX_HTTP_HEADER_VALUE | 2      | HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4) |\n     *\n     * @generated from field: buf.validate.KnownRegex well_known_regex = 24;\n     */\n    value: KnownRegex;\n    case: \"wellKnownRegex\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * This applies to regexes `HTTP_HEADER_NAME` and `HTTP_HEADER_VALUE` to\n   * enable strict header validation. By default, this is true, and HTTP header\n   * validations are [RFC-compliant](https://datatracker.ietf.org/doc/html/rfc7230#section-3). Setting to false will enable looser\n   * validations that only disallow `\\r\\n\\0` characters, which can be used to\n   * bypass header matching rules.\n   *\n   * ```proto\n   * message MyString {\n   *   // The field `value` must have be a valid HTTP headers, but not enforced with strict rules.\n   *   string value = 1 [(buf.validate.field).string.strict = false];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool strict = 25;\n   */\n  strict: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyString {\n   *   string value = 1 [\n   *     (buf.validate.field).string.example = \"hello\",\n   *     (buf.validate.field).string.example = \"world\"\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string example = 34;\n   */\n  example: string[];\n};\n\n/**\n * Describes the message buf.validate.StringRules.\n * Use `create(StringRulesSchema)` to create a new message.\n */\nexport const StringRulesSchema: GenMessage<StringRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 19);\n\n/**\n * BytesRules describe the rules applied to `bytes` values. These rules\n * may also be applied to the `google.protobuf.BytesValue` Well-Known-Type.\n *\n * @generated from message buf.validate.BytesRules\n */\nexport type BytesRules = Message<\"buf.validate.BytesRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified bytes\n   * value. If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must be \"\\x01\\x02\\x03\\x04\"\n   *   bytes value = 1 [(buf.validate.field).bytes.const = \"\\x01\\x02\\x03\\x04\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes const = 1;\n   */\n  const: Uint8Array;\n\n  /**\n   * `len` requires the field value to have the specified length in bytes.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value length must be 4 bytes.\n   *   optional bytes value = 1 [(buf.validate.field).bytes.len = 4];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 len = 13;\n   */\n  len: bigint;\n\n  /**\n   * `min_len` requires the field value to have at least the specified minimum\n   * length in bytes.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value length must be at least 2 bytes.\n   *   optional bytes value = 1 [(buf.validate.field).bytes.min_len = 2];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_len = 2;\n   */\n  minLen: bigint;\n\n  /**\n   * `max_len` requires the field value to have at most the specified maximum\n   * length in bytes.\n   * If the field value exceeds the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must be at most 6 bytes.\n   *   optional bytes value = 1 [(buf.validate.field).bytes.max_len = 6];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_len = 3;\n   */\n  maxLen: bigint;\n\n  /**\n   * `pattern` requires the field value to match the specified regular\n   * expression ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)).\n   * The value of the field must be valid UTF-8 or validation will fail with a\n   * runtime error.\n   * If the field value doesn't match the pattern, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must match regex pattern \"^[a-zA-Z0-9]+$\".\n   *   optional bytes value = 1 [(buf.validate.field).bytes.pattern = \"^[a-zA-Z0-9]+$\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string pattern = 4;\n   */\n  pattern: string;\n\n  /**\n   * `prefix` requires the field value to have the specified bytes at the\n   * beginning of the string.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value does not have prefix \\x01\\x02\n   *   optional bytes value = 1 [(buf.validate.field).bytes.prefix = \"\\x01\\x02\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes prefix = 5;\n   */\n  prefix: Uint8Array;\n\n  /**\n   * `suffix` requires the field value to have the specified bytes at the end\n   * of the string.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value does not have suffix \\x03\\x04\n   *   optional bytes value = 1 [(buf.validate.field).bytes.suffix = \"\\x03\\x04\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes suffix = 6;\n   */\n  suffix: Uint8Array;\n\n  /**\n   * `contains` requires the field value to have the specified bytes anywhere in\n   * the string.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value does not contain \\x02\\x03\n   *   optional bytes value = 1 [(buf.validate.field).bytes.contains = \"\\x02\\x03\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes contains = 7;\n   */\n  contains: Uint8Array;\n\n  /**\n   * `in` requires the field value to be equal to one of the specified\n   * values. If the field value doesn't match any of the specified values, an\n   * error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must in [\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"]\n   *   optional bytes value = 1 [(buf.validate.field).bytes.in = {\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bytes in = 8;\n   */\n  in: Uint8Array[];\n\n  /**\n   * `not_in` requires the field value to be not equal to any of the specified\n   * values.\n   * If the field value matches any of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must not in [\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"]\n   *   optional bytes value = 1 [(buf.validate.field).bytes.not_in = {\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bytes not_in = 9;\n   */\n  notIn: Uint8Array[];\n\n  /**\n   * WellKnown rules provide advanced rules against common byte\n   * patterns\n   *\n   * @generated from oneof buf.validate.BytesRules.well_known\n   */\n  wellKnown: {\n    /**\n     * `ip` ensures that the field `value` is a valid IP address (v4 or v6) in byte format.\n     * If the field value doesn't meet this rule, an error message is generated.\n     *\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid IP address\n     *   optional bytes value = 1 [(buf.validate.field).bytes.ip = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip = 10;\n     */\n    value: boolean;\n    case: \"ip\";\n  } | {\n    /**\n     * `ipv4` ensures that the field `value` is a valid IPv4 address in byte format.\n     * If the field value doesn't meet this rule, an error message is generated.\n     *\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid IPv4 address\n     *   optional bytes value = 1 [(buf.validate.field).bytes.ipv4 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4 = 11;\n     */\n    value: boolean;\n    case: \"ipv4\";\n  } | {\n    /**\n     * `ipv6` ensures that the field `value` is a valid IPv6 address in byte format.\n     * If the field value doesn't meet this rule, an error message is generated.\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid IPv6 address\n     *   optional bytes value = 1 [(buf.validate.field).bytes.ipv6 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6 = 12;\n     */\n    value: boolean;\n    case: \"ipv6\";\n  } | {\n    /**\n     * `uuid` ensures that the field `value` encodes the 128-bit UUID data as\n     * defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2).\n     * The field must contain exactly 16 bytes\n     * representing the UUID. If the field value isn't a valid UUID, an error\n     * message will be generated.\n     *\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid UUID\n     *   optional bytes value = 1 [(buf.validate.field).bytes.uuid = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uuid = 15;\n     */\n    value: boolean;\n    case: \"uuid\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyBytes {\n   *   bytes value = 1 [\n   *     (buf.validate.field).bytes.example = \"\\x01\\x02\",\n   *     (buf.validate.field).bytes.example = \"\\x02\\x03\"\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bytes example = 14;\n   */\n  example: Uint8Array[];\n};\n\n/**\n * Describes the message buf.validate.BytesRules.\n * Use `create(BytesRulesSchema)` to create a new message.\n */\nexport const BytesRulesSchema: GenMessage<BytesRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 20);\n\n/**\n * EnumRules describe the rules applied to `enum` values.\n *\n * @generated from message buf.validate.EnumRules\n */\nexport type EnumRules = Message<\"buf.validate.EnumRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified enum value.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must be exactly MY_ENUM_VALUE1.\n   *   MyEnum value = 1 [(buf.validate.field).enum.const = 1];\n   * }\n   * ```\n   *\n   * @generated from field: optional int32 const = 1;\n   */\n  const: number;\n\n  /**\n   * `defined_only` requires the field value to be one of the defined values for\n   * this enum, failing on any undefined value.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must be a defined value of MyEnum.\n   *   MyEnum value = 1 [(buf.validate.field).enum.defined_only = true];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool defined_only = 2;\n   */\n  definedOnly: boolean;\n\n  /**\n   * `in` requires the field value to be equal to one of the\n   * specified enum values. If the field value doesn't match any of the\n   * specified values, an error message is generated.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must be equal to one of the specified values.\n   *   MyEnum value = 1 [(buf.validate.field).enum = { in: [1, 2]}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 in = 3;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to be not equal to any of the\n   * specified enum values. If the field value matches one of the specified\n   * values, an error message is generated.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must not be equal to any of the specified values.\n   *   MyEnum value = 1 [(buf.validate.field).enum = { not_in: [1, 2]}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 not_in = 4;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *     (buf.validate.field).enum.example = 1,\n   *     (buf.validate.field).enum.example = 2\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 example = 5;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.EnumRules.\n * Use `create(EnumRulesSchema)` to create a new message.\n */\nexport const EnumRulesSchema: GenMessage<EnumRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 21);\n\n/**\n * RepeatedRules describe the rules applied to `repeated` values.\n *\n * @generated from message buf.validate.RepeatedRules\n */\nexport type RepeatedRules = Message<\"buf.validate.RepeatedRules\"> & {\n  /**\n   * `min_items` requires that this field must contain at least the specified\n   * minimum number of items.\n   *\n   * Note that `min_items = 1` is equivalent to setting a field as `required`.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // value must contain at least  2 items\n   *   repeated string value = 1 [(buf.validate.field).repeated.min_items = 2];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_items = 1;\n   */\n  minItems: bigint;\n\n  /**\n   * `max_items` denotes that this field must not exceed a\n   * certain number of items as the upper limit. If the field contains more\n   * items than specified, an error message will be generated, requiring the\n   * field to maintain no more than the specified number of items.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // value must contain no more than 3 item(s)\n   *   repeated string value = 1 [(buf.validate.field).repeated.max_items = 3];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_items = 2;\n   */\n  maxItems: bigint;\n\n  /**\n   * `unique` indicates that all elements in this field must\n   * be unique. This rule is strictly applicable to scalar and enum\n   * types, with message types not being supported.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // repeated value must contain unique items\n   *   repeated string value = 1 [(buf.validate.field).repeated.unique = true];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool unique = 3;\n   */\n  unique: boolean;\n\n  /**\n   * `items` details the rules to be applied to each item\n   * in the field. Even for repeated message fields, validation is executed\n   * against each item unless `ignore` is specified.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // The items in the field `value` must follow the specified rules.\n   *   repeated string value = 1 [(buf.validate.field).repeated.items = {\n   *     string: {\n   *       min_len: 3\n   *       max_len: 10\n   *     }\n   *   }];\n   * }\n   * ```\n   *\n   * Note that the `required` rule does not apply. Repeated items\n   * cannot be unset.\n   *\n   * @generated from field: optional buf.validate.FieldRules items = 4;\n   */\n  items?: FieldRules;\n};\n\n/**\n * Describes the message buf.validate.RepeatedRules.\n * Use `create(RepeatedRulesSchema)` to create a new message.\n */\nexport const RepeatedRulesSchema: GenMessage<RepeatedRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 22);\n\n/**\n * MapRules describe the rules applied to `map` values.\n *\n * @generated from message buf.validate.MapRules\n */\nexport type MapRules = Message<\"buf.validate.MapRules\"> & {\n  /**\n   * Specifies the minimum number of key-value pairs allowed. If the field has\n   * fewer key-value pairs than specified, an error message is generated.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The field `value` must have at least 2 key-value pairs.\n   *   map<string, string> value = 1 [(buf.validate.field).map.min_pairs = 2];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_pairs = 1;\n   */\n  minPairs: bigint;\n\n  /**\n   * Specifies the maximum number of key-value pairs allowed. If the field has\n   * more key-value pairs than specified, an error message is generated.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The field `value` must have at most 3 key-value pairs.\n   *   map<string, string> value = 1 [(buf.validate.field).map.max_pairs = 3];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_pairs = 2;\n   */\n  maxPairs: bigint;\n\n  /**\n   * Specifies the rules to be applied to each key in the field.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The keys in the field `value` must follow the specified rules.\n   *   map<string, string> value = 1 [(buf.validate.field).map.keys = {\n   *     string: {\n   *       min_len: 3\n   *       max_len: 10\n   *     }\n   *   }];\n   * }\n   * ```\n   *\n   * Note that the `required` rule does not apply. Map keys cannot be unset.\n   *\n   * @generated from field: optional buf.validate.FieldRules keys = 4;\n   */\n  keys?: FieldRules;\n\n  /**\n   * Specifies the rules to be applied to the value of each key in the\n   * field. Message values will still have their validations evaluated unless\n   * `ignore` is specified.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The values in the field `value` must follow the specified rules.\n   *   map<string, string> value = 1 [(buf.validate.field).map.values = {\n   *     string: {\n   *       min_len: 5\n   *       max_len: 20\n   *     }\n   *   }];\n   * }\n   * ```\n   * Note that the `required` rule does not apply. Map values cannot be unset.\n   *\n   * @generated from field: optional buf.validate.FieldRules values = 5;\n   */\n  values?: FieldRules;\n};\n\n/**\n * Describes the message buf.validate.MapRules.\n * Use `create(MapRulesSchema)` to create a new message.\n */\nexport const MapRulesSchema: GenMessage<MapRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 23);\n\n/**\n * AnyRules describe rules applied exclusively to the `google.protobuf.Any` well-known type.\n *\n * @generated from message buf.validate.AnyRules\n */\nexport type AnyRules = Message<\"buf.validate.AnyRules\"> & {\n  /**\n   * `in` requires the field's `type_url` to be equal to one of the\n   * specified values. If it doesn't match any of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyAny {\n   *   //  The `value` field must have a `type_url` equal to one of the specified values.\n   *   google.protobuf.Any value = 1 [(buf.validate.field).any = {\n   *       in: [\"type.googleapis.com/MyType1\", \"type.googleapis.com/MyType2\"]\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string in = 2;\n   */\n  in: string[];\n\n  /**\n   * requires the field's type_url to be not equal to any of the specified values. If it matches any of the specified values, an error message is generated.\n   *\n   * ```proto\n   * message MyAny {\n   *   //  The `value` field must not have a `type_url` equal to any of the specified values.\n   *   google.protobuf.Any value = 1 [(buf.validate.field).any = {\n   *       not_in: [\"type.googleapis.com/ForbiddenType1\", \"type.googleapis.com/ForbiddenType2\"]\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string not_in = 3;\n   */\n  notIn: string[];\n};\n\n/**\n * Describes the message buf.validate.AnyRules.\n * Use `create(AnyRulesSchema)` to create a new message.\n */\nexport const AnyRulesSchema: GenMessage<AnyRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 24);\n\n/**\n * DurationRules describe the rules applied exclusively to the `google.protobuf.Duration` well-known type.\n *\n * @generated from message buf.validate.DurationRules\n */\nexport type DurationRules = Message<\"buf.validate.DurationRules\"> & {\n  /**\n   * `const` dictates that the field must match the specified value of the `google.protobuf.Duration` type exactly.\n   * If the field's value deviates from the specified value, an error message\n   * will be generated.\n   *\n   * ```proto\n   * message MyDuration {\n   *   // value must equal 5s\n   *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.const = \"5s\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.Duration const = 2;\n   */\n  const?: Duration;\n\n  /**\n   * @generated from oneof buf.validate.DurationRules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` stipulates that the field must be less than the specified value of the `google.protobuf.Duration` type,\n     * exclusive. If the field's value is greater than or equal to the specified\n     * value, an error message will be generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // value must be less than 5s\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = \"5s\"];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration lt = 3;\n     */\n    value: Duration;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` indicates that the field must be less than or equal to the specified\n     * value of the `google.protobuf.Duration` type, inclusive. If the field's value is greater than the specified value,\n     * an error message will be generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // value must be less than or equal to 10s\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.lte = \"10s\"];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration lte = 4;\n     */\n    value: Duration;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.DurationRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the duration field value to be greater than the specified\n     * value (exclusive). If the value of `gt` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // duration must be greater than 5s [duration.gt]\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.gt = { seconds: 5 }];\n     *\n     *   // duration must be greater than 5s and less than 10s [duration.gt_lt]\n     *   google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gt: { seconds: 5 }, lt: { seconds: 10 } }];\n     *\n     *   // duration must be greater than 10s or less than 5s [duration.gt_lt_exclusive]\n     *   google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gt: { seconds: 10 }, lt: { seconds: 5 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration gt = 5;\n     */\n    value: Duration;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the duration field value to be greater than or equal to the\n     * specified value (exclusive). If the value of `gte` is larger than a\n     * specified `lt` or `lte`, the range is reversed, and the field value must\n     * be outside the specified range. If the field value doesn't meet the\n     * required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *  // duration must be greater than or equal to 5s [duration.gte]\n     *  google.protobuf.Duration value = 1 [(buf.validate.field).duration.gte = { seconds: 5 }];\n     *\n     *  // duration must be greater than or equal to 5s and less than 10s [duration.gte_lt]\n     *  google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gte: { seconds: 5 }, lt: { seconds: 10 } }];\n     *\n     *  // duration must be greater than or equal to 10s or less than 5s [duration.gte_lt_exclusive]\n     *  google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gte: { seconds: 10 }, lt: { seconds: 5 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration gte = 6;\n     */\n    value: Duration;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` asserts that the field must be equal to one of the specified values of the `google.protobuf.Duration` type.\n   * If the field's value doesn't correspond to any of the specified values,\n   * an error message will be generated.\n   *\n   * ```proto\n   * message MyDuration {\n   *   // value must be in list [1s, 2s, 3s]\n   *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.in = [\"1s\", \"2s\", \"3s\"]];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Duration in = 7;\n   */\n  in: Duration[];\n\n  /**\n   * `not_in` denotes that the field must not be equal to\n   * any of the specified values of the `google.protobuf.Duration` type.\n   * If the field's value matches any of these values, an error message will be\n   * generated.\n   *\n   * ```proto\n   * message MyDuration {\n   *   // value must not be in list [1s, 2s, 3s]\n   *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.not_in = [\"1s\", \"2s\", \"3s\"]];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Duration not_in = 8;\n   */\n  notIn: Duration[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyDuration {\n   *   google.protobuf.Duration value = 1 [\n   *     (buf.validate.field).duration.example = { seconds: 1 },\n   *     (buf.validate.field).duration.example = { seconds: 2 },\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Duration example = 9;\n   */\n  example: Duration[];\n};\n\n/**\n * Describes the message buf.validate.DurationRules.\n * Use `create(DurationRulesSchema)` to create a new message.\n */\nexport const DurationRulesSchema: GenMessage<DurationRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 25);\n\n/**\n * FieldMaskRules describe rules applied exclusively to the `google.protobuf.FieldMask` well-known type.\n *\n * @generated from message buf.validate.FieldMaskRules\n */\nexport type FieldMaskRules = Message<\"buf.validate.FieldMaskRules\"> & {\n  /**\n   * `const` dictates that the field must match the specified value of the `google.protobuf.FieldMask` type exactly.\n   * If the field's value deviates from the specified value, an error message\n   * will be generated.\n   *\n   * ```proto\n   * message MyFieldMask {\n   *   // value must equal [\"a\"]\n   *   google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask.const = {\n   *       paths: [\"a\"]\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.FieldMask const = 1;\n   */\n  const?: FieldMask;\n\n  /**\n   * `in` requires the field value to only contain paths matching specified\n   * values or their subpaths.\n   * If any of the field value's paths doesn't match the rule,\n   * an error message is generated.\n   * See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask\n   *\n   * ```proto\n   * message MyFieldMask {\n   *   //  The `value` FieldMask must only contain paths listed in `in`.\n   *   google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = {\n   *       in: [\"a\", \"b\", \"c.a\"]\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string in = 2;\n   */\n  in: string[];\n\n  /**\n   * `not_in` requires the field value to not contain paths matching specified\n   * values or their subpaths.\n   * If any of the field value's paths matches the rule,\n   * an error message is generated.\n   * See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask\n   *\n   * ```proto\n   * message MyFieldMask {\n   *   //  The `value` FieldMask shall not contain paths listed in `not_in`.\n   *   google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = {\n   *       not_in: [\"forbidden\", \"immutable\", \"c.a\"]\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string not_in = 3;\n   */\n  notIn: string[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFieldMask {\n   *   google.protobuf.FieldMask value = 1 [\n   *     (buf.validate.field).field_mask.example = { paths: [\"a\", \"b\"] },\n   *     (buf.validate.field).field_mask.example = { paths: [\"c.a\", \"d\"] },\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.FieldMask example = 4;\n   */\n  example: FieldMask[];\n};\n\n/**\n * Describes the message buf.validate.FieldMaskRules.\n * Use `create(FieldMaskRulesSchema)` to create a new message.\n */\nexport const FieldMaskRulesSchema: GenMessage<FieldMaskRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 26);\n\n/**\n * TimestampRules describe the rules applied exclusively to the `google.protobuf.Timestamp` well-known type.\n *\n * @generated from message buf.validate.TimestampRules\n */\nexport type TimestampRules = Message<\"buf.validate.TimestampRules\"> & {\n  /**\n   * `const` dictates that this field, of the `google.protobuf.Timestamp` type, must exactly match the specified value. If the field value doesn't correspond to the specified timestamp, an error message will be generated.\n   *\n   * ```proto\n   * message MyTimestamp {\n   *   // value must equal 2023-05-03T10:00:00Z\n   *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.const = {seconds: 1727998800}];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.Timestamp const = 2;\n   */\n  const?: Timestamp;\n\n  /**\n   * @generated from oneof buf.validate.TimestampRules.less_than\n   */\n  lessThan: {\n    /**\n     * requires the duration field value to be less than the specified value (field < value). If the field value doesn't meet the required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // duration must be less than 'P3D' [duration.lt]\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = { seconds: 259200 }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp lt = 3;\n     */\n    value: Timestamp;\n    case: \"lt\";\n  } | {\n    /**\n     * requires the timestamp field value to be less than or equal to the specified value (field <= value). If the field value doesn't meet the required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // timestamp must be less than or equal to '2023-05-14T00:00:00Z' [timestamp.lte]\n     *   google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.lte = { seconds: 1678867200 }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp lte = 4;\n     */\n    value: Timestamp;\n    case: \"lte\";\n  } | {\n    /**\n     * `lt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be less than the current time. `lt_now` can only be used with the `within` rule.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *  // value must be less than now\n     *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool lt_now = 7;\n     */\n    value: boolean;\n    case: \"ltNow\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.TimestampRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the timestamp field value to be greater than the specified\n     * value (exclusive). If the value of `gt` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // timestamp must be greater than '2023-01-01T00:00:00Z' [timestamp.gt]\n     *   google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gt = { seconds: 1672444800 }];\n     *\n     *   // timestamp must be greater than '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gt_lt]\n     *   google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gt: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }];\n     *\n     *   // timestamp must be greater than '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gt_lt_exclusive]\n     *   google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gt: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp gt = 5;\n     */\n    value: Timestamp;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the timestamp field value to be greater than or equal to the\n     * specified value (exclusive). If the value of `gte` is larger than a\n     * specified `lt` or `lte`, the range is reversed, and the field value\n     * must be outside the specified range. If the field value doesn't meet\n     * the required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' [timestamp.gte]\n     *   google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gte = { seconds: 1672444800 }];\n     *\n     *   // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gte_lt]\n     *   google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gte: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }];\n     *\n     *   // timestamp must be greater than or equal to '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gte_lt_exclusive]\n     *   google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gte: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp gte = 6;\n     */\n    value: Timestamp;\n    case: \"gte\";\n  } | {\n    /**\n     * `gt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be greater than the current time. `gt_now` can only be used with the `within` rule.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // value must be greater than now\n     *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.gt_now = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool gt_now = 8;\n     */\n    value: boolean;\n    case: \"gtNow\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `within` specifies that this field, of the `google.protobuf.Timestamp` type, must be within the specified duration of the current time. If the field value isn't within the duration, an error message is generated.\n   *\n   * ```proto\n   * message MyTimestamp {\n   *   // value must be within 1 hour of now\n   *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.within = {seconds: 3600}];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.Duration within = 9;\n   */\n  within?: Duration;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyTimestamp {\n   *   google.protobuf.Timestamp value = 1 [\n   *     (buf.validate.field).timestamp.example = { seconds: 1672444800 },\n   *     (buf.validate.field).timestamp.example = { seconds: 1672531200 },\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Timestamp example = 10;\n   */\n  example: Timestamp[];\n};\n\n/**\n * Describes the message buf.validate.TimestampRules.\n * Use `create(TimestampRulesSchema)` to create a new message.\n */\nexport const TimestampRulesSchema: GenMessage<TimestampRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 27);\n\n/**\n * `Violations` is a collection of `Violation` messages. This message type is returned by\n * Protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules.\n * Each individual violation is represented by a `Violation` message.\n *\n * @generated from message buf.validate.Violations\n */\nexport type Violations = Message<\"buf.validate.Violations\"> & {\n  /**\n   * `violations` is a repeated field that contains all the `Violation` messages corresponding to the violations detected.\n   *\n   * @generated from field: repeated buf.validate.Violation violations = 1;\n   */\n  violations: Violation[];\n};\n\n/**\n * Describes the message buf.validate.Violations.\n * Use `create(ViolationsSchema)` to create a new message.\n */\nexport const ViolationsSchema: GenMessage<Violations> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 28);\n\n/**\n * `Violation` represents a single instance where a validation rule, expressed\n * as a `Rule`, was not met. It provides information about the field that\n * caused the violation, the specific rule that wasn't fulfilled, and a\n * human-readable error message.\n *\n * For example, consider the following message:\n *\n * ```proto\n * message User {\n *     int32 age = 1 [(buf.validate.field).cel = {\n *         id: \"user.age\",\n *         expression: \"this < 18 ? 'User must be at least 18 years old' : ''\",\n *     }];\n * }\n * ```\n *\n * It could produce the following violation:\n *\n * ```json\n * {\n *   \"ruleId\": \"user.age\",\n *   \"message\": \"User must be at least 18 years old\",\n *   \"field\": {\n *     \"elements\": [\n *       {\n *         \"fieldNumber\": 1,\n *         \"fieldName\": \"age\",\n *         \"fieldType\": \"TYPE_INT32\"\n *       }\n *     ]\n *   },\n *   \"rule\": {\n *     \"elements\": [\n *       {\n *         \"fieldNumber\": 23,\n *         \"fieldName\": \"cel\",\n *         \"fieldType\": \"TYPE_MESSAGE\",\n *         \"index\": \"0\"\n *       }\n *     ]\n *   }\n * }\n * ```\n *\n * @generated from message buf.validate.Violation\n */\nexport type Violation = Message<\"buf.validate.Violation\"> & {\n  /**\n   * `field` is a machine-readable path to the field that failed validation.\n   * This could be a nested field, in which case the path will include all the parent fields leading to the actual field that caused the violation.\n   *\n   * For example, consider the following message:\n   *\n   * ```proto\n   * message Message {\n   *   bool a = 1 [(buf.validate.field).required = true];\n   * }\n   * ```\n   *\n   * It could produce the following violation:\n   *\n   * ```textproto\n   * violation {\n   *   field { element { field_number: 1, field_name: \"a\", field_type: 8 } }\n   *   ...\n   * }\n   * ```\n   *\n   * @generated from field: optional buf.validate.FieldPath field = 5;\n   */\n  field?: FieldPath;\n\n  /**\n   * `rule` is a machine-readable path that points to the specific rule that failed validation.\n   * This will be a nested field starting from the FieldRules of the field that failed validation.\n   * For custom rules, this will provide the path of the rule, e.g. `cel[0]`.\n   *\n   * For example, consider the following message:\n   *\n   * ```proto\n   * message Message {\n   *   bool a = 1 [(buf.validate.field).required = true];\n   *   bool b = 2 [(buf.validate.field).cel = {\n   *     id: \"custom_rule\",\n   *     expression: \"!this ? 'b must be true': ''\"\n   *   }]\n   * }\n   * ```\n   *\n   * It could produce the following violations:\n   *\n   * ```textproto\n   * violation {\n   *   rule { element { field_number: 25, field_name: \"required\", field_type: 8 } }\n   *   ...\n   * }\n   * violation {\n   *   rule { element { field_number: 23, field_name: \"cel\", field_type: 11, index: 0 } }\n   *   ...\n   * }\n   * ```\n   *\n   * @generated from field: optional buf.validate.FieldPath rule = 6;\n   */\n  rule?: FieldPath;\n\n  /**\n   * `rule_id` is the unique identifier of the `Rule` that was not fulfilled.\n   * This is the same `id` that was specified in the `Rule` message, allowing easy tracing of which rule was violated.\n   *\n   * @generated from field: optional string rule_id = 2;\n   */\n  ruleId: string;\n\n  /**\n   * `message` is a human-readable error message that describes the nature of the violation.\n   * This can be the default error message from the violated `Rule`, or it can be a custom message that gives more context about the violation.\n   *\n   * @generated from field: optional string message = 3;\n   */\n  message: string;\n\n  /**\n   * `for_key` indicates whether the violation was caused by a map key, rather than a value.\n   *\n   * @generated from field: optional bool for_key = 4;\n   */\n  forKey: boolean;\n};\n\n/**\n * Describes the message buf.validate.Violation.\n * Use `create(ViolationSchema)` to create a new message.\n */\nexport const ViolationSchema: GenMessage<Violation> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 29);\n\n/**\n * `FieldPath` provides a path to a nested protobuf field.\n *\n * This message provides enough information to render a dotted field path even without protobuf descriptors.\n * It also provides enough information to resolve a nested field through unknown wire data.\n *\n * @generated from message buf.validate.FieldPath\n */\nexport type FieldPath = Message<\"buf.validate.FieldPath\"> & {\n  /**\n   * `elements` contains each element of the path, starting from the root and recursing downward.\n   *\n   * @generated from field: repeated buf.validate.FieldPathElement elements = 1;\n   */\n  elements: FieldPathElement[];\n};\n\n/**\n * Describes the message buf.validate.FieldPath.\n * Use `create(FieldPathSchema)` to create a new message.\n */\nexport const FieldPathSchema: GenMessage<FieldPath> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 30);\n\n/**\n * `FieldPathElement` provides enough information to nest through a single protobuf field.\n *\n * If the selected field is a map or repeated field, the `subscript` value selects a specific element from it.\n * A path that refers to a value nested under a map key or repeated field index will have a `subscript` value.\n * The `field_type` field allows unambiguous resolution of a field even if descriptors are not available.\n *\n * @generated from message buf.validate.FieldPathElement\n */\nexport type FieldPathElement = Message<\"buf.validate.FieldPathElement\"> & {\n  /**\n   * `field_number` is the field number this path element refers to.\n   *\n   * @generated from field: optional int32 field_number = 1;\n   */\n  fieldNumber: number;\n\n  /**\n   * `field_name` contains the field name this path element refers to.\n   * This can be used to display a human-readable path even if the field number is unknown.\n   *\n   * @generated from field: optional string field_name = 2;\n   */\n  fieldName: string;\n\n  /**\n   * `field_type` specifies the type of this field. When using reflection, this value is not needed.\n   *\n   * This value is provided to make it possible to traverse unknown fields through wire data.\n   * When traversing wire data, be mindful of both packed[1] and delimited[2] encoding schemes.\n   *\n   * [1]: https://protobuf.dev/programming-guides/encoding/#packed\n   * [2]: https://protobuf.dev/programming-guides/encoding/#groups\n   *\n   * N.B.: Although groups are deprecated, the corresponding delimited encoding scheme is not, and\n   * can be explicitly used in Protocol Buffers 2023 Edition.\n   *\n   * @generated from field: optional google.protobuf.FieldDescriptorProto.Type field_type = 3;\n   */\n  fieldType: FieldDescriptorProto_Type;\n\n  /**\n   * `key_type` specifies the map key type of this field. This value is useful when traversing\n   * unknown fields through wire data: specifically, it allows handling the differences between\n   * different integer encodings.\n   *\n   * @generated from field: optional google.protobuf.FieldDescriptorProto.Type key_type = 4;\n   */\n  keyType: FieldDescriptorProto_Type;\n\n  /**\n   * `value_type` specifies map value type of this field. This is useful if you want to display a\n   * value inside unknown fields through wire data.\n   *\n   * @generated from field: optional google.protobuf.FieldDescriptorProto.Type value_type = 5;\n   */\n  valueType: FieldDescriptorProto_Type;\n\n  /**\n   * `subscript` contains a repeated index or map key, if this path element nests into a repeated or map field.\n   *\n   * @generated from oneof buf.validate.FieldPathElement.subscript\n   */\n  subscript: {\n    /**\n     * `index` specifies a 0-based index into a repeated field.\n     *\n     * @generated from field: uint64 index = 6;\n     */\n    value: bigint;\n    case: \"index\";\n  } | {\n    /**\n     * `bool_key` specifies a map key of type bool.\n     *\n     * @generated from field: bool bool_key = 7;\n     */\n    value: boolean;\n    case: \"boolKey\";\n  } | {\n    /**\n     * `int_key` specifies a map key of type int32, int64, sint32, sint64, sfixed32 or sfixed64.\n     *\n     * @generated from field: int64 int_key = 8;\n     */\n    value: bigint;\n    case: \"intKey\";\n  } | {\n    /**\n     * `uint_key` specifies a map key of type uint32, uint64, fixed32 or fixed64.\n     *\n     * @generated from field: uint64 uint_key = 9;\n     */\n    value: bigint;\n    case: \"uintKey\";\n  } | {\n    /**\n     * `string_key` specifies a map key of type string.\n     *\n     * @generated from field: string string_key = 10;\n     */\n    value: string;\n    case: \"stringKey\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message buf.validate.FieldPathElement.\n * Use `create(FieldPathElementSchema)` to create a new message.\n */\nexport const FieldPathElementSchema: GenMessage<FieldPathElement> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 31);\n\n/**\n * Specifies how `FieldRules.ignore` behaves, depending on the field's value, and\n * whether the field tracks presence.\n *\n * @generated from enum buf.validate.Ignore\n */\nexport enum Ignore {\n  /**\n   * Ignore rules if the field tracks presence and is unset. This is the default\n   * behavior.\n   *\n   * In proto3, only message fields, members of a Protobuf `oneof`, and fields\n   * with the `optional` label track presence. Consequently, the following fields\n   * are always validated, whether a value is set or not:\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message RulesApply {\n   *   string email = 1 [\n   *     (buf.validate.field).string.email = true\n   *   ];\n   *   int32 age = 2 [\n   *     (buf.validate.field).int32.gt = 0\n   *   ];\n   *   repeated string labels = 3 [\n   *     (buf.validate.field).repeated.min_items = 1\n   *   ];\n   * }\n   * ```\n   *\n   * In contrast, the following fields track presence, and are only validated if\n   * a value is set:\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message RulesApplyIfSet {\n   *   optional string email = 1 [\n   *     (buf.validate.field).string.email = true\n   *   ];\n   *   oneof ref {\n   *     string reference = 2 [\n   *       (buf.validate.field).string.uuid = true\n   *     ];\n   *     string name = 3 [\n   *       (buf.validate.field).string.min_len = 4\n   *     ];\n   *   }\n   *   SomeMessage msg = 4 [\n   *     (buf.validate.field).cel = {/* ... *\\/}\n   *   ];\n   * }\n   * ```\n   *\n   * To ensure that such a field is set, add the `required` rule.\n   *\n   * To learn which fields track presence, see the\n   * [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat).\n   *\n   * @generated from enum value: IGNORE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * Ignore rules if the field is unset, or set to the zero value.\n   *\n   * The zero value depends on the field type:\n   * - For strings, the zero value is the empty string.\n   * - For bytes, the zero value is empty bytes.\n   * - For bool, the zero value is false.\n   * - For numeric types, the zero value is zero.\n   * - For enums, the zero value is the first defined enum value.\n   * - For repeated fields, the zero is an empty list.\n   * - For map fields, the zero is an empty map.\n   * - For message fields, absence of the message (typically a null-value) is considered zero value.\n   *\n   * For fields that track presence (e.g. adding the `optional` label in proto3),\n   * this a no-op and behavior is the same as the default `IGNORE_UNSPECIFIED`.\n   *\n   * @generated from enum value: IGNORE_IF_ZERO_VALUE = 1;\n   */\n  IF_ZERO_VALUE = 1,\n\n  /**\n   * Always ignore rules, including the `required` rule.\n   *\n   * This is useful for ignoring the rules of a referenced message, or to\n   * temporarily ignore rules during development.\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field's rules will always be ignored, including any validations\n   *   // on value's fields.\n   *   MyOtherMessage value = 1 [\n   *     (buf.validate.field).ignore = IGNORE_ALWAYS\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from enum value: IGNORE_ALWAYS = 3;\n   */\n  ALWAYS = 3,\n}\n\n/**\n * Describes the enum buf.validate.Ignore.\n */\nexport const IgnoreSchema: GenEnum<Ignore> = /*@__PURE__*/\n  enumDesc(file_buf_validate_validate, 0);\n\n/**\n * KnownRegex contains some well-known patterns.\n *\n * @generated from enum buf.validate.KnownRegex\n */\nexport enum KnownRegex {\n  /**\n   * @generated from enum value: KNOWN_REGEX_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2).\n   *\n   * @generated from enum value: KNOWN_REGEX_HTTP_HEADER_NAME = 1;\n   */\n  HTTP_HEADER_NAME = 1,\n\n  /**\n   * HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4).\n   *\n   * @generated from enum value: KNOWN_REGEX_HTTP_HEADER_VALUE = 2;\n   */\n  HTTP_HEADER_VALUE = 2,\n}\n\n/**\n * Describes the enum buf.validate.KnownRegex.\n */\nexport const KnownRegexSchema: GenEnum<KnownRegex> = /*@__PURE__*/\n  enumDesc(file_buf_validate_validate, 1);\n\n/**\n * Rules specify the validations to be performed on this message. By default,\n * no validation is performed against a message.\n *\n * @generated from extension: optional buf.validate.MessageRules message = 1159;\n */\nexport const message: GenExtension<MessageOptions, MessageRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 0);\n\n/**\n * Rules specify the validations to be performed on this oneof. By default,\n * no validation is performed against a oneof.\n *\n * @generated from extension: optional buf.validate.OneofRules oneof = 1159;\n */\nexport const oneof: GenExtension<OneofOptions, OneofRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 1);\n\n/**\n * Rules specify the validations to be performed on this field. By default,\n * no validation is performed against a field.\n *\n * @generated from extension: optional buf.validate.FieldRules field = 1159;\n */\nexport const field: GenExtension<FieldOptions, FieldRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 2);\n\n/**\n * Specifies predefined rules. When extending a standard rule message,\n * this adds additional CEL expressions that apply when the extension is used.\n *\n * ```proto\n * extend buf.validate.Int32Rules {\n *   bool is_zero [(buf.validate.predefined).cel = {\n *     id: \"int32.is_zero\",\n *     message: \"value must be zero\",\n *     expression: \"!rule || this == 0\",\n *   }];\n * }\n *\n * message Foo {\n *   int32 reserved = 1 [(buf.validate.field).int32.(is_zero) = true];\n * }\n * ```\n *\n * @generated from extension: optional buf.validate.PredefinedRules predefined = 1160;\n */\nexport const predefined: GenExtension<FieldOptions, PredefinedRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 3);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/gnostic/openapi/v3/annotations_pb.ts",
    "content": "// Copyright 2022 Google LLC. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//    http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file gnostic/openapi/v3/annotations.proto (package gnostic.openapi.v3, syntax proto3)\n/* eslint-disable */\n\nimport type { GenExtension, GenFile } from \"@bufbuild/protobuf/codegenv2\";\nimport { extDesc, fileDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Document, Operation, Schema } from \"./openapiv3_pb.ts\";\nimport { file_gnostic_openapi_v3_openapiv3 } from \"./openapiv3_pb.ts\";\nimport type { FieldOptions, FileOptions, MessageOptions, MethodOptions } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_descriptor } from \"@bufbuild/protobuf/wkt\";\n\n/**\n * Describes the file gnostic/openapi/v3/annotations.proto.\n */\nexport const file_gnostic_openapi_v3_annotations: GenFile = /*@__PURE__*/\n  fileDesc(\"CiRnbm9zdGljL29wZW5hcGkvdjMvYW5ub3RhdGlvbnMucHJvdG8SEmdub3N0aWMub3BlbmFwaS52MzpXCghkb2N1bWVudBIcLmdvb2dsZS5wcm90b2J1Zi5GaWxlT3B0aW9ucxj3CCABKAsyHC5nbm9zdGljLm9wZW5hcGkudjMuRG9jdW1lbnRSCGRvY3VtZW50OlwKCW9wZXJhdGlvbhIeLmdvb2dsZS5wcm90b2J1Zi5NZXRob2RPcHRpb25zGPcIIAEoCzIdLmdub3N0aWMub3BlbmFwaS52My5PcGVyYXRpb25SCW9wZXJhdGlvbjpUCgZzY2hlbWESHy5nb29nbGUucHJvdG9idWYuTWVzc2FnZU9wdGlvbnMY9wggASgLMhouZ25vc3RpYy5vcGVuYXBpLnYzLlNjaGVtYVIGc2NoZW1hOlYKCHByb3BlcnR5Eh0uZ29vZ2xlLnByb3RvYnVmLkZpZWxkT3B0aW9ucxj3CCABKAsyGi5nbm9zdGljLm9wZW5hcGkudjMuU2NoZW1hUghwcm9wZXJ0eUJaCg5vcmcub3BlbmFwaV92M0IQQW5ub3RhdGlvbnNQcm90b1ABWi5naXRodWIuY29tL2dvb2dsZS9nbm9zdGljL29wZW5hcGl2MztvcGVuYXBpX3YzogIDT0FTYgZwcm90bzM\", [file_gnostic_openapi_v3_openapiv3, file_google_protobuf_descriptor]);\n\n/**\n * @generated from extension: gnostic.openapi.v3.Document document = 1143;\n */\nexport const document: GenExtension<FileOptions, Document> = /*@__PURE__*/\n  extDesc(file_gnostic_openapi_v3_annotations, 0);\n\n/**\n * @generated from extension: gnostic.openapi.v3.Operation operation = 1143;\n */\nexport const operation: GenExtension<MethodOptions, Operation> = /*@__PURE__*/\n  extDesc(file_gnostic_openapi_v3_annotations, 1);\n\n/**\n * @generated from extension: gnostic.openapi.v3.Schema schema = 1143;\n */\nexport const schema: GenExtension<MessageOptions, Schema> = /*@__PURE__*/\n  extDesc(file_gnostic_openapi_v3_annotations, 2);\n\n/**\n * @generated from extension: gnostic.openapi.v3.Schema property = 1143;\n */\nexport const property: GenExtension<FieldOptions, Schema> = /*@__PURE__*/\n  extDesc(file_gnostic_openapi_v3_annotations, 3);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/gnostic/openapi/v3/openapiv3_pb.ts",
    "content": "// Copyright 2020 Google LLC. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//    http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// THIS FILE IS AUTOMATICALLY GENERATED.\n\n// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file gnostic/openapi/v3/openapiv3.proto (package gnostic.openapi.v3, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Any as Any$1 } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_any } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file gnostic/openapi/v3/openapiv3.proto.\n */\nexport const file_gnostic_openapi_v3_openapiv3: GenFile = /*@__PURE__*/\n  fileDesc(\"CiJnbm9zdGljL29wZW5hcGkvdjMvb3BlbmFwaXYzLnByb3RvEhJnbm9zdGljLm9wZW5hcGkudjMifAoYQWRkaXRpb25hbFByb3BlcnRpZXNJdGVtEkQKE3NjaGVtYV9vcl9yZWZlcmVuY2UYASABKAsyJS5nbm9zdGljLm9wZW5hcGkudjMuU2NoZW1hT3JSZWZlcmVuY2VIABIRCgdib29sZWFuGAIgASgISABCBwoFb25lb2YiOAoDQW55EiMKBXZhbHVlGAEgASgLMhQuZ29vZ2xlLnByb3RvYnVmLkFueRIMCgR5YW1sGAIgASgJIngKD0FueU9yRXhwcmVzc2lvbhImCgNhbnkYASABKAsyFy5nbm9zdGljLm9wZW5hcGkudjMuQW55SAASNAoKZXhwcmVzc2lvbhgCIAEoCzIeLmdub3N0aWMub3BlbmFwaS52My5FeHByZXNzaW9uSABCBwoFb25lb2YiegoIQ2FsbGJhY2sSLwoEcGF0aBgBIAMoCzIhLmdub3N0aWMub3BlbmFwaS52My5OYW1lZFBhdGhJdGVtEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAIgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55IoQBChNDYWxsYmFja09yUmVmZXJlbmNlEjAKCGNhbGxiYWNrGAEgASgLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLkNhbGxiYWNrSAASMgoJcmVmZXJlbmNlGAIgASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLlJlZmVyZW5jZUgAQgcKBW9uZW9mImQKFUNhbGxiYWNrc09yUmVmZXJlbmNlcxJLChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyLC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRDYWxsYmFja09yUmVmZXJlbmNlIv8ECgpDb21wb25lbnRzEjgKB3NjaGVtYXMYASABKAsyJy5nbm9zdGljLm9wZW5hcGkudjMuU2NoZW1hc09yUmVmZXJlbmNlcxI8CglyZXNwb25zZXMYAiABKAsyKS5nbm9zdGljLm9wZW5hcGkudjMuUmVzcG9uc2VzT3JSZWZlcmVuY2VzEj4KCnBhcmFtZXRlcnMYAyABKAsyKi5nbm9zdGljLm9wZW5hcGkudjMuUGFyYW1ldGVyc09yUmVmZXJlbmNlcxI6CghleGFtcGxlcxgEIAEoCzIoLmdub3N0aWMub3BlbmFwaS52My5FeGFtcGxlc09yUmVmZXJlbmNlcxJFCg5yZXF1ZXN0X2JvZGllcxgFIAEoCzItLmdub3N0aWMub3BlbmFwaS52My5SZXF1ZXN0Qm9kaWVzT3JSZWZlcmVuY2VzEjgKB2hlYWRlcnMYBiABKAsyJy5nbm9zdGljLm9wZW5hcGkudjMuSGVhZGVyc09yUmVmZXJlbmNlcxJJChBzZWN1cml0eV9zY2hlbWVzGAcgASgLMi8uZ25vc3RpYy5vcGVuYXBpLnYzLlNlY3VyaXR5U2NoZW1lc09yUmVmZXJlbmNlcxI0CgVsaW5rcxgIIAEoCzIlLmdub3N0aWMub3BlbmFwaS52My5MaW5rc09yUmVmZXJlbmNlcxI8CgljYWxsYmFja3MYCSABKAsyKS5nbm9zdGljLm9wZW5hcGkudjMuQ2FsbGJhY2tzT3JSZWZlcmVuY2VzEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAogAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55InIKB0NvbnRhY3QSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSDQoFZW1haWwYAyABKAkSPQoXc3BlY2lmaWNhdGlvbl9leHRlbnNpb24YBCADKAsyHC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRBbnkiTQoLRGVmYXVsdFR5cGUSEAoGbnVtYmVyGAEgASgBSAASEQoHYm9vbGVhbhgCIAEoCEgAEhAKBnN0cmluZxgDIAEoCUgAQgcKBW9uZW9mIpMBCg1EaXNjcmltaW5hdG9yEhUKDXByb3BlcnR5X25hbWUYASABKAkSLAoHbWFwcGluZxgCIAEoCzIbLmdub3N0aWMub3BlbmFwaS52My5TdHJpbmdzEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAMgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55IqgDCghEb2N1bWVudBIPCgdvcGVuYXBpGAEgASgJEiYKBGluZm8YAiABKAsyGC5nbm9zdGljLm9wZW5hcGkudjMuSW5mbxIrCgdzZXJ2ZXJzGAMgAygLMhouZ25vc3RpYy5vcGVuYXBpLnYzLlNlcnZlchIoCgVwYXRocxgEIAEoCzIZLmdub3N0aWMub3BlbmFwaS52My5QYXRocxIyCgpjb21wb25lbnRzGAUgASgLMh4uZ25vc3RpYy5vcGVuYXBpLnYzLkNvbXBvbmVudHMSOQoIc2VjdXJpdHkYBiADKAsyJy5nbm9zdGljLm9wZW5hcGkudjMuU2VjdXJpdHlSZXF1aXJlbWVudBIlCgR0YWdzGAcgAygLMhcuZ25vc3RpYy5vcGVuYXBpLnYzLlRhZxI3Cg1leHRlcm5hbF9kb2NzGAggASgLMiAuZ25vc3RpYy5vcGVuYXBpLnYzLkV4dGVybmFsRG9jcxI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgJIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSLRAQoIRW5jb2RpbmcSFAoMY29udGVudF90eXBlGAEgASgJEjgKB2hlYWRlcnMYAiABKAsyJy5nbm9zdGljLm9wZW5hcGkudjMuSGVhZGVyc09yUmVmZXJlbmNlcxINCgVzdHlsZRgDIAEoCRIPCgdleHBsb2RlGAQgASgIEhYKDmFsbG93X3Jlc2VydmVkGAUgASgIEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAYgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55Ik0KCUVuY29kaW5ncxJAChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyIS5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRFbmNvZGluZyKuAQoHRXhhbXBsZRIPCgdzdW1tYXJ5GAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEiYKBXZhbHVlGAMgASgLMhcuZ25vc3RpYy5vcGVuYXBpLnYzLkFueRIWCg5leHRlcm5hbF92YWx1ZRgEIAEoCRI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgFIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSKBAQoSRXhhbXBsZU9yUmVmZXJlbmNlEi4KB2V4YW1wbGUYASABKAsyGy5nbm9zdGljLm9wZW5hcGkudjMuRXhhbXBsZUgAEjIKCXJlZmVyZW5jZRgCIAEoCzIdLmdub3N0aWMub3BlbmFwaS52My5SZWZlcmVuY2VIAEIHCgVvbmVvZiJiChRFeGFtcGxlc09yUmVmZXJlbmNlcxJKChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyKy5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRFeGFtcGxlT3JSZWZlcmVuY2UiSQoKRXhwcmVzc2lvbhI7ChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyHC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRBbnkibwoMRXh0ZXJuYWxEb2NzEhMKC2Rlc2NyaXB0aW9uGAEgASgJEgsKA3VybBgCIAEoCRI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgDIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSKjAwoGSGVhZGVyEhMKC2Rlc2NyaXB0aW9uGAEgASgJEhAKCHJlcXVpcmVkGAIgASgIEhIKCmRlcHJlY2F0ZWQYAyABKAgSGQoRYWxsb3dfZW1wdHlfdmFsdWUYBCABKAgSDQoFc3R5bGUYBSABKAkSDwoHZXhwbG9kZRgGIAEoCBIWCg5hbGxvd19yZXNlcnZlZBgHIAEoCBI1CgZzY2hlbWEYCCABKAsyJS5nbm9zdGljLm9wZW5hcGkudjMuU2NoZW1hT3JSZWZlcmVuY2USKAoHZXhhbXBsZRgJIAEoCzIXLmdub3N0aWMub3BlbmFwaS52My5BbnkSOgoIZXhhbXBsZXMYCiABKAsyKC5nbm9zdGljLm9wZW5hcGkudjMuRXhhbXBsZXNPclJlZmVyZW5jZXMSLwoHY29udGVudBgLIAEoCzIeLmdub3N0aWMub3BlbmFwaS52My5NZWRpYVR5cGVzEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAwgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55In4KEUhlYWRlck9yUmVmZXJlbmNlEiwKBmhlYWRlchgBIAEoCzIaLmdub3N0aWMub3BlbmFwaS52My5IZWFkZXJIABIyCglyZWZlcmVuY2UYAiABKAsyHS5nbm9zdGljLm9wZW5hcGkudjMuUmVmZXJlbmNlSABCBwoFb25lb2YiYAoTSGVhZGVyc09yUmVmZXJlbmNlcxJJChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyKi5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRIZWFkZXJPclJlZmVyZW5jZSKBAgoESW5mbxINCgV0aXRsZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIYChB0ZXJtc19vZl9zZXJ2aWNlGAMgASgJEiwKB2NvbnRhY3QYBCABKAsyGy5nbm9zdGljLm9wZW5hcGkudjMuQ29udGFjdBIsCgdsaWNlbnNlGAUgASgLMhsuZ25vc3RpYy5vcGVuYXBpLnYzLkxpY2Vuc2USDwoHdmVyc2lvbhgGIAEoCRI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgHIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueRIPCgdzdW1tYXJ5GAggASgJIk8KCUl0ZW1zSXRlbRJCChNzY2hlbWFfb3JfcmVmZXJlbmNlGAEgAygLMiUuZ25vc3RpYy5vcGVuYXBpLnYzLlNjaGVtYU9yUmVmZXJlbmNlImMKB0xpY2Vuc2USDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSPQoXc3BlY2lmaWNhdGlvbl9leHRlbnNpb24YAyADKAsyHC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRBbnkipwIKBExpbmsSFQoNb3BlcmF0aW9uX3JlZhgBIAEoCRIUCgxvcGVyYXRpb25faWQYAiABKAkSNwoKcGFyYW1ldGVycxgDIAEoCzIjLmdub3N0aWMub3BlbmFwaS52My5BbnlPckV4cHJlc3Npb24SOQoMcmVxdWVzdF9ib2R5GAQgASgLMiMuZ25vc3RpYy5vcGVuYXBpLnYzLkFueU9yRXhwcmVzc2lvbhITCgtkZXNjcmlwdGlvbhgFIAEoCRIqCgZzZXJ2ZXIYBiABKAsyGi5nbm9zdGljLm9wZW5hcGkudjMuU2VydmVyEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAcgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55IngKD0xpbmtPclJlZmVyZW5jZRIoCgRsaW5rGAEgASgLMhguZ25vc3RpYy5vcGVuYXBpLnYzLkxpbmtIABIyCglyZWZlcmVuY2UYAiABKAsyHS5nbm9zdGljLm9wZW5hcGkudjMuUmVmZXJlbmNlSABCBwoFb25lb2YiXAoRTGlua3NPclJlZmVyZW5jZXMSRwoVYWRkaXRpb25hbF9wcm9wZXJ0aWVzGAEgAygLMiguZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkTGlua09yUmVmZXJlbmNlIpgCCglNZWRpYVR5cGUSNQoGc2NoZW1hGAEgASgLMiUuZ25vc3RpYy5vcGVuYXBpLnYzLlNjaGVtYU9yUmVmZXJlbmNlEigKB2V4YW1wbGUYAiABKAsyFy5nbm9zdGljLm9wZW5hcGkudjMuQW55EjoKCGV4YW1wbGVzGAMgASgLMiguZ25vc3RpYy5vcGVuYXBpLnYzLkV4YW1wbGVzT3JSZWZlcmVuY2VzEi8KCGVuY29kaW5nGAQgASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLkVuY29kaW5ncxI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgFIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSJPCgpNZWRpYVR5cGVzEkEKFWFkZGl0aW9uYWxfcHJvcGVydGllcxgBIAMoCzIiLmdub3N0aWMub3BlbmFwaS52My5OYW1lZE1lZGlhVHlwZSJACghOYW1lZEFueRIMCgRuYW1lGAEgASgJEiYKBXZhbHVlGAIgASgLMhcuZ25vc3RpYy5vcGVuYXBpLnYzLkFueSJgChhOYW1lZENhbGxiYWNrT3JSZWZlcmVuY2USDAoEbmFtZRgBIAEoCRI2CgV2YWx1ZRgCIAEoCzInLmdub3N0aWMub3BlbmFwaS52My5DYWxsYmFja09yUmVmZXJlbmNlIkoKDU5hbWVkRW5jb2RpbmcSDAoEbmFtZRgBIAEoCRIrCgV2YWx1ZRgCIAEoCzIcLmdub3N0aWMub3BlbmFwaS52My5FbmNvZGluZyJeChdOYW1lZEV4YW1wbGVPclJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEjUKBXZhbHVlGAIgASgLMiYuZ25vc3RpYy5vcGVuYXBpLnYzLkV4YW1wbGVPclJlZmVyZW5jZSJcChZOYW1lZEhlYWRlck9yUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSNAoFdmFsdWUYAiABKAsyJS5nbm9zdGljLm9wZW5hcGkudjMuSGVhZGVyT3JSZWZlcmVuY2UiWAoUTmFtZWRMaW5rT3JSZWZlcmVuY2USDAoEbmFtZRgBIAEoCRIyCgV2YWx1ZRgCIAEoCzIjLmdub3N0aWMub3BlbmFwaS52My5MaW5rT3JSZWZlcmVuY2UiTAoOTmFtZWRNZWRpYVR5cGUSDAoEbmFtZRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLmdub3N0aWMub3BlbmFwaS52My5NZWRpYVR5cGUiYgoZTmFtZWRQYXJhbWV0ZXJPclJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEjcKBXZhbHVlGAIgASgLMiguZ25vc3RpYy5vcGVuYXBpLnYzLlBhcmFtZXRlck9yUmVmZXJlbmNlIkoKDU5hbWVkUGF0aEl0ZW0SDAoEbmFtZRgBIAEoCRIrCgV2YWx1ZRgCIAEoCzIcLmdub3N0aWMub3BlbmFwaS52My5QYXRoSXRlbSJmChtOYW1lZFJlcXVlc3RCb2R5T3JSZWZlcmVuY2USDAoEbmFtZRgBIAEoCRI5CgV2YWx1ZRgCIAEoCzIqLmdub3N0aWMub3BlbmFwaS52My5SZXF1ZXN0Qm9keU9yUmVmZXJlbmNlImAKGE5hbWVkUmVzcG9uc2VPclJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEjYKBXZhbHVlGAIgASgLMicuZ25vc3RpYy5vcGVuYXBpLnYzLlJlc3BvbnNlT3JSZWZlcmVuY2UiXAoWTmFtZWRTY2hlbWFPclJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEjQKBXZhbHVlGAIgASgLMiUuZ25vc3RpYy5vcGVuYXBpLnYzLlNjaGVtYU9yUmVmZXJlbmNlImwKHk5hbWVkU2VjdXJpdHlTY2hlbWVPclJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEjwKBXZhbHVlGAIgASgLMi0uZ25vc3RpYy5vcGVuYXBpLnYzLlNlY3VyaXR5U2NoZW1lT3JSZWZlcmVuY2UiVgoTTmFtZWRTZXJ2ZXJWYXJpYWJsZRIMCgRuYW1lGAEgASgJEjEKBXZhbHVlGAIgASgLMiIuZ25vc3RpYy5vcGVuYXBpLnYzLlNlcnZlclZhcmlhYmxlIioKC05hbWVkU3RyaW5nEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiUAoQTmFtZWRTdHJpbmdBcnJheRIMCgRuYW1lGAEgASgJEi4KBXZhbHVlGAIgASgLMh8uZ25vc3RpYy5vcGVuYXBpLnYzLlN0cmluZ0FycmF5IroBCglPYXV0aEZsb3cSGQoRYXV0aG9yaXphdGlvbl91cmwYASABKAkSEQoJdG9rZW5fdXJsGAIgASgJEhMKC3JlZnJlc2hfdXJsGAMgASgJEisKBnNjb3BlcxgEIAEoCzIbLmdub3N0aWMub3BlbmFwaS52My5TdHJpbmdzEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAUgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55IqMCCgpPYXV0aEZsb3dzEi8KCGltcGxpY2l0GAEgASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLk9hdXRoRmxvdxIvCghwYXNzd29yZBgCIAEoCzIdLmdub3N0aWMub3BlbmFwaS52My5PYXV0aEZsb3cSOQoSY2xpZW50X2NyZWRlbnRpYWxzGAMgASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLk9hdXRoRmxvdxI5ChJhdXRob3JpemF0aW9uX2NvZGUYBCABKAsyHS5nbm9zdGljLm9wZW5hcGkudjMuT2F1dGhGbG93Ej0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAUgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55IkUKBk9iamVjdBI7ChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyHC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRBbnkiuQQKCU9wZXJhdGlvbhIMCgR0YWdzGAEgAygJEg8KB3N1bW1hcnkYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSNwoNZXh0ZXJuYWxfZG9jcxgEIAEoCzIgLmdub3N0aWMub3BlbmFwaS52My5FeHRlcm5hbERvY3MSFAoMb3BlcmF0aW9uX2lkGAUgASgJEjwKCnBhcmFtZXRlcnMYBiADKAsyKC5nbm9zdGljLm9wZW5hcGkudjMuUGFyYW1ldGVyT3JSZWZlcmVuY2USQAoMcmVxdWVzdF9ib2R5GAcgASgLMiouZ25vc3RpYy5vcGVuYXBpLnYzLlJlcXVlc3RCb2R5T3JSZWZlcmVuY2USMAoJcmVzcG9uc2VzGAggASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLlJlc3BvbnNlcxI8CgljYWxsYmFja3MYCSABKAsyKS5nbm9zdGljLm9wZW5hcGkudjMuQ2FsbGJhY2tzT3JSZWZlcmVuY2VzEhIKCmRlcHJlY2F0ZWQYCiABKAgSOQoIc2VjdXJpdHkYCyADKAsyJy5nbm9zdGljLm9wZW5hcGkudjMuU2VjdXJpdHlSZXF1aXJlbWVudBIrCgdzZXJ2ZXJzGAwgAygLMhouZ25vc3RpYy5vcGVuYXBpLnYzLlNlcnZlchI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgNIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSLAAwoJUGFyYW1ldGVyEgwKBG5hbWUYASABKAkSCgoCaW4YAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSEAoIcmVxdWlyZWQYBCABKAgSEgoKZGVwcmVjYXRlZBgFIAEoCBIZChFhbGxvd19lbXB0eV92YWx1ZRgGIAEoCBINCgVzdHlsZRgHIAEoCRIPCgdleHBsb2RlGAggASgIEhYKDmFsbG93X3Jlc2VydmVkGAkgASgIEjUKBnNjaGVtYRgKIAEoCzIlLmdub3N0aWMub3BlbmFwaS52My5TY2hlbWFPclJlZmVyZW5jZRIoCgdleGFtcGxlGAsgASgLMhcuZ25vc3RpYy5vcGVuYXBpLnYzLkFueRI6CghleGFtcGxlcxgMIAEoCzIoLmdub3N0aWMub3BlbmFwaS52My5FeGFtcGxlc09yUmVmZXJlbmNlcxIvCgdjb250ZW50GA0gASgLMh4uZ25vc3RpYy5vcGVuYXBpLnYzLk1lZGlhVHlwZXMSPQoXc3BlY2lmaWNhdGlvbl9leHRlbnNpb24YDiADKAsyHC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRBbnkihwEKFFBhcmFtZXRlck9yUmVmZXJlbmNlEjIKCXBhcmFtZXRlchgBIAEoCzIdLmdub3N0aWMub3BlbmFwaS52My5QYXJhbWV0ZXJIABIyCglyZWZlcmVuY2UYAiABKAsyHS5nbm9zdGljLm9wZW5hcGkudjMuUmVmZXJlbmNlSABCBwoFb25lb2YiZgoWUGFyYW1ldGVyc09yUmVmZXJlbmNlcxJMChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyLS5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRQYXJhbWV0ZXJPclJlZmVyZW5jZSLVBAoIUGF0aEl0ZW0SDAoEX3JlZhgBIAEoCRIPCgdzdW1tYXJ5GAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEioKA2dldBgEIAEoCzIdLmdub3N0aWMub3BlbmFwaS52My5PcGVyYXRpb24SKgoDcHV0GAUgASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLk9wZXJhdGlvbhIrCgRwb3N0GAYgASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLk9wZXJhdGlvbhItCgZkZWxldGUYByABKAsyHS5nbm9zdGljLm9wZW5hcGkudjMuT3BlcmF0aW9uEi4KB29wdGlvbnMYCCABKAsyHS5nbm9zdGljLm9wZW5hcGkudjMuT3BlcmF0aW9uEisKBGhlYWQYCSABKAsyHS5nbm9zdGljLm9wZW5hcGkudjMuT3BlcmF0aW9uEiwKBXBhdGNoGAogASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLk9wZXJhdGlvbhIsCgV0cmFjZRgLIAEoCzIdLmdub3N0aWMub3BlbmFwaS52My5PcGVyYXRpb24SKwoHc2VydmVycxgMIAMoCzIaLmdub3N0aWMub3BlbmFwaS52My5TZXJ2ZXISPAoKcGFyYW1ldGVycxgNIAMoCzIoLmdub3N0aWMub3BlbmFwaS52My5QYXJhbWV0ZXJPclJlZmVyZW5jZRI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgOIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSJ3CgVQYXRocxIvCgRwYXRoGAEgAygLMiEuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkUGF0aEl0ZW0SPQoXc3BlY2lmaWNhdGlvbl9leHRlbnNpb24YAiADKAsyHC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRBbnkiVwoKUHJvcGVydGllcxJJChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyKi5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRTY2hlbWFPclJlZmVyZW5jZSI/CglSZWZlcmVuY2USDAoEX3JlZhgBIAEoCRIPCgdzdW1tYXJ5GAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJImsKGVJlcXVlc3RCb2RpZXNPclJlZmVyZW5jZXMSTgoVYWRkaXRpb25hbF9wcm9wZXJ0aWVzGAEgAygLMi8uZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkUmVxdWVzdEJvZHlPclJlZmVyZW5jZSKkAQoLUmVxdWVzdEJvZHkSEwoLZGVzY3JpcHRpb24YASABKAkSLwoHY29udGVudBgCIAEoCzIeLmdub3N0aWMub3BlbmFwaS52My5NZWRpYVR5cGVzEhAKCHJlcXVpcmVkGAMgASgIEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAQgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55Io4BChZSZXF1ZXN0Qm9keU9yUmVmZXJlbmNlEjcKDHJlcXVlc3RfYm9keRgBIAEoCzIfLmdub3N0aWMub3BlbmFwaS52My5SZXF1ZXN0Qm9keUgAEjIKCXJlZmVyZW5jZRgCIAEoCzIdLmdub3N0aWMub3BlbmFwaS52My5SZWZlcmVuY2VIAEIHCgVvbmVvZiL/AQoIUmVzcG9uc2USEwoLZGVzY3JpcHRpb24YASABKAkSOAoHaGVhZGVycxgCIAEoCzInLmdub3N0aWMub3BlbmFwaS52My5IZWFkZXJzT3JSZWZlcmVuY2VzEi8KB2NvbnRlbnQYAyABKAsyHi5nbm9zdGljLm9wZW5hcGkudjMuTWVkaWFUeXBlcxI0CgVsaW5rcxgEIAEoCzIlLmdub3N0aWMub3BlbmFwaS52My5MaW5rc09yUmVmZXJlbmNlcxI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgFIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSKEAQoTUmVzcG9uc2VPclJlZmVyZW5jZRIwCghyZXNwb25zZRgBIAEoCzIcLmdub3N0aWMub3BlbmFwaS52My5SZXNwb25zZUgAEjIKCXJlZmVyZW5jZRgCIAEoCzIdLmdub3N0aWMub3BlbmFwaS52My5SZWZlcmVuY2VIAEIHCgVvbmVvZiLRAQoJUmVzcG9uc2VzEjgKB2RlZmF1bHQYASABKAsyJy5nbm9zdGljLm9wZW5hcGkudjMuUmVzcG9uc2VPclJlZmVyZW5jZRJLChVyZXNwb25zZV9vcl9yZWZlcmVuY2UYAiADKAsyLC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRSZXNwb25zZU9yUmVmZXJlbmNlEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAMgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55ImQKFVJlc3BvbnNlc09yUmVmZXJlbmNlcxJLChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyLC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRSZXNwb25zZU9yUmVmZXJlbmNlIpMJCgZTY2hlbWESEAoIbnVsbGFibGUYASABKAgSOAoNZGlzY3JpbWluYXRvchgCIAEoCzIhLmdub3N0aWMub3BlbmFwaS52My5EaXNjcmltaW5hdG9yEhEKCXJlYWRfb25seRgDIAEoCBISCgp3cml0ZV9vbmx5GAQgASgIEiQKA3htbBgFIAEoCzIXLmdub3N0aWMub3BlbmFwaS52My5YbWwSNwoNZXh0ZXJuYWxfZG9jcxgGIAEoCzIgLmdub3N0aWMub3BlbmFwaS52My5FeHRlcm5hbERvY3MSKAoHZXhhbXBsZRgHIAEoCzIXLmdub3N0aWMub3BlbmFwaS52My5BbnkSEgoKZGVwcmVjYXRlZBgIIAEoCBINCgV0aXRsZRgJIAEoCRITCgttdWx0aXBsZV9vZhgKIAEoARIPCgdtYXhpbXVtGAsgASgBEhkKEWV4Y2x1c2l2ZV9tYXhpbXVtGAwgASgIEg8KB21pbmltdW0YDSABKAESGQoRZXhjbHVzaXZlX21pbmltdW0YDiABKAgSEgoKbWF4X2xlbmd0aBgPIAEoAxISCgptaW5fbGVuZ3RoGBAgASgDEg8KB3BhdHRlcm4YESABKAkSEQoJbWF4X2l0ZW1zGBIgASgDEhEKCW1pbl9pdGVtcxgTIAEoAxIUCgx1bmlxdWVfaXRlbXMYFCABKAgSFgoObWF4X3Byb3BlcnRpZXMYFSABKAMSFgoObWluX3Byb3BlcnRpZXMYFiABKAMSEAoIcmVxdWlyZWQYFyADKAkSJQoEZW51bRgYIAMoCzIXLmdub3N0aWMub3BlbmFwaS52My5BbnkSDAoEdHlwZRgZIAEoCRI1CgZhbGxfb2YYGiADKAsyJS5nbm9zdGljLm9wZW5hcGkudjMuU2NoZW1hT3JSZWZlcmVuY2USNQoGb25lX29mGBsgAygLMiUuZ25vc3RpYy5vcGVuYXBpLnYzLlNjaGVtYU9yUmVmZXJlbmNlEjUKBmFueV9vZhgcIAMoCzIlLmdub3N0aWMub3BlbmFwaS52My5TY2hlbWFPclJlZmVyZW5jZRInCgNub3QYHSABKAsyGi5nbm9zdGljLm9wZW5hcGkudjMuU2NoZW1hEiwKBWl0ZW1zGB4gASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLkl0ZW1zSXRlbRIyCgpwcm9wZXJ0aWVzGB8gASgLMh4uZ25vc3RpYy5vcGVuYXBpLnYzLlByb3BlcnRpZXMSSwoVYWRkaXRpb25hbF9wcm9wZXJ0aWVzGCAgASgLMiwuZ25vc3RpYy5vcGVuYXBpLnYzLkFkZGl0aW9uYWxQcm9wZXJ0aWVzSXRlbRIwCgdkZWZhdWx0GCEgASgLMh8uZ25vc3RpYy5vcGVuYXBpLnYzLkRlZmF1bHRUeXBlEhMKC2Rlc2NyaXB0aW9uGCIgASgJEg4KBmZvcm1hdBgjIAEoCRI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgkIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSJ+ChFTY2hlbWFPclJlZmVyZW5jZRIsCgZzY2hlbWEYASABKAsyGi5nbm9zdGljLm9wZW5hcGkudjMuU2NoZW1hSAASMgoJcmVmZXJlbmNlGAIgASgLMh0uZ25vc3RpYy5vcGVuYXBpLnYzLlJlZmVyZW5jZUgAQgcKBW9uZW9mImAKE1NjaGVtYXNPclJlZmVyZW5jZXMSSQoVYWRkaXRpb25hbF9wcm9wZXJ0aWVzGAEgAygLMiouZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkU2NoZW1hT3JSZWZlcmVuY2UiWgoTU2VjdXJpdHlSZXF1aXJlbWVudBJDChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyJC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRTdHJpbmdBcnJheSL/AQoOU2VjdXJpdHlTY2hlbWUSDAoEdHlwZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIMCgRuYW1lGAMgASgJEgoKAmluGAQgASgJEg4KBnNjaGVtZRgFIAEoCRIVCg1iZWFyZXJfZm9ybWF0GAYgASgJEi0KBWZsb3dzGAcgASgLMh4uZ25vc3RpYy5vcGVuYXBpLnYzLk9hdXRoRmxvd3MSGwoTb3Blbl9pZF9jb25uZWN0X3VybBgIIAEoCRI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgJIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSKXAQoZU2VjdXJpdHlTY2hlbWVPclJlZmVyZW5jZRI9Cg9zZWN1cml0eV9zY2hlbWUYASABKAsyIi5nbm9zdGljLm9wZW5hcGkudjMuU2VjdXJpdHlTY2hlbWVIABIyCglyZWZlcmVuY2UYAiABKAsyHS5nbm9zdGljLm9wZW5hcGkudjMuUmVmZXJlbmNlSABCBwoFb25lb2YicAobU2VjdXJpdHlTY2hlbWVzT3JSZWZlcmVuY2VzElEKFWFkZGl0aW9uYWxfcHJvcGVydGllcxgBIAMoCzIyLmdub3N0aWMub3BlbmFwaS52My5OYW1lZFNlY3VyaXR5U2NoZW1lT3JSZWZlcmVuY2UioQEKBlNlcnZlchILCgN1cmwYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSNgoJdmFyaWFibGVzGAMgASgLMiMuZ25vc3RpYy5vcGVuYXBpLnYzLlNlcnZlclZhcmlhYmxlcxI9ChdzcGVjaWZpY2F0aW9uX2V4dGVuc2lvbhgEIAMoCzIcLmdub3N0aWMub3BlbmFwaS52My5OYW1lZEFueSKDAQoOU2VydmVyVmFyaWFibGUSDAoEZW51bRgBIAMoCRIPCgdkZWZhdWx0GAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEj0KF3NwZWNpZmljYXRpb25fZXh0ZW5zaW9uGAQgAygLMhwuZ25vc3RpYy5vcGVuYXBpLnYzLk5hbWVkQW55IlkKD1NlcnZlclZhcmlhYmxlcxJGChVhZGRpdGlvbmFsX3Byb3BlcnRpZXMYASADKAsyJy5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRTZXJ2ZXJWYXJpYWJsZSJYChZTcGVjaWZpY2F0aW9uRXh0ZW5zaW9uEhAKBm51bWJlchgBIAEoAUgAEhEKB2Jvb2xlYW4YAiABKAhIABIQCgZzdHJpbmcYAyABKAlIAEIHCgVvbmVvZiIcCgtTdHJpbmdBcnJheRINCgV2YWx1ZRgBIAMoCSJJCgdTdHJpbmdzEj4KFWFkZGl0aW9uYWxfcHJvcGVydGllcxgBIAMoCzIfLmdub3N0aWMub3BlbmFwaS52My5OYW1lZFN0cmluZyKgAQoDVGFnEgwKBG5hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSNwoNZXh0ZXJuYWxfZG9jcxgDIAEoCzIgLmdub3N0aWMub3BlbmFwaS52My5FeHRlcm5hbERvY3MSPQoXc3BlY2lmaWNhdGlvbl9leHRlbnNpb24YBCADKAsyHC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRBbnkimQEKA1htbBIMCgRuYW1lGAEgASgJEhEKCW5hbWVzcGFjZRgCIAEoCRIOCgZwcmVmaXgYAyABKAkSEQoJYXR0cmlidXRlGAQgASgIEg8KB3dyYXBwZWQYBSABKAgSPQoXc3BlY2lmaWNhdGlvbl9leHRlbnNpb24YBiADKAsyHC5nbm9zdGljLm9wZW5hcGkudjMuTmFtZWRBbnlCVgoOb3JnLm9wZW5hcGlfdjNCDE9wZW5BUElQcm90b1ABWi5naXRodWIuY29tL2dvb2dsZS9nbm9zdGljL29wZW5hcGl2MztvcGVuYXBpX3YzogIDT0FTYgZwcm90bzM\", [file_google_protobuf_any]);\n\n/**\n * @generated from message gnostic.openapi.v3.AdditionalPropertiesItem\n */\nexport type AdditionalPropertiesItem = Message<\"gnostic.openapi.v3.AdditionalPropertiesItem\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.AdditionalPropertiesItem.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.SchemaOrReference schema_or_reference = 1;\n     */\n    value: SchemaOrReference;\n    case: \"schemaOrReference\";\n  } | {\n    /**\n     * @generated from field: bool boolean = 2;\n     */\n    value: boolean;\n    case: \"boolean\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.AdditionalPropertiesItem.\n * Use `create(AdditionalPropertiesItemSchema)` to create a new message.\n */\nexport const AdditionalPropertiesItemSchema: GenMessage<AdditionalPropertiesItem> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 0);\n\n/**\n * @generated from message gnostic.openapi.v3.Any\n */\nexport type Any = Message<\"gnostic.openapi.v3.Any\"> & {\n  /**\n   * @generated from field: google.protobuf.Any value = 1;\n   */\n  value?: Any$1;\n\n  /**\n   * @generated from field: string yaml = 2;\n   */\n  yaml: string;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Any.\n * Use `create(AnySchema)` to create a new message.\n */\nexport const AnySchema: GenMessage<Any> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 1);\n\n/**\n * @generated from message gnostic.openapi.v3.AnyOrExpression\n */\nexport type AnyOrExpression = Message<\"gnostic.openapi.v3.AnyOrExpression\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.AnyOrExpression.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.Any any = 1;\n     */\n    value: Any;\n    case: \"any\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Expression expression = 2;\n     */\n    value: Expression;\n    case: \"expression\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.AnyOrExpression.\n * Use `create(AnyOrExpressionSchema)` to create a new message.\n */\nexport const AnyOrExpressionSchema: GenMessage<AnyOrExpression> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 2);\n\n/**\n * A map of possible out-of band callbacks related to the parent operation. Each value in the map is a Path Item Object that describes a set of requests that may be initiated by the API provider and the expected responses. The key value used to identify the callback object is an expression, evaluated at runtime, that identifies a URL to use for the callback operation.\n *\n * @generated from message gnostic.openapi.v3.Callback\n */\nexport type Callback = Message<\"gnostic.openapi.v3.Callback\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedPathItem path = 1;\n   */\n  path: NamedPathItem[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 2;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Callback.\n * Use `create(CallbackSchema)` to create a new message.\n */\nexport const CallbackSchema: GenMessage<Callback> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 3);\n\n/**\n * @generated from message gnostic.openapi.v3.CallbackOrReference\n */\nexport type CallbackOrReference = Message<\"gnostic.openapi.v3.CallbackOrReference\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.CallbackOrReference.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.Callback callback = 1;\n     */\n    value: Callback;\n    case: \"callback\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Reference reference = 2;\n     */\n    value: Reference;\n    case: \"reference\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.CallbackOrReference.\n * Use `create(CallbackOrReferenceSchema)` to create a new message.\n */\nexport const CallbackOrReferenceSchema: GenMessage<CallbackOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 4);\n\n/**\n * @generated from message gnostic.openapi.v3.CallbacksOrReferences\n */\nexport type CallbacksOrReferences = Message<\"gnostic.openapi.v3.CallbacksOrReferences\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedCallbackOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedCallbackOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.CallbacksOrReferences.\n * Use `create(CallbacksOrReferencesSchema)` to create a new message.\n */\nexport const CallbacksOrReferencesSchema: GenMessage<CallbacksOrReferences> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 5);\n\n/**\n * Holds a set of reusable objects for different aspects of the OAS. All objects defined within the components object will have no effect on the API unless they are explicitly referenced from properties outside the components object.\n *\n * @generated from message gnostic.openapi.v3.Components\n */\nexport type Components = Message<\"gnostic.openapi.v3.Components\"> & {\n  /**\n   * @generated from field: gnostic.openapi.v3.SchemasOrReferences schemas = 1;\n   */\n  schemas?: SchemasOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ResponsesOrReferences responses = 2;\n   */\n  responses?: ResponsesOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ParametersOrReferences parameters = 3;\n   */\n  parameters?: ParametersOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ExamplesOrReferences examples = 4;\n   */\n  examples?: ExamplesOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.RequestBodiesOrReferences request_bodies = 5;\n   */\n  requestBodies?: RequestBodiesOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.HeadersOrReferences headers = 6;\n   */\n  headers?: HeadersOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.SecuritySchemesOrReferences security_schemes = 7;\n   */\n  securitySchemes?: SecuritySchemesOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.LinksOrReferences links = 8;\n   */\n  links?: LinksOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.CallbacksOrReferences callbacks = 9;\n   */\n  callbacks?: CallbacksOrReferences;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 10;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Components.\n * Use `create(ComponentsSchema)` to create a new message.\n */\nexport const ComponentsSchema: GenMessage<Components> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 6);\n\n/**\n * Contact information for the exposed API.\n *\n * @generated from message gnostic.openapi.v3.Contact\n */\nexport type Contact = Message<\"gnostic.openapi.v3.Contact\"> & {\n  /**\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * @generated from field: string url = 2;\n   */\n  url: string;\n\n  /**\n   * @generated from field: string email = 3;\n   */\n  email: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 4;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Contact.\n * Use `create(ContactSchema)` to create a new message.\n */\nexport const ContactSchema: GenMessage<Contact> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 7);\n\n/**\n * @generated from message gnostic.openapi.v3.DefaultType\n */\nexport type DefaultType = Message<\"gnostic.openapi.v3.DefaultType\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.DefaultType.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: double number = 1;\n     */\n    value: number;\n    case: \"number\";\n  } | {\n    /**\n     * @generated from field: bool boolean = 2;\n     */\n    value: boolean;\n    case: \"boolean\";\n  } | {\n    /**\n     * @generated from field: string string = 3;\n     */\n    value: string;\n    case: \"string\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.DefaultType.\n * Use `create(DefaultTypeSchema)` to create a new message.\n */\nexport const DefaultTypeSchema: GenMessage<DefaultType> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 8);\n\n/**\n * When request bodies or response payloads may be one of a number of different schemas, a `discriminator` object can be used to aid in serialization, deserialization, and validation.  The discriminator is a specific object in a schema which is used to inform the consumer of the specification of an alternative schema based on the value associated with it.  When using the discriminator, _inline_ schemas will not be considered.\n *\n * @generated from message gnostic.openapi.v3.Discriminator\n */\nexport type Discriminator = Message<\"gnostic.openapi.v3.Discriminator\"> & {\n  /**\n   * @generated from field: string property_name = 1;\n   */\n  propertyName: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Strings mapping = 2;\n   */\n  mapping?: Strings;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 3;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Discriminator.\n * Use `create(DiscriminatorSchema)` to create a new message.\n */\nexport const DiscriminatorSchema: GenMessage<Discriminator> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 9);\n\n/**\n * @generated from message gnostic.openapi.v3.Document\n */\nexport type Document = Message<\"gnostic.openapi.v3.Document\"> & {\n  /**\n   * @generated from field: string openapi = 1;\n   */\n  openapi: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Info info = 2;\n   */\n  info?: Info;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.Server servers = 3;\n   */\n  servers: Server[];\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Paths paths = 4;\n   */\n  paths?: Paths;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Components components = 5;\n   */\n  components?: Components;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.SecurityRequirement security = 6;\n   */\n  security: SecurityRequirement[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.Tag tags = 7;\n   */\n  tags: Tag[];\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ExternalDocs external_docs = 8;\n   */\n  externalDocs?: ExternalDocs;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 9;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Document.\n * Use `create(DocumentSchema)` to create a new message.\n */\nexport const DocumentSchema: GenMessage<Document> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 10);\n\n/**\n * A single encoding definition applied to a single schema property.\n *\n * @generated from message gnostic.openapi.v3.Encoding\n */\nexport type Encoding = Message<\"gnostic.openapi.v3.Encoding\"> & {\n  /**\n   * @generated from field: string content_type = 1;\n   */\n  contentType: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.HeadersOrReferences headers = 2;\n   */\n  headers?: HeadersOrReferences;\n\n  /**\n   * @generated from field: string style = 3;\n   */\n  style: string;\n\n  /**\n   * @generated from field: bool explode = 4;\n   */\n  explode: boolean;\n\n  /**\n   * @generated from field: bool allow_reserved = 5;\n   */\n  allowReserved: boolean;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 6;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Encoding.\n * Use `create(EncodingSchema)` to create a new message.\n */\nexport const EncodingSchema: GenMessage<Encoding> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 11);\n\n/**\n * @generated from message gnostic.openapi.v3.Encodings\n */\nexport type Encodings = Message<\"gnostic.openapi.v3.Encodings\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedEncoding additional_properties = 1;\n   */\n  additionalProperties: NamedEncoding[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Encodings.\n * Use `create(EncodingsSchema)` to create a new message.\n */\nexport const EncodingsSchema: GenMessage<Encodings> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 12);\n\n/**\n * @generated from message gnostic.openapi.v3.Example\n */\nexport type Example = Message<\"gnostic.openapi.v3.Example\"> & {\n  /**\n   * @generated from field: string summary = 1;\n   */\n  summary: string;\n\n  /**\n   * @generated from field: string description = 2;\n   */\n  description: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Any value = 3;\n   */\n  value?: Any;\n\n  /**\n   * @generated from field: string external_value = 4;\n   */\n  externalValue: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 5;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Example.\n * Use `create(ExampleSchema)` to create a new message.\n */\nexport const ExampleSchema: GenMessage<Example> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 13);\n\n/**\n * @generated from message gnostic.openapi.v3.ExampleOrReference\n */\nexport type ExampleOrReference = Message<\"gnostic.openapi.v3.ExampleOrReference\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.ExampleOrReference.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.Example example = 1;\n     */\n    value: Example;\n    case: \"example\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Reference reference = 2;\n     */\n    value: Reference;\n    case: \"reference\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ExampleOrReference.\n * Use `create(ExampleOrReferenceSchema)` to create a new message.\n */\nexport const ExampleOrReferenceSchema: GenMessage<ExampleOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 14);\n\n/**\n * @generated from message gnostic.openapi.v3.ExamplesOrReferences\n */\nexport type ExamplesOrReferences = Message<\"gnostic.openapi.v3.ExamplesOrReferences\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedExampleOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedExampleOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ExamplesOrReferences.\n * Use `create(ExamplesOrReferencesSchema)` to create a new message.\n */\nexport const ExamplesOrReferencesSchema: GenMessage<ExamplesOrReferences> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 15);\n\n/**\n * @generated from message gnostic.openapi.v3.Expression\n */\nexport type Expression = Message<\"gnostic.openapi.v3.Expression\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny additional_properties = 1;\n   */\n  additionalProperties: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Expression.\n * Use `create(ExpressionSchema)` to create a new message.\n */\nexport const ExpressionSchema: GenMessage<Expression> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 16);\n\n/**\n * Allows referencing an external resource for extended documentation.\n *\n * @generated from message gnostic.openapi.v3.ExternalDocs\n */\nexport type ExternalDocs = Message<\"gnostic.openapi.v3.ExternalDocs\"> & {\n  /**\n   * @generated from field: string description = 1;\n   */\n  description: string;\n\n  /**\n   * @generated from field: string url = 2;\n   */\n  url: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 3;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ExternalDocs.\n * Use `create(ExternalDocsSchema)` to create a new message.\n */\nexport const ExternalDocsSchema: GenMessage<ExternalDocs> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 17);\n\n/**\n * The Header Object follows the structure of the Parameter Object with the following changes:  1. `name` MUST NOT be specified, it is given in the corresponding `headers` map. 1. `in` MUST NOT be specified, it is implicitly in `header`. 1. All traits that are affected by the location MUST be applicable to a location of `header` (for example, `style`).\n *\n * @generated from message gnostic.openapi.v3.Header\n */\nexport type Header = Message<\"gnostic.openapi.v3.Header\"> & {\n  /**\n   * @generated from field: string description = 1;\n   */\n  description: string;\n\n  /**\n   * @generated from field: bool required = 2;\n   */\n  required: boolean;\n\n  /**\n   * @generated from field: bool deprecated = 3;\n   */\n  deprecated: boolean;\n\n  /**\n   * @generated from field: bool allow_empty_value = 4;\n   */\n  allowEmptyValue: boolean;\n\n  /**\n   * @generated from field: string style = 5;\n   */\n  style: string;\n\n  /**\n   * @generated from field: bool explode = 6;\n   */\n  explode: boolean;\n\n  /**\n   * @generated from field: bool allow_reserved = 7;\n   */\n  allowReserved: boolean;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.SchemaOrReference schema = 8;\n   */\n  schema?: SchemaOrReference;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Any example = 9;\n   */\n  example?: Any;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ExamplesOrReferences examples = 10;\n   */\n  examples?: ExamplesOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.MediaTypes content = 11;\n   */\n  content?: MediaTypes;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 12;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Header.\n * Use `create(HeaderSchema)` to create a new message.\n */\nexport const HeaderSchema: GenMessage<Header> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 18);\n\n/**\n * @generated from message gnostic.openapi.v3.HeaderOrReference\n */\nexport type HeaderOrReference = Message<\"gnostic.openapi.v3.HeaderOrReference\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.HeaderOrReference.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.Header header = 1;\n     */\n    value: Header;\n    case: \"header\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Reference reference = 2;\n     */\n    value: Reference;\n    case: \"reference\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.HeaderOrReference.\n * Use `create(HeaderOrReferenceSchema)` to create a new message.\n */\nexport const HeaderOrReferenceSchema: GenMessage<HeaderOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 19);\n\n/**\n * @generated from message gnostic.openapi.v3.HeadersOrReferences\n */\nexport type HeadersOrReferences = Message<\"gnostic.openapi.v3.HeadersOrReferences\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedHeaderOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedHeaderOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.HeadersOrReferences.\n * Use `create(HeadersOrReferencesSchema)` to create a new message.\n */\nexport const HeadersOrReferencesSchema: GenMessage<HeadersOrReferences> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 20);\n\n/**\n * The object provides metadata about the API. The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience.\n *\n * @generated from message gnostic.openapi.v3.Info\n */\nexport type Info = Message<\"gnostic.openapi.v3.Info\"> & {\n  /**\n   * @generated from field: string title = 1;\n   */\n  title: string;\n\n  /**\n   * @generated from field: string description = 2;\n   */\n  description: string;\n\n  /**\n   * @generated from field: string terms_of_service = 3;\n   */\n  termsOfService: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Contact contact = 4;\n   */\n  contact?: Contact;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.License license = 5;\n   */\n  license?: License;\n\n  /**\n   * @generated from field: string version = 6;\n   */\n  version: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 7;\n   */\n  specificationExtension: NamedAny[];\n\n  /**\n   * @generated from field: string summary = 8;\n   */\n  summary: string;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Info.\n * Use `create(InfoSchema)` to create a new message.\n */\nexport const InfoSchema: GenMessage<Info> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 21);\n\n/**\n * @generated from message gnostic.openapi.v3.ItemsItem\n */\nexport type ItemsItem = Message<\"gnostic.openapi.v3.ItemsItem\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.SchemaOrReference schema_or_reference = 1;\n   */\n  schemaOrReference: SchemaOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ItemsItem.\n * Use `create(ItemsItemSchema)` to create a new message.\n */\nexport const ItemsItemSchema: GenMessage<ItemsItem> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 22);\n\n/**\n * License information for the exposed API.\n *\n * @generated from message gnostic.openapi.v3.License\n */\nexport type License = Message<\"gnostic.openapi.v3.License\"> & {\n  /**\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * @generated from field: string url = 2;\n   */\n  url: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 3;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.License.\n * Use `create(LicenseSchema)` to create a new message.\n */\nexport const LicenseSchema: GenMessage<License> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 23);\n\n/**\n * The `Link object` represents a possible design-time link for a response. The presence of a link does not guarantee the caller's ability to successfully invoke it, rather it provides a known relationship and traversal mechanism between responses and other operations.  Unlike _dynamic_ links (i.e. links provided **in** the response payload), the OAS linking mechanism does not require link information in the runtime response.  For computing links, and providing instructions to execute them, a runtime expression is used for accessing values in an operation and using them as parameters while invoking the linked operation.\n *\n * @generated from message gnostic.openapi.v3.Link\n */\nexport type Link = Message<\"gnostic.openapi.v3.Link\"> & {\n  /**\n   * @generated from field: string operation_ref = 1;\n   */\n  operationRef: string;\n\n  /**\n   * @generated from field: string operation_id = 2;\n   */\n  operationId: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.AnyOrExpression parameters = 3;\n   */\n  parameters?: AnyOrExpression;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.AnyOrExpression request_body = 4;\n   */\n  requestBody?: AnyOrExpression;\n\n  /**\n   * @generated from field: string description = 5;\n   */\n  description: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Server server = 6;\n   */\n  server?: Server;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 7;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Link.\n * Use `create(LinkSchema)` to create a new message.\n */\nexport const LinkSchema: GenMessage<Link> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 24);\n\n/**\n * @generated from message gnostic.openapi.v3.LinkOrReference\n */\nexport type LinkOrReference = Message<\"gnostic.openapi.v3.LinkOrReference\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.LinkOrReference.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.Link link = 1;\n     */\n    value: Link;\n    case: \"link\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Reference reference = 2;\n     */\n    value: Reference;\n    case: \"reference\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.LinkOrReference.\n * Use `create(LinkOrReferenceSchema)` to create a new message.\n */\nexport const LinkOrReferenceSchema: GenMessage<LinkOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 25);\n\n/**\n * @generated from message gnostic.openapi.v3.LinksOrReferences\n */\nexport type LinksOrReferences = Message<\"gnostic.openapi.v3.LinksOrReferences\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedLinkOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedLinkOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.LinksOrReferences.\n * Use `create(LinksOrReferencesSchema)` to create a new message.\n */\nexport const LinksOrReferencesSchema: GenMessage<LinksOrReferences> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 26);\n\n/**\n * Each Media Type Object provides schema and examples for the media type identified by its key.\n *\n * @generated from message gnostic.openapi.v3.MediaType\n */\nexport type MediaType = Message<\"gnostic.openapi.v3.MediaType\"> & {\n  /**\n   * @generated from field: gnostic.openapi.v3.SchemaOrReference schema = 1;\n   */\n  schema?: SchemaOrReference;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Any example = 2;\n   */\n  example?: Any;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ExamplesOrReferences examples = 3;\n   */\n  examples?: ExamplesOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Encodings encoding = 4;\n   */\n  encoding?: Encodings;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 5;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.MediaType.\n * Use `create(MediaTypeSchema)` to create a new message.\n */\nexport const MediaTypeSchema: GenMessage<MediaType> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 27);\n\n/**\n * @generated from message gnostic.openapi.v3.MediaTypes\n */\nexport type MediaTypes = Message<\"gnostic.openapi.v3.MediaTypes\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedMediaType additional_properties = 1;\n   */\n  additionalProperties: NamedMediaType[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.MediaTypes.\n * Use `create(MediaTypesSchema)` to create a new message.\n */\nexport const MediaTypesSchema: GenMessage<MediaTypes> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 28);\n\n/**\n * Automatically-generated message used to represent maps of Any as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedAny\n */\nexport type NamedAny = Message<\"gnostic.openapi.v3.NamedAny\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.Any value = 2;\n   */\n  value?: Any;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedAny.\n * Use `create(NamedAnySchema)` to create a new message.\n */\nexport const NamedAnySchema: GenMessage<NamedAny> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 29);\n\n/**\n * Automatically-generated message used to represent maps of CallbackOrReference as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedCallbackOrReference\n */\nexport type NamedCallbackOrReference = Message<\"gnostic.openapi.v3.NamedCallbackOrReference\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.CallbackOrReference value = 2;\n   */\n  value?: CallbackOrReference;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedCallbackOrReference.\n * Use `create(NamedCallbackOrReferenceSchema)` to create a new message.\n */\nexport const NamedCallbackOrReferenceSchema: GenMessage<NamedCallbackOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 30);\n\n/**\n * Automatically-generated message used to represent maps of Encoding as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedEncoding\n */\nexport type NamedEncoding = Message<\"gnostic.openapi.v3.NamedEncoding\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.Encoding value = 2;\n   */\n  value?: Encoding;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedEncoding.\n * Use `create(NamedEncodingSchema)` to create a new message.\n */\nexport const NamedEncodingSchema: GenMessage<NamedEncoding> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 31);\n\n/**\n * Automatically-generated message used to represent maps of ExampleOrReference as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedExampleOrReference\n */\nexport type NamedExampleOrReference = Message<\"gnostic.openapi.v3.NamedExampleOrReference\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.ExampleOrReference value = 2;\n   */\n  value?: ExampleOrReference;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedExampleOrReference.\n * Use `create(NamedExampleOrReferenceSchema)` to create a new message.\n */\nexport const NamedExampleOrReferenceSchema: GenMessage<NamedExampleOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 32);\n\n/**\n * Automatically-generated message used to represent maps of HeaderOrReference as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedHeaderOrReference\n */\nexport type NamedHeaderOrReference = Message<\"gnostic.openapi.v3.NamedHeaderOrReference\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.HeaderOrReference value = 2;\n   */\n  value?: HeaderOrReference;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedHeaderOrReference.\n * Use `create(NamedHeaderOrReferenceSchema)` to create a new message.\n */\nexport const NamedHeaderOrReferenceSchema: GenMessage<NamedHeaderOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 33);\n\n/**\n * Automatically-generated message used to represent maps of LinkOrReference as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedLinkOrReference\n */\nexport type NamedLinkOrReference = Message<\"gnostic.openapi.v3.NamedLinkOrReference\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.LinkOrReference value = 2;\n   */\n  value?: LinkOrReference;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedLinkOrReference.\n * Use `create(NamedLinkOrReferenceSchema)` to create a new message.\n */\nexport const NamedLinkOrReferenceSchema: GenMessage<NamedLinkOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 34);\n\n/**\n * Automatically-generated message used to represent maps of MediaType as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedMediaType\n */\nexport type NamedMediaType = Message<\"gnostic.openapi.v3.NamedMediaType\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.MediaType value = 2;\n   */\n  value?: MediaType;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedMediaType.\n * Use `create(NamedMediaTypeSchema)` to create a new message.\n */\nexport const NamedMediaTypeSchema: GenMessage<NamedMediaType> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 35);\n\n/**\n * Automatically-generated message used to represent maps of ParameterOrReference as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedParameterOrReference\n */\nexport type NamedParameterOrReference = Message<\"gnostic.openapi.v3.NamedParameterOrReference\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.ParameterOrReference value = 2;\n   */\n  value?: ParameterOrReference;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedParameterOrReference.\n * Use `create(NamedParameterOrReferenceSchema)` to create a new message.\n */\nexport const NamedParameterOrReferenceSchema: GenMessage<NamedParameterOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 36);\n\n/**\n * Automatically-generated message used to represent maps of PathItem as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedPathItem\n */\nexport type NamedPathItem = Message<\"gnostic.openapi.v3.NamedPathItem\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.PathItem value = 2;\n   */\n  value?: PathItem;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedPathItem.\n * Use `create(NamedPathItemSchema)` to create a new message.\n */\nexport const NamedPathItemSchema: GenMessage<NamedPathItem> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 37);\n\n/**\n * Automatically-generated message used to represent maps of RequestBodyOrReference as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedRequestBodyOrReference\n */\nexport type NamedRequestBodyOrReference = Message<\"gnostic.openapi.v3.NamedRequestBodyOrReference\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.RequestBodyOrReference value = 2;\n   */\n  value?: RequestBodyOrReference;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedRequestBodyOrReference.\n * Use `create(NamedRequestBodyOrReferenceSchema)` to create a new message.\n */\nexport const NamedRequestBodyOrReferenceSchema: GenMessage<NamedRequestBodyOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 38);\n\n/**\n * Automatically-generated message used to represent maps of ResponseOrReference as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedResponseOrReference\n */\nexport type NamedResponseOrReference = Message<\"gnostic.openapi.v3.NamedResponseOrReference\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.ResponseOrReference value = 2;\n   */\n  value?: ResponseOrReference;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedResponseOrReference.\n * Use `create(NamedResponseOrReferenceSchema)` to create a new message.\n */\nexport const NamedResponseOrReferenceSchema: GenMessage<NamedResponseOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 39);\n\n/**\n * Automatically-generated message used to represent maps of SchemaOrReference as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedSchemaOrReference\n */\nexport type NamedSchemaOrReference = Message<\"gnostic.openapi.v3.NamedSchemaOrReference\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.SchemaOrReference value = 2;\n   */\n  value?: SchemaOrReference;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedSchemaOrReference.\n * Use `create(NamedSchemaOrReferenceSchema)` to create a new message.\n */\nexport const NamedSchemaOrReferenceSchema: GenMessage<NamedSchemaOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 40);\n\n/**\n * Automatically-generated message used to represent maps of SecuritySchemeOrReference as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedSecuritySchemeOrReference\n */\nexport type NamedSecuritySchemeOrReference = Message<\"gnostic.openapi.v3.NamedSecuritySchemeOrReference\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.SecuritySchemeOrReference value = 2;\n   */\n  value?: SecuritySchemeOrReference;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedSecuritySchemeOrReference.\n * Use `create(NamedSecuritySchemeOrReferenceSchema)` to create a new message.\n */\nexport const NamedSecuritySchemeOrReferenceSchema: GenMessage<NamedSecuritySchemeOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 41);\n\n/**\n * Automatically-generated message used to represent maps of ServerVariable as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedServerVariable\n */\nexport type NamedServerVariable = Message<\"gnostic.openapi.v3.NamedServerVariable\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.ServerVariable value = 2;\n   */\n  value?: ServerVariable;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedServerVariable.\n * Use `create(NamedServerVariableSchema)` to create a new message.\n */\nexport const NamedServerVariableSchema: GenMessage<NamedServerVariable> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 42);\n\n/**\n * Automatically-generated message used to represent maps of string as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedString\n */\nexport type NamedString = Message<\"gnostic.openapi.v3.NamedString\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: string value = 2;\n   */\n  value: string;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedString.\n * Use `create(NamedStringSchema)` to create a new message.\n */\nexport const NamedStringSchema: GenMessage<NamedString> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 43);\n\n/**\n * Automatically-generated message used to represent maps of StringArray as ordered (name,value) pairs.\n *\n * @generated from message gnostic.openapi.v3.NamedStringArray\n */\nexport type NamedStringArray = Message<\"gnostic.openapi.v3.NamedStringArray\"> & {\n  /**\n   * Map key\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Mapped value\n   *\n   * @generated from field: gnostic.openapi.v3.StringArray value = 2;\n   */\n  value?: StringArray;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.NamedStringArray.\n * Use `create(NamedStringArraySchema)` to create a new message.\n */\nexport const NamedStringArraySchema: GenMessage<NamedStringArray> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 44);\n\n/**\n * Configuration details for a supported OAuth Flow\n *\n * @generated from message gnostic.openapi.v3.OauthFlow\n */\nexport type OauthFlow = Message<\"gnostic.openapi.v3.OauthFlow\"> & {\n  /**\n   * @generated from field: string authorization_url = 1;\n   */\n  authorizationUrl: string;\n\n  /**\n   * @generated from field: string token_url = 2;\n   */\n  tokenUrl: string;\n\n  /**\n   * @generated from field: string refresh_url = 3;\n   */\n  refreshUrl: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Strings scopes = 4;\n   */\n  scopes?: Strings;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 5;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.OauthFlow.\n * Use `create(OauthFlowSchema)` to create a new message.\n */\nexport const OauthFlowSchema: GenMessage<OauthFlow> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 45);\n\n/**\n * Allows configuration of the supported OAuth Flows.\n *\n * @generated from message gnostic.openapi.v3.OauthFlows\n */\nexport type OauthFlows = Message<\"gnostic.openapi.v3.OauthFlows\"> & {\n  /**\n   * @generated from field: gnostic.openapi.v3.OauthFlow implicit = 1;\n   */\n  implicit?: OauthFlow;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.OauthFlow password = 2;\n   */\n  password?: OauthFlow;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.OauthFlow client_credentials = 3;\n   */\n  clientCredentials?: OauthFlow;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.OauthFlow authorization_code = 4;\n   */\n  authorizationCode?: OauthFlow;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 5;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.OauthFlows.\n * Use `create(OauthFlowsSchema)` to create a new message.\n */\nexport const OauthFlowsSchema: GenMessage<OauthFlows> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 46);\n\n/**\n * @generated from message gnostic.openapi.v3.Object\n */\nexport type Object$ = Message<\"gnostic.openapi.v3.Object\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny additional_properties = 1;\n   */\n  additionalProperties: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Object.\n * Use `create(ObjectSchema)` to create a new message.\n */\nexport const ObjectSchema: GenMessage<Object$> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 47);\n\n/**\n * Describes a single API operation on a path.\n *\n * @generated from message gnostic.openapi.v3.Operation\n */\nexport type Operation = Message<\"gnostic.openapi.v3.Operation\"> & {\n  /**\n   * @generated from field: repeated string tags = 1;\n   */\n  tags: string[];\n\n  /**\n   * @generated from field: string summary = 2;\n   */\n  summary: string;\n\n  /**\n   * @generated from field: string description = 3;\n   */\n  description: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ExternalDocs external_docs = 4;\n   */\n  externalDocs?: ExternalDocs;\n\n  /**\n   * @generated from field: string operation_id = 5;\n   */\n  operationId: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.ParameterOrReference parameters = 6;\n   */\n  parameters: ParameterOrReference[];\n\n  /**\n   * @generated from field: gnostic.openapi.v3.RequestBodyOrReference request_body = 7;\n   */\n  requestBody?: RequestBodyOrReference;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Responses responses = 8;\n   */\n  responses?: Responses;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.CallbacksOrReferences callbacks = 9;\n   */\n  callbacks?: CallbacksOrReferences;\n\n  /**\n   * @generated from field: bool deprecated = 10;\n   */\n  deprecated: boolean;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.SecurityRequirement security = 11;\n   */\n  security: SecurityRequirement[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.Server servers = 12;\n   */\n  servers: Server[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 13;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Operation.\n * Use `create(OperationSchema)` to create a new message.\n */\nexport const OperationSchema: GenMessage<Operation> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 48);\n\n/**\n * Describes a single operation parameter.  A unique parameter is defined by a combination of a name and location.\n *\n * @generated from message gnostic.openapi.v3.Parameter\n */\nexport type Parameter = Message<\"gnostic.openapi.v3.Parameter\"> & {\n  /**\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * @generated from field: string in = 2;\n   */\n  in: string;\n\n  /**\n   * @generated from field: string description = 3;\n   */\n  description: string;\n\n  /**\n   * @generated from field: bool required = 4;\n   */\n  required: boolean;\n\n  /**\n   * @generated from field: bool deprecated = 5;\n   */\n  deprecated: boolean;\n\n  /**\n   * @generated from field: bool allow_empty_value = 6;\n   */\n  allowEmptyValue: boolean;\n\n  /**\n   * @generated from field: string style = 7;\n   */\n  style: string;\n\n  /**\n   * @generated from field: bool explode = 8;\n   */\n  explode: boolean;\n\n  /**\n   * @generated from field: bool allow_reserved = 9;\n   */\n  allowReserved: boolean;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.SchemaOrReference schema = 10;\n   */\n  schema?: SchemaOrReference;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Any example = 11;\n   */\n  example?: Any;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ExamplesOrReferences examples = 12;\n   */\n  examples?: ExamplesOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.MediaTypes content = 13;\n   */\n  content?: MediaTypes;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 14;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Parameter.\n * Use `create(ParameterSchema)` to create a new message.\n */\nexport const ParameterSchema: GenMessage<Parameter> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 49);\n\n/**\n * @generated from message gnostic.openapi.v3.ParameterOrReference\n */\nexport type ParameterOrReference = Message<\"gnostic.openapi.v3.ParameterOrReference\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.ParameterOrReference.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.Parameter parameter = 1;\n     */\n    value: Parameter;\n    case: \"parameter\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Reference reference = 2;\n     */\n    value: Reference;\n    case: \"reference\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ParameterOrReference.\n * Use `create(ParameterOrReferenceSchema)` to create a new message.\n */\nexport const ParameterOrReferenceSchema: GenMessage<ParameterOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 50);\n\n/**\n * @generated from message gnostic.openapi.v3.ParametersOrReferences\n */\nexport type ParametersOrReferences = Message<\"gnostic.openapi.v3.ParametersOrReferences\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedParameterOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedParameterOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ParametersOrReferences.\n * Use `create(ParametersOrReferencesSchema)` to create a new message.\n */\nexport const ParametersOrReferencesSchema: GenMessage<ParametersOrReferences> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 51);\n\n/**\n * Describes the operations available on a single path. A Path Item MAY be empty, due to ACL constraints. The path itself is still exposed to the documentation viewer but they will not know which operations and parameters are available.\n *\n * @generated from message gnostic.openapi.v3.PathItem\n */\nexport type PathItem = Message<\"gnostic.openapi.v3.PathItem\"> & {\n  /**\n   * @generated from field: string _ref = 1;\n   */\n  Ref: string;\n\n  /**\n   * @generated from field: string summary = 2;\n   */\n  summary: string;\n\n  /**\n   * @generated from field: string description = 3;\n   */\n  description: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Operation get = 4;\n   */\n  get?: Operation;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Operation put = 5;\n   */\n  put?: Operation;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Operation post = 6;\n   */\n  post?: Operation;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Operation delete = 7;\n   */\n  delete?: Operation;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Operation options = 8;\n   */\n  options?: Operation;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Operation head = 9;\n   */\n  head?: Operation;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Operation patch = 10;\n   */\n  patch?: Operation;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Operation trace = 11;\n   */\n  trace?: Operation;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.Server servers = 12;\n   */\n  servers: Server[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.ParameterOrReference parameters = 13;\n   */\n  parameters: ParameterOrReference[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 14;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.PathItem.\n * Use `create(PathItemSchema)` to create a new message.\n */\nexport const PathItemSchema: GenMessage<PathItem> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 52);\n\n/**\n * Holds the relative paths to the individual endpoints and their operations. The path is appended to the URL from the `Server Object` in order to construct the full URL.  The Paths MAY be empty, due to ACL constraints.\n *\n * @generated from message gnostic.openapi.v3.Paths\n */\nexport type Paths = Message<\"gnostic.openapi.v3.Paths\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedPathItem path = 1;\n   */\n  path: NamedPathItem[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 2;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Paths.\n * Use `create(PathsSchema)` to create a new message.\n */\nexport const PathsSchema: GenMessage<Paths> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 53);\n\n/**\n * @generated from message gnostic.openapi.v3.Properties\n */\nexport type Properties = Message<\"gnostic.openapi.v3.Properties\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedSchemaOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedSchemaOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Properties.\n * Use `create(PropertiesSchema)` to create a new message.\n */\nexport const PropertiesSchema: GenMessage<Properties> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 54);\n\n/**\n * A simple object to allow referencing other components in the specification, internally and externally.  The Reference Object is defined by JSON Reference and follows the same structure, behavior and rules.   For this specification, reference resolution is accomplished as defined by the JSON Reference specification and not by the JSON Schema specification.\n *\n * @generated from message gnostic.openapi.v3.Reference\n */\nexport type Reference = Message<\"gnostic.openapi.v3.Reference\"> & {\n  /**\n   * @generated from field: string _ref = 1;\n   */\n  Ref: string;\n\n  /**\n   * @generated from field: string summary = 2;\n   */\n  summary: string;\n\n  /**\n   * @generated from field: string description = 3;\n   */\n  description: string;\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Reference.\n * Use `create(ReferenceSchema)` to create a new message.\n */\nexport const ReferenceSchema: GenMessage<Reference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 55);\n\n/**\n * @generated from message gnostic.openapi.v3.RequestBodiesOrReferences\n */\nexport type RequestBodiesOrReferences = Message<\"gnostic.openapi.v3.RequestBodiesOrReferences\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedRequestBodyOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedRequestBodyOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.RequestBodiesOrReferences.\n * Use `create(RequestBodiesOrReferencesSchema)` to create a new message.\n */\nexport const RequestBodiesOrReferencesSchema: GenMessage<RequestBodiesOrReferences> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 56);\n\n/**\n * Describes a single request body.\n *\n * @generated from message gnostic.openapi.v3.RequestBody\n */\nexport type RequestBody = Message<\"gnostic.openapi.v3.RequestBody\"> & {\n  /**\n   * @generated from field: string description = 1;\n   */\n  description: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.MediaTypes content = 2;\n   */\n  content?: MediaTypes;\n\n  /**\n   * @generated from field: bool required = 3;\n   */\n  required: boolean;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 4;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.RequestBody.\n * Use `create(RequestBodySchema)` to create a new message.\n */\nexport const RequestBodySchema: GenMessage<RequestBody> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 57);\n\n/**\n * @generated from message gnostic.openapi.v3.RequestBodyOrReference\n */\nexport type RequestBodyOrReference = Message<\"gnostic.openapi.v3.RequestBodyOrReference\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.RequestBodyOrReference.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.RequestBody request_body = 1;\n     */\n    value: RequestBody;\n    case: \"requestBody\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Reference reference = 2;\n     */\n    value: Reference;\n    case: \"reference\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.RequestBodyOrReference.\n * Use `create(RequestBodyOrReferenceSchema)` to create a new message.\n */\nexport const RequestBodyOrReferenceSchema: GenMessage<RequestBodyOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 58);\n\n/**\n * Describes a single response from an API Operation, including design-time, static  `links` to operations based on the response.\n *\n * @generated from message gnostic.openapi.v3.Response\n */\nexport type Response = Message<\"gnostic.openapi.v3.Response\"> & {\n  /**\n   * @generated from field: string description = 1;\n   */\n  description: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.HeadersOrReferences headers = 2;\n   */\n  headers?: HeadersOrReferences;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.MediaTypes content = 3;\n   */\n  content?: MediaTypes;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.LinksOrReferences links = 4;\n   */\n  links?: LinksOrReferences;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 5;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Response.\n * Use `create(ResponseSchema)` to create a new message.\n */\nexport const ResponseSchema: GenMessage<Response> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 59);\n\n/**\n * @generated from message gnostic.openapi.v3.ResponseOrReference\n */\nexport type ResponseOrReference = Message<\"gnostic.openapi.v3.ResponseOrReference\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.ResponseOrReference.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.Response response = 1;\n     */\n    value: Response;\n    case: \"response\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Reference reference = 2;\n     */\n    value: Reference;\n    case: \"reference\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ResponseOrReference.\n * Use `create(ResponseOrReferenceSchema)` to create a new message.\n */\nexport const ResponseOrReferenceSchema: GenMessage<ResponseOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 60);\n\n/**\n * A container for the expected responses of an operation. The container maps a HTTP response code to the expected response.  The documentation is not necessarily expected to cover all possible HTTP response codes because they may not be known in advance. However, documentation is expected to cover a successful operation response and any known errors.  The `default` MAY be used as a default response object for all HTTP codes  that are not covered individually by the specification.  The `Responses Object` MUST contain at least one response code, and it  SHOULD be the response for a successful operation call.\n *\n * @generated from message gnostic.openapi.v3.Responses\n */\nexport type Responses = Message<\"gnostic.openapi.v3.Responses\"> & {\n  /**\n   * @generated from field: gnostic.openapi.v3.ResponseOrReference default = 1;\n   */\n  default?: ResponseOrReference;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedResponseOrReference response_or_reference = 2;\n   */\n  responseOrReference: NamedResponseOrReference[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 3;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Responses.\n * Use `create(ResponsesSchema)` to create a new message.\n */\nexport const ResponsesSchema: GenMessage<Responses> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 61);\n\n/**\n * @generated from message gnostic.openapi.v3.ResponsesOrReferences\n */\nexport type ResponsesOrReferences = Message<\"gnostic.openapi.v3.ResponsesOrReferences\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedResponseOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedResponseOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ResponsesOrReferences.\n * Use `create(ResponsesOrReferencesSchema)` to create a new message.\n */\nexport const ResponsesOrReferencesSchema: GenMessage<ResponsesOrReferences> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 62);\n\n/**\n * The Schema Object allows the definition of input and output data types. These types can be objects, but also primitives and arrays. This object is an extended subset of the JSON Schema Specification Wright Draft 00.  For more information about the properties, see JSON Schema Core and JSON Schema Validation. Unless stated otherwise, the property definitions follow the JSON Schema.\n *\n * @generated from message gnostic.openapi.v3.Schema\n */\nexport type Schema = Message<\"gnostic.openapi.v3.Schema\"> & {\n  /**\n   * @generated from field: bool nullable = 1;\n   */\n  nullable: boolean;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Discriminator discriminator = 2;\n   */\n  discriminator?: Discriminator;\n\n  /**\n   * @generated from field: bool read_only = 3;\n   */\n  readOnly: boolean;\n\n  /**\n   * @generated from field: bool write_only = 4;\n   */\n  writeOnly: boolean;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Xml xml = 5;\n   */\n  xml?: Xml;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ExternalDocs external_docs = 6;\n   */\n  externalDocs?: ExternalDocs;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Any example = 7;\n   */\n  example?: Any;\n\n  /**\n   * @generated from field: bool deprecated = 8;\n   */\n  deprecated: boolean;\n\n  /**\n   * @generated from field: string title = 9;\n   */\n  title: string;\n\n  /**\n   * @generated from field: double multiple_of = 10;\n   */\n  multipleOf: number;\n\n  /**\n   * @generated from field: double maximum = 11;\n   */\n  maximum: number;\n\n  /**\n   * @generated from field: bool exclusive_maximum = 12;\n   */\n  exclusiveMaximum: boolean;\n\n  /**\n   * @generated from field: double minimum = 13;\n   */\n  minimum: number;\n\n  /**\n   * @generated from field: bool exclusive_minimum = 14;\n   */\n  exclusiveMinimum: boolean;\n\n  /**\n   * @generated from field: int64 max_length = 15;\n   */\n  maxLength: bigint;\n\n  /**\n   * @generated from field: int64 min_length = 16;\n   */\n  minLength: bigint;\n\n  /**\n   * @generated from field: string pattern = 17;\n   */\n  pattern: string;\n\n  /**\n   * @generated from field: int64 max_items = 18;\n   */\n  maxItems: bigint;\n\n  /**\n   * @generated from field: int64 min_items = 19;\n   */\n  minItems: bigint;\n\n  /**\n   * @generated from field: bool unique_items = 20;\n   */\n  uniqueItems: boolean;\n\n  /**\n   * @generated from field: int64 max_properties = 21;\n   */\n  maxProperties: bigint;\n\n  /**\n   * @generated from field: int64 min_properties = 22;\n   */\n  minProperties: bigint;\n\n  /**\n   * @generated from field: repeated string required = 23;\n   */\n  required: string[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.Any enum = 24;\n   */\n  enum: Any[];\n\n  /**\n   * @generated from field: string type = 25;\n   */\n  type: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.SchemaOrReference all_of = 26;\n   */\n  allOf: SchemaOrReference[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.SchemaOrReference one_of = 27;\n   */\n  oneOf: SchemaOrReference[];\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.SchemaOrReference any_of = 28;\n   */\n  anyOf: SchemaOrReference[];\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Schema not = 29;\n   */\n  not?: Schema;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ItemsItem items = 30;\n   */\n  items?: ItemsItem;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.Properties properties = 31;\n   */\n  properties?: Properties;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.AdditionalPropertiesItem additional_properties = 32;\n   */\n  additionalProperties?: AdditionalPropertiesItem;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.DefaultType default = 33;\n   */\n  default?: DefaultType;\n\n  /**\n   * @generated from field: string description = 34;\n   */\n  description: string;\n\n  /**\n   * @generated from field: string format = 35;\n   */\n  format: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 36;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Schema.\n * Use `create(SchemaSchema)` to create a new message.\n */\nexport const SchemaSchema: GenMessage<Schema> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 63);\n\n/**\n * @generated from message gnostic.openapi.v3.SchemaOrReference\n */\nexport type SchemaOrReference = Message<\"gnostic.openapi.v3.SchemaOrReference\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.SchemaOrReference.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.Schema schema = 1;\n     */\n    value: Schema;\n    case: \"schema\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Reference reference = 2;\n     */\n    value: Reference;\n    case: \"reference\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.SchemaOrReference.\n * Use `create(SchemaOrReferenceSchema)` to create a new message.\n */\nexport const SchemaOrReferenceSchema: GenMessage<SchemaOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 64);\n\n/**\n * @generated from message gnostic.openapi.v3.SchemasOrReferences\n */\nexport type SchemasOrReferences = Message<\"gnostic.openapi.v3.SchemasOrReferences\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedSchemaOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedSchemaOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.SchemasOrReferences.\n * Use `create(SchemasOrReferencesSchema)` to create a new message.\n */\nexport const SchemasOrReferencesSchema: GenMessage<SchemasOrReferences> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 65);\n\n/**\n * Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a security scheme declared in the Security Schemes under the Components Object.  Security Requirement Objects that contain multiple schemes require that all schemes MUST be satisfied for a request to be authorized. This enables support for scenarios where multiple query parameters or HTTP headers are required to convey security information.  When a list of Security Requirement Objects is defined on the OpenAPI Object or Operation Object, only one of the Security Requirement Objects in the list needs to be satisfied to authorize the request.\n *\n * @generated from message gnostic.openapi.v3.SecurityRequirement\n */\nexport type SecurityRequirement = Message<\"gnostic.openapi.v3.SecurityRequirement\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedStringArray additional_properties = 1;\n   */\n  additionalProperties: NamedStringArray[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.SecurityRequirement.\n * Use `create(SecurityRequirementSchema)` to create a new message.\n */\nexport const SecurityRequirementSchema: GenMessage<SecurityRequirement> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 66);\n\n/**\n * Defines a security scheme that can be used by the operations. Supported schemes are HTTP authentication, an API key (either as a header, a cookie parameter or as a query parameter), mutual TLS (use of a client certificate), OAuth2's common flows (implicit, password, application and access code) as defined in RFC6749, and OpenID Connect.   Please note that currently (2019) the implicit flow is about to be deprecated OAuth 2.0 Security Best Current Practice. Recommended for most use case is Authorization Code Grant flow with PKCE.\n *\n * @generated from message gnostic.openapi.v3.SecurityScheme\n */\nexport type SecurityScheme = Message<\"gnostic.openapi.v3.SecurityScheme\"> & {\n  /**\n   * @generated from field: string type = 1;\n   */\n  type: string;\n\n  /**\n   * @generated from field: string description = 2;\n   */\n  description: string;\n\n  /**\n   * @generated from field: string name = 3;\n   */\n  name: string;\n\n  /**\n   * @generated from field: string in = 4;\n   */\n  in: string;\n\n  /**\n   * @generated from field: string scheme = 5;\n   */\n  scheme: string;\n\n  /**\n   * @generated from field: string bearer_format = 6;\n   */\n  bearerFormat: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.OauthFlows flows = 7;\n   */\n  flows?: OauthFlows;\n\n  /**\n   * @generated from field: string open_id_connect_url = 8;\n   */\n  openIdConnectUrl: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 9;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.SecurityScheme.\n * Use `create(SecuritySchemeSchema)` to create a new message.\n */\nexport const SecuritySchemeSchema: GenMessage<SecurityScheme> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 67);\n\n/**\n * @generated from message gnostic.openapi.v3.SecuritySchemeOrReference\n */\nexport type SecuritySchemeOrReference = Message<\"gnostic.openapi.v3.SecuritySchemeOrReference\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.SecuritySchemeOrReference.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: gnostic.openapi.v3.SecurityScheme security_scheme = 1;\n     */\n    value: SecurityScheme;\n    case: \"securityScheme\";\n  } | {\n    /**\n     * @generated from field: gnostic.openapi.v3.Reference reference = 2;\n     */\n    value: Reference;\n    case: \"reference\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.SecuritySchemeOrReference.\n * Use `create(SecuritySchemeOrReferenceSchema)` to create a new message.\n */\nexport const SecuritySchemeOrReferenceSchema: GenMessage<SecuritySchemeOrReference> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 68);\n\n/**\n * @generated from message gnostic.openapi.v3.SecuritySchemesOrReferences\n */\nexport type SecuritySchemesOrReferences = Message<\"gnostic.openapi.v3.SecuritySchemesOrReferences\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedSecuritySchemeOrReference additional_properties = 1;\n   */\n  additionalProperties: NamedSecuritySchemeOrReference[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.SecuritySchemesOrReferences.\n * Use `create(SecuritySchemesOrReferencesSchema)` to create a new message.\n */\nexport const SecuritySchemesOrReferencesSchema: GenMessage<SecuritySchemesOrReferences> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 69);\n\n/**\n * An object representing a Server.\n *\n * @generated from message gnostic.openapi.v3.Server\n */\nexport type Server = Message<\"gnostic.openapi.v3.Server\"> & {\n  /**\n   * @generated from field: string url = 1;\n   */\n  url: string;\n\n  /**\n   * @generated from field: string description = 2;\n   */\n  description: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ServerVariables variables = 3;\n   */\n  variables?: ServerVariables;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 4;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Server.\n * Use `create(ServerSchema)` to create a new message.\n */\nexport const ServerSchema: GenMessage<Server> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 70);\n\n/**\n * An object representing a Server Variable for server URL template substitution.\n *\n * @generated from message gnostic.openapi.v3.ServerVariable\n */\nexport type ServerVariable = Message<\"gnostic.openapi.v3.ServerVariable\"> & {\n  /**\n   * @generated from field: repeated string enum = 1;\n   */\n  enum: string[];\n\n  /**\n   * @generated from field: string default = 2;\n   */\n  default: string;\n\n  /**\n   * @generated from field: string description = 3;\n   */\n  description: string;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 4;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ServerVariable.\n * Use `create(ServerVariableSchema)` to create a new message.\n */\nexport const ServerVariableSchema: GenMessage<ServerVariable> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 71);\n\n/**\n * @generated from message gnostic.openapi.v3.ServerVariables\n */\nexport type ServerVariables = Message<\"gnostic.openapi.v3.ServerVariables\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedServerVariable additional_properties = 1;\n   */\n  additionalProperties: NamedServerVariable[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.ServerVariables.\n * Use `create(ServerVariablesSchema)` to create a new message.\n */\nexport const ServerVariablesSchema: GenMessage<ServerVariables> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 72);\n\n/**\n * Any property starting with x- is valid.\n *\n * @generated from message gnostic.openapi.v3.SpecificationExtension\n */\nexport type SpecificationExtension = Message<\"gnostic.openapi.v3.SpecificationExtension\"> & {\n  /**\n   * @generated from oneof gnostic.openapi.v3.SpecificationExtension.oneof\n   */\n  oneof: {\n    /**\n     * @generated from field: double number = 1;\n     */\n    value: number;\n    case: \"number\";\n  } | {\n    /**\n     * @generated from field: bool boolean = 2;\n     */\n    value: boolean;\n    case: \"boolean\";\n  } | {\n    /**\n     * @generated from field: string string = 3;\n     */\n    value: string;\n    case: \"string\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message gnostic.openapi.v3.SpecificationExtension.\n * Use `create(SpecificationExtensionSchema)` to create a new message.\n */\nexport const SpecificationExtensionSchema: GenMessage<SpecificationExtension> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 73);\n\n/**\n * @generated from message gnostic.openapi.v3.StringArray\n */\nexport type StringArray = Message<\"gnostic.openapi.v3.StringArray\"> & {\n  /**\n   * @generated from field: repeated string value = 1;\n   */\n  value: string[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.StringArray.\n * Use `create(StringArraySchema)` to create a new message.\n */\nexport const StringArraySchema: GenMessage<StringArray> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 74);\n\n/**\n * @generated from message gnostic.openapi.v3.Strings\n */\nexport type Strings = Message<\"gnostic.openapi.v3.Strings\"> & {\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedString additional_properties = 1;\n   */\n  additionalProperties: NamedString[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Strings.\n * Use `create(StringsSchema)` to create a new message.\n */\nexport const StringsSchema: GenMessage<Strings> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 75);\n\n/**\n * Adds metadata to a single tag that is used by the Operation Object. It is not mandatory to have a Tag Object per tag defined in the Operation Object instances.\n *\n * @generated from message gnostic.openapi.v3.Tag\n */\nexport type Tag = Message<\"gnostic.openapi.v3.Tag\"> & {\n  /**\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * @generated from field: string description = 2;\n   */\n  description: string;\n\n  /**\n   * @generated from field: gnostic.openapi.v3.ExternalDocs external_docs = 3;\n   */\n  externalDocs?: ExternalDocs;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 4;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Tag.\n * Use `create(TagSchema)` to create a new message.\n */\nexport const TagSchema: GenMessage<Tag> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 76);\n\n/**\n * A metadata object that allows for more fine-tuned XML model definitions.  When using arrays, XML element names are *not* inferred (for singular/plural forms) and the `name` property SHOULD be used to add that information. See examples for expected behavior.\n *\n * @generated from message gnostic.openapi.v3.Xml\n */\nexport type Xml = Message<\"gnostic.openapi.v3.Xml\"> & {\n  /**\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * @generated from field: string namespace = 2;\n   */\n  namespace: string;\n\n  /**\n   * @generated from field: string prefix = 3;\n   */\n  prefix: string;\n\n  /**\n   * @generated from field: bool attribute = 4;\n   */\n  attribute: boolean;\n\n  /**\n   * @generated from field: bool wrapped = 5;\n   */\n  wrapped: boolean;\n\n  /**\n   * @generated from field: repeated gnostic.openapi.v3.NamedAny specification_extension = 6;\n   */\n  specificationExtension: NamedAny[];\n};\n\n/**\n * Describes the message gnostic.openapi.v3.Xml.\n * Use `create(XmlSchema)` to create a new message.\n */\nexport const XmlSchema: GenMessage<Xml> = /*@__PURE__*/\n  messageDesc(file_gnostic_openapi_v3_openapiv3, 77);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/index.ts",
    "content": "// @openstatus/proto main exports\nexport * from \"./openstatus/monitor/v1/index.js\";\nexport * from \"./openstatus/health/v1/index.js\";\nexport * from \"./openstatus/status_report/v1/index.js\";\nexport * from \"./openstatus/status_page/v1/index.js\";\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/health/v1/health_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/health/v1/health.proto (package openstatus.health.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/health/v1/health.proto.\n */\nexport const file_openstatus_health_v1_health: GenFile = /*@__PURE__*/\n  fileDesc(\"CiFvcGVuc3RhdHVzL2hlYWx0aC92MS9oZWFsdGgucHJvdG8SFG9wZW5zdGF0dXMuaGVhbHRoLnYxIh8KDENoZWNrUmVxdWVzdBIPCgdzZXJ2aWNlGAEgASgJIr8BCg1DaGVja1Jlc3BvbnNlEkEKBnN0YXR1cxgBIAEoDjIxLm9wZW5zdGF0dXMuaGVhbHRoLnYxLkNoZWNrUmVzcG9uc2UuU2VydmluZ1N0YXR1cyJrCg1TZXJ2aW5nU3RhdHVzEh4KGlNFUlZJTkdfU1RBVFVTX1VOU1BFQ0lGSUVEEAASGgoWU0VSVklOR19TVEFUVVNfU0VSVklORxABEh4KGlNFUlZJTkdfU1RBVFVTX05PVF9TRVJWSU5HEAIyZgoNSGVhbHRoU2VydmljZRJVCgVDaGVjaxIiLm9wZW5zdGF0dXMuaGVhbHRoLnYxLkNoZWNrUmVxdWVzdBojLm9wZW5zdGF0dXMuaGVhbHRoLnYxLkNoZWNrUmVzcG9uc2UiA5ACAUJRWk9naXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvaGVhbHRoL3YxO2hlYWx0aHYxYgZwcm90bzM\");\n\n/**\n * CheckRequest is the request message for health checks.\n *\n * @generated from message openstatus.health.v1.CheckRequest\n */\nexport type CheckRequest = Message<\"openstatus.health.v1.CheckRequest\"> & {\n  /**\n   * Optional service name to check. If empty, checks overall service health.\n   *\n   * @generated from field: string service = 1;\n   */\n  service: string;\n};\n\n/**\n * Describes the message openstatus.health.v1.CheckRequest.\n * Use `create(CheckRequestSchema)` to create a new message.\n */\nexport const CheckRequestSchema: GenMessage<CheckRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_health_v1_health, 0);\n\n/**\n * CheckResponse is the response message for health checks.\n *\n * @generated from message openstatus.health.v1.CheckResponse\n */\nexport type CheckResponse = Message<\"openstatus.health.v1.CheckResponse\"> & {\n  /**\n   * The serving status of the service.\n   *\n   * @generated from field: openstatus.health.v1.CheckResponse.ServingStatus status = 1;\n   */\n  status: CheckResponse_ServingStatus;\n};\n\n/**\n * Describes the message openstatus.health.v1.CheckResponse.\n * Use `create(CheckResponseSchema)` to create a new message.\n */\nexport const CheckResponseSchema: GenMessage<CheckResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_health_v1_health, 1);\n\n/**\n * ServingStatus represents the health status of the service.\n *\n * @generated from enum openstatus.health.v1.CheckResponse.ServingStatus\n */\nexport enum CheckResponse_ServingStatus {\n  /**\n   * SERVING_STATUS_UNSPECIFIED indicates an unknown status.\n   *\n   * @generated from enum value: SERVING_STATUS_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * SERVING_STATUS_SERVING indicates the service is healthy and serving.\n   *\n   * @generated from enum value: SERVING_STATUS_SERVING = 1;\n   */\n  SERVING = 1,\n\n  /**\n   * SERVING_STATUS_NOT_SERVING indicates the service is not healthy.\n   *\n   * @generated from enum value: SERVING_STATUS_NOT_SERVING = 2;\n   */\n  NOT_SERVING = 2,\n}\n\n/**\n * Describes the enum openstatus.health.v1.CheckResponse.ServingStatus.\n */\nexport const CheckResponse_ServingStatusSchema: GenEnum<CheckResponse_ServingStatus> = /*@__PURE__*/\n  enumDesc(file_openstatus_health_v1_health, 1, 0);\n\n/**\n * HealthService provides health check endpoints for load balancer probes.\n *\n * @generated from service openstatus.health.v1.HealthService\n */\nexport const HealthService: GenService<{\n  /**\n   * Check returns the current serving status of the service.\n   *\n   * @generated from rpc openstatus.health.v1.HealthService.Check\n   */\n  check: {\n    methodKind: \"unary\";\n    input: typeof CheckRequestSchema;\n    output: typeof CheckResponseSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_openstatus_health_v1_health, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/health/v1/index.ts",
    "content": "// Health service exports\nexport * from \"./health_pb.js\";\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/maintenance/v1/index.ts",
    "content": "// Maintenance service exports\nexport * from \"./maintenance_pb.js\";\nexport * from \"./service_pb.js\";\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/maintenance/v1/maintenance_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/maintenance/v1/maintenance.proto (package openstatus.maintenance.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/maintenance/v1/maintenance.proto.\n */\nexport const file_openstatus_maintenance_v1_maintenance: GenFile = /*@__PURE__*/\n  fileDesc(\"CitvcGVuc3RhdHVzL21haW50ZW5hbmNlL3YxL21haW50ZW5hbmNlLnByb3RvEhlvcGVuc3RhdHVzLm1haW50ZW5hbmNlLnYxIq8BChJNYWludGVuYW5jZVN1bW1hcnkSCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDwoHbWVzc2FnZRgDIAEoCRIMCgRmcm9tGAQgASgJEgoKAnRvGAUgASgJEg8KB3BhZ2VfaWQYBiABKAkSGgoScGFnZV9jb21wb25lbnRfaWRzGAcgAygJEhIKCmNyZWF0ZWRfYXQYCCABKAkSEgoKdXBkYXRlZF9hdBgJIAEoCSKoAQoLTWFpbnRlbmFuY2USCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDwoHbWVzc2FnZRgDIAEoCRIMCgRmcm9tGAQgASgJEgoKAnRvGAUgASgJEg8KB3BhZ2VfaWQYBiABKAkSGgoScGFnZV9jb21wb25lbnRfaWRzGAcgAygJEhIKCmNyZWF0ZWRfYXQYCCABKAkSEgoKdXBkYXRlZF9hdBgJIAEoCUJbWllnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvbWFpbnRlbmFuY2UvdjE7bWFpbnRlbmFuY2V2MWIGcHJvdG8z\");\n\n/**\n * MaintenanceSummary represents metadata for a maintenance window (used in list responses).\n *\n * @generated from message openstatus.maintenance.v1.MaintenanceSummary\n */\nexport type MaintenanceSummary = Message<\"openstatus.maintenance.v1.MaintenanceSummary\"> & {\n  /**\n   * Unique identifier for the maintenance.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Title of the maintenance.\n   *\n   * @generated from field: string title = 2;\n   */\n  title: string;\n\n  /**\n   * Message describing the maintenance.\n   *\n   * @generated from field: string message = 3;\n   */\n  message: string;\n\n  /**\n   * Start time of the maintenance window (RFC 3339 format).\n   *\n   * @generated from field: string from = 4;\n   */\n  from: string;\n\n  /**\n   * End time of the maintenance window (RFC 3339 format).\n   *\n   * @generated from field: string to = 5;\n   */\n  to: string;\n\n  /**\n   * ID of the page this maintenance is associated with.\n   *\n   * @generated from field: string page_id = 6;\n   */\n  pageId: string;\n\n  /**\n   * IDs of affected page components.\n   *\n   * @generated from field: repeated string page_component_ids = 7;\n   */\n  pageComponentIds: string[];\n\n  /**\n   * Timestamp when the maintenance was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 8;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the maintenance was last updated (RFC 3339 format).\n   *\n   * @generated from field: string updated_at = 9;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.MaintenanceSummary.\n * Use `create(MaintenanceSummarySchema)` to create a new message.\n */\nexport const MaintenanceSummarySchema: GenMessage<MaintenanceSummary> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_maintenance, 0);\n\n/**\n * Maintenance represents a maintenance window with full details.\n *\n * @generated from message openstatus.maintenance.v1.Maintenance\n */\nexport type Maintenance = Message<\"openstatus.maintenance.v1.Maintenance\"> & {\n  /**\n   * Unique identifier for the maintenance.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Title of the maintenance.\n   *\n   * @generated from field: string title = 2;\n   */\n  title: string;\n\n  /**\n   * Message describing the maintenance.\n   *\n   * @generated from field: string message = 3;\n   */\n  message: string;\n\n  /**\n   * Start time of the maintenance window (RFC 3339 format).\n   *\n   * @generated from field: string from = 4;\n   */\n  from: string;\n\n  /**\n   * End time of the maintenance window (RFC 3339 format).\n   *\n   * @generated from field: string to = 5;\n   */\n  to: string;\n\n  /**\n   * ID of the page this maintenance is associated with.\n   *\n   * @generated from field: string page_id = 6;\n   */\n  pageId: string;\n\n  /**\n   * IDs of affected page components.\n   *\n   * @generated from field: repeated string page_component_ids = 7;\n   */\n  pageComponentIds: string[];\n\n  /**\n   * Timestamp when the maintenance was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 8;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the maintenance was last updated (RFC 3339 format).\n   *\n   * @generated from field: string updated_at = 9;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.Maintenance.\n * Use `create(MaintenanceSchema)` to create a new message.\n */\nexport const MaintenanceSchema: GenMessage<Maintenance> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_maintenance, 1);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/maintenance/v1/service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/maintenance/v1/service.proto (package openstatus.maintenance.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { Maintenance, MaintenanceSummary } from \"./maintenance_pb.ts\";\nimport { file_openstatus_maintenance_v1_maintenance } from \"./maintenance_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/maintenance/v1/service.proto.\n */\nexport const file_openstatus_maintenance_v1_service: GenFile = /*@__PURE__*/\n  fileDesc(\"CidvcGVuc3RhdHVzL21haW50ZW5hbmNlL3YxL3NlcnZpY2UucHJvdG8SGW9wZW5zdGF0dXMubWFpbnRlbmFuY2UudjEiqAMKGENyZWF0ZU1haW50ZW5hbmNlUmVxdWVzdBIyCgV0aXRsZRgBIAEoCUIjukcWOhQSEkRhdGFiYXNlIE1pZ3JhdGlvbrpIB3IFEAEYgAISGAoHbWVzc2FnZRgCIAEoCUIHukgEcgIQARJ0CgRmcm9tGAMgASgJQma6Rxg6FhIUMjAyNC0wMy0wMVQwMjowMDowMFq6SEhyRjJEXlxkezR9LVxkezJ9LVxkezJ9VFxkezJ9OlxkezJ9OlxkezJ9KFwuXGR7MSw5fSk/KFp8WystXVxkezJ9OlxkezJ9KSQScgoCdG8YBCABKAlCZrpHGDoWEhQyMDI0LTAzLTAxVDA2OjAwOjAwWrpISHJGMkReXGR7NH0tXGR7Mn0tXGR7Mn1UXGR7Mn06XGR7Mn06XGR7Mn0oXC5cZHsxLDl9KT8oWnxbKy1dXGR7Mn06XGR7Mn0pJBIYCgdwYWdlX2lkGAUgASgJQge6SARyAhABEhoKEnBhZ2VfY29tcG9uZW50X2lkcxgGIAMoCRITCgZub3RpZnkYByABKAhIAIgBAUIJCgdfbm90aWZ5IlgKGUNyZWF0ZU1haW50ZW5hbmNlUmVzcG9uc2USOwoLbWFpbnRlbmFuY2UYASABKAsyJi5vcGVuc3RhdHVzLm1haW50ZW5hbmNlLnYxLk1haW50ZW5hbmNlIiwKFUdldE1haW50ZW5hbmNlUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQASJVChZHZXRNYWludGVuYW5jZVJlc3BvbnNlEjsKC21haW50ZW5hbmNlGAEgASgLMiYub3BlbnN0YXR1cy5tYWludGVuYW5jZS52MS5NYWludGVuYW5jZSKNAQoXTGlzdE1haW50ZW5hbmNlc1JlcXVlc3QSHQoFbGltaXQYASABKAVCCbpIBhoEGGQoAUgAiAEBEhwKBm9mZnNldBgCIAEoBUIHukgEGgIoAEgBiAEBEhQKB3BhZ2VfaWQYAyABKAlIAogBAUIICgZfbGltaXRCCQoHX29mZnNldEIKCghfcGFnZV9pZCJzChhMaXN0TWFpbnRlbmFuY2VzUmVzcG9uc2USQwoMbWFpbnRlbmFuY2VzGAEgAygLMi0ub3BlbnN0YXR1cy5tYWludGVuYW5jZS52MS5NYWludGVuYW5jZVN1bW1hcnkSEgoKdG90YWxfc2l6ZRgCIAEoBSKHAwoYVXBkYXRlTWFpbnRlbmFuY2VSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABEh4KBXRpdGxlGAIgASgJQgq6SAdyBRABGIACSACIAQESFAoHbWVzc2FnZRgDIAEoCUgBiAEBEl4KBGZyb20YBCABKAlCS7pISHJGMkReXGR7NH0tXGR7Mn0tXGR7Mn1UXGR7Mn06XGR7Mn06XGR7Mn0oXC5cZHsxLDl9KT8oWnxbKy1dXGR7Mn06XGR7Mn0pJEgCiAEBElwKAnRvGAUgASgJQku6SEhyRjJEXlxkezR9LVxkezJ9LVxkezJ9VFxkezJ9OlxkezJ9OlxkezJ9KFwuXGR7MSw5fSk/KFp8WystXVxkezJ9OlxkezJ9KSRIA4gBARIUCgdwYWdlX2lkGAYgASgJSASIAQESGgoScGFnZV9jb21wb25lbnRfaWRzGAcgAygJQggKBl90aXRsZUIKCghfbWVzc2FnZUIHCgVfZnJvbUIFCgNfdG9CCgoIX3BhZ2VfaWQiWAoZVXBkYXRlTWFpbnRlbmFuY2VSZXNwb25zZRI7CgttYWludGVuYW5jZRgBIAEoCzImLm9wZW5zdGF0dXMubWFpbnRlbmFuY2UudjEuTWFpbnRlbmFuY2UiLwoYRGVsZXRlTWFpbnRlbmFuY2VSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIiwKGURlbGV0ZU1haW50ZW5hbmNlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCDKTBQoSTWFpbnRlbmFuY2VTZXJ2aWNlEn4KEUNyZWF0ZU1haW50ZW5hbmNlEjMub3BlbnN0YXR1cy5tYWludGVuYW5jZS52MS5DcmVhdGVNYWludGVuYW5jZVJlcXVlc3QaNC5vcGVuc3RhdHVzLm1haW50ZW5hbmNlLnYxLkNyZWF0ZU1haW50ZW5hbmNlUmVzcG9uc2USegoOR2V0TWFpbnRlbmFuY2USMC5vcGVuc3RhdHVzLm1haW50ZW5hbmNlLnYxLkdldE1haW50ZW5hbmNlUmVxdWVzdBoxLm9wZW5zdGF0dXMubWFpbnRlbmFuY2UudjEuR2V0TWFpbnRlbmFuY2VSZXNwb25zZSIDkAIBEoABChBMaXN0TWFpbnRlbmFuY2VzEjIub3BlbnN0YXR1cy5tYWludGVuYW5jZS52MS5MaXN0TWFpbnRlbmFuY2VzUmVxdWVzdBozLm9wZW5zdGF0dXMubWFpbnRlbmFuY2UudjEuTGlzdE1haW50ZW5hbmNlc1Jlc3BvbnNlIgOQAgESfgoRVXBkYXRlTWFpbnRlbmFuY2USMy5vcGVuc3RhdHVzLm1haW50ZW5hbmNlLnYxLlVwZGF0ZU1haW50ZW5hbmNlUmVxdWVzdBo0Lm9wZW5zdGF0dXMubWFpbnRlbmFuY2UudjEuVXBkYXRlTWFpbnRlbmFuY2VSZXNwb25zZRJ+ChFEZWxldGVNYWludGVuYW5jZRIzLm9wZW5zdGF0dXMubWFpbnRlbmFuY2UudjEuRGVsZXRlTWFpbnRlbmFuY2VSZXF1ZXN0GjQub3BlbnN0YXR1cy5tYWludGVuYW5jZS52MS5EZWxldGVNYWludGVuYW5jZVJlc3BvbnNlQltaWWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tYWludGVuYW5jZS92MTttYWludGVuYW5jZXYxYgZwcm90bzM\", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_maintenance_v1_maintenance]);\n\n/**\n * CreateMaintenanceRequest is the request to create a new maintenance window.\n *\n * @generated from message openstatus.maintenance.v1.CreateMaintenanceRequest\n */\nexport type CreateMaintenanceRequest = Message<\"openstatus.maintenance.v1.CreateMaintenanceRequest\"> & {\n  /**\n   * Title of the maintenance (required, 1-256 characters).\n   *\n   * @generated from field: string title = 1;\n   */\n  title: string;\n\n  /**\n   * Message describing the maintenance (required).\n   *\n   * @generated from field: string message = 2;\n   */\n  message: string;\n\n  /**\n   * Start time of the maintenance window (RFC 3339 format, required).\n   *\n   * @generated from field: string from = 3;\n   */\n  from: string;\n\n  /**\n   * End time of the maintenance window (RFC 3339 format, required).\n   *\n   * @generated from field: string to = 4;\n   */\n  to: string;\n\n  /**\n   * Page ID to associate with this maintenance (required).\n   *\n   * @generated from field: string page_id = 5;\n   */\n  pageId: string;\n\n  /**\n   * Page component IDs to associate with this maintenance (optional).\n   *\n   * @generated from field: repeated string page_component_ids = 6;\n   */\n  pageComponentIds: string[];\n\n  /**\n   * Whether to notify subscribers about this maintenance (optional, defaults to false).\n   *\n   * @generated from field: optional bool notify = 7;\n   */\n  notify?: boolean;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.CreateMaintenanceRequest.\n * Use `create(CreateMaintenanceRequestSchema)` to create a new message.\n */\nexport const CreateMaintenanceRequestSchema: GenMessage<CreateMaintenanceRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 0);\n\n/**\n * CreateMaintenanceResponse is the response after creating a maintenance window.\n *\n * @generated from message openstatus.maintenance.v1.CreateMaintenanceResponse\n */\nexport type CreateMaintenanceResponse = Message<\"openstatus.maintenance.v1.CreateMaintenanceResponse\"> & {\n  /**\n   * The created maintenance.\n   *\n   * @generated from field: openstatus.maintenance.v1.Maintenance maintenance = 1;\n   */\n  maintenance?: Maintenance;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.CreateMaintenanceResponse.\n * Use `create(CreateMaintenanceResponseSchema)` to create a new message.\n */\nexport const CreateMaintenanceResponseSchema: GenMessage<CreateMaintenanceResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 1);\n\n/**\n * GetMaintenanceRequest is the request to get a maintenance window by ID.\n *\n * @generated from message openstatus.maintenance.v1.GetMaintenanceRequest\n */\nexport type GetMaintenanceRequest = Message<\"openstatus.maintenance.v1.GetMaintenanceRequest\"> & {\n  /**\n   * ID of the maintenance to retrieve (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.GetMaintenanceRequest.\n * Use `create(GetMaintenanceRequestSchema)` to create a new message.\n */\nexport const GetMaintenanceRequestSchema: GenMessage<GetMaintenanceRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 2);\n\n/**\n * GetMaintenanceResponse is the response containing the maintenance window.\n *\n * @generated from message openstatus.maintenance.v1.GetMaintenanceResponse\n */\nexport type GetMaintenanceResponse = Message<\"openstatus.maintenance.v1.GetMaintenanceResponse\"> & {\n  /**\n   * The requested maintenance.\n   *\n   * @generated from field: openstatus.maintenance.v1.Maintenance maintenance = 1;\n   */\n  maintenance?: Maintenance;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.GetMaintenanceResponse.\n * Use `create(GetMaintenanceResponseSchema)` to create a new message.\n */\nexport const GetMaintenanceResponseSchema: GenMessage<GetMaintenanceResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 3);\n\n/**\n * ListMaintenancesRequest is the request to list maintenance windows.\n *\n * @generated from message openstatus.maintenance.v1.ListMaintenancesRequest\n */\nexport type ListMaintenancesRequest = Message<\"openstatus.maintenance.v1.ListMaintenancesRequest\"> & {\n  /**\n   * Maximum number of maintenances to return (1-100, defaults to 50).\n   *\n   * @generated from field: optional int32 limit = 1;\n   */\n  limit?: number;\n\n  /**\n   * Number of maintenances to skip for pagination (defaults to 0).\n   *\n   * @generated from field: optional int32 offset = 2;\n   */\n  offset?: number;\n\n  /**\n   * Filter by page ID (optional).\n   *\n   * @generated from field: optional string page_id = 3;\n   */\n  pageId?: string;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.ListMaintenancesRequest.\n * Use `create(ListMaintenancesRequestSchema)` to create a new message.\n */\nexport const ListMaintenancesRequestSchema: GenMessage<ListMaintenancesRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 4);\n\n/**\n * ListMaintenancesResponse is the response containing maintenance window summaries.\n *\n * @generated from message openstatus.maintenance.v1.ListMaintenancesResponse\n */\nexport type ListMaintenancesResponse = Message<\"openstatus.maintenance.v1.ListMaintenancesResponse\"> & {\n  /**\n   * List of maintenances.\n   *\n   * @generated from field: repeated openstatus.maintenance.v1.MaintenanceSummary maintenances = 1;\n   */\n  maintenances: MaintenanceSummary[];\n\n  /**\n   * Total number of maintenances matching the filter.\n   *\n   * @generated from field: int32 total_size = 2;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.ListMaintenancesResponse.\n * Use `create(ListMaintenancesResponseSchema)` to create a new message.\n */\nexport const ListMaintenancesResponseSchema: GenMessage<ListMaintenancesResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 5);\n\n/**\n * UpdateMaintenanceRequest is the request to update a maintenance window.\n *\n * @generated from message openstatus.maintenance.v1.UpdateMaintenanceRequest\n */\nexport type UpdateMaintenanceRequest = Message<\"openstatus.maintenance.v1.UpdateMaintenanceRequest\"> & {\n  /**\n   * ID of the maintenance to update (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * New title for the maintenance (optional).\n   *\n   * @generated from field: optional string title = 2;\n   */\n  title?: string;\n\n  /**\n   * New message for the maintenance (optional).\n   *\n   * @generated from field: optional string message = 3;\n   */\n  message?: string;\n\n  /**\n   * New start time (RFC 3339 format, optional).\n   *\n   * @generated from field: optional string from = 4;\n   */\n  from?: string;\n\n  /**\n   * New end time (RFC 3339 format, optional).\n   *\n   * @generated from field: optional string to = 5;\n   */\n  to?: string;\n\n  /**\n   * New page ID (optional).\n   *\n   * @generated from field: optional string page_id = 6;\n   */\n  pageId?: string;\n\n  /**\n   * New list of page component IDs (optional, replaces existing list).\n   *\n   * @generated from field: repeated string page_component_ids = 7;\n   */\n  pageComponentIds: string[];\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.UpdateMaintenanceRequest.\n * Use `create(UpdateMaintenanceRequestSchema)` to create a new message.\n */\nexport const UpdateMaintenanceRequestSchema: GenMessage<UpdateMaintenanceRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 6);\n\n/**\n * UpdateMaintenanceResponse is the response after updating a maintenance window.\n *\n * @generated from message openstatus.maintenance.v1.UpdateMaintenanceResponse\n */\nexport type UpdateMaintenanceResponse = Message<\"openstatus.maintenance.v1.UpdateMaintenanceResponse\"> & {\n  /**\n   * The updated maintenance.\n   *\n   * @generated from field: openstatus.maintenance.v1.Maintenance maintenance = 1;\n   */\n  maintenance?: Maintenance;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.UpdateMaintenanceResponse.\n * Use `create(UpdateMaintenanceResponseSchema)` to create a new message.\n */\nexport const UpdateMaintenanceResponseSchema: GenMessage<UpdateMaintenanceResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 7);\n\n/**\n * DeleteMaintenanceRequest is the request to delete a maintenance window.\n *\n * @generated from message openstatus.maintenance.v1.DeleteMaintenanceRequest\n */\nexport type DeleteMaintenanceRequest = Message<\"openstatus.maintenance.v1.DeleteMaintenanceRequest\"> & {\n  /**\n   * ID of the maintenance to delete (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.DeleteMaintenanceRequest.\n * Use `create(DeleteMaintenanceRequestSchema)` to create a new message.\n */\nexport const DeleteMaintenanceRequestSchema: GenMessage<DeleteMaintenanceRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 8);\n\n/**\n * DeleteMaintenanceResponse is the response after deleting a maintenance window.\n *\n * @generated from message openstatus.maintenance.v1.DeleteMaintenanceResponse\n */\nexport type DeleteMaintenanceResponse = Message<\"openstatus.maintenance.v1.DeleteMaintenanceResponse\"> & {\n  /**\n   * Whether the deletion was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n};\n\n/**\n * Describes the message openstatus.maintenance.v1.DeleteMaintenanceResponse.\n * Use `create(DeleteMaintenanceResponseSchema)` to create a new message.\n */\nexport const DeleteMaintenanceResponseSchema: GenMessage<DeleteMaintenanceResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_maintenance_v1_service, 9);\n\n/**\n * MaintenanceService provides CRUD operations for maintenance windows.\n *\n * @generated from service openstatus.maintenance.v1.MaintenanceService\n */\nexport const MaintenanceService: GenService<{\n  /**\n   * CreateMaintenance creates a new maintenance window.\n   *\n   * @generated from rpc openstatus.maintenance.v1.MaintenanceService.CreateMaintenance\n   */\n  createMaintenance: {\n    methodKind: \"unary\";\n    input: typeof CreateMaintenanceRequestSchema;\n    output: typeof CreateMaintenanceResponseSchema;\n  },\n  /**\n   * GetMaintenance retrieves a specific maintenance window by ID.\n   *\n   * @generated from rpc openstatus.maintenance.v1.MaintenanceService.GetMaintenance\n   */\n  getMaintenance: {\n    methodKind: \"unary\";\n    input: typeof GetMaintenanceRequestSchema;\n    output: typeof GetMaintenanceResponseSchema;\n  },\n  /**\n   * ListMaintenances returns all maintenance windows for the workspace.\n   *\n   * @generated from rpc openstatus.maintenance.v1.MaintenanceService.ListMaintenances\n   */\n  listMaintenances: {\n    methodKind: \"unary\";\n    input: typeof ListMaintenancesRequestSchema;\n    output: typeof ListMaintenancesResponseSchema;\n  },\n  /**\n   * UpdateMaintenance updates a maintenance window.\n   *\n   * @generated from rpc openstatus.maintenance.v1.MaintenanceService.UpdateMaintenance\n   */\n  updateMaintenance: {\n    methodKind: \"unary\";\n    input: typeof UpdateMaintenanceRequestSchema;\n    output: typeof UpdateMaintenanceResponseSchema;\n  },\n  /**\n   * DeleteMaintenance removes a maintenance window.\n   *\n   * @generated from rpc openstatus.maintenance.v1.MaintenanceService.DeleteMaintenance\n   */\n  deleteMaintenance: {\n    methodKind: \"unary\";\n    input: typeof DeleteMaintenanceRequestSchema;\n    output: typeof DeleteMaintenanceResponseSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_openstatus_maintenance_v1_service, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/monitor/v1/assertions_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/monitor/v1/assertions.proto (package openstatus.monitor.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/monitor/v1/assertions.proto.\n */\nexport const file_openstatus_monitor_v1_assertions: GenFile = /*@__PURE__*/\n  fileDesc(\"CiZvcGVuc3RhdHVzL21vbml0b3IvdjEvYXNzZXJ0aW9ucy5wcm90bxIVb3BlbnN0YXR1cy5tb25pdG9yLnYxIngKE1N0YXR1c0NvZGVBc3NlcnRpb24SGgoGdGFyZ2V0GAEgASgDQgq6SAciBRjXBChkEkUKCmNvbXBhcmF0b3IYAiABKA4yJy5vcGVuc3RhdHVzLm1vbml0b3IudjEuTnVtYmVyQ29tcGFyYXRvckIIukgFggECIAAiZgoNQm9keUFzc2VydGlvbhIOCgZ0YXJnZXQYASABKAkSRQoKY29tcGFyYXRvchgCIAEoDjInLm9wZW5zdGF0dXMubW9uaXRvci52MS5TdHJpbmdDb21wYXJhdG9yQgi6SAWCAQIgACJ+Cg9IZWFkZXJBc3NlcnRpb24SDgoGdGFyZ2V0GAEgASgJEkUKCmNvbXBhcmF0b3IYAiABKA4yJy5vcGVuc3RhdHVzLm1vbml0b3IudjEuU3RyaW5nQ29tcGFyYXRvckIIukgFggECIAASFAoDa2V5GAMgASgJQge6SARyAhABIpgBCg9SZWNvcmRBc3NlcnRpb24SLgoGcmVjb3JkGAEgASgJQh66SBtyGVIBQVIEQUFBQVIFQ05BTUVSAk1YUgNUWFQSRQoKY29tcGFyYXRvchgCIAEoDjInLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWNvcmRDb21wYXJhdG9yQgi6SAWCAQIgABIOCgZ0YXJnZXQYAyABKAkqjwIKEE51bWJlckNvbXBhcmF0b3ISIQodTlVNQkVSX0NPTVBBUkFUT1JfVU5TUEVDSUZJRUQQABIbChdOVU1CRVJfQ09NUEFSQVRPUl9FUVVBTBABEh8KG05VTUJFUl9DT01QQVJBVE9SX05PVF9FUVVBTBACEiIKHk5VTUJFUl9DT01QQVJBVE9SX0dSRUFURVJfVEhBThADEisKJ05VTUJFUl9DT01QQVJBVE9SX0dSRUFURVJfVEhBTl9PUl9FUVVBTBAEEh8KG05VTUJFUl9DT01QQVJBVE9SX0xFU1NfVEhBThAFEigKJE5VTUJFUl9DT01QQVJBVE9SX0xFU1NfVEhBTl9PUl9FUVVBTBAGKpEDChBTdHJpbmdDb21wYXJhdG9yEiEKHVNUUklOR19DT01QQVJBVE9SX1VOU1BFQ0lGSUVEEAASHgoaU1RSSU5HX0NPTVBBUkFUT1JfQ09OVEFJTlMQARIiCh5TVFJJTkdfQ09NUEFSQVRPUl9OT1RfQ09OVEFJTlMQAhIbChdTVFJJTkdfQ09NUEFSQVRPUl9FUVVBTBADEh8KG1NUUklOR19DT01QQVJBVE9SX05PVF9FUVVBTBAEEhsKF1NUUklOR19DT01QQVJBVE9SX0VNUFRZEAUSHwobU1RSSU5HX0NPTVBBUkFUT1JfTk9UX0VNUFRZEAYSIgoeU1RSSU5HX0NPTVBBUkFUT1JfR1JFQVRFUl9USEFOEAcSKwonU1RSSU5HX0NPTVBBUkFUT1JfR1JFQVRFUl9USEFOX09SX0VRVUFMEAgSHwobU1RSSU5HX0NPTVBBUkFUT1JfTEVTU19USEFOEAkSKAokU1RSSU5HX0NPTVBBUkFUT1JfTEVTU19USEFOX09SX0VRVUFMEAoqtwEKEFJlY29yZENvbXBhcmF0b3ISIQodUkVDT1JEX0NPTVBBUkFUT1JfVU5TUEVDSUZJRUQQABIbChdSRUNPUkRfQ09NUEFSQVRPUl9FUVVBTBABEh8KG1JFQ09SRF9DT01QQVJBVE9SX05PVF9FUVVBTBACEh4KGlJFQ09SRF9DT01QQVJBVE9SX0NPTlRBSU5TEAMSIgoeUkVDT1JEX0NPTVBBUkFUT1JfTk9UX0NPTlRBSU5TEARCU1pRZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL21vbml0b3IvdjE7bW9uaXRvcnYxYgZwcm90bzM\", [file_buf_validate_validate]);\n\n/**\n * StatusCodeAssertion defines an assertion for HTTP status codes.\n *\n * @generated from message openstatus.monitor.v1.StatusCodeAssertion\n */\nexport type StatusCodeAssertion = Message<\"openstatus.monitor.v1.StatusCodeAssertion\"> & {\n  /**\n   * Target status code to compare against (100-599).\n   *\n   * @generated from field: int64 target = 1;\n   */\n  target: bigint;\n\n  /**\n   * Comparison operation (required, must not be UNSPECIFIED).\n   *\n   * @generated from field: openstatus.monitor.v1.NumberComparator comparator = 2;\n   */\n  comparator: NumberComparator;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.StatusCodeAssertion.\n * Use `create(StatusCodeAssertionSchema)` to create a new message.\n */\nexport const StatusCodeAssertionSchema: GenMessage<StatusCodeAssertion> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_assertions, 0);\n\n/**\n * BodyAssertion defines an assertion for response body content.\n *\n * @generated from message openstatus.monitor.v1.BodyAssertion\n */\nexport type BodyAssertion = Message<\"openstatus.monitor.v1.BodyAssertion\"> & {\n  /**\n   * Target value to compare against.\n   *\n   * @generated from field: string target = 1;\n   */\n  target: string;\n\n  /**\n   * Comparison operation (required, must not be UNSPECIFIED).\n   *\n   * @generated from field: openstatus.monitor.v1.StringComparator comparator = 2;\n   */\n  comparator: StringComparator;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.BodyAssertion.\n * Use `create(BodyAssertionSchema)` to create a new message.\n */\nexport const BodyAssertionSchema: GenMessage<BodyAssertion> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_assertions, 1);\n\n/**\n * HeaderAssertion defines an assertion for response headers.\n *\n * @generated from message openstatus.monitor.v1.HeaderAssertion\n */\nexport type HeaderAssertion = Message<\"openstatus.monitor.v1.HeaderAssertion\"> & {\n  /**\n   * Target value to compare against.\n   *\n   * @generated from field: string target = 1;\n   */\n  target: string;\n\n  /**\n   * Comparison operation (required, must not be UNSPECIFIED).\n   *\n   * @generated from field: openstatus.monitor.v1.StringComparator comparator = 2;\n   */\n  comparator: StringComparator;\n\n  /**\n   * Header key to check (required).\n   *\n   * @generated from field: string key = 3;\n   */\n  key: string;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.HeaderAssertion.\n * Use `create(HeaderAssertionSchema)` to create a new message.\n */\nexport const HeaderAssertionSchema: GenMessage<HeaderAssertion> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_assertions, 2);\n\n/**\n * RecordAssertion defines an assertion for DNS records.\n *\n * @generated from message openstatus.monitor.v1.RecordAssertion\n */\nexport type RecordAssertion = Message<\"openstatus.monitor.v1.RecordAssertion\"> & {\n  /**\n   * DNS record type (e.g., \"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\").\n   *\n   * @generated from field: string record = 1;\n   */\n  record: string;\n\n  /**\n   * Comparison operation (required, must not be UNSPECIFIED).\n   *\n   * @generated from field: openstatus.monitor.v1.RecordComparator comparator = 2;\n   */\n  comparator: RecordComparator;\n\n  /**\n   * Target value to compare against.\n   *\n   * @generated from field: string target = 3;\n   */\n  target: string;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.RecordAssertion.\n * Use `create(RecordAssertionSchema)` to create a new message.\n */\nexport const RecordAssertionSchema: GenMessage<RecordAssertion> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_assertions, 3);\n\n/**\n * NumberComparator defines comparison operations for numeric values.\n *\n * @generated from enum openstatus.monitor.v1.NumberComparator\n */\nexport enum NumberComparator {\n  /**\n   * @generated from enum value: NUMBER_COMPARATOR_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: NUMBER_COMPARATOR_EQUAL = 1;\n   */\n  EQUAL = 1,\n\n  /**\n   * @generated from enum value: NUMBER_COMPARATOR_NOT_EQUAL = 2;\n   */\n  NOT_EQUAL = 2,\n\n  /**\n   * @generated from enum value: NUMBER_COMPARATOR_GREATER_THAN = 3;\n   */\n  GREATER_THAN = 3,\n\n  /**\n   * @generated from enum value: NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL = 4;\n   */\n  GREATER_THAN_OR_EQUAL = 4,\n\n  /**\n   * @generated from enum value: NUMBER_COMPARATOR_LESS_THAN = 5;\n   */\n  LESS_THAN = 5,\n\n  /**\n   * @generated from enum value: NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL = 6;\n   */\n  LESS_THAN_OR_EQUAL = 6,\n}\n\n/**\n * Describes the enum openstatus.monitor.v1.NumberComparator.\n */\nexport const NumberComparatorSchema: GenEnum<NumberComparator> = /*@__PURE__*/\n  enumDesc(file_openstatus_monitor_v1_assertions, 0);\n\n/**\n * StringComparator defines comparison operations for string values.\n *\n * @generated from enum openstatus.monitor.v1.StringComparator\n */\nexport enum StringComparator {\n  /**\n   * @generated from enum value: STRING_COMPARATOR_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_CONTAINS = 1;\n   */\n  CONTAINS = 1,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_NOT_CONTAINS = 2;\n   */\n  NOT_CONTAINS = 2,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_EQUAL = 3;\n   */\n  EQUAL = 3,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_NOT_EQUAL = 4;\n   */\n  NOT_EQUAL = 4,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_EMPTY = 5;\n   */\n  EMPTY = 5,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_NOT_EMPTY = 6;\n   */\n  NOT_EMPTY = 6,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_GREATER_THAN = 7;\n   */\n  GREATER_THAN = 7,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_GREATER_THAN_OR_EQUAL = 8;\n   */\n  GREATER_THAN_OR_EQUAL = 8,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_LESS_THAN = 9;\n   */\n  LESS_THAN = 9,\n\n  /**\n   * @generated from enum value: STRING_COMPARATOR_LESS_THAN_OR_EQUAL = 10;\n   */\n  LESS_THAN_OR_EQUAL = 10,\n}\n\n/**\n * Describes the enum openstatus.monitor.v1.StringComparator.\n */\nexport const StringComparatorSchema: GenEnum<StringComparator> = /*@__PURE__*/\n  enumDesc(file_openstatus_monitor_v1_assertions, 1);\n\n/**\n * RecordComparator defines comparison operations for DNS records.\n *\n * @generated from enum openstatus.monitor.v1.RecordComparator\n */\nexport enum RecordComparator {\n  /**\n   * @generated from enum value: RECORD_COMPARATOR_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: RECORD_COMPARATOR_EQUAL = 1;\n   */\n  EQUAL = 1,\n\n  /**\n   * @generated from enum value: RECORD_COMPARATOR_NOT_EQUAL = 2;\n   */\n  NOT_EQUAL = 2,\n\n  /**\n   * @generated from enum value: RECORD_COMPARATOR_CONTAINS = 3;\n   */\n  CONTAINS = 3,\n\n  /**\n   * @generated from enum value: RECORD_COMPARATOR_NOT_CONTAINS = 4;\n   */\n  NOT_CONTAINS = 4,\n}\n\n/**\n * Describes the enum openstatus.monitor.v1.RecordComparator.\n */\nexport const RecordComparatorSchema: GenEnum<RecordComparator> = /*@__PURE__*/\n  enumDesc(file_openstatus_monitor_v1_assertions, 2);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/monitor/v1/dns_monitor_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/monitor/v1/dns_monitor.proto (package openstatus.monitor.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { RecordAssertion } from \"./assertions_pb.ts\";\nimport { file_openstatus_monitor_v1_assertions } from \"./assertions_pb.ts\";\nimport type { OpenTelemetryConfig } from \"./http_monitor_pb.ts\";\nimport { file_openstatus_monitor_v1_http_monitor } from \"./http_monitor_pb.ts\";\nimport type { MonitorStatus, Periodicity, Region } from \"./monitor_pb.ts\";\nimport { file_openstatus_monitor_v1_monitor } from \"./monitor_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/monitor/v1/dns_monitor.proto.\n */\nexport const file_openstatus_monitor_v1_dns_monitor: GenFile = /*@__PURE__*/\n  fileDesc(\"CidvcGVuc3RhdHVzL21vbml0b3IvdjEvZG5zX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSLxBAoKRE5TTW9uaXRvchIKCgJpZBgBIAEoCRIzCgRuYW1lGAIgASgJQiW6Rxg6FhIURE5TIFJlc29sdXRpb24gQ2hlY2u6SAdyBRABGIACEikKA3VyaRgDIAEoCUIcukcPOg0SC2V4YW1wbGUuY29tukgHcgUQARiAEBJBCgtwZXJpb2RpY2l0eRgEIAEoDjIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5QZXJpb2RpY2l0eUIIukgFggECIAASHAoHdGltZW91dBgFIAEoA0ILukgIIgYYwKkHKAASJQoLZGVncmFkZWRfYXQYBiABKANCC7pICCIGGMCpBygASACIAQESGAoFcmV0cnkYByABKANCCbpIBiIEGAooABJLChFyZWNvcmRfYXNzZXJ0aW9ucxgIIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWNvcmRBc3NlcnRpb25CCLpIBZIBAhAKEh0KC2Rlc2NyaXB0aW9uGAkgASgJQgi6SAVyAxiACBIOCgZhY3RpdmUYCiABKAgSDgoGcHVibGljGAsgASgIEj8KB3JlZ2lvbnMYDCADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYDSABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYDiABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1c0IOCgxfZGVncmFkZWRfYXRCU1pRZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL21vbml0b3IvdjE7bW9uaXRvcnYxYgZwcm90bzM\", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]);\n\n/**\n * DNSMonitor defines the configuration for a DNS monitor.\n *\n * @generated from message openstatus.monitor.v1.DNSMonitor\n */\nexport type DNSMonitor = Message<\"openstatus.monitor.v1.DNSMonitor\"> & {\n  /**\n   * Unique identifier for the monitor (output only for create requests).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Name of the monitor (required, max 256 characters).\n   *\n   * @generated from field: string name = 2;\n   */\n  name: string;\n\n  /**\n   * Domain to resolve (required, max 2048 characters).\n   *\n   * @generated from field: string uri = 3;\n   */\n  uri: string;\n\n  /**\n   * Check periodicity (required).\n   *\n   * @generated from field: openstatus.monitor.v1.Periodicity periodicity = 4;\n   */\n  periodicity: Periodicity;\n\n  /**\n   * Timeout in milliseconds (0-120000, defaults to 45000).\n   *\n   * @generated from field: int64 timeout = 5;\n   */\n  timeout: bigint;\n\n  /**\n   * Latency threshold for degraded status in milliseconds (optional, 0-120000).\n   *\n   * @generated from field: optional int64 degraded_at = 6;\n   */\n  degradedAt?: bigint;\n\n  /**\n   * Number of retry attempts (0-10, defaults to 3).\n   *\n   * @generated from field: int64 retry = 7;\n   */\n  retry: bigint;\n\n  /**\n   * DNS record assertions for validation.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.RecordAssertion record_assertions = 8;\n   */\n  recordAssertions: RecordAssertion[];\n\n  /**\n   * Description of the monitor (optional).\n   *\n   * @generated from field: string description = 9;\n   */\n  description: string;\n\n  /**\n   * Whether the monitor is active (defaults to false).\n   *\n   * @generated from field: bool active = 10;\n   */\n  active: boolean;\n\n  /**\n   * Whether the monitor is publicly visible (defaults to false).\n   *\n   * @generated from field: bool public = 11;\n   */\n  public: boolean;\n\n  /**\n   * Geographic regions to run checks from.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.Region regions = 12;\n   */\n  regions: Region[];\n\n  /**\n   * OpenTelemetry configuration for exporting metrics.\n   *\n   * @generated from field: openstatus.monitor.v1.OpenTelemetryConfig open_telemetry = 13;\n   */\n  openTelemetry?: OpenTelemetryConfig;\n\n  /**\n   * Current operational status of the monitor.\n   *\n   * @generated from field: openstatus.monitor.v1.MonitorStatus status = 14;\n   */\n  status: MonitorStatus;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.DNSMonitor.\n * Use `create(DNSMonitorSchema)` to create a new message.\n */\nexport const DNSMonitorSchema: GenMessage<DNSMonitor> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_dns_monitor, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/monitor/v1/http_monitor_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/monitor/v1/http_monitor.proto (package openstatus.monitor.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { BodyAssertion, HeaderAssertion, StatusCodeAssertion } from \"./assertions_pb.ts\";\nimport { file_openstatus_monitor_v1_assertions } from \"./assertions_pb.ts\";\nimport type { MonitorStatus, Periodicity, Region } from \"./monitor_pb.ts\";\nimport { file_openstatus_monitor_v1_monitor } from \"./monitor_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/monitor/v1/http_monitor.proto.\n */\nexport const file_openstatus_monitor_v1_http_monitor: GenFile = /*@__PURE__*/\n  fileDesc(\"CihvcGVuc3RhdHVzL21vbml0b3IvdjEvaHR0cF9tb25pdG9yLnByb3RvEhVvcGVuc3RhdHVzLm1vbml0b3IudjEiWgoHSGVhZGVycxIoCgNrZXkYASABKAlCG7pHEToPEg1BdXRob3JpemF0aW9uukgEcgIQARIlCgV2YWx1ZRgCIAEoCUIWukcTOhESD0JlYXJlciB0b2tlbjEyMyJsChNPcGVuVGVsZW1ldHJ5Q29uZmlnEhoKCGVuZHBvaW50GAEgASgJQgi6SAVyAxiAEBI5CgdoZWFkZXJzGAIgAygLMh4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhlYWRlcnNCCLpIBZIBAhAUIoEICgtIVFRQTW9uaXRvchIKCgJpZBgBIAEoCRI6CgRuYW1lGAIgASgJQiy6Rx86HRIbUHJvZHVjdGlvbiBBUEkgSGVhbHRoIENoZWNrukgHcgUQARiAAhI/CgN1cmwYAyABKAlCMrpHIjogEh5odHRwczovL2FwaS5leGFtcGxlLmNvbS9oZWFsdGi6SApyCBABGIAQiAEBEkEKC3BlcmlvZGljaXR5GAQgASgOMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLlBlcmlvZGljaXR5Qgi6SAWCAQIgABI7CgZtZXRob2QYBSABKA4yIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuSFRUUE1ldGhvZEIIukgFggECIAASJQoEYm9keRgGIAEoCUIXukcUOhISEHsia2V5IjogInZhbHVlIn0SHAoHdGltZW91dBgHIAEoA0ILukgIIgYYwKkHKAASJQoLZGVncmFkZWRfYXQYCCABKANCC7pICCIGGMCpBygASACIAQESGAoFcmV0cnkYCSABKANCCbpIBiIEGAooABIdChBmb2xsb3dfcmVkaXJlY3RzGAogASgISAGIAQESOQoHaGVhZGVycxgLIAMoCzIeLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJzQgi6SAWSAQIQFBJUChZzdGF0dXNfY29kZV9hc3NlcnRpb25zGAwgAygLMioub3BlbnN0YXR1cy5tb25pdG9yLnYxLlN0YXR1c0NvZGVBc3NlcnRpb25CCLpIBZIBAhAKEkcKD2JvZHlfYXNzZXJ0aW9ucxgNIAMoCzIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Cb2R5QXNzZXJ0aW9uQgi6SAWSAQIQChJLChFoZWFkZXJfYXNzZXJ0aW9ucxgOIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJBc3NlcnRpb25CCLpIBZIBAhAKEh0KC2Rlc2NyaXB0aW9uGA8gASgJQgi6SAVyAxiACBIOCgZhY3RpdmUYECABKAgSDgoGcHVibGljGBEgASgIEj8KB3JlZ2lvbnMYEiADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYEyABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYFCABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1c0IOCgxfZGVncmFkZWRfYXRCEwoRX2ZvbGxvd19yZWRpcmVjdHMq9wEKCkhUVFBNZXRob2QSGwoXSFRUUF9NRVRIT0RfVU5TUEVDSUZJRUQQABITCg9IVFRQX01FVEhPRF9HRVQQARIUChBIVFRQX01FVEhPRF9QT1NUEAISFAoQSFRUUF9NRVRIT0RfSEVBRBADEhMKD0hUVFBfTUVUSE9EX1BVVBAEEhUKEUhUVFBfTUVUSE9EX1BBVENIEAUSFgoSSFRUUF9NRVRIT0RfREVMRVRFEAYSFQoRSFRUUF9NRVRIT0RfVFJBQ0UQBxIXChNIVFRQX01FVEhPRF9DT05ORUNUEAgSFwoTSFRUUF9NRVRIT0RfT1BUSU9OUxAJQlNaUWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tb25pdG9yL3YxO21vbml0b3J2MWIGcHJvdG8z\", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_monitor]);\n\n/**\n * Headers represents a key-value pair for HTTP headers.\n *\n * @generated from message openstatus.monitor.v1.Headers\n */\nexport type Headers = Message<\"openstatus.monitor.v1.Headers\"> & {\n  /**\n   * Header name.\n   *\n   * @generated from field: string key = 1;\n   */\n  key: string;\n\n  /**\n   * Header value.\n   *\n   * @generated from field: string value = 2;\n   */\n  value: string;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.Headers.\n * Use `create(HeadersSchema)` to create a new message.\n */\nexport const HeadersSchema: GenMessage<Headers> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_http_monitor, 0);\n\n/**\n * OpenTelemetry configuration for exporting metrics.\n *\n * @generated from message openstatus.monitor.v1.OpenTelemetryConfig\n */\nexport type OpenTelemetryConfig = Message<\"openstatus.monitor.v1.OpenTelemetryConfig\"> & {\n  /**\n   * OTEL endpoint URL.\n   *\n   * @generated from field: string endpoint = 1;\n   */\n  endpoint: string;\n\n  /**\n   * Custom headers for OTEL requests.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.Headers headers = 2;\n   */\n  headers: Headers[];\n};\n\n/**\n * Describes the message openstatus.monitor.v1.OpenTelemetryConfig.\n * Use `create(OpenTelemetryConfigSchema)` to create a new message.\n */\nexport const OpenTelemetryConfigSchema: GenMessage<OpenTelemetryConfig> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_http_monitor, 1);\n\n/**\n * HTTPMonitor defines the configuration for an HTTP monitor.\n *\n * @generated from message openstatus.monitor.v1.HTTPMonitor\n */\nexport type HTTPMonitor = Message<\"openstatus.monitor.v1.HTTPMonitor\"> & {\n  /**\n   * Unique identifier for the monitor (output only for create requests).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Name of the monitor (required, max 256 characters).\n   *\n   * @generated from field: string name = 2;\n   */\n  name: string;\n\n  /**\n   * URL to monitor (required, max 2048 characters).\n   *\n   * @generated from field: string url = 3;\n   */\n  url: string;\n\n  /**\n   * Check periodicity (required).\n   *\n   * @generated from field: openstatus.monitor.v1.Periodicity periodicity = 4;\n   */\n  periodicity: Periodicity;\n\n  /**\n   * HTTP method to use (defaults to GET).\n   *\n   * @generated from field: openstatus.monitor.v1.HTTPMethod method = 5;\n   */\n  method: HTTPMethod;\n\n  /**\n   * Request body (optional).\n   *\n   * @generated from field: string body = 6;\n   */\n  body: string;\n\n  /**\n   * Timeout in milliseconds (0-120000, defaults to 45000).\n   *\n   * @generated from field: int64 timeout = 7;\n   */\n  timeout: bigint;\n\n  /**\n   * Latency threshold for degraded status in milliseconds (optional, 0-120000).\n   *\n   * @generated from field: optional int64 degraded_at = 8;\n   */\n  degradedAt?: bigint;\n\n  /**\n   * Number of retry attempts (0-10, defaults to 3).\n   *\n   * @generated from field: int64 retry = 9;\n   */\n  retry: bigint;\n\n  /**\n   * Whether to follow HTTP redirects (defaults to true when not specified).\n   *\n   * @generated from field: optional bool follow_redirects = 10;\n   */\n  followRedirects?: boolean;\n\n  /**\n   * Custom headers for the request.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.Headers headers = 11;\n   */\n  headers: Headers[];\n\n  /**\n   * Status code assertions for the response.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.StatusCodeAssertion status_code_assertions = 12;\n   */\n  statusCodeAssertions: StatusCodeAssertion[];\n\n  /**\n   * Body content assertions for the response.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.BodyAssertion body_assertions = 13;\n   */\n  bodyAssertions: BodyAssertion[];\n\n  /**\n   * Header assertions for the response.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.HeaderAssertion header_assertions = 14;\n   */\n  headerAssertions: HeaderAssertion[];\n\n  /**\n   * Description of the monitor (optional).\n   *\n   * @generated from field: string description = 15;\n   */\n  description: string;\n\n  /**\n   * Whether the monitor is active (defaults to false).\n   *\n   * @generated from field: bool active = 16;\n   */\n  active: boolean;\n\n  /**\n   * Whether the monitor is publicly visible (defaults to false).\n   *\n   * @generated from field: bool public = 17;\n   */\n  public: boolean;\n\n  /**\n   * Geographic regions to run checks from.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.Region regions = 18;\n   */\n  regions: Region[];\n\n  /**\n   * OpenTelemetry configuration for exporting metrics.\n   *\n   * @generated from field: openstatus.monitor.v1.OpenTelemetryConfig open_telemetry = 19;\n   */\n  openTelemetry?: OpenTelemetryConfig;\n\n  /**\n   * Current operational status of the monitor.\n   *\n   * @generated from field: openstatus.monitor.v1.MonitorStatus status = 20;\n   */\n  status: MonitorStatus;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.HTTPMonitor.\n * Use `create(HTTPMonitorSchema)` to create a new message.\n */\nexport const HTTPMonitorSchema: GenMessage<HTTPMonitor> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_http_monitor, 2);\n\n/**\n * HTTP methods supported for monitors.\n *\n * @generated from enum openstatus.monitor.v1.HTTPMethod\n */\nexport enum HTTPMethod {\n  /**\n   * @generated from enum value: HTTP_METHOD_UNSPECIFIED = 0;\n   */\n  HTTP_METHOD_UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: HTTP_METHOD_GET = 1;\n   */\n  HTTP_METHOD_GET = 1,\n\n  /**\n   * @generated from enum value: HTTP_METHOD_POST = 2;\n   */\n  HTTP_METHOD_POST = 2,\n\n  /**\n   * @generated from enum value: HTTP_METHOD_HEAD = 3;\n   */\n  HTTP_METHOD_HEAD = 3,\n\n  /**\n   * @generated from enum value: HTTP_METHOD_PUT = 4;\n   */\n  HTTP_METHOD_PUT = 4,\n\n  /**\n   * @generated from enum value: HTTP_METHOD_PATCH = 5;\n   */\n  HTTP_METHOD_PATCH = 5,\n\n  /**\n   * @generated from enum value: HTTP_METHOD_DELETE = 6;\n   */\n  HTTP_METHOD_DELETE = 6,\n\n  /**\n   * @generated from enum value: HTTP_METHOD_TRACE = 7;\n   */\n  HTTP_METHOD_TRACE = 7,\n\n  /**\n   * @generated from enum value: HTTP_METHOD_CONNECT = 8;\n   */\n  HTTP_METHOD_CONNECT = 8,\n\n  /**\n   * @generated from enum value: HTTP_METHOD_OPTIONS = 9;\n   */\n  HTTP_METHOD_OPTIONS = 9,\n}\n\n/**\n * Describes the enum openstatus.monitor.v1.HTTPMethod.\n */\nexport const HTTPMethodSchema: GenEnum<HTTPMethod> = /*@__PURE__*/\n  enumDesc(file_openstatus_monitor_v1_http_monitor, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/monitor/v1/index.ts",
    "content": "// Monitor service exports\nexport * from \"./assertions_pb.js\";\nexport * from \"./dns_monitor_pb.js\";\nexport * from \"./http_monitor_pb.js\";\nexport * from \"./tcp_monitor_pb.js\";\nexport * from \"./monitor_pb.js\";\nexport * from \"./service_pb.js\";\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/monitor/v1/monitor_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/monitor/v1/monitor.proto (package openstatus.monitor.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc } from \"@bufbuild/protobuf/codegenv2\";\n\n/**\n * Describes the file openstatus/monitor/v1/monitor.proto.\n */\nexport const file_openstatus_monitor_v1_monitor: GenFile = /*@__PURE__*/\n  fileDesc(\"CiNvcGVuc3RhdHVzL21vbml0b3IvdjEvbW9uaXRvci5wcm90bxIVb3BlbnN0YXR1cy5tb25pdG9yLnYxKoEBCg1Nb25pdG9yU3RhdHVzEh4KGk1PTklUT1JfU1RBVFVTX1VOU1BFQ0lGSUVEEAASGQoVTU9OSVRPUl9TVEFUVVNfQUNUSVZFEAESGwoXTU9OSVRPUl9TVEFUVVNfREVHUkFERUQQAhIYChRNT05JVE9SX1NUQVRVU19FUlJPUhADKqUBCgtQZXJpb2RpY2l0eRIbChdQRVJJT0RJQ0lUWV9VTlNQRUNJRklFRBAAEhMKD1BFUklPRElDSVRZXzMwUxABEhIKDlBFUklPRElDSVRZXzFNEAISEgoOUEVSSU9ESUNJVFlfNU0QAxITCg9QRVJJT0RJQ0lUWV8xME0QBBITCg9QRVJJT0RJQ0lUWV8zME0QBRISCg5QRVJJT0RJQ0lUWV8xSBAGKosFCgZSZWdpb24SFgoSUkVHSU9OX1VOU1BFQ0lGSUVEEAASEgoOUkVHSU9OX0ZMWV9BTVMQARISCg5SRUdJT05fRkxZX0FSThACEhIKDlJFR0lPTl9GTFlfQk9NEAMSEgoOUkVHSU9OX0ZMWV9DREcQBBISCg5SRUdJT05fRkxZX0RGVxAFEhIKDlJFR0lPTl9GTFlfRVdSEAYSEgoOUkVHSU9OX0ZMWV9GUkEQBxISCg5SRUdJT05fRkxZX0dSVRAIEhIKDlJFR0lPTl9GTFlfSUFEEAkSEgoOUkVHSU9OX0ZMWV9KTkIQChISCg5SRUdJT05fRkxZX0xBWBALEhIKDlJFR0lPTl9GTFlfTEhSEAwSEgoOUkVHSU9OX0ZMWV9OUlQQDRISCg5SRUdJT05fRkxZX09SRBAOEhIKDlJFR0lPTl9GTFlfU0pDEA8SEgoOUkVHSU9OX0ZMWV9TSU4QEBISCg5SRUdJT05fRkxZX1NZRBAREhIKDlJFR0lPTl9GTFlfWVlaEBISFAoQUkVHSU9OX0tPWUVCX0ZSQRATEhQKEFJFR0lPTl9LT1lFQl9QQVIQFBIUChBSRUdJT05fS09ZRUJfU0ZPEBUSFAoQUkVHSU9OX0tPWUVCX1NJThAWEhQKEFJFR0lPTl9LT1lFQl9UWU8QFxIUChBSRUdJT05fS09ZRUJfV0FTEBgSGwoXUkVHSU9OX1JBSUxXQVlfVVNfV0VTVDIQGRIbChdSRUdJT05fUkFJTFdBWV9VU19FQVNUNBAaEh8KG1JFR0lPTl9SQUlMV0FZX0VVUk9QRV9XRVNUNBAbEiIKHlJFR0lPTl9SQUlMV0FZX0FTSUFfU09VVEhFQVNUMRAcQlNaUWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tb25pdG9yL3YxO21vbml0b3J2MWIGcHJvdG8z\");\n\n/**\n * MonitorStatus represents the operational status of a monitor.\n *\n * @generated from enum openstatus.monitor.v1.MonitorStatus\n */\nexport enum MonitorStatus {\n  /**\n   * MONITOR_STATUS_UNSPECIFIED indicates an unknown status.\n   *\n   * @generated from enum value: MONITOR_STATUS_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * MONITOR_STATUS_ACTIVE indicates the monitor is actively checking.\n   *\n   * @generated from enum value: MONITOR_STATUS_ACTIVE = 1;\n   */\n  ACTIVE = 1,\n\n  /**\n   * MONITOR_STATUS_DEGRADED indicates the monitor is degraded.\n   *\n   * @generated from enum value: MONITOR_STATUS_DEGRADED = 2;\n   */\n  DEGRADED = 2,\n\n  /**\n   * MONITOR_STATUS_ERROR indicates the monitor is in an error state.\n   *\n   * @generated from enum value: MONITOR_STATUS_ERROR = 3;\n   */\n  ERROR = 3,\n}\n\n/**\n * Describes the enum openstatus.monitor.v1.MonitorStatus.\n */\nexport const MonitorStatusSchema: GenEnum<MonitorStatus> = /*@__PURE__*/\n  enumDesc(file_openstatus_monitor_v1_monitor, 0);\n\n/**\n * Monitor periodicity options.\n *\n * @generated from enum openstatus.monitor.v1.Periodicity\n */\nexport enum Periodicity {\n  /**\n   * @generated from enum value: PERIODICITY_UNSPECIFIED = 0;\n   */\n  PERIODICITY_UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: PERIODICITY_30S = 1;\n   */\n  PERIODICITY_30S = 1,\n\n  /**\n   * @generated from enum value: PERIODICITY_1M = 2;\n   */\n  PERIODICITY_1M = 2,\n\n  /**\n   * @generated from enum value: PERIODICITY_5M = 3;\n   */\n  PERIODICITY_5M = 3,\n\n  /**\n   * @generated from enum value: PERIODICITY_10M = 4;\n   */\n  PERIODICITY_10M = 4,\n\n  /**\n   * @generated from enum value: PERIODICITY_30M = 5;\n   */\n  PERIODICITY_30M = 5,\n\n  /**\n   * @generated from enum value: PERIODICITY_1H = 6;\n   */\n  PERIODICITY_1H = 6,\n}\n\n/**\n * Describes the enum openstatus.monitor.v1.Periodicity.\n */\nexport const PeriodicitySchema: GenEnum<Periodicity> = /*@__PURE__*/\n  enumDesc(file_openstatus_monitor_v1_monitor, 1);\n\n/**\n * Geographic regions where monitors can run checks from.\n *\n * @generated from enum openstatus.monitor.v1.Region\n */\nexport enum Region {\n  /**\n   * @generated from enum value: REGION_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * Fly.io regions\n   *\n   * Amsterdam, Netherlands\n   *\n   * @generated from enum value: REGION_FLY_AMS = 1;\n   */\n  FLY_AMS = 1,\n\n  /**\n   * Stockholm, Sweden\n   *\n   * @generated from enum value: REGION_FLY_ARN = 2;\n   */\n  FLY_ARN = 2,\n\n  /**\n   * Mumbai, India\n   *\n   * @generated from enum value: REGION_FLY_BOM = 3;\n   */\n  FLY_BOM = 3,\n\n  /**\n   * Paris, France\n   *\n   * @generated from enum value: REGION_FLY_CDG = 4;\n   */\n  FLY_CDG = 4,\n\n  /**\n   * Dallas, USA\n   *\n   * @generated from enum value: REGION_FLY_DFW = 5;\n   */\n  FLY_DFW = 5,\n\n  /**\n   * Newark, USA\n   *\n   * @generated from enum value: REGION_FLY_EWR = 6;\n   */\n  FLY_EWR = 6,\n\n  /**\n   * Frankfurt, Germany\n   *\n   * @generated from enum value: REGION_FLY_FRA = 7;\n   */\n  FLY_FRA = 7,\n\n  /**\n   * São Paulo, Brazil\n   *\n   * @generated from enum value: REGION_FLY_GRU = 8;\n   */\n  FLY_GRU = 8,\n\n  /**\n   * Ashburn, USA\n   *\n   * @generated from enum value: REGION_FLY_IAD = 9;\n   */\n  FLY_IAD = 9,\n\n  /**\n   * Johannesburg, South Africa\n   *\n   * @generated from enum value: REGION_FLY_JNB = 10;\n   */\n  FLY_JNB = 10,\n\n  /**\n   * Los Angeles, USA\n   *\n   * @generated from enum value: REGION_FLY_LAX = 11;\n   */\n  FLY_LAX = 11,\n\n  /**\n   * London, UK\n   *\n   * @generated from enum value: REGION_FLY_LHR = 12;\n   */\n  FLY_LHR = 12,\n\n  /**\n   * Tokyo, Japan\n   *\n   * @generated from enum value: REGION_FLY_NRT = 13;\n   */\n  FLY_NRT = 13,\n\n  /**\n   * Chicago, USA\n   *\n   * @generated from enum value: REGION_FLY_ORD = 14;\n   */\n  FLY_ORD = 14,\n\n  /**\n   * San Jose, USA\n   *\n   * @generated from enum value: REGION_FLY_SJC = 15;\n   */\n  FLY_SJC = 15,\n\n  /**\n   * Singapore\n   *\n   * @generated from enum value: REGION_FLY_SIN = 16;\n   */\n  FLY_SIN = 16,\n\n  /**\n   * Sydney, Australia\n   *\n   * @generated from enum value: REGION_FLY_SYD = 17;\n   */\n  FLY_SYD = 17,\n\n  /**\n   * Toronto, Canada\n   *\n   * @generated from enum value: REGION_FLY_YYZ = 18;\n   */\n  FLY_YYZ = 18,\n\n  /**\n   * Koyeb regions\n   *\n   * Koyeb Frankfurt\n   *\n   * @generated from enum value: REGION_KOYEB_FRA = 19;\n   */\n  KOYEB_FRA = 19,\n\n  /**\n   * Koyeb Paris\n   *\n   * @generated from enum value: REGION_KOYEB_PAR = 20;\n   */\n  KOYEB_PAR = 20,\n\n  /**\n   * Koyeb San Francisco\n   *\n   * @generated from enum value: REGION_KOYEB_SFO = 21;\n   */\n  KOYEB_SFO = 21,\n\n  /**\n   * Koyeb Singapore\n   *\n   * @generated from enum value: REGION_KOYEB_SIN = 22;\n   */\n  KOYEB_SIN = 22,\n\n  /**\n   * Koyeb Tokyo\n   *\n   * @generated from enum value: REGION_KOYEB_TYO = 23;\n   */\n  KOYEB_TYO = 23,\n\n  /**\n   * Koyeb Washington\n   *\n   * @generated from enum value: REGION_KOYEB_WAS = 24;\n   */\n  KOYEB_WAS = 24,\n\n  /**\n   * Railway regions\n   *\n   * Railway US West\n   *\n   * @generated from enum value: REGION_RAILWAY_US_WEST2 = 25;\n   */\n  RAILWAY_US_WEST2 = 25,\n\n  /**\n   * Railway US East\n   *\n   * @generated from enum value: REGION_RAILWAY_US_EAST4 = 26;\n   */\n  RAILWAY_US_EAST4 = 26,\n\n  /**\n   * Railway Europe West\n   *\n   * @generated from enum value: REGION_RAILWAY_EUROPE_WEST4 = 27;\n   */\n  RAILWAY_EUROPE_WEST4 = 27,\n\n  /**\n   * Railway Asia Southeast\n   *\n   * @generated from enum value: REGION_RAILWAY_ASIA_SOUTHEAST1 = 28;\n   */\n  RAILWAY_ASIA_SOUTHEAST1 = 28,\n}\n\n/**\n * Describes the enum openstatus.monitor.v1.Region.\n */\nexport const RegionSchema: GenEnum<Region> = /*@__PURE__*/\n  enumDesc(file_openstatus_monitor_v1_monitor, 2);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/monitor/v1/service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/monitor/v1/service.proto (package openstatus.monitor.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { DNSMonitor } from \"./dns_monitor_pb.ts\";\nimport { file_openstatus_monitor_v1_dns_monitor } from \"./dns_monitor_pb.ts\";\nimport type { HTTPMonitor } from \"./http_monitor_pb.ts\";\nimport { file_openstatus_monitor_v1_http_monitor } from \"./http_monitor_pb.ts\";\nimport type { MonitorStatus, Region } from \"./monitor_pb.ts\";\nimport { file_openstatus_monitor_v1_monitor } from \"./monitor_pb.ts\";\nimport type { TCPMonitor } from \"./tcp_monitor_pb.ts\";\nimport { file_openstatus_monitor_v1_tcp_monitor } from \"./tcp_monitor_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/monitor/v1/service.proto.\n */\nexport const file_openstatus_monitor_v1_service: GenFile = /*@__PURE__*/\n  fileDesc(\"CiNvcGVuc3RhdHVzL21vbml0b3IvdjEvc2VydmljZS5wcm90bxIVb3BlbnN0YXR1cy5tb25pdG9yLnYxIlcKGENyZWF0ZUhUVFBNb25pdG9yUmVxdWVzdBI7Cgdtb25pdG9yGAEgASgLMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhUVFBNb25pdG9yQga6SAPIAQEiUAoZQ3JlYXRlSFRUUE1vbml0b3JSZXNwb25zZRIzCgdtb25pdG9yGAEgASgLMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhUVFBNb25pdG9yIlUKF0NyZWF0ZVRDUE1vbml0b3JSZXF1ZXN0EjoKB21vbml0b3IYASABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuVENQTW9uaXRvckIGukgDyAEBIk4KGENyZWF0ZVRDUE1vbml0b3JSZXNwb25zZRIyCgdtb25pdG9yGAEgASgLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRDUE1vbml0b3IiVQoXQ3JlYXRlRE5TTW9uaXRvclJlcXVlc3QSOgoHbW9uaXRvchgBIAEoCzIhLm9wZW5zdGF0dXMubW9uaXRvci52MS5ETlNNb25pdG9yQga6SAPIAQEiTgoYQ3JlYXRlRE5TTW9uaXRvclJlc3BvbnNlEjIKB21vbml0b3IYASABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuRE5TTW9uaXRvciJ1ChhVcGRhdGVIVFRQTW9uaXRvclJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAESOAoHbW9uaXRvchgCIAEoCzIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5IVFRQTW9uaXRvckgAiAEBQgoKCF9tb25pdG9yIlAKGVVwZGF0ZUhUVFBNb25pdG9yUmVzcG9uc2USMwoHbW9uaXRvchgBIAEoCzIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5IVFRQTW9uaXRvciJzChdVcGRhdGVUQ1BNb25pdG9yUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQARI3Cgdtb25pdG9yGAIgASgLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRDUE1vbml0b3JIAIgBAUIKCghfbW9uaXRvciJOChhVcGRhdGVUQ1BNb25pdG9yUmVzcG9uc2USMgoHbW9uaXRvchgBIAEoCzIhLm9wZW5zdGF0dXMubW9uaXRvci52MS5UQ1BNb25pdG9yInMKF1VwZGF0ZUROU01vbml0b3JSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABEjcKB21vbml0b3IYAiABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuRE5TTW9uaXRvckgAiAEBQgoKCF9tb25pdG9yIk4KGFVwZGF0ZUROU01vbml0b3JSZXNwb25zZRIyCgdtb25pdG9yGAEgASgLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLkROU01vbml0b3IiLAoVVHJpZ2dlck1vbml0b3JSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIikKFlRyaWdnZXJNb25pdG9yUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCIrChREZWxldGVNb25pdG9yUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQASIoChVEZWxldGVNb25pdG9yUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCJnChNMaXN0TW9uaXRvcnNSZXF1ZXN0Eh0KBWxpbWl0GAEgASgFQgm6SAYaBBhkKAFIAIgBARIcCgZvZmZzZXQYAiABKAVCB7pIBBoCKABIAYgBAUIICgZfbGltaXRCCQoHX29mZnNldCLXAQoUTGlzdE1vbml0b3JzUmVzcG9uc2USOQoNaHR0cF9tb25pdG9ycxgBIAMoCzIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5IVFRQTW9uaXRvchI3Cgx0Y3BfbW9uaXRvcnMYAiADKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuVENQTW9uaXRvchI3CgxkbnNfbW9uaXRvcnMYAyADKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuRE5TTW9uaXRvchISCgp0b3RhbF9zaXplGAQgASgFIi4KF0dldE1vbml0b3JTdGF0dXNSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABInMKDFJlZ2lvblN0YXR1cxItCgZyZWdpb24YASABKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uEjQKBnN0YXR1cxgCIAEoDjIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Nb25pdG9yU3RhdHVzIlwKGEdldE1vbml0b3JTdGF0dXNSZXNwb25zZRIKCgJpZBgBIAEoCRI0CgdyZWdpb25zGAIgAygLMiMub3BlbnN0YXR1cy5tb25pdG9yLnYxLlJlZ2lvblN0YXR1cyKxAQoNTW9uaXRvckNvbmZpZxIyCgRodHRwGAEgASgLMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhUVFBNb25pdG9ySAASMAoDdGNwGAIgASgLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRDUE1vbml0b3JIABIwCgNkbnMYAyABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuRE5TTW9uaXRvckgAQggKBmNvbmZpZyKfAQoYR2V0TW9uaXRvclN1bW1hcnlSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABEjQKCnRpbWVfcmFuZ2UYAiABKA4yIC5vcGVuc3RhdHVzLm1vbml0b3IudjEuVGltZVJhbmdlEjgKB3JlZ2lvbnMYAyADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQgi6SAWSAQIQHCKsAgoZR2V0TW9uaXRvclN1bW1hcnlSZXNwb25zZRIKCgJpZBgBIAEoCRIUCgxsYXN0X3BpbmdfYXQYAiABKAkSGAoQdG90YWxfc3VjY2Vzc2Z1bBgDIAEoAxIWCg50b3RhbF9kZWdyYWRlZBgEIAEoAxIUCgx0b3RhbF9mYWlsZWQYBSABKAMSCwoDcDUwGAYgASgDEgsKA3A3NRgHIAEoAxILCgNwOTAYCCABKAMSCwoDcDk1GAkgASgDEgsKA3A5ORgKIAEoAxI0Cgp0aW1lX3JhbmdlGAsgASgOMiAub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRpbWVSYW5nZRIuCgdyZWdpb25zGAwgAygOMh0ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlJlZ2lvbiIoChFHZXRNb25pdG9yUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQASJLChJHZXRNb25pdG9yUmVzcG9uc2USNQoHbW9uaXRvchgBIAEoCzIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Nb25pdG9yQ29uZmlnKmEKCVRpbWVSYW5nZRIaChZUSU1FX1JBTkdFX1VOU1BFQ0lGSUVEEAASEQoNVElNRV9SQU5HRV8xRBABEhEKDVRJTUVfUkFOR0VfN0QQAhISCg5USU1FX1JBTkdFXzE0RBADMukRCg5Nb25pdG9yU2VydmljZRK5AwoRQ3JlYXRlSFRUUE1vbml0b3ISLy5vcGVuc3RhdHVzLm1vbml0b3IudjEuQ3JlYXRlSFRUUE1vbml0b3JSZXF1ZXN0GjAub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZUhUVFBNb25pdG9yUmVzcG9uc2UiwAK6R7wCGrkCQ3JlYXRlcyBhIG5ldyBIVFRQIG1vbml0b3IgaW4gdGhlIGF1dGhlbnRpY2F0ZWQgd29ya3NwYWNlLiBDb25maWd1cmUgdGhlIHRhcmdldCBVUkwsIEhUVFAgbWV0aG9kLCByZXF1ZXN0IGhlYWRlcnMgYW5kIGJvZHksIHJlc3BvbnNlIGFzc2VydGlvbnMgKHN0YXR1cyBjb2RlLCBib2R5IGNvbnRlbnQsIGhlYWRlcnMpLCBjaGVjayBwZXJpb2RpY2l0eSwgZ2VvZ3JhcGhpYyByZWdpb25zLCBhbmQgb3B0aW9uYWwgT3BlblRlbGVtZXRyeSBleHBvcnQuIFRoZSBtb25pdG9yIHN0YXJ0cyBjaGVja2luZyBpbW1lZGlhdGVseSBpZiBzZXQgdG8gYWN0aXZlLhJzChBDcmVhdGVUQ1BNb25pdG9yEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZVRDUE1vbml0b3JSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZVRDUE1vbml0b3JSZXNwb25zZRJzChBDcmVhdGVETlNNb25pdG9yEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZUROU01vbml0b3JSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZUROU01vbml0b3JSZXNwb25zZRJ2ChFVcGRhdGVIVFRQTW9uaXRvchIvLm9wZW5zdGF0dXMubW9uaXRvci52MS5VcGRhdGVIVFRQTW9uaXRvclJlcXVlc3QaMC5vcGVuc3RhdHVzLm1vbml0b3IudjEuVXBkYXRlSFRUUE1vbml0b3JSZXNwb25zZRJzChBVcGRhdGVUQ1BNb25pdG9yEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlVwZGF0ZVRDUE1vbml0b3JSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlVwZGF0ZVRDUE1vbml0b3JSZXNwb25zZRJzChBVcGRhdGVETlNNb25pdG9yEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlVwZGF0ZUROU01vbml0b3JSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlVwZGF0ZUROU01vbml0b3JSZXNwb25zZRLpAgoOVHJpZ2dlck1vbml0b3ISLC5vcGVuc3RhdHVzLm1vbml0b3IudjEuVHJpZ2dlck1vbml0b3JSZXF1ZXN0Gi0ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRyaWdnZXJNb25pdG9yUmVzcG9uc2Ui+QG6R/UBGvIBTWFudWFsbHkgdHJpZ2dlcnMgYW4gaW1tZWRpYXRlIGNoZWNrIGZvciB0aGUgc3BlY2lmaWVkIG1vbml0b3IgYWNyb3NzIGFsbCBjb25maWd1cmVkIHJlZ2lvbnMuIFRoaXMgb3BlcmF0aW9uIGlzIHJhdGUtbGltaXRlZCB1bmRlciB0aGUgc3ludGhldGljLWNoZWNrcyBxdW90YS4gQSBtb25pdG9yIHJ1biByZWNvcmQgaXMgY3JlYXRlZCBhbmQgdGhlIGNoZWNrIGlzIGRpc3BhdGNoZWQgdG8gdGhlIGNoZWNrZXIgc2VydmljZS4SagoNRGVsZXRlTW9uaXRvchIrLm9wZW5zdGF0dXMubW9uaXRvci52MS5EZWxldGVNb25pdG9yUmVxdWVzdBosLm9wZW5zdGF0dXMubW9uaXRvci52MS5EZWxldGVNb25pdG9yUmVzcG9uc2USbAoMTGlzdE1vbml0b3JzEioub3BlbnN0YXR1cy5tb25pdG9yLnYxLkxpc3RNb25pdG9yc1JlcXVlc3QaKy5vcGVuc3RhdHVzLm1vbml0b3IudjEuTGlzdE1vbml0b3JzUmVzcG9uc2UiA5ACARJ4ChBHZXRNb25pdG9yU3RhdHVzEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkdldE1vbml0b3JTdGF0dXNSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkdldE1vbml0b3JTdGF0dXNSZXNwb25zZSIDkAIBEqYDChFHZXRNb25pdG9yU3VtbWFyeRIvLm9wZW5zdGF0dXMubW9uaXRvci52MS5HZXRNb25pdG9yU3VtbWFyeVJlcXVlc3QaMC5vcGVuc3RhdHVzLm1vbml0b3IudjEuR2V0TW9uaXRvclN1bW1hcnlSZXNwb25zZSKtApACAbpHpgIaowJSZXR1cm5zIGFnZ3JlZ2F0ZWQgbWV0cmljcyBmb3IgYSBtb25pdG9yIGluY2x1ZGluZyBsYXRlbmN5IHBlcmNlbnRpbGVzIChwNTAsIHA3NSwgcDkwLCBwOTUsIHA5OSksIHJlcXVlc3QgY291bnRzIGJ5IHN0YXR1cyAoc3VjY2Vzc2Z1bCwgZGVncmFkZWQsIGZhaWxlZCksIGFuZCB0aGUgdGltZXN0YW1wIG9mIHRoZSBsYXN0IGNoZWNrLiBNZXRyaWNzIGNhbiBiZSBzY29wZWQgdG8gYSB0aW1lIHJhbmdlICgxIGRheSwgNyBkYXlzLCBvciAxNCBkYXlzKSBhbmQgZmlsdGVyZWQgYnkgc3BlY2lmaWMgcmVnaW9ucy4SZgoKR2V0TW9uaXRvchIoLm9wZW5zdGF0dXMubW9uaXRvci52MS5HZXRNb25pdG9yUmVxdWVzdBopLm9wZW5zdGF0dXMubW9uaXRvci52MS5HZXRNb25pdG9yUmVzcG9uc2UiA5ACAUJTWlFnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvbW9uaXRvci92MTttb25pdG9ydjFiBnByb3RvMw\", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_dns_monitor, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor, file_openstatus_monitor_v1_tcp_monitor]);\n\n/**\n * CreateHTTPMonitorRequest is the request to create a new HTTP monitor.\n *\n * @generated from message openstatus.monitor.v1.CreateHTTPMonitorRequest\n */\nexport type CreateHTTPMonitorRequest = Message<\"openstatus.monitor.v1.CreateHTTPMonitorRequest\"> & {\n  /**\n   * Monitor configuration (required).\n   *\n   * @generated from field: openstatus.monitor.v1.HTTPMonitor monitor = 1;\n   */\n  monitor?: HTTPMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.CreateHTTPMonitorRequest.\n * Use `create(CreateHTTPMonitorRequestSchema)` to create a new message.\n */\nexport const CreateHTTPMonitorRequestSchema: GenMessage<CreateHTTPMonitorRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 0);\n\n/**\n * CreateHTTPMonitorResponse is the response after creating an HTTP monitor.\n *\n * @generated from message openstatus.monitor.v1.CreateHTTPMonitorResponse\n */\nexport type CreateHTTPMonitorResponse = Message<\"openstatus.monitor.v1.CreateHTTPMonitorResponse\"> & {\n  /**\n   * The created monitor with assigned ID.\n   *\n   * @generated from field: openstatus.monitor.v1.HTTPMonitor monitor = 1;\n   */\n  monitor?: HTTPMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.CreateHTTPMonitorResponse.\n * Use `create(CreateHTTPMonitorResponseSchema)` to create a new message.\n */\nexport const CreateHTTPMonitorResponseSchema: GenMessage<CreateHTTPMonitorResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 1);\n\n/**\n * CreateTCPMonitorRequest is the request to create a new TCP monitor.\n *\n * @generated from message openstatus.monitor.v1.CreateTCPMonitorRequest\n */\nexport type CreateTCPMonitorRequest = Message<\"openstatus.monitor.v1.CreateTCPMonitorRequest\"> & {\n  /**\n   * Monitor configuration (required).\n   *\n   * @generated from field: openstatus.monitor.v1.TCPMonitor monitor = 1;\n   */\n  monitor?: TCPMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.CreateTCPMonitorRequest.\n * Use `create(CreateTCPMonitorRequestSchema)` to create a new message.\n */\nexport const CreateTCPMonitorRequestSchema: GenMessage<CreateTCPMonitorRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 2);\n\n/**\n * CreateTCPMonitorResponse is the response after creating a TCP monitor.\n *\n * @generated from message openstatus.monitor.v1.CreateTCPMonitorResponse\n */\nexport type CreateTCPMonitorResponse = Message<\"openstatus.monitor.v1.CreateTCPMonitorResponse\"> & {\n  /**\n   * The created monitor with assigned ID.\n   *\n   * @generated from field: openstatus.monitor.v1.TCPMonitor monitor = 1;\n   */\n  monitor?: TCPMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.CreateTCPMonitorResponse.\n * Use `create(CreateTCPMonitorResponseSchema)` to create a new message.\n */\nexport const CreateTCPMonitorResponseSchema: GenMessage<CreateTCPMonitorResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 3);\n\n/**\n * CreateDNSMonitorRequest is the request to create a new DNS monitor.\n *\n * @generated from message openstatus.monitor.v1.CreateDNSMonitorRequest\n */\nexport type CreateDNSMonitorRequest = Message<\"openstatus.monitor.v1.CreateDNSMonitorRequest\"> & {\n  /**\n   * Monitor configuration (required).\n   *\n   * @generated from field: openstatus.monitor.v1.DNSMonitor monitor = 1;\n   */\n  monitor?: DNSMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.CreateDNSMonitorRequest.\n * Use `create(CreateDNSMonitorRequestSchema)` to create a new message.\n */\nexport const CreateDNSMonitorRequestSchema: GenMessage<CreateDNSMonitorRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 4);\n\n/**\n * CreateDNSMonitorResponse is the response after creating a DNS monitor.\n *\n * @generated from message openstatus.monitor.v1.CreateDNSMonitorResponse\n */\nexport type CreateDNSMonitorResponse = Message<\"openstatus.monitor.v1.CreateDNSMonitorResponse\"> & {\n  /**\n   * The created monitor with assigned ID.\n   *\n   * @generated from field: openstatus.monitor.v1.DNSMonitor monitor = 1;\n   */\n  monitor?: DNSMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.CreateDNSMonitorResponse.\n * Use `create(CreateDNSMonitorResponseSchema)` to create a new message.\n */\nexport const CreateDNSMonitorResponseSchema: GenMessage<CreateDNSMonitorResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 5);\n\n/**\n * UpdateHTTPMonitorRequest is the request to update an existing HTTP monitor.\n *\n * @generated from message openstatus.monitor.v1.UpdateHTTPMonitorRequest\n */\nexport type UpdateHTTPMonitorRequest = Message<\"openstatus.monitor.v1.UpdateHTTPMonitorRequest\"> & {\n  /**\n   * Monitor ID to update (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Updated monitor configuration (all fields optional for partial updates).\n   *\n   * @generated from field: optional openstatus.monitor.v1.HTTPMonitor monitor = 2;\n   */\n  monitor?: HTTPMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.UpdateHTTPMonitorRequest.\n * Use `create(UpdateHTTPMonitorRequestSchema)` to create a new message.\n */\nexport const UpdateHTTPMonitorRequestSchema: GenMessage<UpdateHTTPMonitorRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 6);\n\n/**\n * UpdateHTTPMonitorResponse is the response after updating an HTTP monitor.\n *\n * @generated from message openstatus.monitor.v1.UpdateHTTPMonitorResponse\n */\nexport type UpdateHTTPMonitorResponse = Message<\"openstatus.monitor.v1.UpdateHTTPMonitorResponse\"> & {\n  /**\n   * The updated monitor.\n   *\n   * @generated from field: openstatus.monitor.v1.HTTPMonitor monitor = 1;\n   */\n  monitor?: HTTPMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.UpdateHTTPMonitorResponse.\n * Use `create(UpdateHTTPMonitorResponseSchema)` to create a new message.\n */\nexport const UpdateHTTPMonitorResponseSchema: GenMessage<UpdateHTTPMonitorResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 7);\n\n/**\n * UpdateTCPMonitorRequest is the request to update an existing TCP monitor.\n *\n * @generated from message openstatus.monitor.v1.UpdateTCPMonitorRequest\n */\nexport type UpdateTCPMonitorRequest = Message<\"openstatus.monitor.v1.UpdateTCPMonitorRequest\"> & {\n  /**\n   * Monitor ID to update (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Updated monitor configuration (all fields optional for partial updates).\n   *\n   * @generated from field: optional openstatus.monitor.v1.TCPMonitor monitor = 2;\n   */\n  monitor?: TCPMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.UpdateTCPMonitorRequest.\n * Use `create(UpdateTCPMonitorRequestSchema)` to create a new message.\n */\nexport const UpdateTCPMonitorRequestSchema: GenMessage<UpdateTCPMonitorRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 8);\n\n/**\n * UpdateTCPMonitorResponse is the response after updating a TCP monitor.\n *\n * @generated from message openstatus.monitor.v1.UpdateTCPMonitorResponse\n */\nexport type UpdateTCPMonitorResponse = Message<\"openstatus.monitor.v1.UpdateTCPMonitorResponse\"> & {\n  /**\n   * The updated monitor.\n   *\n   * @generated from field: openstatus.monitor.v1.TCPMonitor monitor = 1;\n   */\n  monitor?: TCPMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.UpdateTCPMonitorResponse.\n * Use `create(UpdateTCPMonitorResponseSchema)` to create a new message.\n */\nexport const UpdateTCPMonitorResponseSchema: GenMessage<UpdateTCPMonitorResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 9);\n\n/**\n * UpdateDNSMonitorRequest is the request to update an existing DNS monitor.\n *\n * @generated from message openstatus.monitor.v1.UpdateDNSMonitorRequest\n */\nexport type UpdateDNSMonitorRequest = Message<\"openstatus.monitor.v1.UpdateDNSMonitorRequest\"> & {\n  /**\n   * Monitor ID to update (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Updated monitor configuration (all fields optional for partial updates).\n   *\n   * @generated from field: optional openstatus.monitor.v1.DNSMonitor monitor = 2;\n   */\n  monitor?: DNSMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.UpdateDNSMonitorRequest.\n * Use `create(UpdateDNSMonitorRequestSchema)` to create a new message.\n */\nexport const UpdateDNSMonitorRequestSchema: GenMessage<UpdateDNSMonitorRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 10);\n\n/**\n * UpdateDNSMonitorResponse is the response after updating a DNS monitor.\n *\n * @generated from message openstatus.monitor.v1.UpdateDNSMonitorResponse\n */\nexport type UpdateDNSMonitorResponse = Message<\"openstatus.monitor.v1.UpdateDNSMonitorResponse\"> & {\n  /**\n   * The updated monitor.\n   *\n   * @generated from field: openstatus.monitor.v1.DNSMonitor monitor = 1;\n   */\n  monitor?: DNSMonitor;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.UpdateDNSMonitorResponse.\n * Use `create(UpdateDNSMonitorResponseSchema)` to create a new message.\n */\nexport const UpdateDNSMonitorResponseSchema: GenMessage<UpdateDNSMonitorResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 11);\n\n/**\n * TriggerMonitorRequest is the request to trigger a monitor check.\n *\n * @generated from message openstatus.monitor.v1.TriggerMonitorRequest\n */\nexport type TriggerMonitorRequest = Message<\"openstatus.monitor.v1.TriggerMonitorRequest\"> & {\n  /**\n   * Monitor ID to trigger (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.TriggerMonitorRequest.\n * Use `create(TriggerMonitorRequestSchema)` to create a new message.\n */\nexport const TriggerMonitorRequestSchema: GenMessage<TriggerMonitorRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 12);\n\n/**\n * TriggerMonitorResponse is the response after triggering a monitor.\n *\n * @generated from message openstatus.monitor.v1.TriggerMonitorResponse\n */\nexport type TriggerMonitorResponse = Message<\"openstatus.monitor.v1.TriggerMonitorResponse\"> & {\n  /**\n   * Whether the trigger was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.TriggerMonitorResponse.\n * Use `create(TriggerMonitorResponseSchema)` to create a new message.\n */\nexport const TriggerMonitorResponseSchema: GenMessage<TriggerMonitorResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 13);\n\n/**\n * DeleteMonitorRequest is the request to delete a monitor.\n *\n * @generated from message openstatus.monitor.v1.DeleteMonitorRequest\n */\nexport type DeleteMonitorRequest = Message<\"openstatus.monitor.v1.DeleteMonitorRequest\"> & {\n  /**\n   * Monitor ID to delete (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.DeleteMonitorRequest.\n * Use `create(DeleteMonitorRequestSchema)` to create a new message.\n */\nexport const DeleteMonitorRequestSchema: GenMessage<DeleteMonitorRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 14);\n\n/**\n * DeleteMonitorResponse is the response after deleting a monitor.\n *\n * @generated from message openstatus.monitor.v1.DeleteMonitorResponse\n */\nexport type DeleteMonitorResponse = Message<\"openstatus.monitor.v1.DeleteMonitorResponse\"> & {\n  /**\n   * Whether the deletion was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.DeleteMonitorResponse.\n * Use `create(DeleteMonitorResponseSchema)` to create a new message.\n */\nexport const DeleteMonitorResponseSchema: GenMessage<DeleteMonitorResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 15);\n\n/**\n * ListMonitorsRequest is the request to list monitors.\n *\n * @generated from message openstatus.monitor.v1.ListMonitorsRequest\n */\nexport type ListMonitorsRequest = Message<\"openstatus.monitor.v1.ListMonitorsRequest\"> & {\n  /**\n   * Maximum number of monitors to return (1-100, defaults to 50).\n   *\n   * @generated from field: optional int32 limit = 1;\n   */\n  limit?: number;\n\n  /**\n   * Number of monitors to skip for pagination (defaults to 0).\n   *\n   * @generated from field: optional int32 offset = 2;\n   */\n  offset?: number;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.ListMonitorsRequest.\n * Use `create(ListMonitorsRequestSchema)` to create a new message.\n */\nexport const ListMonitorsRequestSchema: GenMessage<ListMonitorsRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 16);\n\n/**\n * ListMonitorsResponse is the response containing a list of monitors.\n *\n * @generated from message openstatus.monitor.v1.ListMonitorsResponse\n */\nexport type ListMonitorsResponse = Message<\"openstatus.monitor.v1.ListMonitorsResponse\"> & {\n  /**\n   * HTTP monitors in the workspace.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.HTTPMonitor http_monitors = 1;\n   */\n  httpMonitors: HTTPMonitor[];\n\n  /**\n   * TCP monitors in the workspace.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.TCPMonitor tcp_monitors = 2;\n   */\n  tcpMonitors: TCPMonitor[];\n\n  /**\n   * DNS monitors in the workspace.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.DNSMonitor dns_monitors = 3;\n   */\n  dnsMonitors: DNSMonitor[];\n\n  /**\n   * Total number of monitors across all types.\n   *\n   * @generated from field: int32 total_size = 4;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.ListMonitorsResponse.\n * Use `create(ListMonitorsResponseSchema)` to create a new message.\n */\nexport const ListMonitorsResponseSchema: GenMessage<ListMonitorsResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 17);\n\n/**\n * GetMonitorStatusRequest is the request to get the status of all regions for a monitor.\n *\n * @generated from message openstatus.monitor.v1.GetMonitorStatusRequest\n */\nexport type GetMonitorStatusRequest = Message<\"openstatus.monitor.v1.GetMonitorStatusRequest\"> & {\n  /**\n   * Monitor ID to get status for (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.GetMonitorStatusRequest.\n * Use `create(GetMonitorStatusRequestSchema)` to create a new message.\n */\nexport const GetMonitorStatusRequestSchema: GenMessage<GetMonitorStatusRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 18);\n\n/**\n * RegionStatus represents the status of a monitor in a specific region.\n *\n * @generated from message openstatus.monitor.v1.RegionStatus\n */\nexport type RegionStatus = Message<\"openstatus.monitor.v1.RegionStatus\"> & {\n  /**\n   * The region identifier.\n   *\n   * @generated from field: openstatus.monitor.v1.Region region = 1;\n   */\n  region: Region;\n\n  /**\n   * The status of the monitor in this region.\n   *\n   * @generated from field: openstatus.monitor.v1.MonitorStatus status = 2;\n   */\n  status: MonitorStatus;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.RegionStatus.\n * Use `create(RegionStatusSchema)` to create a new message.\n */\nexport const RegionStatusSchema: GenMessage<RegionStatus> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 19);\n\n/**\n * GetMonitorStatusResponse is the response containing the status of all regions for a monitor.\n *\n * @generated from message openstatus.monitor.v1.GetMonitorStatusResponse\n */\nexport type GetMonitorStatusResponse = Message<\"openstatus.monitor.v1.GetMonitorStatusResponse\"> & {\n  /**\n   * Monitor ID.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Status for each region.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.RegionStatus regions = 2;\n   */\n  regions: RegionStatus[];\n};\n\n/**\n * Describes the message openstatus.monitor.v1.GetMonitorStatusResponse.\n * Use `create(GetMonitorStatusResponseSchema)` to create a new message.\n */\nexport const GetMonitorStatusResponseSchema: GenMessage<GetMonitorStatusResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 20);\n\n/**\n * MonitorConfig represents the type-specific configuration for a monitor.\n *\n * @generated from message openstatus.monitor.v1.MonitorConfig\n */\nexport type MonitorConfig = Message<\"openstatus.monitor.v1.MonitorConfig\"> & {\n  /**\n   * @generated from oneof openstatus.monitor.v1.MonitorConfig.config\n   */\n  config: {\n    /**\n     * HTTP monitor configuration.\n     *\n     * @generated from field: openstatus.monitor.v1.HTTPMonitor http = 1;\n     */\n    value: HTTPMonitor;\n    case: \"http\";\n  } | {\n    /**\n     * TCP monitor configuration.\n     *\n     * @generated from field: openstatus.monitor.v1.TCPMonitor tcp = 2;\n     */\n    value: TCPMonitor;\n    case: \"tcp\";\n  } | {\n    /**\n     * DNS monitor configuration.\n     *\n     * @generated from field: openstatus.monitor.v1.DNSMonitor dns = 3;\n     */\n    value: DNSMonitor;\n    case: \"dns\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message openstatus.monitor.v1.MonitorConfig.\n * Use `create(MonitorConfigSchema)` to create a new message.\n */\nexport const MonitorConfigSchema: GenMessage<MonitorConfig> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 21);\n\n/**\n * GetMonitorSummaryRequest is the request to get aggregated metrics for a monitor.\n *\n * @generated from message openstatus.monitor.v1.GetMonitorSummaryRequest\n */\nexport type GetMonitorSummaryRequest = Message<\"openstatus.monitor.v1.GetMonitorSummaryRequest\"> & {\n  /**\n   * Monitor ID to get summary for (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Time range for metrics aggregation (defaults to 1 day if unspecified).\n   *\n   * @generated from field: openstatus.monitor.v1.TimeRange time_range = 2;\n   */\n  timeRange: TimeRange;\n\n  /**\n   * Optional filter by regions. If empty, returns metrics for all regions.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.Region regions = 3;\n   */\n  regions: Region[];\n};\n\n/**\n * Describes the message openstatus.monitor.v1.GetMonitorSummaryRequest.\n * Use `create(GetMonitorSummaryRequestSchema)` to create a new message.\n */\nexport const GetMonitorSummaryRequestSchema: GenMessage<GetMonitorSummaryRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 22);\n\n/**\n * GetMonitorSummaryResponse is the response containing aggregated metrics for a monitor.\n *\n * @generated from message openstatus.monitor.v1.GetMonitorSummaryResponse\n */\nexport type GetMonitorSummaryResponse = Message<\"openstatus.monitor.v1.GetMonitorSummaryResponse\"> & {\n  /**\n   * Monitor ID.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Timestamp of the last check in RFC 3339 format.\n   *\n   * @generated from field: string last_ping_at = 2;\n   */\n  lastPingAt: string;\n\n  /**\n   * Total number of successful requests.\n   *\n   * @generated from field: int64 total_successful = 3;\n   */\n  totalSuccessful: bigint;\n\n  /**\n   * Total number of degraded requests.\n   *\n   * @generated from field: int64 total_degraded = 4;\n   */\n  totalDegraded: bigint;\n\n  /**\n   * Total number of failed requests.\n   *\n   * @generated from field: int64 total_failed = 5;\n   */\n  totalFailed: bigint;\n\n  /**\n   * 50th percentile (median) latency in milliseconds.\n   *\n   * @generated from field: int64 p50 = 6;\n   */\n  p50: bigint;\n\n  /**\n   * 75th percentile latency in milliseconds.\n   *\n   * @generated from field: int64 p75 = 7;\n   */\n  p75: bigint;\n\n  /**\n   * 90th percentile latency in milliseconds.\n   *\n   * @generated from field: int64 p90 = 8;\n   */\n  p90: bigint;\n\n  /**\n   * 95th percentile latency in milliseconds.\n   *\n   * @generated from field: int64 p95 = 9;\n   */\n  p95: bigint;\n\n  /**\n   * 99th percentile latency in milliseconds.\n   *\n   * @generated from field: int64 p99 = 10;\n   */\n  p99: bigint;\n\n  /**\n   * Time range used for the metrics.\n   *\n   * @generated from field: openstatus.monitor.v1.TimeRange time_range = 11;\n   */\n  timeRange: TimeRange;\n\n  /**\n   * Regions included in the metrics.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.Region regions = 12;\n   */\n  regions: Region[];\n};\n\n/**\n * Describes the message openstatus.monitor.v1.GetMonitorSummaryResponse.\n * Use `create(GetMonitorSummaryResponseSchema)` to create a new message.\n */\nexport const GetMonitorSummaryResponseSchema: GenMessage<GetMonitorSummaryResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 23);\n\n/**\n * GetMonitorRequest is the request to get a single monitor by ID.\n *\n * @generated from message openstatus.monitor.v1.GetMonitorRequest\n */\nexport type GetMonitorRequest = Message<\"openstatus.monitor.v1.GetMonitorRequest\"> & {\n  /**\n   * Monitor ID to retrieve (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.GetMonitorRequest.\n * Use `create(GetMonitorRequestSchema)` to create a new message.\n */\nexport const GetMonitorRequestSchema: GenMessage<GetMonitorRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 24);\n\n/**\n * GetMonitorResponse is the response containing the monitor.\n *\n * @generated from message openstatus.monitor.v1.GetMonitorResponse\n */\nexport type GetMonitorResponse = Message<\"openstatus.monitor.v1.GetMonitorResponse\"> & {\n  /**\n   * The monitor configuration (one of HTTP, TCP, or DNS).\n   *\n   * @generated from field: openstatus.monitor.v1.MonitorConfig monitor = 1;\n   */\n  monitor?: MonitorConfig;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.GetMonitorResponse.\n * Use `create(GetMonitorResponseSchema)` to create a new message.\n */\nexport const GetMonitorResponseSchema: GenMessage<GetMonitorResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_service, 25);\n\n/**\n * TimeRange represents the time period for metrics aggregation.\n *\n * @generated from enum openstatus.monitor.v1.TimeRange\n */\nexport enum TimeRange {\n  /**\n   * Unspecified time range.\n   *\n   * @generated from enum value: TIME_RANGE_UNSPECIFIED = 0;\n   */\n  TIME_RANGE_UNSPECIFIED = 0,\n\n  /**\n   * Last 24 hours.\n   *\n   * @generated from enum value: TIME_RANGE_1D = 1;\n   */\n  TIME_RANGE_1D = 1,\n\n  /**\n   * Last 7 days.\n   *\n   * @generated from enum value: TIME_RANGE_7D = 2;\n   */\n  TIME_RANGE_7D = 2,\n\n  /**\n   * Last 14 days.\n   *\n   * @generated from enum value: TIME_RANGE_14D = 3;\n   */\n  TIME_RANGE_14D = 3,\n}\n\n/**\n * Describes the enum openstatus.monitor.v1.TimeRange.\n */\nexport const TimeRangeSchema: GenEnum<TimeRange> = /*@__PURE__*/\n  enumDesc(file_openstatus_monitor_v1_service, 0);\n\n/**\n * MonitorService provides CRUD and operational commands for monitors.\n *\n * @generated from service openstatus.monitor.v1.MonitorService\n */\nexport const MonitorService: GenService<{\n  /**\n   * CreateHTTPMonitor creates a new HTTP monitor with URL, method, headers, assertions, and optional OpenTelemetry configuration.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.CreateHTTPMonitor\n   */\n  createHTTPMonitor: {\n    methodKind: \"unary\";\n    input: typeof CreateHTTPMonitorRequestSchema;\n    output: typeof CreateHTTPMonitorResponseSchema;\n  },\n  /**\n   * CreateTCPMonitor creates a new TCP monitor.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.CreateTCPMonitor\n   */\n  createTCPMonitor: {\n    methodKind: \"unary\";\n    input: typeof CreateTCPMonitorRequestSchema;\n    output: typeof CreateTCPMonitorResponseSchema;\n  },\n  /**\n   * CreateDNSMonitor creates a new DNS monitor.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.CreateDNSMonitor\n   */\n  createDNSMonitor: {\n    methodKind: \"unary\";\n    input: typeof CreateDNSMonitorRequestSchema;\n    output: typeof CreateDNSMonitorResponseSchema;\n  },\n  /**\n   * UpdateHTTPMonitor updates an existing HTTP monitor.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.UpdateHTTPMonitor\n   */\n  updateHTTPMonitor: {\n    methodKind: \"unary\";\n    input: typeof UpdateHTTPMonitorRequestSchema;\n    output: typeof UpdateHTTPMonitorResponseSchema;\n  },\n  /**\n   * UpdateTCPMonitor updates an existing TCP monitor.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.UpdateTCPMonitor\n   */\n  updateTCPMonitor: {\n    methodKind: \"unary\";\n    input: typeof UpdateTCPMonitorRequestSchema;\n    output: typeof UpdateTCPMonitorResponseSchema;\n  },\n  /**\n   * UpdateDNSMonitor updates an existing DNS monitor.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.UpdateDNSMonitor\n   */\n  updateDNSMonitor: {\n    methodKind: \"unary\";\n    input: typeof UpdateDNSMonitorRequestSchema;\n    output: typeof UpdateDNSMonitorResponseSchema;\n  },\n  /**\n   * TriggerMonitor initiates an immediate check for a monitor across all configured regions.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.TriggerMonitor\n   */\n  triggerMonitor: {\n    methodKind: \"unary\";\n    input: typeof TriggerMonitorRequestSchema;\n    output: typeof TriggerMonitorResponseSchema;\n  },\n  /**\n   * DeleteMonitor removes a monitor.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.DeleteMonitor\n   */\n  deleteMonitor: {\n    methodKind: \"unary\";\n    input: typeof DeleteMonitorRequestSchema;\n    output: typeof DeleteMonitorResponseSchema;\n  },\n  /**\n   * ListMonitors returns a paginated list of all monitors in the workspace.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.ListMonitors\n   */\n  listMonitors: {\n    methodKind: \"unary\";\n    input: typeof ListMonitorsRequestSchema;\n    output: typeof ListMonitorsResponseSchema;\n  },\n  /**\n   * GetMonitorStatus returns the current status of all regions for a monitor.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.GetMonitorStatus\n   */\n  getMonitorStatus: {\n    methodKind: \"unary\";\n    input: typeof GetMonitorStatusRequestSchema;\n    output: typeof GetMonitorStatusResponseSchema;\n  },\n  /**\n   * GetMonitorSummary returns aggregated metrics and statistics for a monitor over a configurable time range.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.GetMonitorSummary\n   */\n  getMonitorSummary: {\n    methodKind: \"unary\";\n    input: typeof GetMonitorSummaryRequestSchema;\n    output: typeof GetMonitorSummaryResponseSchema;\n  },\n  /**\n   * GetMonitor returns a single monitor by ID within the authenticated workspace.\n   * Returns the monitor configuration (HTTP, TCP, or DNS) using the MonitorConfig oneof type.\n   *\n   * @generated from rpc openstatus.monitor.v1.MonitorService.GetMonitor\n   */\n  getMonitor: {\n    methodKind: \"unary\";\n    input: typeof GetMonitorRequestSchema;\n    output: typeof GetMonitorResponseSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_openstatus_monitor_v1_service, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/monitor/v1/tcp_monitor_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/monitor/v1/tcp_monitor.proto (package openstatus.monitor.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { OpenTelemetryConfig } from \"./http_monitor_pb.ts\";\nimport { file_openstatus_monitor_v1_http_monitor } from \"./http_monitor_pb.ts\";\nimport type { MonitorStatus, Periodicity, Region } from \"./monitor_pb.ts\";\nimport { file_openstatus_monitor_v1_monitor } from \"./monitor_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/monitor/v1/tcp_monitor.proto.\n */\nexport const file_openstatus_monitor_v1_tcp_monitor: GenFile = /*@__PURE__*/\n  fileDesc(\"CidvcGVuc3RhdHVzL21vbml0b3IvdjEvdGNwX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSK3BAoKVENQTW9uaXRvchIKCgJpZBgBIAEoCRI4CgRuYW1lGAIgASgJQiq6Rx06GxIZRGF0YWJhc2UgQ29ubmVjdGlvbiBDaGVja7pIB3IFEAEYgAISNwoDdXJpGAMgASgJQiq6Rx06GxIZdGNwOi8vZGIuZXhhbXBsZS5jb206NTQzMrpIB3IFEAEYgBASQQoLcGVyaW9kaWNpdHkYBCABKA4yIi5vcGVuc3RhdHVzLm1vbml0b3IudjEuUGVyaW9kaWNpdHlCCLpIBYIBAiAAEhwKB3RpbWVvdXQYBSABKANCC7pICCIGGMCpBygAEiUKC2RlZ3JhZGVkX2F0GAYgASgDQgu6SAgiBhjAqQcoAEgAiAEBEhgKBXJldHJ5GAcgASgDQgm6SAYiBBgKKAASHQoLZGVzY3JpcHRpb24YCCABKAlCCLpIBXIDGIAIEg4KBmFjdGl2ZRgJIAEoCBIOCgZwdWJsaWMYCiABKAgSPwoHcmVnaW9ucxgLIAMoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb25CD7pIDJIBCRAcIgWCAQIgABJCCg5vcGVuX3RlbGVtZXRyeRgMIAEoCzIqLm9wZW5zdGF0dXMubW9uaXRvci52MS5PcGVuVGVsZW1ldHJ5Q29uZmlnEjQKBnN0YXR1cxgNIAEoDjIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Nb25pdG9yU3RhdHVzQg4KDF9kZWdyYWRlZF9hdEJTWlFnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvbW9uaXRvci92MTttb25pdG9ydjFiBnByb3RvMw\", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]);\n\n/**\n * TCPMonitor defines the configuration for a TCP monitor.\n *\n * @generated from message openstatus.monitor.v1.TCPMonitor\n */\nexport type TCPMonitor = Message<\"openstatus.monitor.v1.TCPMonitor\"> & {\n  /**\n   * Unique identifier for the monitor (output only for create requests).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Name of the monitor (required, max 256 characters).\n   *\n   * @generated from field: string name = 2;\n   */\n  name: string;\n\n  /**\n   * URI to monitor in format \"host:port\" (required, max 2048 characters).\n   *\n   * @generated from field: string uri = 3;\n   */\n  uri: string;\n\n  /**\n   * Check periodicity (required).\n   *\n   * @generated from field: openstatus.monitor.v1.Periodicity periodicity = 4;\n   */\n  periodicity: Periodicity;\n\n  /**\n   * Timeout in milliseconds (0-120000, defaults to 45000).\n   *\n   * @generated from field: int64 timeout = 5;\n   */\n  timeout: bigint;\n\n  /**\n   * Latency threshold for degraded status in milliseconds (optional, 0-120000).\n   *\n   * @generated from field: optional int64 degraded_at = 6;\n   */\n  degradedAt?: bigint;\n\n  /**\n   * Number of retry attempts (0-10, defaults to 3).\n   *\n   * @generated from field: int64 retry = 7;\n   */\n  retry: bigint;\n\n  /**\n   * Description of the monitor (optional).\n   *\n   * @generated from field: string description = 8;\n   */\n  description: string;\n\n  /**\n   * Whether the monitor is active (defaults to false).\n   *\n   * @generated from field: bool active = 9;\n   */\n  active: boolean;\n\n  /**\n   * Whether the monitor is publicly visible (defaults to false).\n   *\n   * @generated from field: bool public = 10;\n   */\n  public: boolean;\n\n  /**\n   * Geographic regions to run checks from.\n   *\n   * @generated from field: repeated openstatus.monitor.v1.Region regions = 11;\n   */\n  regions: Region[];\n\n  /**\n   * OpenTelemetry configuration for exporting metrics.\n   *\n   * @generated from field: openstatus.monitor.v1.OpenTelemetryConfig open_telemetry = 12;\n   */\n  openTelemetry?: OpenTelemetryConfig;\n\n  /**\n   * Current operational status of the monitor.\n   *\n   * @generated from field: openstatus.monitor.v1.MonitorStatus status = 13;\n   */\n  status: MonitorStatus;\n};\n\n/**\n * Describes the message openstatus.monitor.v1.TCPMonitor.\n * Use `create(TCPMonitorSchema)` to create a new message.\n */\nexport const TCPMonitorSchema: GenMessage<TCPMonitor> = /*@__PURE__*/\n  messageDesc(file_openstatus_monitor_v1_tcp_monitor, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/notification/v1/index.ts",
    "content": "// Notification service exports\nexport * from \"./providers_pb.js\";\nexport * from \"./notification_pb.js\";\nexport * from \"./service_pb.js\";\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/notification/v1/notification_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/notification/v1/notification.proto (package openstatus.notification.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { NotificationData, NotificationProvider } from \"./providers_pb.ts\";\nimport { file_openstatus_notification_v1_providers } from \"./providers_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/notification/v1/notification.proto.\n */\nexport const file_openstatus_notification_v1_notification: GenFile = /*@__PURE__*/\n  fileDesc(\"Ci1vcGVuc3RhdHVzL25vdGlmaWNhdGlvbi92MS9ub3RpZmljYXRpb24ucHJvdG8SGm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxIuUBCgxOb3RpZmljYXRpb24SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRJCCghwcm92aWRlchgDIAEoDjIwLm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxLk5vdGlmaWNhdGlvblByb3ZpZGVyEjoKBGRhdGEYBCABKAsyLC5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5Ob3RpZmljYXRpb25EYXRhEhMKC21vbml0b3JfaWRzGAUgAygJEhIKCmNyZWF0ZWRfYXQYBiABKAkSEgoKdXBkYXRlZF9hdBgHIAEoCSKyAQoTTm90aWZpY2F0aW9uU3VtbWFyeRIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEkIKCHByb3ZpZGVyGAMgASgOMjAub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuTm90aWZpY2F0aW9uUHJvdmlkZXISFQoNbW9uaXRvcl9jb3VudBgEIAEoBRISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAlCXVpbZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL25vdGlmaWNhdGlvbi92MTtub3RpZmljYXRpb252MWIGcHJvdG8z\", [file_openstatus_notification_v1_providers]);\n\n/**\n * Notification represents a notification channel with full details.\n *\n * @generated from message openstatus.notification.v1.Notification\n */\nexport type Notification = Message<\"openstatus.notification.v1.Notification\"> & {\n  /**\n   * Unique identifier for the notification.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Display name for the notification channel.\n   *\n   * @generated from field: string name = 2;\n   */\n  name: string;\n\n  /**\n   * Provider type.\n   *\n   * @generated from field: openstatus.notification.v1.NotificationProvider provider = 3;\n   */\n  provider: NotificationProvider;\n\n  /**\n   * Provider-specific configuration.\n   *\n   * @generated from field: openstatus.notification.v1.NotificationData data = 4;\n   */\n  data?: NotificationData;\n\n  /**\n   * IDs of monitors associated with this notification.\n   *\n   * @generated from field: repeated string monitor_ids = 5;\n   */\n  monitorIds: string[];\n\n  /**\n   * Timestamp when the notification was created (RFC 3339).\n   *\n   * @generated from field: string created_at = 6;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the notification was last updated (RFC 3339).\n   *\n   * @generated from field: string updated_at = 7;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.Notification.\n * Use `create(NotificationSchema)` to create a new message.\n */\nexport const NotificationSchema: GenMessage<Notification> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_notification, 0);\n\n/**\n * NotificationSummary represents a notification channel summary for list responses.\n *\n * @generated from message openstatus.notification.v1.NotificationSummary\n */\nexport type NotificationSummary = Message<\"openstatus.notification.v1.NotificationSummary\"> & {\n  /**\n   * Unique identifier for the notification.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Display name for the notification channel.\n   *\n   * @generated from field: string name = 2;\n   */\n  name: string;\n\n  /**\n   * Provider type.\n   *\n   * @generated from field: openstatus.notification.v1.NotificationProvider provider = 3;\n   */\n  provider: NotificationProvider;\n\n  /**\n   * Number of monitors associated with this notification.\n   *\n   * @generated from field: int32 monitor_count = 4;\n   */\n  monitorCount: number;\n\n  /**\n   * Timestamp when the notification was created (RFC 3339).\n   *\n   * @generated from field: string created_at = 5;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the notification was last updated (RFC 3339).\n   *\n   * @generated from field: string updated_at = 6;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.NotificationSummary.\n * Use `create(NotificationSummarySchema)` to create a new message.\n */\nexport const NotificationSummarySchema: GenMessage<NotificationSummary> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_notification, 1);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/notification/v1/providers_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/notification/v1/providers.proto (package openstatus.notification.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/notification/v1/providers.proto.\n */\nexport const file_openstatus_notification_v1_providers: GenFile = /*@__PURE__*/\n  fileDesc(\"CipvcGVuc3RhdHVzL25vdGlmaWNhdGlvbi92MS9wcm92aWRlcnMucHJvdG8SGm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxIlsKC0Rpc2NvcmREYXRhEkwKC3dlYmhvb2tfdXJsGAEgASgJQje6Ryw6KhIoaHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTIzL2FiY7pIBXIDiAEBIj4KCUVtYWlsRGF0YRIxCgVlbWFpbBgBIAEoCUIiukcYOhYSFG9wcy10ZWFtQGV4YW1wbGUuY29tukgEcgJgASIvCg5Hb29nbGVDaGF0RGF0YRIdCgt3ZWJob29rX3VybBgBIAEoCUIIukgFcgOIAQEiMgoRR3JhZmFuYU9uY2FsbERhdGESHQoLd2ViaG9va191cmwYASABKAlCCLpIBXIDiAEBImwKCE50ZnlEYXRhEi4KBXRvcGljGAEgASgJQh+6RxU6ExIRb3BlbnN0YXR1cy1hbGVydHO6SARyAhABEhIKCnNlcnZlcl91cmwYAiABKAkSEgoFdG9rZW4YAyABKAlIAIgBAUIICgZfdG9rZW4iTAoNUGFnZXJEdXR5RGF0YRI7Cg9pbnRlZ3JhdGlvbl9rZXkYASABKAlCIrpHGDoWEhRhMWIyYzNkNGU1ZjZnN2g4aTlqMLpIBHICEAEibgoMT3BzZ2VuaWVEYXRhEhgKB2FwaV9rZXkYASABKAlCB7pIBHICEAESRAoGcmVnaW9uGAIgASgOMioub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuT3BzZ2VuaWVSZWdpb25CCLpIBYIBAhABIl0KCVNsYWNrRGF0YRJQCgt3ZWJob29rX3VybBgBIAEoCUI7ukcwOi4SLGh0dHBzOi8vaG9va3Muc2xhY2suY29tL3NlcnZpY2VzL1QwMC9CMDAveHh4ukgFcgOIAQEiOwoHU21zRGF0YRIwCgxwaG9uZV9udW1iZXIYASABKAlCGrpHEDoOEgwrMTQxNTU1NTEyMzS6SARyAhABIj0KDFRlbGVncmFtRGF0YRItCgdjaGF0X2lkGAEgASgJQhy6RxI6EBIOLTEwMDEyMzQ1Njc4OTC6SARyAhABIjQKDVdlYmhvb2tIZWFkZXISFAoDa2V5GAEgASgJQge6SARyAhABEg0KBXZhbHVlGAIgASgJIpcBCgtXZWJob29rRGF0YRJMCghlbmRwb2ludBgBIAEoCUI6ukcvOi0SK2h0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3dlYmhvb2tzL29wZW5zdGF0dXO6SAVyA4gBARI6CgdoZWFkZXJzGAIgAygLMikub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuV2ViaG9va0hlYWRlciItCgxXaGF0c2FwcERhdGESHQoMcGhvbmVfbnVtYmVyGAEgASgJQge6SARyAhABIvIFChBOb3RpZmljYXRpb25EYXRhEjoKB2Rpc2NvcmQYASABKAsyJy5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5EaXNjb3JkRGF0YUgAEjYKBWVtYWlsGAIgASgLMiUub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuRW1haWxEYXRhSAASQQoLZ29vZ2xlX2NoYXQYAyABKAsyKi5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5Hb29nbGVDaGF0RGF0YUgAEkcKDmdyYWZhbmFfb25jYWxsGAQgASgLMi0ub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuR3JhZmFuYU9uY2FsbERhdGFIABI0CgRudGZ5GAUgASgLMiQub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuTnRmeURhdGFIABI+CglwYWdlcmR1dHkYBiABKAsyKS5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5QYWdlckR1dHlEYXRhSAASPAoIb3BzZ2VuaWUYByABKAsyKC5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5PcHNnZW5pZURhdGFIABI2CgVzbGFjaxgIIAEoCzIlLm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxLlNsYWNrRGF0YUgAEjIKA3NtcxgJIAEoCzIjLm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxLlNtc0RhdGFIABI8Cgh0ZWxlZ3JhbRgKIAEoCzIoLm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxLlRlbGVncmFtRGF0YUgAEjoKB3dlYmhvb2sYCyABKAsyJy5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5XZWJob29rRGF0YUgAEjwKCHdoYXRzYXBwGAwgASgLMigub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuV2hhdHNhcHBEYXRhSABCBgoEZGF0YSrmAwoUTm90aWZpY2F0aW9uUHJvdmlkZXISJQohTk9USUZJQ0FUSU9OX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASIQodTk9USUZJQ0FUSU9OX1BST1ZJREVSX0RJU0NPUkQQARIfChtOT1RJRklDQVRJT05fUFJPVklERVJfRU1BSUwQAhIlCiFOT1RJRklDQVRJT05fUFJPVklERVJfR09PR0xFX0NIQVQQAxIoCiROT1RJRklDQVRJT05fUFJPVklERVJfR1JBRkFOQV9PTkNBTEwQBBIeChpOT1RJRklDQVRJT05fUFJPVklERVJfTlRGWRAFEiMKH05PVElGSUNBVElPTl9QUk9WSURFUl9QQUdFUkRVVFkQBhIiCh5OT1RJRklDQVRJT05fUFJPVklERVJfT1BTR0VOSUUQBxIfChtOT1RJRklDQVRJT05fUFJPVklERVJfU0xBQ0sQCBIdChlOT1RJRklDQVRJT05fUFJPVklERVJfU01TEAkSIgoeTk9USUZJQ0FUSU9OX1BST1ZJREVSX1RFTEVHUkFNEAoSIQodTk9USUZJQ0FUSU9OX1BST1ZJREVSX1dFQkhPT0sQCxIiCh5OT1RJRklDQVRJT05fUFJPVklERVJfV0hBVFNBUFAQDCphCg5PcHNnZW5pZVJlZ2lvbhIfChtPUFNHRU5JRV9SRUdJT05fVU5TUEVDSUZJRUQQABIWChJPUFNHRU5JRV9SRUdJT05fVVMQARIWChJPUFNHRU5JRV9SRUdJT05fRVUQAkJdWltnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvbm90aWZpY2F0aW9uL3YxO25vdGlmaWNhdGlvbnYxYgZwcm90bzM\", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations]);\n\n/**\n * DiscordData contains configuration for Discord notifications.\n *\n * @generated from message openstatus.notification.v1.DiscordData\n */\nexport type DiscordData = Message<\"openstatus.notification.v1.DiscordData\"> & {\n  /**\n   * Discord webhook URL.\n   *\n   * @generated from field: string webhook_url = 1;\n   */\n  webhookUrl: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.DiscordData.\n * Use `create(DiscordDataSchema)` to create a new message.\n */\nexport const DiscordDataSchema: GenMessage<DiscordData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 0);\n\n/**\n * EmailData contains configuration for email notifications.\n *\n * @generated from message openstatus.notification.v1.EmailData\n */\nexport type EmailData = Message<\"openstatus.notification.v1.EmailData\"> & {\n  /**\n   * Email address to send notifications to.\n   *\n   * @generated from field: string email = 1;\n   */\n  email: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.EmailData.\n * Use `create(EmailDataSchema)` to create a new message.\n */\nexport const EmailDataSchema: GenMessage<EmailData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 1);\n\n/**\n * GoogleChatData contains configuration for Google Chat notifications.\n *\n * @generated from message openstatus.notification.v1.GoogleChatData\n */\nexport type GoogleChatData = Message<\"openstatus.notification.v1.GoogleChatData\"> & {\n  /**\n   * Google Chat webhook URL.\n   *\n   * @generated from field: string webhook_url = 1;\n   */\n  webhookUrl: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.GoogleChatData.\n * Use `create(GoogleChatDataSchema)` to create a new message.\n */\nexport const GoogleChatDataSchema: GenMessage<GoogleChatData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 2);\n\n/**\n * GrafanaOncallData contains configuration for Grafana OnCall notifications.\n *\n * @generated from message openstatus.notification.v1.GrafanaOncallData\n */\nexport type GrafanaOncallData = Message<\"openstatus.notification.v1.GrafanaOncallData\"> & {\n  /**\n   * Grafana OnCall webhook URL.\n   *\n   * @generated from field: string webhook_url = 1;\n   */\n  webhookUrl: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.GrafanaOncallData.\n * Use `create(GrafanaOncallDataSchema)` to create a new message.\n */\nexport const GrafanaOncallDataSchema: GenMessage<GrafanaOncallData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 3);\n\n/**\n * NtfyData contains configuration for Ntfy notifications.\n *\n * @generated from message openstatus.notification.v1.NtfyData\n */\nexport type NtfyData = Message<\"openstatus.notification.v1.NtfyData\"> & {\n  /**\n   * Ntfy topic to publish to.\n   *\n   * @generated from field: string topic = 1;\n   */\n  topic: string;\n\n  /**\n   * Ntfy server URL (defaults to https://ntfy.sh).\n   *\n   * @generated from field: string server_url = 2;\n   */\n  serverUrl: string;\n\n  /**\n   * Optional authentication token.\n   *\n   * @generated from field: optional string token = 3;\n   */\n  token?: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.NtfyData.\n * Use `create(NtfyDataSchema)` to create a new message.\n */\nexport const NtfyDataSchema: GenMessage<NtfyData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 4);\n\n/**\n * PagerDutyData contains configuration for PagerDuty notifications.\n *\n * @generated from message openstatus.notification.v1.PagerDutyData\n */\nexport type PagerDutyData = Message<\"openstatus.notification.v1.PagerDutyData\"> & {\n  /**\n   * PagerDuty integration key.\n   *\n   * @generated from field: string integration_key = 1;\n   */\n  integrationKey: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.PagerDutyData.\n * Use `create(PagerDutyDataSchema)` to create a new message.\n */\nexport const PagerDutyDataSchema: GenMessage<PagerDutyData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 5);\n\n/**\n * OpsgenieData contains configuration for Opsgenie notifications.\n *\n * @generated from message openstatus.notification.v1.OpsgenieData\n */\nexport type OpsgenieData = Message<\"openstatus.notification.v1.OpsgenieData\"> & {\n  /**\n   * Opsgenie API key.\n   *\n   * @generated from field: string api_key = 1;\n   */\n  apiKey: string;\n\n  /**\n   * Opsgenie region.\n   *\n   * @generated from field: openstatus.notification.v1.OpsgenieRegion region = 2;\n   */\n  region: OpsgenieRegion;\n};\n\n/**\n * Describes the message openstatus.notification.v1.OpsgenieData.\n * Use `create(OpsgenieDataSchema)` to create a new message.\n */\nexport const OpsgenieDataSchema: GenMessage<OpsgenieData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 6);\n\n/**\n * SlackData contains configuration for Slack notifications.\n *\n * @generated from message openstatus.notification.v1.SlackData\n */\nexport type SlackData = Message<\"openstatus.notification.v1.SlackData\"> & {\n  /**\n   * Slack webhook URL.\n   *\n   * @generated from field: string webhook_url = 1;\n   */\n  webhookUrl: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.SlackData.\n * Use `create(SlackDataSchema)` to create a new message.\n */\nexport const SlackDataSchema: GenMessage<SlackData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 7);\n\n/**\n * SmsData contains configuration for SMS notifications.\n *\n * @generated from message openstatus.notification.v1.SmsData\n */\nexport type SmsData = Message<\"openstatus.notification.v1.SmsData\"> & {\n  /**\n   * Phone number to send SMS to.\n   *\n   * @generated from field: string phone_number = 1;\n   */\n  phoneNumber: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.SmsData.\n * Use `create(SmsDataSchema)` to create a new message.\n */\nexport const SmsDataSchema: GenMessage<SmsData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 8);\n\n/**\n * TelegramData contains configuration for Telegram notifications.\n *\n * @generated from message openstatus.notification.v1.TelegramData\n */\nexport type TelegramData = Message<\"openstatus.notification.v1.TelegramData\"> & {\n  /**\n   * Telegram chat ID.\n   *\n   * @generated from field: string chat_id = 1;\n   */\n  chatId: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.TelegramData.\n * Use `create(TelegramDataSchema)` to create a new message.\n */\nexport const TelegramDataSchema: GenMessage<TelegramData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 9);\n\n/**\n * WebhookHeader represents a custom header for webhook requests.\n *\n * @generated from message openstatus.notification.v1.WebhookHeader\n */\nexport type WebhookHeader = Message<\"openstatus.notification.v1.WebhookHeader\"> & {\n  /**\n   * Header name.\n   *\n   * @generated from field: string key = 1;\n   */\n  key: string;\n\n  /**\n   * Header value.\n   *\n   * @generated from field: string value = 2;\n   */\n  value: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.WebhookHeader.\n * Use `create(WebhookHeaderSchema)` to create a new message.\n */\nexport const WebhookHeaderSchema: GenMessage<WebhookHeader> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 10);\n\n/**\n * WebhookData contains configuration for custom webhook notifications.\n *\n * @generated from message openstatus.notification.v1.WebhookData\n */\nexport type WebhookData = Message<\"openstatus.notification.v1.WebhookData\"> & {\n  /**\n   * Webhook endpoint URL.\n   *\n   * @generated from field: string endpoint = 1;\n   */\n  endpoint: string;\n\n  /**\n   * Optional custom headers.\n   *\n   * @generated from field: repeated openstatus.notification.v1.WebhookHeader headers = 2;\n   */\n  headers: WebhookHeader[];\n};\n\n/**\n * Describes the message openstatus.notification.v1.WebhookData.\n * Use `create(WebhookDataSchema)` to create a new message.\n */\nexport const WebhookDataSchema: GenMessage<WebhookData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 11);\n\n/**\n * WhatsappData contains configuration for WhatsApp notifications.\n *\n * @generated from message openstatus.notification.v1.WhatsappData\n */\nexport type WhatsappData = Message<\"openstatus.notification.v1.WhatsappData\"> & {\n  /**\n   * Phone number to send WhatsApp messages to.\n   *\n   * @generated from field: string phone_number = 1;\n   */\n  phoneNumber: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.WhatsappData.\n * Use `create(WhatsappDataSchema)` to create a new message.\n */\nexport const WhatsappDataSchema: GenMessage<WhatsappData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 12);\n\n/**\n * NotificationData is a union of provider-specific configuration.\n *\n * @generated from message openstatus.notification.v1.NotificationData\n */\nexport type NotificationData = Message<\"openstatus.notification.v1.NotificationData\"> & {\n  /**\n   * @generated from oneof openstatus.notification.v1.NotificationData.data\n   */\n  data: {\n    /**\n     * Discord configuration.\n     *\n     * @generated from field: openstatus.notification.v1.DiscordData discord = 1;\n     */\n    value: DiscordData;\n    case: \"discord\";\n  } | {\n    /**\n     * Email configuration.\n     *\n     * @generated from field: openstatus.notification.v1.EmailData email = 2;\n     */\n    value: EmailData;\n    case: \"email\";\n  } | {\n    /**\n     * Google Chat configuration.\n     *\n     * @generated from field: openstatus.notification.v1.GoogleChatData google_chat = 3;\n     */\n    value: GoogleChatData;\n    case: \"googleChat\";\n  } | {\n    /**\n     * Grafana OnCall configuration.\n     *\n     * @generated from field: openstatus.notification.v1.GrafanaOncallData grafana_oncall = 4;\n     */\n    value: GrafanaOncallData;\n    case: \"grafanaOncall\";\n  } | {\n    /**\n     * Ntfy configuration.\n     *\n     * @generated from field: openstatus.notification.v1.NtfyData ntfy = 5;\n     */\n    value: NtfyData;\n    case: \"ntfy\";\n  } | {\n    /**\n     * PagerDuty configuration.\n     *\n     * @generated from field: openstatus.notification.v1.PagerDutyData pagerduty = 6;\n     */\n    value: PagerDutyData;\n    case: \"pagerduty\";\n  } | {\n    /**\n     * Opsgenie configuration.\n     *\n     * @generated from field: openstatus.notification.v1.OpsgenieData opsgenie = 7;\n     */\n    value: OpsgenieData;\n    case: \"opsgenie\";\n  } | {\n    /**\n     * Slack configuration.\n     *\n     * @generated from field: openstatus.notification.v1.SlackData slack = 8;\n     */\n    value: SlackData;\n    case: \"slack\";\n  } | {\n    /**\n     * SMS configuration.\n     *\n     * @generated from field: openstatus.notification.v1.SmsData sms = 9;\n     */\n    value: SmsData;\n    case: \"sms\";\n  } | {\n    /**\n     * Telegram configuration.\n     *\n     * @generated from field: openstatus.notification.v1.TelegramData telegram = 10;\n     */\n    value: TelegramData;\n    case: \"telegram\";\n  } | {\n    /**\n     * Webhook configuration.\n     *\n     * @generated from field: openstatus.notification.v1.WebhookData webhook = 11;\n     */\n    value: WebhookData;\n    case: \"webhook\";\n  } | {\n    /**\n     * WhatsApp configuration.\n     *\n     * @generated from field: openstatus.notification.v1.WhatsappData whatsapp = 12;\n     */\n    value: WhatsappData;\n    case: \"whatsapp\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message openstatus.notification.v1.NotificationData.\n * Use `create(NotificationDataSchema)` to create a new message.\n */\nexport const NotificationDataSchema: GenMessage<NotificationData> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_providers, 13);\n\n/**\n * NotificationProvider represents the supported notification channel types.\n *\n * @generated from enum openstatus.notification.v1.NotificationProvider\n */\nexport enum NotificationProvider {\n  /**\n   * Unspecified provider.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * Discord webhook.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_DISCORD = 1;\n   */\n  DISCORD = 1,\n\n  /**\n   * Email notification.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_EMAIL = 2;\n   */\n  EMAIL = 2,\n\n  /**\n   * Google Chat webhook.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_GOOGLE_CHAT = 3;\n   */\n  GOOGLE_CHAT = 3,\n\n  /**\n   * Grafana OnCall webhook.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_GRAFANA_ONCALL = 4;\n   */\n  GRAFANA_ONCALL = 4,\n\n  /**\n   * Ntfy notification service.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_NTFY = 5;\n   */\n  NTFY = 5,\n\n  /**\n   * PagerDuty integration.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_PAGERDUTY = 6;\n   */\n  PAGERDUTY = 6,\n\n  /**\n   * Opsgenie integration.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_OPSGENIE = 7;\n   */\n  OPSGENIE = 7,\n\n  /**\n   * Slack webhook.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_SLACK = 8;\n   */\n  SLACK = 8,\n\n  /**\n   * SMS notification.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_SMS = 9;\n   */\n  SMS = 9,\n\n  /**\n   * Telegram bot.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_TELEGRAM = 10;\n   */\n  TELEGRAM = 10,\n\n  /**\n   * Custom webhook.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_WEBHOOK = 11;\n   */\n  WEBHOOK = 11,\n\n  /**\n   * WhatsApp notification.\n   *\n   * @generated from enum value: NOTIFICATION_PROVIDER_WHATSAPP = 12;\n   */\n  WHATSAPP = 12,\n}\n\n/**\n * Describes the enum openstatus.notification.v1.NotificationProvider.\n */\nexport const NotificationProviderSchema: GenEnum<NotificationProvider> = /*@__PURE__*/\n  enumDesc(file_openstatus_notification_v1_providers, 0);\n\n/**\n * OpsgenieRegion represents the Opsgenie API region.\n *\n * @generated from enum openstatus.notification.v1.OpsgenieRegion\n */\nexport enum OpsgenieRegion {\n  /**\n   * Unspecified region.\n   *\n   * @generated from enum value: OPSGENIE_REGION_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * US region.\n   *\n   * @generated from enum value: OPSGENIE_REGION_US = 1;\n   */\n  US = 1,\n\n  /**\n   * EU region.\n   *\n   * @generated from enum value: OPSGENIE_REGION_EU = 2;\n   */\n  EU = 2,\n}\n\n/**\n * Describes the enum openstatus.notification.v1.OpsgenieRegion.\n */\nexport const OpsgenieRegionSchema: GenEnum<OpsgenieRegion> = /*@__PURE__*/\n  enumDesc(file_openstatus_notification_v1_providers, 1);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/notification/v1/service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/notification/v1/service.proto (package openstatus.notification.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { Notification, NotificationSummary } from \"./notification_pb.ts\";\nimport { file_openstatus_notification_v1_notification } from \"./notification_pb.ts\";\nimport type { NotificationData, NotificationProvider } from \"./providers_pb.ts\";\nimport { file_openstatus_notification_v1_providers } from \"./providers_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/notification/v1/service.proto.\n */\nexport const file_openstatus_notification_v1_service: GenFile = /*@__PURE__*/\n  fileDesc(\"CihvcGVuc3RhdHVzL25vdGlmaWNhdGlvbi92MS9zZXJ2aWNlLnByb3RvEhpvcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MSLzAQoZQ3JlYXRlTm90aWZpY2F0aW9uUmVxdWVzdBItCgRuYW1lGAEgASgJQh+6RxU6ExIRU2xhY2sgT3BzIENoYW5uZWy6SARyAhABEk4KCHByb3ZpZGVyGAIgASgOMjAub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuTm90aWZpY2F0aW9uUHJvdmlkZXJCCrpIB4IBBBABIAASQgoEZGF0YRgDIAEoCzIsLm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxLk5vdGlmaWNhdGlvbkRhdGFCBrpIA8gBARITCgttb25pdG9yX2lkcxgEIAMoCSJcChpDcmVhdGVOb3RpZmljYXRpb25SZXNwb25zZRI+Cgxub3RpZmljYXRpb24YASABKAsyKC5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5Ob3RpZmljYXRpb24iLQoWR2V0Tm90aWZpY2F0aW9uUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQASJZChdHZXROb3RpZmljYXRpb25SZXNwb25zZRI+Cgxub3RpZmljYXRpb24YASABKAsyKC5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5Ob3RpZmljYXRpb24ibAoYTGlzdE5vdGlmaWNhdGlvbnNSZXF1ZXN0Eh0KBWxpbWl0GAEgASgFQgm6SAYaBBhkKAFIAIgBARIcCgZvZmZzZXQYAiABKAVCB7pIBBoCKABIAYgBAUIICgZfbGltaXRCCQoHX29mZnNldCJ3ChlMaXN0Tm90aWZpY2F0aW9uc1Jlc3BvbnNlEkYKDW5vdGlmaWNhdGlvbnMYASADKAsyLy5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5Ob3RpZmljYXRpb25TdW1tYXJ5EhIKCnRvdGFsX3NpemUYAiABKAUiqwEKGVVwZGF0ZU5vdGlmaWNhdGlvblJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAESEQoEbmFtZRgCIAEoCUgAiAEBEj8KBGRhdGEYAyABKAsyLC5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5Ob3RpZmljYXRpb25EYXRhSAGIAQESEwoLbW9uaXRvcl9pZHMYBCADKAlCBwoFX25hbWVCBwoFX2RhdGEiXAoaVXBkYXRlTm90aWZpY2F0aW9uUmVzcG9uc2USPgoMbm90aWZpY2F0aW9uGAEgASgLMigub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuTm90aWZpY2F0aW9uIjAKGURlbGV0ZU5vdGlmaWNhdGlvblJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAEiLQoaRGVsZXRlTm90aWZpY2F0aW9uUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCKxAQobU2VuZFRlc3ROb3RpZmljYXRpb25SZXF1ZXN0Ek4KCHByb3ZpZGVyGAEgASgOMjAub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuTm90aWZpY2F0aW9uUHJvdmlkZXJCCrpIB4IBBBABIAASQgoEZGF0YRgCIAEoCzIsLm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxLk5vdGlmaWNhdGlvbkRhdGFCBrpIA8gBASJdChxTZW5kVGVzdE5vdGlmaWNhdGlvblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSGgoNZXJyb3JfbWVzc2FnZRgCIAEoCUgAiAEBQhAKDl9lcnJvcl9tZXNzYWdlIh8KHUNoZWNrTm90aWZpY2F0aW9uTGltaXRSZXF1ZXN0ImEKHkNoZWNrTm90aWZpY2F0aW9uTGltaXRSZXNwb25zZRIVCg1saW1pdF9yZWFjaGVkGAEgASgIEhUKDWN1cnJlbnRfY291bnQYAiABKAUSEQoJbWF4X2NvdW50GAMgASgFMvEJChNOb3RpZmljYXRpb25TZXJ2aWNlEoMBChJDcmVhdGVOb3RpZmljYXRpb24SNS5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5DcmVhdGVOb3RpZmljYXRpb25SZXF1ZXN0GjYub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuQ3JlYXRlTm90aWZpY2F0aW9uUmVzcG9uc2USfwoPR2V0Tm90aWZpY2F0aW9uEjIub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuR2V0Tm90aWZpY2F0aW9uUmVxdWVzdBozLm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxLkdldE5vdGlmaWNhdGlvblJlc3BvbnNlIgOQAgEShQEKEUxpc3ROb3RpZmljYXRpb25zEjQub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuTGlzdE5vdGlmaWNhdGlvbnNSZXF1ZXN0GjUub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuTGlzdE5vdGlmaWNhdGlvbnNSZXNwb25zZSIDkAIBEoMBChJVcGRhdGVOb3RpZmljYXRpb24SNS5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5VcGRhdGVOb3RpZmljYXRpb25SZXF1ZXN0GjYub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuVXBkYXRlTm90aWZpY2F0aW9uUmVzcG9uc2USgwEKEkRlbGV0ZU5vdGlmaWNhdGlvbhI1Lm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxLkRlbGV0ZU5vdGlmaWNhdGlvblJlcXVlc3QaNi5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5EZWxldGVOb3RpZmljYXRpb25SZXNwb25zZRKnAwoUU2VuZFRlc3ROb3RpZmljYXRpb24SNy5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5TZW5kVGVzdE5vdGlmaWNhdGlvblJlcXVlc3QaOC5vcGVuc3RhdHVzLm5vdGlmaWNhdGlvbi52MS5TZW5kVGVzdE5vdGlmaWNhdGlvblJlc3BvbnNlIpsCukeXAhqUAlNlbmRzIGEgdGVzdCBub3RpZmljYXRpb24gdG8gdGhlIHNwZWNpZmllZCBwcm92aWRlciB0byB2ZXJpZnkgdGhhdCB0aGUgY29uZmlndXJhdGlvbiBpcyBjb3JyZWN0LiBUaGlzIGRvZXMgbm90IHJlcXVpcmUgYW4gZXhpc3Rpbmcgbm90aWZpY2F0aW9uIGNoYW5uZWwgLSBqdXN0IHByb3ZpZGUgdGhlIHByb3ZpZGVyIHR5cGUgYW5kIGl0cyBjb25maWd1cmF0aW9uIGRhdGEuIFJldHVybnMgc3VjY2VzcyBzdGF0dXMgYW5kIGFuIGVycm9yIG1lc3NhZ2UgaWYgdGhlIHRlc3QgZmFpbGVkLhKUAQoWQ2hlY2tOb3RpZmljYXRpb25MaW1pdBI5Lm9wZW5zdGF0dXMubm90aWZpY2F0aW9uLnYxLkNoZWNrTm90aWZpY2F0aW9uTGltaXRSZXF1ZXN0Gjoub3BlbnN0YXR1cy5ub3RpZmljYXRpb24udjEuQ2hlY2tOb3RpZmljYXRpb25MaW1pdFJlc3BvbnNlIgOQAgFCXVpbZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL25vdGlmaWNhdGlvbi92MTtub3RpZmljYXRpb252MWIGcHJvdG8z\", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_notification_v1_notification, file_openstatus_notification_v1_providers]);\n\n/**\n * CreateNotificationRequest is the request to create a new notification channel.\n *\n * @generated from message openstatus.notification.v1.CreateNotificationRequest\n */\nexport type CreateNotificationRequest = Message<\"openstatus.notification.v1.CreateNotificationRequest\"> & {\n  /**\n   * Display name for the notification channel.\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Provider type.\n   *\n   * @generated from field: openstatus.notification.v1.NotificationProvider provider = 2;\n   */\n  provider: NotificationProvider;\n\n  /**\n   * Provider-specific configuration.\n   *\n   * @generated from field: openstatus.notification.v1.NotificationData data = 3;\n   */\n  data?: NotificationData;\n\n  /**\n   * IDs of monitors to associate with this notification.\n   *\n   * @generated from field: repeated string monitor_ids = 4;\n   */\n  monitorIds: string[];\n};\n\n/**\n * Describes the message openstatus.notification.v1.CreateNotificationRequest.\n * Use `create(CreateNotificationRequestSchema)` to create a new message.\n */\nexport const CreateNotificationRequestSchema: GenMessage<CreateNotificationRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 0);\n\n/**\n * CreateNotificationResponse is the response after creating a notification channel.\n *\n * @generated from message openstatus.notification.v1.CreateNotificationResponse\n */\nexport type CreateNotificationResponse = Message<\"openstatus.notification.v1.CreateNotificationResponse\"> & {\n  /**\n   * The created notification channel.\n   *\n   * @generated from field: openstatus.notification.v1.Notification notification = 1;\n   */\n  notification?: Notification;\n};\n\n/**\n * Describes the message openstatus.notification.v1.CreateNotificationResponse.\n * Use `create(CreateNotificationResponseSchema)` to create a new message.\n */\nexport const CreateNotificationResponseSchema: GenMessage<CreateNotificationResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 1);\n\n/**\n * GetNotificationRequest is the request to get a notification channel.\n *\n * @generated from message openstatus.notification.v1.GetNotificationRequest\n */\nexport type GetNotificationRequest = Message<\"openstatus.notification.v1.GetNotificationRequest\"> & {\n  /**\n   * Notification ID to retrieve (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.GetNotificationRequest.\n * Use `create(GetNotificationRequestSchema)` to create a new message.\n */\nexport const GetNotificationRequestSchema: GenMessage<GetNotificationRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 2);\n\n/**\n * GetNotificationResponse is the response containing the notification channel.\n *\n * @generated from message openstatus.notification.v1.GetNotificationResponse\n */\nexport type GetNotificationResponse = Message<\"openstatus.notification.v1.GetNotificationResponse\"> & {\n  /**\n   * The notification channel.\n   *\n   * @generated from field: openstatus.notification.v1.Notification notification = 1;\n   */\n  notification?: Notification;\n};\n\n/**\n * Describes the message openstatus.notification.v1.GetNotificationResponse.\n * Use `create(GetNotificationResponseSchema)` to create a new message.\n */\nexport const GetNotificationResponseSchema: GenMessage<GetNotificationResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 3);\n\n/**\n * ListNotificationsRequest is the request to list notification channels.\n *\n * @generated from message openstatus.notification.v1.ListNotificationsRequest\n */\nexport type ListNotificationsRequest = Message<\"openstatus.notification.v1.ListNotificationsRequest\"> & {\n  /**\n   * Maximum number of notifications to return (1-100, defaults to 50).\n   *\n   * @generated from field: optional int32 limit = 1;\n   */\n  limit?: number;\n\n  /**\n   * Number of notifications to skip for pagination (defaults to 0).\n   *\n   * @generated from field: optional int32 offset = 2;\n   */\n  offset?: number;\n};\n\n/**\n * Describes the message openstatus.notification.v1.ListNotificationsRequest.\n * Use `create(ListNotificationsRequestSchema)` to create a new message.\n */\nexport const ListNotificationsRequestSchema: GenMessage<ListNotificationsRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 4);\n\n/**\n * ListNotificationsResponse is the response containing notification channels.\n *\n * @generated from message openstatus.notification.v1.ListNotificationsResponse\n */\nexport type ListNotificationsResponse = Message<\"openstatus.notification.v1.ListNotificationsResponse\"> & {\n  /**\n   * Notification channel summaries.\n   *\n   * @generated from field: repeated openstatus.notification.v1.NotificationSummary notifications = 1;\n   */\n  notifications: NotificationSummary[];\n\n  /**\n   * Total number of notification channels.\n   *\n   * @generated from field: int32 total_size = 2;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message openstatus.notification.v1.ListNotificationsResponse.\n * Use `create(ListNotificationsResponseSchema)` to create a new message.\n */\nexport const ListNotificationsResponseSchema: GenMessage<ListNotificationsResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 5);\n\n/**\n * UpdateNotificationRequest is the request to update a notification channel.\n *\n * @generated from message openstatus.notification.v1.UpdateNotificationRequest\n */\nexport type UpdateNotificationRequest = Message<\"openstatus.notification.v1.UpdateNotificationRequest\"> & {\n  /**\n   * Notification ID to update (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Updated display name.\n   *\n   * @generated from field: optional string name = 2;\n   */\n  name?: string;\n\n  /**\n   * Updated provider-specific configuration.\n   *\n   * @generated from field: optional openstatus.notification.v1.NotificationData data = 3;\n   */\n  data?: NotificationData;\n\n  /**\n   * Updated monitor IDs to associate.\n   *\n   * @generated from field: repeated string monitor_ids = 4;\n   */\n  monitorIds: string[];\n};\n\n/**\n * Describes the message openstatus.notification.v1.UpdateNotificationRequest.\n * Use `create(UpdateNotificationRequestSchema)` to create a new message.\n */\nexport const UpdateNotificationRequestSchema: GenMessage<UpdateNotificationRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 6);\n\n/**\n * UpdateNotificationResponse is the response after updating a notification channel.\n *\n * @generated from message openstatus.notification.v1.UpdateNotificationResponse\n */\nexport type UpdateNotificationResponse = Message<\"openstatus.notification.v1.UpdateNotificationResponse\"> & {\n  /**\n   * The updated notification channel.\n   *\n   * @generated from field: openstatus.notification.v1.Notification notification = 1;\n   */\n  notification?: Notification;\n};\n\n/**\n * Describes the message openstatus.notification.v1.UpdateNotificationResponse.\n * Use `create(UpdateNotificationResponseSchema)` to create a new message.\n */\nexport const UpdateNotificationResponseSchema: GenMessage<UpdateNotificationResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 7);\n\n/**\n * DeleteNotificationRequest is the request to delete a notification channel.\n *\n * @generated from message openstatus.notification.v1.DeleteNotificationRequest\n */\nexport type DeleteNotificationRequest = Message<\"openstatus.notification.v1.DeleteNotificationRequest\"> & {\n  /**\n   * Notification ID to delete (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.DeleteNotificationRequest.\n * Use `create(DeleteNotificationRequestSchema)` to create a new message.\n */\nexport const DeleteNotificationRequestSchema: GenMessage<DeleteNotificationRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 8);\n\n/**\n * DeleteNotificationResponse is the response after deleting a notification channel.\n *\n * @generated from message openstatus.notification.v1.DeleteNotificationResponse\n */\nexport type DeleteNotificationResponse = Message<\"openstatus.notification.v1.DeleteNotificationResponse\"> & {\n  /**\n   * Whether the deletion was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n};\n\n/**\n * Describes the message openstatus.notification.v1.DeleteNotificationResponse.\n * Use `create(DeleteNotificationResponseSchema)` to create a new message.\n */\nexport const DeleteNotificationResponseSchema: GenMessage<DeleteNotificationResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 9);\n\n/**\n * SendTestNotificationRequest is the request to send a test notification.\n *\n * @generated from message openstatus.notification.v1.SendTestNotificationRequest\n */\nexport type SendTestNotificationRequest = Message<\"openstatus.notification.v1.SendTestNotificationRequest\"> & {\n  /**\n   * Provider type.\n   *\n   * @generated from field: openstatus.notification.v1.NotificationProvider provider = 1;\n   */\n  provider: NotificationProvider;\n\n  /**\n   * Provider-specific configuration.\n   *\n   * @generated from field: openstatus.notification.v1.NotificationData data = 2;\n   */\n  data?: NotificationData;\n};\n\n/**\n * Describes the message openstatus.notification.v1.SendTestNotificationRequest.\n * Use `create(SendTestNotificationRequestSchema)` to create a new message.\n */\nexport const SendTestNotificationRequestSchema: GenMessage<SendTestNotificationRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 10);\n\n/**\n * SendTestNotificationResponse is the response after sending a test notification.\n *\n * @generated from message openstatus.notification.v1.SendTestNotificationResponse\n */\nexport type SendTestNotificationResponse = Message<\"openstatus.notification.v1.SendTestNotificationResponse\"> & {\n  /**\n   * Whether the test was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n\n  /**\n   * Optional error message if the test failed.\n   *\n   * @generated from field: optional string error_message = 2;\n   */\n  errorMessage?: string;\n};\n\n/**\n * Describes the message openstatus.notification.v1.SendTestNotificationResponse.\n * Use `create(SendTestNotificationResponseSchema)` to create a new message.\n */\nexport const SendTestNotificationResponseSchema: GenMessage<SendTestNotificationResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 11);\n\n/**\n * CheckNotificationLimitRequest is the request to check notification limits.\n *\n * @generated from message openstatus.notification.v1.CheckNotificationLimitRequest\n */\nexport type CheckNotificationLimitRequest = Message<\"openstatus.notification.v1.CheckNotificationLimitRequest\"> & {\n};\n\n/**\n * Describes the message openstatus.notification.v1.CheckNotificationLimitRequest.\n * Use `create(CheckNotificationLimitRequestSchema)` to create a new message.\n */\nexport const CheckNotificationLimitRequestSchema: GenMessage<CheckNotificationLimitRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 12);\n\n/**\n * CheckNotificationLimitResponse is the response containing limit information.\n *\n * @generated from message openstatus.notification.v1.CheckNotificationLimitResponse\n */\nexport type CheckNotificationLimitResponse = Message<\"openstatus.notification.v1.CheckNotificationLimitResponse\"> & {\n  /**\n   * Whether the workspace has reached its notification limit.\n   *\n   * @generated from field: bool limit_reached = 1;\n   */\n  limitReached: boolean;\n\n  /**\n   * Current number of notification channels.\n   *\n   * @generated from field: int32 current_count = 2;\n   */\n  currentCount: number;\n\n  /**\n   * Maximum allowed notification channels.\n   *\n   * @generated from field: int32 max_count = 3;\n   */\n  maxCount: number;\n};\n\n/**\n * Describes the message openstatus.notification.v1.CheckNotificationLimitResponse.\n * Use `create(CheckNotificationLimitResponseSchema)` to create a new message.\n */\nexport const CheckNotificationLimitResponseSchema: GenMessage<CheckNotificationLimitResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_notification_v1_service, 13);\n\n/**\n * NotificationService provides CRUD operations for notification channels.\n *\n * @generated from service openstatus.notification.v1.NotificationService\n */\nexport const NotificationService: GenService<{\n  /**\n   * CreateNotification creates a new notification channel.\n   *\n   * @generated from rpc openstatus.notification.v1.NotificationService.CreateNotification\n   */\n  createNotification: {\n    methodKind: \"unary\";\n    input: typeof CreateNotificationRequestSchema;\n    output: typeof CreateNotificationResponseSchema;\n  },\n  /**\n   * GetNotification retrieves a notification channel by ID.\n   *\n   * @generated from rpc openstatus.notification.v1.NotificationService.GetNotification\n   */\n  getNotification: {\n    methodKind: \"unary\";\n    input: typeof GetNotificationRequestSchema;\n    output: typeof GetNotificationResponseSchema;\n  },\n  /**\n   * ListNotifications returns a list of notification channels.\n   *\n   * @generated from rpc openstatus.notification.v1.NotificationService.ListNotifications\n   */\n  listNotifications: {\n    methodKind: \"unary\";\n    input: typeof ListNotificationsRequestSchema;\n    output: typeof ListNotificationsResponseSchema;\n  },\n  /**\n   * UpdateNotification updates an existing notification channel.\n   *\n   * @generated from rpc openstatus.notification.v1.NotificationService.UpdateNotification\n   */\n  updateNotification: {\n    methodKind: \"unary\";\n    input: typeof UpdateNotificationRequestSchema;\n    output: typeof UpdateNotificationResponseSchema;\n  },\n  /**\n   * DeleteNotification removes a notification channel.\n   *\n   * @generated from rpc openstatus.notification.v1.NotificationService.DeleteNotification\n   */\n  deleteNotification: {\n    methodKind: \"unary\";\n    input: typeof DeleteNotificationRequestSchema;\n    output: typeof DeleteNotificationResponseSchema;\n  },\n  /**\n   * SendTestNotification sends a test notification to verify the provider configuration without requiring an existing notification channel.\n   *\n   * @generated from rpc openstatus.notification.v1.NotificationService.SendTestNotification\n   */\n  sendTestNotification: {\n    methodKind: \"unary\";\n    input: typeof SendTestNotificationRequestSchema;\n    output: typeof SendTestNotificationResponseSchema;\n  },\n  /**\n   * CheckNotificationLimit checks if the workspace has reached its notification limit.\n   *\n   * @generated from rpc openstatus.notification.v1.NotificationService.CheckNotificationLimit\n   */\n  checkNotificationLimit: {\n    methodKind: \"unary\";\n    input: typeof CheckNotificationLimitRequestSchema;\n    output: typeof CheckNotificationLimitResponseSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_openstatus_notification_v1_service, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/status_page/v1/index.ts",
    "content": "// Status page service exports\nexport * from \"./status_page_pb.js\";\nexport * from \"./page_component_pb.js\";\nexport * from \"./page_subscriber_pb.js\";\nexport * from \"./service_pb.js\";\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/status_page/v1/page_component_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/status_page/v1/page_component.proto (package openstatus.status_page.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/status_page/v1/page_component.proto.\n */\nexport const file_openstatus_status_page_v1_page_component: GenFile = /*@__PURE__*/\n  fileDesc(\"Ci5vcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3BhZ2VfY29tcG9uZW50LnByb3RvEhlvcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxIv0BCg1QYWdlQ29tcG9uZW50EgoKAmlkGAEgASgJEg8KB3BhZ2VfaWQYAiABKAkSDAoEbmFtZRgDIAEoCRITCgtkZXNjcmlwdGlvbhgEIAEoCRI6CgR0eXBlGAUgASgOMiwub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50VHlwZRISCgptb25pdG9yX2lkGAYgASgJEg0KBW9yZGVyGAcgASgFEhAKCGdyb3VwX2lkGAggASgJEhMKC2dyb3VwX29yZGVyGAkgASgFEhIKCmNyZWF0ZWRfYXQYCiABKAkSEgoKdXBkYXRlZF9hdBgLIAEoCSJnChJQYWdlQ29tcG9uZW50R3JvdXASCgoCaWQYASABKAkSDwoHcGFnZV9pZBgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCmNyZWF0ZWRfYXQYBCABKAkSEgoKdXBkYXRlZF9hdBgFIAEoCSp5ChFQYWdlQ29tcG9uZW50VHlwZRIjCh9QQUdFX0NPTVBPTkVOVF9UWVBFX1VOU1BFQ0lGSUVEEAASHwobUEFHRV9DT01QT05FTlRfVFlQRV9NT05JVE9SEAESHgoaUEFHRV9DT01QT05FTlRfVFlQRV9TVEFUSUMQAkJaWlhnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvc3RhdHVzX3BhZ2UvdjE7c3RhdHVzcGFnZXYxYgZwcm90bzM\");\n\n/**\n * PageComponent represents a component displayed on a status page.\n *\n * @generated from message openstatus.status_page.v1.PageComponent\n */\nexport type PageComponent = Message<\"openstatus.status_page.v1.PageComponent\"> & {\n  /**\n   * Unique identifier for the component.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * ID of the status page this component belongs to.\n   *\n   * @generated from field: string page_id = 2;\n   */\n  pageId: string;\n\n  /**\n   * Display name of the component.\n   *\n   * @generated from field: string name = 3;\n   */\n  name: string;\n\n  /**\n   * Description of the component (optional).\n   *\n   * @generated from field: string description = 4;\n   */\n  description: string;\n\n  /**\n   * Type of the component (monitor or static).\n   *\n   * @generated from field: openstatus.status_page.v1.PageComponentType type = 5;\n   */\n  type: PageComponentType;\n\n  /**\n   * ID of the monitor if type is MONITOR (optional).\n   *\n   * @generated from field: string monitor_id = 6;\n   */\n  monitorId: string;\n\n  /**\n   * Display order of the component.\n   *\n   * @generated from field: int32 order = 7;\n   */\n  order: number;\n\n  /**\n   * ID of the group this component belongs to (optional).\n   *\n   * @generated from field: string group_id = 8;\n   */\n  groupId: string;\n\n  /**\n   * Order within the group if grouped.\n   *\n   * @generated from field: int32 group_order = 9;\n   */\n  groupOrder: number;\n\n  /**\n   * Timestamp when the component was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 10;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the component was last updated (RFC 3339 format).\n   *\n   * @generated from field: string updated_at = 11;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.PageComponent.\n * Use `create(PageComponentSchema)` to create a new message.\n */\nexport const PageComponentSchema: GenMessage<PageComponent> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_page_component, 0);\n\n/**\n * PageComponentGroup represents a group of components on a status page.\n *\n * @generated from message openstatus.status_page.v1.PageComponentGroup\n */\nexport type PageComponentGroup = Message<\"openstatus.status_page.v1.PageComponentGroup\"> & {\n  /**\n   * Unique identifier for the group.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * ID of the status page this group belongs to.\n   *\n   * @generated from field: string page_id = 2;\n   */\n  pageId: string;\n\n  /**\n   * Display name of the group.\n   *\n   * @generated from field: string name = 3;\n   */\n  name: string;\n\n  /**\n   * Timestamp when the group was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 4;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the group was last updated (RFC 3339 format).\n   *\n   * @generated from field: string updated_at = 5;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.PageComponentGroup.\n * Use `create(PageComponentGroupSchema)` to create a new message.\n */\nexport const PageComponentGroupSchema: GenMessage<PageComponentGroup> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_page_component, 1);\n\n/**\n * PageComponentType defines the type of a component on a status page.\n *\n * @generated from enum openstatus.status_page.v1.PageComponentType\n */\nexport enum PageComponentType {\n  /**\n   * @generated from enum value: PAGE_COMPONENT_TYPE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: PAGE_COMPONENT_TYPE_MONITOR = 1;\n   */\n  MONITOR = 1,\n\n  /**\n   * @generated from enum value: PAGE_COMPONENT_TYPE_STATIC = 2;\n   */\n  STATIC = 2,\n}\n\n/**\n * Describes the enum openstatus.status_page.v1.PageComponentType.\n */\nexport const PageComponentTypeSchema: GenEnum<PageComponentType> = /*@__PURE__*/\n  enumDesc(file_openstatus_status_page_v1_page_component, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/status_page/v1/page_subscriber_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/status_page/v1/page_subscriber.proto (package openstatus.status_page.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/status_page/v1/page_subscriber.proto.\n */\nexport const file_openstatus_status_page_v1_page_subscriber: GenFile = /*@__PURE__*/\n  fileDesc(\"Ci9vcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3BhZ2Vfc3Vic2NyaWJlci5wcm90bxIZb3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MSKSAQoOUGFnZVN1YnNjcmliZXISCgoCaWQYASABKAkSDwoHcGFnZV9pZBgCIAEoCRINCgVlbWFpbBgDIAEoCRITCgthY2NlcHRlZF9hdBgEIAEoCRIXCg91bnN1YnNjcmliZWRfYXQYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoCRISCgp1cGRhdGVkX2F0GAcgASgJQlpaWGdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9zdGF0dXNfcGFnZS92MTtzdGF0dXNwYWdldjFiBnByb3RvMw\");\n\n/**\n * PageSubscriber represents a subscriber to a status page.\n *\n * @generated from message openstatus.status_page.v1.PageSubscriber\n */\nexport type PageSubscriber = Message<\"openstatus.status_page.v1.PageSubscriber\"> & {\n  /**\n   * Unique identifier for the subscriber.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * ID of the status page the user is subscribed to.\n   *\n   * @generated from field: string page_id = 2;\n   */\n  pageId: string;\n\n  /**\n   * Email address of the subscriber.\n   *\n   * @generated from field: string email = 3;\n   */\n  email: string;\n\n  /**\n   * Timestamp when the subscription was accepted/confirmed (RFC 3339 format, optional).\n   *\n   * @generated from field: string accepted_at = 4;\n   */\n  acceptedAt: string;\n\n  /**\n   * Timestamp when the user unsubscribed (RFC 3339 format, optional).\n   *\n   * @generated from field: string unsubscribed_at = 5;\n   */\n  unsubscribedAt: string;\n\n  /**\n   * Timestamp when the subscription was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 6;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the subscription was last updated (RFC 3339 format).\n   *\n   * @generated from field: string updated_at = 7;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.PageSubscriber.\n * Use `create(PageSubscriberSchema)` to create a new message.\n */\nexport const PageSubscriberSchema: GenMessage<PageSubscriber> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_page_subscriber, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/status_page/v1/service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/status_page/v1/service.proto (package openstatus.status_page.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { MaintenanceSummary } from \"../../maintenance/v1/maintenance_pb.ts\";\nimport { file_openstatus_maintenance_v1_maintenance } from \"../../maintenance/v1/maintenance_pb.ts\";\nimport type { PageComponent, PageComponentGroup } from \"./page_component_pb.ts\";\nimport { file_openstatus_status_page_v1_page_component } from \"./page_component_pb.ts\";\nimport type { PageSubscriber } from \"./page_subscriber_pb.ts\";\nimport { file_openstatus_status_page_v1_page_subscriber } from \"./page_subscriber_pb.ts\";\nimport type { OverallStatus, StatusPage, StatusPageSummary } from \"./status_page_pb.ts\";\nimport { file_openstatus_status_page_v1_status_page } from \"./status_page_pb.ts\";\nimport type { StatusReport } from \"../../status_report/v1/status_report_pb.ts\";\nimport { file_openstatus_status_report_v1_status_report } from \"../../status_report/v1/status_report_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/status_page/v1/service.proto.\n */\nexport const file_openstatus_status_page_v1_service: GenFile = /*@__PURE__*/\n  fileDesc(\"CidvcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3NlcnZpY2UucHJvdG8SGW9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEiwAIKF0NyZWF0ZVN0YXR1c1BhZ2VSZXF1ZXN0EjAKBXRpdGxlGAEgASgJQiG6RxQ6EhIQQWNtZSBDb3JwIFN0YXR1c7pIB3IFEAEYgAISIgoLZGVzY3JpcHRpb24YAiABKAlCCLpIBXIDGIAISACIAQESSQoEc2x1ZxgDIAEoCUI7ukcSOhASDm15LXN0YXR1cy1wYWdlukgjciEQARiAAjIaXlthLXowLTldKyg/Oi1bYS16MC05XSspKiQSOQoMaG9tZXBhZ2VfdXJsGAQgASgJQh66Rxs6GRIXaHR0cHM6Ly93d3cuZXhhbXBsZS5jb21IAYgBARIYCgtjb250YWN0X3VybBgFIAEoCUgCiAEBQg4KDF9kZXNjcmlwdGlvbkIPCg1faG9tZXBhZ2VfdXJsQg4KDF9jb250YWN0X3VybCJWChhDcmVhdGVTdGF0dXNQYWdlUmVzcG9uc2USOgoLc3RhdHVzX3BhZ2UYASABKAsyJS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN0YXR1c1BhZ2UiKwoUR2V0U3RhdHVzUGFnZVJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAEiUwoVR2V0U3RhdHVzUGFnZVJlc3BvbnNlEjoKC3N0YXR1c19wYWdlGAEgASgLMiUub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5TdGF0dXNQYWdlImoKFkxpc3RTdGF0dXNQYWdlc1JlcXVlc3QSHQoFbGltaXQYASABKAVCCbpIBhoEGGQoAUgAiAEBEhwKBm9mZnNldBgCIAEoBUIHukgEGgIoAEgBiAEBQggKBl9saW1pdEIJCgdfb2Zmc2V0InEKF0xpc3RTdGF0dXNQYWdlc1Jlc3BvbnNlEkIKDHN0YXR1c19wYWdlcxgBIAMoCzIsLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuU3RhdHVzUGFnZVN1bW1hcnkSEgoKdG90YWxfc2l6ZRgCIAEoBSKmAgoXVXBkYXRlU3RhdHVzUGFnZVJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAESHgoFdGl0bGUYAiABKAlCCrpIB3IFEAEYgAJIAIgBARIiCgtkZXNjcmlwdGlvbhgDIAEoCUIIukgFcgMYgAhIAYgBARI5CgRzbHVnGAQgASgJQia6SCNyIRABGIACMhpeW2EtejAtOV0rKD86LVthLXowLTldKykqJEgCiAEBEhkKDGhvbWVwYWdlX3VybBgFIAEoCUgDiAEBEhgKC2NvbnRhY3RfdXJsGAYgASgJSASIAQFCCAoGX3RpdGxlQg4KDF9kZXNjcmlwdGlvbkIHCgVfc2x1Z0IPCg1faG9tZXBhZ2VfdXJsQg4KDF9jb250YWN0X3VybCJWChhVcGRhdGVTdGF0dXNQYWdlUmVzcG9uc2USOgoLc3RhdHVzX3BhZ2UYASABKAsyJS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlN0YXR1c1BhZ2UiLgoXRGVsZXRlU3RhdHVzUGFnZVJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAEiKwoYRGVsZXRlU3RhdHVzUGFnZVJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgi7wEKGkFkZE1vbml0b3JDb21wb25lbnRSZXF1ZXN0EhgKB3BhZ2VfaWQYASABKAlCB7pIBHICEAESGwoKbW9uaXRvcl9pZBgCIAEoCUIHukgEcgIQARIbCgRuYW1lGAMgASgJQgi6SAVyAxiAAkgAiAEBEiIKC2Rlc2NyaXB0aW9uGAQgASgJQgi6SAVyAxiACEgBiAEBEhIKBW9yZGVyGAUgASgFSAKIAQESFQoIZ3JvdXBfaWQYBiABKAlIA4gBAUIHCgVfbmFtZUIOCgxfZGVzY3JpcHRpb25CCAoGX29yZGVyQgsKCV9ncm91cF9pZCJaChtBZGRNb25pdG9yQ29tcG9uZW50UmVzcG9uc2USOwoJY29tcG9uZW50GAEgASgLMigub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50IsUBChlBZGRTdGF0aWNDb21wb25lbnRSZXF1ZXN0EhgKB3BhZ2VfaWQYASABKAlCB7pIBHICEAESGAoEbmFtZRgCIAEoCUIKukgHcgUQARiAAhIiCgtkZXNjcmlwdGlvbhgDIAEoCUIIukgFcgMYgAhIAIgBARISCgVvcmRlchgEIAEoBUgBiAEBEhUKCGdyb3VwX2lkGAUgASgJSAKIAQFCDgoMX2Rlc2NyaXB0aW9uQggKBl9vcmRlckILCglfZ3JvdXBfaWQiWQoaQWRkU3RhdGljQ29tcG9uZW50UmVzcG9uc2USOwoJY29tcG9uZW50GAEgASgLMigub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50Ii0KFlJlbW92ZUNvbXBvbmVudFJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAEiKgoXUmVtb3ZlQ29tcG9uZW50UmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCLzAQoWVXBkYXRlQ29tcG9uZW50UmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQARIbCgRuYW1lGAIgASgJQgi6SAVyAxiAAkgAiAEBEiIKC2Rlc2NyaXB0aW9uGAMgASgJQgi6SAVyAxiACEgBiAEBEhIKBW9yZGVyGAQgASgFSAKIAQESFQoIZ3JvdXBfaWQYBSABKAlIA4gBARIYCgtncm91cF9vcmRlchgGIAEoBUgEiAEBQgcKBV9uYW1lQg4KDF9kZXNjcmlwdGlvbkIICgZfb3JkZXJCCwoJX2dyb3VwX2lkQg4KDF9ncm91cF9vcmRlciJWChdVcGRhdGVDb21wb25lbnRSZXNwb25zZRI7Cgljb21wb25lbnQYASABKAsyKC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnQiUQobQ3JlYXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0EhgKB3BhZ2VfaWQYASABKAlCB7pIBHICEAESGAoEbmFtZRgCIAEoCUIKukgHcgUQARiAAiJcChxDcmVhdGVDb21wb25lbnRHcm91cFJlc3BvbnNlEjwKBWdyb3VwGAEgASgLMi0ub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50R3JvdXAiMgobRGVsZXRlQ29tcG9uZW50R3JvdXBSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIi8KHERlbGV0ZUNvbXBvbmVudEdyb3VwUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCJaChtVcGRhdGVDb21wb25lbnRHcm91cFJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAESHQoEbmFtZRgCIAEoCUIKukgHcgUQARiAAkgAiAEBQgcKBV9uYW1lIlwKHFVwZGF0ZUNvbXBvbmVudEdyb3VwUmVzcG9uc2USPAoFZ3JvdXAYASABKAsyLS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnRHcm91cCJhChZTdWJzY3JpYmVUb1BhZ2VSZXF1ZXN0EhgKB3BhZ2VfaWQYASABKAlCB7pIBHICEAESLQoFZW1haWwYAiABKAlCHrpHFDoSEhB1c2VyQGV4YW1wbGUuY29tukgEcgJgASJYChdTdWJzY3JpYmVUb1BhZ2VSZXNwb25zZRI9CgpzdWJzY3JpYmVyGAEgASgLMikub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlU3Vic2NyaWJlciJjChpVbnN1YnNjcmliZUZyb21QYWdlUmVxdWVzdBIYCgdwYWdlX2lkGAEgASgJQge6SARyAhABEg8KBWVtYWlsGAIgASgJSAASDAoCaWQYAyABKAlIAEIMCgppZGVudGlmaWVyIi4KG1Vuc3Vic2NyaWJlRnJvbVBhZ2VSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIsABChZMaXN0U3Vic2NyaWJlcnNSZXF1ZXN0EhgKB3BhZ2VfaWQYASABKAlCB7pIBHICEAESHQoFbGltaXQYAiABKAVCCbpIBhoEGGQoAUgAiAEBEhwKBm9mZnNldBgDIAEoBUIHukgEGgIoAEgBiAEBEiEKFGluY2x1ZGVfdW5zdWJzY3JpYmVkGAQgASgISAKIAQFCCAoGX2xpbWl0QgkKB19vZmZzZXRCFwoVX2luY2x1ZGVfdW5zdWJzY3JpYmVkIm0KF0xpc3RTdWJzY3JpYmVyc1Jlc3BvbnNlEj4KC3N1YnNjcmliZXJzGAEgAygLMikub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlU3Vic2NyaWJlchISCgp0b3RhbF9zaXplGAIgASgFIkkKG0dldFN0YXR1c1BhZ2VDb250ZW50UmVxdWVzdBIMCgJpZBgBIAEoCUgAEg4KBHNsdWcYAiABKAlIAEIMCgppZGVudGlmaWVyIt8CChxHZXRTdGF0dXNQYWdlQ29udGVudFJlc3BvbnNlEjoKC3N0YXR1c19wYWdlGAEgASgLMiUub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5TdGF0dXNQYWdlEjwKCmNvbXBvbmVudHMYAiADKAsyKC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VDb21wb25lbnQSPQoGZ3JvdXBzGAMgAygLMi0ub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlQ29tcG9uZW50R3JvdXASQQoOc3RhdHVzX3JlcG9ydHMYBCADKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuU3RhdHVzUmVwb3J0EkMKDG1haW50ZW5hbmNlcxgFIAMoCzItLm9wZW5zdGF0dXMubWFpbnRlbmFuY2UudjEuTWFpbnRlbmFuY2VTdW1tYXJ5IkUKF0dldE92ZXJhbGxTdGF0dXNSZXF1ZXN0EgwKAmlkGAEgASgJSAASDgoEc2x1ZxgCIAEoCUgAQgwKCmlkZW50aWZpZXIiYQoPQ29tcG9uZW50U3RhdHVzEhQKDGNvbXBvbmVudF9pZBgBIAEoCRI4CgZzdGF0dXMYAiABKA4yKC5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLk92ZXJhbGxTdGF0dXMipAEKGEdldE92ZXJhbGxTdGF0dXNSZXNwb25zZRJACg5vdmVyYWxsX3N0YXR1cxgBIAEoDjIoLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuT3ZlcmFsbFN0YXR1cxJGChJjb21wb25lbnRfc3RhdHVzZXMYAiADKAsyKi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNvbXBvbmVudFN0YXR1czLNFwoRU3RhdHVzUGFnZVNlcnZpY2USewoQQ3JlYXRlU3RhdHVzUGFnZRIyLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQ3JlYXRlU3RhdHVzUGFnZVJlcXVlc3QaMy5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNyZWF0ZVN0YXR1c1BhZ2VSZXNwb25zZRJ3Cg1HZXRTdGF0dXNQYWdlEi8ub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5HZXRTdGF0dXNQYWdlUmVxdWVzdBowLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0U3RhdHVzUGFnZVJlc3BvbnNlIgOQAgESfQoPTGlzdFN0YXR1c1BhZ2VzEjEub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5MaXN0U3RhdHVzUGFnZXNSZXF1ZXN0GjIub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5MaXN0U3RhdHVzUGFnZXNSZXNwb25zZSIDkAIBEnsKEFVwZGF0ZVN0YXR1c1BhZ2USMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlVwZGF0ZVN0YXR1c1BhZ2VSZXF1ZXN0GjMub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5VcGRhdGVTdGF0dXNQYWdlUmVzcG9uc2USewoQRGVsZXRlU3RhdHVzUGFnZRIyLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuRGVsZXRlU3RhdHVzUGFnZVJlcXVlc3QaMy5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkRlbGV0ZVN0YXR1c1BhZ2VSZXNwb25zZRKEAQoTQWRkTW9uaXRvckNvbXBvbmVudBI1Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuQWRkTW9uaXRvckNvbXBvbmVudFJlcXVlc3QaNi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkFkZE1vbml0b3JDb21wb25lbnRSZXNwb25zZRKBAQoSQWRkU3RhdGljQ29tcG9uZW50EjQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5BZGRTdGF0aWNDb21wb25lbnRSZXF1ZXN0GjUub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5BZGRTdGF0aWNDb21wb25lbnRSZXNwb25zZRJ4Cg9SZW1vdmVDb21wb25lbnQSMS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlJlbW92ZUNvbXBvbmVudFJlcXVlc3QaMi5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlJlbW92ZUNvbXBvbmVudFJlc3BvbnNlEngKD1VwZGF0ZUNvbXBvbmVudBIxLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuVXBkYXRlQ29tcG9uZW50UmVxdWVzdBoyLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuVXBkYXRlQ29tcG9uZW50UmVzcG9uc2UShwEKFENyZWF0ZUNvbXBvbmVudEdyb3VwEjYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5DcmVhdGVDb21wb25lbnRHcm91cFJlcXVlc3QaNy5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkNyZWF0ZUNvbXBvbmVudEdyb3VwUmVzcG9uc2UShwEKFERlbGV0ZUNvbXBvbmVudEdyb3VwEjYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5EZWxldGVDb21wb25lbnRHcm91cFJlcXVlc3QaNy5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkRlbGV0ZUNvbXBvbmVudEdyb3VwUmVzcG9uc2UShwEKFFVwZGF0ZUNvbXBvbmVudEdyb3VwEjYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5VcGRhdGVDb21wb25lbnRHcm91cFJlcXVlc3QaNy5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlVwZGF0ZUNvbXBvbmVudEdyb3VwUmVzcG9uc2UStQIKD1N1YnNjcmliZVRvUGFnZRIxLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuU3Vic2NyaWJlVG9QYWdlUmVxdWVzdBoyLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuU3Vic2NyaWJlVG9QYWdlUmVzcG9uc2UiugG6R7YBGrMBU3Vic2NyaWJlcyBhbiBlbWFpbCBhZGRyZXNzIHRvIHJlY2VpdmUgbm90aWZpY2F0aW9ucyBmcm9tIGEgc3RhdHVzIHBhZ2UuIElmIHRoZSBlbWFpbCB3YXMgcHJldmlvdXNseSB1bnN1YnNjcmliZWQsIHRoZSBzdWJzY3JpcHRpb24gaXMgcmVhY3RpdmF0ZWQgaW5zdGVhZCBvZiBjcmVhdGluZyBhIGR1cGxpY2F0ZS4ShAEKE1Vuc3Vic2NyaWJlRnJvbVBhZ2USNS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlVuc3Vic2NyaWJlRnJvbVBhZ2VSZXF1ZXN0GjYub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5VbnN1YnNjcmliZUZyb21QYWdlUmVzcG9uc2USfQoPTGlzdFN1YnNjcmliZXJzEjEub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5MaXN0U3Vic2NyaWJlcnNSZXF1ZXN0GjIub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5MaXN0U3Vic2NyaWJlcnNSZXNwb25zZSIDkAIBEsADChRHZXRTdGF0dXNQYWdlQ29udGVudBI2Lm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0U3RhdHVzUGFnZUNvbnRlbnRSZXF1ZXN0Gjcub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5HZXRTdGF0dXNQYWdlQ29udGVudFJlc3BvbnNlIrYCkAIBukevAhqsAlJldHVybnMgdGhlIGZ1bGwgY29udGVudCBvZiBhIHN0YXR1cyBwYWdlIGluY2x1ZGluZyBpdHMgY29tcG9uZW50cywgY29tcG9uZW50IGdyb3VwcywgYWN0aXZlIHN0YXR1cyByZXBvcnRzLCBhbmQgc2NoZWR1bGVkIG1haW50ZW5hbmNlcy4gU3VwcG9ydHMgdHdvIGFjY2VzcyBwYXRoczogYnkgSUQgKHJlcXVpcmVzIGF1dGhlbnRpY2F0aW9uLCB3b3Jrc3BhY2Utc2NvcGVkKSBvciBieSBzbHVnIChwdWJsaWMgYWNjZXNzLCByZXF1aXJlcyB0aGUgcGFnZSB0byBiZSBwdWJsaXNoZWQgd2l0aCBhY2Nlc3NfdHlwZT1QVUJMSUMpLhKqAwoQR2V0T3ZlcmFsbFN0YXR1cxIyLm9wZW5zdGF0dXMuc3RhdHVzX3BhZ2UudjEuR2V0T3ZlcmFsbFN0YXR1c1JlcXVlc3QaMy5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLkdldE92ZXJhbGxTdGF0dXNSZXNwb25zZSKsApACAbpHpQIaogJSZXR1cm5zIHRoZSBvdmVyYWxsIHN0YXR1cyBvZiBhIHN0YXR1cyBwYWdlIGFsb25nIHdpdGggaW5kaXZpZHVhbCBjb21wb25lbnQgc3RhdHVzZXMuIFRoZSBvdmVyYWxsIHN0YXR1cyBpcyBjb21wdXRlZCBmcm9tIGFjdGl2ZSBzdGF0dXMgcmVwb3J0cyBhbmQgbWFpbnRlbmFuY2VzIHdpdGggdGhlIGZvbGxvd2luZyBwcmlvcml0eTogZGVncmFkZWQgKGZyb20gYWN0aXZlIHN0YXR1cyByZXBvcnRzKSA+IG1haW50ZW5hbmNlIChmcm9tIGFjdGl2ZSBtYWludGVuYW5jZSB3aW5kb3dzKSA+IG9wZXJhdGlvbmFsLkJaWlhnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvc3RhdHVzX3BhZ2UvdjE7c3RhdHVzcGFnZXYxYgZwcm90bzM\", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_maintenance_v1_maintenance, file_openstatus_status_page_v1_page_component, file_openstatus_status_page_v1_page_subscriber, file_openstatus_status_page_v1_status_page, file_openstatus_status_report_v1_status_report]);\n\n/**\n * CreateStatusPageRequest is the request to create a new status page.\n *\n * @generated from message openstatus.status_page.v1.CreateStatusPageRequest\n */\nexport type CreateStatusPageRequest = Message<\"openstatus.status_page.v1.CreateStatusPageRequest\"> & {\n  /**\n   * Title of the status page (required).\n   *\n   * @generated from field: string title = 1;\n   */\n  title: string;\n\n  /**\n   * Description of the status page (optional).\n   *\n   * @generated from field: optional string description = 2;\n   */\n  description?: string;\n\n  /**\n   * URL-friendly slug for the status page (required). Must be lowercase alphanumeric with hyphens.\n   *\n   * @generated from field: string slug = 3;\n   */\n  slug: string;\n\n  /**\n   * URL to the homepage (optional).\n   *\n   * @generated from field: optional string homepage_url = 4;\n   */\n  homepageUrl?: string;\n\n  /**\n   * URL to the contact page (optional).\n   *\n   * @generated from field: optional string contact_url = 5;\n   */\n  contactUrl?: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.CreateStatusPageRequest.\n * Use `create(CreateStatusPageRequestSchema)` to create a new message.\n */\nexport const CreateStatusPageRequestSchema: GenMessage<CreateStatusPageRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 0);\n\n/**\n * CreateStatusPageResponse is the response after creating a status page.\n *\n * @generated from message openstatus.status_page.v1.CreateStatusPageResponse\n */\nexport type CreateStatusPageResponse = Message<\"openstatus.status_page.v1.CreateStatusPageResponse\"> & {\n  /**\n   * The created status page.\n   *\n   * @generated from field: openstatus.status_page.v1.StatusPage status_page = 1;\n   */\n  statusPage?: StatusPage;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.CreateStatusPageResponse.\n * Use `create(CreateStatusPageResponseSchema)` to create a new message.\n */\nexport const CreateStatusPageResponseSchema: GenMessage<CreateStatusPageResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 1);\n\n/**\n * GetStatusPageRequest is the request to get a status page by ID.\n *\n * @generated from message openstatus.status_page.v1.GetStatusPageRequest\n */\nexport type GetStatusPageRequest = Message<\"openstatus.status_page.v1.GetStatusPageRequest\"> & {\n  /**\n   * ID of the status page to retrieve (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.GetStatusPageRequest.\n * Use `create(GetStatusPageRequestSchema)` to create a new message.\n */\nexport const GetStatusPageRequestSchema: GenMessage<GetStatusPageRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 2);\n\n/**\n * GetStatusPageResponse is the response containing the status page.\n *\n * @generated from message openstatus.status_page.v1.GetStatusPageResponse\n */\nexport type GetStatusPageResponse = Message<\"openstatus.status_page.v1.GetStatusPageResponse\"> & {\n  /**\n   * The requested status page.\n   *\n   * @generated from field: openstatus.status_page.v1.StatusPage status_page = 1;\n   */\n  statusPage?: StatusPage;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.GetStatusPageResponse.\n * Use `create(GetStatusPageResponseSchema)` to create a new message.\n */\nexport const GetStatusPageResponseSchema: GenMessage<GetStatusPageResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 3);\n\n/**\n * ListStatusPagesRequest is the request to list status pages.\n *\n * @generated from message openstatus.status_page.v1.ListStatusPagesRequest\n */\nexport type ListStatusPagesRequest = Message<\"openstatus.status_page.v1.ListStatusPagesRequest\"> & {\n  /**\n   * Maximum number of pages to return (1-100, defaults to 50).\n   *\n   * @generated from field: optional int32 limit = 1;\n   */\n  limit?: number;\n\n  /**\n   * Number of pages to skip for pagination (defaults to 0).\n   *\n   * @generated from field: optional int32 offset = 2;\n   */\n  offset?: number;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.ListStatusPagesRequest.\n * Use `create(ListStatusPagesRequestSchema)` to create a new message.\n */\nexport const ListStatusPagesRequestSchema: GenMessage<ListStatusPagesRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 4);\n\n/**\n * ListStatusPagesResponse is the response containing status page summaries.\n *\n * @generated from message openstatus.status_page.v1.ListStatusPagesResponse\n */\nexport type ListStatusPagesResponse = Message<\"openstatus.status_page.v1.ListStatusPagesResponse\"> & {\n  /**\n   * List of status pages (metadata only).\n   *\n   * @generated from field: repeated openstatus.status_page.v1.StatusPageSummary status_pages = 1;\n   */\n  statusPages: StatusPageSummary[];\n\n  /**\n   * Total number of status pages.\n   *\n   * @generated from field: int32 total_size = 2;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.ListStatusPagesResponse.\n * Use `create(ListStatusPagesResponseSchema)` to create a new message.\n */\nexport const ListStatusPagesResponseSchema: GenMessage<ListStatusPagesResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 5);\n\n/**\n * UpdateStatusPageRequest is the request to update a status page.\n *\n * @generated from message openstatus.status_page.v1.UpdateStatusPageRequest\n */\nexport type UpdateStatusPageRequest = Message<\"openstatus.status_page.v1.UpdateStatusPageRequest\"> & {\n  /**\n   * ID of the status page to update (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * New title for the status page (optional).\n   *\n   * @generated from field: optional string title = 2;\n   */\n  title?: string;\n\n  /**\n   * New description for the status page (optional).\n   *\n   * @generated from field: optional string description = 3;\n   */\n  description?: string;\n\n  /**\n   * New slug for the status page (optional).\n   *\n   * @generated from field: optional string slug = 4;\n   */\n  slug?: string;\n\n  /**\n   * New homepage URL (optional).\n   *\n   * @generated from field: optional string homepage_url = 5;\n   */\n  homepageUrl?: string;\n\n  /**\n   * New contact URL (optional).\n   *\n   * @generated from field: optional string contact_url = 6;\n   */\n  contactUrl?: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.UpdateStatusPageRequest.\n * Use `create(UpdateStatusPageRequestSchema)` to create a new message.\n */\nexport const UpdateStatusPageRequestSchema: GenMessage<UpdateStatusPageRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 6);\n\n/**\n * UpdateStatusPageResponse is the response after updating a status page.\n *\n * @generated from message openstatus.status_page.v1.UpdateStatusPageResponse\n */\nexport type UpdateStatusPageResponse = Message<\"openstatus.status_page.v1.UpdateStatusPageResponse\"> & {\n  /**\n   * The updated status page.\n   *\n   * @generated from field: openstatus.status_page.v1.StatusPage status_page = 1;\n   */\n  statusPage?: StatusPage;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.UpdateStatusPageResponse.\n * Use `create(UpdateStatusPageResponseSchema)` to create a new message.\n */\nexport const UpdateStatusPageResponseSchema: GenMessage<UpdateStatusPageResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 7);\n\n/**\n * DeleteStatusPageRequest is the request to delete a status page.\n *\n * @generated from message openstatus.status_page.v1.DeleteStatusPageRequest\n */\nexport type DeleteStatusPageRequest = Message<\"openstatus.status_page.v1.DeleteStatusPageRequest\"> & {\n  /**\n   * ID of the status page to delete (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.DeleteStatusPageRequest.\n * Use `create(DeleteStatusPageRequestSchema)` to create a new message.\n */\nexport const DeleteStatusPageRequestSchema: GenMessage<DeleteStatusPageRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 8);\n\n/**\n * DeleteStatusPageResponse is the response after deleting a status page.\n *\n * @generated from message openstatus.status_page.v1.DeleteStatusPageResponse\n */\nexport type DeleteStatusPageResponse = Message<\"openstatus.status_page.v1.DeleteStatusPageResponse\"> & {\n  /**\n   * Whether the deletion was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.DeleteStatusPageResponse.\n * Use `create(DeleteStatusPageResponseSchema)` to create a new message.\n */\nexport const DeleteStatusPageResponseSchema: GenMessage<DeleteStatusPageResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 9);\n\n/**\n * AddMonitorComponentRequest is the request to add a monitor-based component to a status page.\n *\n * @generated from message openstatus.status_page.v1.AddMonitorComponentRequest\n */\nexport type AddMonitorComponentRequest = Message<\"openstatus.status_page.v1.AddMonitorComponentRequest\"> & {\n  /**\n   * ID of the status page to add the component to (required).\n   *\n   * @generated from field: string page_id = 1;\n   */\n  pageId: string;\n\n  /**\n   * ID of the monitor to associate with this component (required).\n   *\n   * @generated from field: string monitor_id = 2;\n   */\n  monitorId: string;\n\n  /**\n   * Display name for the component (optional, defaults to monitor name).\n   *\n   * @generated from field: optional string name = 3;\n   */\n  name?: string;\n\n  /**\n   * Description of the component (optional).\n   *\n   * @generated from field: optional string description = 4;\n   */\n  description?: string;\n\n  /**\n   * Display order of the component (optional).\n   *\n   * @generated from field: optional int32 order = 5;\n   */\n  order?: number;\n\n  /**\n   * ID of the group to add this component to (optional).\n   *\n   * @generated from field: optional string group_id = 6;\n   */\n  groupId?: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.AddMonitorComponentRequest.\n * Use `create(AddMonitorComponentRequestSchema)` to create a new message.\n */\nexport const AddMonitorComponentRequestSchema: GenMessage<AddMonitorComponentRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 10);\n\n/**\n * AddMonitorComponentResponse is the response after adding a monitor component.\n *\n * @generated from message openstatus.status_page.v1.AddMonitorComponentResponse\n */\nexport type AddMonitorComponentResponse = Message<\"openstatus.status_page.v1.AddMonitorComponentResponse\"> & {\n  /**\n   * The created component.\n   *\n   * @generated from field: openstatus.status_page.v1.PageComponent component = 1;\n   */\n  component?: PageComponent;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.AddMonitorComponentResponse.\n * Use `create(AddMonitorComponentResponseSchema)` to create a new message.\n */\nexport const AddMonitorComponentResponseSchema: GenMessage<AddMonitorComponentResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 11);\n\n/**\n * AddStaticComponentRequest is the request to add a static component to a status page.\n *\n * @generated from message openstatus.status_page.v1.AddStaticComponentRequest\n */\nexport type AddStaticComponentRequest = Message<\"openstatus.status_page.v1.AddStaticComponentRequest\"> & {\n  /**\n   * ID of the status page to add the component to (required).\n   *\n   * @generated from field: string page_id = 1;\n   */\n  pageId: string;\n\n  /**\n   * Display name for the component (required).\n   *\n   * @generated from field: string name = 2;\n   */\n  name: string;\n\n  /**\n   * Description of the component (optional).\n   *\n   * @generated from field: optional string description = 3;\n   */\n  description?: string;\n\n  /**\n   * Display order of the component (optional).\n   *\n   * @generated from field: optional int32 order = 4;\n   */\n  order?: number;\n\n  /**\n   * ID of the group to add this component to (optional).\n   *\n   * @generated from field: optional string group_id = 5;\n   */\n  groupId?: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.AddStaticComponentRequest.\n * Use `create(AddStaticComponentRequestSchema)` to create a new message.\n */\nexport const AddStaticComponentRequestSchema: GenMessage<AddStaticComponentRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 12);\n\n/**\n * AddStaticComponentResponse is the response after adding a static component.\n *\n * @generated from message openstatus.status_page.v1.AddStaticComponentResponse\n */\nexport type AddStaticComponentResponse = Message<\"openstatus.status_page.v1.AddStaticComponentResponse\"> & {\n  /**\n   * The created component.\n   *\n   * @generated from field: openstatus.status_page.v1.PageComponent component = 1;\n   */\n  component?: PageComponent;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.AddStaticComponentResponse.\n * Use `create(AddStaticComponentResponseSchema)` to create a new message.\n */\nexport const AddStaticComponentResponseSchema: GenMessage<AddStaticComponentResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 13);\n\n/**\n * RemoveComponentRequest is the request to remove a component from a status page.\n *\n * @generated from message openstatus.status_page.v1.RemoveComponentRequest\n */\nexport type RemoveComponentRequest = Message<\"openstatus.status_page.v1.RemoveComponentRequest\"> & {\n  /**\n   * ID of the component to remove (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.RemoveComponentRequest.\n * Use `create(RemoveComponentRequestSchema)` to create a new message.\n */\nexport const RemoveComponentRequestSchema: GenMessage<RemoveComponentRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 14);\n\n/**\n * RemoveComponentResponse is the response after removing a component.\n *\n * @generated from message openstatus.status_page.v1.RemoveComponentResponse\n */\nexport type RemoveComponentResponse = Message<\"openstatus.status_page.v1.RemoveComponentResponse\"> & {\n  /**\n   * Whether the removal was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.RemoveComponentResponse.\n * Use `create(RemoveComponentResponseSchema)` to create a new message.\n */\nexport const RemoveComponentResponseSchema: GenMessage<RemoveComponentResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 15);\n\n/**\n * UpdateComponentRequest is the request to update a component.\n *\n * @generated from message openstatus.status_page.v1.UpdateComponentRequest\n */\nexport type UpdateComponentRequest = Message<\"openstatus.status_page.v1.UpdateComponentRequest\"> & {\n  /**\n   * ID of the component to update (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * New display name for the component (optional).\n   *\n   * @generated from field: optional string name = 2;\n   */\n  name?: string;\n\n  /**\n   * New description for the component (optional).\n   *\n   * @generated from field: optional string description = 3;\n   */\n  description?: string;\n\n  /**\n   * New display order (optional).\n   *\n   * @generated from field: optional int32 order = 4;\n   */\n  order?: number;\n\n  /**\n   * New group ID (optional, set to empty string to remove from group).\n   *\n   * @generated from field: optional string group_id = 5;\n   */\n  groupId?: string;\n\n  /**\n   * New order within the group (optional).\n   *\n   * @generated from field: optional int32 group_order = 6;\n   */\n  groupOrder?: number;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.UpdateComponentRequest.\n * Use `create(UpdateComponentRequestSchema)` to create a new message.\n */\nexport const UpdateComponentRequestSchema: GenMessage<UpdateComponentRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 16);\n\n/**\n * UpdateComponentResponse is the response after updating a component.\n *\n * @generated from message openstatus.status_page.v1.UpdateComponentResponse\n */\nexport type UpdateComponentResponse = Message<\"openstatus.status_page.v1.UpdateComponentResponse\"> & {\n  /**\n   * The updated component.\n   *\n   * @generated from field: openstatus.status_page.v1.PageComponent component = 1;\n   */\n  component?: PageComponent;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.UpdateComponentResponse.\n * Use `create(UpdateComponentResponseSchema)` to create a new message.\n */\nexport const UpdateComponentResponseSchema: GenMessage<UpdateComponentResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 17);\n\n/**\n * CreateComponentGroupRequest is the request to create a new component group.\n *\n * @generated from message openstatus.status_page.v1.CreateComponentGroupRequest\n */\nexport type CreateComponentGroupRequest = Message<\"openstatus.status_page.v1.CreateComponentGroupRequest\"> & {\n  /**\n   * ID of the status page to create the group in (required).\n   *\n   * @generated from field: string page_id = 1;\n   */\n  pageId: string;\n\n  /**\n   * Display name for the group (required).\n   *\n   * @generated from field: string name = 2;\n   */\n  name: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.CreateComponentGroupRequest.\n * Use `create(CreateComponentGroupRequestSchema)` to create a new message.\n */\nexport const CreateComponentGroupRequestSchema: GenMessage<CreateComponentGroupRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 18);\n\n/**\n * CreateComponentGroupResponse is the response after creating a component group.\n *\n * @generated from message openstatus.status_page.v1.CreateComponentGroupResponse\n */\nexport type CreateComponentGroupResponse = Message<\"openstatus.status_page.v1.CreateComponentGroupResponse\"> & {\n  /**\n   * The created component group.\n   *\n   * @generated from field: openstatus.status_page.v1.PageComponentGroup group = 1;\n   */\n  group?: PageComponentGroup;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.CreateComponentGroupResponse.\n * Use `create(CreateComponentGroupResponseSchema)` to create a new message.\n */\nexport const CreateComponentGroupResponseSchema: GenMessage<CreateComponentGroupResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 19);\n\n/**\n * DeleteComponentGroupRequest is the request to delete a component group.\n *\n * @generated from message openstatus.status_page.v1.DeleteComponentGroupRequest\n */\nexport type DeleteComponentGroupRequest = Message<\"openstatus.status_page.v1.DeleteComponentGroupRequest\"> & {\n  /**\n   * ID of the component group to delete (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.DeleteComponentGroupRequest.\n * Use `create(DeleteComponentGroupRequestSchema)` to create a new message.\n */\nexport const DeleteComponentGroupRequestSchema: GenMessage<DeleteComponentGroupRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 20);\n\n/**\n * DeleteComponentGroupResponse is the response after deleting a component group.\n *\n * @generated from message openstatus.status_page.v1.DeleteComponentGroupResponse\n */\nexport type DeleteComponentGroupResponse = Message<\"openstatus.status_page.v1.DeleteComponentGroupResponse\"> & {\n  /**\n   * Whether the deletion was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.DeleteComponentGroupResponse.\n * Use `create(DeleteComponentGroupResponseSchema)` to create a new message.\n */\nexport const DeleteComponentGroupResponseSchema: GenMessage<DeleteComponentGroupResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 21);\n\n/**\n * UpdateComponentGroupRequest is the request to update a component group.\n *\n * @generated from message openstatus.status_page.v1.UpdateComponentGroupRequest\n */\nexport type UpdateComponentGroupRequest = Message<\"openstatus.status_page.v1.UpdateComponentGroupRequest\"> & {\n  /**\n   * ID of the component group to update (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * New display name for the group (optional).\n   *\n   * @generated from field: optional string name = 2;\n   */\n  name?: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.UpdateComponentGroupRequest.\n * Use `create(UpdateComponentGroupRequestSchema)` to create a new message.\n */\nexport const UpdateComponentGroupRequestSchema: GenMessage<UpdateComponentGroupRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 22);\n\n/**\n * UpdateComponentGroupResponse is the response after updating a component group.\n *\n * @generated from message openstatus.status_page.v1.UpdateComponentGroupResponse\n */\nexport type UpdateComponentGroupResponse = Message<\"openstatus.status_page.v1.UpdateComponentGroupResponse\"> & {\n  /**\n   * The updated component group.\n   *\n   * @generated from field: openstatus.status_page.v1.PageComponentGroup group = 1;\n   */\n  group?: PageComponentGroup;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.UpdateComponentGroupResponse.\n * Use `create(UpdateComponentGroupResponseSchema)` to create a new message.\n */\nexport const UpdateComponentGroupResponseSchema: GenMessage<UpdateComponentGroupResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 23);\n\n/**\n * SubscribeToPageRequest is the request to subscribe an email to a status page.\n *\n * @generated from message openstatus.status_page.v1.SubscribeToPageRequest\n */\nexport type SubscribeToPageRequest = Message<\"openstatus.status_page.v1.SubscribeToPageRequest\"> & {\n  /**\n   * ID of the status page to subscribe to (required).\n   *\n   * @generated from field: string page_id = 1;\n   */\n  pageId: string;\n\n  /**\n   * Email address to subscribe (required).\n   *\n   * @generated from field: string email = 2;\n   */\n  email: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.SubscribeToPageRequest.\n * Use `create(SubscribeToPageRequestSchema)` to create a new message.\n */\nexport const SubscribeToPageRequestSchema: GenMessage<SubscribeToPageRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 24);\n\n/**\n * SubscribeToPageResponse is the response after subscribing to a status page.\n *\n * @generated from message openstatus.status_page.v1.SubscribeToPageResponse\n */\nexport type SubscribeToPageResponse = Message<\"openstatus.status_page.v1.SubscribeToPageResponse\"> & {\n  /**\n   * The created subscriber.\n   *\n   * @generated from field: openstatus.status_page.v1.PageSubscriber subscriber = 1;\n   */\n  subscriber?: PageSubscriber;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.SubscribeToPageResponse.\n * Use `create(SubscribeToPageResponseSchema)` to create a new message.\n */\nexport const SubscribeToPageResponseSchema: GenMessage<SubscribeToPageResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 25);\n\n/**\n * UnsubscribeFromPageRequest is the request to unsubscribe from a status page.\n *\n * @generated from message openstatus.status_page.v1.UnsubscribeFromPageRequest\n */\nexport type UnsubscribeFromPageRequest = Message<\"openstatus.status_page.v1.UnsubscribeFromPageRequest\"> & {\n  /**\n   * ID of the status page to unsubscribe from (required).\n   *\n   * @generated from field: string page_id = 1;\n   */\n  pageId: string;\n\n  /**\n   * Identifier for the subscription (either email or id).\n   *\n   * @generated from oneof openstatus.status_page.v1.UnsubscribeFromPageRequest.identifier\n   */\n  identifier: {\n    /**\n     * Email address to unsubscribe.\n     *\n     * @generated from field: string email = 2;\n     */\n    value: string;\n    case: \"email\";\n  } | {\n    /**\n     * Subscriber ID.\n     *\n     * @generated from field: string id = 3;\n     */\n    value: string;\n    case: \"id\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message openstatus.status_page.v1.UnsubscribeFromPageRequest.\n * Use `create(UnsubscribeFromPageRequestSchema)` to create a new message.\n */\nexport const UnsubscribeFromPageRequestSchema: GenMessage<UnsubscribeFromPageRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 26);\n\n/**\n * UnsubscribeFromPageResponse is the response after unsubscribing from a status page.\n *\n * @generated from message openstatus.status_page.v1.UnsubscribeFromPageResponse\n */\nexport type UnsubscribeFromPageResponse = Message<\"openstatus.status_page.v1.UnsubscribeFromPageResponse\"> & {\n  /**\n   * Whether the unsubscription was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.UnsubscribeFromPageResponse.\n * Use `create(UnsubscribeFromPageResponseSchema)` to create a new message.\n */\nexport const UnsubscribeFromPageResponseSchema: GenMessage<UnsubscribeFromPageResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 27);\n\n/**\n * ListSubscribersRequest is the request to list subscribers of a status page.\n *\n * @generated from message openstatus.status_page.v1.ListSubscribersRequest\n */\nexport type ListSubscribersRequest = Message<\"openstatus.status_page.v1.ListSubscribersRequest\"> & {\n  /**\n   * ID of the status page to list subscribers for (required).\n   *\n   * @generated from field: string page_id = 1;\n   */\n  pageId: string;\n\n  /**\n   * Maximum number of subscribers to return (1-100, defaults to 50).\n   *\n   * @generated from field: optional int32 limit = 2;\n   */\n  limit?: number;\n\n  /**\n   * Number of subscribers to skip for pagination (defaults to 0).\n   *\n   * @generated from field: optional int32 offset = 3;\n   */\n  offset?: number;\n\n  /**\n   * Whether to include unsubscribed users (defaults to false).\n   *\n   * @generated from field: optional bool include_unsubscribed = 4;\n   */\n  includeUnsubscribed?: boolean;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.ListSubscribersRequest.\n * Use `create(ListSubscribersRequestSchema)` to create a new message.\n */\nexport const ListSubscribersRequestSchema: GenMessage<ListSubscribersRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 28);\n\n/**\n * ListSubscribersResponse is the response containing status page subscribers.\n *\n * @generated from message openstatus.status_page.v1.ListSubscribersResponse\n */\nexport type ListSubscribersResponse = Message<\"openstatus.status_page.v1.ListSubscribersResponse\"> & {\n  /**\n   * List of subscribers.\n   *\n   * @generated from field: repeated openstatus.status_page.v1.PageSubscriber subscribers = 1;\n   */\n  subscribers: PageSubscriber[];\n\n  /**\n   * Total number of subscribers matching the filter.\n   *\n   * @generated from field: int32 total_size = 2;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.ListSubscribersResponse.\n * Use `create(ListSubscribersResponseSchema)` to create a new message.\n */\nexport const ListSubscribersResponseSchema: GenMessage<ListSubscribersResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 29);\n\n/**\n * GetStatusPageContentRequest is the request to get the full content of a status page.\n *\n * @generated from message openstatus.status_page.v1.GetStatusPageContentRequest\n */\nexport type GetStatusPageContentRequest = Message<\"openstatus.status_page.v1.GetStatusPageContentRequest\"> & {\n  /**\n   * Identifier for the status page (either id or slug).\n   *\n   * @generated from oneof openstatus.status_page.v1.GetStatusPageContentRequest.identifier\n   */\n  identifier: {\n    /**\n     * ID of the status page.\n     *\n     * @generated from field: string id = 1;\n     */\n    value: string;\n    case: \"id\";\n  } | {\n    /**\n     * Slug of the status page.\n     *\n     * @generated from field: string slug = 2;\n     */\n    value: string;\n    case: \"slug\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message openstatus.status_page.v1.GetStatusPageContentRequest.\n * Use `create(GetStatusPageContentRequestSchema)` to create a new message.\n */\nexport const GetStatusPageContentRequestSchema: GenMessage<GetStatusPageContentRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 30);\n\n/**\n * GetStatusPageContentResponse is the response containing the full status page content.\n *\n * @generated from message openstatus.status_page.v1.GetStatusPageContentResponse\n */\nexport type GetStatusPageContentResponse = Message<\"openstatus.status_page.v1.GetStatusPageContentResponse\"> & {\n  /**\n   * The status page details.\n   *\n   * @generated from field: openstatus.status_page.v1.StatusPage status_page = 1;\n   */\n  statusPage?: StatusPage;\n\n  /**\n   * Components on the status page.\n   *\n   * @generated from field: repeated openstatus.status_page.v1.PageComponent components = 2;\n   */\n  components: PageComponent[];\n\n  /**\n   * Component groups on the status page.\n   *\n   * @generated from field: repeated openstatus.status_page.v1.PageComponentGroup groups = 3;\n   */\n  groups: PageComponentGroup[];\n\n  /**\n   * Active and recent status reports.\n   *\n   * @generated from field: repeated openstatus.status_report.v1.StatusReport status_reports = 4;\n   */\n  statusReports: StatusReport[];\n\n  /**\n   * Scheduled maintenances.\n   *\n   * @generated from field: repeated openstatus.maintenance.v1.MaintenanceSummary maintenances = 5;\n   */\n  maintenances: MaintenanceSummary[];\n};\n\n/**\n * Describes the message openstatus.status_page.v1.GetStatusPageContentResponse.\n * Use `create(GetStatusPageContentResponseSchema)` to create a new message.\n */\nexport const GetStatusPageContentResponseSchema: GenMessage<GetStatusPageContentResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 31);\n\n/**\n * GetOverallStatusRequest is the request to get the overall status of a status page.\n *\n * @generated from message openstatus.status_page.v1.GetOverallStatusRequest\n */\nexport type GetOverallStatusRequest = Message<\"openstatus.status_page.v1.GetOverallStatusRequest\"> & {\n  /**\n   * Identifier for the status page (either id or slug).\n   *\n   * @generated from oneof openstatus.status_page.v1.GetOverallStatusRequest.identifier\n   */\n  identifier: {\n    /**\n     * ID of the status page.\n     *\n     * @generated from field: string id = 1;\n     */\n    value: string;\n    case: \"id\";\n  } | {\n    /**\n     * Slug of the status page.\n     *\n     * @generated from field: string slug = 2;\n     */\n    value: string;\n    case: \"slug\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message openstatus.status_page.v1.GetOverallStatusRequest.\n * Use `create(GetOverallStatusRequestSchema)` to create a new message.\n */\nexport const GetOverallStatusRequestSchema: GenMessage<GetOverallStatusRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 32);\n\n/**\n * ComponentStatus represents the status of a single component.\n *\n * @generated from message openstatus.status_page.v1.ComponentStatus\n */\nexport type ComponentStatus = Message<\"openstatus.status_page.v1.ComponentStatus\"> & {\n  /**\n   * ID of the component.\n   *\n   * @generated from field: string component_id = 1;\n   */\n  componentId: string;\n\n  /**\n   * Current status of the component.\n   *\n   * @generated from field: openstatus.status_page.v1.OverallStatus status = 2;\n   */\n  status: OverallStatus;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.ComponentStatus.\n * Use `create(ComponentStatusSchema)` to create a new message.\n */\nexport const ComponentStatusSchema: GenMessage<ComponentStatus> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 33);\n\n/**\n * GetOverallStatusResponse is the response containing the overall status and individual component statuses.\n *\n * @generated from message openstatus.status_page.v1.GetOverallStatusResponse\n */\nexport type GetOverallStatusResponse = Message<\"openstatus.status_page.v1.GetOverallStatusResponse\"> & {\n  /**\n   * Aggregated status across all components.\n   *\n   * @generated from field: openstatus.status_page.v1.OverallStatus overall_status = 1;\n   */\n  overallStatus: OverallStatus;\n\n  /**\n   * Status of individual components.\n   *\n   * @generated from field: repeated openstatus.status_page.v1.ComponentStatus component_statuses = 2;\n   */\n  componentStatuses: ComponentStatus[];\n};\n\n/**\n * Describes the message openstatus.status_page.v1.GetOverallStatusResponse.\n * Use `create(GetOverallStatusResponseSchema)` to create a new message.\n */\nexport const GetOverallStatusResponseSchema: GenMessage<GetOverallStatusResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_service, 34);\n\n/**\n * StatusPageService provides CRUD and management operations for status pages,\n * including component management, component grouping, subscriber handling, and aggregated status queries.\n *\n * @generated from service openstatus.status_page.v1.StatusPageService\n */\nexport const StatusPageService: GenService<{\n  /**\n   * CreateStatusPage creates a new status page.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.CreateStatusPage\n   */\n  createStatusPage: {\n    methodKind: \"unary\";\n    input: typeof CreateStatusPageRequestSchema;\n    output: typeof CreateStatusPageResponseSchema;\n  },\n  /**\n   * GetStatusPage retrieves a specific status page by ID.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.GetStatusPage\n   */\n  getStatusPage: {\n    methodKind: \"unary\";\n    input: typeof GetStatusPageRequestSchema;\n    output: typeof GetStatusPageResponseSchema;\n  },\n  /**\n   * ListStatusPages returns all status pages for the workspace.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.ListStatusPages\n   */\n  listStatusPages: {\n    methodKind: \"unary\";\n    input: typeof ListStatusPagesRequestSchema;\n    output: typeof ListStatusPagesResponseSchema;\n  },\n  /**\n   * UpdateStatusPage updates an existing status page.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.UpdateStatusPage\n   */\n  updateStatusPage: {\n    methodKind: \"unary\";\n    input: typeof UpdateStatusPageRequestSchema;\n    output: typeof UpdateStatusPageResponseSchema;\n  },\n  /**\n   * DeleteStatusPage removes a status page.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.DeleteStatusPage\n   */\n  deleteStatusPage: {\n    methodKind: \"unary\";\n    input: typeof DeleteStatusPageRequestSchema;\n    output: typeof DeleteStatusPageResponseSchema;\n  },\n  /**\n   * AddMonitorComponent adds a monitor-based component to a status page.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.AddMonitorComponent\n   */\n  addMonitorComponent: {\n    methodKind: \"unary\";\n    input: typeof AddMonitorComponentRequestSchema;\n    output: typeof AddMonitorComponentResponseSchema;\n  },\n  /**\n   * AddStaticComponent adds a static component to a status page.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.AddStaticComponent\n   */\n  addStaticComponent: {\n    methodKind: \"unary\";\n    input: typeof AddStaticComponentRequestSchema;\n    output: typeof AddStaticComponentResponseSchema;\n  },\n  /**\n   * RemoveComponent removes a component from a status page.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.RemoveComponent\n   */\n  removeComponent: {\n    methodKind: \"unary\";\n    input: typeof RemoveComponentRequestSchema;\n    output: typeof RemoveComponentResponseSchema;\n  },\n  /**\n   * UpdateComponent updates an existing component.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.UpdateComponent\n   */\n  updateComponent: {\n    methodKind: \"unary\";\n    input: typeof UpdateComponentRequestSchema;\n    output: typeof UpdateComponentResponseSchema;\n  },\n  /**\n   * CreateComponentGroup creates a new component group.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.CreateComponentGroup\n   */\n  createComponentGroup: {\n    methodKind: \"unary\";\n    input: typeof CreateComponentGroupRequestSchema;\n    output: typeof CreateComponentGroupResponseSchema;\n  },\n  /**\n   * DeleteComponentGroup removes a component group.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.DeleteComponentGroup\n   */\n  deleteComponentGroup: {\n    methodKind: \"unary\";\n    input: typeof DeleteComponentGroupRequestSchema;\n    output: typeof DeleteComponentGroupResponseSchema;\n  },\n  /**\n   * UpdateComponentGroup updates an existing component group.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.UpdateComponentGroup\n   */\n  updateComponentGroup: {\n    methodKind: \"unary\";\n    input: typeof UpdateComponentGroupRequestSchema;\n    output: typeof UpdateComponentGroupResponseSchema;\n  },\n  /**\n   * SubscribeToPage subscribes an email to a status page. If the email was previously unsubscribed, the subscription is reactivated.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.SubscribeToPage\n   */\n  subscribeToPage: {\n    methodKind: \"unary\";\n    input: typeof SubscribeToPageRequestSchema;\n    output: typeof SubscribeToPageResponseSchema;\n  },\n  /**\n   * UnsubscribeFromPage removes a subscription from a status page.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.UnsubscribeFromPage\n   */\n  unsubscribeFromPage: {\n    methodKind: \"unary\";\n    input: typeof UnsubscribeFromPageRequestSchema;\n    output: typeof UnsubscribeFromPageResponseSchema;\n  },\n  /**\n   * ListSubscribers returns all subscribers for a status page.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.ListSubscribers\n   */\n  listSubscribers: {\n    methodKind: \"unary\";\n    input: typeof ListSubscribersRequestSchema;\n    output: typeof ListSubscribersResponseSchema;\n  },\n  /**\n   * GetStatusPageContent retrieves the full content of a status page including components, groups, active reports, and maintenances.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.GetStatusPageContent\n   */\n  getStatusPageContent: {\n    methodKind: \"unary\";\n    input: typeof GetStatusPageContentRequestSchema;\n    output: typeof GetStatusPageContentResponseSchema;\n  },\n  /**\n   * GetOverallStatus returns the aggregated status of a status page and its individual components.\n   *\n   * @generated from rpc openstatus.status_page.v1.StatusPageService.GetOverallStatus\n   */\n  getOverallStatus: {\n    methodKind: \"unary\";\n    input: typeof GetOverallStatusRequestSchema;\n    output: typeof GetOverallStatusResponseSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_openstatus_status_page_v1_service, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/status_page/v1/status_page_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/status_page/v1/status_page.proto (package openstatus.status_page.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/status_page/v1/status_page.proto.\n */\nexport const file_openstatus_status_page_v1_status_page: GenFile = /*@__PURE__*/\n  fileDesc(\"CitvcGVuc3RhdHVzL3N0YXR1c19wYWdlL3YxL3N0YXR1c19wYWdlLnByb3RvEhlvcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxIrEDCgpTdGF0dXNQYWdlEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEh4KBHNsdWcYBCABKAlCELpHDToLEglhY21lLWNvcnASMAoNY3VzdG9tX2RvbWFpbhgFIAEoCUIZukcWOhQSEnN0YXR1cy5leGFtcGxlLmNvbRIRCglwdWJsaXNoZWQYBiABKAgSPgoLYWNjZXNzX3R5cGUYByABKA4yKS5vcGVuc3RhdHVzLnN0YXR1c19wYWdlLnYxLlBhZ2VBY2Nlc3NUeXBlEjMKBXRoZW1lGAggASgOMiQub3BlbnN0YXR1cy5zdGF0dXNfcGFnZS52MS5QYWdlVGhlbWUSFAoMaG9tZXBhZ2VfdXJsGAkgASgJEhMKC2NvbnRhY3RfdXJsGAogASgJEgwKBGljb24YCyABKAkSLwoKY3JlYXRlZF9hdBgMIAEoCUIbukcYOhYSFDIwMjQtMDEtMTVUMDk6MDA6MDBaEi8KCnVwZGF0ZWRfYXQYDSABKAlCG7pHGDoWEhQyMDI0LTA2LTIwVDE0OjMwOjAwWiJ3ChFTdGF0dXNQYWdlU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRIMCgRzbHVnGAMgASgJEhEKCXB1Ymxpc2hlZBgEIAEoCBISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAkqnAEKDlBhZ2VBY2Nlc3NUeXBlEiAKHFBBR0VfQUNDRVNTX1RZUEVfVU5TUEVDSUZJRUQQABIbChdQQUdFX0FDQ0VTU19UWVBFX1BVQkxJQxABEicKI1BBR0VfQUNDRVNTX1RZUEVfUEFTU1dPUkRfUFJPVEVDVEVEEAISIgoeUEFHRV9BQ0NFU1NfVFlQRV9BVVRIRU5USUNBVEVEEAMqaQoJUGFnZVRoZW1lEhoKFlBBR0VfVEhFTUVfVU5TUEVDSUZJRUQQABIVChFQQUdFX1RIRU1FX1NZU1RFTRABEhQKEFBBR0VfVEhFTUVfTElHSFQQAhITCg9QQUdFX1RIRU1FX0RBUksQAyrsAQoNT3ZlcmFsbFN0YXR1cxIeChpPVkVSQUxMX1NUQVRVU19VTlNQRUNJRklFRBAAEh4KGk9WRVJBTExfU1RBVFVTX09QRVJBVElPTkFMEAESGwoXT1ZFUkFMTF9TVEFUVVNfREVHUkFERUQQAhIhCh1PVkVSQUxMX1NUQVRVU19QQVJUSUFMX09VVEFHRRADEh8KG09WRVJBTExfU1RBVFVTX01BSk9SX09VVEFHRRAEEh4KGk9WRVJBTExfU1RBVFVTX01BSU5URU5BTkNFEAUSGgoWT1ZFUkFMTF9TVEFUVVNfVU5LTk9XThAGQlpaWGdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9zdGF0dXNfcGFnZS92MTtzdGF0dXNwYWdldjFiBnByb3RvMw\", [file_gnostic_openapi_v3_annotations]);\n\n/**\n * StatusPage represents a full status page with all details.\n *\n * @generated from message openstatus.status_page.v1.StatusPage\n */\nexport type StatusPage = Message<\"openstatus.status_page.v1.StatusPage\"> & {\n  /**\n   * Unique identifier for the status page.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Title of the status page.\n   *\n   * @generated from field: string title = 2;\n   */\n  title: string;\n\n  /**\n   * Description of the status page.\n   *\n   * @generated from field: string description = 3;\n   */\n  description: string;\n\n  /**\n   * URL-friendly slug for the status page.\n   *\n   * @generated from field: string slug = 4;\n   */\n  slug: string;\n\n  /**\n   * Custom domain for the status page (optional).\n   *\n   * @generated from field: string custom_domain = 5;\n   */\n  customDomain: string;\n\n  /**\n   * Whether the status page is published and visible.\n   *\n   * @generated from field: bool published = 6;\n   */\n  published: boolean;\n\n  /**\n   * Access type for the status page.\n   *\n   * @generated from field: openstatus.status_page.v1.PageAccessType access_type = 7;\n   */\n  accessType: PageAccessType;\n\n  /**\n   * Visual theme for the status page.\n   *\n   * @generated from field: openstatus.status_page.v1.PageTheme theme = 8;\n   */\n  theme: PageTheme;\n\n  /**\n   * URL to the homepage (optional).\n   *\n   * @generated from field: string homepage_url = 9;\n   */\n  homepageUrl: string;\n\n  /**\n   * URL to the contact page (optional).\n   *\n   * @generated from field: string contact_url = 10;\n   */\n  contactUrl: string;\n\n  /**\n   * Icon URL for the status page (optional).\n   *\n   * @generated from field: string icon = 11;\n   */\n  icon: string;\n\n  /**\n   * Timestamp when the page was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 12;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the page was last updated (RFC 3339 format).\n   *\n   * @generated from field: string updated_at = 13;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.StatusPage.\n * Use `create(StatusPageSchema)` to create a new message.\n */\nexport const StatusPageSchema: GenMessage<StatusPage> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_status_page, 0);\n\n/**\n * StatusPageSummary represents metadata for a status page (used in list responses).\n *\n * @generated from message openstatus.status_page.v1.StatusPageSummary\n */\nexport type StatusPageSummary = Message<\"openstatus.status_page.v1.StatusPageSummary\"> & {\n  /**\n   * Unique identifier for the status page.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Title of the status page.\n   *\n   * @generated from field: string title = 2;\n   */\n  title: string;\n\n  /**\n   * URL-friendly slug for the status page.\n   *\n   * @generated from field: string slug = 3;\n   */\n  slug: string;\n\n  /**\n   * Whether the status page is published and visible.\n   *\n   * @generated from field: bool published = 4;\n   */\n  published: boolean;\n\n  /**\n   * Timestamp when the page was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 5;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the page was last updated (RFC 3339 format).\n   *\n   * @generated from field: string updated_at = 6;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.status_page.v1.StatusPageSummary.\n * Use `create(StatusPageSummarySchema)` to create a new message.\n */\nexport const StatusPageSummarySchema: GenMessage<StatusPageSummary> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_page_v1_status_page, 1);\n\n/**\n * PageAccessType defines who can access the status page.\n *\n * @generated from enum openstatus.status_page.v1.PageAccessType\n */\nexport enum PageAccessType {\n  /**\n   * @generated from enum value: PAGE_ACCESS_TYPE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: PAGE_ACCESS_TYPE_PUBLIC = 1;\n   */\n  PUBLIC = 1,\n\n  /**\n   * @generated from enum value: PAGE_ACCESS_TYPE_PASSWORD_PROTECTED = 2;\n   */\n  PASSWORD_PROTECTED = 2,\n\n  /**\n   * @generated from enum value: PAGE_ACCESS_TYPE_AUTHENTICATED = 3;\n   */\n  AUTHENTICATED = 3,\n}\n\n/**\n * Describes the enum openstatus.status_page.v1.PageAccessType.\n */\nexport const PageAccessTypeSchema: GenEnum<PageAccessType> = /*@__PURE__*/\n  enumDesc(file_openstatus_status_page_v1_status_page, 0);\n\n/**\n * PageTheme defines the visual theme of the status page.\n *\n * @generated from enum openstatus.status_page.v1.PageTheme\n */\nexport enum PageTheme {\n  /**\n   * @generated from enum value: PAGE_THEME_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: PAGE_THEME_SYSTEM = 1;\n   */\n  SYSTEM = 1,\n\n  /**\n   * @generated from enum value: PAGE_THEME_LIGHT = 2;\n   */\n  LIGHT = 2,\n\n  /**\n   * @generated from enum value: PAGE_THEME_DARK = 3;\n   */\n  DARK = 3,\n}\n\n/**\n * Describes the enum openstatus.status_page.v1.PageTheme.\n */\nexport const PageThemeSchema: GenEnum<PageTheme> = /*@__PURE__*/\n  enumDesc(file_openstatus_status_page_v1_status_page, 1);\n\n/**\n * OverallStatus represents the aggregated status of all components on a page.\n *\n * @generated from enum openstatus.status_page.v1.OverallStatus\n */\nexport enum OverallStatus {\n  /**\n   * @generated from enum value: OVERALL_STATUS_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: OVERALL_STATUS_OPERATIONAL = 1;\n   */\n  OPERATIONAL = 1,\n\n  /**\n   * @generated from enum value: OVERALL_STATUS_DEGRADED = 2;\n   */\n  DEGRADED = 2,\n\n  /**\n   * @generated from enum value: OVERALL_STATUS_PARTIAL_OUTAGE = 3;\n   */\n  PARTIAL_OUTAGE = 3,\n\n  /**\n   * @generated from enum value: OVERALL_STATUS_MAJOR_OUTAGE = 4;\n   */\n  MAJOR_OUTAGE = 4,\n\n  /**\n   * @generated from enum value: OVERALL_STATUS_MAINTENANCE = 5;\n   */\n  MAINTENANCE = 5,\n\n  /**\n   * @generated from enum value: OVERALL_STATUS_UNKNOWN = 6;\n   */\n  UNKNOWN = 6,\n}\n\n/**\n * Describes the enum openstatus.status_page.v1.OverallStatus.\n */\nexport const OverallStatusSchema: GenEnum<OverallStatus> = /*@__PURE__*/\n  enumDesc(file_openstatus_status_page_v1_status_page, 2);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/status_report/v1/index.ts",
    "content": "// Status report service exports\nexport * from \"./status_report_pb.js\";\nexport * from \"./service_pb.js\";\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/status_report/v1/service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/status_report/v1/service.proto (package openstatus.status_report.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"../../../buf/validate/validate_pb.ts\";\nimport { file_gnostic_openapi_v3_annotations } from \"../../../gnostic/openapi/v3/annotations_pb.ts\";\nimport type { StatusReport, StatusReportStatus, StatusReportSummary } from \"./status_report_pb.ts\";\nimport { file_openstatus_status_report_v1_status_report } from \"./status_report_pb.ts\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/status_report/v1/service.proto.\n */\nexport const file_openstatus_status_report_v1_service: GenFile = /*@__PURE__*/\n  fileDesc(\"CilvcGVuc3RhdHVzL3N0YXR1c19yZXBvcnQvdjEvc2VydmljZS5wcm90bxIbb3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxIsUDChlDcmVhdGVTdGF0dXNSZXBvcnRSZXF1ZXN0EjoKBXRpdGxlGAEgASgJQiu6RyE6HxIdQVBJIERlZ3JhZGF0aW9uIEludmVzdGlnYXRpb266SARyAhABEkkKBnN0YXR1cxgCIAEoDjIvLm9wZW5zdGF0dXMuc3RhdHVzX3JlcG9ydC52MS5TdGF0dXNSZXBvcnRTdGF0dXNCCLpIBYIBAhABElUKB21lc3NhZ2UYAyABKAlCRLpHOjo4EjZXZSBhcmUgaW52ZXN0aWdhdGluZyByZXBvcnRzIG9mIGluY3JlYXNlZCBBUEkgbGF0ZW5jeS66SARyAhABEnQKBGRhdGUYBCABKAlCZrpHGDoWEhQyMDI0LTAzLTE1VDEwOjMwOjAwWrpISHJGMkReXGR7NH0tXGR7Mn0tXGR7Mn1UXGR7Mn06XGR7Mn06XGR7Mn0oXC5cZHsxLDl9KT8oWnxbKy1dXGR7Mn06XGR7Mn0pJBIYCgdwYWdlX2lkGAUgASgJQge6SARyAhABEhoKEnBhZ2VfY29tcG9uZW50X2lkcxgGIAMoCRITCgZub3RpZnkYByABKAhIAIgBAUIJCgdfbm90aWZ5Il4KGkNyZWF0ZVN0YXR1c1JlcG9ydFJlc3BvbnNlEkAKDXN0YXR1c19yZXBvcnQYASABKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuU3RhdHVzUmVwb3J0Ii0KFkdldFN0YXR1c1JlcG9ydFJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAEiWwoXR2V0U3RhdHVzUmVwb3J0UmVzcG9uc2USQAoNc3RhdHVzX3JlcG9ydBgBIAEoCzIpLm9wZW5zdGF0dXMuc3RhdHVzX3JlcG9ydC52MS5TdGF0dXNSZXBvcnQirwEKGExpc3RTdGF0dXNSZXBvcnRzUmVxdWVzdBIdCgVsaW1pdBgBIAEoBUIJukgGGgQYZCgBSACIAQESHAoGb2Zmc2V0GAIgASgFQge6SAQaAigASAGIAQESQQoIc3RhdHVzZXMYAyADKA4yLy5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuU3RhdHVzUmVwb3J0U3RhdHVzQggKBl9saW1pdEIJCgdfb2Zmc2V0InkKGUxpc3RTdGF0dXNSZXBvcnRzUmVzcG9uc2USSAoOc3RhdHVzX3JlcG9ydHMYASADKAsyMC5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuU3RhdHVzUmVwb3J0U3VtbWFyeRISCgp0b3RhbF9zaXplGAIgASgFImoKGVVwZGF0ZVN0YXR1c1JlcG9ydFJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAESEgoFdGl0bGUYAiABKAlIAIgBARIaChJwYWdlX2NvbXBvbmVudF9pZHMYAyADKAlCCAoGX3RpdGxlIl4KGlVwZGF0ZVN0YXR1c1JlcG9ydFJlc3BvbnNlEkAKDXN0YXR1c19yZXBvcnQYASABKAsyKS5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuU3RhdHVzUmVwb3J0IjAKGURlbGV0ZVN0YXR1c1JlcG9ydFJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAEiLQoaRGVsZXRlU3RhdHVzUmVwb3J0UmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCKvAgocQWRkU3RhdHVzUmVwb3J0VXBkYXRlUmVxdWVzdBIhChBzdGF0dXNfcmVwb3J0X2lkGAEgASgJQge6SARyAhABEkkKBnN0YXR1cxgCIAEoDjIvLm9wZW5zdGF0dXMuc3RhdHVzX3JlcG9ydC52MS5TdGF0dXNSZXBvcnRTdGF0dXNCCLpIBYIBAhABEhgKB21lc3NhZ2UYAyABKAlCB7pIBHICEAESXgoEZGF0ZRgEIAEoCUJLukhIckYyRF5cZHs0fS1cZHsyfS1cZHsyfVRcZHsyfTpcZHsyfTpcZHsyfShcLlxkezEsOX0pPyhafFsrLV1cZHsyfTpcZHsyfSkkSACIAQESEwoGbm90aWZ5GAUgASgISAGIAQFCBwoFX2RhdGVCCQoHX25vdGlmeSJhCh1BZGRTdGF0dXNSZXBvcnRVcGRhdGVSZXNwb25zZRJACg1zdGF0dXNfcmVwb3J0GAEgASgLMikub3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxLlN0YXR1c1JlcG9ydDK/CwoTU3RhdHVzUmVwb3J0U2VydmljZRLOAwoSQ3JlYXRlU3RhdHVzUmVwb3J0EjYub3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxLkNyZWF0ZVN0YXR1c1JlcG9ydFJlcXVlc3QaNy5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuQ3JlYXRlU3RhdHVzUmVwb3J0UmVzcG9uc2UixgK6R8ICGr8CQ3JlYXRlcyBhIG5ldyBzdGF0dXMgcmVwb3J0IHdpdGggYW4gaW5pdGlhbCB1cGRhdGUgZW50cnkuIFRoZSByZXBvcnQgaXMgYXNzb2NpYXRlZCB3aXRoIGEgc3RhdHVzIHBhZ2UgYW5kIG9wdGlvbmFsbHkgc3BlY2lmaWMgcGFnZSBjb21wb25lbnRzLiBBbiBpbml0aWFsIFN0YXR1c1JlcG9ydFVwZGF0ZSBpcyBjcmVhdGVkIGF1dG9tYXRpY2FsbHkgd2l0aCB0aGUgcHJvdmlkZWQgc3RhdHVzLCBtZXNzYWdlLCBhbmQgZGF0ZS4gSWYgbm90aWZ5IGlzIHRydWUsIHN1YnNjcmliZXJzIG9mIHRoZSBhc3NvY2lhdGVkIHBhZ2UgYXJlIG5vdGlmaWVkIGJ5IGVtYWlsLhKBAQoPR2V0U3RhdHVzUmVwb3J0EjMub3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxLkdldFN0YXR1c1JlcG9ydFJlcXVlc3QaNC5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuR2V0U3RhdHVzUmVwb3J0UmVzcG9uc2UiA5ACARKHAQoRTGlzdFN0YXR1c1JlcG9ydHMSNS5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuTGlzdFN0YXR1c1JlcG9ydHNSZXF1ZXN0GjYub3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxLkxpc3RTdGF0dXNSZXBvcnRzUmVzcG9uc2UiA5ACARKFAQoSVXBkYXRlU3RhdHVzUmVwb3J0EjYub3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxLlVwZGF0ZVN0YXR1c1JlcG9ydFJlcXVlc3QaNy5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuVXBkYXRlU3RhdHVzUmVwb3J0UmVzcG9uc2UShQEKEkRlbGV0ZVN0YXR1c1JlcG9ydBI2Lm9wZW5zdGF0dXMuc3RhdHVzX3JlcG9ydC52MS5EZWxldGVTdGF0dXNSZXBvcnRSZXF1ZXN0Gjcub3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxLkRlbGV0ZVN0YXR1c1JlcG9ydFJlc3BvbnNlErgDChVBZGRTdGF0dXNSZXBvcnRVcGRhdGUSOS5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuQWRkU3RhdHVzUmVwb3J0VXBkYXRlUmVxdWVzdBo6Lm9wZW5zdGF0dXMuc3RhdHVzX3JlcG9ydC52MS5BZGRTdGF0dXNSZXBvcnRVcGRhdGVSZXNwb25zZSKnArpHowIaoAJBZGRzIGEgbmV3IHVwZGF0ZSBlbnRyeSB0byBhbiBleGlzdGluZyBzdGF0dXMgcmVwb3J0IGFuZCB0cmFuc2l0aW9ucyB0aGUgcmVwb3J0IHRvIHRoZSBzcGVjaWZpZWQgc3RhdHVzLiBTdGF0dXMgcmVwb3J0cyBmb2xsb3cgYSBsaWZlY3ljbGU6IGludmVzdGlnYXRpbmcgLT4gaWRlbnRpZmllZCAtPiBtb25pdG9yaW5nIC0+IHJlc29sdmVkLiBJZiBub3RpZnkgaXMgdHJ1ZSwgc3Vic2NyaWJlcnMgb2YgdGhlIGFzc29jaWF0ZWQgcGFnZSBhcmUgbm90aWZpZWQgYnkgZW1haWwgYWJvdXQgdGhlIHVwZGF0ZS5CXlpcZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL3N0YXR1c19yZXBvcnQvdjE7c3RhdHVzcmVwb3J0djFiBnByb3RvMw\", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_status_report_v1_status_report]);\n\n/**\n * CreateStatusReportRequest is the request to create a new status report.\n *\n * @generated from message openstatus.status_report.v1.CreateStatusReportRequest\n */\nexport type CreateStatusReportRequest = Message<\"openstatus.status_report.v1.CreateStatusReportRequest\"> & {\n  /**\n   * Title of the status report (required).\n   *\n   * @generated from field: string title = 1;\n   */\n  title: string;\n\n  /**\n   * Initial status (required).\n   *\n   * @generated from field: openstatus.status_report.v1.StatusReportStatus status = 2;\n   */\n  status: StatusReportStatus;\n\n  /**\n   * Initial message describing the incident (required).\n   *\n   * @generated from field: string message = 3;\n   */\n  message: string;\n\n  /**\n   * Date when the event occurred (RFC 3339 format, required).\n   *\n   * @generated from field: string date = 4;\n   */\n  date: string;\n\n  /**\n   * Page ID to associate with this report (required).\n   *\n   * @generated from field: string page_id = 5;\n   */\n  pageId: string;\n\n  /**\n   * Page component IDs to associate with this report (optional).\n   *\n   * @generated from field: repeated string page_component_ids = 6;\n   */\n  pageComponentIds: string[];\n\n  /**\n   * Whether to notify subscribers about this status report (optional, defaults to false).\n   *\n   * @generated from field: optional bool notify = 7;\n   */\n  notify?: boolean;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.CreateStatusReportRequest.\n * Use `create(CreateStatusReportRequestSchema)` to create a new message.\n */\nexport const CreateStatusReportRequestSchema: GenMessage<CreateStatusReportRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 0);\n\n/**\n * CreateStatusReportResponse is the response after creating a status report.\n *\n * @generated from message openstatus.status_report.v1.CreateStatusReportResponse\n */\nexport type CreateStatusReportResponse = Message<\"openstatus.status_report.v1.CreateStatusReportResponse\"> & {\n  /**\n   * The created status report.\n   *\n   * @generated from field: openstatus.status_report.v1.StatusReport status_report = 1;\n   */\n  statusReport?: StatusReport;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.CreateStatusReportResponse.\n * Use `create(CreateStatusReportResponseSchema)` to create a new message.\n */\nexport const CreateStatusReportResponseSchema: GenMessage<CreateStatusReportResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 1);\n\n/**\n * GetStatusReportRequest is the request to get a status report by ID.\n *\n * @generated from message openstatus.status_report.v1.GetStatusReportRequest\n */\nexport type GetStatusReportRequest = Message<\"openstatus.status_report.v1.GetStatusReportRequest\"> & {\n  /**\n   * ID of the status report to retrieve (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.GetStatusReportRequest.\n * Use `create(GetStatusReportRequestSchema)` to create a new message.\n */\nexport const GetStatusReportRequestSchema: GenMessage<GetStatusReportRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 2);\n\n/**\n * GetStatusReportResponse is the response containing the status report with its full update timeline.\n *\n * @generated from message openstatus.status_report.v1.GetStatusReportResponse\n */\nexport type GetStatusReportResponse = Message<\"openstatus.status_report.v1.GetStatusReportResponse\"> & {\n  /**\n   * The requested status report.\n   *\n   * @generated from field: openstatus.status_report.v1.StatusReport status_report = 1;\n   */\n  statusReport?: StatusReport;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.GetStatusReportResponse.\n * Use `create(GetStatusReportResponseSchema)` to create a new message.\n */\nexport const GetStatusReportResponseSchema: GenMessage<GetStatusReportResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 3);\n\n/**\n * ListStatusReportsRequest is the request to list status reports.\n *\n * @generated from message openstatus.status_report.v1.ListStatusReportsRequest\n */\nexport type ListStatusReportsRequest = Message<\"openstatus.status_report.v1.ListStatusReportsRequest\"> & {\n  /**\n   * Maximum number of reports to return (1-100, defaults to 50).\n   *\n   * @generated from field: optional int32 limit = 1;\n   */\n  limit?: number;\n\n  /**\n   * Number of reports to skip for pagination (defaults to 0).\n   *\n   * @generated from field: optional int32 offset = 2;\n   */\n  offset?: number;\n\n  /**\n   * Filter by status (optional). If empty, returns all statuses.\n   *\n   * @generated from field: repeated openstatus.status_report.v1.StatusReportStatus statuses = 3;\n   */\n  statuses: StatusReportStatus[];\n};\n\n/**\n * Describes the message openstatus.status_report.v1.ListStatusReportsRequest.\n * Use `create(ListStatusReportsRequestSchema)` to create a new message.\n */\nexport const ListStatusReportsRequestSchema: GenMessage<ListStatusReportsRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 4);\n\n/**\n * ListStatusReportsResponse is the response containing status report summaries.\n *\n * @generated from message openstatus.status_report.v1.ListStatusReportsResponse\n */\nexport type ListStatusReportsResponse = Message<\"openstatus.status_report.v1.ListStatusReportsResponse\"> & {\n  /**\n   * List of status reports (metadata only, use GetStatusReport for full details).\n   *\n   * @generated from field: repeated openstatus.status_report.v1.StatusReportSummary status_reports = 1;\n   */\n  statusReports: StatusReportSummary[];\n\n  /**\n   * Total number of reports matching the filter.\n   *\n   * @generated from field: int32 total_size = 2;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.ListStatusReportsResponse.\n * Use `create(ListStatusReportsResponseSchema)` to create a new message.\n */\nexport const ListStatusReportsResponseSchema: GenMessage<ListStatusReportsResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 5);\n\n/**\n * UpdateStatusReportRequest is the request to update a status report's metadata.\n *\n * @generated from message openstatus.status_report.v1.UpdateStatusReportRequest\n */\nexport type UpdateStatusReportRequest = Message<\"openstatus.status_report.v1.UpdateStatusReportRequest\"> & {\n  /**\n   * ID of the status report to update (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * New title for the report (optional).\n   *\n   * @generated from field: optional string title = 2;\n   */\n  title?: string;\n\n  /**\n   * New list of page component IDs (optional, replaces existing list).\n   *\n   * @generated from field: repeated string page_component_ids = 3;\n   */\n  pageComponentIds: string[];\n};\n\n/**\n * Describes the message openstatus.status_report.v1.UpdateStatusReportRequest.\n * Use `create(UpdateStatusReportRequestSchema)` to create a new message.\n */\nexport const UpdateStatusReportRequestSchema: GenMessage<UpdateStatusReportRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 6);\n\n/**\n * UpdateStatusReportResponse is the response after updating a status report.\n *\n * @generated from message openstatus.status_report.v1.UpdateStatusReportResponse\n */\nexport type UpdateStatusReportResponse = Message<\"openstatus.status_report.v1.UpdateStatusReportResponse\"> & {\n  /**\n   * The updated status report.\n   *\n   * @generated from field: openstatus.status_report.v1.StatusReport status_report = 1;\n   */\n  statusReport?: StatusReport;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.UpdateStatusReportResponse.\n * Use `create(UpdateStatusReportResponseSchema)` to create a new message.\n */\nexport const UpdateStatusReportResponseSchema: GenMessage<UpdateStatusReportResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 7);\n\n/**\n * DeleteStatusReportRequest is the request to delete a status report.\n *\n * @generated from message openstatus.status_report.v1.DeleteStatusReportRequest\n */\nexport type DeleteStatusReportRequest = Message<\"openstatus.status_report.v1.DeleteStatusReportRequest\"> & {\n  /**\n   * ID of the status report to delete (required).\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.DeleteStatusReportRequest.\n * Use `create(DeleteStatusReportRequestSchema)` to create a new message.\n */\nexport const DeleteStatusReportRequestSchema: GenMessage<DeleteStatusReportRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 8);\n\n/**\n * DeleteStatusReportResponse is the response after deleting a status report.\n *\n * @generated from message openstatus.status_report.v1.DeleteStatusReportResponse\n */\nexport type DeleteStatusReportResponse = Message<\"openstatus.status_report.v1.DeleteStatusReportResponse\"> & {\n  /**\n   * Whether the deletion was successful.\n   *\n   * @generated from field: bool success = 1;\n   */\n  success: boolean;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.DeleteStatusReportResponse.\n * Use `create(DeleteStatusReportResponseSchema)` to create a new message.\n */\nexport const DeleteStatusReportResponseSchema: GenMessage<DeleteStatusReportResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 9);\n\n/**\n * AddStatusReportUpdateRequest is the request to add a new update to a status report.\n *\n * @generated from message openstatus.status_report.v1.AddStatusReportUpdateRequest\n */\nexport type AddStatusReportUpdateRequest = Message<\"openstatus.status_report.v1.AddStatusReportUpdateRequest\"> & {\n  /**\n   * ID of the status report to update (required).\n   *\n   * @generated from field: string status_report_id = 1;\n   */\n  statusReportId: string;\n\n  /**\n   * New status for the report (required).\n   *\n   * @generated from field: openstatus.status_report.v1.StatusReportStatus status = 2;\n   */\n  status: StatusReportStatus;\n\n  /**\n   * Message describing what changed (required).\n   *\n   * @generated from field: string message = 3;\n   */\n  message: string;\n\n  /**\n   * Optional date for the update (RFC 3339 format). Defaults to current time if not provided.\n   *\n   * @generated from field: optional string date = 4;\n   */\n  date?: string;\n\n  /**\n   * Whether to notify subscribers about this update (optional, defaults to false).\n   *\n   * @generated from field: optional bool notify = 5;\n   */\n  notify?: boolean;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.AddStatusReportUpdateRequest.\n * Use `create(AddStatusReportUpdateRequestSchema)` to create a new message.\n */\nexport const AddStatusReportUpdateRequestSchema: GenMessage<AddStatusReportUpdateRequest> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 10);\n\n/**\n * AddStatusReportUpdateResponse is the response after adding an update to a status report.\n *\n * @generated from message openstatus.status_report.v1.AddStatusReportUpdateResponse\n */\nexport type AddStatusReportUpdateResponse = Message<\"openstatus.status_report.v1.AddStatusReportUpdateResponse\"> & {\n  /**\n   * The updated status report with the new update included.\n   *\n   * @generated from field: openstatus.status_report.v1.StatusReport status_report = 1;\n   */\n  statusReport?: StatusReport;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.AddStatusReportUpdateResponse.\n * Use `create(AddStatusReportUpdateResponseSchema)` to create a new message.\n */\nexport const AddStatusReportUpdateResponseSchema: GenMessage<AddStatusReportUpdateResponse> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_service, 11);\n\n/**\n * StatusReportService provides CRUD operations for status reports.\n *\n * @generated from service openstatus.status_report.v1.StatusReportService\n */\nexport const StatusReportService: GenService<{\n  /**\n   * CreateStatusReport creates a new status report with an initial update entry and optionally notifies subscribers.\n   *\n   * @generated from rpc openstatus.status_report.v1.StatusReportService.CreateStatusReport\n   */\n  createStatusReport: {\n    methodKind: \"unary\";\n    input: typeof CreateStatusReportRequestSchema;\n    output: typeof CreateStatusReportResponseSchema;\n  },\n  /**\n   * GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n   *\n   * @generated from rpc openstatus.status_report.v1.StatusReportService.GetStatusReport\n   */\n  getStatusReport: {\n    methodKind: \"unary\";\n    input: typeof GetStatusReportRequestSchema;\n    output: typeof GetStatusReportResponseSchema;\n  },\n  /**\n   * ListStatusReports returns all status reports for the workspace (metadata only).\n   *\n   * @generated from rpc openstatus.status_report.v1.StatusReportService.ListStatusReports\n   */\n  listStatusReports: {\n    methodKind: \"unary\";\n    input: typeof ListStatusReportsRequestSchema;\n    output: typeof ListStatusReportsResponseSchema;\n  },\n  /**\n   * UpdateStatusReport updates the metadata of a status report (title, page components).\n   *\n   * @generated from rpc openstatus.status_report.v1.StatusReportService.UpdateStatusReport\n   */\n  updateStatusReport: {\n    methodKind: \"unary\";\n    input: typeof UpdateStatusReportRequestSchema;\n    output: typeof UpdateStatusReportResponseSchema;\n  },\n  /**\n   * DeleteStatusReport removes a status report and all its updates.\n   *\n   * @generated from rpc openstatus.status_report.v1.StatusReportService.DeleteStatusReport\n   */\n  deleteStatusReport: {\n    methodKind: \"unary\";\n    input: typeof DeleteStatusReportRequestSchema;\n    output: typeof DeleteStatusReportResponseSchema;\n  },\n  /**\n   * AddStatusReportUpdate adds a new update to an existing status report timeline and transitions the report status.\n   *\n   * @generated from rpc openstatus.status_report.v1.StatusReportService.AddStatusReportUpdate\n   */\n  addStatusReportUpdate: {\n    methodKind: \"unary\";\n    input: typeof AddStatusReportUpdateRequestSchema;\n    output: typeof AddStatusReportUpdateResponseSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_openstatus_status_report_v1_service, 0);\n\n"
  },
  {
    "path": "packages/proto/gen/ts/openstatus/status_report/v1/status_report_pb.ts",
    "content": "// @generated by protoc-gen-es v2.10.2 with parameter \"target=ts,import_extension=.ts\"\n// @generated from file openstatus/status_report/v1/status_report.proto (package openstatus.status_report.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file openstatus/status_report/v1/status_report.proto.\n */\nexport const file_openstatus_status_report_v1_status_report: GenFile = /*@__PURE__*/\n  fileDesc(\"Ci9vcGVuc3RhdHVzL3N0YXR1c19yZXBvcnQvdjEvc3RhdHVzX3JlcG9ydC5wcm90bxIbb3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxIpQBChJTdGF0dXNSZXBvcnRVcGRhdGUSCgoCaWQYASABKAkSPwoGc3RhdHVzGAIgASgOMi8ub3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxLlN0YXR1c1JlcG9ydFN0YXR1cxIMCgRkYXRlGAMgASgJEg8KB21lc3NhZ2UYBCABKAkSEgoKY3JlYXRlZF9hdBgFIAEoCSK1AQoTU3RhdHVzUmVwb3J0U3VtbWFyeRIKCgJpZBgBIAEoCRI/CgZzdGF0dXMYAiABKA4yLy5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuU3RhdHVzUmVwb3J0U3RhdHVzEg0KBXRpdGxlGAMgASgJEhoKEnBhZ2VfY29tcG9uZW50X2lkcxgEIAMoCRISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAki8AEKDFN0YXR1c1JlcG9ydBIKCgJpZBgBIAEoCRI/CgZzdGF0dXMYAiABKA4yLy5vcGVuc3RhdHVzLnN0YXR1c19yZXBvcnQudjEuU3RhdHVzUmVwb3J0U3RhdHVzEg0KBXRpdGxlGAMgASgJEhoKEnBhZ2VfY29tcG9uZW50X2lkcxgEIAMoCRJACgd1cGRhdGVzGAUgAygLMi8ub3BlbnN0YXR1cy5zdGF0dXNfcmVwb3J0LnYxLlN0YXR1c1JlcG9ydFVwZGF0ZRISCgpjcmVhdGVkX2F0GAYgASgJEhIKCnVwZGF0ZWRfYXQYByABKAkqzwEKElN0YXR1c1JlcG9ydFN0YXR1cxIkCiBTVEFUVVNfUkVQT1JUX1NUQVRVU19VTlNQRUNJRklFRBAAEiYKIlNUQVRVU19SRVBPUlRfU1RBVFVTX0lOVkVTVElHQVRJTkcQARIjCh9TVEFUVVNfUkVQT1JUX1NUQVRVU19JREVOVElGSUVEEAISIwofU1RBVFVTX1JFUE9SVF9TVEFUVVNfTU9OSVRPUklORxADEiEKHVNUQVRVU19SRVBPUlRfU1RBVFVTX1JFU09MVkVEEARCXlpcZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL3N0YXR1c19yZXBvcnQvdjE7c3RhdHVzcmVwb3J0djFiBnByb3RvMw\");\n\n/**\n * StatusReportUpdate represents a single update entry in a status report timeline.\n *\n * @generated from message openstatus.status_report.v1.StatusReportUpdate\n */\nexport type StatusReportUpdate = Message<\"openstatus.status_report.v1.StatusReportUpdate\"> & {\n  /**\n   * Unique identifier for the update.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Status at the time of this update.\n   *\n   * @generated from field: openstatus.status_report.v1.StatusReportStatus status = 2;\n   */\n  status: StatusReportStatus;\n\n  /**\n   * Timestamp when this update occurred (RFC 3339 format).\n   *\n   * @generated from field: string date = 3;\n   */\n  date: string;\n\n  /**\n   * Message describing the update.\n   *\n   * @generated from field: string message = 4;\n   */\n  message: string;\n\n  /**\n   * Timestamp when the update was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 5;\n   */\n  createdAt: string;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.StatusReportUpdate.\n * Use `create(StatusReportUpdateSchema)` to create a new message.\n */\nexport const StatusReportUpdateSchema: GenMessage<StatusReportUpdate> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_status_report, 0);\n\n/**\n * StatusReportSummary represents metadata for a status report (used in list responses).\n *\n * @generated from message openstatus.status_report.v1.StatusReportSummary\n */\nexport type StatusReportSummary = Message<\"openstatus.status_report.v1.StatusReportSummary\"> & {\n  /**\n   * Unique identifier for the status report.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Current status of the report.\n   *\n   * @generated from field: openstatus.status_report.v1.StatusReportStatus status = 2;\n   */\n  status: StatusReportStatus;\n\n  /**\n   * Title of the status report.\n   *\n   * @generated from field: string title = 3;\n   */\n  title: string;\n\n  /**\n   * IDs of affected page components.\n   *\n   * @generated from field: repeated string page_component_ids = 4;\n   */\n  pageComponentIds: string[];\n\n  /**\n   * Timestamp when the report was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 5;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the report was last updated (RFC 3339 format).\n   *\n   * @generated from field: string updated_at = 6;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.StatusReportSummary.\n * Use `create(StatusReportSummarySchema)` to create a new message.\n */\nexport const StatusReportSummarySchema: GenMessage<StatusReportSummary> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_status_report, 1);\n\n/**\n * StatusReport represents an incident or maintenance report with full details.\n *\n * @generated from message openstatus.status_report.v1.StatusReport\n */\nexport type StatusReport = Message<\"openstatus.status_report.v1.StatusReport\"> & {\n  /**\n   * Unique identifier for the status report.\n   *\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * Current status of the report.\n   *\n   * @generated from field: openstatus.status_report.v1.StatusReportStatus status = 2;\n   */\n  status: StatusReportStatus;\n\n  /**\n   * Title of the status report.\n   *\n   * @generated from field: string title = 3;\n   */\n  title: string;\n\n  /**\n   * IDs of affected page components.\n   *\n   * @generated from field: repeated string page_component_ids = 4;\n   */\n  pageComponentIds: string[];\n\n  /**\n   * Timeline of updates for this report (only included in GetStatusReport).\n   *\n   * @generated from field: repeated openstatus.status_report.v1.StatusReportUpdate updates = 5;\n   */\n  updates: StatusReportUpdate[];\n\n  /**\n   * Timestamp when the report was created (RFC 3339 format).\n   *\n   * @generated from field: string created_at = 6;\n   */\n  createdAt: string;\n\n  /**\n   * Timestamp when the report was last updated (RFC 3339 format).\n   *\n   * @generated from field: string updated_at = 7;\n   */\n  updatedAt: string;\n};\n\n/**\n * Describes the message openstatus.status_report.v1.StatusReport.\n * Use `create(StatusReportSchema)` to create a new message.\n */\nexport const StatusReportSchema: GenMessage<StatusReport> = /*@__PURE__*/\n  messageDesc(file_openstatus_status_report_v1_status_report, 2);\n\n/**\n * StatusReportStatus represents the current state of a status report.\n *\n * @generated from enum openstatus.status_report.v1.StatusReportStatus\n */\nexport enum StatusReportStatus {\n  /**\n   * @generated from enum value: STATUS_REPORT_STATUS_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: STATUS_REPORT_STATUS_INVESTIGATING = 1;\n   */\n  INVESTIGATING = 1,\n\n  /**\n   * @generated from enum value: STATUS_REPORT_STATUS_IDENTIFIED = 2;\n   */\n  IDENTIFIED = 2,\n\n  /**\n   * @generated from enum value: STATUS_REPORT_STATUS_MONITORING = 3;\n   */\n  MONITORING = 3,\n\n  /**\n   * @generated from enum value: STATUS_REPORT_STATUS_RESOLVED = 4;\n   */\n  RESOLVED = 4,\n}\n\n/**\n * Describes the enum openstatus.status_report.v1.StatusReportStatus.\n */\nexport const StatusReportStatusSchema: GenEnum<StatusReportStatus> = /*@__PURE__*/\n  enumDesc(file_openstatus_status_report_v1_status_report, 0);\n\n"
  },
  {
    "path": "packages/proto/go.mod",
    "content": "module github.com/openstatushq/openstatus/packages/proto\n\ngo 1.25.2\n\ntool (\n\tconnectrpc.com/connect\n\tgoogle.golang.org/protobuf\n)\n\nrequire (\n\tconnectrpc.com/connect v1.19.1 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n)\n"
  },
  {
    "path": "packages/proto/go.sum",
    "content": "connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=\nconnectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\n"
  },
  {
    "path": "packages/proto/internal/private_location/v1/assertions.proto",
    "content": "syntax = \"proto3\";\n\npackage private_location.v1;\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/private_location/v1;v1\";\n\nenum NumberComparator {\n  NUMBER_COMPARATOR_UNSPECIFIED = 0;\n  NUMBER_COMPARATOR_EQUAL = 1;\n  NUMBER_COMPARATOR_NOT_EQUAL = 2;\n  NUMBER_COMPARATOR_GREATER_THAN = 3;\n  NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL = 4;\n  NUMBER_COMPARATOR_LESS_THAN = 5;\n  NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL = 6;\n}\n\nenum StringComparator {\n  STRING_COMPARATOR_UNSPECIFIED = 0;\n  STRING_COMPARATOR_CONTAINS = 1;\n  STRING_COMPARATOR_NOT_CONTAINS = 2;\n  STRING_COMPARATOR_EQUAL = 3;\n  STRING_COMPARATOR_NOT_EQUAL = 4;\n  STRING_COMPARATOR_EMPTY = 5;\n  STRING_COMPARATOR_NOT_EMPTY = 6;\n  STRING_COMPARATOR_GREATER_THAN = 7;\n  STRING_COMPARATOR_GREATER_THAN_OR_EQUAL = 8;\n  STRING_COMPARATOR_LESS_THAN = 9;\n  STRING_COMPARATOR_LESS_THAN_OR_EQUAL = 10;\n}\n\nenum RecordComparator {\n  RECORD_COMPARATOR_UNSPECIFIED = 0;\n  RECORD_COMPARATOR_EQUAL = 1;\n  RECORD_COMPARATOR_NOT_EQUAL = 2;\n  RECORD_COMPARATOR_CONTAINS = 3;\n  RECORD_COMPARATOR_NOT_CONTAINS = 4;\n}\n\nmessage StatusCodeAssertion {\n  int64 target = 1;\n  NumberComparator comparator = 2;\n}\n\nmessage BodyAssertion {\n  string target = 1;\n  StringComparator comparator = 2;\n}\n\nmessage HeaderAssertion {\n  string target = 1;\n  StringComparator comparator = 2;\n  string key = 3;\n}\n\nmessage RecordAssertion {\n  string record = 1;\n  RecordComparator comparator = 2;\n  string target = 3;\n}\n"
  },
  {
    "path": "packages/proto/internal/private_location/v1/dns_monitor.proto",
    "content": "syntax = \"proto3\";\n\npackage private_location.v1;\n\nimport \"private_location/v1/assertions.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/private_location/v1;v1\";\n\nmessage DNSMonitor {\n  string id = 1;\n  string uri = 2;\n  int64 timeout = 3;\n  optional int64 degraded_at = 4;\n  string periodicity = 5;\n  int64 retry = 6;\n\n  repeated RecordAssertion record_assertions = 13;\n\n}\n"
  },
  {
    "path": "packages/proto/internal/private_location/v1/http_monitor.proto",
    "content": "syntax = \"proto3\";\n\npackage private_location.v1;\n\nimport \"private_location/v1/assertions.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/private_location/v1;v1\";\n\n\nmessage Headers {\n    string key = 1;\n    string value = 2;\n}\n\nmessage HTTPMonitor {\n    string id = 1;\n    string url = 2;\n    string periodicity = 3;\n    string method = 4;\n    string body = 5;\n    int64 timeout = 6;\n    optional int64 degraded_at = 7;\n    int64 retry = 8;\n    bool follow_redirects = 9;\n\n    repeated Headers headers = 10;\n\n    repeated StatusCodeAssertion status_code_assertions = 11;\n    repeated BodyAssertion body_assertions = 12;\n    repeated HeaderAssertion header_assertions = 13;\n\n}\n"
  },
  {
    "path": "packages/proto/internal/private_location/v1/private_location.proto",
    "content": "syntax = \"proto3\";\n\npackage private_location.v1;\n\nimport \"private_location/v1/dns_monitor.proto\";\nimport \"private_location/v1/http_monitor.proto\";\nimport \"private_location/v1/tcp_monitor.proto\";\n\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/private_location/v1;v1\";\n\nservice PrivateLocationService {\n    rpc Monitors(MonitorsRequest) returns (MonitorsResponse) {}\n    rpc IngestTCP(IngestTCPRequest) returns (IngestTCPResponse) {}\n    rpc IngestHTTP(IngestHTTPRequest) returns (IngestHTTPResponse) {}\n    rpc IngestDNS(IngestDNSRequest) returns (IngestDNSResponse) {}\n\n}\n\nmessage MonitorsRequest {}\n\nmessage MonitorsResponse {\n    repeated HTTPMonitor http_monitors = 1;\n    repeated TCPMonitor tcp_monitors = 2;\n    repeated DNSMonitor dns_monitors = 3;\n}\n\n\nmessage IngestTCPRequest {\n    string id = 1;\n    string monitorId = 2;\n    int64 latency = 3;\n    int64 timestamp = 4;\n    int64 cronTimestamp = 5;\n    string uri = 6;\n    string message = 7;\n    string requestStatus = 8;\n    int64 error = 9;\n    string timing = 10;\n\n}\n\nmessage IngestTCPResponse {\n\n}\n\nmessage IngestHTTPRequest {\n    string id = 1;\n    string monitorId = 2;\n    int64 latency = 3;\n    int64 timestamp = 4;\n    int64 cronTimestamp = 5;\n    string url = 6;\n    string requestStatus = 7;\n    string message = 8;\n    string body = 9;\n    string headers = 10;\n    string timing = 11;\n    int64 statusCode = 12;\n    int64 error = 13;\n}\n\nmessage IngestHTTPResponse {\n\n}\n\n\nmessage Records {\n    repeated string record = 1;\n}\nmessage IngestDNSRequest {\n    string id = 1;\n    string monitorId = 2;\n    int64 latency = 3;\n    int64 timestamp = 4;\n    int64 cronTimestamp = 5;\n    string uri = 6;\n    string requestStatus = 7;\n    string message = 8;\n    map<string, Records>  records = 9;\n    string timing = 10;\n    int64 error = 11;\n}\nmessage IngestDNSResponse {\n\n}\n"
  },
  {
    "path": "packages/proto/internal/private_location/v1/tcp_monitor.proto",
    "content": "syntax = \"proto3\";\n\npackage private_location.v1;\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/private_location/v1;v1\";\n\n\n\n\nmessage TCPMonitor {\n    string id = 1;\n    string uri = 2;\n    int64 timeout = 3;\n    optional int64 degraded_at = 4;\n    string periodicity = 5;\n    int64 retry = 6;\n\n}\n"
  },
  {
    "path": "packages/proto/justfile",
    "content": "set fallback := true\n\ninit:\n    go install github.com/bufbuild/buf/cmd/buf@latest\n    go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest\n    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest\n    go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest\n    go install github.com/sudorandom/protoc-gen-connect-openapi@latest\n\nbuf:\n    pnpm buf\n\ngen-private-location:\n    buf generate --path internal/private_location --template buf.gen.go.yaml\n\ngen-api:\n    buf generate --path api/openstatus --template buf.gen.ts.yaml\n\ngen-openapi:\n    buf generate --path api/openstatus --template buf.gen.openapi.yaml && bun scripts/clean-openapi.ts\n"
  },
  {
    "path": "packages/proto/package.json",
    "content": "{\n  \"name\": \"@openstatus/proto\",\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"main\": \"./gen/ts/index.ts\",\n  \"types\": \"./gen/ts/index.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./gen/ts/index.ts\",\n      \"types\": \"./gen/ts/index.ts\"\n    },\n    \"./monitor/v1\": {\n      \"import\": \"./gen/ts/openstatus/monitor/v1/index.ts\",\n      \"types\": \"./gen/ts/openstatus/monitor/v1/index.ts\"\n    },\n    \"./health/v1\": {\n      \"import\": \"./gen/ts/openstatus/health/v1/index.ts\",\n      \"types\": \"./gen/ts/openstatus/health/v1/index.ts\"\n    },\n    \"./status_report/v1\": {\n      \"import\": \"./gen/ts/openstatus/status_report/v1/index.ts\",\n      \"types\": \"./gen/ts/openstatus/status_report/v1/index.ts\"\n    },\n    \"./status_page/v1\": {\n      \"import\": \"./gen/ts/openstatus/status_page/v1/index.ts\",\n      \"types\": \"./gen/ts/openstatus/status_page/v1/index.ts\"\n    },\n    \"./maintenance/v1\": {\n      \"import\": \"./gen/ts/openstatus/maintenance/v1/index.ts\",\n      \"types\": \"./gen/ts/openstatus/maintenance/v1/index.ts\"\n    },\n    \"./notification/v1\": {\n      \"import\": \"./gen/ts/openstatus/notification/v1/index.ts\",\n      \"types\": \"./gen/ts/openstatus/notification/v1/index.ts\"\n    }\n  },\n  \"scripts\": {\n    \"buf:openapi\": \"buf generate --path api/openstatus --template buf.gen.openapi.yaml && bun scripts/clean-openapi.ts\",\n    \"buf:ts\": \"buf generate --path api/openstatus --template buf.gen.ts.yaml\",\n    \"buf:go\": \"buf generate --path internal/private_location --template buf.gen.go.yaml\",\n    \"buf:lint\": \"buf lint\",\n    \"build\": \"buf generate\",\n    \"clean\": \"rm -rf gen/ts\"\n  },\n  \"dependencies\": {\n    \"@bufbuild/protobuf\": \"2.10.2\",\n    \"@bufbuild/protovalidate\": \"^1.1.1\",\n    \"@connectrpc/connect\": \"^2.1.1\"\n  },\n  \"devDependencies\": {\n    \"@bufbuild/buf\": \"1.57.2\",\n    \"@bufbuild/protoc-gen-es\": \"2.10.2\",\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/proto/plan/PLAN.md",
    "content": "# Status Report Proto Implementation Plan\n\n## Overview\n\nCreate proto definitions for Status Report CRUD operations, following the existing patterns in `packages/proto/api/openstatus/monitor/v1/`.\n\n## Directory Structure\n\n```\npackages/proto/api/openstatus/status_report/\n└── v1/\n    ├── status_report.proto    # Message definitions\n    └── service.proto          # Service and RPC definitions\n```\n\n## Database Schema Reference\n\nFrom `packages/db/src/schema/status_reports/status_reports.ts`:\n\n### StatusReport Table\n| Field | Type | Constraints |\n|-------|------|-------------|\n| id | integer | primary key |\n| status | enum | \"investigating\", \"identified\", \"monitoring\", \"resolved\" |\n| title | text(256) | not null |\n| workspaceId | integer | foreign key |\n| pageId | integer | foreign key (cascade delete) |\n| createdAt | timestamp | default now |\n| updatedAt | timestamp | default now |\n\n### StatusReportUpdate Table\n| Field | Type | Constraints |\n|-------|------|-------------|\n| id | integer | primary key |\n| status | enum | same as above |\n| date | timestamp | not null |\n| message | text | not null |\n| statusReportId | integer | foreign key, not null (cascade delete) |\n| createdAt | timestamp | default now |\n| updatedAt | timestamp | default now |\n\n### Relationships\n- StatusReport has many StatusReportUpdates\n- StatusReport belongs to Workspace\n- StatusReport can be linked to multiple PageComponents (via `statusReportsToPageComponents` junction table)\n- Legacy: StatusReport belongs to Page (deprecated, use page components instead)\n- Legacy: StatusReport can be linked to Monitors (via `monitorsToStatusReport`, deprecated)\n\n---\n\n## Proto Definitions\n\n### File 1: `status_report.proto`\n\n```protobuf\nsyntax = \"proto3\";\n\npackage openstatus.status_report.v1;\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/status_report/v1;statusreportv1\";\n\n// StatusReportStatus represents the current state of a status report.\nenum StatusReportStatus {\n  STATUS_REPORT_STATUS_UNSPECIFIED = 0;\n  STATUS_REPORT_STATUS_INVESTIGATING = 1;\n  STATUS_REPORT_STATUS_IDENTIFIED = 2;\n  STATUS_REPORT_STATUS_MONITORING = 3;\n  STATUS_REPORT_STATUS_RESOLVED = 4;\n}\n\n// StatusReportUpdate represents a single update entry in a status report timeline.\nmessage StatusReportUpdate {\n  // Unique identifier for the update.\n  string id = 1;\n\n  // Status at the time of this update.\n  StatusReportStatus status = 2;\n\n  // Timestamp when this update occurred (RFC 3339 format).\n  string date = 3;\n\n  // Message describing the update.\n  string message = 4;\n\n  // Timestamp when the update was created (RFC 3339 format).\n  string created_at = 5;\n}\n\n// StatusReportSummary represents metadata for a status report (used in list responses).\nmessage StatusReportSummary {\n  // Unique identifier for the status report.\n  string id = 1;\n\n  // Current status of the report.\n  StatusReportStatus status = 2;\n\n  // Title of the status report.\n  string title = 3;\n\n  // IDs of affected page components.\n  repeated string page_component_ids = 4;\n\n  // Timestamp when the report was created (RFC 3339 format).\n  string created_at = 5;\n\n  // Timestamp when the report was last updated (RFC 3339 format).\n  string updated_at = 6;\n}\n\n// StatusReport represents an incident or maintenance report with full details.\nmessage StatusReport {\n  // Unique identifier for the status report.\n  string id = 1;\n\n  // Current status of the report.\n  StatusReportStatus status = 2;\n\n  // Title of the status report.\n  string title = 3;\n\n  // IDs of affected page components.\n  repeated string page_component_ids = 4;\n\n  // Timeline of updates for this report (only included in GetStatusReport).\n  repeated StatusReportUpdate updates = 5;\n\n  // Timestamp when the report was created (RFC 3339 format).\n  string created_at = 6;\n\n  // Timestamp when the report was last updated (RFC 3339 format).\n  string updated_at = 7;\n}\n```\n\n### File 2: `service.proto`\n\n```protobuf\nsyntax = \"proto3\";\n\npackage openstatus.status_report.v1;\n\nimport \"buf/validate/validate.proto\";\nimport \"openstatus/status_report/v1/status_report.proto\";\n\noption go_package = \"github.com/openstatushq/openstatus/packages/proto/openstatus/status_report/v1;statusreportv1\";\n\n// StatusReportService provides CRUD operations for status reports.\nservice StatusReportService {\n  // CreateStatusReport creates a new status report.\n  rpc CreateStatusReport(CreateStatusReportRequest) returns (CreateStatusReportResponse);\n\n  // GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n  rpc GetStatusReport(GetStatusReportRequest) returns (GetStatusReportResponse);\n\n  // ListStatusReports returns all status reports for the workspace (metadata only).\n  rpc ListStatusReports(ListStatusReportsRequest) returns (ListStatusReportsResponse);\n\n  // UpdateStatusReport updates the metadata of a status report (title, page, monitors).\n  rpc UpdateStatusReport(UpdateStatusReportRequest) returns (UpdateStatusReportResponse);\n\n  // DeleteStatusReport removes a status report and all its updates.\n  rpc DeleteStatusReport(DeleteStatusReportRequest) returns (DeleteStatusReportResponse);\n\n  // AddStatusReportUpdate adds a new update to an existing status report timeline.\n  rpc AddStatusReportUpdate(AddStatusReportUpdateRequest) returns (AddStatusReportUpdateResponse);\n}\n\n// --- Create Status Report ---\n\nmessage CreateStatusReportRequest {\n  // Title of the status report (required).\n  string title = 1 [(buf.validate.field).string.min_len = 1];\n\n  // Initial status (required).\n  StatusReportStatus status = 2 [(buf.validate.field).enum.defined_only = true];\n\n  // Initial message describing the incident (required).\n  string message = 3 [(buf.validate.field).string.min_len = 1];\n\n  // Date when the event occurred (RFC 3339 format, required).\n  string date = 4 [(buf.validate.field).string.min_len = 1];\n\n  // Page component IDs to associate with this report (required).\n  repeated string page_component_ids = 5 [(buf.validate.field).repeated.min_items = 1];\n}\n\nmessage CreateStatusReportResponse {\n  // The created status report.\n  StatusReport status_report = 1;\n}\n\n// --- Get Status Report ---\n\nmessage GetStatusReportRequest {\n  // ID of the status report to retrieve (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\nmessage GetStatusReportResponse {\n  // The requested status report.\n  StatusReport status_report = 1;\n}\n\n// --- List Status Reports ---\n\nmessage ListStatusReportsRequest {\n  // Maximum number of reports to return (1-100, defaults to 50).\n  optional int32 limit = 1 [(buf.validate.field).int32 = {\n    gte: 1\n    lte: 100\n  }];\n\n  // Number of reports to skip for pagination (defaults to 0).\n  optional int32 offset = 2 [(buf.validate.field).int32.gte = 0];\n\n  // Filter by status (optional). If empty, returns all statuses.\n  repeated StatusReportStatus statuses = 3;\n}\n\nmessage ListStatusReportsResponse {\n  // List of status reports (metadata only, use GetStatusReport for full details).\n  repeated StatusReportSummary status_reports = 1;\n\n  // Total number of reports matching the filter.\n  int32 total_size = 2;\n}\n\n// --- Update Status Report ---\n\nmessage UpdateStatusReportRequest {\n  // ID of the status report to update (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // New title for the report (optional).\n  optional string title = 2;\n\n  // New list of page component IDs (optional, replaces existing list).\n  repeated string page_component_ids = 3;\n}\n\nmessage UpdateStatusReportResponse {\n  // The updated status report.\n  StatusReport status_report = 1;\n}\n\n// --- Delete Status Report ---\n\nmessage DeleteStatusReportRequest {\n  // ID of the status report to delete (required).\n  string id = 1 [(buf.validate.field).string.min_len = 1];\n}\n\nmessage DeleteStatusReportResponse {\n  // Whether the deletion was successful.\n  bool success = 1;\n}\n\n// --- Add Status Report Update ---\n\nmessage AddStatusReportUpdateRequest {\n  // ID of the status report to update (required).\n  string status_report_id = 1 [(buf.validate.field).string.min_len = 1];\n\n  // New status for the report (required).\n  StatusReportStatus status = 2 [(buf.validate.field).enum.defined_only = true];\n\n  // Message describing what changed (required).\n  string message = 3 [(buf.validate.field).string.min_len = 1];\n\n  // Optional date for the update. Defaults to current time if not provided.\n  optional string date = 4;\n}\n\nmessage AddStatusReportUpdateResponse {\n  // The updated status report with the new update included.\n  StatusReport status_report = 1;\n}\n```\n\n---\n\n## Questions Resolved\n\nBased on the database schema:\n\n1. **Status Report Structure**: Uses `id`, `status`, `title`, `workspaceId`, `createdAt`, `updatedAt`\n2. **Updates**: Separate table `status_report_update` with timeline entries (id, status, date, message, statusReportId)\n3. **Status Values**: `investigating`, `identified`, `monitoring`, `resolved`\n4. **Relationships**: Links to PageComponents (many-to-many via `statusReportsToPageComponents` junction table)\n\n---\n\n## Implementation Notes\n\n1. **Workspace ID**: Not included in request/response as it should be derived from the authenticated context (API key belongs to workspace)\n2. **Timestamps**: Use RFC 3339 format strings for interoperability\n3. **Pagination**: Uses offset-based pagination with `limit` and `offset` parameters\n4. **Validation**: Use `buf.validate` for field validation\n\n---\n\n## Decisions Made\n\n1. **Delete Operation**: Yes - `DeleteStatusReport` RPC added (cascade deletes all updates)\n2. **Update Status Report**: Yes - `UpdateStatusReport` RPC added to modify title/page/monitors without creating a timeline entry\n3. **Filtering**: `ListStatusReports` supports filtering by status only (via `statuses` field)\n4. **Include Updates**: `ListStatusReports` returns `StatusReportSummary` (metadata only), `GetStatusReport` returns full `StatusReport` with update timeline\n"
  },
  {
    "path": "packages/proto/plan/api.md",
    "content": "# Status Page Proto Service Implementation Plan\n\n## Overview\n\nCreate a new proto service for Status Pages in `packages/proto/api/openstatus/status_page/v1/` following existing patterns from `monitor` and `status_report` services.\n\n## File Structure\n\n```\npackages/proto/api/openstatus/status_page/v1/\n├── service.proto          # RPC service definitions\n├── status_page.proto      # StatusPage message and enums\n├── page_component.proto   # PageComponent message and enums\n└── page_subscriber.proto  # PageSubscriber message\n```\n\n## Proto Definitions\n\n### 1. `status_page.proto` - Core Types\n\n**Enums:**\n- `PageAccessType`: PUBLIC, PASSWORD_PROTECTED, AUTHENTICATED\n- `PageTheme`: SYSTEM, LIGHT, DARK\n- `OverallStatus`: OPERATIONAL, DEGRADED, PARTIAL_OUTAGE, MAJOR_OUTAGE, MAINTENANCE, UNKNOWN\n\n**Messages:**\n- `StatusPage` - Full page details (id, title, description, slug, custom_domain, published, access_type, theme, homepage_url, contact_url, icon, timestamps)\n- `StatusPageSummary` - List response (id, title, slug, published, timestamps)\n\n### 2. `page_component.proto` - Component Types\n\n**Enums:**\n- `PageComponentType`: MONITOR, STATIC\n\n**Messages:**\n- `PageComponent` - Component details (id, page_id, name, description, type, monitor_id, order, group_id, group_order, timestamps)\n- `PageComponentGroup` - Group details (id, page_id, name, timestamps)\n\n### 3. `page_subscriber.proto` - Subscriber Types\n\n**Messages:**\n- `PageSubscriber` - Subscriber details (id, page_id, email, accepted_at, unsubscribed_at, timestamps)\n\n### 4. `service.proto` - RPC Service\n\n```protobuf\nservice StatusPageService {\n  // Page CRUD\n  rpc CreateStatusPage(CreateStatusPageRequest) returns (CreateStatusPageResponse);\n  rpc GetStatusPage(GetStatusPageRequest) returns (GetStatusPageResponse);\n  rpc ListStatusPages(ListStatusPagesRequest) returns (ListStatusPagesResponse);\n  rpc UpdateStatusPage(UpdateStatusPageRequest) returns (UpdateStatusPageResponse);\n  rpc DeleteStatusPage(DeleteStatusPageRequest) returns (DeleteStatusPageResponse);\n\n  // Components\n  rpc AddMonitorComponent(AddMonitorComponentRequest) returns (AddMonitorComponentResponse);\n  rpc AddExternalComponent(AddExternalComponentRequest) returns (AddExternalComponentResponse);\n  rpc RemoveComponent(RemoveComponentRequest) returns (RemoveComponentResponse);\n  rpc UpdateComponent(UpdateComponentRequest) returns (UpdateComponentResponse);\n\n  // Component Groups\n  rpc CreateComponentGroup(CreateComponentGroupRequest) returns (CreateComponentGroupResponse);\n  rpc DeleteComponentGroup(DeleteComponentGroupRequest) returns (DeleteComponentGroupResponse);\n  rpc UpdateComponentGroup(UpdateComponentGroupRequest) returns (UpdateComponentGroupResponse);\n\n  // Subscribers\n  rpc SubscribeToPage(SubscribeToPageRequest) returns (SubscribeToPageResponse);\n  rpc UnsubscribeFromPage(UnsubscribeFromPageRequest) returns (UnsubscribeFromPageResponse);\n  rpc ListSubscribers(ListSubscribersRequest) returns (ListSubscribersResponse);\n\n  // Full Content & Status\n  rpc GetStatusPageContent(GetStatusPageContentRequest) returns (GetStatusPageContentResponse);\n  rpc GetOverallStatus(GetOverallStatusRequest) returns (GetOverallStatusResponse);\n}\n```\n\n## RPC Methods Breakdown\n\n### Page CRUD (5 methods)\n| Method | Request Fields | Response |\n|--------|---------------|----------|\n| `CreateStatusPage` | title, description, slug,  homepage_url?, contact_url?| StatusPage |\n| `GetStatusPage` | id | StatusPage |\n| `ListStatusPages` | limit?, offset? | StatusPageSummary[], total_size |\n| `UpdateStatusPage` | id, title?, description?, slug?, homepage_url?, contact_url? | StatusPage |\n| `DeleteStatusPage` | id | success |\n\n### Component Management (4 methods)\n| Method | Request Fields | Response |\n|--------|---------------|----------|\n| `AddMonitorComponent` | page_id, monitor_id, name?, description?, order?, group_id? | PageComponent |\n| `AddExternalComponent` | page_id, name, description?, order?, group_id? | PageComponent |\n| `RemoveComponent` | id | success |\n| `UpdateComponent` | id, name?, description?, order?, group_id?, group_order? | PageComponent |\n\n### Component Groups (3 methods)\n| Method | Request Fields | Response |\n|--------|---------------|----------|\n| `CreateComponentGroup` | page_id, name | PageComponentGroup |\n| `DeleteComponentGroup` | id | success |\n| `UpdateComponentGroup` | id, name? | PageComponentGroup |\n\n### Subscriber Management (3 methods)\n| Method | Request Fields | Response |\n|--------|---------------|----------|\n| `SubscribeToPage` | page_id, email | PageSubscriber |\n| `UnsubscribeFromPage` | page_id, email or token | success |\n| `ListSubscribers` | page_id, limit?, offset?, include_unsubscribed? | PageSubscriber[], total_size |\n\n### Full Content & Status (2 methods)\n| Method | Request Fields | Response |\n|--------|---------------|----------|\n| `GetStatusPageContent` | id or slug | StatusPage, components[], groups[], status_reports[], maintenances[] |\n| `GetOverallStatus` | id or slug | overall_status, component_statuses[] |\n\n## Validation Rules (using buf.validate)\n\n- `title`: min_len=1, max_len=256\n- `description`: max_len=1024\n- `slug`: min_len=1, max_len=256, pattern for valid slug\n- `email`: email format validation\n- `limit`: gte=1, lte=100\n- `offset`: gte=0\n- Enums: defined_only=true, not_in=[0] where appropriate\n\n## Implementation Order\n\n1. Create `status_page.proto` with enums and base messages\n2. Create `page_component.proto` with component types\n3. Create `page_subscriber.proto` with subscriber message\n4. Create `service.proto` with all RPC definitions\n5. Update buf.yaml if needed to include new package\n\n## Files to Create\n\n| File | Description |\n|------|-------------|\n| `packages/proto/api/openstatus/status_page/v1/status_page.proto` | Core enums and StatusPage messages |\n| `packages/proto/api/openstatus/status_page/v1/page_component.proto` | PageComponent and PageComponentGroup messages |\n| `packages/proto/api/openstatus/status_page/v1/page_subscriber.proto` | PageSubscriber message |\n| `packages/proto/api/openstatus/status_page/v1/service.proto` | StatusPageService RPC definitions |\n\n## Verification\n\n1. Run `pnpm buf lint` to verify proto files are valid\n2. Run `pnpm buf generate` to generate Go/TS code\n3. Check generated code compiles without errors\n"
  },
  {
    "path": "packages/proto/scripts/clean-openapi.ts",
    "content": "import { readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n// biome-ignore lint: only  a script\nconst OPENAPI_PATH = join(dirname(import.meta.dirname!), \"gen\", \"openapi.yaml\");\n\nconst CONNECT_PARAMS = new Set([\n  \"Connect-Protocol-Version\",\n  \"Connect-Timeout-Ms\",\n  \"encoding\",\n  \"base64\",\n  \"compression\",\n  \"connect\",\n]);\n\nconst CONNECT_SCHEMAS = new Set([\n  \"connect-protocol-version\",\n  \"connect-timeout-header\",\n  \"encoding\",\n  \"base64\",\n  \"compression\",\n  \"connect\",\n]);\n\nconst lines = readFileSync(OPENAPI_PATH, \"utf-8\").split(\"\\n\");\nconst out: string[] = [];\n\nlet i = 0;\nwhile (i < lines.length) {\n  const line = lines[i];\n\n  // Remove Connect-specific parameter blocks\n  if (/^\\s+- name:\\s/.test(line)) {\n    const nameMatch = line.match(/- name:\\s+(.+)/);\n    if (nameMatch && CONNECT_PARAMS.has(nameMatch[1].trim())) {\n      const baseIndent = line.search(/\\S/);\n      i++;\n      while (i < lines.length) {\n        const nextLine = lines[i];\n        if (nextLine.trim() === \"\") {\n          i++;\n          continue;\n        }\n        const nextIndent = nextLine.search(/\\S/);\n        if (nextIndent > baseIndent) {\n          i++;\n          continue;\n        }\n        break;\n      }\n      continue;\n    }\n  }\n\n  // Remove Connect-specific schema definitions from components.schemas\n  if (/^ {4}\\S/.test(line) && line.trim().endsWith(\":\")) {\n    const schemaName = line.trim().replace(/:$/, \"\");\n    if (CONNECT_SCHEMAS.has(schemaName)) {\n      const baseIndent = line.search(/\\S/);\n      i++;\n      while (i < lines.length) {\n        const nextLine = lines[i];\n        if (nextLine.trim() === \"\") {\n          i++;\n          continue;\n        }\n        const nextIndent = nextLine.search(/\\S/);\n        if (nextIndent > baseIndent) {\n          i++;\n          continue;\n        }\n        break;\n      }\n      continue;\n    }\n  }\n\n  out.push(line);\n  i++;\n}\n\n// Second pass: remove empty \"parameters:\" keys left after stripping all params\nconst final: string[] = [];\nfor (let j = 0; j < out.length; j++) {\n  if (/^\\s+parameters:\\s*$/.test(out[j])) {\n    let k = j + 1;\n    while (k < out.length && out[k].trim() === \"\") k++;\n    if (k < out.length && /^\\s+- name:/.test(out[k])) {\n      final.push(out[j]);\n    }\n    continue;\n  }\n  final.push(out[j]);\n}\n\nwriteFileSync(OPENAPI_PATH, final.join(\"\\n\"));\n"
  },
  {
    "path": "packages/proto/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"gen/ts\",\n    \"rootDir\": \"gen/ts\",\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"emitDeclarationOnly\": true,\n    \"allowImportingTsExtensions\": true\n  },\n  \"include\": [\"gen/ts/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/react/README.md",
    "content": "# Status Widget\n\nCreate an account on [openstatus.dev](https://openstatus.dev), start monitoring\nyour endpoints and include your own StatusWidget into your React Application.\n\n![Image of StatusWidget on openstatus.dev](https://openstatus.dev/assets/changelog/status-widget.png)\n\n## Install\n\n```bash\nnpm install @openstatus/react\npnpm add @openstatus/react\nyarn add @openstatus/react\nbun add @openstatus/react\n```\n\n## How to use the StatusWidget in your Next.js App Router\n\n### Include the styles.css\n\nIf you are using tailwind, extend your config with:\n\n```js\n// tailwind.config.js\nmodule.exports = {\n  content: [\n    \"./app/**/*.{tsx,ts,mdx,md}\",\n    // OpenStatus Widget\n    \"./node_modules/@openstatus/react/**/*.{js,ts,jsx,tsx}\",\n  ],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n};\n```\n\nOtherwise, include the styles in your App:\n\n```tsx\n// app/layout.tsx\nimport \"@openstatus/react/dist/styles.css\";\n```\n\nThe `StatusWidget` is a React Server Component. Include the `slug` to your\nstatus-page.\n\n```tsx\nimport { StatusWidget } from \"@openstatus/react\";\n\nexport function Page() {\n  return <StatusWidget slug=\"status\" />;\n}\n```\n\n## Headless getStatus utility function\n\nIf you would like to style it yourself, you can use the `getStatus` function to\naccess the type response of the api call to:\n\n`https://api.openstatus.dev/public/status/:slug`\n\nLearn more about our supported\n[API endpoints](https://docs.openstatus.dev/api-reference/auth).\n\n```ts\nimport { getStatus } from \"@openstatus/react\";\n\n// React Server Component\nasync function CustomStatusWidget() {\n  const res = await getStatus(\"slug\");\n  // ^StatusResponse = { status: Status }\n\n  const { status } = res;\n  // ^Status = \"unknown\" | \"operational\" | \"degraded_performance\" | \"partial_outage\" | \"major_outage\" | \"under_maintenance\"\n\n  return <div>{/* customize */}</div>;\n}\n```\n\n```ts\nexport type Status =\n  | \"operational\"\n  | \"degraded_performance\"\n  | \"partial_outage\"\n  | \"major_outage\"\n  | \"under_maintenance\"\n  | \"unknown\"\n  | \"incident\";\n```\n\nLearn more in the [docs](https://docs.openstatus.dev/packages/react).\n\n### About OpenStatus\n\nOpenStatus is an open source monitoring services with incident managements.\n\nFollow our journey [@openstatusHQ](https://x.com/openstatusHQ).\n"
  },
  {
    "path": "packages/react/package.json",
    "content": "{\n  \"name\": \"@openstatus/react\",\n  \"version\": \"0.0.3\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/openstatusHQ/openstatus.git\"\n  },\n  \"keywords\": [\"openstatus\", \"react\", \"monitoring\"],\n  \"homepage\": \"https://github.com/openstatusHQ/openstatus#readme\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"module\": \"./dist/index.mjs\",\n  \"files\": [\"./dist/**\"],\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"dev\": \"tsup --watch && pnpm run dev:tailwind\",\n    \"dev:tailwind\": \"npx @tailwindcss/cli -i ./src/styles.css -o ./dist/styles.css --watch\",\n    \"build:tailwind\": \"npx @tailwindcss/cli -i ./src/styles.css -o ./dist/styles.css --minify\",\n    \"build\": \"tsup && pnpm run build:tailwind\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@tailwindcss/cli\": \"4.1.8\",\n    \"@types/node\": \"20.8.0\",\n    \"@types/react\": \"18.3.3\",\n    \"tailwindcss\": \"4.1.8\",\n    \"tsup\": \"7.2.0\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^18.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react/src/index.ts",
    "content": "export * from \"./widget\";\n"
  },
  {
    "path": "packages/react/src/styles.css",
    "content": "@import 'tailwindcss';\n\n/*\n  The default border color has changed to `currentcolor` in Tailwind CSS v4,\n  so we've added these compatibility styles to make sure everything still\n  looks the same as it did with Tailwind CSS v3.\n\n  If we ever want to remove these styles, we need to add an explicit border\n  color utility to any element that depends on these defaults.\n*/\n@layer base {\n  *,\n  ::after,\n  ::before,\n  ::backdrop,\n  ::file-selector-button {\n    border-color: var(--color-gray-200, currentcolor);\n  }\n}\n"
  },
  {
    "path": "packages/react/src/utils.ts",
    "content": "import type { Status } from \"./widget\";\n\nexport const statusDictionary: Record<\n  Status,\n  { label: string; color: string }\n> = {\n  operational: {\n    label: \"Operational\",\n    color: \"bg-green-500\",\n  },\n  degraded_performance: {\n    label: \"Degraded Performance\",\n    color: \"bg-yellow-500\",\n  },\n  partial_outage: {\n    label: \"Partial Outage\",\n    color: \"bg-yellow-500\",\n  },\n  major_outage: {\n    label: \"Major Outage\",\n    color: \"bg-red-500\",\n  },\n  unknown: {\n    label: \"Unknown\",\n    color: \"bg-gray-500\",\n  },\n  incident: {\n    label: \"Incident\",\n    color: \"bg-yellow-500\",\n  },\n  under_maintenance: {\n    label: \"Under Maintenance\",\n    color: \"bg-blue-500\",\n  },\n} as const;\n"
  },
  {
    "path": "packages/react/src/widget.tsx",
    "content": "import { statusDictionary } from \"./utils\";\n\nexport type Status =\n  | \"operational\"\n  | \"degraded_performance\"\n  | \"partial_outage\"\n  | \"major_outage\"\n  | \"under_maintenance\"\n  | \"unknown\"\n  | \"incident\";\n\nexport type StatusResponse = { status: Status };\n\nexport async function getStatus(slug: string): Promise<StatusResponse> {\n  const res = await fetch(`https://api.openstatus.dev/public/status/${slug}`, {\n    cache: \"no-cache\",\n  });\n\n  if (res.ok) {\n    const data = (await res.json()) as StatusResponse;\n    return data;\n  }\n\n  return { status: \"unknown\" };\n}\n\nexport type StatusWidgetProps = {\n  slug: string;\n  href?: string;\n};\n\nexport async function StatusWidget({ slug, href }: StatusWidgetProps) {\n  const { status } = await getStatus(slug);\n\n  const { label, color } = statusDictionary[status];\n\n  return (\n    <a\n      className=\"inline-flex max-w-fit items-center gap-2 rounded-md border border-gray-200 px-3 py-1 text-gray-700 text-sm hover:bg-gray-100 hover:text-black dark:border-gray-800 dark:text-gray-300 dark:hover:bg-gray-900 dark:hover:text-white\"\n      href={href || `https://${slug}.openstatus.dev`}\n      target=\"_blank\"\n      rel=\"noreferrer\"\n    >\n      {label}\n      <span className=\"relative flex h-2 w-2\">\n        {status === \"operational\" ? (\n          <span\n            className={`absolute inline-flex h-full w-full animate-ping rounded-full ${color} opacity-75 duration-1000`}\n          />\n        ) : null}\n        <span\n          className={`relative inline-flex h-2 w-2 rounded-full ${color}`}\n        />\n      </span>\n    </a>\n  );\n}\n"
  },
  {
    "path": "packages/react/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/react-library.json\",\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"moduleResolution\": \"node\",\n    \"preserveWatchOutput\": true,\n    \"skipLibCheck\": true,\n    \"noEmit\": true,\n    \"strict\": true\n  }\n}\n"
  },
  {
    "path": "packages/react/tsup.config.js",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\"],\n  format: [\"cjs\", \"esm\"],\n  splitting: false,\n  sourcemap: true,\n  clean: true,\n  bundle: true,\n  dts: true,\n});\n"
  },
  {
    "path": "packages/regions/index.ts",
    "content": "export const FLY_REGIONS = [\n  \"ams\",\n  \"arn\",\n  \"atl\",\n  \"bog\",\n  \"bom\",\n  \"bos\",\n  \"cdg\",\n  \"den\",\n  \"dfw\",\n  \"ewr\",\n  \"eze\",\n  \"fra\",\n  \"gdl\",\n  \"gig\",\n  \"gru\",\n  \"hkg\",\n  \"iad\",\n  \"jnb\",\n  \"lax\",\n  \"lhr\",\n  \"mad\",\n  \"mia\",\n  \"nrt\",\n  \"ord\",\n  \"otp\",\n  \"phx\",\n  \"qro\",\n  \"scl\",\n  \"sjc\",\n  \"sea\",\n  \"sin\",\n  \"syd\",\n  \"waw\",\n  \"yul\",\n  \"yyz\",\n] as const;\n\nexport const KOYEB_REGIONS = [\n  \"koyeb_fra\",\n  \"koyeb_was\",\n  \"koyeb_sin\",\n  \"koyeb_tyo\",\n  \"koyeb_par\",\n  \"koyeb_sfo\",\n] as const;\n\nexport const RAILWAY_REGIONS = [\n  \"railway_europe-west4-drams3a\",\n  \"railway_us-east4-eqdc4a\",\n  \"railway_asia-southeast1-eqsg3a\",\n  \"railway_us-west2\",\n] as const;\n\nexport const FREE_FLY_REGIONS = [\n  \"iad\",\n  \"ams\",\n  \"gru\",\n  \"syd\",\n  \"sin\",\n  \"jnb\",\n] as const satisfies (typeof FLY_REGIONS)[number][];\n\nexport const ALL_REGIONS = [\n  ...FLY_REGIONS,\n  ...KOYEB_REGIONS,\n  ...RAILWAY_REGIONS,\n] as const;\n\nexport type Region = (typeof ALL_REGIONS)[number];\n\nexport type Continent =\n  | \"Europe\"\n  | \"North America\"\n  | \"South America\"\n  | \"Asia\"\n  | \"Africa\"\n  | \"Oceania\";\n\nexport type RegionInfo = {\n  code: Region;\n  location: string;\n  flag: string;\n  continent: Continent;\n  deprecated: boolean;\n  provider: \"fly\" | \"koyeb\" | \"railway\" | \"private\";\n};\n\n// TODO: we could think of doing the inverse and use \"EU\" as key\nexport const continentDict: Record<Continent, { code: string }> = {\n  Europe: { code: \"EU\" },\n  \"North America\": { code: \"NA\" },\n  \"South America\": { code: \"SA\" },\n  Asia: { code: \"AS\" },\n  Africa: { code: \"AF\" },\n  Oceania: { code: \"OC\" },\n};\n\nexport const regionDict: Record<Region, RegionInfo> = {\n  ams: {\n    code: \"ams\",\n    location: \"Amsterdam, Netherlands\",\n    flag: \"🇳🇱\",\n    continent: \"Europe\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  arn: {\n    code: \"arn\",\n    location: \"Stockholm, Sweden\",\n    flag: \"🇸🇪\",\n    continent: \"Europe\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n\n  atl: {\n    code: \"atl\",\n    location: \"Atlanta, Georgia, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  bog: {\n    code: \"bog\",\n    location: \"Bogotá, Colombia\",\n    flag: \"🇨🇴\",\n    continent: \"South America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  bom: {\n    code: \"bom\",\n    location: \"Mumbai, India\",\n    flag: \"🇮🇳\",\n    continent: \"Asia\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  bos: {\n    code: \"bos\",\n    location: \"Boston, Massachusetts, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  cdg: {\n    code: \"cdg\",\n    location: \"Paris, France\",\n    flag: \"🇫🇷\",\n    continent: \"Europe\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  den: {\n    code: \"den\",\n    location: \"Denver, Colorado, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  dfw: {\n    code: \"dfw\",\n    location: \"Dallas, Texas, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  ewr: {\n    code: \"ewr\",\n    location: \"Secaucus, New Jersey, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  eze: {\n    code: \"eze\",\n    location: \"Ezeiza, Argentina\",\n    flag: \"🇦🇷\",\n    continent: \"South America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  fra: {\n    code: \"fra\",\n    location: \"Frankfurt, Germany\",\n    flag: \"🇩🇪\",\n    continent: \"Europe\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  gdl: {\n    code: \"gdl\",\n    location: \"Guadalajara, Mexico\",\n    flag: \"🇲🇽\",\n    continent: \"North America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  gig: {\n    code: \"gig\",\n    location: \"Rio de Janeiro, Brazil\",\n    flag: \"🇧🇷\",\n    continent: \"South America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  gru: {\n    code: \"gru\",\n    location: \"Sao Paulo, Brazil\",\n    flag: \"🇧🇷\",\n    continent: \"South America\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  hkg: {\n    code: \"hkg\",\n    location: \"Hong Kong, Hong Kong\",\n    flag: \"🇭🇰\",\n    continent: \"Asia\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  iad: {\n    code: \"iad\",\n    location: \"Ashburn, Virginia, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  jnb: {\n    code: \"jnb\",\n    location: \"Johannesburg, South Africa\",\n    flag: \"🇿🇦\",\n    continent: \"Africa\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  lax: {\n    code: \"lax\",\n    location: \"Los Angeles, California, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  lhr: {\n    code: \"lhr\",\n    location: \"London, United Kingdom\",\n    flag: \"🇬🇧\",\n    continent: \"Europe\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  mad: {\n    code: \"mad\",\n    location: \"Madrid, Spain\",\n    flag: \"🇪🇸\",\n    continent: \"Europe\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  mia: {\n    code: \"mia\",\n    location: \"Miami, Florida, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  nrt: {\n    code: \"nrt\",\n    location: \"Tokyo, Japan\",\n    flag: \"🇯🇵\",\n    continent: \"Asia\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  ord: {\n    code: \"ord\",\n    location: \"Chicago, Illinois, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  otp: {\n    code: \"otp\",\n    location: \"Bucharest, Romania\",\n    flag: \"🇷🇴\",\n    continent: \"Europe\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  phx: {\n    code: \"phx\",\n    location: \"Phoenix, Arizona, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  qro: {\n    code: \"qro\",\n    location: \"Querétaro, Mexico\",\n    flag: \"🇲🇽\",\n    continent: \"North America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  scl: {\n    code: \"scl\",\n    location: \"Santiago, Chile\",\n    flag: \"🇨🇱\",\n    continent: \"South America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  sjc: {\n    code: \"sjc\",\n    location: \"San Jose, California, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  sea: {\n    code: \"sea\",\n    location: \"Seattle, Washington, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  sin: {\n    code: \"sin\",\n    location: \"Singapore, Singapore\",\n    flag: \"🇸🇬\",\n    continent: \"Asia\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  syd: {\n    code: \"syd\",\n    location: \"Sydney, Australia\",\n    flag: \"🇦🇺\",\n    continent: \"Oceania\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  waw: {\n    code: \"waw\",\n    location: \"Warsaw, Poland\",\n    flag: \"🇵🇱\",\n    continent: \"Europe\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  yul: {\n    code: \"yul\",\n    location: \"Montreal, Canada\",\n    flag: \"🇨🇦\",\n    continent: \"North America\",\n    deprecated: true,\n    provider: \"fly\",\n  },\n  yyz: {\n    code: \"yyz\",\n    location: \"Toronto, Canada\",\n    flag: \"🇨🇦\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"fly\",\n  },\n  koyeb_fra: {\n    code: \"koyeb_fra\",\n    location: \"Frankfurt, Germany\",\n    flag: \"🇩🇪\",\n    continent: \"Europe\",\n    deprecated: false,\n    provider: \"koyeb\",\n  },\n  koyeb_par: {\n    code: \"koyeb_par\",\n    location: \"Paris, France\",\n    flag: \"🇫🇷\",\n    continent: \"Europe\",\n    deprecated: false,\n    provider: \"koyeb\",\n  },\n  koyeb_sfo: {\n    code: \"koyeb_sfo\",\n    location: \"San Francisco, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"koyeb\",\n  },\n  koyeb_sin: {\n    code: \"koyeb_sin\",\n    location: \"Singapore, Singapore\",\n    flag: \"🇸🇬\",\n    continent: \"Asia\",\n    deprecated: false,\n    provider: \"koyeb\",\n  },\n  koyeb_tyo: {\n    code: \"koyeb_tyo\",\n    location: \"Tokyo, Japan\",\n    flag: \"🇯🇵\",\n    continent: \"Asia\",\n    deprecated: false,\n    provider: \"koyeb\",\n  },\n  koyeb_was: {\n    code: \"koyeb_was\",\n    location: \"Washington, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"koyeb\",\n  },\n  \"railway_us-west2\": {\n    code: \"railway_us-west2\",\n    location: \"California, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"railway\",\n  },\n  \"railway_us-east4-eqdc4a\": {\n    code: \"railway_us-east4-eqdc4a\",\n    location: \"Virginia, USA\",\n    flag: \"🇺🇸\",\n    continent: \"North America\",\n    deprecated: false,\n    provider: \"railway\",\n  },\n  \"railway_europe-west4-drams3a\": {\n    code: \"railway_europe-west4-drams3a\",\n    location: \"Amsterdam, Netherlands\",\n    flag: \"🇳🇱\",\n    continent: \"Europe\",\n    deprecated: false,\n    provider: \"railway\",\n  },\n  \"railway_asia-southeast1-eqsg3a\": {\n    code: \"railway_asia-southeast1-eqsg3a\",\n    location: \"Singapore, Singapore\",\n    flag: \"🇸🇬\",\n    continent: \"Asia\",\n    deprecated: false,\n    provider: \"railway\",\n  },\n} as const;\n\nexport const AVAILABLE_REGIONS = ALL_REGIONS.filter(\n  (r) => !regionDict[r].deprecated,\n);\n\nexport function formatRegionCode(region: RegionInfo | Region | string) {\n  const r = typeof region === \"string\" ? getRegionInfo(region) : region;\n  const suffix = r.code.replace(/(koyeb_|railway_|fly_)/g, \"\");\n\n  if (r.provider === \"railway\") {\n    return suffix.replace(/(-eqdc4a|-eqsg3a|-drams3a)/g, \"\");\n  }\n\n  return suffix;\n}\n\n// NOTE: opts is used to override the location for private locations\nexport function getRegionInfo(region: string, opts?: { location?: string }) {\n  if (region in regionDict) {\n    return regionDict[region as Region];\n  }\n\n  return {\n    code: region,\n    location: opts?.location ?? \"Private Location\",\n    flag: \"🌐\",\n    continent: \"Private\",\n    deprecated: false,\n    provider: \"private\",\n  } satisfies Omit<RegionInfo, \"code\" | \"continent\"> & {\n    code: string;\n    continent: \"Private\";\n  };\n}\n\nexport const groupByContinent = Object.entries(regionDict).reduce<\n  Record<Continent, RegionInfo[]>\n>(\n  (acc, [_key, value]) => {\n    if (value.deprecated) {\n      return acc;\n    }\n    Object.assign(acc, {\n      [value.continent]: [...acc[value.continent], value],\n    });\n    return acc;\n  },\n  {\n    \"North America\": [],\n    Europe: [],\n    \"South America\": [],\n    Oceania: [],\n    Asia: [],\n    Africa: [],\n  },\n);\n"
  },
  {
    "path": "packages/regions/package.json",
    "content": "{\n  \"name\": \"@openstatus/regions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.ts\",\n  \"scripts\": {},\n  \"dependencies\": {\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/regions/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\"]\n}\n"
  },
  {
    "path": "packages/status-fetcher/README.md",
    "content": "# @openstatus/status-fetcher\n\nA production-ready, type-safe library for fetching real-time status from major tech companies and service providers, with support for 6 status page platforms.\n\n## Features\n\n- 📋 **Curated Registry** - TypeScript-based list of verified status pages with runtime validation\n- 🔌 **6 Provider Fetchers** - Support for Atlassian, Instatus, BetterStack, Incident.io, Custom APIs, and HTML scraping\n- ✅ **Type-safe** - Full TypeScript support with Zod runtime validation\n- 🔄 **Automatic Retries** - Exponential backoff retry logic for transient failures\n- ⏱️ **Smart Timeouts** - Configurable 30s timeout prevents hanging requests\n- 🎯 **Minimal Dependencies** - Only requires `zod` and `node-html-parser`\n- 🛡️ **Production Ready** - Comprehensive error handling and context-rich error messages\n\n## Quick Start\n\n```typescript\nimport { getStatusDirectory } from \"@openstatus/status-fetcher\";\nimport { fetchers } from \"@openstatus/status-fetcher/fetchers\";\n\nconst directory = getStatusDirectory();\nconst github = directory.find((e) => e.id === \"github\");\n\nif (github) {\n  const fetcher = fetchers.find((f) => f.canHandle(github));\n  if (fetcher) {\n    const status = await fetcher.fetch(github);\n    console.log(`${github.name}: ${status.status} - ${status.description}`);\n    // Output: GitHub: operational - All Systems Operational\n  }\n}\n```\n\n## Usage\n\n### Get Directory Entries\n\n```typescript\nimport { getStatusDirectory } from \"@openstatus/status-fetcher\";\n\nconst directory = getStatusDirectory();\nconsole.log(`Found ${directory.length} status pages`);\n\n// Filter by industry\nconst saasCompanies = directory.filter((e) =>\n  e.industry.includes(\"saas\"),\n);\n\n// Filter by provider\nconst atlassianPages = directory.filter((e) =>\n  e.provider === \"atlassian-statuspage\",\n);\n```\n\n### Fetch Status with Error Handling\n\n```typescript\nimport { getStatusDirectory } from \"@openstatus/status-fetcher\";\nimport { fetchers, FetchError } from \"@openstatus/status-fetcher/fetchers\";\n\nconst directory = getStatusDirectory();\n\nfor (const entry of directory) {\n  const fetcher = fetchers.find((f) => f.canHandle(entry));\n  if (!fetcher) {\n    console.log(`No fetcher for ${entry.name}`);\n    continue;\n  }\n\n  try {\n    const status = await fetcher.fetch(entry);\n    console.log(\n      `✅ ${entry.name}: ${status.status} (${status.severity}) - ${status.description}`,\n    );\n  } catch (error) {\n    if (error instanceof FetchError) {\n      console.error(\n        `❌ ${error.entryId}: ${error.message}`,\n      );\n    } else {\n      console.error(`Failed to fetch ${entry.name}:`, error);\n    }\n  }\n}\n```\n\n### Using Fetch Utilities Directly\n\n```typescript\nimport {\n  fetchWithRetry,\n  fetchWithTimeout,\n  fetchWithDeduplication,\n} from \"@openstatus/status-fetcher\";\n\n// Fetch with automatic retry (3 attempts, exponential backoff)\nconst response = await fetchWithRetry(\"https://api.example.com/status\", {\n  timeout: 30000,\n  maxRetries: 3,\n  headers: { \"User-Agent\": \"MyApp/1.0\" },\n});\n\n// Fetch with timeout only\nconst response2 = await fetchWithTimeout(\"https://api.example.com/health\", {\n  timeout: 10000,\n});\n\n// Fetch with request deduplication (concurrent requests to same URL are deduplicated)\nconst [r1, r2, r3] = await Promise.all([\n  fetchWithDeduplication(\"https://api.example.com/status\"),\n  fetchWithDeduplication(\"https://api.example.com/status\"), // Reuses first request\n  fetchWithDeduplication(\"https://api.example.com/status\"), // Reuses first request\n]);\n```\n\n## Supported Providers\n\n| Provider | Coverage | Features | Authentication |\n|----------|----------|----------|----------------|\n| Atlassian Statuspage | ~60% of status pages | Full API support, rich metadata | None required |\n| Instatus | Growing adoption | Real-time updates, maintenance windows | None required |\n| BetterStack (Better Uptime) | Popular in startups | Aggregate state, timezone support | None required |\n| Incident.io | Enterprise | Incident workflow states | None required |\n| Custom APIs | Slack, etc. | Configurable parsers | None required |\n| HTML Scraper | Universal fallback | Pattern-based extraction | None required |\n\n## Architecture\n\n### Type System\n\nAll types are derived from single-source-of-truth arrays:\n\n```typescript\n// Source of truth\nexport const STATUS_PAGE_PROVIDERS = [\n  \"atlassian-statuspage\",\n  \"instatus\",\n  // ...\n] as const;\n\n// TypeScript type (automatically inferred)\nexport type StatusPageProvider = (typeof STATUS_PAGE_PROVIDERS)[number];\n\n// Zod schema (automatically derived)\nexport const statusPageProviderSchema = z.enum(STATUS_PAGE_PROVIDERS);\n```\n\nThis eliminates duplication and ensures TypeScript types and runtime validation are always in sync.\n\n### Severity vs Status\n\nThe package provides two complementary fields:\n\n- **Severity**: Impact level (`none`, `minor`, `major`, `critical`)\n- **Status**: Actual state (`operational`, `degraded`, `investigating`, `major_outage`, etc.)\n\nThis allows for nuanced status reporting:\n\n```typescript\n{\n  severity: \"none\",\n  status: \"under_maintenance\"  // Scheduled, not impactful\n}\n\n{\n  severity: \"major\",\n  status: \"investigating\"  // Active incident being investigated\n}\n```\n\n### Retry & Timeout Logic\n\nAll fetchers use intelligent retry logic:\n\n- **Automatic retries**: 3 attempts with exponential backoff (100ms → 200ms → 400ms)\n- **Smart retry**: Only retries network errors and 5xx responses, not 4xx client errors\n- **Timeout**: 30s default timeout prevents hanging requests\n- **Context-rich errors**: Errors include fetcher name, entry ID, and URL for debugging\n\n```typescript\n// Retry logic (simplified)\nfor (let attempt = 0; attempt <= maxRetries; attempt++) {\n  try {\n    const response = await fetchWithTimeout(url, { timeout: 30000 });\n    if (response.ok || response.status < 500) return response;\n  } catch (error) {\n    if (attempt < maxRetries && shouldRetry(error)) {\n      await sleep(delay);\n      delay *= 2; // Exponential backoff\n    }\n  }\n}\n```\n\n## Data Structures\n\n### StatusPageEntry\n\n```typescript\ninterface StatusPageEntry {\n  id: string;                    // Unique slug (e.g., \"github\")\n  name: string;                  // Display name\n  url: string;                   // Main company website\n  status_page_url: string;       // Status page URL\n  provider: StatusPageProvider;  // Platform used\n  industry: Industry[];          // Categorization (e.g., [\"saas\", \"development-tools\"])\n  description?: string;          // Short description\n  api_config?: ApiConfig;        // Fetcher configuration\n}\n```\n\n### StatusResult\n\n```typescript\ninterface StatusResult {\n  severity: SeverityLevel;  // Impact: \"none\" | \"minor\" | \"major\" | \"critical\"\n  status: StatusType;       // State: \"operational\" | \"degraded\" | \"partial_outage\" |\n                            //        \"major_outage\" | \"under_maintenance\" | \"investigating\" |\n                            //        \"identified\" | \"monitoring\" | \"resolved\"\n  description: string;      // Human-readable status message\n  updated_at: number;       // Timestamp (ms since epoch)\n  timezone?: string;        // Timezone (e.g., \"UTC\", \"America/New_York\")\n}\n```\n\n### ApiConfig\n\n```typescript\ninterface ApiConfig {\n  type: \"atlassian\" | \"instatus\" | \"betterstack\" | \"incidentio\" | \"custom\" | \"html-scraper\";\n  endpoint?: string;  // Custom API endpoint (overrides default)\n  parser?: string;    // Custom parser name (for custom type)\n}\n```\n\n## Testing\n\nRun the test suite:\n\n```bash\nbun test\n# or\nnpm test\n```\n\nTest all fetchers manually:\n\n```bash\ntsx scripts/test-fetchers.ts\n```\n\nExample output:\n```\n🔍 Testing 7 entries...\n\n✅ GitHub: operational (none) - All Systems Operational (245ms)\n✅ Vercel: operational (none) - All Systems Operational (198ms)\n✅ Slack: operational (none) - All Systems Operational (312ms)\n✅ Linear: operational (none) - All Systems Operational (156ms)\n✅ OpenAI: operational (none) - All Systems Operational (223ms)\n✅ Stripe: operational (none) - All Systems Operational (189ms)\n✅ Cloudflare: operational (none) - All Systems Operational (267ms)\n\n✨ Testing complete! 7/7 passed\n```\n\n## Adding New Entries\n\n### 1. Add to Directory\n\nEdit `src/data/directory.ts`:\n\n```typescript\n{\n  id: \"stripe\",\n  name: \"Stripe\",\n  url: \"https://stripe.com\",\n  status_page_url: \"https://status.stripe.com\",\n  provider: \"atlassian-statuspage\",\n  industry: [\"fintech\"],\n  description: \"Online payment processing platform\",\n  api_config: {\n    type: \"atlassian\",\n  },\n}\n```\n\nThe directory is validated at startup using Zod, so invalid entries will fail immediately.\n\n### 2. Test the Entry\n\n```bash\nbun test\n```\n\n### 3. Verify Manually\n\n```typescript\nimport { getStatusDirectory } from \"@openstatus/status-fetcher\";\nimport { fetchers } from \"@openstatus/status-fetcher/fetchers\";\n\nconst stripe = getStatusDirectory().find((e) => e.id === \"stripe\");\nconst fetcher = fetchers.find((f) => f.canHandle(stripe!));\nconst status = await fetcher!.fetch(stripe!);\nconsole.log(status);\n```\n\n## Implementing a Custom Parser\n\nFor companies with proprietary APIs (like Slack), add a parser to `CustomApiFetcher`:\n\n```typescript\n// In src/fetchers/custom.ts\nprivate parseMyCompany(json: unknown): StatusResult {\n  const schema = z.object({\n    status: z.string(),\n    lastUpdate: z.number(),\n  });\n\n  const data = schema.parse(json);\n\n  return {\n    severity: data.status === \"ok\" ? \"none\" : \"major\",\n    status: data.status === \"ok\" ? \"operational\" : \"major_outage\",\n    description: data.status === \"ok\" ? \"All Systems Operational\" : \"Service Disruption\",\n    updated_at: data.lastUpdate,\n    timezone: \"UTC\",\n  };\n}\n\nprivate parseResponse(json: unknown, parser: string): StatusResult {\n  switch (parser) {\n    case \"slack\":\n      return this.parseSlack(json);\n    case \"aws\":\n      return this.parseAws(json);\n    case \"mycompany\":  // Add your parser\n      return this.parseMyCompany(json);\n    default:\n      return this.parseGeneric(json);\n  }\n}\n```\n\nThen use it in your directory entry:\n\n```typescript\n{\n  id: \"mycompany\",\n  name: \"My Company\",\n  url: \"https://mycompany.com\",\n  status_page_url: \"https://status.mycompany.com\",\n  provider: \"custom\",\n  industry: [\"saas\"],\n  api_config: {\n    type: \"custom\",\n    endpoint: \"https://status.mycompany.com/api/current\",\n    parser: \"mycompany\",\n  },\n}\n```\n\n## Error Handling\n\nThe package provides rich error context through the `FetchError` class:\n\n```typescript\nimport { FetchError } from \"@openstatus/status-fetcher\";\n\ntry {\n  const status = await fetcher.fetch(entry);\n} catch (error) {\n  if (error instanceof FetchError) {\n    console.error({\n      message: error.message,      // \"HTTP 500: Internal Server Error\"\n      url: error.url,              // \"https://api.example.com/status\"\n      fetcherName: error.fetcherName,  // \"atlassian\"\n      entryId: error.entryId,      // \"github\"\n      cause: error.cause,          // Original error\n    });\n  }\n}\n```\n\n## API Reference\n\n### Exported Types\n\n```typescript\n// Core types\nexport type { StatusPageEntry, StatusResult, ApiConfig, StatusFetcher };\n\n// Type unions\nexport type { StatusPageProvider, Industry, SeverityLevel, StatusType };\n\n// Arrays (source of truth)\nexport {\n  STATUS_PAGE_PROVIDERS,\n  INDUSTRIES,\n  SEVERITY_LEVELS,\n  STATUS_TYPES,\n  API_CONFIG_TYPES,\n};\n\n// Zod schemas\nexport {\n  statusPageEntrySchema,\n  statusPageProviderSchema,\n  industrySchema,\n  apiConfigSchema,\n};\n\n// Utilities\nexport { fetchWithTimeout, fetchWithRetry, FetchError };\nexport { inferStatus };\n```\n\n### Functions\n\n```typescript\n// Get the full directory\ngetStatusDirectory(): StatusPageEntry[]\n\n// Infer status from description and severity\ninferStatus(description: string, severity: SeverityLevel): StatusType\n\n// Fetch with timeout\nfetchWithTimeout(url: string, options?: FetchWithTimeoutOptions): Promise<Response>\n\n// Fetch with retry\nfetchWithRetry(url: string, options?: FetchWithTimeoutOptions & RetryOptions): Promise<Response>\n```\n\n## Performance Considerations\n\n- **Validation overhead**: Directory validation happens once at module load (~1ms for 100 entries)\n- **Retry timing**: Default retry strategy adds ~700ms max (100ms + 200ms + 400ms) for failures\n- **Timeout**: 30s timeout per request prevents indefinite hanging\n- **Parallel fetching**: Fetchers are stateless and can be called concurrently\n\n```typescript\n// Fetch multiple statuses in parallel\nconst statuses = await Promise.allSettled(\n  directory.map(async (entry) => {\n    const fetcher = fetchers.find((f) => f.canHandle(entry));\n    return fetcher ? fetcher.fetch(entry) : null;\n  }),\n);\n```\n\n## Contributing\n\nContributions are welcome! Please:\n\n1. Add tests for new features\n2. Ensure all tests pass (`bun test`)\n3. Follow existing code style\n4. Update documentation\n\n## Roadmap\n\n- [ ] Database storage for status history\n- [ ] Cron job for automated status updates\n- [ ] Web UI displaying the directory\n- [ ] Public REST API endpoint\n- [ ] Community submission form\n- [ ] Webhook notifications\n- [ ] GraphQL API\n- [ ] Status page analytics\n\n## License\n\nMIT\n\n## Support\n\n- 📚 [Documentation](https://github.com/openstatusHQ/openstatus)\n- 🐛 [Report Issues](https://github.com/openstatusHQ/openstatus/issues)\n- 💬 [Discord Community](https://openstatus.dev/discord)\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/fetch-utils.test.ts",
    "content": "import { describe, expect, it, mock } from \"bun:test\";\nimport {\n  FetchError,\n  fetchWithDeduplication,\n  fetchWithRetry,\n  fetchWithTimeout,\n} from \"../src/fetch-utils\";\n\ndescribe(\"fetchWithTimeout\", () => {\n  it(\"should successfully fetch with timeout\", async () => {\n    global.fetch = mock(() =>\n      Promise.resolve({\n        ok: true,\n        status: 200,\n        json: async () => ({ data: \"test\" }),\n      } as Response),\n    );\n\n    const response = await fetchWithTimeout(\"https://api.example.com\", {\n      timeout: 5000,\n    });\n\n    expect(response.ok).toBe(true);\n    expect(response.status).toBe(200);\n  });\n\n  it.skip(\"should timeout after specified duration\", async () => {\n    // Note: This test is skipped because testing AbortController timeout\n    // behavior with mocked fetch is unreliable in test environments\n    global.fetch = mock(\n      () =>\n        new Promise((resolve) => {\n          // Never resolve - let timeout trigger\n          setTimeout(() => resolve({} as Response), 10000);\n        }),\n    );\n\n    await expect(\n      fetchWithTimeout(\"https://api.example.com\", { timeout: 50 }),\n    ).rejects.toThrow(\"Request timeout after 50ms\");\n  });\n\n  it(\"should use default timeout of 30000ms\", async () => {\n    let _timeoutDuration = 0;\n\n    global.fetch = mock(\n      () =>\n        new Promise((resolve) => {\n          setTimeout(() => {\n            _timeoutDuration = Date.now();\n            resolve({ ok: true } as Response);\n          }, 10);\n        }),\n    );\n\n    const start = Date.now();\n    await fetchWithTimeout(\"https://api.example.com\");\n    const duration = Date.now() - start;\n\n    // Should not timeout on quick response\n    expect(duration).toBeLessThan(100);\n  });\n\n  it(\"should clear timeout on successful response\", async () => {\n    global.fetch = mock(() => Promise.resolve({ ok: true } as Response));\n\n    // Should not throw or cause memory leak\n    await fetchWithTimeout(\"https://api.example.com\", { timeout: 1000 });\n\n    // Wait a bit to ensure timeout is cleared\n    await new Promise((resolve) => setTimeout(resolve, 50));\n  });\n\n  it(\"should pass through fetch options\", async () => {\n    let capturedOptions: RequestInit | undefined;\n\n    global.fetch = mock((_url: string, options?: RequestInit) => {\n      capturedOptions = options;\n      return Promise.resolve({ ok: true } as Response);\n    });\n\n    await fetchWithTimeout(\"https://api.example.com\", {\n      timeout: 5000,\n      headers: { \"X-Custom\": \"header\" },\n      method: \"POST\",\n    });\n\n    expect(capturedOptions?.headers).toEqual({ \"X-Custom\": \"header\" });\n    expect(capturedOptions?.method).toBe(\"POST\");\n    expect(capturedOptions?.signal).toBeDefined();\n  });\n});\n\ndescribe(\"fetchWithRetry\", () => {\n  it(\"should succeed on first attempt\", async () => {\n    global.fetch = mock(() =>\n      Promise.resolve({\n        ok: true,\n        status: 200,\n        json: async () => ({ data: \"test\" }),\n      } as Response),\n    );\n\n    const response = await fetchWithRetry(\"https://api.example.com\");\n\n    expect(response.ok).toBe(true);\n    expect(response.status).toBe(200);\n  });\n\n  it(\"should retry on 5xx errors\", async () => {\n    let attempts = 0;\n\n    global.fetch = mock(() => {\n      attempts++;\n      if (attempts < 3) {\n        return Promise.resolve({\n          ok: false,\n          status: 503,\n          statusText: \"Service Unavailable\",\n        } as Response);\n      }\n      return Promise.resolve({\n        ok: true,\n        status: 200,\n      } as Response);\n    });\n\n    const response = await fetchWithRetry(\"https://api.example.com\", {\n      maxRetries: 3,\n      initialDelay: 10,\n    });\n\n    expect(attempts).toBe(3);\n    expect(response.ok).toBe(true);\n  });\n\n  it(\"should not retry on 4xx errors\", async () => {\n    let attempts = 0;\n\n    global.fetch = mock(() => {\n      attempts++;\n      return Promise.resolve({\n        ok: false,\n        status: 404,\n        statusText: \"Not Found\",\n      } as Response);\n    });\n\n    const response = await fetchWithRetry(\"https://api.example.com\", {\n      maxRetries: 3,\n    });\n\n    expect(attempts).toBe(1); // Should not retry\n    expect(response.status).toBe(404);\n  });\n\n  it(\"should throw after max retries\", async () => {\n    global.fetch = mock(() =>\n      Promise.resolve({\n        ok: false,\n        status: 500,\n        statusText: \"Internal Server Error\",\n      } as Response),\n    );\n\n    await expect(\n      fetchWithRetry(\"https://api.example.com\", {\n        maxRetries: 2,\n        initialDelay: 10,\n      }),\n    ).rejects.toThrow(\"HTTP 500: Internal Server Error\");\n  });\n\n  it(\"should use exponential backoff\", async () => {\n    const attemptTimes: number[] = [];\n\n    global.fetch = mock(() => {\n      attemptTimes.push(Date.now());\n      return Promise.resolve({\n        ok: false,\n        status: 503,\n        statusText: \"Service Unavailable\",\n      } as Response);\n    });\n\n    try {\n      await fetchWithRetry(\"https://api.example.com\", {\n        maxRetries: 3,\n        initialDelay: 50,\n        maxDelay: 1000,\n      });\n    } catch {\n      // Expected to throw\n    }\n\n    // Should have 4 attempts (1 initial + 3 retries)\n    expect(attemptTimes.length).toBe(4);\n\n    // Calculate delays between attempts\n    const delays = [\n      attemptTimes[1] - attemptTimes[0],\n      attemptTimes[2] - attemptTimes[1],\n      attemptTimes[3] - attemptTimes[2],\n    ];\n\n    // First delay should be ~50ms (with jitter ±12.5ms)\n    expect(delays[0]).toBeGreaterThanOrEqual(35);\n    expect(delays[0]).toBeLessThanOrEqual(75);\n\n    // Second delay should be ~100ms (with jitter ±25ms)\n    expect(delays[1]).toBeGreaterThanOrEqual(70);\n    expect(delays[1]).toBeLessThanOrEqual(150);\n\n    // Third delay should be ~200ms (with jitter ±50ms)\n    expect(delays[2]).toBeGreaterThanOrEqual(140);\n    expect(delays[2]).toBeLessThanOrEqual(300);\n  });\n\n  it(\"should respect maxDelay cap\", async () => {\n    const delays: number[] = [];\n    let lastTime = Date.now();\n\n    global.fetch = mock(() => {\n      const now = Date.now();\n      if (delays.length > 0) {\n        delays.push(now - lastTime);\n      }\n      lastTime = now;\n\n      return Promise.resolve({\n        ok: false,\n        status: 503,\n        statusText: \"Service Unavailable\",\n      } as Response);\n    });\n\n    try {\n      await fetchWithRetry(\"https://api.example.com\", {\n        maxRetries: 5,\n        initialDelay: 100,\n        maxDelay: 150, // Cap at 150ms\n      });\n    } catch {\n      // Expected to throw\n    }\n\n    // All delays should be capped at ~150ms (with jitter)\n    delays.forEach((delay) => {\n      expect(delay).toBeLessThanOrEqual(200); // 150ms + max jitter\n    });\n  });\n\n  it(\"should add jitter to prevent thundering herd\", async () => {\n    const delays: number[] = [];\n\n    global.fetch = mock(() =>\n      Promise.resolve({\n        ok: false,\n        status: 500,\n        statusText: \"Error\",\n      } as Response),\n    );\n\n    // Run multiple retry sequences\n    const _results = await Promise.allSettled(\n      Array.from({ length: 5 }, async (_, i) => {\n        const startTime = Date.now();\n        try {\n          await fetchWithRetry(`https://api.example.com/${i}`, {\n            maxRetries: 1,\n            initialDelay: 100,\n          });\n        } catch {\n          delays.push(Date.now() - startTime);\n        }\n      }),\n    );\n\n    // Delays should vary due to jitter (not all exactly the same)\n    const uniqueDelays = new Set(delays);\n    expect(uniqueDelays.size).toBeGreaterThan(1);\n  });\n\n  it(\"should allow custom shouldRetry function\", async () => {\n    let attempts = 0;\n\n    global.fetch = mock(() => {\n      attempts++;\n      return Promise.resolve({\n        ok: false,\n        status: 503, // Service Unavailable\n        statusText: \"Service Unavailable\",\n      } as Response);\n    });\n\n    // Custom retry logic: never retry\n    const shouldRetry = () => false;\n\n    try {\n      await fetchWithRetry(\"https://api.example.com\", {\n        maxRetries: 2,\n        initialDelay: 10,\n        shouldRetry,\n      });\n    } catch {\n      // Expected to throw\n    }\n\n    expect(attempts).toBe(1); // Should not retry due to custom function\n  });\n\n  it(\"should handle network errors with retry\", async () => {\n    let attempts = 0;\n\n    global.fetch = mock(() => {\n      attempts++;\n      if (attempts < 2) {\n        return Promise.reject(new Error(\"fetch failed\"));\n      }\n      return Promise.resolve({ ok: true } as Response);\n    });\n\n    const response = await fetchWithRetry(\"https://api.example.com\", {\n      maxRetries: 3,\n      initialDelay: 10,\n    });\n\n    expect(attempts).toBe(2);\n    expect(response.ok).toBe(true);\n  });\n});\n\ndescribe(\"FetchError\", () => {\n  it(\"should create error with all context\", () => {\n    const originalError = new Error(\"Network timeout\");\n    const fetchError = new FetchError(\n      \"Request failed\",\n      \"https://api.example.com\",\n      \"atlassian\",\n      \"github\",\n      originalError,\n    );\n\n    expect(fetchError.message).toBe(\"Request failed\");\n    expect(fetchError.url).toBe(\"https://api.example.com\");\n    expect(fetchError.fetcherName).toBe(\"atlassian\");\n    expect(fetchError.entryId).toBe(\"github\");\n    expect(fetchError.cause).toBe(originalError);\n    expect(fetchError.name).toBe(\"FetchError\");\n  });\n\n  it(\"should format toString with all context\", () => {\n    const originalError = new Error(\"Connection reset\");\n    const fetchError = new FetchError(\n      \"HTTP 500\",\n      \"https://api.example.com\",\n      \"atlassian\",\n      \"github\",\n      originalError,\n    );\n\n    const str = fetchError.toString();\n\n    expect(str).toContain(\"[FetchError]\");\n    expect(str).toContain(\"atlassian\");\n    expect(str).toContain(\"(github)\");\n    expect(str).toContain(\"HTTP 500\");\n    expect(str).toContain(\"Connection reset\");\n  });\n\n  it(\"should format toString without optional fields\", () => {\n    const fetchError = new FetchError(\n      \"Request failed\",\n      \"https://api.example.com\",\n    );\n\n    const str = fetchError.toString();\n\n    expect(str).toBe(\"[FetchError]: Request failed\");\n  });\n\n  it(\"should support Error.cause standard property\", () => {\n    const originalError = new Error(\"Original error\");\n    const fetchError = new FetchError(\n      \"Wrapper error\",\n      \"https://api.example.com\",\n      undefined,\n      undefined,\n      originalError,\n    );\n\n    expect(fetchError.cause).toBe(originalError);\n  });\n});\n\ndescribe(\"fetchWithDeduplication\", () => {\n  it(\"should deduplicate concurrent requests to same URL\", async () => {\n    let fetchCount = 0;\n\n    global.fetch = mock(() => {\n      fetchCount++;\n      return new Promise((resolve) =>\n        setTimeout(() => resolve({ ok: true, status: 200 } as Response), 50),\n      );\n    });\n\n    // Make 5 concurrent requests to the same URL\n    const promises = Array.from({ length: 5 }, () =>\n      fetchWithDeduplication(\"https://api.example.com\"),\n    );\n\n    const responses = await Promise.all(promises);\n\n    // Should only make 1 actual fetch\n    expect(fetchCount).toBe(1);\n\n    // All responses should be the same\n    responses.forEach((response) => {\n      expect(response.ok).toBe(true);\n      expect(response.status).toBe(200);\n    });\n  });\n\n  it(\"should not deduplicate requests to different URLs\", async () => {\n    let fetchCount = 0;\n\n    global.fetch = mock(() => {\n      fetchCount++;\n      return Promise.resolve({ ok: true } as Response);\n    });\n\n    await Promise.all([\n      fetchWithDeduplication(\"https://api.example.com/1\"),\n      fetchWithDeduplication(\"https://api.example.com/2\"),\n      fetchWithDeduplication(\"https://api.example.com/3\"),\n    ]);\n\n    // Should make 3 separate fetches\n    expect(fetchCount).toBe(3);\n  });\n\n  it(\"should not deduplicate requests with different methods\", async () => {\n    let fetchCount = 0;\n\n    global.fetch = mock(() => {\n      fetchCount++;\n      return Promise.resolve({ ok: true } as Response);\n    });\n\n    await Promise.all([\n      fetchWithDeduplication(\"https://api.example.com\", { method: \"GET\" }),\n      fetchWithDeduplication(\"https://api.example.com\", { method: \"POST\" }),\n    ]);\n\n    // Should make 2 separate fetches\n    expect(fetchCount).toBe(2);\n  });\n\n  it(\"should clean up cache after request completes\", async () => {\n    let fetchCount = 0;\n\n    global.fetch = mock(() => {\n      fetchCount++;\n      return Promise.resolve({ ok: true } as Response);\n    });\n\n    // First request\n    await fetchWithDeduplication(\"https://api.example.com\");\n\n    // Wait a bit for cleanup\n    await new Promise((resolve) => setTimeout(resolve, 10));\n\n    // Second request after first completes\n    await fetchWithDeduplication(\"https://api.example.com\");\n\n    // Should make 2 separate fetches (no deduplication after completion)\n    expect(fetchCount).toBe(2);\n  });\n\n  it(\"should handle errors in deduplicated requests\", async () => {\n    global.fetch = mock(() =>\n      Promise.resolve({\n        ok: false,\n        status: 500,\n        statusText: \"Error\",\n      } as Response),\n    );\n\n    const promises = Array.from({ length: 3 }, () =>\n      fetchWithDeduplication(\"https://api.example.com\", { maxRetries: 0 }),\n    );\n\n    // All should receive the same error\n    await expect(Promise.all(promises)).rejects.toThrow();\n  });\n\n  it(\"should deduplicate requests with same headers\", async () => {\n    let fetchCount = 0;\n\n    global.fetch = mock(() => {\n      fetchCount++;\n      return Promise.resolve({ ok: true } as Response);\n    });\n\n    await Promise.all([\n      fetchWithDeduplication(\"https://api.example.com\", {\n        headers: { \"X-Test\": \"value\" },\n      }),\n      fetchWithDeduplication(\"https://api.example.com\", {\n        headers: { \"X-Test\": \"value\" },\n      }),\n    ]);\n\n    // Should only make 1 fetch\n    expect(fetchCount).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/fetchers/atlassian.test.ts",
    "content": "import { beforeEach, describe, expect, it, mock } from \"bun:test\";\nimport { AtlassianFetcher } from \"../../src/fetchers/atlassian\";\nimport type { StatusPageEntry } from \"../../src/types\";\n\ndescribe(\"AtlassianFetcher\", () => {\n  let fetcher: AtlassianFetcher;\n\n  beforeEach(() => {\n    fetcher = new AtlassianFetcher();\n  });\n\n  describe(\"canHandle\", () => {\n    it(\"should identify entries with api_config.type = atlassian\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"atlassian\" },\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should identify entries with provider = atlassian-statuspage\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should identify entries with statuspage.io in URL\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.statuspage.io\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should not handle other providers\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(false);\n    });\n  });\n\n  describe(\"fetch\", () => {\n    it(\"should fetch and parse status correctly\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"github\",\n        name: \"GitHub\",\n        url: \"https://github.com\",\n        status_page_url: \"https://www.githubstatus.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"development-tools\"],\n        api_config: { type: \"atlassian\" },\n      };\n\n      const mockResponse = {\n        page: {\n          id: \"abc123\",\n          name: \"GitHub\",\n          url: \"https://www.githubstatus.com\",\n          timezone: \"Etc/UTC\",\n          updated_at: \"2024-02-16T12:00:00.000Z\",\n        },\n        status: {\n          indicator: \"none\",\n          description: \"All Systems Operational\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"All Systems Operational\");\n      expect(result.timezone).toBe(\"Etc/UTC\");\n      expect(typeof result.updated_at).toBe(\"number\");\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"https://www.githubstatus.com/api/v2/summary.json\",\n        expect.objectContaining({\n          headers: expect.objectContaining({\n            \"User-Agent\": \"OpenStatus-Directory/1.0\",\n          }),\n        }),\n      );\n    });\n\n    it(\"should handle status with optional time_zone\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"openai\",\n        name: \"OpenAI\",\n        url: \"https://openai.com\",\n        status_page_url: \"https://status.openai.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"ai-ml\"],\n      };\n\n      const mockResponse = {\n        page: {\n          id: \"abc123\",\n          name: \"OpenAI\",\n          url: \"https://status.openai.com\",\n          updated_at: \"2024-02-16T12:00:00.000Z\",\n        },\n        status: {\n          indicator: \"minor\",\n          description: \"Elevated Error Rates\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"minor\");\n      expect(result.description).toBe(\"Elevated Error Rates\");\n      expect(result.timezone).toBeUndefined();\n    });\n\n    it(\"should handle major incidents\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        page: {\n          id: \"abc123\",\n          name: \"Test\",\n          url: \"https://status.test.com\",\n          timezone: \"America/New_York\",\n          updated_at: \"2024-02-16T12:00:00.000Z\",\n        },\n        status: {\n          indicator: \"major\",\n          description: \"Major Service Outage\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"major\");\n      expect(result.description).toBe(\"Major Service Outage\");\n    });\n\n    it(\"should use custom endpoint if provided\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n        api_config: {\n          type: \"atlassian\",\n          endpoint: \"https://custom.endpoint.com/status.json\",\n        },\n      };\n\n      const mockResponse = {\n        page: {\n          id: \"abc123\",\n          name: \"Test\",\n          url: \"https://status.test.com\",\n          timezone: \"UTC\",\n          updated_at: \"2024-02-16T12:00:00.000Z\",\n        },\n        status: {\n          indicator: \"none\",\n          description: \"All Systems Operational\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      await fetcher.fetch(entry);\n\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"https://custom.endpoint.com/status.json\",\n        expect.any(Object),\n      );\n    });\n\n    it(\"should throw error on non-200 response\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: false,\n          status: 404,\n          statusText: \"Not Found\",\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\"HTTP 404: Not Found\");\n    });\n\n    it(\"should throw error on invalid JSON schema\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => ({ invalid: \"data\" }),\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/fetchers/betterstack.test.ts",
    "content": "import { beforeEach, describe, expect, it, mock } from \"bun:test\";\nimport { BetterStackFetcher } from \"../../src/fetchers/betterstack\";\nimport type { StatusPageEntry } from \"../../src/types\";\n\ndescribe(\"BetterStackFetcher\", () => {\n  let fetcher: BetterStackFetcher;\n\n  beforeEach(() => {\n    fetcher = new BetterStackFetcher();\n  });\n\n  describe(\"canHandle\", () => {\n    it(\"should identify entries with api_config.type = betterstack\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"betterstack\" },\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should identify entries with provider = better-uptime\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"better-uptime\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should identify entries with betteruptime.com in URL\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.betteruptime.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should identify entries with betterstack.com in URL\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.betterstack.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should not handle other providers\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(false);\n    });\n  });\n\n  describe(\"fetch\", () => {\n    it(\"should fetch and parse operational status\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test Service\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"better-uptime\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        data: {\n          id: \"123\",\n          type: \"status_page\",\n          attributes: {\n            company_name: \"Test Service\",\n            timezone: \"America/New_York\",\n            aggregate_state: \"operational\",\n            updated_at: \"2024-02-16T12:00:00.000Z\",\n          },\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"All Systems Operational\");\n      expect(result.timezone).toBe(\"America/New_York\");\n      expect(typeof result.updated_at).toBe(\"number\");\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"https://status.test.com/index.json\",\n        expect.objectContaining({\n          headers: expect.objectContaining({\n            \"User-Agent\": \"OpenStatus-Directory/1.0\",\n            Accept: \"application/json\",\n          }),\n        }),\n      );\n    });\n\n    it(\"should map degraded state to minor indicator\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"better-uptime\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        data: {\n          id: \"123\",\n          type: \"status_page\",\n          attributes: {\n            company_name: \"Test\",\n            timezone: \"UTC\",\n            aggregate_state: \"degraded\",\n            updated_at: \"2024-02-16T12:00:00.000Z\",\n          },\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"minor\");\n      expect(result.description).toBe(\"Degraded Service\");\n    });\n\n    it(\"should map downtime state to major indicator\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"better-uptime\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        data: {\n          id: \"123\",\n          type: \"status_page\",\n          attributes: {\n            company_name: \"Test\",\n            timezone: \"UTC\",\n            aggregate_state: \"downtime\",\n            updated_at: \"2024-02-16T12:00:00.000Z\",\n          },\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"major\");\n      expect(result.description).toBe(\"Service Outage\");\n    });\n\n    it(\"should use custom endpoint if provided\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"better-uptime\",\n        industry: [\"saas\"],\n        api_config: {\n          type: \"betterstack\",\n          endpoint: \"https://custom.endpoint.com/status.json\",\n        },\n      };\n\n      const mockResponse = {\n        data: {\n          id: \"123\",\n          type: \"status_page\",\n          attributes: {\n            company_name: \"Test\",\n            timezone: \"UTC\",\n            aggregate_state: \"operational\",\n            updated_at: \"2024-02-16T12:00:00.000Z\",\n          },\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      await fetcher.fetch(entry);\n\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"https://custom.endpoint.com/status.json\",\n        expect.any(Object),\n      );\n    });\n\n    it(\"should throw error on non-200 response\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"better-uptime\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: false,\n          status: 403,\n          statusText: \"Forbidden\",\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\"HTTP 403: Forbidden\");\n    });\n\n    it(\"should throw error on invalid JSON schema\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"better-uptime\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => ({ invalid: \"data\" }),\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/fetchers/custom.test.ts",
    "content": "import { beforeEach, describe, expect, it, mock } from \"bun:test\";\nimport { CustomApiFetcher } from \"../../src/fetchers/custom\";\nimport type { StatusPageEntry } from \"../../src/types\";\n\ndescribe(\"CustomApiFetcher\", () => {\n  let fetcher: CustomApiFetcher;\n\n  beforeEach(() => {\n    fetcher = new CustomApiFetcher();\n  });\n\n  describe(\"canHandle\", () => {\n    it(\"should only handle entries with api_config.type = custom\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"custom\",\n        industry: [\"saas\"],\n        api_config: { type: \"custom\", endpoint: \"https://api.test.com/status\" },\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should not handle entries without custom api_config\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"custom\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(false);\n    });\n\n    it(\"should not handle other api_config types\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"custom\",\n        industry: [\"saas\"],\n        api_config: { type: \"atlassian\" },\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(false);\n    });\n  });\n\n  describe(\"fetch\", () => {\n    it(\"should throw error if endpoint is not provided\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"custom\",\n        industry: [\"saas\"],\n        api_config: { type: \"custom\" },\n      };\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\n        \"Custom API requires explicit endpoint configuration\",\n      );\n    });\n\n    describe(\"Slack parser\", () => {\n      it(\"should parse Slack API with no incidents (ok status)\", async () => {\n        const entry: StatusPageEntry = {\n          id: \"slack\",\n          name: \"Slack\",\n          url: \"https://slack.com\",\n          status_page_url: \"https://slack-status.com\",\n          provider: \"custom\",\n          industry: [\"communication\"],\n          api_config: {\n            type: \"custom\",\n            endpoint: \"https://slack-status.com/api/v2.0.0/current\",\n            parser: \"slack\",\n          },\n        };\n\n        const mockResponse = {\n          status: \"ok\",\n          date_created: \"2024-02-16T12:00:00.000Z\",\n          date_updated: \"2024-02-16T13:00:00.000Z\",\n          active_incidents: [],\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => mockResponse,\n          } as Response),\n        );\n\n        const result = await fetcher.fetch(entry);\n\n        expect(result.severity).toBe(\"none\");\n        expect(result.description).toBe(\"All Systems Operational\");\n        expect(result.timezone).toBe(\"UTC\");\n        expect(typeof result.updated_at).toBe(\"number\");\n      });\n\n      it(\"should parse Slack API with numeric timestamps\", async () => {\n        const entry: StatusPageEntry = {\n          id: \"slack\",\n          name: \"Slack\",\n          url: \"https://slack.com\",\n          status_page_url: \"https://slack-status.com\",\n          provider: \"custom\",\n          industry: [\"communication\"],\n          api_config: {\n            type: \"custom\",\n            endpoint: \"https://slack-status.com/api/v2.0.0/current\",\n            parser: \"slack\",\n          },\n        };\n\n        const mockResponse = {\n          status: \"ok\",\n          date_created: 1708091234,\n          date_updated: 1708091234,\n          active_incidents: [],\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => mockResponse,\n          } as Response),\n        );\n\n        const result = await fetcher.fetch(entry);\n\n        expect(result.severity).toBe(\"none\");\n        expect(result.updated_at).toBe(1708091234000);\n      });\n\n      it(\"should handle Slack incidents (non-outage)\", async () => {\n        const entry: StatusPageEntry = {\n          id: \"slack\",\n          name: \"Slack\",\n          url: \"https://slack.com\",\n          status_page_url: \"https://slack-status.com\",\n          provider: \"custom\",\n          industry: [\"communication\"],\n          api_config: {\n            type: \"custom\",\n            endpoint: \"https://slack-status.com/api/v2.0.0/current\",\n            parser: \"slack\",\n          },\n        };\n\n        const mockResponse = {\n          status: \"active\",\n          date_created: 1708091234,\n          date_updated: 1708091234,\n          active_incidents: [\n            {\n              id: 123,\n              title: \"Login Issues\",\n              type: \"incident\",\n              status: \"investigating\",\n              services: [\"Login\"],\n            },\n          ],\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => mockResponse,\n          } as Response),\n        );\n\n        const result = await fetcher.fetch(entry);\n\n        expect(result.severity).toBe(\"minor\");\n        expect(result.description).toBe(\"Login Issues\");\n      });\n\n      it(\"should handle Slack outages\", async () => {\n        const entry: StatusPageEntry = {\n          id: \"slack\",\n          name: \"Slack\",\n          url: \"https://slack.com\",\n          status_page_url: \"https://slack-status.com\",\n          provider: \"custom\",\n          industry: [\"communication\"],\n          api_config: {\n            type: \"custom\",\n            endpoint: \"https://slack-status.com/api/v2.0.0/current\",\n            parser: \"slack\",\n          },\n        };\n\n        const mockResponse = {\n          status: \"active\",\n          date_created: 1708091234,\n          date_updated: 1708091234,\n          active_incidents: [\n            {\n              id: 123,\n              title: \"Service Unavailable\",\n              type: \"outage\",\n              status: \"investigating\",\n              services: [\"Messaging\", \"Files\"],\n            },\n          ],\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => mockResponse,\n          } as Response),\n        );\n\n        const result = await fetcher.fetch(entry);\n\n        expect(result.severity).toBe(\"major\");\n        expect(result.description).toBe(\"Service Unavailable\");\n      });\n    });\n\n    describe(\"Generic parser\", () => {\n      it(\"should parse generic status with 'operational' keyword\", async () => {\n        const entry: StatusPageEntry = {\n          id: \"test\",\n          name: \"Test\",\n          url: \"https://test.com\",\n          status_page_url: \"https://status.test.com\",\n          provider: \"custom\",\n          industry: [\"saas\"],\n          api_config: {\n            type: \"custom\",\n            endpoint: \"https://api.test.com/status\",\n          },\n        };\n\n        const mockResponse = {\n          status: \"operational\",\n          message: \"All systems running smoothly\",\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => mockResponse,\n          } as Response),\n        );\n\n        const result = await fetcher.fetch(entry);\n\n        expect(result.severity).toBe(\"none\");\n        expect(result.description).toBe(\"All systems running smoothly\");\n      });\n\n      it(\"should infer major status from 'down' keyword\", async () => {\n        const entry: StatusPageEntry = {\n          id: \"test\",\n          name: \"Test\",\n          url: \"https://test.com\",\n          status_page_url: \"https://status.test.com\",\n          provider: \"custom\",\n          industry: [\"saas\"],\n          api_config: {\n            type: \"custom\",\n            endpoint: \"https://api.test.com/status\",\n          },\n        };\n\n        const mockResponse = {\n          status: \"down\",\n          message: \"System is down\",\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => mockResponse,\n          } as Response),\n        );\n\n        const result = await fetcher.fetch(entry);\n\n        expect(result.severity).toBe(\"major\");\n        expect(result.description).toBe(\"System is down\");\n      });\n\n      it(\"should infer minor status from 'degraded' keyword\", async () => {\n        const entry: StatusPageEntry = {\n          id: \"test\",\n          name: \"Test\",\n          url: \"https://test.com\",\n          status_page_url: \"https://status.test.com\",\n          provider: \"custom\",\n          industry: [\"saas\"],\n          api_config: {\n            type: \"custom\",\n            endpoint: \"https://api.test.com/status\",\n          },\n        };\n\n        const mockResponse = {\n          state: \"degraded\",\n          description: \"Performance issues\",\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => mockResponse,\n          } as Response),\n        );\n\n        const result = await fetcher.fetch(entry);\n\n        expect(result.severity).toBe(\"minor\");\n        expect(result.description).toBe(\"Performance issues\");\n      });\n\n      it(\"should handle health field\", async () => {\n        const entry: StatusPageEntry = {\n          id: \"test\",\n          name: \"Test\",\n          url: \"https://test.com\",\n          status_page_url: \"https://status.test.com\",\n          provider: \"custom\",\n          industry: [\"saas\"],\n          api_config: {\n            type: \"custom\",\n            endpoint: \"https://api.test.com/status\",\n          },\n        };\n\n        const mockResponse = {\n          health: \"healthy\",\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => mockResponse,\n          } as Response),\n        );\n\n        const result = await fetcher.fetch(entry);\n\n        expect(result.severity).toBe(\"none\");\n        expect(result.description).toBe(\"healthy\");\n      });\n    });\n\n    describe(\"AWS parser\", () => {\n      it(\"should throw error for unimplemented AWS parser\", async () => {\n        const entry: StatusPageEntry = {\n          id: \"aws\",\n          name: \"AWS\",\n          url: \"https://aws.amazon.com\",\n          status_page_url: \"https://status.aws.amazon.com\",\n          provider: \"custom\",\n          industry: [\"cloud-providers\"],\n          api_config: {\n            type: \"custom\",\n            endpoint: \"https://status.aws.amazon.com/data.json\",\n            parser: \"aws\",\n          },\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => ({}),\n          } as Response),\n        );\n\n        await expect(fetcher.fetch(entry)).rejects.toThrow(\n          \"AWS parser not implemented - uses RSS feeds\",\n        );\n      });\n    });\n\n    it(\"should throw error on non-200 response\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"custom\",\n        industry: [\"saas\"],\n        api_config: {\n          type: \"custom\",\n          endpoint: \"https://api.test.com/status\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: false,\n          status: 401,\n          statusText: \"Unauthorized\",\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\n        \"HTTP 401: Unauthorized\",\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/fetchers/edge-cases.test.ts",
    "content": "import { describe, expect, it, mock } from \"bun:test\";\nimport { AtlassianFetcher } from \"../../src/fetchers/atlassian\";\nimport { BetterStackFetcher } from \"../../src/fetchers/betterstack\";\nimport { CustomApiFetcher } from \"../../src/fetchers/custom\";\nimport { HtmlScraperFetcher } from \"../../src/fetchers/html\";\nimport { InstatusFetcher } from \"../../src/fetchers/instatus\";\nimport type { StatusPageEntry } from \"../../src/types\";\n\ndescribe(\"Fetcher Edge Cases\", () => {\n  describe(\"Network Errors\", () => {\n    it(\"should handle fetch network errors\", async () => {\n      const fetcher = new AtlassianFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() => Promise.reject(new Error(\"Network error\")));\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\"Network error\");\n    });\n\n    it(\"should handle timeout errors\", async () => {\n      const fetcher = new InstatusFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() => Promise.reject(new Error(\"Request timeout\")));\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\"Request timeout\");\n    });\n  });\n\n  describe(\"Malformed JSON Responses\", () => {\n    it(\"should handle invalid JSON in Atlassian response\", async () => {\n      const fetcher = new AtlassianFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => {\n            throw new SyntaxError(\"Invalid JSON\");\n          },\n        } as unknown as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\"Invalid JSON\");\n    });\n\n    it(\"should handle missing required fields in BetterStack response\", async () => {\n      const fetcher = new BetterStackFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"better-uptime\",\n        industry: [\"saas\"],\n      };\n\n      // Missing data.attributes\n      const malformedResponse = {\n        data: {\n          id: \"123\",\n          type: \"status_page\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => malformedResponse,\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow();\n    });\n\n    it(\"should handle empty response\", async () => {\n      const fetcher = new InstatusFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => ({}),\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow();\n    });\n  });\n\n  describe(\"Invalid Status Values\", () => {\n    it(\"should handle unknown indicator values in Atlassian\", async () => {\n      const fetcher = new AtlassianFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        page: {\n          id: \"123\",\n          name: \"Test\",\n          url: \"https://status.test.com\",\n          timezone: \"UTC\",\n          updated_at: \"2024-02-16T12:00:00.000Z\",\n        },\n        status: {\n          indicator: \"unknown\", // Invalid value\n          description: \"Unknown Status\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow();\n    });\n\n    it(\"should handle unknown status type in Instatus\", async () => {\n      const fetcher = new InstatusFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        activeIncidents: [],\n        activeMaintenances: [],\n        status: {\n          text: \"Unknown\",\n          type: \"UNKNOWN\", // Invalid type\n        },\n        page: {\n          name: \"Test\",\n          url: \"https://test.instatus.com\",\n          updated: \"2024-02-16T12:00:00.000Z\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow();\n    });\n  });\n\n  describe(\"HTML Parser Edge Cases\", () => {\n    it(\"should handle malformed HTML\", async () => {\n      const fetcher = new HtmlScraperFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const malformedHtml = \"<html><body><div class='status'>Unclosed div\";\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => malformedHtml,\n        } as Response),\n      );\n\n      // Should not throw, but return Unknown\n      const result = await fetcher.fetch(entry);\n      expect(result.description).toBe(\"Unknown\");\n      expect(result.severity).toBe(\"none\");\n      expect(result.status).toBe(\"operational\");\n    });\n\n    it(\"should handle empty HTML\", async () => {\n      const fetcher = new HtmlScraperFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => \"\",\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n      expect(result.description).toBe(\"Unknown\");\n      expect(result.severity).toBe(\"none\");\n    });\n\n    it(\"should handle HTML with no status indicators\", async () => {\n      const fetcher = new HtmlScraperFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const htmlWithNoStatus = `\n        <html>\n          <body>\n            <h1>Welcome</h1>\n            <p>This is a page</p>\n          </body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => htmlWithNoStatus,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n      expect(result.description).toBe(\"Unknown\");\n    });\n  });\n\n  describe(\"Custom API Edge Cases\", () => {\n    it(\"should handle missing endpoint configuration\", async () => {\n      const fetcher = new CustomApiFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"custom\",\n        industry: [\"saas\"],\n        api_config: { type: \"custom\" }, // Missing endpoint\n      };\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\n        \"Custom API requires explicit endpoint configuration\",\n      );\n    });\n\n    it(\"should handle AWS parser (not implemented)\", async () => {\n      const fetcher = new CustomApiFetcher();\n      const entry: StatusPageEntry = {\n        id: \"aws\",\n        name: \"AWS\",\n        url: \"https://aws.amazon.com\",\n        status_page_url: \"https://status.aws.amazon.com\",\n        provider: \"custom\",\n        industry: [\"cloud-providers\"],\n        api_config: {\n          type: \"custom\",\n          endpoint: \"https://status.aws.amazon.com/data.json\",\n          parser: \"aws\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => ({}),\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\n        \"AWS parser not implemented - uses RSS feeds\",\n      );\n    });\n\n    it(\"should handle generic parser with minimal data\", async () => {\n      const fetcher = new CustomApiFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"custom\",\n        industry: [\"saas\"],\n        api_config: {\n          type: \"custom\",\n          endpoint: \"https://api.test.com/status\",\n        },\n      };\n\n      const minimalResponse = { status: \"ok\" };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => minimalResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n      expect(result.severity).toBe(\"none\");\n      expect(result.status).toBe(\"operational\");\n    });\n  });\n\n  describe(\"HTTP Error Codes\", () => {\n    const testCases = [\n      { code: 400, text: \"Bad Request\" },\n      { code: 401, text: \"Unauthorized\" },\n      { code: 403, text: \"Forbidden\" },\n      { code: 404, text: \"Not Found\" },\n      { code: 429, text: \"Too Many Requests\" },\n      { code: 500, text: \"Internal Server Error\" },\n      { code: 502, text: \"Bad Gateway\" },\n      { code: 503, text: \"Service Unavailable\" },\n    ];\n\n    testCases.forEach(({ code, text }) => {\n      it(`should handle ${code} ${text}`, async () => {\n        const fetcher = new AtlassianFetcher();\n        const entry: StatusPageEntry = {\n          id: \"test\",\n          name: \"Test\",\n          url: \"https://test.com\",\n          status_page_url: \"https://status.test.com\",\n          provider: \"atlassian-statuspage\",\n          industry: [\"saas\"],\n        };\n\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: false,\n            status: code,\n            statusText: text,\n          } as Response),\n        );\n\n        await expect(fetcher.fetch(entry)).rejects.toThrow(`${code}`);\n      });\n    });\n  });\n\n  describe(\"Date/Timestamp Edge Cases\", () => {\n    it(\"should handle invalid date strings\", async () => {\n      const fetcher = new AtlassianFetcher();\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        page: {\n          id: \"123\",\n          name: \"Test\",\n          url: \"https://status.test.com\",\n          timezone: \"UTC\",\n          updated_at: \"not-a-date\", // Invalid date\n        },\n        status: {\n          indicator: \"none\",\n          description: \"All Systems Operational\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow();\n    });\n\n    it(\"should handle Slack API with string timestamp\", async () => {\n      const fetcher = new CustomApiFetcher();\n      const entry: StatusPageEntry = {\n        id: \"slack\",\n        name: \"Slack\",\n        url: \"https://slack.com\",\n        status_page_url: \"https://slack-status.com\",\n        provider: \"custom\",\n        industry: [\"communication\"],\n        api_config: {\n          type: \"custom\",\n          endpoint: \"https://slack-status.com/api/v2.0.0/current\",\n          parser: \"slack\",\n        },\n      };\n\n      const mockResponse = {\n        status: \"ok\",\n        date_created: \"2024-02-16T12:00:00.000Z\",\n        date_updated: \"2024-02-16T13:00:00.000Z\",\n        active_incidents: [],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      // Should handle string timestamps by parsing them\n      const result = await fetcher.fetch(entry);\n      expect(result.severity).toBe(\"none\");\n      expect(result.status).toBe(\"operational\");\n      expect(typeof result.updated_at).toBe(\"number\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/fetchers/html.test.ts",
    "content": "import { beforeEach, describe, expect, it, mock } from \"bun:test\";\nimport { HtmlScraperFetcher } from \"../../src/fetchers/html\";\nimport type { StatusPageEntry } from \"../../src/types\";\n\ndescribe(\"HtmlScraperFetcher\", () => {\n  let fetcher: HtmlScraperFetcher;\n\n  beforeEach(() => {\n    fetcher = new HtmlScraperFetcher();\n  });\n\n  describe(\"canHandle\", () => {\n    it(\"should only handle entries with api_config.type = html-scraper\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should not handle entries without html-scraper api_config\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(false);\n    });\n\n    it(\"should not handle other api_config types\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"atlassian\" },\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(false);\n    });\n  });\n\n  describe(\"fetch\", () => {\n    it(\"should scrape status from class attribute\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const mockHtml = `\n        <html>\n          <body>\n            <div class=\"status-operational\">All Systems Operational</div>\n          </body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => mockHtml,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"All Systems Operational\");\n      expect(result.timezone).toBe(\"UTC\");\n      expect(typeof result.updated_at).toBe(\"number\");\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"https://status.test.com\",\n        expect.objectContaining({\n          headers: expect.objectContaining({\n            \"User-Agent\": \"Mozilla/5.0 (compatible; OpenStatus-Bot/1.0)\",\n          }),\n        }),\n      );\n    });\n\n    it(\"should scrape status from data-status attribute\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const mockHtml = `\n        <html>\n          <body>\n            <div data-status=\"operational\">Services running</div>\n          </body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => mockHtml,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"operational\");\n    });\n\n    it(\"should scrape status from meta tag\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const mockHtml = `\n        <html>\n          <head>\n            <meta name=\"status\" content=\"All systems operational\" />\n          </head>\n          <body></body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => mockHtml,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"All systems operational\");\n    });\n\n    it(\"should infer minor status from 'degraded' keyword\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const mockHtml = `\n        <html>\n          <body>\n            <div class=\"status-indicator\">Service Degraded</div>\n          </body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => mockHtml,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"minor\");\n      expect(result.description).toBe(\"Service Degraded\");\n    });\n\n    it(\"should infer minor status from 'partial' keyword\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const mockHtml = `\n        <html>\n          <body>\n            <div class=\"status-message\">Partial Service Outage</div>\n          </body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => mockHtml,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"minor\");\n      expect(result.description).toBe(\"Partial Service Outage\");\n    });\n\n    it(\"should infer major status from 'outage' keyword\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const mockHtml = `\n        <html>\n          <body>\n            <div class=\"status-box\">Major Outage in Progress</div>\n          </body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => mockHtml,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"major\");\n      expect(result.description).toBe(\"Major Outage in Progress\");\n    });\n\n    it(\"should infer major status from 'down' keyword\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const mockHtml = `\n        <html>\n          <body>\n            <div class=\"status-text\">System Down</div>\n          </body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => mockHtml,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"major\");\n      expect(result.description).toBe(\"System Down\");\n    });\n\n    it(\"should return Unknown if no status pattern found\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const mockHtml = `\n        <html>\n          <body>\n            <div>No status information</div>\n          </body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => mockHtml,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"Unknown\");\n    });\n\n    it(\"should throw error on non-200 response\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: false,\n          status: 404,\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\"HTTP 404:\");\n    });\n\n    it(\"should trim whitespace from description\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      const mockHtml = `\n        <html>\n          <body>\n            <div class=\"status\">\n\n              All Systems Operational\n\n            </div>\n          </body>\n        </html>\n      `;\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          text: async () => mockHtml,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.description).toBe(\"All Systems Operational\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/fetchers/incidentio.test.ts",
    "content": "import { beforeEach, describe, expect, it, mock } from \"bun:test\";\nimport { IncidentioFetcher } from \"../../src/fetchers/incidentio\";\nimport type { StatusPageEntry } from \"../../src/types\";\n\ndescribe(\"IncidentioFetcher\", () => {\n  let fetcher: IncidentioFetcher;\n\n  beforeEach(() => {\n    fetcher = new IncidentioFetcher();\n  });\n\n  describe(\"canHandle\", () => {\n    it(\"should identify entries with api_config.type = incidentio\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"incidentio\" },\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should identify entries with provider = incidentio\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"incidentio\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should identify entries with incident.io in URL\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.incident.io\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should not handle other providers\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(false);\n    });\n  });\n\n  describe(\"fetch\", () => {\n    it(\"should fetch and parse operational status (no incidents)\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test Service\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"incidentio\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        ongoing_incidents: [],\n        in_progress_maintenances: [],\n        scheduled_maintenances: [],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"All Systems Operational\");\n      expect(result.timezone).toBe(\"UTC\");\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"https://status.test.com/api/widget\",\n        expect.objectContaining({\n          headers: expect.objectContaining({\n            \"User-Agent\": \"OpenStatus-Directory/1.0\",\n            Accept: \"application/json\",\n          }),\n        }),\n      );\n    });\n\n    it(\"should handle ongoing incidents with investigating status\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"incidentio\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        ongoing_incidents: [\n          {\n            id: \"123\",\n            name: \"API Errors\",\n            status: \"investigating\",\n            last_update: {\n              message: \"We are investigating\",\n              updated_at: \"2024-02-16T12:00:00.000Z\",\n            },\n          },\n        ],\n        in_progress_maintenances: [],\n        scheduled_maintenances: [],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"major\");\n      expect(result.description).toBe(\"Incident: API Errors\");\n    });\n\n    it(\"should handle ongoing incidents with monitoring status\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"incidentio\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        ongoing_incidents: [\n          {\n            id: \"123\",\n            name: \"Database Slowness\",\n            status: \"monitoring\",\n            last_update: {\n              message: \"Monitoring the fix\",\n              updated_at: \"2024-02-16T12:00:00.000Z\",\n            },\n          },\n        ],\n        in_progress_maintenances: [],\n        scheduled_maintenances: [],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"minor\");\n      expect(result.description).toBe(\"Monitoring: Database Slowness\");\n    });\n\n    it(\"should handle in-progress maintenance\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"incidentio\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        ongoing_incidents: [],\n        in_progress_maintenances: [\n          {\n            id: \"456\",\n            name: \"Database Upgrade\",\n            status: \"in_progress\",\n            last_update: {\n              message: \"Maintenance in progress\",\n              updated_at: \"2024-02-16T12:00:00.000Z\",\n            },\n          },\n        ],\n        scheduled_maintenances: [],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"Maintenance: Database Upgrade\");\n    });\n\n    it(\"should handle scheduled maintenance\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"incidentio\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        ongoing_incidents: [],\n        in_progress_maintenances: [],\n        scheduled_maintenances: [\n          {\n            id: \"789\",\n            name: \"Server Maintenance\",\n            status: \"scheduled\",\n            last_update: {\n              message: \"Scheduled for tomorrow\",\n              updated_at: \"2024-02-16T12:00:00.000Z\",\n            },\n          },\n        ],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\n        \"All Systems Operational (Scheduled: Server Maintenance)\",\n      );\n    });\n\n    it(\"should prioritize ongoing incidents over maintenance\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"incidentio\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        ongoing_incidents: [\n          {\n            id: \"123\",\n            name: \"Critical Issue\",\n            status: \"investigating\",\n            last_update: {\n              message: \"Investigating\",\n              updated_at: \"2024-02-16T12:00:00.000Z\",\n            },\n          },\n        ],\n        in_progress_maintenances: [\n          {\n            id: \"456\",\n            name: \"Maintenance\",\n            status: \"in_progress\",\n            last_update: {\n              message: \"In progress\",\n              updated_at: \"2024-02-16T12:00:00.000Z\",\n            },\n          },\n        ],\n        scheduled_maintenances: [],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"major\");\n      expect(result.description).toBe(\"Incident: Critical Issue\");\n    });\n\n    it(\"should use custom endpoint if provided\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"incidentio\",\n        industry: [\"saas\"],\n        api_config: {\n          type: \"incidentio\",\n          endpoint: \"https://custom.endpoint.com/widget\",\n        },\n      };\n\n      const mockResponse = {\n        ongoing_incidents: [],\n        in_progress_maintenances: [],\n        scheduled_maintenances: [],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      await fetcher.fetch(entry);\n\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"https://custom.endpoint.com/widget\",\n        expect.any(Object),\n      );\n    });\n\n    it(\"should throw error on non-200 response\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"incidentio\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: false,\n          status: 503,\n          statusText: \"Service Unavailable\",\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\n        \"HTTP 503: Service Unavailable\",\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/fetchers/instatus.test.ts",
    "content": "import { beforeEach, describe, expect, it, mock } from \"bun:test\";\nimport { InstatusFetcher } from \"../../src/fetchers/instatus\";\nimport type { StatusPageEntry } from \"../../src/types\";\n\ndescribe(\"InstatusFetcher\", () => {\n  let fetcher: InstatusFetcher;\n\n  beforeEach(() => {\n    fetcher = new InstatusFetcher();\n  });\n\n  describe(\"canHandle\", () => {\n    it(\"should identify entries with api_config.type = instatus\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"instatus\" },\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should identify entries with provider = instatus\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should identify entries with instatus.com in URL\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(true);\n    });\n\n    it(\"should not handle other providers\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      expect(fetcher.canHandle(entry)).toBe(false);\n    });\n  });\n\n  describe(\"fetch\", () => {\n    it(\"should fetch and parse UP status\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test Service\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        activeIncidents: [],\n        activeMaintenances: [],\n        status: {\n          text: \"All Systems Operational\",\n          type: \"UP\",\n        },\n        page: {\n          name: \"Test Service\",\n          url: \"https://test.instatus.com\",\n          updated: \"2024-02-16T12:00:00.000Z\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"All Systems Operational\");\n      expect(result.timezone).toBe(\"UTC\");\n      expect(typeof result.updated_at).toBe(\"number\");\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"https://test.instatus.com/summary.json\",\n        expect.objectContaining({\n          headers: expect.objectContaining({\n            \"User-Agent\": \"OpenStatus-Directory/1.0\",\n          }),\n        }),\n      );\n    });\n\n    it(\"should map HASISSUES to major indicator\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        activeIncidents: [{ id: 1, name: \"API Errors\" }],\n        activeMaintenances: [],\n        status: {\n          text: \"Service Degraded\",\n          type: \"HASISSUES\",\n        },\n        page: {\n          name: \"Test\",\n          url: \"https://test.instatus.com\",\n          updated: \"2024-02-16T12:00:00.000Z\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"major\");\n      expect(result.description).toBe(\"Service Degraded\");\n    });\n\n    it(\"should map UNDERMAINTENANCE to minor indicator\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      const mockResponse = {\n        activeIncidents: [],\n        activeMaintenances: [{ id: 1, name: \"Scheduled Maintenance\" }],\n        status: {\n          text: \"Under Maintenance\",\n          type: \"UNDERMAINTENANCE\",\n        },\n        page: {\n          name: \"Test\",\n          url: \"https://test.instatus.com\",\n          updated: \"2024-02-16T12:00:00.000Z\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      const result = await fetcher.fetch(entry);\n\n      expect(result.severity).toBe(\"none\");\n      expect(result.description).toBe(\"Under Maintenance\");\n    });\n\n    it(\"should use custom endpoint if provided\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n        api_config: {\n          type: \"instatus\",\n          endpoint: \"https://custom.endpoint.com/status.json\",\n        },\n      };\n\n      const mockResponse = {\n        activeIncidents: [],\n        activeMaintenances: [],\n        status: {\n          text: \"Operational\",\n          type: \"UP\",\n        },\n        page: {\n          name: \"Test\",\n          url: \"https://test.instatus.com\",\n          updated: \"2024-02-16T12:00:00.000Z\",\n        },\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => mockResponse,\n        } as Response),\n      );\n\n      await fetcher.fetch(entry);\n\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"https://custom.endpoint.com/status.json\",\n        expect.any(Object),\n      );\n    });\n\n    it(\"should throw error on non-200 response\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: false,\n          status: 500,\n          statusText: \"Internal Server Error\",\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow(\n        \"HTTP 500: Internal Server Error\",\n      );\n    });\n\n    it(\"should throw error on invalid JSON schema\", async () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"instatus\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => ({ invalid: \"data\" }),\n        } as Response),\n      );\n\n      await expect(fetcher.fetch(entry)).rejects.toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/integration.test.ts",
    "content": "import { describe, expect, it, mock } from \"bun:test\";\nimport { fetchers as allFetchers } from \"../src/fetchers\";\nimport type { StatusPageEntry, StatusPageProvider } from \"../src/types\";\n\ndescribe(\"Integration Tests\", () => {\n  describe(\"Fetcher Selection\", () => {\n    it(\"should select AtlassianFetcher for statuspage.io URLs\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.statuspage.io\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      // Use allFetchers imported above\n      const selectedFetcher = allFetchers.find((f) => f.canHandle(entry));\n\n      expect(selectedFetcher?.name).toBe(\"atlassian\");\n    });\n\n    it(\"should select InstatusFetcher for instatus.com URLs\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.instatus.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      // Use allFetchers imported above\n      const selectedFetcher = allFetchers.find((f) => f.canHandle(entry));\n\n      expect(selectedFetcher?.name).toBe(\"instatus\");\n    });\n\n    it(\"should select BetterStackFetcher for betteruptime.com URLs\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.betteruptime.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      // Use allFetchers imported above\n      const selectedFetcher = allFetchers.find((f) => f.canHandle(entry));\n\n      expect(selectedFetcher?.name).toBe(\"betterstack\");\n    });\n\n    it(\"should select IncidentioFetcher for incident.io URLs\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.incident.io\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      // Use allFetchers imported above\n      const selectedFetcher = allFetchers.find((f) => f.canHandle(entry));\n\n      expect(selectedFetcher?.name).toBe(\"incidentio\");\n    });\n\n    it(\"should select CustomApiFetcher for custom api_config\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"custom\",\n        industry: [\"saas\"],\n        api_config: {\n          type: \"custom\",\n          endpoint: \"https://api.test.com/status\",\n        },\n      };\n\n      // Use allFetchers imported above\n      const selectedFetcher = allFetchers.find((f) => f.canHandle(entry));\n\n      expect(selectedFetcher?.name).toBe(\"custom\");\n    });\n\n    it(\"should select HtmlScraperFetcher for html-scraper config\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" },\n      };\n\n      // Use allFetchers imported above\n      const selectedFetcher = allFetchers.find((f) => f.canHandle(entry));\n\n      expect(selectedFetcher?.name).toBe(\"html-scraper\");\n    });\n\n    it(\"should match fetcher based on api_config.type when URL is ambiguous\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://customstatus.com\", // Generic URL\n        provider: \"unknown\",\n        industry: [\"saas\"],\n        api_config: { type: \"html-scraper\" }, // Explicit HTML scraper config\n      };\n\n      // Use allFetchers imported above\n      const selectedFetcher = allFetchers.find((f) => f.canHandle(entry));\n\n      expect(selectedFetcher?.name).toBe(\"html-scraper\");\n    });\n\n    it(\"should prioritize provider field over URL patterns\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://customdomain.com\",\n        provider: \"instatus\", // Explicitly set provider\n        industry: [\"saas\"],\n      };\n\n      // Use allFetchers imported above\n      const selectedFetcher = allFetchers.find((f) => f.canHandle(entry));\n\n      expect(selectedFetcher?.name).toBe(\"instatus\");\n    });\n  });\n\n  describe(\"Multiple Fetchers Match\", () => {\n    it(\"should return first matching fetcher when multiple can handle\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.statuspage.io\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n        api_config: { type: \"atlassian\" },\n      };\n\n      // Use allFetchers imported above\n      const matchingFetchers = allFetchers.filter((f) => f.canHandle(entry));\n\n      // Atlassian fetcher should match via provider, api_config, and URL\n      expect(matchingFetchers.length).toBeGreaterThanOrEqual(1);\n      expect(matchingFetchers[0].name).toBe(\"atlassian\");\n    });\n  });\n\n  describe(\"No Fetcher Matches\", () => {\n    it(\"should return undefined when no fetcher can handle entry\", () => {\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://unknown-provider.com/status\",\n        provider: \"unknown\",\n        industry: [\"saas\"],\n      };\n\n      // Use allFetchers imported above\n      const selectedFetcher = allFetchers.find((f) => f.canHandle(entry));\n\n      // Should find no matching fetcher (html-scraper would match if it has no restrictions)\n      // Actually, html-scraper only matches if api_config.type === \"html-scraper\"\n      expect(selectedFetcher).toBeUndefined();\n    });\n  });\n\n  describe(\"StatusResult Consistency\", () => {\n    it(\"should return consistent StatusResult structure across fetchers\", async () => {\n      const atlassianEntry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.statuspage.io\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n      };\n\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => ({\n            page: {\n              id: \"123\",\n              name: \"Test\",\n              url: \"https://test.statuspage.io\",\n              timezone: \"UTC\",\n              updated_at: \"2024-02-16T12:00:00.000Z\",\n            },\n            status: {\n              indicator: \"none\",\n              description: \"All Systems Operational\",\n            },\n          }),\n        } as Response),\n      );\n\n      // Use allFetchers imported above\n      const atlassianFetcher = allFetchers.find((f) => f.name === \"atlassian\");\n      const result = await atlassianFetcher?.fetch(atlassianEntry);\n\n      // Verify all required fields exist\n      expect(result).toHaveProperty(\"severity\");\n      expect(result).toHaveProperty(\"status\");\n      expect(result).toHaveProperty(\"description\");\n      expect(result).toHaveProperty(\"updated_at\");\n\n      if (!result) {\n        throw new Error(\"Result is undefined\");\n      }\n\n      // Verify types\n      expect(typeof result.severity).toBe(\"string\");\n      expect(typeof result.status).toBe(\"string\");\n      expect(typeof result.description).toBe(\"string\");\n      expect(typeof result.updated_at).toBe(\"number\");\n    });\n  });\n\n  describe(\"Severity and Status Mapping\", () => {\n    it(\"should map all operational states to severity 'none'\", async () => {\n      const testCases = [\n        {\n          fetcher: \"instatus\",\n          mockResponse: {\n            activeIncidents: [],\n            activeMaintenances: [],\n            status: { text: \"All Good\", type: \"UP\" },\n            page: {\n              name: \"Test\",\n              url: \"https://test.instatus.com\",\n              updated: \"2024-02-16T12:00:00.000Z\",\n            },\n          },\n        },\n      ];\n\n      for (const testCase of testCases) {\n        global.fetch = mock(() =>\n          Promise.resolve({\n            ok: true,\n            json: async () => testCase.mockResponse,\n          } as Response),\n        );\n\n        // Use allFetchers imported above\n        const fetcher = allFetchers.find((f) => f.name === testCase.fetcher);\n\n        const entry: StatusPageEntry = {\n          id: \"test\",\n          name: \"Test\",\n          url: \"https://test.com\",\n          status_page_url: `https://test.${testCase.fetcher}.com`,\n          provider: testCase.fetcher as StatusPageProvider,\n          industry: [\"saas\"],\n        };\n\n        const result = await fetcher?.fetch(entry);\n\n        if (!result) {\n          throw new Error(\"Result is undefined\");\n        }\n\n        expect(result.severity).toBe(\"none\");\n        expect(result.status).toBe(\"operational\");\n      }\n    });\n\n    it(\"should map degraded states to severity 'minor'\", async () => {\n      global.fetch = mock(() =>\n        Promise.resolve({\n          ok: true,\n          json: async () => ({\n            data: {\n              id: \"123\",\n              type: \"status_page\",\n              attributes: {\n                company_name: \"Test\",\n                timezone: \"UTC\",\n                aggregate_state: \"degraded\",\n                updated_at: \"2024-02-16T12:00:00.000Z\",\n              },\n            },\n          }),\n        } as Response),\n      );\n\n      // Use allFetchers imported above\n      const fetcher = allFetchers.find((f) => f.name === \"betterstack\");\n\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://test.betteruptime.com\",\n        provider: \"better-uptime\",\n        industry: [\"saas\"],\n      };\n\n      const result = await fetcher?.fetch(entry);\n\n      if (!result) {\n        throw new Error(\"Result is undefined\");\n      }\n\n      expect(result.severity).toBe(\"minor\");\n      expect(result.status).toBe(\"degraded\");\n    });\n  });\n\n  describe(\"Custom Endpoint Override\", () => {\n    it(\"should use custom endpoint when provided in api_config\", async () => {\n      const customEndpoint = \"https://custom-api.test.com/v2/status\";\n\n      const entry: StatusPageEntry = {\n        id: \"test\",\n        name: \"Test\",\n        url: \"https://test.com\",\n        status_page_url: \"https://status.test.com\",\n        provider: \"atlassian-statuspage\",\n        industry: [\"saas\"],\n        api_config: {\n          type: \"atlassian\",\n          endpoint: customEndpoint,\n        },\n      };\n\n      let fetchedUrl = \"\";\n      global.fetch = mock((url) => {\n        fetchedUrl = url;\n        return Promise.resolve({\n          ok: true,\n          json: async () => ({\n            page: {\n              id: \"123\",\n              name: \"Test\",\n              url: \"https://status.test.com\",\n              timezone: \"UTC\",\n              updated_at: \"2024-02-16T12:00:00.000Z\",\n            },\n            status: {\n              indicator: \"none\",\n              description: \"All Systems Operational\",\n            },\n          }),\n        } as Response);\n      });\n\n      // Use allFetchers imported above\n      const fetcher = allFetchers.find((f) => f.name === \"atlassian\");\n      await fetcher?.fetch(entry);\n\n      expect(fetchedUrl).toBe(customEndpoint);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/__tests__/utils.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\";\nimport { inferStatus, urlHostnameEndsWith } from \"../src/utils\";\n\ndescribe(\"urlHostnameEndsWith\", () => {\n  it(\"should match exact domain\", () => {\n    expect(urlHostnameEndsWith(\"https://statuspage.io\", \"statuspage.io\")).toBe(\n      true,\n    );\n  });\n\n  it(\"should match subdomains\", () => {\n    expect(\n      urlHostnameEndsWith(\n        \"https://acme.statuspage.io/api/v2/summary.json\",\n        \"statuspage.io\",\n      ),\n    ).toBe(true);\n  });\n\n  it(\"should reject domain in path (spoof attempt)\", () => {\n    expect(\n      urlHostnameEndsWith(\"https://evil.com/statuspage.io\", \"statuspage.io\"),\n    ).toBe(false);\n  });\n\n  it(\"should reject domain as subdomain prefix of another domain (spoof attempt)\", () => {\n    expect(\n      urlHostnameEndsWith(\"https://statuspage.io.evil.com\", \"statuspage.io\"),\n    ).toBe(false);\n  });\n\n  it(\"should reject partial domain match\", () => {\n    expect(\n      urlHostnameEndsWith(\"https://notstatuspage.io\", \"statuspage.io\"),\n    ).toBe(false);\n  });\n\n  it(\"should return false for invalid URLs\", () => {\n    expect(urlHostnameEndsWith(\"not-a-url\", \"statuspage.io\")).toBe(false);\n  });\n});\n\ndescribe(\"inferStatus\", () => {\n  describe(\"Incident workflow states\", () => {\n    it(\"should detect 'investigating' status\", () => {\n      expect(inferStatus(\"Investigating database issues\", \"major\")).toBe(\n        \"investigating\",\n      );\n      expect(inferStatus(\"We are investigating the problem\", \"major\")).toBe(\n        \"investigating\",\n      );\n      expect(inferStatus(\"INVESTIGATING API ERRORS\", \"major\")).toBe(\n        \"investigating\",\n      );\n    });\n\n    it(\"should detect 'identified' status\", () => {\n      expect(inferStatus(\"Issue identified and working on fix\", \"major\")).toBe(\n        \"identified\",\n      );\n      expect(inferStatus(\"Root cause identified\", \"major\")).toBe(\"identified\");\n      expect(inferStatus(\"IDENTIFIED THE PROBLEM\", \"minor\")).toBe(\"identified\");\n    });\n\n    it(\"should detect 'monitoring' status\", () => {\n      expect(inferStatus(\"Monitoring the fix\", \"minor\")).toBe(\"monitoring\");\n      expect(inferStatus(\"We are monitoring the situation\", \"minor\")).toBe(\n        \"monitoring\",\n      );\n      expect(inferStatus(\"MONITORING DEPLOYMENT\", \"minor\")).toBe(\"monitoring\");\n    });\n\n    it(\"should detect 'resolved' status\", () => {\n      expect(inferStatus(\"Issue resolved\", \"none\")).toBe(\"resolved\");\n      expect(inferStatus(\"Problem has been resolved\", \"none\")).toBe(\"resolved\");\n      expect(inferStatus(\"RESOLVED\", \"none\")).toBe(\"resolved\");\n    });\n  });\n\n  describe(\"Maintenance states\", () => {\n    it(\"should detect maintenance status\", () => {\n      expect(inferStatus(\"Scheduled maintenance in progress\", \"minor\")).toBe(\n        \"under_maintenance\",\n      );\n      expect(inferStatus(\"Under maintenance\", \"none\")).toBe(\n        \"under_maintenance\",\n      );\n      expect(inferStatus(\"MAINTENANCE WINDOW\", \"minor\")).toBe(\n        \"under_maintenance\",\n      );\n      expect(inferStatus(\"System maintenance\", \"none\")).toBe(\n        \"under_maintenance\",\n      );\n    });\n  });\n\n  describe(\"Outage states\", () => {\n    it(\"should detect major outage\", () => {\n      expect(inferStatus(\"Major outage affecting all services\", \"major\")).toBe(\n        \"major_outage\",\n      );\n      expect(inferStatus(\"Complete outage\", \"critical\")).toBe(\"major_outage\");\n      expect(inferStatus(\"MAJOR OUTAGE IN PROGRESS\", \"major\")).toBe(\n        \"major_outage\",\n      );\n    });\n\n    it(\"should detect partial outage\", () => {\n      expect(inferStatus(\"Partial outage in US region\", \"major\")).toBe(\n        \"partial_outage\",\n      );\n      expect(inferStatus(\"Partial system outage\", \"minor\")).toBe(\n        \"partial_outage\",\n      );\n      expect(inferStatus(\"PARTIAL OUTAGE\", \"major\")).toBe(\"partial_outage\");\n    });\n\n    it(\"should detect 'down' as major outage\", () => {\n      expect(inferStatus(\"Service is down\", \"major\")).toBe(\"major_outage\");\n      expect(inferStatus(\"API down\", \"critical\")).toBe(\"major_outage\");\n      expect(inferStatus(\"SYSTEM DOWN\", \"major\")).toBe(\"major_outage\");\n    });\n  });\n\n  describe(\"Degraded states\", () => {\n    it(\"should detect degraded service\", () => {\n      expect(inferStatus(\"Degraded performance\", \"minor\")).toBe(\"degraded\");\n      expect(inferStatus(\"Service degraded\", \"minor\")).toBe(\"degraded\");\n      expect(inferStatus(\"DEGRADED\", \"minor\")).toBe(\"degraded\");\n    });\n\n    it(\"should detect performance issues as degraded\", () => {\n      expect(inferStatus(\"Performance issues detected\", \"minor\")).toBe(\n        \"degraded\",\n      );\n      expect(inferStatus(\"Slow performance\", \"minor\")).toBe(\"degraded\");\n      expect(inferStatus(\"PERFORMANCE DEGRADATION\", \"minor\")).toBe(\"degraded\");\n    });\n  });\n\n  describe(\"Operational states\", () => {\n    it(\"should return operational for severity none with no keywords\", () => {\n      expect(inferStatus(\"All Systems Operational\", \"none\")).toBe(\n        \"operational\",\n      );\n      expect(inferStatus(\"Everything is working\", \"none\")).toBe(\"operational\");\n      expect(inferStatus(\"No issues detected\", \"none\")).toBe(\"operational\");\n    });\n  });\n\n  describe(\"Fallback logic based on severity\", () => {\n    it(\"should fallback to operational for severity none\", () => {\n      expect(inferStatus(\"Some random text\", \"none\")).toBe(\"operational\");\n      expect(inferStatus(\"\", \"none\")).toBe(\"operational\");\n    });\n\n    it(\"should fallback to major_outage for severity major/critical\", () => {\n      expect(inferStatus(\"Some issue\", \"major\")).toBe(\"major_outage\");\n      expect(inferStatus(\"Problem detected\", \"critical\")).toBe(\"major_outage\");\n    });\n\n    it(\"should fallback to degraded for severity minor\", () => {\n      expect(inferStatus(\"Some minor issue\", \"minor\")).toBe(\"degraded\");\n      expect(inferStatus(\"Small problem\", \"minor\")).toBe(\"degraded\");\n    });\n  });\n\n  describe(\"Priority of keyword matching\", () => {\n    it(\"should prioritize 'investigating' over severity-based fallback\", () => {\n      expect(inferStatus(\"Investigating degraded performance\", \"minor\")).toBe(\n        \"investigating\",\n      );\n    });\n\n    it(\"should prioritize 'maintenance' over 'degraded' keyword\", () => {\n      expect(\n        inferStatus(\"Maintenance causing degraded performance\", \"minor\"),\n      ).toBe(\"under_maintenance\");\n    });\n\n    it(\"should prioritize 'major outage' over 'partial outage'\", () => {\n      expect(inferStatus(\"Major outage with partial recovery\", \"major\")).toBe(\n        \"major_outage\",\n      );\n    });\n  });\n\n  describe(\"Case insensitivity\", () => {\n    it(\"should handle mixed case input\", () => {\n      expect(inferStatus(\"InVeStIgAtInG\", \"major\")).toBe(\"investigating\");\n      expect(inferStatus(\"MaInTeNaNcE\", \"minor\")).toBe(\"under_maintenance\");\n      expect(inferStatus(\"DeGrAdEd\", \"minor\")).toBe(\"degraded\");\n    });\n  });\n\n  describe(\"Real-world examples\", () => {\n    it(\"should correctly infer from Atlassian-style descriptions\", () => {\n      expect(inferStatus(\"All Systems Operational\", \"none\")).toBe(\n        \"operational\",\n      );\n      expect(inferStatus(\"Partial System Outage\", \"major\")).toBe(\n        \"partial_outage\",\n      );\n      expect(inferStatus(\"Service Under Maintenance\", \"minor\")).toBe(\n        \"under_maintenance\",\n      );\n    });\n\n    it(\"should correctly infer from incident descriptions\", () => {\n      expect(inferStatus(\"Incident: API Errors\", \"major\")).toBe(\"major_outage\");\n      expect(\n        inferStatus(\"Investigating: Database Connection Issues\", \"major\"),\n      ).toBe(\"investigating\");\n      expect(inferStatus(\"Monitoring: Deployment Rollout\", \"minor\")).toBe(\n        \"monitoring\",\n      );\n    });\n\n    it(\"should correctly infer from maintenance descriptions\", () => {\n      expect(\n        inferStatus(\"Scheduled Maintenance: Database Upgrade\", \"none\"),\n      ).toBe(\"under_maintenance\");\n      expect(inferStatus(\"Maintenance: Server Updates\", \"minor\")).toBe(\n        \"under_maintenance\",\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/status-fetcher/package.json",
    "content": "{\n  \"name\": \"@openstatus/status-fetcher\",\n  \"version\": \"0.1.0\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./fetchers\": \"./src/fetchers/index.ts\",\n    \"./data\": \"./src/data/index.ts\",\n    \"./types\": \"./src/types.ts\"\n  },\n  \"scripts\": {\n    \"test\": \"bun test\",\n    \"test:watch\": \"bun test --watch\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"zod\": \"4.1.13\",\n    \"node-html-parser\": \"6.1.12\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"latest\"\n  }\n}\n"
  },
  {
    "path": "packages/status-fetcher/scripts/test-fetchers.ts",
    "content": "import { fetchers } from \"../src/fetchers\";\nimport { getStatusDirectory } from \"../src/index\";\nimport type { SeverityLevel } from \"../src/types\";\n\n/**\n * Test all fetchers against real status page APIs\n *\n * Run with: bun scripts/test-fetchers.ts\n */\nasync function testFetchers() {\n  const directory = getStatusDirectory();\n\n  console.log(`\\n🔍 Testing ${directory.length} entries...\\n`);\n\n  let successCount = 0;\n  let failureCount = 0;\n  let skippedCount = 0;\n\n  for (const entry of directory) {\n    if (!entry.api_config) {\n      console.log(`⏭️  ${entry.name}: No API config`);\n      skippedCount++;\n      continue;\n    }\n\n    const fetcher = fetchers.find((f) => f.canHandle(entry));\n\n    if (!fetcher) {\n      console.log(`❌ ${entry.name}: No fetcher found`);\n      failureCount++;\n      continue;\n    }\n\n    try {\n      const startTime = Date.now();\n      const status = await fetcher.fetch(entry);\n      const duration = Date.now() - startTime;\n\n      const statusEmoji = getStatusEmoji(status.severity);\n      const statusText = `${status.status} (${status.severity})`;\n\n      console.log(\n        `${statusEmoji} ${entry.name}: ${statusText} - ${status.description} (${duration}ms)`,\n      );\n      successCount++;\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      console.log(`❌ ${entry.name}: ${errorMessage}`);\n      failureCount++;\n    }\n  }\n\n  console.log(`\\n${\"=\".repeat(60)}`);\n  console.log(\"📊 Summary\");\n  console.log(\"=\".repeat(60));\n  console.log(`Total entries: ${directory.length}`);\n  console.log(`✅ Success: ${successCount}`);\n  console.log(`❌ Failed: ${failureCount}`);\n  console.log(`⏭️  Skipped: ${skippedCount}`);\n  console.log(\n    `Success rate: ${((successCount / (successCount + failureCount)) * 100).toFixed(1)}%`,\n  );\n  console.log(\"\\n✨ Testing complete!\\n\");\n\n  // Exit with error code if any failures\n  if (failureCount > 0) {\n    process.exit(1);\n  }\n}\n\n/**\n * Get emoji for severity level\n */\nfunction getStatusEmoji(severity: SeverityLevel): string {\n  switch (severity) {\n    case \"none\":\n      return \"✅\";\n    case \"minor\":\n      return \"⚠️\";\n    case \"major\":\n      return \"🔴\";\n    case \"critical\":\n      return \"💥\";\n    default:\n      return \"❓\";\n  }\n}\n\ntestFetchers().catch((error) => {\n  console.error(\"\\n❌ Fatal error:\", error);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/status-fetcher/src/data/directory.ts",
    "content": "import type { StatusPageEntry } from \"../types\";\nimport { statusPageEntrySchema } from \"../types\";\n\nconst rawDirectory: StatusPageEntry[] = [\n  {\n    id: \"github\",\n    name: \"GitHub\",\n    url: \"https://github.com\",\n    status_page_url: \"https://www.githubstatus.com\",\n    provider: \"atlassian-statuspage\",\n    industry: [\"development-tools\"],\n    description: \"The world's leading software development platform\",\n    api_config: {\n      type: \"atlassian\",\n    },\n  },\n  {\n    id: \"vercel\",\n    name: \"Vercel\",\n    url: \"https://vercel.com\",\n    status_page_url: \"https://www.vercel-status.com\",\n    provider: \"atlassian-statuspage\",\n    industry: [\"cloud-providers\", \"development-tools\"],\n    description: \"Platform for frontend developers\",\n    api_config: {\n      type: \"atlassian\",\n    },\n  },\n  {\n    id: \"slack\",\n    name: \"Slack\",\n    url: \"https://slack.com\",\n    status_page_url: \"https://slack-status.com\",\n    provider: \"custom\",\n    industry: [\"communication\"],\n    description: \"Team collaboration and messaging platform\",\n    api_config: {\n      type: \"custom\",\n      endpoint: \"https://slack-status.com/api/v2.0.0/current\",\n      parser: \"slack\",\n    },\n  },\n  {\n    id: \"linear\",\n    name: \"Linear\",\n    url: \"https://linear.app\",\n    status_page_url: \"https://status.linear.app\",\n    provider: \"incidentio\",\n    industry: [\"development-tools\", \"saas\"],\n    description: \"Issue tracking tool built for modern software teams\",\n    api_config: {\n      type: \"incidentio\",\n    },\n  },\n  {\n    id: \"openai\",\n    name: \"OpenAI\",\n    url: \"https://openai.com\",\n    status_page_url: \"https://status.openai.com\",\n    provider: \"atlassian-statuspage\",\n    industry: [\"ai-ml\"],\n    description: \"AI research and deployment company\",\n    api_config: {\n      type: \"atlassian\",\n    },\n  },\n  {\n    id: \"stripe\",\n    name: \"Stripe\",\n    url: \"https://stripe.com\",\n    status_page_url: \"https://status.stripe.com\",\n    provider: \"atlassian-statuspage\",\n    industry: [\"fintech\"],\n    description: \"Online payment processing platform\",\n    api_config: {\n      type: \"atlassian\",\n    },\n  },\n  {\n    id: \"cloudflare\",\n    name: \"Cloudflare\",\n    url: \"https://cloudflare.com\",\n    status_page_url: \"https://www.cloudflarestatus.com\",\n    provider: \"atlassian-statuspage\",\n    industry: [\"cdn\", \"security\"],\n    description: \"Web infrastructure and security company\",\n    api_config: {\n      type: \"atlassian\",\n    },\n  },\n  {\n    id: \"turso\",\n    name: \"Turso\",\n    url: \"https://turso.tech\",\n    status_page_url: \"https://status.turso.tech\",\n    provider: \"better-uptime\",\n    industry: [\"databases\"],\n    description: \"Turso is a database for the modern web\",\n    api_config: {\n      type: \"atlassian\",\n    },\n  },\n];\n\n/**\n * Validates all directory entries at module load time\n *\n * This function runs once when the module is imported and ensures all entries\n * conform to the StatusPageEntry schema. If any validation errors are found,\n * it throws immediately with detailed error information.\n *\n * **Validation checks:**\n * - ID is non-empty string\n * - Name is non-empty string\n * - URLs are valid (url, status_page_url, logo_url if present)\n * - Provider is from allowed list\n * - At least one industry category\n * - API config matches expected format\n *\n * @returns The validated directory array\n * @throws {Error} If any entry fails validation, with details about which fields failed\n *\n * @example\n * Error message format:\n * ```\n * Directory validation failed with 2 error(s):\n *   - Entry 0 (github): url: Invalid url\n *   - Entry 3 (stripe): industry: Array must contain at least 1 element(s)\n * ```\n */\nfunction validateDirectory(): StatusPageEntry[] {\n  const errors: string[] = [];\n\n  rawDirectory.forEach((entry, index) => {\n    const result = statusPageEntrySchema.safeParse(entry);\n    if (!result.success) {\n      const formattedErrors = result.error.issues\n        .map((issue) => `${issue.path.join(\".\")}: ${issue.message}`)\n        .join(\", \");\n      errors.push(\n        `Entry ${index} (${entry.id || \"unknown\"}): ${formattedErrors}`,\n      );\n    }\n  });\n\n  if (errors.length > 0) {\n    throw new Error(\n      `Directory validation failed with ${errors.length} error(s):\\n${errors\n        .map((e) => `  - ${e}`)\n        .join(\"\\n\")}`,\n    );\n  }\n\n  return rawDirectory;\n}\n\nexport const directory = validateDirectory();\n"
  },
  {
    "path": "packages/status-fetcher/src/data/index.ts",
    "content": "import type { StatusPageEntry } from \"../types\";\n\nexport type DirectoryEntry = StatusPageEntry;\n\nexport { directory } from \"./directory\";\n"
  },
  {
    "path": "packages/status-fetcher/src/fetch-utils.ts",
    "content": "/**\n * Fetch utilities with timeout and retry logic\n *\n * @module fetch-utils\n * @description Provides robust HTTP fetching with timeout and automatic retry capabilities\n */\n\n/**\n * Options for fetch operations with timeout support\n * Extends standard RequestInit but excludes 'signal' since we manage it internally\n */\nexport type FetchWithTimeoutOptions = Omit<RequestInit, \"signal\"> & {\n  /** Timeout in milliseconds (default: 30000ms / 30s) */\n  timeout?: number;\n};\n\n/**\n * Options for retry behavior\n */\nexport type RetryOptions = {\n  /** Maximum number of retry attempts (default: 3) */\n  maxRetries?: number;\n  /** Initial delay between retries in milliseconds (default: 100ms) */\n  initialDelay?: number;\n  /** Maximum delay cap in milliseconds (default: 5000ms) */\n  maxDelay?: number;\n  /**\n   * Custom function to determine if a request should be retried\n   * @param error - The error that occurred\n   * @param attempt - The current attempt number (0-indexed)\n   * @returns true to retry, false to stop\n   */\n  shouldRetry?: (error: Error, attempt: number) => boolean;\n};\n\n/**\n * Fetch with timeout support using AbortController\n *\n * @param url - The URL to fetch\n * @param options - Fetch options including timeout\n * @returns Promise resolving to the Response\n * @throws {Error} If request times out or fetch fails\n *\n * @example\n * ```typescript\n * const response = await fetchWithTimeout('https://api.example.com', {\n *   timeout: 5000,\n *   headers: { 'Authorization': 'Bearer token' }\n * });\n * ```\n */\nexport async function fetchWithTimeout(\n  url: string,\n  options: FetchWithTimeoutOptions = {},\n): Promise<Response> {\n  const { timeout = 30000, ...fetchOptions } = options;\n\n  const controller = new AbortController();\n  const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n  try {\n    const response = await fetch(url, {\n      ...fetchOptions,\n      signal: controller.signal,\n    });\n    clearTimeout(timeoutId);\n    return response;\n  } catch (error) {\n    clearTimeout(timeoutId);\n    if (error instanceof Error && error.name === \"AbortError\") {\n      throw new Error(`Request timeout after ${timeout}ms: ${url}`);\n    }\n    throw error;\n  }\n}\n\n/**\n * Fetch with automatic retry on transient failures\n *\n * Features:\n * - Exponential backoff with jitter (±25%) to prevent thundering herd\n * - Smart retry: only retries on network errors and 5xx server errors\n * - No retry on 4xx client errors\n * - Respects maxDelay cap to prevent excessively long waits\n *\n * @param url - The URL to fetch\n * @param options - Combined fetch and retry options\n * @returns Promise resolving to the Response\n * @throws {Error} If all retry attempts fail\n *\n * @example\n * ```typescript\n * // Basic usage with defaults (3 retries, 30s timeout)\n * const response = await fetchWithRetry('https://api.example.com');\n *\n * // Custom retry configuration\n * const response = await fetchWithRetry('https://api.example.com', {\n *   maxRetries: 5,\n *   initialDelay: 200,\n *   timeout: 10000,\n *   shouldRetry: (error, attempt) => {\n *     // Custom retry logic\n *     return attempt < 3 && error.message.includes('ECONNRESET');\n *   }\n * });\n * ```\n */\nexport async function fetchWithRetry(\n  url: string,\n  options: FetchWithTimeoutOptions & RetryOptions = {},\n): Promise<Response> {\n  const {\n    maxRetries = 3,\n    initialDelay = 100,\n    maxDelay = 5000,\n    shouldRetry = defaultShouldRetry,\n    ...fetchOptions\n  } = options;\n\n  let lastError: Error | undefined;\n  let delay = initialDelay;\n\n  for (let attempt = 0; attempt <= maxRetries; attempt++) {\n    try {\n      const response = await fetchWithTimeout(url, fetchOptions);\n\n      // Don't retry on successful responses or 4xx client errors\n      if (response.ok || (response.status >= 400 && response.status < 500)) {\n        return response;\n      }\n\n      // 5xx server errors - retry\n      lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);\n    } catch (error) {\n      lastError = error instanceof Error ? error : new Error(String(error));\n    }\n\n    // Check if we should retry\n    if (attempt < maxRetries && shouldRetry(lastError, attempt)) {\n      // Add jitter (±25%) to prevent thundering herd\n      const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n      await sleep(delay + jitter);\n      delay = Math.min(delay * 2, maxDelay); // Exponential backoff with cap\n    } else {\n      break;\n    }\n  }\n\n  throw lastError || new Error(\"Unknown error during fetch with retry\");\n}\n\n/**\n * Default retry logic: retry on network errors and 5xx responses\n *\n * Retry conditions:\n * - Network errors (fetch failed, network issues)\n * - 5xx server errors (503 Service Unavailable, 500 Internal Server Error, etc.)\n * - Timeout errors only on first attempt\n *\n * Does NOT retry:\n * - 4xx client errors (400 Bad Request, 404 Not Found, etc.)\n * - Timeout errors after first attempt (likely not transient)\n *\n * @param error - The error that occurred\n * @param attempt - The current attempt number (0-indexed)\n * @returns true if request should be retried\n */\nfunction defaultShouldRetry(error: Error, attempt: number): boolean {\n  // Don't retry on timeout errors after first attempt\n  if (error.message.includes(\"timeout\") && attempt > 0) {\n    return false;\n  }\n\n  // Retry on network errors\n  if (\n    error.message.includes(\"fetch failed\") ||\n    error.message.includes(\"network\")\n  ) {\n    return true;\n  }\n\n  // Retry on 5xx errors\n  if (error.message.match(/HTTP 5\\d{2}/)) {\n    return true;\n  }\n\n  return false;\n}\n\n/**\n * Sleep utility for retry delays\n */\nfunction sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * In-flight request cache for deduplication\n * Prevents multiple concurrent requests to the same URL\n */\nconst inflightRequests = new Map<string, Promise<Response>>();\n\n/**\n * Fetch with request deduplication\n *\n * If multiple requests to the same URL are made concurrently, only one actual\n * fetch is performed and all callers receive the same promise. This prevents\n * thundering herd to the same endpoint.\n *\n * @param url - The URL to fetch\n * @param options - Fetch options with timeout and retry\n * @returns Promise resolving to the Response\n *\n * @example\n * ```typescript\n * // These three concurrent calls will result in only ONE actual fetch\n * const [r1, r2, r3] = await Promise.all([\n *   fetchWithDeduplication('https://api.example.com'),\n *   fetchWithDeduplication('https://api.example.com'),\n *   fetchWithDeduplication('https://api.example.com'),\n * ]);\n * ```\n */\nexport async function fetchWithDeduplication(\n  url: string,\n  options: FetchWithTimeoutOptions & RetryOptions = {},\n): Promise<Response> {\n  // Create cache key from URL and relevant options\n  const cacheKey = `${url}:${JSON.stringify({\n    method: options.method || \"GET\",\n    headers: options.headers,\n  })}`;\n\n  // Check if request is already in flight\n  const existing = inflightRequests.get(cacheKey);\n  if (existing) {\n    return existing;\n  }\n\n  // Start new request and cache the promise\n  const promise = fetchWithRetry(url, options).finally(() => {\n    // Clean up when request completes\n    inflightRequests.delete(cacheKey);\n  });\n\n  inflightRequests.set(cacheKey, promise);\n  return promise;\n}\n\n/**\n * Custom error class for fetch operations with rich context\n *\n * Provides detailed information about fetch failures including:\n * - The URL that was being fetched\n * - Which fetcher was making the request\n * - Which directory entry was being processed\n * - The underlying cause (via standard Error.cause)\n *\n * @example\n * ```typescript\n * try {\n *   await fetcher.fetch(entry);\n * } catch (error) {\n *   if (error instanceof FetchError) {\n *     console.error({\n *       message: error.message,     // \"HTTP 503: Service Unavailable\"\n *       url: error.url,             // \"https://api.github.com/status\"\n *       fetcher: error.fetcherName, // \"atlassian\"\n *       entry: error.entryId,       // \"github\"\n *       cause: error.cause          // Original error\n *     });\n *   }\n * }\n * ```\n */\nexport class FetchError extends Error {\n  /**\n   * Creates a new FetchError\n   *\n   * @param message - Human-readable error message\n   * @param url - The URL that failed\n   * @param fetcherName - Name of the fetcher (e.g., \"atlassian\", \"instatus\")\n   * @param entryId - Directory entry ID (e.g., \"github\", \"slack\")\n   * @param cause - Original error that caused this failure\n   */\n  constructor(\n    message: string,\n    public readonly url: string,\n    public readonly fetcherName?: string,\n    public readonly entryId?: string,\n    cause?: Error,\n  ) {\n    super(message, { cause }); // Use standard Error.cause\n    this.name = \"FetchError\";\n  }\n\n  /**\n   * Formats error with full context for logging\n   * @returns Formatted error string\n   */\n  toString(): string {\n    let msg = `[${this.name}]`;\n    if (this.fetcherName) msg += ` ${this.fetcherName}`;\n    if (this.entryId) msg += ` (${this.entryId})`;\n    msg += `: ${this.message}`;\n    if (this.cause) msg += ` - Caused by: ${(this.cause as Error).message}`;\n    return msg;\n  }\n}\n"
  },
  {
    "path": "packages/status-fetcher/src/fetchers/atlassian.ts",
    "content": "import { z } from \"zod\";\nimport { FetchError, fetchWithRetry } from \"../fetch-utils\";\nimport type { StatusFetcher, StatusPageEntry, StatusResult } from \"../types\";\nimport { SEVERITY_LEVELS } from \"../types\";\nimport { inferStatus, urlHostnameEndsWith } from \"../utils\";\n\n// DOCS: https://status.atlassian.com/api\n\nconst atlassianResponseSchema = z.object({\n  page: z.object({\n    id: z.string(),\n    name: z.string(),\n    url: z.string().url(),\n    timezone: z.string().optional(),\n    updated_at: z.string().datetime({ offset: true }),\n  }),\n  status: z.object({\n    indicator: z.enum(SEVERITY_LEVELS),\n    description: z.string(),\n  }),\n});\n\nexport class AtlassianFetcher implements StatusFetcher {\n  name = \"atlassian\";\n\n  canHandle(entry: StatusPageEntry): boolean {\n    return (\n      entry.api_config?.type === \"atlassian\" ||\n      entry.provider === \"atlassian-statuspage\" ||\n      urlHostnameEndsWith(entry.status_page_url, \"statuspage.io\")\n    );\n  }\n\n  async fetch(entry: StatusPageEntry): Promise<StatusResult> {\n    // Construct API URL\n    // Format: https://[id].statuspage.io/api/v2/summary.json\n    const apiUrl =\n      entry.api_config?.endpoint ||\n      `${entry.status_page_url}/api/v2/summary.json`;\n\n    try {\n      const response = await fetchWithRetry(apiUrl, {\n        headers: {\n          \"User-Agent\": \"OpenStatus-Directory/1.0\",\n        },\n        timeout: 30000,\n      });\n\n      if (!response.ok) {\n        throw new FetchError(\n          `HTTP ${response.status}: ${response.statusText}`,\n          apiUrl,\n          this.name,\n          entry.id,\n        );\n      }\n\n      const json = await response.json();\n      const data = atlassianResponseSchema.parse(json);\n\n      const severity = data.status.indicator;\n      const description = data.status.description;\n\n      return {\n        severity,\n        status: inferStatus(description, severity),\n        description,\n        updated_at: new Date(data.page.updated_at).getTime(),\n        timezone: data.page.timezone,\n      };\n    } catch (error) {\n      if (error instanceof FetchError) {\n        throw error;\n      }\n      throw new FetchError(\n        error instanceof Error ? error.message : \"Unknown error\",\n        apiUrl,\n        this.name,\n        entry.id,\n        error instanceof Error ? error : undefined,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/status-fetcher/src/fetchers/betterstack.ts",
    "content": "import { z } from \"zod\";\nimport { FetchError, fetchWithRetry } from \"../fetch-utils\";\nimport type { StatusFetcher, StatusPageEntry, StatusResult } from \"../types\";\nimport { urlHostnameEndsWith } from \"../utils\";\n\n// DOCS: https://betterstack.com/docs/uptime/status-pages/subscribing-to-status-updates/subscribing-to-api/#access-the-json-endpoint\n\nconst betterStackResponseSchema = z.object({\n  data: z.object({\n    id: z.string(),\n    type: z.literal(\"status_page\"),\n    attributes: z.object({\n      company_name: z.string(),\n      timezone: z.string(),\n      aggregate_state: z.enum([\n        \"operational\",\n        \"degraded\",\n        \"downtime\",\n        \"maintenance\",\n      ]),\n      updated_at: z.string(),\n    }),\n  }),\n  included: z.array(z.unknown()).optional(),\n});\n\nexport class BetterStackFetcher implements StatusFetcher {\n  name = \"betterstack\";\n\n  canHandle(entry: StatusPageEntry): boolean {\n    return (\n      entry.api_config?.type === \"betterstack\" ||\n      entry.provider === \"better-uptime\" ||\n      urlHostnameEndsWith(entry.status_page_url, \"betteruptime.com\") ||\n      urlHostnameEndsWith(entry.status_page_url, \"betterstack.com\")\n    );\n  }\n\n  async fetch(entry: StatusPageEntry): Promise<StatusResult> {\n    const apiUrl =\n      entry.api_config?.endpoint || `${entry.status_page_url}/index.json`;\n\n    try {\n      const response = await fetchWithRetry(apiUrl, {\n        headers: {\n          \"User-Agent\": \"OpenStatus-Directory/1.0\",\n          Accept: \"application/json\",\n        },\n        timeout: 30000,\n      });\n\n      if (!response.ok) {\n        throw new FetchError(\n          `HTTP ${response.status}: ${response.statusText}`,\n          apiUrl,\n          this.name,\n          entry.id,\n        );\n      }\n\n      const json = await response.json();\n      const data = betterStackResponseSchema.parse(json);\n\n      const { aggregate_state, updated_at, timezone } = data.data.attributes;\n      const { severity, status, description } =\n        this.mapAggregateState(aggregate_state);\n\n      return {\n        severity,\n        status,\n        description,\n        updated_at: new Date(updated_at).getTime(),\n        timezone: timezone,\n      };\n    } catch (error) {\n      if (error instanceof FetchError) {\n        throw error;\n      }\n      throw new FetchError(\n        error instanceof Error ? error.message : \"Unknown error\",\n        apiUrl,\n        this.name,\n        entry.id,\n        error instanceof Error ? error : undefined,\n      );\n    }\n  }\n\n  private mapAggregateState(\n    state: \"operational\" | \"degraded\" | \"downtime\" | \"maintenance\",\n  ): {\n    severity: \"none\" | \"minor\" | \"major\";\n    status: \"operational\" | \"degraded\" | \"major_outage\" | \"under_maintenance\";\n    description: string;\n  } {\n    switch (state) {\n      case \"operational\":\n        return {\n          severity: \"none\",\n          status: \"operational\",\n          description: \"All Systems Operational\",\n        };\n      case \"degraded\":\n        return {\n          severity: \"minor\",\n          status: \"degraded\",\n          description: \"Degraded Service\",\n        };\n      case \"downtime\":\n        return {\n          severity: \"major\",\n          status: \"major_outage\",\n          description: \"Service Outage\",\n        };\n      case \"maintenance\":\n        return {\n          severity: \"none\",\n          status: \"under_maintenance\",\n          description: \"Maintenance Mode\",\n        };\n      default:\n        return {\n          severity: \"none\",\n          status: \"operational\",\n          description: \"Unknown Status\",\n        };\n    }\n  }\n}\n"
  },
  {
    "path": "packages/status-fetcher/src/fetchers/custom.ts",
    "content": "import { z } from \"zod\";\nimport { FetchError, fetchWithRetry } from \"../fetch-utils\";\nimport type { StatusFetcher, StatusPageEntry, StatusResult } from \"../types\";\nimport { inferStatus } from \"../utils\";\n\nexport class CustomApiFetcher implements StatusFetcher {\n  name = \"custom\";\n\n  canHandle(entry: StatusPageEntry): boolean {\n    return entry.api_config?.type === \"custom\";\n  }\n\n  async fetch(entry: StatusPageEntry): Promise<StatusResult> {\n    if (!entry.api_config?.endpoint) {\n      throw new FetchError(\n        \"Custom API requires explicit endpoint configuration\",\n        \"\",\n        this.name,\n        entry.id,\n      );\n    }\n\n    const apiUrl = entry.api_config.endpoint;\n\n    try {\n      const response = await fetchWithRetry(apiUrl, {\n        headers: {\n          \"User-Agent\": \"OpenStatus-Directory/1.0\",\n          Accept: \"application/json\",\n        },\n        timeout: 30000,\n      });\n\n      if (!response.ok) {\n        throw new FetchError(\n          `HTTP ${response.status}: ${response.statusText}`,\n          apiUrl,\n          this.name,\n          entry.id,\n        );\n      }\n\n      const json = await response.json();\n      const parser = entry.api_config.parser || \"generic\";\n      return this.parseResponse(json, parser);\n    } catch (error) {\n      if (error instanceof FetchError) {\n        throw error;\n      }\n      throw new FetchError(\n        error instanceof Error ? error.message : \"Unknown error\",\n        apiUrl,\n        this.name,\n        entry.id,\n        error instanceof Error ? error : undefined,\n      );\n    }\n  }\n\n  private parseResponse(json: unknown, parser: string): StatusResult {\n    switch (parser) {\n      case \"slack\":\n        return this.parseSlack(json);\n      case \"aws\":\n        return this.parseAws(json);\n      default:\n        return this.parseGeneric(json);\n    }\n  }\n\n  /**\n   * Slack Status API v2.0.0 parser\n   * API: https://slack-status.com/api/v2.0.0/current\n   * Docs: https://docs.slack.dev/reference/slack-status-api/\n   */\n  private parseSlack(json: unknown): StatusResult {\n    const schema = z.object({\n      status: z.enum([\"ok\", \"active\", \"resolved\"]),\n      date_created: z.union([z.number(), z.string()]),\n      date_updated: z.union([z.number(), z.string()]),\n      active_incidents: z.array(\n        z.object({\n          id: z.number(),\n          title: z.string(),\n          type: z.enum([\"incident\", \"notice\", \"outage\"]),\n          status: z.string(),\n          services: z.array(z.string()),\n        }),\n      ),\n    });\n\n    const data = schema.parse(json);\n    const hasActiveIncidents = data.active_incidents.length > 0;\n    const hasOutage = data.active_incidents.some((i) => i.type === \"outage\");\n\n    let severity: \"none\" | \"minor\" | \"major\";\n    let statusType: \"operational\" | \"major_outage\" | \"degraded\";\n    let description: string;\n\n    if (!hasActiveIncidents || data.status === \"ok\") {\n      severity = \"none\";\n      statusType = \"operational\";\n      description = \"All Systems Operational\";\n    } else if (hasOutage) {\n      severity = \"major\";\n      statusType = \"major_outage\";\n      description = data.active_incidents[0].title;\n    } else {\n      severity = \"minor\";\n      statusType = \"degraded\";\n      description = data.active_incidents[0].title;\n    }\n\n    // Handle both number (seconds) and string (ISO) timestamps\n    const dateUpdated =\n      typeof data.date_updated === \"number\"\n        ? data.date_updated * 1000\n        : new Date(data.date_updated).getTime();\n\n    return {\n      severity,\n      status: statusType,\n      description,\n      updated_at: dateUpdated,\n      timezone: \"UTC\",\n    };\n  }\n\n  /**\n   * AWS Health Dashboard parser (placeholder)\n   */\n  private parseAws(_json: unknown): StatusResult {\n    throw new Error(\"AWS parser not implemented - uses RSS feeds\");\n  }\n\n  /**\n   * Generic parser for unknown custom APIs\n   */\n  private parseGeneric(json: unknown): StatusResult {\n    const jsonObject = json as {\n      status?: string;\n      state?: string;\n      health?: string;\n      description?: string;\n      message?: string;\n    };\n    const statusField =\n      jsonObject.status || jsonObject.state || jsonObject.health || \"unknown\";\n    const descriptionField =\n      jsonObject.description || jsonObject.message || String(statusField);\n\n    const statusLower = String(statusField).toLowerCase();\n    let severity: \"none\" | \"minor\" | \"major\" = \"none\";\n\n    if (statusLower.includes(\"down\") || statusLower.includes(\"outage\")) {\n      severity = \"major\";\n    } else if (\n      statusLower.includes(\"degraded\") ||\n      statusLower.includes(\"partial\")\n    ) {\n      severity = \"minor\";\n    } else if (statusLower.includes(\"maintenance\")) {\n      severity = \"minor\";\n    }\n\n    const description = String(descriptionField);\n\n    return {\n      severity,\n      status: inferStatus(description, severity),\n      description,\n      updated_at: Date.now(),\n      timezone: \"UTC\",\n    };\n  }\n}\n"
  },
  {
    "path": "packages/status-fetcher/src/fetchers/html.ts",
    "content": "import { parse } from \"node-html-parser\";\nimport { FetchError, fetchWithRetry } from \"../fetch-utils\";\nimport type { StatusFetcher, StatusPageEntry, StatusResult } from \"../types\";\nimport { inferStatus } from \"../utils\";\n\nexport class HtmlScraperFetcher implements StatusFetcher {\n  name = \"html-scraper\";\n\n  canHandle(entry: StatusPageEntry): boolean {\n    // Fallback fetcher - only use if explicitly enabled\n    return entry.api_config?.type === \"html-scraper\";\n  }\n\n  async fetch(entry: StatusPageEntry): Promise<StatusResult> {\n    const apiUrl = entry.status_page_url;\n\n    try {\n      const response = await fetchWithRetry(apiUrl, {\n        headers: {\n          \"User-Agent\": \"Mozilla/5.0 (compatible; OpenStatus-Bot/1.0)\",\n        },\n        timeout: 30000,\n      });\n\n      if (!response.ok) {\n        throw new FetchError(\n          `HTTP ${response.status}: ${response.statusText}`,\n          apiUrl,\n          this.name,\n          entry.id,\n        );\n      }\n\n      const html = await response.text();\n      const root = parse(html);\n\n      const patterns = [\n        { selector: '[class*=\"status\"]', attr: \"textContent\" },\n        { selector: \"[data-status]\", attr: \"data-status\" },\n        { selector: 'meta[name=\"status\"]', attr: \"content\" },\n      ];\n\n      let description = \"Unknown\";\n      let severity: \"none\" | \"minor\" | \"major\" = \"none\";\n\n      for (const pattern of patterns) {\n        const element = root.querySelector(pattern.selector);\n        if (element) {\n          const text =\n            pattern.attr === \"textContent\"\n              ? element.textContent\n              : element.getAttribute(pattern.attr);\n\n          if (text) {\n            description = text.trim();\n            severity = this.inferSeverity(description);\n            break;\n          }\n        }\n      }\n\n      return {\n        severity,\n        status: inferStatus(description, severity),\n        description,\n        updated_at: Date.now(),\n        timezone: \"UTC\",\n      };\n    } catch (error) {\n      if (error instanceof FetchError) {\n        throw error;\n      }\n      throw new FetchError(\n        error instanceof Error ? error.message : \"Unknown error\",\n        apiUrl,\n        this.name,\n        entry.id,\n        error instanceof Error ? error : undefined,\n      );\n    }\n  }\n\n  private inferSeverity(text: string): \"none\" | \"minor\" | \"major\" {\n    const lower = text.toLowerCase();\n\n    if (lower.includes(\"operational\") || lower.includes(\"all systems\")) {\n      return \"none\";\n    }\n    if (lower.includes(\"degraded\") || lower.includes(\"partial\")) {\n      return \"minor\";\n    }\n    if (lower.includes(\"outage\") || lower.includes(\"down\")) {\n      return \"major\";\n    }\n\n    return \"none\";\n  }\n}\n"
  },
  {
    "path": "packages/status-fetcher/src/fetchers/incidentio.ts",
    "content": "import { z } from \"zod\";\nimport { FetchError, fetchWithRetry } from \"../fetch-utils\";\nimport type { StatusFetcher, StatusPageEntry, StatusResult } from \"../types\";\nimport { urlHostnameEndsWith } from \"../utils\";\n\n// DOCS: https://help.incident.io/articles/7434055319-embed-your-status-page%27s-data-into-your-own-product\n// NOTE: this only works if Widget API is enabled\n\nconst incidentSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  status: z.string(),\n  last_update: z\n    .object({\n      message: z.string(),\n      updated_at: z.string(),\n    })\n    .optional(),\n  affected_components: z.array(z.string()).optional(),\n});\n\nconst incidentioResponseSchema = z.object({\n  ongoing_incidents: z.array(incidentSchema),\n  in_progress_maintenances: z.array(incidentSchema),\n  scheduled_maintenances: z.array(incidentSchema),\n});\n\nexport class IncidentioFetcher implements StatusFetcher {\n  name = \"incidentio\";\n\n  canHandle(entry: StatusPageEntry): boolean {\n    return (\n      entry.api_config?.type === \"incidentio\" ||\n      entry.provider === \"incidentio\" ||\n      urlHostnameEndsWith(entry.status_page_url, \"incident.io\") ||\n      urlHostnameEndsWith(entry.status_page_url, \"incidentio.com\")\n    );\n  }\n\n  async fetch(entry: StatusPageEntry): Promise<StatusResult> {\n    const apiUrl = entry.api_config?.endpoint || this.constructApiUrl(entry);\n\n    try {\n      const response = await fetchWithRetry(apiUrl, {\n        headers: {\n          \"User-Agent\": \"OpenStatus-Directory/1.0\",\n          Accept: \"application/json\",\n        },\n        timeout: 30000,\n      });\n\n      if (!response.ok) {\n        throw new FetchError(\n          `HTTP ${response.status}: ${response.statusText}`,\n          apiUrl,\n          this.name,\n          entry.id,\n        );\n      }\n\n      const json = await response.json();\n      const data = incidentioResponseSchema.parse(json);\n\n      const { severity, status, description } = this.analyzeIncidents(data);\n      const latestUpdate = this.getLatestUpdateTime(data);\n\n      return {\n        severity,\n        status,\n        description,\n        updated_at: latestUpdate,\n        timezone: \"UTC\",\n      };\n    } catch (error) {\n      if (error instanceof FetchError) {\n        throw error;\n      }\n      throw new FetchError(\n        error instanceof Error ? error.message : \"Unknown error\",\n        apiUrl,\n        this.name,\n        entry.id,\n        error instanceof Error ? error : undefined,\n      );\n    }\n  }\n\n  private constructApiUrl(entry: StatusPageEntry): string {\n    const url = new URL(entry.status_page_url);\n    return `${url.origin}/api/widget`;\n  }\n\n  private analyzeIncidents(data: z.infer<typeof incidentioResponseSchema>): {\n    severity: \"none\" | \"minor\" | \"major\";\n    status:\n      | \"operational\"\n      | \"investigating\"\n      | \"identified\"\n      | \"monitoring\"\n      | \"under_maintenance\";\n    description: string;\n  } {\n    const {\n      ongoing_incidents,\n      in_progress_maintenances,\n      scheduled_maintenances,\n    } = data;\n\n    if (ongoing_incidents.length > 0) {\n      const incident = ongoing_incidents[0];\n      const incidentStatus = incident.status.toLowerCase();\n\n      if (incidentStatus.includes(\"investigating\")) {\n        return {\n          severity: \"major\",\n          status: \"investigating\",\n          description: `Incident: ${incident.name}`,\n        };\n      }\n      if (incidentStatus.includes(\"identified\")) {\n        return {\n          severity: \"major\",\n          status: \"identified\",\n          description: `Incident: ${incident.name}`,\n        };\n      }\n      if (incidentStatus.includes(\"monitoring\")) {\n        return {\n          severity: \"minor\",\n          status: \"monitoring\",\n          description: `Monitoring: ${incident.name}`,\n        };\n      }\n\n      return {\n        severity: \"major\",\n        status: \"investigating\",\n        description: incident.name,\n      };\n    }\n\n    if (in_progress_maintenances.length > 0) {\n      const maintenance = in_progress_maintenances[0];\n      return {\n        severity: \"none\",\n        status: \"under_maintenance\",\n        description: `Maintenance: ${maintenance.name}`,\n      };\n    }\n\n    if (scheduled_maintenances.length > 0) {\n      const maintenance = scheduled_maintenances[0];\n      return {\n        severity: \"none\",\n        status: \"operational\",\n        description: `All Systems Operational (Scheduled: ${maintenance.name})`,\n      };\n    }\n\n    return {\n      severity: \"none\",\n      status: \"operational\",\n      description: \"All Systems Operational\",\n    };\n  }\n\n  private getLatestUpdateTime(\n    data: z.infer<typeof incidentioResponseSchema>,\n  ): number {\n    const allItems = [\n      ...data.ongoing_incidents,\n      ...data.in_progress_maintenances,\n      ...data.scheduled_maintenances,\n    ];\n\n    if (allItems.length === 0) return Date.now();\n\n    const timestamps = allItems\n      .filter((item) => item.last_update?.updated_at)\n      .map((item) => new Date(item.last_update?.updated_at).getTime());\n\n    return timestamps.length > 0 ? Math.max(...timestamps) : Date.now();\n  }\n}\n"
  },
  {
    "path": "packages/status-fetcher/src/fetchers/index.ts",
    "content": "import type { StatusFetcher } from \"../types\";\nimport { AtlassianFetcher } from \"./atlassian\";\nimport { BetterStackFetcher } from \"./betterstack\";\nimport { CustomApiFetcher } from \"./custom\";\nimport { HtmlScraperFetcher } from \"./html\";\nimport { IncidentioFetcher } from \"./incidentio\";\nimport { InstatusFetcher } from \"./instatus\";\n\nexport const fetchers: StatusFetcher[] = [\n  new AtlassianFetcher(),\n  new InstatusFetcher(),\n  new BetterStackFetcher(),\n  new IncidentioFetcher(),\n  new CustomApiFetcher(),\n  new HtmlScraperFetcher(),\n];\n\nexport * from \"./atlassian\";\nexport * from \"./instatus\";\nexport * from \"./betterstack\";\nexport * from \"./incidentio\";\nexport * from \"./custom\";\nexport * from \"./html\";\n"
  },
  {
    "path": "packages/status-fetcher/src/fetchers/instatus.ts",
    "content": "import { z } from \"zod\";\nimport { FetchError, fetchWithRetry } from \"../fetch-utils\";\nimport type { StatusFetcher, StatusPageEntry, StatusResult } from \"../types\";\nimport { urlHostnameEndsWith } from \"../utils\";\n\n// DOCS: https://instatus.com/help/status-page/widgets\n\nconst instatusResponseSchema = z.object({\n  activeIncidents: z.array(z.unknown()),\n  activeMaintenances: z.array(z.unknown()),\n  status: z.object({\n    text: z.string(),\n    type: z.enum([\"UP\", \"HASISSUES\", \"UNDERMAINTENANCE\"]),\n  }),\n  page: z.object({\n    name: z.string(),\n    url: z.string(),\n    updated: z.string(),\n  }),\n});\n\nexport class InstatusFetcher implements StatusFetcher {\n  name = \"instatus\";\n\n  canHandle(entry: StatusPageEntry): boolean {\n    return (\n      entry.api_config?.type === \"instatus\" ||\n      entry.provider === \"instatus\" ||\n      urlHostnameEndsWith(entry.status_page_url, \"instatus.com\")\n    );\n  }\n\n  async fetch(entry: StatusPageEntry): Promise<StatusResult> {\n    const apiUrl =\n      entry.api_config?.endpoint || `${entry.status_page_url}/summary.json`;\n\n    try {\n      const response = await fetchWithRetry(apiUrl, {\n        headers: { \"User-Agent\": \"OpenStatus-Directory/1.0\" },\n        timeout: 30000,\n      });\n\n      if (!response.ok) {\n        throw new FetchError(\n          `HTTP ${response.status}: ${response.statusText}`,\n          apiUrl,\n          this.name,\n          entry.id,\n        );\n      }\n\n      const json = await response.json();\n      const data = instatusResponseSchema.parse(json);\n\n      const mapping = this.mapInstatusType(data.status.type);\n\n      return {\n        severity: mapping.severity,\n        status: mapping.status,\n        description: data.status.text,\n        updated_at: new Date(data.page.updated).getTime(),\n        timezone: \"UTC\",\n      };\n    } catch (error) {\n      if (error instanceof FetchError) {\n        throw error;\n      }\n      throw new FetchError(\n        error instanceof Error ? error.message : \"Unknown error\",\n        apiUrl,\n        this.name,\n        entry.id,\n        error instanceof Error ? error : undefined,\n      );\n    }\n  }\n\n  private mapInstatusType(type: \"UP\" | \"HASISSUES\" | \"UNDERMAINTENANCE\"): {\n    severity: \"none\" | \"minor\" | \"major\";\n    status: \"operational\" | \"degraded\" | \"under_maintenance\";\n  } {\n    switch (type) {\n      case \"UP\":\n        return { severity: \"none\", status: \"operational\" };\n      case \"HASISSUES\":\n        return { severity: \"major\", status: \"degraded\" };\n      case \"UNDERMAINTENANCE\":\n        return { severity: \"none\", status: \"under_maintenance\" };\n      default:\n        return { severity: \"none\", status: \"operational\" };\n    }\n  }\n}\n"
  },
  {
    "path": "packages/status-fetcher/src/index.ts",
    "content": "import { directory } from \"./data/directory\";\nimport type { StatusPageEntry } from \"./types\";\n\nexport function getStatusDirectory(): StatusPageEntry[] {\n  return directory;\n}\n\nexport * from \"./types\";\nexport * from \"./utils\";\nexport {\n  fetchWithTimeout,\n  fetchWithRetry,\n  fetchWithDeduplication,\n  FetchError,\n  type FetchWithTimeoutOptions,\n  type RetryOptions,\n} from \"./fetch-utils\";\nexport type { DirectoryEntry } from \"./data/index\";\n"
  },
  {
    "path": "packages/status-fetcher/src/types.ts",
    "content": "import { z } from \"zod\";\n\n// Define arrays as source of truth\nexport const API_CONFIG_TYPES = [\n  \"atlassian\",\n  \"instatus\",\n  \"betterstack\",\n  \"incidentio\",\n  \"custom\",\n  \"html-scraper\",\n] as const;\n\nexport const STATUS_PAGE_PROVIDERS = [\n  \"atlassian-statuspage\",\n  \"instatus\",\n  \"openstatus\",\n  \"incidentio\",\n  \"status.io\",\n  \"custom\",\n  \"better-uptime\",\n  \"unknown\",\n] as const;\n\nexport const INDUSTRIES = [\n  \"cloud-providers\",\n  \"development-tools\",\n  \"saas\",\n  \"communication\",\n  \"ai-ml\",\n  \"cdn\",\n  \"databases\",\n  \"monitoring\",\n  \"security\",\n  \"fintech\",\n  \"e-commerce\",\n] as const;\n\n// Derive TypeScript types from arrays\nexport type ApiConfigType = (typeof API_CONFIG_TYPES)[number];\nexport type StatusPageProvider = (typeof STATUS_PAGE_PROVIDERS)[number];\nexport type Industry = (typeof INDUSTRIES)[number];\n\n// Derive Zod schemas from arrays\nexport const apiConfigSchema = z.object({\n  type: z.enum(API_CONFIG_TYPES),\n  endpoint: z.string().url().optional(),\n  parser: z.string().optional(),\n});\n\nexport const statusPageProviderSchema = z.enum(STATUS_PAGE_PROVIDERS);\nexport const industrySchema = z.enum(INDUSTRIES);\n\n// Interfaces using derived types\nexport interface ApiConfig {\n  type: ApiConfigType;\n  endpoint?: string;\n  parser?: string;\n}\n\nexport interface StatusPageEntry {\n  id: string;\n  name: string;\n  url: string;\n  status_page_url: string;\n  provider: StatusPageProvider;\n  industry: Industry[];\n  description?: string;\n  api_config?: ApiConfig;\n}\n\nexport const statusPageEntrySchema = z.object({\n  id: z.string().min(1),\n  name: z.string().min(1),\n  url: z.string().url(),\n  status_page_url: z.string().url(),\n  provider: statusPageProviderSchema,\n  industry: z.array(industrySchema).min(1),\n  description: z.string().optional(),\n  api_config: apiConfigSchema.optional(),\n});\n\nexport const SEVERITY_LEVELS = [\"none\", \"minor\", \"major\", \"critical\"] as const;\nexport type SeverityLevel = (typeof SEVERITY_LEVELS)[number];\n\nexport const STATUS_TYPES = [\n  \"operational\",\n  \"degraded\",\n  \"partial_outage\",\n  \"major_outage\",\n  \"under_maintenance\",\n  \"investigating\",\n  \"identified\",\n  \"monitoring\",\n  \"resolved\",\n] as const;\nexport type StatusType = (typeof STATUS_TYPES)[number];\n\nexport interface StatusResult {\n  severity: SeverityLevel; // Impact level: none, minor, major, critical\n  status: StatusType; // Normalized status type\n  description: string; // Human-readable status message\n  updated_at: number; // ms since epoch\n  timezone?: string;\n}\n\nexport interface StatusFetcher {\n  name: string;\n  canHandle(entry: StatusPageEntry): boolean;\n  fetch(entry: StatusPageEntry): Promise<StatusResult>;\n}\n"
  },
  {
    "path": "packages/status-fetcher/src/utils.ts",
    "content": "import type { SeverityLevel, StatusType } from \"./types\";\n\n/**\n * Check if a URL's hostname equals or is a subdomain of the given domain.\n * Uses proper hostname parsing to prevent substring spoofing attacks\n * (e.g. \"evil.com/statuspage.io\" or \"statuspage.io.evil.com\").\n */\nexport function urlHostnameEndsWith(url: string, domain: string): boolean {\n  try {\n    const { hostname } = new URL(url);\n    return hostname === domain || hostname.endsWith(`.${domain}`);\n  } catch {\n    return false;\n  }\n}\n\n/**\n * Infer normalized status type from free-text description and severity level\n *\n * This function maps diverse status messages from different providers into a\n * standardized set of status types. It uses keyword matching with priority ordering\n * to ensure consistent classification.\n *\n * **Detection Priority** (highest to lowest):\n * 1. **Incident workflow** - investigating, identified, monitoring, resolved\n * 2. **Maintenance** - scheduled or emergency maintenance\n * 3. **Specific outages** - major outage, partial outage\n * 4. **General down state** - with word boundary detection to avoid false matches\n * 5. **Degraded performance** - degraded, performance issues\n * 6. **Severity fallback** - when no keywords match, use severity level\n *\n * @param description - Free-text status description (e.g., \"Investigating database issues\")\n * @param severity - Impact level: none, minor, major, or critical\n * @returns Normalized status type\n *\n * @example\n * ```typescript\n * inferStatus(\"Investigating API errors\", \"major\")\n * // => \"investigating\"\n *\n * inferStatus(\"Service degraded\", \"minor\")\n * // => \"degraded\"\n *\n * inferStatus(\"All Systems Operational\", \"none\")\n * // => \"operational\"\n *\n * inferStatus(\"Scheduled maintenance in progress\", \"none\")\n * // => \"under_maintenance\"\n * ```\n */\nexport function inferStatus(\n  description: string,\n  severity: SeverityLevel,\n): StatusType {\n  const lower = description.toLowerCase();\n\n  // Incident workflow states (highest priority - specific states)\n  if (lower.includes(\"investigating\")) return \"investigating\";\n  if (lower.includes(\"identified\")) return \"identified\";\n  if (lower.includes(\"monitoring\")) return \"monitoring\";\n  if (lower.includes(\"resolved\")) return \"resolved\";\n\n  // Maintenance (high priority - planned work)\n  if (lower.includes(\"maintenance\")) return \"under_maintenance\";\n\n  // Specific outage types (check specific before general)\n  if (lower.includes(\"major outage\") || lower.includes(\"complete outage\")) {\n    return \"major_outage\";\n  }\n  if (lower.includes(\"partial outage\") || lower.includes(\"partial system\")) {\n    return \"partial_outage\";\n  }\n\n  // General down state (use word boundaries to avoid false matches)\n  // Matches: \"is down\", \"are down\", \"service down\", but not \"countdown\"\n  if (/\\b(is|are|service|system|services|systems)\\s+down\\b/.test(lower)) {\n    return \"major_outage\";\n  }\n  // Also match standalone \"down\" at end of sentence or after punctuation\n  if (/(\\s|^)down(\\s|[.,!?]|$)/.test(lower)) {\n    return \"major_outage\";\n  }\n\n  // Degraded/performance (lower priority)\n  if (lower.includes(\"degraded\") || lower.includes(\"performance\")) {\n    return \"degraded\";\n  }\n\n  // Operational (default for severity: \"none\")\n  if (severity === \"none\") return \"operational\";\n\n  // Fallback based on severity\n  if (severity === \"critical\") return \"major_outage\";\n  if (severity === \"major\") return \"major_outage\";\n  if (severity === \"minor\") return \"degraded\";\n\n  return \"operational\";\n}\n"
  },
  {
    "path": "packages/status-fetcher/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2022\"],\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"noEmit\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"types\": [\"bun-types\"]\n  },\n  \"include\": [\"src/**/*\", \"__tests__/**/*\"]\n}\n"
  },
  {
    "path": "packages/subscriptions/bunfig.toml",
    "content": "[test]\npreload = [\"./src/test-preload.ts\"]\n"
  },
  {
    "path": "packages/subscriptions/package.json",
    "content": "{\n  \"name\": \"@openstatus/subscriptions\",\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"main\": \"./src/index.ts\",\n  \"types\": \"./src/index.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./src/index.ts\",\n      \"types\": \"./src/index.ts\"\n    }\n  },\n  \"scripts\": {\n    \"test\": \"bun test\"\n  },\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/emails\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/bun\": \"latest\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/subscriptions/src/channels/email.test.ts",
    "content": "import { beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport { EmailClient } from \"@openstatus/emails\";\nimport type { PageUpdate, Subscription } from \"../types\";\nimport {\n  sendEmailNotifications,\n  sendEmailVerification,\n  validateEmailConfig,\n} from \"./email\";\n\n// RESEND_API_KEY is set in test-preload.ts (see bunfig.toml) so @openstatus/emails\n// loads successfully and EmailClient prototype methods can be spied on.\n\nconst sendPageSubscriptionMock = spyOn(\n  EmailClient.prototype,\n  \"sendPageSubscription\",\n).mockResolvedValue(undefined);\n\nconst sendStatusReportUpdateMock = spyOn(\n  EmailClient.prototype,\n  \"sendStatusReportUpdate\",\n).mockResolvedValue(undefined);\n\nfunction makeSub(overrides: Partial<Subscription> = {}): Subscription {\n  return {\n    id: 1,\n    pageId: 1,\n    pageName: \"Test Page\",\n    pageSlug: \"test\",\n    channelType: \"email\",\n    email: \"user@example.com\",\n    token: \"token-123\",\n    componentIds: [],\n    ...overrides,\n  };\n}\n\nfunction makeUpdate(overrides: Partial<PageUpdate> = {}): PageUpdate {\n  return {\n    id: 1,\n    pageId: 1,\n    title: \"Test Incident\",\n    status: \"investigating\",\n    message: \"We are investigating.\",\n    pageComponentIds: [],\n    pageComponents: [],\n    date: new Date().toISOString(),\n    ...overrides,\n  };\n}\n\nbeforeEach(() => {\n  sendPageSubscriptionMock.mockClear();\n  sendStatusReportUpdateMock.mockClear();\n});\n\n// ─── validateEmailConfig ──────────────────────────────────────────────────────\n\ndescribe(\"validateEmailConfig\", () => {\n  test(\"returns valid for a valid email address\", async () => {\n    const result = await validateEmailConfig(\"user@example.com\");\n    expect(result.valid).toBe(true);\n  });\n\n  test(\"returns invalid for a plain string without @\", async () => {\n    const result = await validateEmailConfig(\"not-an-email\");\n    expect(result.valid).toBe(false);\n    expect(result.error).toBeDefined();\n  });\n\n  test(\"returns invalid for non-string input\", async () => {\n    const result = await validateEmailConfig(42);\n    expect(result.valid).toBe(false);\n  });\n\n  test(\"returns invalid for null\", async () => {\n    const result = await validateEmailConfig(null);\n    expect(result.valid).toBe(false);\n  });\n});\n\n// ─── sendEmailVerification ────────────────────────────────────────────────────\n\ndescribe(\"sendEmailVerification\", () => {\n  test(\"throws when subscription has no email\", async () => {\n    const sub = makeSub({ email: undefined });\n    await expect(\n      sendEmailVerification(sub, \"https://example.com/verify/token\"),\n    ).rejects.toThrow(\"Email is required\");\n  });\n\n  test(\"calls sendPageSubscription with the correct arguments\", async () => {\n    const sub = makeSub({ email: \"user@example.com\", pageName: \"My Page\" });\n    await sendEmailVerification(sub, \"https://example.com/verify/abc\");\n\n    expect(sendPageSubscriptionMock).toHaveBeenCalledTimes(1);\n    const [args] = sendPageSubscriptionMock.mock.calls[0];\n    expect(args.to).toBe(\"user@example.com\");\n    expect(args.link).toBe(\"https://example.com/verify/abc\");\n    expect(args.page).toBe(\"My Page\");\n  });\n});\n\n// ─── sendEmailNotifications ───────────────────────────────────────────────────\n\ndescribe(\"sendEmailNotifications\", () => {\n  test(\"does nothing for an empty subscriptions array\", async () => {\n    await sendEmailNotifications([], makeUpdate());\n    expect(sendStatusReportUpdateMock).not.toHaveBeenCalled();\n  });\n\n  test(\"filters out subscriptions without an email address\", async () => {\n    const sub = makeSub({ email: undefined });\n    await sendEmailNotifications([sub], makeUpdate());\n    expect(sendStatusReportUpdateMock).not.toHaveBeenCalled();\n  });\n\n  test(\"calls sendStatusReportUpdate once with all valid subscribers\", async () => {\n    const sub1 = makeSub({ email: \"a@example.com\", token: \"token-a\" });\n    const sub2 = makeSub({ email: \"b@example.com\", token: \"token-b\" });\n    const update = makeUpdate({ title: \"Outage\", status: \"resolved\" });\n\n    await sendEmailNotifications([sub1, sub2], update);\n\n    expect(sendStatusReportUpdateMock).toHaveBeenCalledTimes(1);\n    const [args] = sendStatusReportUpdateMock.mock.calls[0];\n    expect(args.subscribers).toHaveLength(2);\n    expect(args.subscribers[0].email).toBe(\"a@example.com\");\n    expect(args.subscribers[1].email).toBe(\"b@example.com\");\n    expect(args.reportTitle).toBe(\"Outage\");\n    expect(args.status).toBe(\"resolved\");\n  });\n\n  test(\"passes page components to sendStatusReportUpdate\", async () => {\n    const sub = makeSub({ email: \"user@example.com\" });\n    const update = makeUpdate({ pageComponents: [\"API\", \"Database\"] });\n\n    await sendEmailNotifications([sub], update);\n\n    const [args] = sendStatusReportUpdateMock.mock.calls[0];\n    expect(args.pageComponents).toEqual([\"API\", \"Database\"]);\n  });\n});\n"
  },
  {
    "path": "packages/subscriptions/src/channels/email.ts",
    "content": "import { EmailClient } from \"@openstatus/emails\";\nimport { z } from \"zod\";\nimport type { PageUpdate, Subscription } from \"../types\";\n\nlet emailClient: EmailClient | null = null;\n\nfunction getEmailClient(): EmailClient {\n  if (!emailClient) {\n    if (!process.env.RESEND_API_KEY) {\n      throw new Error(\n        \"RESEND_API_KEY environment variable is required for email notifications\",\n      );\n    }\n    emailClient = new EmailClient({\n      apiKey: process.env.RESEND_API_KEY,\n    });\n  }\n  return emailClient;\n}\n\nexport async function validateEmailConfig(config: unknown) {\n  const email = z.email().safeParse(config);\n  return { valid: email.success, error: email.error?.message };\n}\n\nfunction hasEmailAndToken(\n  sub: Subscription,\n): sub is Subscription & { email: string; token: string } {\n  return (\n    sub.email !== undefined &&\n    sub.email !== null &&\n    sub.token !== undefined &&\n    sub.token !== null\n  );\n}\n\nexport async function sendEmailVerification(\n  subscription: Subscription,\n  verifyUrl: string,\n) {\n  if (!subscription.email) {\n    throw new Error(\"Email is required for email channel\");\n  }\n\n  const client = getEmailClient();\n  await client.sendPageSubscription({\n    to: subscription.email,\n    link: verifyUrl,\n    page: subscription.pageName,\n  });\n}\n\nexport async function sendEmailNotifications(\n  subscriptions: Subscription[],\n  pageUpdate: PageUpdate,\n) {\n  if (subscriptions.length === 0) return;\n\n  const validSubscriptions = subscriptions.filter(hasEmailAndToken);\n  if (validSubscriptions.length === 0) return;\n\n  const firstSub = validSubscriptions[0];\n\n  const client = getEmailClient();\n  await client.sendStatusReportUpdate({\n    subscribers: validSubscriptions.map((sub) => ({\n      email: sub.email,\n      token: sub.token,\n    })),\n    pageTitle: firstSub.pageName,\n    pageSlug: firstSub.pageSlug,\n    customDomain: firstSub.customDomain,\n    reportTitle: pageUpdate.title,\n    status: pageUpdate.status,\n    message: pageUpdate.message,\n    date: pageUpdate.date,\n    pageComponents: pageUpdate.pageComponents,\n  });\n}\n"
  },
  {
    "path": "packages/subscriptions/src/channels/index.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\";\nimport { getChannel } from \"./index\";\n\ndescribe(\"getChannel\", () => {\n  test('returns email channel for \"email\"', () => {\n    const channel = getChannel(\"email\");\n    expect(channel).not.toBeNull();\n    expect(channel?.id).toBe(\"email\");\n    expect(typeof channel?.sendNotifications).toBe(\"function\");\n    expect(typeof channel?.sendVerification).toBe(\"function\");\n    expect(typeof channel?.validateConfig).toBe(\"function\");\n  });\n\n  test('returns webhook channel for \"webhook\"', () => {\n    const channel = getChannel(\"webhook\");\n    expect(channel).not.toBeNull();\n    expect(channel?.id).toBe(\"webhook\");\n    expect(typeof channel?.sendNotifications).toBe(\"function\");\n    expect(typeof channel?.sendVerification).toBe(\"function\");\n    expect(typeof channel?.validateConfig).toBe(\"function\");\n  });\n\n  test(\"returns null for unknown channel types\", () => {\n    expect(getChannel(\"sms\")).toBeNull();\n    expect(getChannel(\"slack\")).toBeNull();\n    expect(getChannel(\"\")).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/subscriptions/src/channels/index.ts",
    "content": "import type { SubscriptionChannel } from \"../types\";\nimport {\n  sendEmailNotifications,\n  sendEmailVerification,\n  validateEmailConfig,\n} from \"./email\";\nimport {\n  sendWebhookNotifications,\n  sendWebhookVerification,\n  validateWebhookConfig,\n} from \"./webhook\";\n\nexport function getChannel(channelType: string): SubscriptionChannel | null {\n  switch (channelType) {\n    case \"email\": {\n      return {\n        id: \"email\",\n        sendNotifications: sendEmailNotifications,\n        sendVerification: sendEmailVerification,\n        validateConfig: validateEmailConfig,\n      };\n    }\n    case \"webhook\": {\n      return {\n        id: \"webhook\",\n        sendNotifications: sendWebhookNotifications,\n        sendVerification: sendWebhookVerification,\n        validateConfig: validateWebhookConfig,\n      };\n    }\n    default:\n      return null;\n  }\n}\n"
  },
  {
    "path": "packages/subscriptions/src/channels/webhook.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, spyOn, test } from \"bun:test\";\nimport type { PageUpdate, Subscription } from \"../types\";\nimport {\n  sendWebhookNotifications,\n  sendWebhookVerification,\n  validateWebhookConfig,\n} from \"./webhook\";\n\n// biome-ignore lint/suspicious/noExplicitAny: test spy\nlet fetchMock: any;\n\nfunction makeSub(overrides: Partial<Subscription> = {}): Subscription {\n  return {\n    id: 1,\n    pageId: 1,\n    pageName: \"Test Page\",\n    pageSlug: \"test\",\n    channelType: \"webhook\",\n    webhookUrl: \"https://example.com/webhook\",\n    token: \"token-123\",\n    componentIds: [],\n    ...overrides,\n  };\n}\n\nfunction makeUpdate(overrides: Partial<PageUpdate> = {}): PageUpdate {\n  return {\n    id: 1,\n    pageId: 1,\n    title: \"Test Incident\",\n    status: \"investigating\",\n    message: \"We are investigating.\",\n    pageComponentIds: [],\n    pageComponents: [\"API\", \"Database\"],\n    date: new Date().toISOString(),\n    ...overrides,\n  };\n}\n\nbeforeEach(() => {\n  fetchMock = spyOn(global, \"fetch\");\n});\n\nafterEach(() => {\n  fetchMock.mockRestore();\n});\n\n// ─── validateWebhookConfig ────────────────────────────────────────────────────\n\ndescribe(\"validateWebhookConfig\", () => {\n  test(\"returns valid for an empty config\", async () => {\n    const result = await validateWebhookConfig({});\n    expect(result.valid).toBe(true);\n  });\n\n  test(\"returns valid with optional headers and secret\", async () => {\n    const result = await validateWebhookConfig({\n      headers: [{ key: \"Authorization\", value: \"Bearer token\" }],\n      secret: \"my-secret\",\n    });\n    expect(result.valid).toBe(true);\n  });\n\n  test(\"returns invalid when a header is missing a key\", async () => {\n    const result = await validateWebhookConfig({\n      headers: [{ key: \"\", value: \"v\" }], // key must be min length 1\n    });\n    expect(result.valid).toBe(false);\n  });\n\n  test(\"returns invalid for a non-object value\", async () => {\n    const result = await validateWebhookConfig(\"not-an-object\");\n    expect(result.valid).toBe(false);\n    expect(result.error).toBeDefined();\n  });\n\n  test(\"returns valid with only headers\", async () => {\n    const result = await validateWebhookConfig({ headers: [] });\n    expect(result.valid).toBe(true);\n  });\n});\n\n// ─── sendWebhookVerification ──────────────────────────────────────────────────\n\ndescribe(\"sendWebhookVerification\", () => {\n  test(\"throws when subscription has no webhookUrl\", async () => {\n    const sub = makeSub({ webhookUrl: undefined });\n    await expect(\n      sendWebhookVerification(sub, \"https://example.com/verify/token\"),\n    ).rejects.toThrow(\"Webhook URL is required\");\n  });\n\n  test(\"resolves when fetch returns 200\", async () => {\n    fetchMock.mockResolvedValue(new Response(null, { status: 200 }));\n    const sub = makeSub();\n    await expect(\n      sendWebhookVerification(sub, \"https://example.com/verify/abc\"),\n    ).resolves.toBeUndefined();\n  });\n\n  test(\"throws when fetch returns a non-ok status\", async () => {\n    fetchMock.mockResolvedValue(\n      new Response(null, { status: 500, statusText: \"Server Error\" }),\n    );\n    const sub = makeSub();\n    await expect(\n      sendWebhookVerification(sub, \"https://example.com/verify/abc\"),\n    ).rejects.toThrow(\"Webhook verification failed: 500\");\n  });\n\n  test(\"posts to the correct URL with the expected payload\", async () => {\n    fetchMock.mockResolvedValue(new Response(null, { status: 200 }));\n    const sub = makeSub({\n      token: \"my-token\",\n      webhookUrl: \"https://hooks.example.com/sub\",\n    });\n    await sendWebhookVerification(sub, \"https://example.com/verify/my-token\");\n\n    expect(fetchMock).toHaveBeenCalledTimes(1);\n    const [url, init] = fetchMock.mock.calls[0];\n    expect(url).toBe(\"https://hooks.example.com/sub\");\n    expect(init?.method).toBe(\"POST\");\n    const body = JSON.parse(init?.body as string);\n    expect(body.type).toBe(\"verification\");\n    expect(body.token).toBe(\"my-token\");\n    expect(body.verifyUrl).toBe(\"https://example.com/verify/my-token\");\n  });\n});\n\n// ─── sendWebhookNotifications ─────────────────────────────────────────────────\n\ndescribe(\"sendWebhookNotifications\", () => {\n  test(\"skips subscriptions without a webhookUrl\", async () => {\n    const sub = makeSub({ webhookUrl: undefined });\n    await sendWebhookNotifications([sub], makeUpdate());\n    expect(fetchMock).not.toHaveBeenCalled();\n  });\n\n  test(\"sends a POST request to each webhook URL\", async () => {\n    fetchMock.mockResolvedValue(new Response(null, { status: 200 }));\n    const sub1 = makeSub({ webhookUrl: \"https://hook1.example.com\" });\n    const sub2 = makeSub({ webhookUrl: \"https://hook2.example.com\" });\n\n    await sendWebhookNotifications([sub1, sub2], makeUpdate());\n\n    expect(fetchMock).toHaveBeenCalledTimes(2);\n    const urls = fetchMock.mock.calls.map(([url]: [string]) => url);\n    expect(urls).toContain(\"https://hook1.example.com\");\n    expect(urls).toContain(\"https://hook2.example.com\");\n  });\n\n  test(\"sends the correct payload shape\", async () => {\n    fetchMock.mockResolvedValue(new Response(null, { status: 200 }));\n    const sub = makeSub({ pageId: 42, pageName: \"My Page\" });\n    const update = makeUpdate({\n      title: \"Outage\",\n      status: \"resolved\",\n      pageComponents: [\"API\"],\n    });\n\n    await sendWebhookNotifications([sub], update);\n\n    const [, init] = fetchMock.mock.calls[0];\n    const body = JSON.parse(init?.body as string);\n    expect(body.type).toBe(\"page_update\");\n    expect(body.page.id).toBe(42);\n    expect(body.page.name).toBe(\"My Page\");\n    expect(body.update.status).toBe(\"resolved\");\n    expect(body.update.title).toBe(\"Outage\");\n    expect(body.update.pageComponents).toEqual([\"API\"]);\n  });\n\n  test(\"applies custom headers from channelConfig\", async () => {\n    fetchMock.mockResolvedValue(new Response(null, { status: 200 }));\n    const sub = makeSub({\n      channelConfig: JSON.stringify({\n        headers: [{ key: \"X-Custom-Header\", value: \"my-value\" }],\n      }),\n    });\n\n    await sendWebhookNotifications([sub], makeUpdate());\n\n    const [, init] = fetchMock.mock.calls[0];\n    const headers = init?.headers as Record<string, string>;\n    expect(headers[\"X-Custom-Header\"]).toBe(\"my-value\");\n  });\n\n  test(\"continues sending to remaining webhooks when one fails\", async () => {\n    fetchMock\n      .mockRejectedValueOnce(new Error(\"Network error\"))\n      .mockResolvedValueOnce(new Response(null, { status: 200 }));\n\n    const sub1 = makeSub({ webhookUrl: \"https://fail.example.com\" });\n    const sub2 = makeSub({ webhookUrl: \"https://succeed.example.com\" });\n\n    await expect(\n      sendWebhookNotifications([sub1, sub2], makeUpdate()),\n    ).resolves.toBeUndefined();\n\n    expect(fetchMock).toHaveBeenCalledTimes(2);\n  });\n});\n"
  },
  {
    "path": "packages/subscriptions/src/channels/webhook.ts",
    "content": "import { z } from \"zod\";\nimport type { PageUpdate, Subscription } from \"../types\";\n\nexport async function validateWebhookConfig(config: unknown) {\n  const schema = z.object({\n    headers: z\n      .array(\n        z.object({\n          key: z.string().min(1),\n          value: z.string(),\n        }),\n      )\n      .optional(),\n    secret: z.string().optional(),\n  });\n  const result = schema.safeParse(config);\n  return { valid: result.success, error: result.error?.message };\n}\n\nfunction hasWebhookUrl(\n  sub: Subscription,\n): sub is Subscription & { webhookUrl: string } {\n  return sub.webhookUrl !== undefined && sub.webhookUrl !== null;\n}\n\nexport async function sendWebhookVerification(\n  subscription: Subscription,\n  verifyUrl: string,\n) {\n  if (!subscription.webhookUrl) {\n    throw new Error(\"Webhook URL is required for webhook channel\");\n  }\n\n  const response = await fetch(subscription.webhookUrl, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({\n      type: \"verification\",\n      token: subscription.token,\n      verifyUrl,\n    }),\n    signal: AbortSignal.timeout(10000),\n  });\n\n  if (!response.ok) {\n    throw new Error(\n      `Webhook verification failed: ${response.status} ${response.statusText}`,\n    );\n  }\n}\n\nexport async function sendWebhookNotifications(\n  subscriptions: Subscription[],\n  pageUpdate: PageUpdate,\n) {\n  const validSubscriptions = subscriptions.filter(hasWebhookUrl);\n  if (validSubscriptions.length === 0) return;\n\n  await Promise.allSettled(\n    validSubscriptions.map(async (subscription) => {\n      let config: Record<string, unknown> = {};\n      try {\n        config = subscription.channelConfig\n          ? JSON.parse(subscription.channelConfig)\n          : {};\n      } catch {\n        console.error(\n          `Invalid channelConfig JSON for subscription ${subscription.id}`,\n        );\n      }\n\n      const payload = {\n        type: \"page_update\",\n        page: { id: subscription.pageId, name: subscription.pageName },\n        update: {\n          id: pageUpdate.id,\n          title: pageUpdate.title,\n          status: pageUpdate.status,\n          message: pageUpdate.message,\n          pageComponents: pageUpdate.pageComponents,\n          date: pageUpdate.date,\n        },\n      };\n\n      const headers: Record<string, string> = {\n        \"Content-Type\": \"application/json\",\n        \"User-Agent\": \"OpenStatus-Webhooks/1.0\",\n      };\n\n      if (config.headers) {\n        for (const header of config.headers as {\n          key: string;\n          value: string;\n        }[]) {\n          headers[header.key] = header.value;\n        }\n      }\n\n      try {\n        const response = await fetch(subscription.webhookUrl, {\n          method: \"POST\",\n          headers,\n          body: JSON.stringify(payload),\n          signal: AbortSignal.timeout(10000),\n        });\n\n        if (!response.ok) {\n          console.error(\n            `Webhook notification failed for ${subscription.webhookUrl}: ${response.status} ${response.statusText}`,\n          );\n          throw new Error(`Webhook returned ${response.status}`);\n        }\n      } catch (error) {\n        console.error(\n          `Failed to send webhook notification to ${subscription.webhookUrl}:`,\n          error,\n        );\n        throw error;\n      }\n    }),\n  );\n}\n"
  },
  {
    "path": "packages/subscriptions/src/dispatcher.test.ts",
    "content": "import {\n  afterAll,\n  beforeAll,\n  beforeEach,\n  describe,\n  expect,\n  spyOn,\n  test,\n} from \"bun:test\";\nimport { db, eq } from \"@openstatus/db\";\nimport {\n  pageSubscriber,\n  pageSubscriberToPageComponent,\n} from \"@openstatus/db/src/schema\";\nimport { EmailClient } from \"@openstatus/emails\";\nimport { dispatchPageUpdate } from \"./dispatcher\";\nimport type { PageUpdate } from \"./types\";\n\n// RESEND_API_KEY is set in test-preload.ts (see bunfig.toml) so @openstatus/emails\n// loads successfully and EmailClient prototype methods can be spied on.\n\nconst sendStatusReportUpdateMock = spyOn(\n  EmailClient.prototype,\n  \"sendStatusReportUpdate\",\n).mockResolvedValue(undefined);\n\n// IDs present in the seeded database\nconst PAGE_ID = 1; // slug: \"status\"\nconst COMPONENT_1 = 1;\nconst COMPONENT_2 = 2;\n\nconst EMAILS = {\n  entirePage: \"dispatcher-page-test@example.com\",\n  component1: \"dispatcher-comp1-test@example.com\",\n  component2: \"dispatcher-comp2-test@example.com\",\n};\n\n// Captured IDs after insert\nlet _subEntirePageId: number;\nlet subComponent1Id: number;\nlet subComponent2Id: number;\n\nfunction makePageUpdate(overrides: Partial<PageUpdate> = {}): PageUpdate {\n  return {\n    id: 1,\n    pageId: PAGE_ID,\n    title: \"Test Incident\",\n    status: \"investigating\",\n    message: \"We are investigating.\",\n    pageComponentIds: [],\n    pageComponents: [],\n    date: new Date().toISOString(),\n    ...overrides,\n  };\n}\n\nasync function cleanAll() {\n  for (const email of Object.values(EMAILS)) {\n    await db.delete(pageSubscriber).where(eq(pageSubscriber.email, email));\n  }\n}\n\nbeforeAll(async () => {\n  await cleanAll();\n\n  const insertAccepted = async (email: string) => {\n    return db\n      .insert(pageSubscriber)\n      .values({\n        channelType: \"email\",\n        email,\n        pageId: PAGE_ID,\n        token: crypto.randomUUID(),\n        acceptedAt: new Date(),\n        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),\n      })\n      .returning()\n      .get();\n  };\n\n  const subEntirePage = await insertAccepted(EMAILS.entirePage);\n  const subComponent1 = await insertAccepted(EMAILS.component1);\n  const subComponent2 = await insertAccepted(EMAILS.component2);\n\n  _subEntirePageId = subEntirePage.id;\n  subComponent1Id = subComponent1.id;\n  subComponent2Id = subComponent2.id;\n\n  // subEntirePage has no component associations → entire-page scope\n  // subComponent1 subscribes to component 1 only\n  await db\n    .insert(pageSubscriberToPageComponent)\n    .values({ pageSubscriberId: subComponent1Id, pageComponentId: COMPONENT_1 })\n    .run();\n\n  // subComponent2 subscribes to component 2 only\n  await db\n    .insert(pageSubscriberToPageComponent)\n    .values({ pageSubscriberId: subComponent2Id, pageComponentId: COMPONENT_2 })\n    .run();\n});\n\nafterAll(cleanAll);\n\nbeforeEach(() => {\n  sendStatusReportUpdateMock.mockClear();\n});\n\n// ─── dispatchPageUpdate - component filtering ─────────────────────────────────\n\ndescribe(\"dispatchPageUpdate - component filtering\", () => {\n  test(\"notifies entire-page and component-1 subscriber when update affects component 1\", async () => {\n    await dispatchPageUpdate(\n      makePageUpdate({ pageComponentIds: [COMPONENT_1] }),\n    );\n\n    expect(sendStatusReportUpdateMock).toHaveBeenCalledTimes(1);\n    const { subscribers } = sendStatusReportUpdateMock.mock.calls[0][0];\n    const emails = subscribers.map((s: { email: string }) => s.email);\n\n    expect(emails).toContain(EMAILS.entirePage);\n    expect(emails).toContain(EMAILS.component1);\n    expect(emails).not.toContain(EMAILS.component2);\n  });\n\n  test(\"notifies entire-page and component-2 subscriber when update affects component 2\", async () => {\n    await dispatchPageUpdate(\n      makePageUpdate({ pageComponentIds: [COMPONENT_2] }),\n    );\n\n    const { subscribers } = sendStatusReportUpdateMock.mock.calls[0][0];\n    const emails = subscribers.map((s: { email: string }) => s.email);\n\n    expect(emails).toContain(EMAILS.entirePage);\n    expect(emails).toContain(EMAILS.component2);\n    expect(emails).not.toContain(EMAILS.component1);\n  });\n\n  test(\"notifies all subscribers when update affects both components\", async () => {\n    await dispatchPageUpdate(\n      makePageUpdate({ pageComponentIds: [COMPONENT_1, COMPONENT_2] }),\n    );\n\n    const { subscribers } = sendStatusReportUpdateMock.mock.calls[0][0];\n    const emails = subscribers.map((s: { email: string }) => s.email);\n\n    expect(emails).toContain(EMAILS.entirePage);\n    expect(emails).toContain(EMAILS.component1);\n    expect(emails).toContain(EMAILS.component2);\n  });\n\n  test(\"notifies only the entire-page subscriber when update has no affected components\", async () => {\n    await dispatchPageUpdate(makePageUpdate({ pageComponentIds: [] }));\n\n    expect(sendStatusReportUpdateMock).toHaveBeenCalledTimes(1);\n    const { subscribers } = sendStatusReportUpdateMock.mock.calls[0][0];\n\n    expect(subscribers).toHaveLength(1);\n    expect(subscribers[0].email).toBe(EMAILS.entirePage);\n  });\n\n  test(\"does not notify component subscribers when update affects a different component\", async () => {\n    // Only component 1 is affected — component 2 subscriber should be skipped\n    await dispatchPageUpdate(\n      makePageUpdate({ pageComponentIds: [COMPONENT_1] }),\n    );\n\n    const { subscribers } = sendStatusReportUpdateMock.mock.calls[0][0];\n    const emails = subscribers.map((s: { email: string }) => s.email);\n\n    expect(emails).not.toContain(EMAILS.component2);\n  });\n});\n\n// ─── dispatchPageUpdate - edge cases ─────────────────────────────────────────\n\ndescribe(\"dispatchPageUpdate - edge cases\", () => {\n  test(\"does not call sendNotifications for a non-existent page\", async () => {\n    await dispatchPageUpdate(\n      makePageUpdate({ pageId: 99999, pageComponentIds: [COMPONENT_1] }),\n    );\n\n    expect(sendStatusReportUpdateMock).not.toHaveBeenCalled();\n  });\n\n  test(\"does not propagate channel failure — resolves even when sendStatusReportUpdate throws\", async () => {\n    sendStatusReportUpdateMock.mockRejectedValueOnce(new Error(\"SMTP failure\"));\n\n    await expect(\n      dispatchPageUpdate(makePageUpdate({ pageComponentIds: [] })),\n    ).resolves.toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/subscriptions/src/dispatcher.ts",
    "content": "import { and, db, eq, isNotNull, isNull } from \"@openstatus/db\";\nimport {\n  maintenance,\n  page,\n  pageSubscriber,\n  statusReportUpdate,\n} from \"@openstatus/db/src/schema\";\nimport { getChannel } from \"./channels\";\nimport type { PageUpdate, Subscription } from \"./types\";\n\n/**\n * Dispatch notifications for a status report update\n */\nexport async function dispatchStatusReportUpdate(statusReportUpdateId: number) {\n  const update = await db.query.statusReportUpdate.findFirst({\n    where: eq(statusReportUpdate.id, statusReportUpdateId),\n    with: {\n      statusReport: {\n        with: {\n          statusReportsToPageComponents: {\n            with: { pageComponent: true },\n          },\n        },\n      },\n    },\n  });\n\n  if (!update?.statusReport) {\n    console.error(`Status report update ${statusReportUpdateId} not found`);\n    return;\n  }\n\n  if (!update.statusReport.pageId) {\n    console.error(`Status report ${update.statusReport.id} has no page ID`);\n    return;\n  }\n\n  const pageComponents = update.statusReport.statusReportsToPageComponents.map(\n    (i) => i.pageComponent,\n  );\n\n  await dispatchPageUpdate({\n    id: update.statusReport.id,\n    pageId: update.statusReport.pageId,\n    title: update.statusReport.title,\n    status: update.status as PageUpdate[\"status\"],\n    message: update.message,\n    pageComponentIds: pageComponents.map((c) => c.id),\n    pageComponents: pageComponents.map((c) => c.name),\n    date: update.date.toISOString(),\n  });\n}\n\n/**\n * Dispatch notifications for a maintenance update\n */\nexport async function dispatchMaintenanceUpdate(maintenanceId: number) {\n  const maintenanceWithComponents = await db.query.maintenance.findFirst({\n    where: eq(maintenance.id, maintenanceId),\n    with: {\n      maintenancesToPageComponents: {\n        with: { pageComponent: true },\n      },\n    },\n  });\n\n  if (!maintenanceWithComponents) {\n    console.error(`Maintenance ${maintenanceId} not found`);\n    return;\n  }\n\n  if (!maintenanceWithComponents.pageId) {\n    console.error(`Maintenance ${maintenanceId} has no page ID`);\n    return;\n  }\n\n  const pageComponents =\n    maintenanceWithComponents.maintenancesToPageComponents.map(\n      (i) => i.pageComponent,\n    );\n\n  await dispatchPageUpdate({\n    id: maintenanceWithComponents.id,\n    pageId: maintenanceWithComponents.pageId,\n    title: maintenanceWithComponents.title,\n    status: \"maintenance\",\n    message: maintenanceWithComponents.message,\n    pageComponentIds: pageComponents.map((c) => c.id),\n    pageComponents: pageComponents.map((c) => c.name),\n    date: `${maintenanceWithComponents.from.toISOString()} - ${maintenanceWithComponents.to.toISOString()}`,\n  });\n}\n\n/**\n * Dispatch notifications for a page update to all matching subscriptions\n *\n * - Entire page subscriptions (empty componentIds): always notified\n * - Component subscriptions: only notified if any affected component matches\n */\nexport async function dispatchPageUpdate(pageUpdate: PageUpdate) {\n  const affectedComponentIds = pageUpdate.pageComponentIds;\n\n  const pageData = await db\n    .select({\n      id: page.id,\n      name: page.title,\n      slug: page.slug,\n      customDomain: page.customDomain,\n    })\n    .from(page)\n    .where(eq(page.id, pageUpdate.pageId))\n    .get();\n\n  if (!pageData) {\n    console.error(`Page ${pageUpdate.pageId} not found`);\n    return;\n  }\n\n  const subscribersWithComponents = await db.query.pageSubscriber.findMany({\n    where: and(\n      eq(pageSubscriber.pageId, pageUpdate.pageId),\n      isNotNull(pageSubscriber.acceptedAt),\n      isNull(pageSubscriber.unsubscribedAt),\n    ),\n    with: {\n      components: true,\n    },\n  });\n\n  const matchingSubscriptions: Subscription[] = subscribersWithComponents\n    .map((sub) => ({\n      id: sub.id,\n      pageId: sub.pageId,\n      pageName: pageData.name,\n      pageSlug: pageData.slug,\n      customDomain: pageData.customDomain,\n      channelType: sub.channelType as \"email\" | \"webhook\",\n      email: sub.email ?? undefined,\n      webhookUrl: sub.webhookUrl ?? undefined,\n      channelConfig: sub.channelConfig ?? undefined,\n      token: sub.token ?? undefined,\n      acceptedAt: sub.acceptedAt ?? undefined,\n      componentIds: sub.components.map((c) => c.pageComponentId),\n    }))\n    .filter((sub) => {\n      // Entire page subscription matches all updates\n      if (sub.componentIds.length === 0) return true;\n\n      // Component subscription: check for overlap\n      return affectedComponentIds.some((id) => sub.componentIds.includes(id));\n    });\n\n  if (matchingSubscriptions.length === 0) {\n    console.log(`No matching subscriptions for page update ${pageUpdate.id}`);\n    return;\n  }\n\n  const byChannel = matchingSubscriptions.reduce(\n    (acc, sub) => {\n      if (!acc[sub.channelType]) {\n        acc[sub.channelType] = [];\n      }\n      acc[sub.channelType].push(sub);\n      return acc;\n    },\n    {} as Record<string, Subscription[]>,\n  );\n\n  await Promise.allSettled(\n    Object.entries(byChannel).map(async ([channelType, subs]) => {\n      const channel = getChannel(channelType);\n      if (!channel) {\n        console.error(`Unknown channel type: ${channelType}`);\n        return;\n      }\n\n      try {\n        await channel.sendNotifications(subs, pageUpdate);\n        console.log(`Sent ${subs.length} notifications via ${channelType}`);\n      } catch (error) {\n        console.error(\n          `Failed to send notifications via ${channelType}:`,\n          error,\n        );\n      }\n    }),\n  );\n}\n"
  },
  {
    "path": "packages/subscriptions/src/index.ts",
    "content": "// Export channel functions\nexport {\n  sendEmailNotifications,\n  sendEmailVerification,\n  validateEmailConfig,\n} from \"./channels/email\";\nexport {\n  sendWebhookNotifications,\n  sendWebhookVerification,\n  validateWebhookConfig,\n} from \"./channels/webhook\";\nexport { getChannel } from \"./channels/index\";\n\n// Export dispatcher functions\nexport {\n  dispatchStatusReportUpdate,\n  dispatchMaintenanceUpdate,\n  dispatchPageUpdate,\n} from \"./dispatcher\";\n\n// Export service functions\nexport {\n  upsertEmailSubscription,\n  hasPendingUnexpiredSubscription,\n  verifySubscription,\n  getSubscriptionByToken,\n  updateSubscriptionScope,\n  unsubscribe,\n} from \"./service\";\n\n// Export types\nexport type { Subscription, PageUpdate, SubscriptionChannel } from \"./types\";\n"
  },
  {
    "path": "packages/subscriptions/src/service.test.ts",
    "content": "import { afterAll, beforeAll, describe, expect, test } from \"bun:test\";\nimport { and, db, eq, isNull } from \"@openstatus/db\";\nimport { pageSubscriber } from \"@openstatus/db/src/schema\";\nimport {\n  getSubscriptionByToken,\n  unsubscribe,\n  updateSubscriptionScope,\n  upsertEmailSubscription,\n  verifySubscription,\n} from \"./service\";\n\n// IDs present in the seeded database\nconst PAGE_ID = 1; // slug: \"status\", customDomain: \"\"\nconst COMPONENT_1 = 1;\nconst COMPONENT_2 = 2;\n\n// One email per describe block to keep them fully independent\nconst EMAILS = {\n  upsert: \"svc-upsert-test@example.com\",\n  verify: \"svc-verify-test@example.com\",\n  verifyExpired: \"svc-expired-test@example.com\",\n  getByToken: \"svc-token-test@example.com\",\n  scope: \"svc-scope-test@example.com\",\n  scopeUnverified: \"svc-scope-unverified@example.com\",\n  scopeUnsubbed: \"svc-scope-unsubbed@example.com\",\n  unsub: \"svc-unsub-test@example.com\",\n};\n\nasync function cleanAll() {\n  for (const email of Object.values(EMAILS)) {\n    await db.delete(pageSubscriber).where(eq(pageSubscriber.email, email));\n  }\n}\n\nbeforeAll(cleanAll);\nafterAll(cleanAll);\n\n// ─── upsertEmailSubscription ──────────────────────────────────────────────────\n\ndescribe(\"upsertEmailSubscription\", () => {\n  const email = EMAILS.upsert;\n\n  beforeAll(async () => {\n    await db.delete(pageSubscriber).where(eq(pageSubscriber.email, email));\n  });\n\n  test(\"creates a new subscription for an unknown email\", async () => {\n    const result = await upsertEmailSubscription({ email, pageId: PAGE_ID });\n\n    expect(result.email).toBe(email);\n    expect(result.pageId).toBe(PAGE_ID);\n    expect(result.token).toBeDefined();\n    expect(result.acceptedAt).toBeUndefined();\n    expect(result.componentIds).toEqual([]);\n  });\n\n  test(\"does not create a duplicate row when called again\", async () => {\n    await upsertEmailSubscription({ email, pageId: PAGE_ID });\n\n    const rows = await db.query.pageSubscriber.findMany({\n      where: eq(pageSubscriber.email, email),\n    });\n\n    expect(rows).toHaveLength(1);\n  });\n\n  test(\"merges new components into an existing subscription\", async () => {\n    const result = await upsertEmailSubscription({\n      email,\n      pageId: PAGE_ID,\n      componentIds: [COMPONENT_1],\n    });\n\n    expect(result.componentIds).toContain(COMPONENT_1);\n  });\n\n  test(\"refreshes expiresAt for a still-pending subscription\", async () => {\n    const before = new Date();\n    await upsertEmailSubscription({ email, pageId: PAGE_ID });\n\n    const row = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.email, email),\n    });\n\n    expect(row?.expiresAt?.getTime()).toBeGreaterThan(before.getTime());\n  });\n\n  test(\"stores email in lowercase regardless of input casing\", async () => {\n    await db.delete(pageSubscriber).where(eq(pageSubscriber.email, email));\n\n    const result = await upsertEmailSubscription({\n      email: email.toUpperCase(),\n      pageId: PAGE_ID,\n    });\n\n    expect(result.email).toBe(email);\n  });\n\n  test(\"creates a new row (does not reactivate) when email was previously unsubscribed\", async () => {\n    const existing = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.email, email),\n    });\n\n    if (!existing) {\n      throw new Error(\"Existing subscriber not found\");\n    }\n\n    await db\n      .update(pageSubscriber)\n      .set({ acceptedAt: new Date(), unsubscribedAt: new Date() })\n      .where(eq(pageSubscriber.id, existing.id))\n      .run();\n\n    const result = await upsertEmailSubscription({\n      email,\n      pageId: PAGE_ID,\n      componentIds: [COMPONENT_1],\n    });\n\n    // A brand-new row — not a mutation of the old one\n    expect(result.id).not.toBe(existing.id);\n    expect(result.acceptedAt).toBeUndefined(); // requires re-verification\n    expect(result.componentIds).toEqual([COMPONENT_1]);\n\n    // Old row is preserved with its unsubscribedAt intact\n    const oldRow = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.id, existing.id),\n    });\n    expect(oldRow?.unsubscribedAt).toBeDefined();\n  });\n\n  test(\"creates a new row when email was unsubscribed before ever verifying\", async () => {\n    // Find the currently active (pending, never verified) row left by the previous test\n    const pending = await db.query.pageSubscriber.findFirst({\n      where: and(\n        eq(pageSubscriber.email, email),\n        isNull(pageSubscriber.unsubscribedAt),\n      ),\n    });\n    if (!pending) throw new Error(\"Pending subscriber not found\");\n    expect(pending.acceptedAt).toBeNull(); // confirm it was never verified\n\n    await db\n      .update(pageSubscriber)\n      .set({ unsubscribedAt: new Date() })\n      .where(eq(pageSubscriber.id, pending.id))\n      .run();\n\n    const result = await upsertEmailSubscription({ email, pageId: PAGE_ID });\n\n    expect(result.id).not.toBe(pending.id);\n    expect(result.acceptedAt).toBeUndefined();\n\n    // Old unverified+unsubscribed row still has both fields set correctly\n    const oldRow = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.id, pending.id),\n    });\n    expect(oldRow?.unsubscribedAt).toBeDefined();\n    expect(oldRow?.acceptedAt).toBeNull();\n  });\n\n  test(\"throws for component IDs that do not belong to this page\", async () => {\n    await expect(\n      upsertEmailSubscription({ email, pageId: PAGE_ID, componentIds: [9999] }),\n    ).rejects.toThrow(\"Invalid components: 9999\");\n  });\n\n  test(\"throws for a page ID that does not exist\", async () => {\n    await expect(\n      upsertEmailSubscription({ email, pageId: 99999 }),\n    ).rejects.toThrow(\"Page 99999 not found\");\n  });\n});\n\n// ─── verifySubscription ───────────────────────────────────────────────────────\n\ndescribe(\"verifySubscription\", () => {\n  const email = EMAILS.verify;\n  let pendingToken: string;\n\n  beforeAll(async () => {\n    await db.delete(pageSubscriber).where(eq(pageSubscriber.email, email));\n    const sub = await upsertEmailSubscription({ email, pageId: PAGE_ID });\n    if (!sub.token) {\n      throw new Error(\"Token is undefined\");\n    }\n    pendingToken = sub.token;\n  });\n\n  test(\"returns null for an unknown token\", async () => {\n    const result = await verifySubscription(\"non-existent-token-xyz\");\n    expect(result).toBeNull();\n  });\n\n  test(\"returns null when domain does not match page slug\", async () => {\n    const result = await verifySubscription(pendingToken, \"wrong-domain\");\n    expect(result).toBeNull();\n  });\n\n  test(\"marks the subscription as accepted on first verification\", async () => {\n    const result = await verifySubscription(pendingToken, \"status\");\n    expect(result).not.toBeNull();\n    expect(result?.acceptedAt).toBeDefined();\n  });\n\n  test(\"returns an already-accepted subscription idempotently\", async () => {\n    // pendingToken now points to an accepted subscription from the previous test\n    const result = await verifySubscription(pendingToken, \"status\");\n    expect(result).not.toBeNull();\n    expect(result?.acceptedAt).toBeDefined();\n  });\n\n  test(\"throws for a token with an expired expiresAt\", async () => {\n    const expiredEmail = EMAILS.verifyExpired;\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, expiredEmail));\n\n    await db\n      .insert(pageSubscriber)\n      .values({\n        channelType: \"email\",\n        email: expiredEmail,\n        pageId: PAGE_ID,\n        token: \"expired-token-xyz\",\n        expiresAt: new Date(Date.now() - 1000), // 1 second in the past\n      })\n      .run();\n\n    await expect(verifySubscription(\"expired-token-xyz\")).rejects.toThrow(\n      \"Verification token expired\",\n    );\n  });\n});\n\n// ─── getSubscriptionByToken ───────────────────────────────────────────────────\n\ndescribe(\"getSubscriptionByToken\", () => {\n  const email = EMAILS.getByToken;\n  let token: string;\n\n  beforeAll(async () => {\n    await db.delete(pageSubscriber).where(eq(pageSubscriber.email, email));\n    const sub = await upsertEmailSubscription({ email, pageId: PAGE_ID });\n    if (!sub.token) {\n      throw new Error(\"Token is undefined\");\n    }\n    token = sub.token;\n  });\n\n  test(\"returns null for an unknown token\", async () => {\n    const result = await getSubscriptionByToken(\"unknown-token-xyz\");\n    expect(result).toBeNull();\n  });\n\n  test(\"returns null when domain does not match\", async () => {\n    const result = await getSubscriptionByToken(token, \"wrong-domain\");\n    expect(result).toBeNull();\n  });\n\n  test('masks the email address as \"x***@domain\"', async () => {\n    const result = await getSubscriptionByToken(token);\n    expect(result).not.toBeNull();\n    // \"svc-token-test@example.com\" → \"s***@example.com\"\n    expect(result?.email).toMatch(/^s\\*\\*\\*@example\\.com$/);\n    expect(result?.email).not.toBe(email);\n  });\n\n  test(\"returns subscription data for a valid token and matching domain\", async () => {\n    const result = await getSubscriptionByToken(token, \"status\");\n    expect(result).not.toBeNull();\n    expect(result?.pageId).toBe(PAGE_ID);\n    expect(result?.pageSlug).toBe(\"status\");\n  });\n});\n\n// ─── updateSubscriptionScope ──────────────────────────────────────────────────\n\ndescribe(\"updateSubscriptionScope\", () => {\n  const email = EMAILS.scope;\n  let token: string;\n\n  beforeAll(async () => {\n    await db.delete(pageSubscriber).where(eq(pageSubscriber.email, email));\n    const sub = await upsertEmailSubscription({\n      email,\n      pageId: PAGE_ID,\n      componentIds: [COMPONENT_1],\n    });\n    if (!sub.token) {\n      throw new Error(\"Token is undefined\");\n    }\n    token = sub.token;\n    // Verify so acceptedAt is set — required by the guard added to updateSubscriptionScope\n    await verifySubscription(token);\n  });\n\n  test(\"throws for an unknown token\", async () => {\n    await expect(\n      updateSubscriptionScope({ token: \"unknown-token-xyz\", componentIds: [] }),\n    ).rejects.toThrow(\"Subscription not found\");\n  });\n\n  test(\"throws when subscription is not yet verified\", async () => {\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, EMAILS.scopeUnverified));\n    const sub = await upsertEmailSubscription({\n      email: EMAILS.scopeUnverified,\n      pageId: PAGE_ID,\n    });\n    if (!sub.token) throw new Error(\"Token is undefined\");\n\n    await expect(\n      updateSubscriptionScope({ token: sub.token, componentIds: [] }),\n    ).rejects.toThrow(\"Subscription not yet verified\");\n  });\n\n  test(\"throws when subscription is unsubscribed\", async () => {\n    await db\n      .delete(pageSubscriber)\n      .where(eq(pageSubscriber.email, EMAILS.scopeUnsubbed));\n    const sub = await upsertEmailSubscription({\n      email: EMAILS.scopeUnsubbed,\n      pageId: PAGE_ID,\n    });\n    if (!sub.token) throw new Error(\"Token is undefined\");\n    await verifySubscription(sub.token);\n    await unsubscribe(sub.token);\n\n    await expect(\n      updateSubscriptionScope({ token: sub.token, componentIds: [] }),\n    ).rejects.toThrow(\"Subscription is unsubscribed\");\n  });\n\n  test(\"replaces existing component scope with new ones\", async () => {\n    const result = await updateSubscriptionScope({\n      token,\n      componentIds: [COMPONENT_2],\n    });\n\n    expect(result.componentIds).toEqual([COMPONENT_2]);\n    expect(result.componentIds).not.toContain(COMPONENT_1);\n  });\n\n  test(\"can clear all components (switches to entire-page scope)\", async () => {\n    const result = await updateSubscriptionScope({ token, componentIds: [] });\n    expect(result.componentIds).toEqual([]);\n  });\n\n  test(\"throws when a component does not belong to this page\", async () => {\n    await expect(\n      updateSubscriptionScope({ token, componentIds: [9999] }),\n    ).rejects.toThrow(\"Some components do not belong to this page\");\n  });\n});\n\n// ─── unsubscribe ──────────────────────────────────────────────────────────────\n\ndescribe(\"unsubscribe\", () => {\n  const email = EMAILS.unsub;\n  let token: string;\n\n  beforeAll(async () => {\n    await db.delete(pageSubscriber).where(eq(pageSubscriber.email, email));\n    const sub = await upsertEmailSubscription({ email, pageId: PAGE_ID });\n    if (!sub.token) {\n      throw new Error(\"Token is undefined\");\n    }\n    token = sub.token;\n  });\n\n  test(\"throws for an unknown token\", async () => {\n    await expect(unsubscribe(\"unknown-token-xyz\")).rejects.toThrow(\n      \"Subscription not found\",\n    );\n  });\n\n  test(\"throws when the domain does not match\", async () => {\n    await expect(unsubscribe(token, \"wrong-domain\")).rejects.toThrow(\n      \"Subscription not found\",\n    );\n  });\n\n  test(\"sets unsubscribedAt on a valid token\", async () => {\n    await expect(unsubscribe(token, \"status\")).resolves.toBeUndefined();\n\n    const row = await db.query.pageSubscriber.findFirst({\n      where: eq(pageSubscriber.token, token),\n    });\n    expect(row?.unsubscribedAt).toBeDefined();\n  });\n\n  test(\"resolves silently if the subscription is already unsubscribed\", async () => {\n    await expect(unsubscribe(token)).resolves.toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/subscriptions/src/service.ts",
    "content": "import { and, db, eq, inArray, isNull } from \"@openstatus/db\";\nimport {\n  page,\n  pageComponent,\n  pageSubscriber,\n  pageSubscriberToPageComponent,\n} from \"@openstatus/db/src/schema\";\n\n/**\n * Subscription Service Layer\n * Handles all subscription CRUD operations\n */\n\n// Verification token expiry configuration\nconst VERIFICATION_EXPIRY_DAYS = 7;\nconst VERIFICATION_EXPIRY_MS = VERIFICATION_EXPIRY_DAYS * 24 * 60 * 60 * 1000;\n\n/**\n * Mask email address for privacy (e.g., \"john@example.com\" -> \"j***@example.com\")\n */\nfunction maskEmail(email: string): string {\n  const [localPart, domain] = email.split(\"@\");\n  if (!domain) return email;\n\n  const masked = localPart.length > 0 ? `${localPart[0]}***` : \"***\";\n  return `${masked}@${domain}`;\n}\n\ninterface UpsertEmailSubscriptionInput {\n  email: string;\n  pageId: number;\n  componentIds?: number[]; // Empty = entire page\n}\n\ninterface UpdateSubscriptionScopeInput {\n  token: string;\n  componentIds: number[]; // New scope (replaces current)\n  domain?: string; // Optional domain validation\n}\n\n/**\n * Create or update email subscription (ADD behavior)\n *\n * - If no subscription exists (or only unsubscribed ones): creates new subscription\n * - If pending subscription exists: merges components and refreshes expiry\n * - If verified subscription exists: returns as-is (caller handles \"already subscribed\")\n */\nexport async function upsertEmailSubscription(\n  input: UpsertEmailSubscriptionInput,\n) {\n  const { email, pageId, componentIds = [] } = input;\n\n  const pageData = await db.query.page.findFirst({\n    where: eq(page.id, pageId),\n  });\n\n  if (!pageData) {\n    throw new Error(`Page ${pageId} not found`);\n  }\n\n  // Validate component IDs belong to this page\n  if (componentIds.length > 0) {\n    const validComponents = await db\n      .select({ id: pageComponent.id })\n      .from(pageComponent)\n      .where(\n        and(\n          eq(pageComponent.pageId, pageId),\n          inArray(pageComponent.id, componentIds),\n        ),\n      )\n      .all();\n\n    if (validComponents.length !== componentIds.length) {\n      const validIds = validComponents.map((c) => c.id);\n      const invalidIds = componentIds.filter((id) => !validIds.includes(id));\n      throw new Error(`Invalid components: ${invalidIds.join(\", \")}`);\n    }\n  }\n\n  // Check for active subscription\n  const existing = await db.query.pageSubscriber.findFirst({\n    where: and(\n      eq(pageSubscriber.email, email.toLowerCase()),\n      eq(pageSubscriber.pageId, pageId),\n      eq(pageSubscriber.channelType, \"email\"),\n      isNull(pageSubscriber.unsubscribedAt),\n    ),\n    with: {\n      components: true,\n    },\n  });\n\n  if (existing) {\n    // Already verified — return as-is without merging components.\n    // The caller (tRPC subscribe) will throw \"Email already subscribed\" on acceptedAt.\n    if (existing.acceptedAt) {\n      return {\n        id: existing.id,\n        pageId: existing.pageId,\n        pageName: pageData.title,\n        pageSlug: pageData.slug,\n        customDomain: pageData.customDomain,\n        channelType: existing.channelType,\n        email: existing.email ?? undefined,\n        token: existing.token,\n        acceptedAt: existing.acceptedAt,\n        componentIds: existing.components.map((c) => c.pageComponentId),\n      };\n    }\n\n    // Pending — merge new components and refresh expiry\n    const currentIds = existing.components.map((c) => c.pageComponentId);\n    const mergedIds = [...new Set([...currentIds, ...componentIds])];\n\n    const newIds = mergedIds.filter((id) => !currentIds.includes(id));\n    if (newIds.length > 0) {\n      await db\n        .insert(pageSubscriberToPageComponent)\n        .values(\n          newIds.map((compId) => ({\n            pageSubscriberId: existing.id,\n            pageComponentId: compId,\n          })),\n        )\n        .onConflictDoNothing()\n        .run();\n    }\n\n    const newExpiresAt = new Date(Date.now() + VERIFICATION_EXPIRY_MS);\n    await db\n      .update(pageSubscriber)\n      .set({ expiresAt: newExpiresAt, updatedAt: new Date() })\n      .where(eq(pageSubscriber.id, existing.id))\n      .run();\n\n    return {\n      id: existing.id,\n      pageId: existing.pageId,\n      pageName: pageData.title,\n      pageSlug: pageData.slug,\n      customDomain: pageData.customDomain,\n      channelType: existing.channelType,\n      email: existing.email ?? undefined,\n      token: existing.token,\n      acceptedAt: undefined,\n      componentIds: mergedIds,\n    };\n  }\n\n  // No existing subscription — create new one\n  const token = crypto.randomUUID();\n  const expiresAt = new Date(Date.now() + VERIFICATION_EXPIRY_MS);\n\n  const subscription = await db.transaction(async (tx) => {\n    const sub = await tx\n      .insert(pageSubscriber)\n      .values({\n        channelType: \"email\",\n        email: email.toLowerCase(),\n        webhookUrl: null,\n        pageId,\n        token,\n        expiresAt,\n      })\n      .returning()\n      .get();\n\n    if (componentIds.length > 0) {\n      await tx\n        .insert(pageSubscriberToPageComponent)\n        .values(\n          componentIds.map((compId) => ({\n            pageSubscriberId: sub.id,\n            pageComponentId: compId,\n          })),\n        )\n        .run();\n    }\n\n    return sub;\n  });\n\n  return {\n    id: subscription.id,\n    pageId: subscription.pageId,\n    pageName: pageData.title,\n    pageSlug: pageData.slug,\n    customDomain: pageData.customDomain,\n    channelType: \"email\" as const,\n    email: subscription.email ?? undefined,\n    token: subscription.token,\n    acceptedAt: subscription.acceptedAt ?? undefined,\n    unsubscribedAt: subscription.unsubscribedAt ?? undefined,\n    componentIds,\n  };\n}\n\n/**\n * Verify a subscription by token\n */\nexport async function verifySubscription(token: string, domain?: string) {\n  const subscription = await db.query.pageSubscriber.findFirst({\n    where: eq(pageSubscriber.token, token),\n    with: {\n      page: true,\n      components: true,\n    },\n  });\n\n  if (!subscription) {\n    return null;\n  }\n\n  // Validate domain if provided\n  if (domain) {\n    const domainLower = domain.toLowerCase();\n    const pageSlugLower = subscription.page.slug.toLowerCase();\n    const customDomainLower = subscription.page.customDomain?.toLowerCase();\n\n    if (domainLower !== pageSlugLower && domainLower !== customDomainLower) {\n      return null;\n    }\n  }\n\n  // Already accepted\n  if (subscription.acceptedAt) {\n    return {\n      id: subscription.id,\n      pageId: subscription.pageId,\n      pageName: subscription.page.title,\n      pageSlug: subscription.page.slug,\n      customDomain: subscription.page.customDomain,\n      channelType: subscription.channelType as \"email\" | \"webhook\",\n      email: subscription.email ?? undefined,\n      webhookUrl: subscription.webhookUrl ?? undefined,\n      token: subscription.token,\n      acceptedAt: subscription.acceptedAt ?? undefined,\n      unsubscribedAt: subscription.unsubscribedAt ?? undefined,\n      componentIds: subscription.components.map((c) => c.pageComponentId),\n    };\n  }\n\n  // Check if expired\n  if (subscription.expiresAt && subscription.expiresAt < new Date()) {\n    throw new Error(\"Verification token expired\");\n  }\n\n  // Mark as accepted\n  const updated = await db\n    .update(pageSubscriber)\n    .set({ acceptedAt: new Date(), updatedAt: new Date() })\n    .where(eq(pageSubscriber.id, subscription.id))\n    .returning()\n    .get();\n\n  return {\n    id: updated.id,\n    pageId: updated.pageId,\n    pageName: subscription.page.title,\n    pageSlug: subscription.page.slug,\n    customDomain: subscription.page.customDomain,\n    channelType: updated.channelType as \"email\" | \"webhook\",\n    email: updated.email ?? undefined,\n    webhookUrl: updated.webhookUrl ?? undefined,\n    token: updated.token,\n    acceptedAt: updated.acceptedAt ?? undefined,\n    componentIds: subscription.components.map((c) => c.pageComponentId),\n  };\n}\n\n/**\n * Get subscription by token (for management UI)\n */\nexport async function getSubscriptionByToken(token: string, domain?: string) {\n  const subscription = await db.query.pageSubscriber.findFirst({\n    where: eq(pageSubscriber.token, token),\n    with: {\n      page: true,\n      components: true,\n    },\n  });\n\n  if (!subscription) {\n    return null;\n  }\n\n  if (domain) {\n    const domainLower = domain.toLowerCase();\n    const pageSlugLower = subscription.page.slug.toLowerCase();\n    const customDomainLower = subscription.page.customDomain?.toLowerCase();\n\n    if (domainLower !== pageSlugLower && domainLower !== customDomainLower) {\n      return null;\n    }\n  }\n\n  return {\n    id: subscription.id,\n    pageId: subscription.pageId,\n    pageName: subscription.page.title,\n    pageSlug: subscription.page.slug,\n    customDomain: subscription.page.customDomain,\n    channelType: subscription.channelType as \"email\" | \"webhook\",\n    email: subscription.email ? maskEmail(subscription.email) : undefined,\n    webhookUrl: subscription.webhookUrl ?? undefined,\n    acceptedAt: subscription.acceptedAt ?? undefined,\n    unsubscribedAt: subscription.unsubscribedAt ?? undefined,\n    componentIds: subscription.components.map((c) => c.pageComponentId),\n  };\n}\n\n/**\n * Check whether an email already has a pending (unverified, unexpired) subscription for a page.\n * Used to prevent re-sending verification emails for already-pending subscriptions.\n */\nexport async function hasPendingUnexpiredSubscription(\n  email: string,\n  pageId: number,\n): Promise<boolean> {\n  const existing = await db.query.pageSubscriber.findFirst({\n    where: and(\n      eq(pageSubscriber.email, email.toLowerCase()),\n      eq(pageSubscriber.pageId, pageId),\n      eq(pageSubscriber.channelType, \"email\"),\n      isNull(pageSubscriber.unsubscribedAt),\n      isNull(pageSubscriber.acceptedAt),\n    ),\n  });\n  return !!(existing?.expiresAt && existing.expiresAt > new Date());\n}\n\n/**\n * Update subscription scope (replace all components)\n */\nexport async function updateSubscriptionScope(\n  input: UpdateSubscriptionScopeInput,\n) {\n  const { token, componentIds, domain } = input;\n\n  const subscription = await db.query.pageSubscriber.findFirst({\n    where: eq(pageSubscriber.token, token),\n    with: {\n      page: true,\n      components: true,\n    },\n  });\n\n  if (!subscription) {\n    throw new Error(\"Subscription not found\");\n  }\n\n  if (domain) {\n    const domainLower = domain.toLowerCase();\n    const pageSlugLower = subscription.page.slug.toLowerCase();\n    const customDomainLower = subscription.page.customDomain?.toLowerCase();\n\n    if (domainLower !== pageSlugLower && domainLower !== customDomainLower) {\n      throw new Error(\"Subscription not found\");\n    }\n  }\n\n  if (!subscription.acceptedAt) {\n    throw new Error(\"Subscription not yet verified\");\n  }\n\n  if (subscription.unsubscribedAt) {\n    throw new Error(\"Subscription is unsubscribed\");\n  }\n\n  if (componentIds.length > 0) {\n    const validComponents = await db\n      .select({ id: pageComponent.id })\n      .from(pageComponent)\n      .where(\n        and(\n          eq(pageComponent.pageId, subscription.pageId),\n          inArray(pageComponent.id, componentIds),\n        ),\n      )\n      .all();\n\n    if (validComponents.length !== componentIds.length) {\n      throw new Error(\"Some components do not belong to this page\");\n    }\n  }\n\n  await db.transaction(async (tx) => {\n    await tx\n      .delete(pageSubscriberToPageComponent)\n      .where(\n        eq(pageSubscriberToPageComponent.pageSubscriberId, subscription.id),\n      )\n      .run();\n\n    if (componentIds.length > 0) {\n      await tx\n        .insert(pageSubscriberToPageComponent)\n        .values(\n          componentIds.map((id) => ({\n            pageSubscriberId: subscription.id,\n            pageComponentId: id,\n          })),\n        )\n        .run();\n    }\n\n    await tx\n      .update(pageSubscriber)\n      .set({ updatedAt: new Date() })\n      .where(eq(pageSubscriber.id, subscription.id))\n      .run();\n  });\n\n  return {\n    id: subscription.id,\n    pageId: subscription.pageId,\n    pageName: subscription.page.title,\n    pageSlug: subscription.page.slug,\n    customDomain: subscription.page.customDomain,\n    channelType: subscription.channelType as \"email\" | \"webhook\",\n    email: subscription.email ?? undefined,\n    token: subscription.token,\n    acceptedAt: subscription.acceptedAt ?? undefined,\n    componentIds,\n  };\n}\n\n/**\n * Unsubscribe by token\n */\nexport async function unsubscribe(token: string, domain?: string) {\n  const subscription = await db.query.pageSubscriber.findFirst({\n    where: eq(pageSubscriber.token, token),\n    with: {\n      page: true,\n    },\n  });\n\n  if (!subscription) {\n    throw new Error(\"Subscription not found\");\n  }\n\n  if (domain) {\n    const domainLower = domain.toLowerCase();\n    const pageSlugLower = subscription.page.slug.toLowerCase();\n    const customDomainLower = subscription.page.customDomain?.toLowerCase();\n\n    if (domainLower !== pageSlugLower && domainLower !== customDomainLower) {\n      throw new Error(\"Subscription not found\");\n    }\n  }\n\n  if (subscription.unsubscribedAt) {\n    return;\n  }\n\n  await db\n    .update(pageSubscriber)\n    .set({ unsubscribedAt: new Date(), updatedAt: new Date() })\n    .where(eq(pageSubscriber.id, subscription.id))\n    .run();\n}\n"
  },
  {
    "path": "packages/subscriptions/src/test-preload.ts",
    "content": "/**\n * Preload file for bun test.\n * Runs before any test file is loaded, ensuring env vars are set\n * before module-level env validations (e.g. @openstatus/emails) fire.\n */\nprocess.env.RESEND_API_KEY = \"test-key\";\n"
  },
  {
    "path": "packages/subscriptions/src/types.ts",
    "content": "// Core types for the subscription system\n\nexport interface Subscription {\n  id: number;\n  pageId: number;\n  pageName: string; // For templates\n  pageSlug: string; // For management URLs\n  customDomain?: string | null; // For custom domain URLs\n  componentIds: number[]; // Empty = entire page\n\n  // Channel (only ONE identifier populated based on channelType)\n  channelType: \"email\" | \"webhook\";\n  email?: string; // For email channel\n  webhookUrl?: string; // For webhook channel\n  channelConfig?: string; // JSON string for headers, secrets, etc.\n\n  token?: string;\n  acceptedAt?: Date;\n  unsubscribedAt?: Date;\n}\n\n/**\n * Generic page update (status reports, maintenance, incidents, etc.)\n * Used for all types of notifications sent to subscribers\n */\nexport interface PageUpdate {\n  id: number;\n  pageId: number;\n  title: string;\n  status:\n    | \"investigating\"\n    | \"identified\"\n    | \"monitoring\"\n    | \"resolved\"\n    | \"maintenance\";\n  message: string;\n  pageComponentIds: number[]; // For subscription matching\n  pageComponents: string[]; // Component names for display\n  date: string; // can be single string or \"from - to\"\n}\n\n/**\n * Interface for notification channels (email, webhook, etc.)\n * Channels handle the actual delivery of notifications.\n */\nexport interface SubscriptionChannel {\n  id: string;\n\n  // Validate channel-specific config\n  validateConfig(config: unknown): Promise<{ valid: boolean; error?: string }>;\n\n  // Send verification if needed\n  sendVerification?(\n    subscription: Subscription,\n    verifyUrl: string,\n  ): Promise<void>;\n\n  // Send notifications to multiple subscribers (batched)\n  sendNotifications(\n    subscriptions: Subscription[],\n    pageUpdate: PageUpdate,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "packages/subscriptions/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/react-library.json\",\n  \"include\": [\".\", \"src\"],\n  \"exclude\": [\"dist\", \"build\", \"node_modules\"]\n}\n"
  },
  {
    "path": "packages/theme-store/README.md",
    "content": "# Community Themes\n\n**Help us build epic status page themes!**\n\nThis directory contains community-contributed themes for openstatus status pages. These themes allow users to customize the visual appearance of their status pages with different color schemes and design styles.\n\n## What are Community Themes?\n\nCommunity themes are predefined color schemes that users can apply to their status pages. Each theme includes:\n- **Light mode** colors for bright environments\n- **Dark mode** colors for low-light environments  \n- **Consistent design** that works across all status page components\n- **Accessibility** considerations for better readability\n\n## Themes Examples\n\n- **Openstatus** - The standard openstatus theme\n- **Openstatus (Rounded)** - The rounded openstatus theme (similar to the legacy page)\n- **Supabase** - Theme matching Supabase's brand colors\n- **GitHub (High Contrast)** - High contrast theme inspired by GitHub's design\n\n## Creating a New Theme\n\nWant to contribute a theme? \n\nWe only support themes via GitHub contributions to keep a certain version control. You can:\n\n- Configure your [themes.openstatus.dev](https://themes.openstatus.dev/?b=true) and copy the configuration\n- Directly test by running it locally\n\n### 1. Run the project\n\nStart by forking the openstatus repository to your GitHub account and run the command `dev:status-page` locally following [Getting Started](https://github.com/openstatusHQ/openstatus?tab=readme-ov-file#getting-started-) steps.\n\nOnce you run the `@openstatus/status-page` app, e.g. via the following command in the root directory:\n\n```bash\npnpm dev:status-page\n```\n\nYou can either access [localhost:3000](http://localhost:3000) to see the theme explorer or [localhost:3000/status](http://localhost:3000/status) to have a status page with seeded entries. On the bottom right is a configration button which has access to settings, including the community theme.\n\n> If something is off, our you think improvements can be made, feel free to raise an issue, join Discord, or DM us!\n\n### 2. Create Your Theme File\n\nCreate a new TypeScript file in this directory (e.g., `my-theme.ts`). You can copy an existing theme file as a starting template.\n\n### 3. Define Your Theme\n\nYour theme file should export a constant that matches the `Theme` interface:\n\n```typescript\nimport type { Theme } from \"./types\";\n\nexport const MY_THEME = {\n  id: \"my-theme\", // Unique identifier (kebab-case)\n  name: \"My Awesome Theme\", // Display name\n  author: { \n    name: \"@yourusername\", \n    url: \"https://github.com/yourusername\" // Add your personal website or a social link\n  },\n  light: {\n    // CSS custom properties for light mode\n    \"--background\": \"oklch(100% 0 0)\",\n    \"--foreground\": \"oklch(20% 0 0)\",\n    // ... more variables\n  },\n  dark: {\n    // CSS custom properties for dark mode\n    \"--background\": \"oklch(10% 0 0)\",\n    \"--foreground\": \"oklch(90% 0 0)\",\n    // ... more variables\n  },\n} as const satisfies Theme;\n```\n\nYou don't need to add every single css var from the `THEME_VAR_NAMES` list.\n\n### 4. Add to Theme Registry\n\nUpdate `index.ts` to include your theme in the `THEMES_LIST` array.\n\n```typescript\nconst THEMES_LIST = [\n  OPENSTATUS_THEME,\n  OPENSTATUS_ROUNDED_THEME,\n  //...\n  MY_THEME\n] satisfies Theme[];\n\n```\n\n### 5. Submit a Pull Request\n\nCreate a pull request with your theme for review.\n\n## Design Guidelines\n\n### Color System\n- Use **OKLCH color space** for better perceptual uniformity (but all css color specs are supported)\n- Ensure sufficient contrast ratios for accessibility\n- Test both light and dark modes thoroughly\n\n### Theme Requirements\n- ✅ **Consistent design** - All components should feel cohesive\n- ✅ **Both modes** - Must support light and dark variants\n- ✅ **Accessibility** - Meet WCAG contrast guidelines\n- ✅ **Unique ID** - Use descriptive, kebab-case identifiers\n- ❌ **No \"Christmas tree\"** - Avoid overly colorful or distracting designs\n\n### Available CSS Variables\nThemes can customize these CSS custom properties:\n- `--background`, `--foreground` - Base colors\n- `--primary`, `--secondary`, `--accent` - Brand colors  \n- `--success`, `--warning`, `--destructive` - Status colors\n- `--border`, `--input`, `--ring` - Interactive elements\n- `--muted`, `--muted-foreground` - Subtle text and backgrounds\n- And many more... (see `types.ts` for the complete list)\n\n## Testing Your Theme\n\nBefore submitting:\n1. Test your theme on a status page\n2. Verify both light and dark modes work correctly\n3. Check accessibility with browser dev tools\n4. Ensure all status indicators (operational, degraded, etc.) are clearly distinguishable\n\nTo test a theme on non-development environment, you can use the `sessionStorage.setItem(\"community-theme\", \"true\");`. It will open a floating button on the right left corner where you can choose between the themes and dark/light mode.\n\n## Questions?\n\nNeed help creating a theme? Open an issue or reach out to the openstatus community!"
  },
  {
    "path": "packages/theme-store/package.json",
    "content": "{\n  \"name\": \"@openstatus/theme-store\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {},\n  \"dependencies\": {},\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/theme-store/src/dracula.ts",
    "content": "// https://draculatheme.com/spec\n\nimport type { Theme } from \"./types\";\n\nexport const DRACULA_THEME = {\n  id: \"dracula\",\n  name: \"Dracula\",\n  author: { name: \"@thibaultleouay\", url: \"https://thibaultleouay.dev\" },\n  light: {\n    \"--background\": \"#FFFBEB\", // Background\n    \"--foreground\": \"#1F1F1F\", // Foreground\n    \"--border\": \"#6C664B\", // Current Line\n    \"--input\": \"var(--border)\", // Current Line\n\n    \"--primary\": \"var(--foreground)\",\n    \"--primary-foreground\": \"var(--background)\",\n    \"--secondary\": \"var(--muted)\",\n    \"--secondary-foreground\": \"var(--accent-foreground)\",\n    \"--muted\": \"#CFCFDE\", // Section\n    \"--muted-foreground\": \"#6C664B\", // NOTE: non standard color for improved readability\n    \"--accent\": \"var(--muted)\",\n    \"--accent-foreground\": \"var(--foreground)\",\n\n    \"--success\": \"#14710a\", // Green\n    \"--destructive\": \"#cb3a2a\", // Red\n    \"--warning\": \"#A34D14\", // Orange\n    \"--info\": \"#644AC9\", // Purple\n\n    \"--chart-1\": \"#A3144D\", // Pink\n    \"--chart-2\": \"#A34D14\", // Orange\n    \"--chart-3\": \"#846E15\", // Yellow\n    \"--chart-4\": \"#14710a\", // Green\n    \"--chart-5\": \"#036A96\", // Cyan\n\n    \"--popover-foreground\": \"var(--foreground)\",\n    \"--popover\": \"var(--background)\",\n    \"--card\": \"var(--background)\",\n    \"--card-foreground\": \"var(--foreground)\",\n  },\n  dark: {\n    \"--background\": \"#282a36\", // Background\n    \"--foreground\": \"#f8f8f2\", // Foreground\n    \"--border\": \"#6272A4\", // Current Line\n    \"--input\": \"var(--border)\", // Current Line\n\n    \"--primary\": \"var(--foreground)\",\n    \"--primary-foreground\": \"var(--background)\",\n    \"--secondary\": \"var(--muted)\",\n    \"--secondary-foreground\": \"var(--accent-foreground)\",\n    \"--muted\": \"#44475A\", // Section\n    \"--muted-foreground\": \"#A6ACCD\", // NOTE: non standard color for improved readability\n    \"--accent\": \"var(--muted)\",\n    \"--accent-foreground\": \"var(--foreground)\",\n\n    \"--success\": \"#50fa7b\", // Green\n    \"--destructive\": \"#ff5555\", // Red\n    \"--warning\": \"#ffb86c\", // Orange\n    \"--info\": \"#644AC9\", // Purple\n\n    \"--chart-1\": \"#ff79c6\", // Pink\n    \"--chart-2\": \"#ffb86c\", // Orange\n    \"--chart-3\": \"#f1fa8c\", // Yellow\n    \"--chart-4\": \"#50fa7b\", // Green\n    \"--chart-5\": \"#036A96\", // Cyan\n\n    \"--popover-foreground\": \"var(--foreground)\",\n    \"--popover\": \"var(--background)\",\n    \"--card\": \"var(--background)\",\n    \"--card-foreground\": \"var(--foreground)\",\n  },\n} as const satisfies Theme;\n"
  },
  {
    "path": "packages/theme-store/src/github.ts",
    "content": "import type { Theme } from \"./types\";\n\nexport const GITHUB_HIGH_CONTRAST_THEME = {\n  id: \"github-contrast\",\n  name: \"GitHub (High Contrast)\",\n  author: { name: \"@openstatus\", url: \"https://openstatus.dev\" },\n  light: {\n    \"--background\": \"oklch(100% 0 0)\",\n    \"--foreground\": \"oklch(24.29% 0.0045 247.86)\",\n    \"--border\": \"oklch(85.86% 0.0054 251.18)\",\n    \"--input\": \"oklch(85.86% 0.0054 251.18)\",\n\n    \"--primary\": \"oklch(60.81% 0.1567 142.5)\",\n    \"--primary-foreground\": \"oklch(24.29% 0.0045 247.86)\",\n    \"--muted\": \"oklch(97.86% 0.0019 247.86)\",\n    \"--muted-foreground\": \"oklch(40.78% 0.0056 247.86)\",\n    \"--secondary\": \"oklch(97.86% 0.0019 247.86)\",\n    \"--secondary-foreground\": \"oklch(24.29% 0.0045 247.86)\",\n    \"--accent\": \"oklch(97.86% 0.0019 247.86)\",\n    \"--accent-foreground\": \"oklch(24.29% 0.0045 247.86)\",\n\n    \"--success\": \"oklch(60.81% 0.1567 142.5)\",\n    \"--destructive\": \"oklch(58.79% 0.1577 22.18)\",\n    \"--warning\": \"oklch(81.84% 0.1328 85.87)\",\n    \"--info\": \"oklch(45.2% 0.1445 252.03)\",\n  },\n  dark: {\n    \"--background\": \"oklch(10.39% 0.0194 248.34)\",\n    \"--foreground\": \"oklch(100% 0 0)\",\n    \"--border\": \"oklch(58.41% 0.011 252.87)\",\n    \"--input\": \"oklch(58.41% 0.011 252.87)\",\n\n    \"--primary\": \"oklch(54.34% 0.1634 145.98)\",\n    \"--primary-foreground\": \"oklch(100% 0 0)\",\n    \"--muted\": \"oklch(33.39% 0.0223 256.4)\",\n    \"--muted-foreground\": \"oklch(79.7% 0.0169 262.74)\",\n    \"--secondary\": \"oklch(33.39% 0.0223 256.4)\",\n    \"--secondary-foreground\": \"oklch(100% 0 0)\",\n    \"--accent\": \"oklch(33.39% 0.0223 256.4)\",\n    \"--accent-foreground\": \"oklch(100% 0 0)\",\n\n    \"--success\": \"oklch(54.34% 0.1634 145.98)\",\n    \"--destructive\": \"oklch(47.1% 0.1909 25.95)\",\n    \"--warning\": \"oklch(81.84% 0.1328 85.87)\",\n    \"--info\": \"oklch(46.96% 0.2957 264.51)\",\n  },\n} as const satisfies Theme;\n"
  },
  {
    "path": "packages/theme-store/src/index.ts",
    "content": "export * from \"./types\";\nimport { DRACULA_THEME } from \"./dracula\";\nimport { GITHUB_HIGH_CONTRAST_THEME } from \"./github\";\nimport { OPENSTATUS_ROUNDED_THEME, OPENSTATUS_THEME } from \"./openstatus\";\nimport { SUPABASE_THEME } from \"./supabase\";\nimport type { Theme, ThemeDefinition, ThemeMap } from \"./types\";\nimport { assertUniqueThemeIds } from \"./utils\";\n// Please keep the themes ordered :)\nconst THEMES_LIST = [\n  OPENSTATUS_THEME,\n  OPENSTATUS_ROUNDED_THEME,\n  SUPABASE_THEME,\n  GITHUB_HIGH_CONTRAST_THEME,\n  DRACULA_THEME,\n] satisfies Theme[];\n\n// NOTE: runtime validation to ensure that the theme IDs are unique\nassertUniqueThemeIds(THEMES_LIST);\n\nexport const THEMES = THEMES_LIST.reduce<ThemeMap>((acc, theme) => {\n  acc[theme.id as keyof ThemeMap] = theme;\n  return acc;\n}, {} as ThemeMap);\n\nexport const THEME_KEYS = THEMES_LIST.map((theme) => theme.id);\nexport type ThemeKey = (typeof THEME_KEYS)[number];\n\nexport function generateThemeStyles(\n  themeKey?: string,\n  overrides?: Partial<ThemeDefinition>,\n) {\n  let theme = themeKey ? THEMES[themeKey] : undefined;\n\n  if (!theme) {\n    // NOTE: fallback to openstatus theme if no theme is found\n    theme = OPENSTATUS_THEME;\n  }\n\n  const lightVars = Object.entries({ ...theme.light, ...overrides?.light })\n    .map(([key, value]) => `${key}: ${value};`)\n    .join(\"\\n    \");\n  const darkVars = Object.entries({ ...theme.dark, ...overrides?.dark })\n    .map(([key, value]) => `${key}: ${value};`)\n    .join(\"\\n    \");\n\n  return `\n      :root {\n        ${lightVars}\n      }\n      .dark {\n        ${darkVars}\n      }\n    `;\n}\n"
  },
  {
    "path": "packages/theme-store/src/openstatus.ts",
    "content": "import type { Theme } from \"./types\";\n\nexport const OPENSTATUS_THEME = {\n  id: \"default\" as const,\n  name: \"Openstatus\",\n  author: { name: \"@openstatus\", url: \"https://openstatus.dev\" },\n  light: {\n    \"--background\": \"oklch(100% 0 0)\",\n    \"--foreground\": \"oklch(14.5% 0 0)\",\n    \"--border\": \"oklch(92.2% 0 0)\",\n    \"--input\": \"oklch(92.2% 0 0)\",\n\n    \"--primary\": \"oklch(20.5% 0 0)\",\n    \"--primary-foreground\": \"oklch(98.5% 0 0)\",\n    \"--secondary\": \"oklch(97% 0 0)\",\n    \"--secondary-foreground\": \"oklch(20.5% 0 0)\",\n    \"--muted\": \"oklch(97% 0 0)\",\n    \"--muted-foreground\": \"oklch(55.6% 0 0)\",\n    \"--accent\": \"oklch(97% 0 0)\",\n    \"--accent-foreground\": \"oklch(20.5% 0 0)\",\n\n    \"--success\": \"oklch(72% 0.19 150)\",\n    \"--destructive\": \"oklch(57.7% 0.245 27.325)\",\n    \"--warning\": \"oklch(77% 0.16 70)\",\n    \"--info\": \"oklch(62% 0.19 260)\",\n  },\n  dark: {\n    \"--background\": \"oklch(14.5% 0 0)\",\n    \"--foreground\": \"oklch(98.5% 0 0)\",\n    \"--border\": \"oklch(100% 0 0 / 10%)\",\n    \"--input\": \"oklch(100% 0 0 / 15%)\",\n\n    \"--primary\": \"oklch(92.2% 0 0)\",\n    \"--primary-foreground\": \"oklch(20.5% 0 0)\",\n    \"--secondary\": \"oklch(26.9% 0 0)\",\n    \"--secondary-foreground\": \"oklch(98.5% 0 0)\",\n    \"--muted\": \"oklch(26.9% 0 0)\",\n    \"--muted-foreground\": \"oklch(70.8% 0 0)\",\n    \"--accent\": \"oklch(26.9% 0 0)\",\n    \"--accent-foreground\": \"oklch(98.5% 0 0)\",\n\n    \"--success\": \"oklch(72% 0.19 150)\",\n    \"--destructive\": \"oklch(70.4% 0.191 22.216)\",\n    \"--warning\": \"oklch(77% 0.16 70)\",\n    \"--info\": \"oklch(62% 0.19 260)\",\n  },\n} as const satisfies Theme;\n\nexport const OPENSTATUS_ROUNDED_THEME = {\n  id: \"default-rounded\" as const,\n  name: \"Openstatus (Rounded)\",\n  author: OPENSTATUS_THEME.author,\n  light: {\n    ...OPENSTATUS_THEME.light,\n    \"--radius\": \"0.625rem\",\n  },\n  dark: {\n    ...OPENSTATUS_THEME.dark,\n    \"--radius\": \"0.625rem\",\n  },\n} as const satisfies Theme;\n"
  },
  {
    "path": "packages/theme-store/src/supabase.ts",
    "content": "import type { Theme } from \"./types\";\n\nexport const SUPABASE_THEME = {\n  id: \"supabase\",\n  name: \"Supabase\",\n  author: { name: \"@supabase\", url: \"https://supabase.com/\" },\n  light: {\n    \"--background\": \"oklch(99.11% 0 0)\",\n    \"--foreground\": \"oklch(20.46% 0 0)\",\n    \"--border\": \"oklch(90.37% 0 0)\",\n    \"--input\": \"oklch(90.37% 0 0)\",\n\n    \"--primary\": \"oklch(76.26% 0.154 159.27)\",\n    \"--primary-foreground\": \"oklch(20.46% 0 0)\",\n    \"--muted\": \"oklch(97.61% 0 0)\",\n    \"--muted-foreground\": \"oklch(54.52% 0 0)\",\n    \"--secondary\": \"oklch(97.61% 0 0)\",\n    \"--secondary-foreground\": \"oklch(20.46% 0 0)\",\n    \"--accent\": \"oklch(97.61% 0 0)\",\n    \"--accent-foreground\": \"oklch(20.46% 0 0)\",\n\n    \"--success\": \"oklch(76.26% 0.154 159.27)\",\n    \"--destructive\": \"oklch(62.71% 0.1936 33.34)\",\n    \"--warning\": \"oklch(81.69% 0.1639 75.84)\",\n    \"--info\": \"oklch(61.26% 0.218 283.85)\",\n  },\n  dark: {\n    \"--background\": \"oklch(18.22% 0 0)\",\n    \"--foreground\": \"oklch(98.51% 0 0)\",\n    \"--border\": \"oklch(30.12% 0 0)\",\n    \"--input\": \"oklch(30.12% 0 0)\",\n\n    \"--primary\": \"oklch(68.56% 0.1558 158.13)\",\n    \"--primary-foreground\": \"oklch(18.22% 0 0)\",\n    \"--muted\": \"oklch(26.03% 0 0)\",\n    \"--muted-foreground\": \"oklch(63.01% 0 0)\",\n    \"--secondary\": \"oklch(26.03% 0 0)\",\n    \"--secondary-foreground\": \"oklch(98.51% 0 0)\",\n    \"--accent\": \"oklch(26.03% 0 0)\",\n    \"--accent-foreground\": \"oklch(98.51% 0 0)\",\n\n    \"--success\": \"oklch(68.56% 0.1558 158.13)\",\n    \"--destructive\": \"oklch(62.71% 0.1936 33.34)\",\n    \"--warning\": \"oklch(70.84% 0.1523 71.24)\",\n    \"--info\": \"oklch(61.26% 0.218 283.85)\",\n  },\n} as const satisfies Theme;\n"
  },
  {
    "path": "packages/theme-store/src/types.ts",
    "content": "export const THEME_VAR_NAMES = [\n  // NOTE: default shadcn/ui colors\n  \"--radius\",\n  \"--background\",\n  \"--foreground\",\n  \"--card\",\n  \"--card-foreground\",\n  \"--popover\",\n  \"--popover-foreground\",\n  \"--primary\",\n  \"--primary-foreground\",\n  \"--secondary\",\n  \"--secondary-foreground\",\n  \"--muted\",\n  \"--muted-foreground\",\n  \"--accent\",\n  \"--accent-foreground\",\n  \"--border\",\n  \"--input\",\n  \"--ring\",\n  \"--destructive\", // red, outage/error status\n  // NOTE: the following colors are used for the public monitors UI to differentiate the percentiles of the response times\n  \"--chart-1\",\n  \"--chart-2\",\n  \"--chart-3\",\n  \"--chart-4\",\n  \"--chart-5\",\n\n  // NOTE: the following colors are not part of shadcn/ui, but are essential part of the status page\n  \"--success\", // green, operational status\n  \"--warning\", // yellow, degraded status\n  \"--info\", // blue, monitoring status\n\n  // NOTE: the following colors are used for the public monitors UI to differentiate the different regions\n  // It is not required to add them to your custom theme, but you can if you want to.\n  // DEFAULT: https://github.com/openstatusHQ/openstatus/blob/main/apps/status-page/src/app/globals.css#L98\n  \"--rainbow-1\",\n  \"--rainbow-2\",\n  \"--rainbow-3\",\n  \"--rainbow-4\",\n  \"--rainbow-5\",\n  \"--rainbow-6\",\n  \"--rainbow-7\",\n  \"--rainbow-8\",\n  \"--rainbow-9\",\n  \"--rainbow-10\",\n  \"--rainbow-11\",\n  \"--rainbow-12\",\n  \"--rainbow-13\",\n  \"--rainbow-14\",\n  \"--rainbow-15\",\n  \"--rainbow-16\",\n  \"--rainbow-17\",\n] as const;\n\nexport type ThemeVarName = (typeof THEME_VAR_NAMES)[number];\nexport type ThemeVars = Partial<Record<ThemeVarName, string>>;\nexport type ThemeMode = \"light\" | \"dark\";\n\nexport interface ThemeDefinition {\n  light: ThemeVars;\n  dark: ThemeVars;\n}\n\nexport interface ThemeInfo {\n  id: string;\n  name: string;\n  author: { name: string; url: string };\n}\n\nexport type Theme = ThemeInfo & ThemeDefinition;\nexport type ThemeMap = Record<string, Theme>;\n"
  },
  {
    "path": "packages/theme-store/src/utils.ts",
    "content": "import type { Theme } from \"./types\";\n\nexport function assertUniqueThemeIds(themes: Theme[]) {\n  const seen = new Set<string>();\n  for (const theme of themes) {\n    if (seen.has(theme.id)) {\n      throw new Error(\n        `Duplicate theme ID detected: \"${theme.id}\" in theme \"${theme.name}\". All theme IDs must be unique.`,\n      );\n    }\n    seen.add(theme.id);\n  }\n}\n"
  },
  {
    "path": "packages/theme-store/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\"]\n}\n"
  },
  {
    "path": "packages/tinybird/README.md",
    "content": "### A guide on how to migrate your tinybird datasource\n\n> What to do when you want to add/remove/update a column in your `datasource`.\n\nThe `_migration` folder includes:\n\n- `ping_response__v4.datasource` which represents the `VERSION 4` of our\n  datasource\n- `ping_response.datasource` which has the upgraded schema and a new `VERSION 5`\n- `tb_backfill_populate.pipe` will fill the datasource with all the data until a\n  given timestamp\n- `tb_materialized_until_change_ingest.pipe` will fill the data from a given\n  timestamp\n\n```\ntb push _migration/ping_response.datasource\ntb push _migration/tb_materialized_until_change_ingest.pipe\n# after the given ts, it is time to run the backfill populate\ntb push _migration/tb_backfill_populate.pipe --populate --wait\n# after populate ends, it is time to remove the pipe\ntb pipe rm tb_backfill_populate  --yes\n```\n\nCheck if all the rows have been migrated:\n\n```\ntb pipe _migration/tb_datasource_union.pipe\n# after checking the result of the pipe\ntb pipe rm tb_datasource_union.pipe --yes\n```\n\n---\n\nLink to the [issue](https://github.com/openstatusHQ/openstatus/issues/278) from\nGonzalo as reference.\n\n\n<!-- FIXME: more content -->\n\n```bash\npython3 -m venv .venv\nsource .venv/bin/activate\npip install tinybird-cli\ntb auth -i\n```\n\n```bash\ntb pull\ntb push aggregate_*.pipe --populate\ntb push endpoint_*.pipe\n...\n```"
  },
  {
    "path": "packages/tinybird/datasources/audit_log__v0.datasource",
    "content": "VERSION 0\n\nSCHEMA >\n    `action` String `json:$.action`,\n    `actor` String `json:$.actor`,\n    `id` String `json:$.id`,\n    `targets` Nullable(String) `json:$.targets`,\n    `metadata` Nullable(String) `json:$.metadata`,\n    `timestamp` Int64 `json:$.timestamp`,\n    `version` Int16 `json:$.version`\n\nENGINE \"MergeTree\"\nENGINE_SORTING_KEY \"id, timestamp, action\"\n"
  },
  {
    "path": "packages/tinybird/datasources/check_response.datasource",
    "content": "VERSION 0\n\nSCHEMA >\n    `requestId` Int64 `json:$.requestId`,\n    `workspaceId` Int64 `json:$.workspaceId`,\n    `latency` Int64 `json:$.latency`,\n    `region` String `json:$.region`,\n    `time` DateTime `json:$.time`,\n    `headers` String `json:$.headers`,\n    `timing_connectDone` Int64 `json:$.timing.connectDone`,\n    `timing_connectStart` Int64 `json:$.timing.connectStart`,\n    `timing_dnsDone` Int64 `json:$.timing.dnsDone`,\n    `timing_dnsStart` Int64 `json:$.timing.dnsStart`,\n    `timing_firstByteDone` Int64 `json:$.timing.firstByteDone`,\n    `timing_firstByteStart` Int64 `json:$.timing.firstByteStart`,\n    `timing_tlsHandshakeDone` Int64 `json:$.timing.tlsHandshakeDone`,\n    `timing_tlsHandshakeStart` Int64 `json:$.timing.tlsHandshakeStart`,\n    `timing_transferDone` Int64 `json:$.timing.transferDone`,\n    `timing_transferStart` Int64 `json:$.timing.transferStart`\n\nENGINE \"MergeTree\"\nENGINE_SORTING_KEY \"workspaceId, requestId, time\"\nENGINE_TTL \"\"\nENGINE_PARTITION_KEY \"\""
  },
  {
    "path": "packages/tinybird/datasources/check_response_http.datasource",
    "content": "VERSION 0\n\nSCHEMA >\n    `body` String `json:$.body`,\n    `headers` String `json:$.headers`,\n    `latency` Int32 `json:$.latency`,\n    `region` String `json:$.region`,\n    `requestId` Int16 `json:$.requestId`,\n    `statusCode` Int16 `json:$.statusCode`,\n    `timestamp` Int64 `json:$.timestamp`,\n    `timing` String `json:$.timing`,\n    `workspaceId` Int16 `json:$.workspaceId`\n\nENGINE \"MergeTree\"\nENGINE_SORTING_KEY \"timestamp, timing, workspaceId\"\n"
  },
  {
    "path": "packages/tinybird/datasources/dns_response__v0.datasource",
    "content": "\nSCHEMA >\n    `assertions` String `json:$.assertions`,\n    `cronTimestamp` Int64 `json:$.cronTimestamp`,\n    `error` Int16 `json:$.error`,\n    `errorMessage` String `json:$.errorMessage`,\n    `id` String `json:$.id`,\n    `latency` Int16 `json:$.latency`,\n    `monitorId` Int16 `json:$.monitorId`,\n    `records` String `json:$.records`,\n    `region` String `json:$.region`,\n    `requestStatus` String `json:$.requestStatus`,\n    `timestamp` Int64 `json:$.timestamp`,\n    `trigger` String `json:$.trigger`,\n    `uri` String `json:$.uri`,\n    `workspaceId` Int16 `json:$.workspaceId`\n\nENGINE \"MergeTree\"\nENGINE_SORTING_KEY \"trigger, uri, workspaceId\"\n"
  },
  {
    "path": "packages/tinybird/datasources/external_status.datasource",
    "content": "\nSCHEMA >\n    `description` String `json:$.description`,\n    `fetched_at` Int64 `json:$.fetched_at`,\n    `indicator` String `json:$.indicator`,\n    `name` String `json:$.name`,\n    `time_zone` String `json:$.time_zone`,\n    `updated_at` Int64 `json:$.updated_at`,\n    `url` String `json:$.url`\n\nENGINE \"MergeTree\"\nENGINE_SORTING_KEY \"description, time_zone, updated_at, url\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__dns_status_45d__v0.datasource",
    "content": "# Data Source created from Pipe 'aggregate__dns_status_45d__v1'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `monitorId` Int16,\n    `count` AggregateFunction(count),\n    `success` AggregateFunction(count, Nullable(UInt8)),\n    `error` AggregateFunction(count, Nullable(UInt8)),\n    `degraded` AggregateFunction(count, Nullable(UInt8))\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(46)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_14d.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__http_14d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int8,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` String,\n    `workspaceId` String\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_14d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__http_14d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int8,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` String,\n    `workspaceId` String\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_14d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_14d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `id` Nullable(String),\n    `latency` Int64,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` String,\n    `requestStatus` Nullable(String),\n    `timing` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_1d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__http_1d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int8,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` String,\n    `workspaceId` String\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(1)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_1d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_1d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `id` Nullable(String),\n    `latency` Int64,\n    `requestStatus` Nullable(String),\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` String,\n    `timing` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(1)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_30d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__http_30d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int8,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` String,\n    `workspaceId` String\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_30d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_30d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `id` Nullable(String),\n    `latency` Int64,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` String,\n    `requestStatus` Nullable(String),\n    `timing` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_7d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__http_7d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int8,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` String,\n    `workspaceId` String\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(7)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_7d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_7d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `id` Nullable(String),\n    `latency` Int64,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` String,\n    `requestStatus` Nullable(String),\n    `timing` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(7)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_full_14d__v0.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_full_14d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `monitorId` String,\n    `region` LowCardinality(String),\n    `statusCode` Nullable(Int16),\n    `error` Int8,\n    `timestamp` Int64,\n    `url` String,\n    `workspaceId` String,\n    `cronTimestamp` Int64,\n    `message` Nullable(String),\n    `timing` Nullable(String),\n    `headers` Nullable(String),\n    `assertions` Nullable(String),\n    `body` Nullable(String),\n    `trigger` Nullable(String),\n    `id` Nullable(String),\n    `requestStatus` Nullable(String),\n    `method` Nullable(String)\n\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_full_30d__v0.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_full_30d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `monitorId` String,\n    `region` LowCardinality(String),\n    `statusCode` Nullable(Int16),\n    `error` Int8,\n    `timestamp` Int64,\n    `url` String,\n    `workspaceId` String,\n    `cronTimestamp` Int64,\n    `message` Nullable(String),\n    `timing` Nullable(String),\n    `headers` Nullable(String),\n    `assertions` Nullable(String),\n    `body` Nullable(String),\n    `trigger` Nullable(String),\n    `id` Nullable(String),\n    `requestStatus` Nullable(String),\n    `method` Nullable(String)\n\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_status_14d__v0.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_status_14d__v0'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `monitorId` String,\n    `total` AggregateFunction(count),\n    `success` AggregateFunction(count, Nullable(UInt8)),\n    `degraded` AggregateFunction(count, Nullable(UInt8)),\n    `error` AggregateFunction(count, Nullable(UInt8))\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_status_45d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__http_status_45d__v0'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `monitorId` String,\n    `count` AggregateFunction(count),\n    `ok` AggregateFunction(count, Nullable(UInt8))\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(45)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_status_45d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_status_45d__v1'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `monitorId` String,\n    `count` AggregateFunction(count),\n    `success` AggregateFunction(count, Nullable(UInt8)),\n    `error` AggregateFunction(count, Nullable(UInt8)),\n    `degraded` AggregateFunction(count, Nullable(UInt8))\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(46)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_status_7d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__http_status_7d__v0'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `monitorId` String,\n    `count` AggregateFunction(count),\n    `ok` AggregateFunction(count, Nullable(UInt8))\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(7)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_timing_phases_14d.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_timing_phases_14d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `monitorId` String,\n    `workspaceId` String,\n    `requestStatus` Nullable(String),\n    `dns` Nullable(Int64),\n    `connect` Nullable(Int64),\n    `tls` Nullable(Int64),\n    `firstByte` Nullable(Int64),\n    `transfer` Nullable(Int64)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_timing_phases_14d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_timing_phases_14d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `region` LowCardinality(String),\n    `trigger` Nullable(String),\n    `statusCode` Nullable(Int16),\n    `monitorId` String,\n    `workspaceId` String,\n    `requestStatus` Nullable(String),\n    `dns` Nullable(Int64),\n    `connect` Nullable(Int64),\n    `tls` Nullable(Int64),\n    `firstByte` Nullable(Int64),\n    `transfer` Nullable(Int64)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_uptime_30d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_uptime_30d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `region` LowCardinality(String),\n    `requestStatus` Nullable(String),\n    `monitorId` String,\n    `workspaceId` String\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_uptime_7d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_uptime_7d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `region` LowCardinality(String),\n    `requestStatus` Nullable(String),\n    `monitorId` String,\n    `workspaceId` String\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(7)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__http_workspace_30d__v0.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_workspace_30d__v0'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `workspaceId` String,\n    `trigger` String,\n    `count_state` AggregateFunction(count)\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"workspaceId, time, trigger\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_14d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__tcp_14d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int16,\n    `region` String,\n    `trigger` Nullable(String),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` Int32,\n    `workspaceId` Int32\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_14d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_14d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int16,\n    `region` String,\n    `trigger` Nullable(String),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` Int32,\n    `requestStatus` Nullable(String),\n    `id` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_1d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__tcp_1d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int16,\n    `region` String,\n    `trigger` Nullable(String),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` Int32,\n    `workspaceId` Int32\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(1)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_1d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_1d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int16,\n    `region` String,\n    `trigger` Nullable(String),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` Int32,\n    `requestStatus` Nullable(String),\n    `id` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(1)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_30d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__tcp_30d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int16,\n    `region` String,\n    `trigger` Nullable(String),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` Int32,\n    `workspaceId` Int32\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_30d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_30d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int16,\n    `region` String,\n    `trigger` Nullable(String),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` Int32,\n    `requestStatus` Nullable(String),\n    `id` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_7d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__tcp_7d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int16,\n    `region` String,\n    `trigger` Nullable(String),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` Int32,\n    `workspaceId` Int32\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(7)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_7d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_7d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `latency` Int64,\n    `error` Int16,\n    `region` String,\n    `trigger` Nullable(String),\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `monitorId` Int32,\n    `requestStatus` Nullable(String),\n    `id` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(7)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_full_14d__v0.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_full_14d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `monitorId` Int32,\n    `region` String,\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `timing` String,\n    `workspaceId` Int32,\n    `latency` Int64,\n    `errorMessage` Nullable(String),\n    `error` Int16,\n    `trigger` Nullable(String),\n    `uri` Nullable(String),\n    `id` Nullable(String),\n    `requestStatus` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_full_30d__v0.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_full_30d__v0'\n\nSCHEMA >\n    `time` DateTime,\n    `monitorId` Int32,\n    `region` String,\n    `timestamp` Int64,\n    `cronTimestamp` Int64,\n    `timing` String,\n    `workspaceId` Int32,\n    `latency` Int64,\n    `errorMessage` Nullable(String),\n    `error` Int16,\n    `trigger` Nullable(String),\n    `uri` Nullable(String),\n    `id` Nullable(String),\n    `requestStatus` Nullable(String)\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_status_45d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__tcp_status_45d__v0'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `monitorId` Int32,\n    `count` AggregateFunction(count),\n    `ok` AggregateFunction(count, Nullable(UInt8))\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(45)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_status_45d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_status_45d__v1'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `monitorId` Int32,\n    `count` AggregateFunction(count),\n    `success` AggregateFunction(count, Nullable(UInt8)),\n    `error` AggregateFunction(count, Nullable(UInt8)),\n    `degraded` AggregateFunction(count, Nullable(UInt8))\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(46)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_status_7d__v0.datasource",
    "content": "VERSION 0\n# Data Source created from Pipe 'aggregate__tcp_status_7d__v0'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `monitorId` Int32,\n    `count` AggregateFunction(count),\n    `ok` AggregateFunction(count, Nullable(UInt8))\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(7)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_uptime_30d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_uptime_30d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `region` String,\n    `requestStatus` Nullable(String),\n    `monitorId` Int32,\n    `workspaceId` Int32\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_uptime_7d__v1.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_uptime_7d__v1'\n\nSCHEMA >\n    `time` DateTime,\n    `region` String,\n    `requestStatus` Nullable(String),\n    `monitorId` Int32,\n    `workspaceId` Int32\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(7)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv__tcp_workspace_30d__v0.datasource",
    "content": "# Data Source created from Pipe 'aggregate__tcp_workspace_30d__v0'\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `workspaceId` Int32,\n    `trigger` String,\n    `count_state` AggregateFunction(count)\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"workspaceId, time, trigger\"\nENGINE_TTL \"time + toIntervalDay(30)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/mv_http_status_14d.datasource",
    "content": "# Data Source created from Pipe 'aggregate__http_status_14d__v0'\nVERSION 0\n\nSCHEMA >\n    `time` DateTime('UTC'),\n    `monitorId` String,\n    `total` AggregateFunction(count),\n    `success` AggregateFunction(count, Nullable(UInt8)),\n    `degraded` AggregateFunction(count, Nullable(UInt8)),\n    `error` AggregateFunction(count, Nullable(UInt8))\n\nENGINE \"AggregatingMergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(time)\"\nENGINE_SORTING_KEY \"monitorId, time\"\nENGINE_TTL \"time + toIntervalDay(14)\"\n"
  },
  {
    "path": "packages/tinybird/datasources/ping_response__v8.datasource",
    "content": "\nSCHEMA >\n    `latency` Int64 `json:$.latency`,\n    `monitorId` String `json:$.monitorId`,\n    `region` LowCardinality(String) `json:$.region`,\n    `statusCode` Nullable(Int16) `json:$.statusCode`,\n    `error` Int8 `json:$.error`,\n    `timestamp` Int64 `json:$.timestamp`,\n    `url` String `json:$.url`,\n    `workspaceId` String `json:$.workspaceId`,\n    `cronTimestamp` Int64 `json:$.cronTimestamp`,\n    `message` Nullable(String) `json:$.message`,\n    `timing` Nullable(String) `json:$.timing`,\n    `headers` Nullable(String) `json:$.headers`,\n    `assertions` Nullable(String) `json:$.assertions`,\n    `body` Nullable(String) `json:$.body`,\n    `trigger` Nullable(String) `json:$.trigger`,\n    `id` Nullable(String) `json:$.id`,\n    `requestStatus` Nullable(String) `json:$.requestStatus`,\n    `method` String `json:$.method`\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(fromUnixTimestamp64Milli(cronTimestamp))\"\nENGINE_SORTING_KEY \"monitorId, cronTimestamp\"\n"
  },
  {
    "path": "packages/tinybird/datasources/tcp_response.datasource",
    "content": "VERSION 0\n\nSCHEMA >\n    `monitorId` Int32 `json:$.monitorId`,\n    `region` String `json:$.region`,\n    `timestamp` Int64 `json:$.timestamp`,\n    `cronTimestamp` Int64 `json:$.timestamp`,\n    `timing` String `json:$.timing`,\n    `workspaceId` Int32 `json:$.workspaceId`,\n    `latency` Int64 `json:$.latency`,\n    `errorMessage` Nullable(String) `json:$.errorMessage`,\n    `error` Int16 `json:$.error`,\n    `trigger` Nullable(String) `json:$.trigger`,\n    `uri` Nullable(String) `json:$.uri`\n\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(fromUnixTimestamp64Milli(timestamp))\"\nENGINE_SORTING_KEY \"monitorId, workspaceId\"\n"
  },
  {
    "path": "packages/tinybird/datasources/tcp_response__v0.datasource",
    "content": "\nSCHEMA >\n    `monitorId` Int32 `json:$.monitorId`,\n    `region` String `json:$.region`,\n    `timestamp` Int64 `json:$.timestamp`,\n    `cronTimestamp` Int64 `json:$.timestamp`,\n    `timing` String `json:$.timing`,\n    `workspaceId` Int32 `json:$.workspaceId`,\n    `latency` Int64 `json:$.latency`,\n    `errorMessage` Nullable(String) `json:$.errorMessage`,\n    `error` Int16 `json:$.error`,\n    `trigger` Nullable(String) `json:$.trigger`,\n    `uri` Nullable(String) `json:$.uri`,\n    `id` Nullable(String) `json:$.id`,\n    `requestStatus` Nullable(String) `json:$.requestStatus`\n\nENGINE \"MergeTree\"\nENGINE_PARTITION_KEY \"toYYYYMM(fromUnixTimestamp64Milli(timestamp))\"\nENGINE_SORTING_KEY \"monitorId, workspaceId\"\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__audit_log.pipe",
    "content": "VERSION 1\n\nTAGS endpoint\n\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT action, id, metadata, timestamp\n    FROM audit_log__v0\n    WHERE\n        id = {{String(monitorId, 'monitor:1', required=True)}}\n        AND timestamp > toUnixTimestamp(now() - INTERVAL {{ Int64(interval, 30) }} day) * 1000\n    ORDER BY timestamp DESC\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__audit_log__v1.pipe",
    "content": "VERSION 1\n\nTAGS endpoint\n\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT action, id, metadata, timestamp\n    FROM audit_log__v0\n    WHERE\n        id = {{String(monitorId, 'monitor:1', required=True)}}\n        AND timestamp > toUnixTimestamp(now() - INTERVAL {{ Int64(interval, 30) }} day) * 1000\n    ORDER BY timestamp DESC\n\nTYPE ENDPOINT\n\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_get_14d__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT *\n        FROM dns_response__v0\n        WHERE\n        monitorId = {{ String(monitorId, '7417', required=True) }}\n        AND id = {{ String(id, '', required=True) }}\n        ORDER BY timestamp DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_list_14d__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT *\n    FROM dns_response__v0\n    WHERE\n    monitorId = {{ String(monitorId, '7417', required=True) }}\n        {% if defined(fromDate) %} AND timestamp >= toInt64({{ String(fromDate) }}) {% end %}\n        {% if defined(toDate) %} AND timestamp <= toInt64({{ String(toDate) }}) {% end %}\n    ORDER BY timestamp DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_metrics_14d__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT\n            round(quantile(0.50)(latency)) as p50Latency,\n            round(quantile(0.75)(latency)) as p75Latency,\n            round(quantile(0.90)(latency)) as p90Latency,\n            round(quantile(0.95)(latency)) as p95Latency,\n            round(quantile(0.99)(latency)) as p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            max(cronTimestamp) AS lastTimestamp\n        FROM dns_response__v0\n        WHERE\n            monitorId = {{ String(monitorId, '7417', required=True) }}\n            AND timestamp >= toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 14 DAY, 3))\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n        UNION ALL\n        SELECT\n            round(quantile(0.50)(latency)) AS p50Latency,\n            round(quantile(0.75)(latency)) AS p75Latency,\n            round(quantile(0.90)(latency)) AS p90Latency,\n            round(quantile(0.95)(latency)) AS p95Latency,\n            round(quantile(0.99)(latency)) AS p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n        FROM dns_response__v0\n        WHERE\n            monitorId = {{ String(monitorId, '7417', required=True) }}\n            AND timestamp >= toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 28 DAY, 3))\n            AND timestamp < toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 14 DAY, 3))\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_metrics_1d__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT\n            round(quantile(0.50)(latency)) as p50Latency,\n            round(quantile(0.75)(latency)) as p75Latency,\n            round(quantile(0.90)(latency)) as p90Latency,\n            round(quantile(0.95)(latency)) as p95Latency,\n            round(quantile(0.99)(latency)) as p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            max(cronTimestamp) AS lastTimestamp\n        FROM dns_response__v0\n        WHERE\n            monitorId = {{ String(monitorId, '7417', required=True) }}\n            AND timestamp >= toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 1 DAY, 3))\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n        UNION ALL\n        SELECT\n            round(quantile(0.50)(latency)) AS p50Latency,\n            round(quantile(0.75)(latency)) AS p75Latency,\n            round(quantile(0.90)(latency)) AS p90Latency,\n            round(quantile(0.95)(latency)) AS p95Latency,\n            round(quantile(0.99)(latency)) AS p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n        FROM dns_response__v0\n        WHERE\n            monitorId = {{ String(monitorId, '7417', required=True) }}\n            AND timestamp >= toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 2 DAY, 3))\n            AND timestamp < toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 1 DAY, 3))\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_metrics_7d__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT\n            round(quantile(0.50)(latency)) as p50Latency,\n            round(quantile(0.75)(latency)) as p75Latency,\n            round(quantile(0.90)(latency)) as p90Latency,\n            round(quantile(0.95)(latency)) as p95Latency,\n            round(quantile(0.99)(latency)) as p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            max(cronTimestamp) AS lastTimestamp\n        FROM dns_response__v0\n        WHERE\n            monitorId = {{ String(monitorId, '7417', required=True) }}\n            AND timestamp >= toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 7 DAY, 3))\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n        UNION ALL\n        SELECT\n            round(quantile(0.50)(latency)) AS p50Latency,\n            round(quantile(0.75)(latency)) AS p75Latency,\n            round(quantile(0.90)(latency)) AS p90Latency,\n            round(quantile(0.95)(latency)) AS p95Latency,\n            round(quantile(0.99)(latency)) AS p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n        FROM dns_response__v0\n        WHERE\n            monitorId = {{ String(monitorId, '7417', required=True) }}\n            AND timestamp >= toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 14 DAY, 3))\n            AND timestamp < toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 7 DAY, 3))\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_metrics_latency_1d_multi__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        monitorId,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM dns_response__v0\n    WHERE\n        monitorId IN {{ Array(monitorIds, 'String', '7417') }}\n        AND timestamp >= toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 1 DAY, 3))\n    GROUP BY h, monitorId\n    ORDER BY h ASC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_metrics_latency_7d__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM dns_response__v0\n    WHERE\n        monitorId = {{ String(monitorId, '7417', required=True) }}\n            {% if defined(fromDate) %}\n            AND toDateTime(timestamp / 1000) >= parseDateTimeBestEffortOrNull({{ String(fromDate) }})\n        {% end %}\n        {% if defined(toDate) %}\n            AND toDateTime(timestamp / 1000) <= parseDateTimeBestEffortOrNull({{ String(toDate) }})\n        {% end %}\n    GROUP BY h\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_metrics_regions_14d__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM dns_response__v0\n    WHERE\n        monitorId = {{ String(monitorId, '7417', required=True) }}\n        {% if defined(regions) %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_status_45d__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        time as day,\n        monitorId,\n        countMerge(count) as count,\n        countMerge(success) as ok,\n        countMerge(error) as error,\n        countMerge(degraded) as degraded\n    FROM mv__dns_status_45d__v0\n    WHERE monitorId IN {{ Array(monitorIds, 'String', '7417') }}\n    GROUP BY day, monitorId\n    ORDER BY day DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__dns_uptime_30d__v0.pipe",
    "content": "TAGS \"dns\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(\n            toDateTime(timestamp / 1000), INTERVAL {{ String(interval, '30', required=True) }} minute\n        ) AS interval,\n        countIf(requestStatus = 'success') AS success,\n        countIf(requestStatus = 'degraded') AS degraded,\n        countIf(requestStatus = 'error') AS error\n    FROM dns_response__v0\n    WHERE\n        monitorId = {{ String(monitorId, '7417', required=True) }}\n        {% if defined(fromDate) %}\n            AND toDateTime(timestamp / 1000) >= parseDateTimeBestEffortOrNull({{ String(fromDate) }})\n        {% end %}\n        {% if defined(toDate) %}\n            AND toDateTime(timestamp / 1000) <= parseDateTimeBestEffortOrNull({{ String(toDate) }})\n        {% end %}\n        {% if defined(regions) %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n    GROUP BY interval\n    ORDER BY interval DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_get_14d__v0.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT *\n    FROM mv__http_full_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND id = {{ String(id, '', required=True) }}\n    ORDER BY time DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_get_30d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT *\n    FROM mv__http_full_30d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND cronTimestamp = {{ Int64(cronTimestamp, 1709477432205, required=True) }}\n        AND region = {{ String(region, 'ams', required=True) }}\n    ORDER BY cronTimestamp DESC\n\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_list_14d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT * FROM mv__http_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    ORDER BY cronTimestamp DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_list_14d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT * FROM mv__http_14d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '1', required=True) }}\n            {% if defined(fromDate) %}\n                AND time >= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(fromDate) }})))\n            {% end %}\n            {% if defined(toDate) %}\n                AND time <= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(toDate) }})))\n            {% end %}\n        ORDER BY time DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_list_1d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT * FROM mv__http_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    ORDER BY cronTimestamp DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_list_1d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT * FROM mv__http_1d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '1', required=True) }}\n            {% if defined(fromDate) %}\n                AND time >= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(fromDate) }})))\n            {% end %}\n            {% if defined(toDate) %}\n                AND time <= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(toDate) }})))\n            {% end %}\n        ORDER BY time DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_list_7d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT * FROM mv__http_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    ORDER BY cronTimestamp DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_list_7d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT * FROM mv__http_7d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '1', required=True) }}\n            {% if defined(fromDate) %}\n                AND time >= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(fromDate) }})))\n            {% end %}\n            {% if defined(toDate) %}\n                AND time <= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(toDate) }})))\n            {% end %}\n        ORDER BY time DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_14d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        max(cronTimestamp) AS lastTimestamp\n    FROM mv__http_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3)\n    UNION ALL\n    SELECT\n        round(quantile(0.50)(latency)) AS p50Latency,\n        round(quantile(0.75)(latency)) AS p75Latency,\n        round(quantile(0.90)(latency)) AS p90Latency,\n        round(quantile(0.95)(latency)) AS p95Latency,\n        round(quantile(0.99)(latency)) AS p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n    FROM mv__http_30d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 28 DAY, 3)\n        AND time < toDateTime64(now() - INTERVAL 14 DAY, 3)\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_14d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT\n            round(quantile(0.50)(latency)) as p50Latency,\n            round(quantile(0.75)(latency)) as p75Latency,\n            round(quantile(0.90)(latency)) as p90Latency,\n            round(quantile(0.95)(latency)) as p95Latency,\n            round(quantile(0.99)(latency)) as p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            max(cronTimestamp) AS lastTimestamp\n        FROM mv__http_14d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '1', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n        UNION ALL\n        SELECT\n            round(quantile(0.50)(latency)) AS p50Latency,\n            round(quantile(0.75)(latency)) AS p75Latency,\n            round(quantile(0.90)(latency)) AS p90Latency,\n            round(quantile(0.95)(latency)) AS p95Latency,\n            round(quantile(0.99)(latency)) AS p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n        FROM mv__http_30d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '1', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 28 DAY, 3)\n            AND time < toDateTime64(now() - INTERVAL 14 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_1d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        max(cronTimestamp) AS lastTimestamp\n    FROM mv__http_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 1 DAY, 3)\n    UNION ALL\n    SELECT\n        round(quantile(0.50)(latency)) AS p50Latency,\n        round(quantile(0.75)(latency)) AS p75Latency,\n        round(quantile(0.90)(latency)) AS p90Latency,\n        round(quantile(0.95)(latency)) AS p95Latency,\n        round(quantile(0.99)(latency)) AS p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n    FROM mv__http_7d__v0 -- REMINDER: this will increase the processed data compared to 3d\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 2 DAY, 3)\n        AND time < toDateTime64(now() - INTERVAL 1 DAY, 3)\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_1d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT\n            round(quantile(0.50)(latency)) as p50Latency,\n            round(quantile(0.75)(latency)) as p75Latency,\n            round(quantile(0.90)(latency)) as p90Latency,\n            round(quantile(0.95)(latency)) as p95Latency,\n            round(quantile(0.99)(latency)) as p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            max(cronTimestamp) AS lastTimestamp\n        FROM mv__http_1d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '1', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 1 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n        UNION ALL\n        SELECT\n            round(quantile(0.50)(latency)) AS p50Latency,\n            round(quantile(0.75)(latency)) AS p75Latency,\n            round(quantile(0.90)(latency)) AS p90Latency,\n            round(quantile(0.95)(latency)) AS p95Latency,\n            round(quantile(0.99)(latency)) AS p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n        FROM mv__http_7d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '1', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 2 DAY, 3)\n            AND time < toDateTime64(now() - INTERVAL 1 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_7d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        max(cronTimestamp) AS lastTimestamp\n    FROM mv__http_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 7 DAY, 3)\n    UNION ALL\n    SELECT\n        round(quantile(0.50)(latency)) AS p50Latency,\n        round(quantile(0.75)(latency)) AS p75Latency,\n        round(quantile(0.90)(latency)) AS p90Latency,\n        round(quantile(0.95)(latency)) AS p95Latency,\n        round(quantile(0.99)(latency)) AS p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n    FROM mv__http_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3)\n        AND time < toDateTime64(now() - INTERVAL 7 DAY, 3)\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_7d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT\n            round(quantile(0.50)(latency)) as p50Latency,\n            round(quantile(0.75)(latency)) as p75Latency,\n            round(quantile(0.90)(latency)) as p90Latency,\n            round(quantile(0.95)(latency)) as p95Latency,\n            round(quantile(0.99)(latency)) as p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            max(cronTimestamp) AS lastTimestamp\n        FROM mv__http_7d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '1', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 7 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n        UNION ALL\n        SELECT\n            round(quantile(0.50)(latency)) AS p50Latency,\n            round(quantile(0.75)(latency)) AS p75Latency,\n            round(quantile(0.90)(latency)) AS p90Latency,\n            round(quantile(0.95)(latency)) AS p95Latency,\n            round(quantile(0.99)(latency)) AS p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n        FROM mv__http_14d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '1', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3)\n            AND time < toDateTime64(now() - INTERVAL 7 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_by_interval_14d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000),\n            INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__http_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_by_interval_1d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000),\n            INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__http_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_by_interval_7d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000),\n            INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__http_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_by_region_14d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        round(quantile(0.5)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.9)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok\n    FROM mv__http_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY region\n\n\n\n\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_by_region_1d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        round(quantile(0.5)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.9)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok\n    FROM mv__http_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY region\n\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_by_region_7d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        round(quantile(0.5)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.9)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok\n    FROM mv__http_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY region\n\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_global_1d__v0.pipe",
    "content": "VERSION 0\n\nTAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        round(min(latency), 0) as minLatency,\n        round(max(latency), 0) as maxLatency,\n        round(quantile(0.5)(latency), 0) as p50Latency,\n        round(quantile(0.75)(latency), 0) as p75Latency,\n        round(quantile(0.9)(latency), 0) as p90Latency,\n        round(quantile(0.95)(latency), 0) as p95Latency,\n        round(quantile(0.99)(latency), 0) as p99Latency,\n        max(cronTimestamp) as lastTimestamp,\n        count() as count,\n        monitorId\n    FROM mv__http_1d__v0\n    WHERE monitorId IN {{ Array(monitorIds, 'String', '1') }}\n    GROUP BY monitorId\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_latency_1d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__http_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY h\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_latency_1d_multi__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        monitorId,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__http_1d__v0\n    WHERE\n        monitorId IN {{ Array(monitorIds, 'String', '1,666') }}\n    GROUP BY h, monitorId\n    ORDER BY h ASC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_latency_7d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__http_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY h\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_regions_14d__v0.pipe",
    "content": "NODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__http_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_regions_1d__v0.pipe",
    "content": "NODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__http_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_metrics_regions_7d__v0.pipe",
    "content": "NODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__http_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_status_14d.pipe",
    "content": "VERSION 0\n\nTAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        time as day,\n        countMerge(total) as total,\n        countMerge(success) as success,\n        countMerge(error) as error,\n        countMerge(degraded) as degraded\n    FROM mv__http_status_14d__v0\n    WHERE monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY day\n    ORDER BY day DESC\n    WITH FILL\n    FROM\n        toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC')))\n        TO toStartOfDay(date_sub(DAY, 14, now())) STEP INTERVAL -1 DAY\n    LIMIT {{ Int16(days, 14) }}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_status_45d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT time as day, countMerge(count) as count, countMerge(ok) as ok\n    FROM mv__http_status_45d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY day\n    ORDER BY day DESC\n    WITH FILL\n    FROM\n        toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC')))\n        TO toStartOfDay(\n            date_sub(DAY, 45, now())\n        ) STEP INTERVAL -1 DAY\n    LIMIT {{ Int16(days, 45) }}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_status_45d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        time as day,\n        monitorId,\n        countMerge(count) as count,\n        countMerge(success) as ok,\n        countMerge(error) as error,\n        countMerge(degraded) as degraded\n    FROM mv__http_status_45d__v1\n    WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }}\n    GROUP BY day, monitorId\n    ORDER BY day DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_status_7d.pipe",
    "content": "VERSION 0\n\nTAGS http\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT time as day, countMerge(count) as count, countMerge(ok) as ok\n    FROM mv__http_status_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY day\n    ORDER BY day DESC\n    WITH FILL\n    FROM\n        toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC')))\n        TO toStartOfDay(\n            date_sub(DAY, 7, now())\n        ) STEP INTERVAL -1 DAY\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_timing_phases_14d__v1.pipe",
    "content": "NODE endpoint__http_timing_phases_14d__v1_0\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(time, INTERVAL {{ Int64(interval, 30) }} MINUTE) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n            round(quantile(0.5)(dns)) as p50Dns,\n            round(quantile(0.5)(firstByte)) as p50Ttfb,\n            round(quantile(0.5)(transfer)) as p50Transfer,\n            round(quantile(0.5)(connect)) as p50Connect,\n            round(quantile(0.5)(tls)) as p50Tls,\n            round(quantile(0.75)(dns)) as p75Dns,\n            round(quantile(0.75)(firstByte)) as p75Ttfb,\n            round(quantile(0.75)(transfer)) as p75Transfer,\n            round(quantile(0.75)(connect)) as p75Connect,\n            round(quantile(0.75)(tls)) as p75Tls,\n            round(quantile(0.90)(dns)) as p90Dns,\n            round(quantile(0.90)(firstByte)) as p90Ttfb,\n            round(quantile(0.90)(transfer)) as p90Transfer,\n            round(quantile(0.90)(connect)) as p90Connect,\n            round(quantile(0.90)(tls)) as p90Tls,\n            round(quantile(0.95)(dns)) as p95Dns,\n            round(quantile(0.95)(firstByte)) as p95Ttfb,\n            round(quantile(0.95)(transfer)) as p95Transfer,\n            round(quantile(0.95)(connect)) as p95Connect,\n            round(quantile(0.95)(tls)) as p95Tls,\n            round(quantile(0.99)(dns)) as p99Dns,\n            round(quantile(0.99)(firstByte)) as p99Ttfb,\n            round(quantile(0.99)(transfer)) as p99Transfer,\n            round(quantile(0.99)(connect)) as p99Connect,\n            round(quantile(0.99)(tls)) as p99Tls\n    FROM mv__http_timing_phases_14d__v1\n    WHERE monitorId = {{ String(monitorId, '141', required=True) }}\n    GROUP BY h\n    ORDER BY h ASC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_uptime_30d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(time, INTERVAL {{ String(interval, '30', required=True) }}  minute) AS interval,\n        countIf(requestStatus = 'success') AS success,\n        countIf(requestStatus = 'degraded') AS degraded,\n        countIf(requestStatus = 'error') AS error\n    FROM mv__http_uptime_30d__v1\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        {% if fromDate %} AND time >= parseDateTimeBestEffortOrNull({{ String(fromDate) }}) {% end %}\n        {% if toDate %} AND time <= parseDateTimeBestEffortOrNull({{ String(toDate) }}) {% end %}\n        {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n    GROUP BY interval\n    ORDER BY interval DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_uptime_7d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(time, INTERVAL {{ String(interval, '30', required=True) }}  minute) AS interval,\n        countIf(requestStatus = 'success') AS success,\n        countIf(requestStatus = 'degraded') AS degraded,\n        countIf(requestStatus = 'error') AS error\n    FROM mv__http_uptime_7d__v1\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        {% if fromDate %} AND time >= parseDateTimeBestEffortOrNull({{ String(fromDate) }}) {% end %}\n        {% if toDate %} AND time <= parseDateTimeBestEffortOrNull({{ String(toDate) }}) {% end %}\n        {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n    GROUP BY interval\n    ORDER BY interval DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__http_workspace_30d__v0.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        time as day,\n        countMerge(count_state) as count\n    FROM mv__http_workspace_30d__v0\n    WHERE workspaceId = {{ String(workspaceId, '1', required=True) }}\n    GROUP BY day\n    ORDER BY day DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__stats_global.pipe",
    "content": "VERSION 0\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT COUNT(*) as count\n    FROM ping_response__v8\n    {% if defined(period) %}\n         {% if String(period) == \"1h\" %}\n      WHERE cronTimestamp > toUnixTimestamp(now() - INTERVAL 1 HOUR) * 1000\n      {% elif String(period) == \"10m\" %}\n          WHERE cronTimestamp > toUnixTimestamp(now() - INTERVAL 10 MINUTE) * 1000\n      {% elif String(period) == \"1d\" %}\n          WHERE cronTimestamp > toUnixTimestamp(now() - INTERVAL 1 DAY) * 1000\n      {% elif String(period) == \"1w\" %}\n          WHERE cronTimestamp > toUnixTimestamp(now() - INTERVAL 7 DAY) * 1000\n      {% elif String(period) == \"1m\" %}\n          WHERE cronTimestamp > toUnixTimestamp(now() - INTERVAL 1 MONTH) * 1000\n     {% else %}\n       WHERE cronTimestamp > 0\n     {% end %}\n    {% end %}\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_get_14d__v0.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT *\n        FROM mv__tcp_full_14d__v0\n        WHERE\n        monitorId = {{ String(monitorId, '4433', required=True) }}\n        AND id = {{ String(id, '', required=True) }}\n        ORDER BY time DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_get_30d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT *\n    FROM mv__tcp_full_30d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND cronTimestamp = {{ Int64(cronTimestamp, 1709477432205, required=True) }}\n        AND region = {{ String(region, 'ams', required=True) }}\n    ORDER BY cronTimestamp DESC\n\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_list_14d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT * FROM mv__tcp_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    ORDER BY cronTimestamp DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_list_14d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT * FROM mv__tcp_14d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '4433', required=True) }}\n            {% if defined(fromDate) %}\n                AND time >= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(fromDate) }})))\n            {% end %}\n            {% if defined(toDate) %}\n                AND time <= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(toDate) }})))\n            {% end %}\n        ORDER BY time DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_list_1d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT * FROM mv__tcp_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    ORDER BY cronTimestamp DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_list_1d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT * FROM mv__tcp_1d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '4433', required=True) }}\n            {% if defined(fromDate) %}\n                AND time >= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(fromDate) }})))\n            {% end %}\n            {% if defined(toDate) %}\n                AND time <= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(toDate) }})))\n            {% end %}\n        ORDER BY time DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_list_7d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT * FROM mv__tcp_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    ORDER BY cronTimestamp DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_list_7d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT * FROM mv__tcp_7d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '4433', required=True) }}\n            {% if defined(fromDate) %}\n                AND time >= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(fromDate) }})))\n            {% end %}\n            {% if defined(toDate) %}\n                AND time <= toDateTime(fromUnixTimestamp64Milli(toInt64({{ String(toDate) }})))\n            {% end %}\n        ORDER BY time DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_14d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        max(cronTimestamp) AS lastTimestamp\n    FROM mv__tcp_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        -- FIXME: we can reduce the data processed by using removing it entirely\n        -- because the query is useless as we are in the 14d context\n        -- TODO: check where we can reduce the data processed\n        AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3)\n    UNION ALL\n    SELECT\n        round(quantile(0.50)(latency)) AS p50Latency,\n        round(quantile(0.75)(latency)) AS p75Latency,\n        round(quantile(0.90)(latency)) AS p90Latency,\n        round(quantile(0.95)(latency)) AS p95Latency,\n        round(quantile(0.99)(latency)) AS p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n    FROM mv__tcp_30d__v0 -- REMINDER: this will increase the processed data compared to 3d\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 28 DAY, 3)\n        AND time < toDateTime64(now() - INTERVAL 14 DAY, 3)\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_14d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT\n            round(quantile(0.50)(latency)) as p50Latency,\n            round(quantile(0.75)(latency)) as p75Latency,\n            round(quantile(0.90)(latency)) as p90Latency,\n            round(quantile(0.95)(latency)) as p95Latency,\n            round(quantile(0.99)(latency)) as p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            max(cronTimestamp) AS lastTimestamp\n        FROM mv__tcp_14d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '4433', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n        UNION ALL\n        SELECT\n            round(quantile(0.50)(latency)) AS p50Latency,\n            round(quantile(0.75)(latency)) AS p75Latency,\n            round(quantile(0.90)(latency)) AS p90Latency,\n            round(quantile(0.95)(latency)) AS p95Latency,\n            round(quantile(0.99)(latency)) AS p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n        FROM mv__tcp_30d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '4433', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 28 DAY, 3)\n            AND time < toDateTime64(now() - INTERVAL 14 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_1d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        max(cronTimestamp) AS lastTimestamp\n    FROM mv__tcp_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 1 DAY, 3)\n    UNION ALL\n    SELECT\n        round(quantile(0.50)(latency)) AS p50Latency,\n        round(quantile(0.75)(latency)) AS p75Latency,\n        round(quantile(0.90)(latency)) AS p90Latency,\n        round(quantile(0.95)(latency)) AS p95Latency,\n        round(quantile(0.99)(latency)) AS p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n    FROM mv__tcp_7d__v0 -- REMINDER: this will increase the processed data compared to 3d\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 2 DAY, 3)\n        AND time < toDateTime64(now() - INTERVAL 1 DAY, 3)\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_1d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT\n            round(quantile(0.50)(latency)) as p50Latency,\n            round(quantile(0.75)(latency)) as p75Latency,\n            round(quantile(0.90)(latency)) as p90Latency,\n            round(quantile(0.95)(latency)) as p95Latency,\n            round(quantile(0.99)(latency)) as p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            max(cronTimestamp) AS lastTimestamp\n        FROM mv__tcp_1d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '4433', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 1 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n        UNION ALL\n        SELECT\n            round(quantile(0.50)(latency)) AS p50Latency,\n            round(quantile(0.75)(latency)) AS p75Latency,\n            round(quantile(0.90)(latency)) AS p90Latency,\n            round(quantile(0.95)(latency)) AS p95Latency,\n            round(quantile(0.99)(latency)) AS p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n        FROM mv__tcp_7d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '4433', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 2 DAY, 3)\n            AND time < toDateTime64(now() - INTERVAL 1 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_7d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        max(cronTimestamp) AS lastTimestamp\n    FROM mv__tcp_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 7 DAY, 3)\n    UNION ALL\n    SELECT\n        round(quantile(0.50)(latency)) AS p50Latency,\n        round(quantile(0.75)(latency)) AS p75Latency,\n        round(quantile(0.90)(latency)) AS p90Latency,\n        round(quantile(0.95)(latency)) AS p95Latency,\n        round(quantile(0.99)(latency)) AS p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok,\n        NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n    FROM mv__tcp_14d__v0 -- REMINDER: this will increase the processed data compared to 3d\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n        AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3)\n        AND time < toDateTime64(now() - INTERVAL 7 DAY, 3)\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_7d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n        SELECT\n            round(quantile(0.50)(latency)) as p50Latency,\n            round(quantile(0.75)(latency)) as p75Latency,\n            round(quantile(0.90)(latency)) as p90Latency,\n            round(quantile(0.95)(latency)) as p95Latency,\n            round(quantile(0.99)(latency)) as p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            max(cronTimestamp) AS lastTimestamp\n        FROM mv__tcp_7d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '4433', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 7 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n        UNION ALL\n        SELECT\n            round(quantile(0.50)(latency)) AS p50Latency,\n            round(quantile(0.75)(latency)) AS p75Latency,\n            round(quantile(0.90)(latency)) AS p90Latency,\n            round(quantile(0.95)(latency)) AS p95Latency,\n            round(quantile(0.99)(latency)) AS p99Latency,\n            count() as count,\n            countIf(requestStatus = 'success') AS success,\n            countIf(requestStatus = 'degraded') AS degraded,\n            countIf(requestStatus = 'error') AS error,\n            NULL as lastTimestamp  -- no need to query the `lastTimestamp` as not relevant\n        FROM mv__tcp_14d__v1\n        WHERE\n            monitorId = {{ String(monitorId, '4433', required=True) }}\n            AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3)\n            AND time < toDateTime64(now() - INTERVAL 7 DAY, 3)\n            {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_by_interval_14d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000),\n            INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__tcp_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_by_interval_1d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000),\n            INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__tcp_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_by_interval_7d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000),\n            INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__tcp_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY h, region\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_by_region_14d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        round(quantile(0.5)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.9)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok\n    FROM mv__tcp_14d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY region\n\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_by_region_1d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        round(quantile(0.5)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.9)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok\n    FROM mv__tcp_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY region\n\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_by_region_7d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        region,\n        round(quantile(0.5)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.9)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency,\n        count() as count,\n        count(if(error = 0, 1, NULL)) AS ok\n    FROM mv__tcp_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY region\n\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_global_1d.pipe",
    "content": "VERSION 0\n\nTAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        round(min(latency), 0) as minLatency,\n        round(max(latency), 0) as maxLatency,\n        round(quantile(0.5)(latency), 0) as p50Latency,\n        round(quantile(0.75)(latency), 0) as p75Latency,\n        round(quantile(0.9)(latency), 0) as p90Latency,\n        round(quantile(0.95)(latency), 0) as p95Latency,\n        round(quantile(0.99)(latency), 0) as p99Latency,\n        max(cronTimestamp) as lastTimestamp,\n        count() as count,\n        monitorId\n    FROM mv__tcp_1d__v0\n    WHERE monitorId IN {{ Array(monitorIds, 'String', '4433') }}\n    GROUP BY monitorId\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_latency_1d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__tcp_1d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '4433', required=True) }}\n    GROUP BY h\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_latency_1d_multi__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        monitorId,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__tcp_1d__v0\n    WHERE\n        monitorId IN {{ Array(monitorIds, 'String', '4433') }}\n    GROUP BY h, monitorId\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_metrics_latency_7d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000), INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(quantile(0.50)(latency)) as p50Latency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.90)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM mv__tcp_7d__v1\n    WHERE\n        monitorId = {{ String(monitorId, '4433', required=True) }}\n    GROUP BY h\n    ORDER BY h DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_status_45d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT time as day, countMerge(count) as count, countMerge(ok) as ok\n    FROM mv__tcp_status_45d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY day\n    ORDER BY day DESC\n    WITH FILL\n    FROM\n        toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC')))\n        TO toStartOfDay(\n            date_sub(DAY, 45, now())\n        ) STEP INTERVAL -1 DAY\n    LIMIT {{ Int16(days, 45) }}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_status_45d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        time as day,\n        monitorId,\n        countMerge(count) as count,\n        countMerge(success) as ok,\n        countMerge(error) as error,\n        countMerge(degraded) as degraded\n    FROM mv__tcp_status_45d__v1\n    WHERE monitorId IN {{ Array(monitorIds, 'String', '4433') }}\n    GROUP BY day, monitorId\n    ORDER BY day DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_status_7d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT time as day, countMerge(count) as count, countMerge(ok) as ok\n    FROM mv__tcp_status_7d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '1', required=True) }}\n    GROUP BY day\n    ORDER BY day DESC\n    WITH FILL\n    FROM\n        toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC')))\n        TO toStartOfDay(\n            date_sub(DAY, 7, now())\n        ) STEP INTERVAL -1 DAY\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_uptime_30d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(time, INTERVAL {{ String(interval, '30', required=True) }}  minute) AS interval,\n        countIf(requestStatus = 'success') AS success,\n        countIf(requestStatus = 'degraded') AS degraded,\n        countIf(requestStatus = 'error') AS error\n    FROM mv__tcp_uptime_30d__v1\n    WHERE\n        monitorId = {{ String(monitorId, '4433', required=True) }}\n        {% if fromDate %} AND time >= parseDateTimeBestEffortOrNull({{ String(fromDate) }}) {% end %}\n        {% if toDate %} AND time <= parseDateTimeBestEffortOrNull({{ String(toDate) }}) {% end %}\n        {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n    GROUP BY interval\n    ORDER BY interval DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_uptime_7d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        toStartOfInterval(time, INTERVAL {{ String(interval, '30', required=True) }}  minute) AS interval,\n        countIf(requestStatus = 'success') AS success,\n        countIf(requestStatus = 'degraded') AS degraded,\n        countIf(requestStatus = 'error') AS error\n    FROM mv__tcp_uptime_7d__v1\n    WHERE\n        monitorId = {{ String(monitorId, '4433', required=True) }}\n        {% if fromDate %} AND time >= parseDateTimeBestEffortOrNull({{ String(fromDate) }}) {% end %}\n        {% if toDate %} AND time <= parseDateTimeBestEffortOrNull({{ String(toDate) }}) {% end %}\n        {% if regions %} AND region IN {{ Array(regions, 'String', 'ams,fra') }} {% end %}\n    GROUP BY interval\n    ORDER BY interval DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint__tcp_workspace_30d__v0.pipe",
    "content": "TAGS \"tcp\"\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        time as day,\n        countMerge(count_state) as count\n    FROM mv__tcp_workspace_30d__v0\n    WHERE workspaceId = {{ Int32(workspaceId, 1, required=True) }}\n    GROUP BY day\n    ORDER BY day DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint_audit_log.pipe",
    "content": "VERSION 0\n\nNODE endpoint_audit_pipe_0\nSQL >\n\n    % SELECT * FROM audit_log__v0 WHERE id = {{ String(event_id, 1) }} ORDER BY timestamp DESC\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/endpoints/endpoint_external_status.pipe",
    "content": "VERSION 0\nNODE external_status_0\nSQL >\n\n    %\n    SELECT *\n    FROM external_status\n    WHERE name = {{ String(name, 'OpenAI') }}\n    ORDER BY fetched_at DESC\n    LIMIT {{ Int16(limit, 10000) }}\n\n\nTYPE ENDPOINT\n"
  },
  {
    "path": "packages/tinybird/package.json",
    "content": "{\n  \"name\": \"@openstatus/tinybird\",\n  \"version\": \"0.0.0\",\n  \"main\": \"src/index.ts\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@chronark/zod-bird\": \"1.0.0\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@openstatus/utils\": \"workspace:*\",\n    \"@types/node\": \"22.10.2\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/tinybird/pipes/__ttl_45d_count_utc_get.pipe",
    "content": "VERSION 1\nTOKEN \"__ttl_45d_count_utc_get__v1_endpoint_read_7956\" READ\n\nNODE __ttl_45d_count_utc_get_0\nSQL >\n\n    %\n    SELECT time as day, countMerge(count) as count, countMerge(ok) as ok\n    FROM mv__http_status_45d__v0\n    WHERE\n        monitorId = {{ String(monitorId, '4') }}\n    GROUP BY day\n    ORDER BY day DESC\n    WITH FILL\n    FROM\n        toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC')))\n        TO toStartOfDay(\n            date_sub(DAY, 45, now())\n        ) STEP INTERVAL -1 DAY\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__dns_status_45d__v1.pipe",
    "content": "TAGS \"statuspage, dns\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        monitorId,\n        countState() AS count,\n        countState(if(requestStatus = 'success', 1, NULL)) AS success,\n        countState(if(requestStatus = 'error', 1, NULL)) AS error,\n        countState(if(requestStatus = 'degraded', 1, NULL)) AS degraded\n    FROM dns_response__v0\n    GROUP BY\n        time,\n        monitorId\n\nTYPE materialized\nDATASOURCE mv__dns_status_45d__v0\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_14d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        id,\n        latency,\n        region,\n        trigger,\n        statusCode,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        requestStatus,\n        timing\n    FROM ping_response__v8\n\nTYPE materialized\nDATASOURCE mv__http_14d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_1d__v1.pipe",
    "content": "TAGS \"http\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        id,\n        latency,\n        region,\n        trigger,\n        statusCode,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        requestStatus,\n        timing\n    FROM ping_response__v8\n\nTYPE materialized\nDATASOURCE mv__http_1d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_30d__v1.pipe",
    "content": "NODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        id,\n        latency,\n        region,\n        trigger,\n        statusCode,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        requestStatus,\n        timing\n    FROM ping_response__v8\n\nTYPE materialized\nDATASOURCE mv__http_30d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_7d__v1.pipe",
    "content": "NODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        id,\n        latency,\n        region,\n        trigger,\n        statusCode,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        requestStatus,\n        timing\n    FROM ping_response__v8\n\nTYPE materialized\nDATASOURCE mv__http_7d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_full_14d__v0.pipe",
    "content": "DESCRIPTION >\n\tStores all the data from the http_response table for the last 30 days, mainly used for accessing the data details.\n\n\nTAGS \"http, full\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        *\n    FROM ping_response__v8\n\nTYPE materialized\nDATASOURCE mv__http_full_14d__v0\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_full_30d__v0.pipe",
    "content": "DESCRIPTION >\n\tStores all the data from the http_response table for the last 30 days, mainly used for accessing the data details.\n\n\nTAGS \"http, full\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        *\n    FROM ping_response__v8\n\nTYPE materialized\nDATASOURCE mv__http_full_30d__v0\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_status_14d.pipe",
    "content": "VERSION 0\n\nTAGS \"http, statuspage\"\n\nNODE aggregate\nSQL >\n\n    WITH\n        if(requestStatus = 'success', 1, NULL) AS is_success,\n        if(requestStatus = 'degraded', 1, NULL) AS is_degraded,\n        if(requestStatus = 'error', 1, NULL) AS is_error\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        monitorId,\n        countState() AS total,\n        countState(is_success) AS success,\n        countState(is_degraded) AS degraded,\n        countState(is_error) AS error\n    FROM ping_response__v8\n    GROUP BY\n        time,\n        monitorId\n\nTYPE materialized\nDATASOURCE mv__http_status_14d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_status_45d.pipe",
    "content": "VERSION 0\n\nTAGS http, statuspage\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        monitorId,\n        countState() AS count,\n        countState(if(error = 0, 1, NULL)) AS ok\n    FROM ping_response__v8\n    GROUP BY\n        time,\n        monitorId\n\nTYPE materialized\nDATASOURCE mv__http_status_45d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_status_45d__v1.pipe",
    "content": "TAGS \"http, statuspage\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        monitorId,\n        countState() AS count,\n        countState(if(requestStatus = 'success', 1, NULL)) AS success,\n        countState(if(requestStatus = 'error', 1, NULL)) AS error,\n        countState(if(requestStatus = 'degraded', 1, NULL)) AS degraded\n    FROM ping_response__v8\n    GROUP BY\n        time,\n        monitorId\n\nTYPE materialized\nDATASOURCE mv__http_status_45d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_status_7d.pipe",
    "content": "VERSION 0\n\nTAGS http, statuspage\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        monitorId,\n        countState() AS count,\n        countState(if(error = 0, 1, NULL)) AS ok\n    FROM ping_response__v8\n    GROUP BY\n        time,\n        monitorId\n\nTYPE materialized\nDATASOURCE mv__http_status_7d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_timing_phases_14d.pipe",
    "content": "NODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        latency,\n        region,\n        trigger,\n        statusCode,\n        monitorId,\n        workspaceId,\n        requestStatus,\n\n        -- Compute actual durations from timing phases\n        if(isNull(JSONExtractInt(timing, 'dnsStart')) OR isNull(JSONExtractInt(timing, 'dnsDone')), NULL,\n           JSONExtractInt(timing, 'dnsDone') - JSONExtractInt(timing, 'dnsStart')) AS dns,\n\n        if(isNull(JSONExtractInt(timing, 'connectStart')) OR isNull(JSONExtractInt(timing, 'connectDone')), NULL,\n           JSONExtractInt(timing, 'connectDone') - JSONExtractInt(timing, 'connectStart')) AS connect,\n\n        if(isNull(JSONExtractInt(timing, 'tlsHandshakeStart')) OR isNull(JSONExtractInt(timing, 'tlsHandshakeDone')), NULL,\n           JSONExtractInt(timing, 'tlsHandshakeDone') - JSONExtractInt(timing, 'tlsHandshakeStart')) AS tls,\n\n        if(isNull(JSONExtractInt(timing, 'firstByteStart')) OR isNull(JSONExtractInt(timing, 'firstByteDone')), NULL,\n           JSONExtractInt(timing, 'firstByteDone') - JSONExtractInt(timing, 'firstByteStart')) AS firstByte,\n\n        if(isNull(JSONExtractInt(timing, 'transferStart')) OR isNull(JSONExtractInt(timing, 'transferDone')), NULL,\n           JSONExtractInt(timing, 'transferDone') - JSONExtractInt(timing, 'transferStart')) AS transfer\n\n    FROM ping_response__v8\n\nTYPE materialized\nDATASOURCE mv__http_timing_phases_14d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_uptime_30d.pipe",
    "content": "VERSION 1\n\nTAGS http\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        region,\n        requestStatus,\n        monitorId,\n        workspaceId\n    FROM ping_response__v8\n\nTYPE materialized\nDATASOURCE mv__http_uptime_30d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_uptime_7d__v1.pipe",
    "content": "NODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        region,\n        requestStatus,\n        monitorId,\n        workspaceId\n    FROM ping_response__v8\n\nTYPE materialized\nDATASOURCE mv__http_uptime_7d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__http_workspace_30d__v0.pipe",
    "content": "TAGS \"http\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        workspaceId,\n        ifNull(trigger, 'cron') AS trigger,\n        countState() AS count_state\n    FROM ping_response__v8\n    GROUP BY\n        time,\n        workspaceId,\n        trigger\n\nTYPE materialized\nDATASOURCE mv__http_workspace_30d__v0\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_14d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        latency,\n        error,\n        region,\n        trigger,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        workspaceId\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_14d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_14d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        latency,\n        error,\n        region,\n        trigger,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        requestStatus,\n        id\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_14d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_1d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        latency,\n        error,\n        region,\n        trigger,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        workspaceId\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_1d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_1d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        latency,\n        error,\n        region,\n        trigger,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        requestStatus,\n        id\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_1d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_30d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        latency,\n        error,\n        region,\n        trigger,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        workspaceId\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_30d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_30d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        latency,\n        error,\n        region,\n        trigger,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        requestStatus,\n        id\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_30d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_7d.pipe",
    "content": "VERSION 0\n\nTAGS tcp\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        latency,\n        error,\n        region,\n        trigger,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        workspaceId\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_7d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_7d__v1.pipe",
    "content": "TAGS \"tcp\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        latency,\n        error,\n        region,\n        trigger,\n        timestamp,\n        cronTimestamp,\n        monitorId,\n        requestStatus,\n        id\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_7d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_full_14d__v0.pipe",
    "content": "DESCRIPTION >\n\tStores all the data from the http_response table for the last 30 days, mainly used for accessing the data details.\n\n\nTAGS \"tcp, full\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        *\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_full_14d__v0\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_full_30d__v0.pipe",
    "content": "DESCRIPTION >\n\tStores all the data from the http_response table for the last 30 days, mainly used for accessing the data details.\n\n\nTAGS \"tcp, full\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(tcp_response__v0.cronTimestamp)) AS time,\n        *\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_full_30d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_status_45d.pipe",
    "content": "VERSION 0\n\nTAGS tcp, statuspage\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        monitorId,\n        countState() AS count,\n        countState(if(error = 0, 1, NULL)) AS ok\n    FROM tcp_response__v0\n    GROUP BY\n        time,\n        monitorId\n\nTYPE materialized\nDATASOURCE mv__tcp_status_45d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_status_45d__v1.pipe",
    "content": "TAGS \"tcp, statuspage\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        monitorId,\n        countState() AS count,\n        countState(if(requestStatus = 'success', 1, NULL)) AS success,\n        countState(if(requestStatus = 'error', 1, NULL)) AS error,\n        countState(if(requestStatus = 'degraded', 1, NULL)) AS degraded\n    FROM tcp_response__v0\n    GROUP BY\n        time,\n        monitorId\n\nTYPE materialized\nDATASOURCE mv__tcp_status_45d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_status_7d.pipe",
    "content": "VERSION 0\n\nTAGS tcp, statuspage\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        monitorId,\n        countState() AS count,\n        countState(if(error = 0, 1, NULL)) AS ok\n    FROM tcp_response__v0\n    GROUP BY\n        time,\n        monitorId\n\nTYPE materialized\nDATASOURCE mv__tcp_status_7d__v0\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_uptime_30d__v1.pipe",
    "content": "NODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        region,\n        requestStatus,\n        monitorId,\n        workspaceId\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_uptime_30d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_uptime_7d__v1.pipe",
    "content": "NODE aggregate\nSQL >\n\n    SELECT\n        toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time,\n        region,\n        requestStatus,\n        monitorId,\n        workspaceId\n    FROM tcp_response__v0\n\nTYPE materialized\nDATASOURCE mv__tcp_uptime_7d__v1\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/aggregate__tcp_workspace_30d__v0.pipe",
    "content": "TAGS \"tcp\"\n\nNODE aggregate\nSQL >\n\n    SELECT\n        toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time,\n        workspaceId,\n        ifNull(trigger, 'cron') AS trigger,\n        countState() AS count_state\n    FROM tcp_response__v0\n    GROUP BY\n        time,\n        workspaceId,\n        trigger\n\nTYPE materialized\nDATASOURCE mv__tcp_workspace_30d__v0\n\n\n"
  },
  {
    "path": "packages/tinybird/pipes/get_result_for_on_demand_check_http.pipe",
    "content": "VERSION 0\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT\n        latency,\n        monitorId,\n        error,\n        region,\n        statusCode,\n        timestamp,\n        url,\n        workspaceId,\n        timing,\n        cronTimestamp\n    FROM mv__http_full_30d__v0\n    where\n         cronTimestamp = {{ String(timestamp, '1729871694536') }} and\n         monitorId = {{ String(monitorId, '2260') }}\n        and url = {{ String(url , 'https://www.openstatus.dev/api/ping/edge') }}\n        order by cronTimestamp desc\n"
  },
  {
    "path": "packages/tinybird/pipes/public_status.pipe",
    "content": "VERSION 0\n\nDESCRIPTION >\n\tlast 5 cron timestamps within last 3 hours\n\nNODE group_by_cronTimestamp\nSQL >\n\n    %\n    SELECT\n        cronTimestamp,\n        count() AS count,\n        count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok\n    FROM ping_response__v8\n    WHERE\n        monitorId = {{ String(monitorId, '1') }}\n        {% if defined(url) %} AND url = {{ String(url) }} {% end %}\n        AND cronTimestamp\n        >= toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 3 HOUR, 3))\n    GROUP BY cronTimestamp, monitorId\n    ORDER BY cronTimestamp DESC\n    LIMIT {{ Int16(limit, 5)}}\n"
  },
  {
    "path": "packages/tinybird/pipes/response_details.pipe",
    "content": "VERSION 0\n\nNODE response_graph_0\nSQL >\n\n    %\n    SELECT *\n    FROM ping_response__v8\n    WHERE\n        monitorId = {{ String(monitorId, '1') }}\n        {% if defined(url) %} AND url = {{ String(url) }} {% end %}\n        AND cronTimestamp = {{ Int64(cronTimestamp, 1706467215188) }}\n        AND region = {{ String(region, 'ams') }}\n"
  },
  {
    "path": "packages/tinybird/pipes/response_graph.pipe",
    "content": "VERSION 0\n\nNODE response_graph_0\nSQL >\n\n    %\n    SELECT\n        region,\n        toStartOfInterval(\n            toDateTime(cronTimestamp / 1000),\n            INTERVAL {{ Int64(interval, 30) }} MINUTE\n        ) as h,\n        toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp,\n        round(avg(latency)) as avgLatency,\n        round(quantile(0.75)(latency)) as p75Latency,\n        round(quantile(0.9)(latency)) as p90Latency,\n        round(quantile(0.95)(latency)) as p95Latency,\n        round(quantile(0.99)(latency)) as p99Latency\n    FROM ping_response__v8\n    WHERE\n        monitorId = {{ String(monitorId, '1') }}\n        {% if defined(url) %} AND url = {{ String(url) }} {% end %}\n        {% if defined(fromDate) %} AND timestamp >= {{ Int64(fromDate) }} {% end %}\n        {% if defined(toDate) %} AND timestamp <= {{ Int64(toDate) }} {% end %}\n    GROUP BY h, region\n    ORDER BY h DESC\n"
  },
  {
    "path": "packages/tinybird/pipes/response_list.pipe",
    "content": "VERSION 2\n\nNODE response_list_0\nSQL >\n\n    %\n    SELECT latency, monitorId, region, statusCode, timestamp, url, workspaceId, cronTimestamp, message\n    FROM ping_response__v8\n    WHERE monitorId = {{ String(monitorId, 'openstatusPing') }}\n    {% if defined(url) %} AND url = {{ String(url) }} {% end %}\n    {% if defined(region) %}\n    AND region = {{ String(region) }}\n    {% end %}\n    {% if defined(cronTimestamp) %}\n    AND cronTimestamp = {{ Int64(cronTimestamp) }}\n    {% end %}\n    {% if defined(fromDate) %}\n    AND cronTimestamp >= {{ Int64(fromDate) }}\n    {% end %}\n    {% if defined(toDate) %}\n    AND cronTimestamp <= {{ Int64(toDate) }}\n    {% end %}\n    ORDER BY timestamp DESC\n    LIMIT {{Int32(limit, 100)}}\n"
  },
  {
    "path": "packages/tinybird/pipes/single_checks_get.pipe",
    "content": "VERSION 1\n\nNODE endpoint\nSQL >\n\n    %\n    SELECT *\n    from check_response_http\n    WHERE\n        workspaceId = {{ Int16(workspaceId, 1) }}\n        {% if defined(requestId) %} AND requestId = {{ Int16(requestId) }} {% end %}\n    ORDER BY timestamp DESC\n    LIMIT {{ Int32(pageSize, 10) }}\n    OFFSET {{ Int32(page, 0) * Int32(pageSize, 10) }}\n\n\n"
  },
  {
    "path": "packages/tinybird/src/audit-log/README.md",
    "content": "## Motivation\n\nWe want to track every change made for the `incident` and `monitor`. Therefore,\nit requires us to build some audit log / event sourcing foundation.\n\nThe `Event` is what the data type stored within [Tinybird](https://tinybird.co).\nIt has basic props that every event includes, as well as a `metadata` prop that\ncan be used to store additional informations.\n\n```ts\nexport type Event = {\n  /**\n   * Unique identifier for the event.\n   */\n  id: string;\n\n  /**\n   * The actor that triggered the event.\n   * @default { id: \"\", name: \"system\" }\n   * @example { id: \"1\", name: \"mxkaske\" }\n   */\n  actor?: {\n    id: string;\n    name: string;\n  };\n\n  /**\n   * The ressources affected by the action taken.\n   * @example [{ id: \"1\", name: \"monitor\" }]\n   */\n  targets?: {\n    id: string;\n    name: string;\n  }[];\n\n  /**\n   * The action that was triggered.\n   * @example monitor.down | incident.create\n   */\n  action: string;\n\n  /**\n   * The timestamp of the event in milliseconds since epoch UTC.\n   * @default Date.now()\n   */\n  timestamp?: number;\n\n  /**\n   * The version of the event. Should be incremented on each update.\n   * @default 1\n   */\n  version?: number;\n\n  /**\n   * Metadata for the event. Defined via zod schema.\n   */\n  metadata?: unknown;\n};\n```\n\nThe objects are parsed and stored as string via\n`schema.transform(val => JSON.stringify(val))` and transformed back into an\nobject before parsing via `z.preprocess(val => JSON.parse(val), schema)`.\n\n## Example\n\n```ts\nconst tb = new Tinybird({ token: process.env.TINY_BIRD_API_KEY || \"\" });\n\nconst auditLog = new AuditLog({ tb });\n\nawait auditLog.publishAuditLog({\n  id: \"monitor:1\",\n  action: \"monitor.down\",\n  targets: [{ id: \"1\", type: \"monitor\" }], // not mandatory, but could be useful later on\n  metadata: { region: \"gru\", statusCode: 400, message: \"timeout\" },\n});\n\nawait auditLog.getAuditLog({ event_id: \"monitor:1\" });\n```\n\n## Inspiration\n\n- WorkOS [Audit Logs](https://workos.com/docs/audit-logs)\n\n## Tinybird\n\nPush the pipe and datasource to tinybird:\n\n```\ntb push datasources/audit_log.datasource\ntb push pipes/endpoint_audit_log.pipe\n```\n\n---\n\n### Possible extention\n\n> TODO: Remove `Nullable` from `targets` to better index and query it.\n"
  },
  {
    "path": "packages/tinybird/src/audit-log/action-schema.ts",
    "content": "import { z } from \"zod\";\n\n/**\n * The schema for the monitor.recovered action.\n * It represents the event when a monitor has recovered from a failure.\n */\nexport const monitorRecoveredSchema = z.object({\n  action: z.literal(\"monitor.recovered\"),\n  metadata: z.object({\n    region: z.string(),\n    statusCode: z.number(),\n    latency: z.number().optional(),\n    cronTimestamp: z.number().optional(),\n  }),\n});\n\n/**\n * The schema for the monitor.recovered action.\n * It represents the event when a monitor has recovered from a failure.\n */\nexport const monitorDegradedSchema = z.object({\n  action: z.literal(\"monitor.degraded\"),\n  metadata: z.object({\n    region: z.string(),\n    statusCode: z.number(),\n    cronTimestamp: z.number().optional(),\n    latency: z.number().optional(),\n  }),\n});\n\n/**\n * The schema for the monitor.failed action.\n * It represents the event when a monitor has failed.\n */\nexport const monitorFailedSchema = z.object({\n  action: z.literal(\"monitor.failed\"),\n  metadata: z.object({\n    region: z.string(),\n    statusCode: z.number().optional(),\n    message: z.string().optional(),\n    latency: z.number().optional(),\n    cronTimestamp: z.number().optional(),\n  }),\n});\n\n/**\n * The schema for the notification.send action.\n *\n */\nexport const notificationSentSchema = z.object({\n  action: z.literal(\"notification.sent\"),\n  metadata: z.object({\n    // we could use the notificationProviderSchema for more type safety\n    provider: z.string(),\n    cronTimestamp: z.number().optional(),\n    type: z.enum([\"alert\", \"recovery\", \"degraded\"]).optional(),\n    notificationId: z.number().optional(),\n  }),\n});\n\nexport const incidentCreatedSchema = z.object({\n  action: z.literal(\"incident.created\"),\n  metadata: z.object({\n    cronTimestamp: z.number().optional(),\n    incidentId: z.number().optional(),\n  }),\n});\n\nexport const incidentResolvedSchema = z.object({\n  action: z.literal(\"incident.resolved\"),\n  metadata: z.object({\n    cronTimestamp: z.number().optional(),\n    incidentId: z.number().optional(),\n  }),\n});\n"
  },
  {
    "path": "packages/tinybird/src/audit-log/action-validation.ts",
    "content": "import { z } from \"zod\";\n\nimport {\n  incidentCreatedSchema,\n  incidentResolvedSchema,\n  monitorDegradedSchema,\n  monitorFailedSchema,\n  monitorRecoveredSchema,\n  notificationSentSchema,\n} from \"./action-schema\";\nimport { ingestBaseEventSchema, pipeBaseResponseData } from \"./base-validation\";\n\n/**\n * The schema for the event object.\n * It extends the base schema. It uses the `discriminatedUnion` method for faster\n * evaluation to determine which schema to be used to parse the input.\n * It also transforms the metadata object into a string.\n *\n * @todo: whenever a new action is added, it should be included to the discriminatedUnion\n */\nexport const ingestActionEventSchema = z\n  .intersection(\n    // Unfortunately, the array cannot be dynamic, otherwise could be added to the Client\n    // and made available to devs as library\n    z.discriminatedUnion(\"action\", [\n      monitorRecoveredSchema,\n      monitorDegradedSchema,\n      monitorFailedSchema,\n      notificationSentSchema,\n      incidentCreatedSchema,\n      incidentResolvedSchema,\n    ]),\n    ingestBaseEventSchema,\n  )\n  .transform((val) => ({\n    ...val,\n    metadata: JSON.stringify(val.metadata),\n  }));\n\n/**\n * The schema for the response object.\n * It extends the base schema. It uses the `discriminatedUnion` method for faster\n * evaluation to determine which schema to be used to parse the input.\n * It also preprocesses the metadata string into the correct schema object.\n *\n * @todo: whenever a new action is added, it should be included to the discriminatedUnion\n */\nexport const pipeActionResponseData = z.intersection(\n  z.discriminatedUnion(\"action\", [\n    monitorRecoveredSchema.extend({\n      metadata: z.preprocess(\n        (val) => JSON.parse(String(val)),\n        monitorRecoveredSchema.shape.metadata,\n      ),\n    }),\n    monitorDegradedSchema.extend({\n      metadata: z.preprocess(\n        (val) => JSON.parse(String(val)),\n        monitorDegradedSchema.shape.metadata,\n      ),\n    }),\n    monitorFailedSchema.extend({\n      metadata: z.preprocess(\n        (val) => JSON.parse(String(val)),\n        monitorFailedSchema.shape.metadata,\n      ),\n    }),\n    notificationSentSchema.extend({\n      metadata: z.preprocess(\n        (val) => JSON.parse(String(val)),\n        notificationSentSchema.shape.metadata,\n      ),\n    }),\n    incidentCreatedSchema.extend({\n      metadata: z.preprocess(\n        (val) => JSON.parse(String(val)),\n        incidentCreatedSchema.shape.metadata,\n      ),\n    }),\n    incidentResolvedSchema.extend({\n      metadata: z.preprocess(\n        (val) => JSON.parse(String(val)),\n        incidentResolvedSchema.shape.metadata,\n      ),\n    }),\n  ]),\n  pipeBaseResponseData,\n);\n\nexport type IngestActionEvent = z.infer<typeof ingestActionEventSchema>;\nexport type PipeActionResponseData = z.infer<typeof pipeActionResponseData>;\n"
  },
  {
    "path": "packages/tinybird/src/audit-log/base-validation.ts",
    "content": "import { z } from \"zod\";\n\n/**\n * The base schema for every event, used to validate it's structure\n * on datasource ingestion and pipe retrieval.\n */\nexport const baseSchema = z.object({\n  id: z.string().min(1), // DISCUSS: we could use the `${targets.type}:${targets.id}` format automatic generation\n  action: z.string(),\n  // REMINDER: do not use .default(Date.now()), it will be evaluated only once\n  timestamp: z\n    .int()\n    .optional()\n    .transform((val) => val || Date.now()),\n  version: z.int().prefault(1),\n});\n\n/**\n * The schema for the actor type.\n * It represents the type of the user that triggered the event.\n */\nexport const actorTypeSchema = z.enum([\"user\", \"system\"]);\n\n/**\n * The schema for the actor object.\n * It represents the user that triggered the event.\n */\nexport const actorSchema = z\n  .object({\n    id: z.string(),\n    type: actorTypeSchema,\n  })\n  .prefault({\n    id: \"server\",\n    type: \"system\",\n  });\n\n/**\n * The schema for the target type.\n * It represents the type of the target that is affected by the event.\n */\nexport const targetTypeSchema = z.enum([\n  \"monitor\",\n  \"page\",\n  // \"incident\", // has been removed from the schema\n  \"status-report\",\n  \"user\",\n  \"notification\",\n  \"organization\",\n]);\n\n/**\n * The schema for the targets object.\n * It represents the targets that are affected by the event.\n */\nexport const targetsSchema = z\n  .object({\n    id: z.string(),\n    type: targetTypeSchema,\n  })\n  .array()\n  .optional();\n\n/**\n * The schema for the event object.\n * It extends the base schema and transforms the actor, targets\n * objects into strings.\n */\nexport const ingestBaseEventSchema = baseSchema.extend({\n  actor: actorSchema.transform((val) => JSON.stringify(val)),\n  targets: targetsSchema.transform((val) => JSON.stringify(val)),\n});\n\n/**\n * The schema for the response object.\n * It extends the base schema and transforms the actor, targets\n * back into typed objects.\n */\nexport const pipeBaseResponseData = baseSchema.extend({\n  actor: z.preprocess((val) => JSON.parse(String(val)), actorSchema),\n  targets: z.preprocess(\n    (val) => (val ? JSON.parse(String(val)) : undefined),\n    targetsSchema,\n  ),\n});\n\n/**\n * The schema for the parameters object.\n * It represents the parameters that are passed to the pipe.\n */\nexport const pipeParameterData = z.object({ event_id: z.string().min(1) });\n\nexport type PipeParameterData = z.infer<typeof pipeParameterData>;\nexport type PipeBaseResponseData = z.infer<typeof pipeBaseResponseData>;\nexport type IngestBaseEvent = z.infer<typeof ingestBaseEventSchema>;\n"
  },
  {
    "path": "packages/tinybird/src/audit-log/client.ts",
    "content": "import type { Tinybird } from \"@chronark/zod-bird\";\n\nimport {\n  ingestActionEventSchema,\n  pipeActionResponseData,\n} from \"./action-validation\";\nimport { pipeParameterData } from \"./base-validation\";\n\nexport class AuditLog {\n  private readonly tb: Tinybird;\n\n  constructor(opts: { tb: Tinybird }) {\n    this.tb = opts.tb;\n  }\n\n  get publishAuditLog() {\n    return this.tb.buildIngestEndpoint({\n      datasource: \"audit_log__v0\",\n      event: ingestActionEventSchema,\n    });\n  }\n\n  get getAuditLog() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint_audit_log__v0\",\n      parameters: pipeParameterData,\n      data: pipeActionResponseData,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/tinybird/src/audit-log/examples.ts",
    "content": "import { Tinybird } from \"@chronark/zod-bird\";\n\nimport { AuditLog } from \"./client\";\n\nconst tb = new Tinybird({ token: process.env.TINY_BIRD_API_KEY || \"\" });\n\nconst auditLog = new AuditLog({ tb });\n\n// biome-ignore lint/correctness/noUnusedVariables: <explanation>\nasync function seed() {\n  await auditLog.publishAuditLog({\n    id: \"monitor:2\",\n    action: \"monitor.failed\",\n    targets: [{ id: \"2\", type: \"monitor\" }],\n    metadata: { region: \"gru\", statusCode: 500, message: \"timeout\" },\n  });\n  await auditLog.publishAuditLog({\n    id: \"monitor:1\",\n    action: \"monitor.recovered\",\n    targets: [{ id: \"1\", type: \"monitor\" }],\n    metadata: { region: \"gru\", statusCode: 200 },\n  });\n  await auditLog.publishAuditLog({\n    id: \"user:1\",\n    actor: {\n      type: \"user\",\n      id: \"1\",\n    },\n    targets: [{ id: \"1\", type: \"user\" }],\n    action: \"notification.sent\",\n    metadata: { provider: \"email\" },\n  });\n}\n\n// biome-ignore lint/correctness/noUnusedVariables: <explanation>\nasync function history() {\n  return await auditLog.getAuditLog({ event_id: \"user:1\" });\n}\n\n// seed();\n// const all = await history();\n// console.log(all);\n// const first = all.data[0];\n\n// if (first.action === \"monitor.failed\") {\n//   first.metadata.message;\n// }\n"
  },
  {
    "path": "packages/tinybird/src/audit-log/index.ts",
    "content": "export * from \"./client\";\n"
  },
  {
    "path": "packages/tinybird/src/client.ts",
    "content": "import { Tinybird as Client, NoopTinybird } from \"@chronark/zod-bird\";\nimport { z } from \"zod\";\nimport { monitorRegions } from \"../../db/src/schema/constants\";\nimport {\n  headersSchema,\n  timingPhasesSchema,\n  timingSchema,\n  triggers,\n} from \"./schema\";\n\nconst PUBLIC_CACHE = 300; // 5 * 60 = 300s = 5m\nconst DEV_CACHE = 10 * 60; // 10m\nconst REVALIDATE = process.env.NODE_ENV === \"development\" ? DEV_CACHE : 0;\n\nexport class OSTinybird {\n  private readonly tb: Client;\n\n  constructor(token: string) {\n    if (\n      process.env.NODE_ENV === \"development\" ||\n      process.env.NODE_ENV === \"test\"\n    ) {\n      this.tb = new NoopTinybird();\n    } else {\n      // Use local Tinybird container if available (Docker/self-hosted)\n      // https://www.tinybird.co/docs/api-reference\n      const tinybirdUrl = process.env.TINYBIRD_URL || \"https://api.tinybird.co\";\n      this.tb = new Client({\n        token,\n        baseUrl: tinybirdUrl,\n      });\n    }\n  }\n\n  public get homeStats() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__stats_global__v0\",\n      parameters: z.object({\n        cronTimestamp: z.int().optional(),\n        period: z.enum([\"total\", \"1h\", \"10m\", \"1d\", \"1w\", \"1m\"]).optional(),\n      }),\n      data: z.object({\n        count: z.int(),\n      }),\n      // REMINDER: cache on build time as it's a global stats\n      opts: { cache: \"force-cache\" },\n    });\n  }\n\n  public get legacy_httpListDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_list_1d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        type: z.literal(\"http\").prefault(\"http\"),\n        latency: z.int(),\n        statusCode: z.int().nullable(),\n        monitorId: z.string(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        workspaceId: z.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpListDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_list_1d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.int().optional(),\n        toDate: z.int().optional(),\n      }),\n      data: z.object({\n        type: z.literal(\"http\").prefault(\"http\"),\n        id: z.string().nullable(),\n        latency: z.int(),\n        statusCode: z.int().nullable(),\n        monitorId: z.string(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        timing: timingPhasesSchema,\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_httpListWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_list_7d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        type: z.literal(\"http\").prefault(\"http\"),\n        latency: z.int(),\n        statusCode: z.int().nullable(),\n        monitorId: z.string(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        workspaceId: z.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpListWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_list_7d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.int().optional(),\n        toDate: z.int().optional(),\n      }),\n      data: z.object({\n        type: z.literal(\"http\").prefault(\"http\"),\n        id: z.string().nullable(),\n        latency: z.int(),\n        statusCode: z.int().nullable(),\n        monitorId: z.string(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        timing: timingPhasesSchema,\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_httpListBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_list_14d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        type: z.literal(\"http\").prefault(\"http\"),\n        latency: z.int(),\n        statusCode: z.int().nullable(),\n        monitorId: z.string(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        workspaceId: z.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpListBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_list_14d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.int().optional(),\n        toDate: z.int().optional(),\n      }),\n      data: z.object({\n        type: z.literal(\"http\").prefault(\"http\"),\n        id: z.string().nullable(),\n        latency: z.int(),\n        statusCode: z.int().nullable(),\n        monitorId: z.string(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        timing: timingPhasesSchema,\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_httpMetricsDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_1d__v0\",\n      parameters: z.object({\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int(),\n        ok: z.int(),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_1d__v1\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int().prefault(0),\n        success: z.int().prefault(0),\n        degraded: z.int().prefault(0),\n        error: z.int().prefault(0),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_httpMetricsWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_7d__v0\",\n      parameters: z.object({\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int(),\n        ok: z.int(),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_7d__v1\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int().prefault(0),\n        success: z.int().prefault(0),\n        degraded: z.int().prefault(0),\n        error: z.int().prefault(0),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_httpMetricsBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_14d__v0\",\n      parameters: z.object({\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int(),\n        ok: z.int(),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_14d__v1\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int().prefault(0),\n        success: z.int().prefault(0),\n        degraded: z.int().prefault(0),\n        error: z.int().prefault(0),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsByIntervalDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_by_interval_1d__v0\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsByIntervalWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_by_interval_7d__v0\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsByIntervalBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_by_interval_14d__v0\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsByRegionDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_by_region_1d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        count: z.int(),\n        ok: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsByRegionWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_by_region_7d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        count: z.int(),\n        ok: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsByRegionBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_by_region_14d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        count: z.int(),\n        ok: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpStatusWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_status_7d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        day: z.string().transform((val) => {\n          // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00)\n          return new Date(`${val} GMT`).toISOString();\n        }),\n        count: z.number().prefault(0),\n        ok: z.number().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_httpStatus45d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_status_45d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        days: z.int().max(45).optional(),\n      }),\n      data: z.object({\n        day: z.string().transform((val) => {\n          // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00)\n          return new Date(`${val} GMT`).toISOString();\n        }),\n        count: z.number().prefault(0),\n        ok: z.number().prefault(0),\n      }),\n      opts: {\n        next: {\n          revalidate: PUBLIC_CACHE,\n        },\n      },\n    });\n  }\n\n  public get httpStatus45d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_status_45d__v1\",\n      parameters: z.object({\n        monitorIds: z.string().array(),\n      }),\n      data: z.object({\n        day: z.string().transform((val) => {\n          // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00)\n          return new Date(`${val} GMT`).toISOString();\n        }),\n        count: z.number().prefault(0),\n        ok: z.number().prefault(0),\n        degraded: z.number().prefault(0),\n        error: z.number().prefault(0),\n        monitorId: z.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpGetBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_get_14d__v0\",\n      parameters: z.object({\n        id: z.string().nullable(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        type: z.literal(\"http\").prefault(\"http\"),\n        latency: z.int(),\n        statusCode: z.int().nullable(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        monitorId: z.string(),\n        url: z.url(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        message: z.string().nullable(),\n        headers: headersSchema,\n        timing: timingPhasesSchema,\n        assertions: z.string().nullable(),\n        body: z.string().nullable(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        workspaceId: z.string(),\n        id: z.string().nullable(),\n      }),\n      // REMINDER: cache the result for accessing the data for a check as it won't change\n      opts: { cache: \"force-cache\" },\n    });\n  }\n\n  public get httpGetMonthly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_get_30d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        region: z.enum(monitorRegions).or(z.string()).optional(),\n        cronTimestamp: z.int().optional(),\n      }),\n      data: z.object({\n        type: z.literal(\"http\").prefault(\"http\"),\n        latency: z.int(),\n        statusCode: z.int().nullable(),\n        monitorId: z.string(),\n        url: z.url(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        message: z.string().nullable(),\n        headers: headersSchema,\n        timing: timingSchema,\n        assertions: z.string().nullable(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        workspaceId: z.string(),\n      }),\n      // REMINDER: cache the result for accessing the data for a check as it won't change\n      opts: { cache: \"force-cache\" },\n    });\n  }\n\n  // FIXME: rename to same convension\n  public get getResultForOnDemandCheckHttp() {\n    return this.tb.buildPipe({\n      pipe: \"get_result_for_on_demand_check_http\",\n      parameters: z.object({\n        monitorId: z.int(),\n        timestamp: z.number(),\n        url: z.string(),\n      }),\n      data: z.object({\n        latency: z.int(), // in ms\n        statusCode: z.int().nullable().prefault(null),\n        monitorId: z.string().prefault(\"\"),\n        url: z.url().optional(),\n        error: z\n          .number()\n          .prefault(0)\n          .transform((val) => val !== 0),\n        region: z.enum(monitorRegions),\n        timestamp: z.int().optional(),\n        message: z.string().nullable().optional(),\n        timing: timingSchema,\n        // TODO: make sure to include all data!\n      }),\n      opts: { cache: \"no-store\" },\n    });\n  }\n  // TODO: add tcpChartDaily, tcpChartWeekly\n\n  public get legacy_tcpListDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_list_1d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        type: z.literal(\"tcp\").prefault(\"tcp\"),\n        latency: z.int(),\n        monitorId: z.coerce.string(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        workspaceId: z.coerce.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpListDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_list_1d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.int().optional(),\n        toDate: z.int().optional(),\n      }),\n      data: z.object({\n        type: z.literal(\"tcp\").prefault(\"tcp\"),\n        id: z.string().nullable(),\n        latency: z.int(),\n        monitorId: z.coerce.string(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_tcpListWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_list_7d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        type: z.literal(\"tcp\").prefault(\"tcp\"),\n        latency: z.int(),\n        monitorId: z.coerce.string(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        workspaceId: z.coerce.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpListWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_list_7d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.int().optional(),\n        toDate: z.int().optional(),\n      }),\n      data: z.object({\n        type: z.literal(\"tcp\").prefault(\"tcp\"),\n        id: z.string().nullable(),\n        latency: z.int(),\n        monitorId: z.coerce.string(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_tcpListBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_list_14d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        type: z.literal(\"tcp\").prefault(\"tcp\"),\n        latency: z.int(),\n        monitorId: z.coerce.string(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        workspaceId: z.coerce.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpListBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_list_14d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.int().optional(),\n        toDate: z.int().optional(),\n      }),\n      data: z.object({\n        type: z.literal(\"tcp\").prefault(\"tcp\"),\n        id: z.string().nullable(),\n        latency: z.int(),\n        monitorId: z.coerce.string(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_tcpMetricsDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_1d__v0\",\n      parameters: z.object({\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int(),\n        ok: z.int(),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_1d__v1\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int().prefault(0),\n        success: z.int().prefault(0),\n        degraded: z.int().prefault(0),\n        error: z.int().prefault(0),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_tcpMetricsWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_7d__v0\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int(),\n        ok: z.int(),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_7d__v1\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int().prefault(0),\n        success: z.int().prefault(0),\n        degraded: z.int().prefault(0),\n        error: z.int().prefault(0),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_tcpMetricsBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_14d__v0\",\n      parameters: z.object({\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int(),\n        ok: z.int(),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_14d__v1\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int().prefault(0),\n        success: z.int().prefault(0),\n        degraded: z.int().prefault(0),\n        error: z.int().prefault(0),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsByIntervalDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_by_interval_1d__v0\",\n      parameters: z.object({\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsByIntervalWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_by_interval_7d__v0\",\n      parameters: z.object({\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsByIntervalBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_by_interval_14d__v0\",\n      parameters: z.object({\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        interval: z.int().optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsByRegionDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_by_region_1d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        count: z.int(),\n        ok: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsByRegionWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_by_region_7d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        count: z.int(),\n        ok: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsByRegionBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_by_region_14d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        count: z.int(),\n        ok: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpStatusWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_status_7d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        day: z.string().transform((val) => {\n          // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00)\n          return new Date(`${val} GMT`).toISOString();\n        }),\n        count: z.number().prefault(0),\n        ok: z.number().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get legacy_tcpStatus45d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_status_45d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        days: z.int().max(45).optional(),\n      }),\n      data: z.object({\n        day: z.string().transform((val) => {\n          // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00)\n          return new Date(`${val} GMT`).toISOString();\n        }),\n        count: z.number().prefault(0),\n        ok: z.number().prefault(0),\n      }),\n      opts: {\n        next: {\n          revalidate: PUBLIC_CACHE,\n        },\n      },\n    });\n  }\n\n  public get tcpStatus45d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_status_45d__v1\",\n      parameters: z.object({\n        monitorIds: z.string().array(),\n        days: z.int().max(45).optional(),\n      }),\n      data: z.object({\n        day: z.string().transform((val) => {\n          // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00)\n          return new Date(`${val} GMT`).toISOString();\n        }),\n        count: z.number().prefault(0),\n        ok: z.number().prefault(0),\n        degraded: z.number().prefault(0),\n        error: z.number().prefault(0),\n        monitorId: z.coerce.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpWorkspace30d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_workspace_30d__v0\",\n      parameters: z.object({\n        workspaceId: z.string(),\n      }),\n      data: z.object({\n        day: z\n          .string()\n          .transform((val) => new Date(`${val} GMT`).toISOString()),\n        count: z.int(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpWorkspace30d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_workspace_30d__v0\",\n      parameters: z.object({\n        workspaceId: z.string(),\n      }),\n      data: z.object({\n        day: z\n          .string()\n          .transform((val) => new Date(`${val} GMT`).toISOString()),\n        count: z.int(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpGetBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_get_14d__v0\",\n      parameters: z.object({\n        id: z.string().nullable(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        type: z.literal(\"tcp\").prefault(\"tcp\"),\n        id: z.string().nullable(),\n        uri: z.string(),\n        latency: z.int(),\n        monitorId: z.coerce.string(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        errorMessage: z.string().nullable(),\n      }),\n      // REMINDER: cache the result for accessing the data for a check as it won't change\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpGetMonthly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_get_30d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        region: z.enum(monitorRegions).or(z.string()).optional(),\n        cronTimestamp: z.int().optional(),\n      }),\n      data: z.object({\n        type: z.literal(\"tcp\").prefault(\"tcp\"),\n        latency: z.int(),\n        monitorId: z.string(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        workspaceId: z.string(),\n      }),\n      // REMINDER: cache the result for accessing the data for a check as it won't change\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  /**\n   * Region + timestamp metrics (quantiles) – aggregated by interval.\n   * NOTE: The Tinybird pipe returns one row per region & interval with latency quantiles.\n   */\n  public get httpMetricsRegionsDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_regions_1d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        interval: z.int().optional(),\n        // Comma-separated list of regions, e.g. \"ams,fra\". Keeping string to pass directly.\n        regions: z.string().array().optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsRegionsWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_regions_7d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        interval: z.int().optional(),\n        regions: z.string().array().optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpMetricsRegionsBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_regions_14d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        interval: z.int().optional(),\n        regions: z.string().array().optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpUptimeWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_uptime_7d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n        regions: z.enum(monitorRegions).or(z.string()).array().optional(),\n        interval: z.int().optional(),\n      }),\n      data: z.object({\n        interval: z.coerce.date(),\n        success: z.int(),\n        degraded: z.int(),\n        error: z.int(),\n      }),\n    });\n  }\n\n  public get httpUptime30d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_uptime_30d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n        regions: z.enum(monitorRegions).or(z.string()).array().optional(),\n        interval: z.int().optional(),\n      }),\n      data: z.object({\n        interval: z.coerce.date(),\n        success: z.int(),\n        degraded: z.int(),\n        error: z.int(),\n      }),\n    });\n  }\n\n  public get tcpUptimeWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_uptime_7d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n        regions: z.enum(monitorRegions).or(z.string()).array().optional(),\n        interval: z.int().optional(),\n      }),\n      data: z.object({\n        interval: z.coerce.date(),\n        success: z.int(),\n        degraded: z.int(),\n        error: z.int(),\n      }),\n    });\n  }\n\n  public get tcpUptime30d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_uptime_30d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n        regions: z.enum(monitorRegions).or(z.string()).array().optional(),\n        interval: z.int().optional(),\n      }),\n      data: z.object({\n        interval: z.coerce.date(),\n        success: z.int(),\n        degraded: z.int(),\n        error: z.int(),\n      }),\n    });\n  }\n\n  public get getAuditLog() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__audit_log__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        interval: z.int().prefault(30), // in days\n      }),\n      data: z.object({\n        action: z.string(),\n        id: z.string(),\n        metadata: z.string().transform((str) => {\n          try {\n            return JSON.parse(str) as Record<string, unknown>;\n          } catch (error) {\n            console.error(error);\n            return {};\n          }\n        }),\n        timestamp: z.int(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpGlobalMetricsDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_global_1d__v0\",\n      parameters: z.object({\n        monitorIds: z.string().array(),\n      }),\n      data: z.object({\n        minLatency: z.int(),\n        maxLatency: z.int(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n        lastTimestamp: z.int(),\n        count: z.int(),\n        monitorId: z.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpGlobalMetricsDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_global_1d__v0\",\n      parameters: z.object({\n        monitorIds: z.string().array(),\n      }),\n      data: z.object({\n        minLatency: z.int(),\n        maxLatency: z.int(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n        lastTimestamp: z.int(),\n        count: z.int(),\n        monitorId: z.coerce.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get httpTimingPhases14d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_timing_phases_14d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n      }),\n      data: z.object({\n        timestamp: z.int(),\n        p50Dns: z.int(),\n        p50Ttfb: z.int(),\n        p50Transfer: z.int(),\n        p50Connect: z.int(),\n        p50Tls: z.int(),\n        p75Dns: z.int(),\n        p75Ttfb: z.int(),\n        p75Transfer: z.int(),\n        p75Connect: z.int(),\n        p75Tls: z.int(),\n        p90Dns: z.int(),\n        p90Ttfb: z.int(),\n        p90Transfer: z.int(),\n        p90Connect: z.int(),\n        p90Tls: z.int(),\n        p95Dns: z.int(),\n        p95Ttfb: z.int(),\n        p95Transfer: z.int(),\n        p95Connect: z.int(),\n        p95Tls: z.int(),\n        p99Dns: z.int(),\n        p99Ttfb: z.int(),\n        p99Transfer: z.int(),\n        p99Connect: z.int(),\n        p99Tls: z.int(),\n      }),\n    });\n  }\n\n  public get httpMetricsLatency1d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_latency_1d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n      data: z.object({\n        timestamp: z.int(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n      }),\n    });\n  }\n\n  public get httpMetricsLatency7d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_latency_7d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n      data: z.object({\n        timestamp: z.int(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n      }),\n    });\n  }\n\n  public get httpMetricsLatency1dMulti() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__http_metrics_latency_1d_multi__v1\",\n      parameters: z.object({\n        monitorIds: z.string().array().min(1),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n      data: z.object({\n        timestamp: z.int(),\n        monitorId: z.string(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get tcpMetricsLatency1d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_latency_1d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n      data: z.object({\n        timestamp: z.int(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n      }),\n    });\n  }\n\n  public get tcpMetricsLatency7d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_latency_7d__v1\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n      data: z.object({\n        timestamp: z.int(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n      }),\n    });\n  }\n\n  public get tcpMetricsLatency1dMulti() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__tcp_metrics_latency_1d_multi__v1\",\n      parameters: z.object({\n        monitorIds: z.string().array().min(1),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n      data: z.object({\n        timestamp: z.int(),\n        monitorId: z.coerce.string(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get dnsGetBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_get_14d__v0\",\n      parameters: z.object({\n        id: z.string().nullable(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        type: z.literal(\"dns\").prefault(\"dns\"),\n        id: z.coerce.string().nullable(),\n        uri: z.string(),\n        latency: z.int(),\n        monitorId: z.coerce.string(),\n        error: z.coerce.boolean(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        errorMessage: z.string().nullable(),\n        assertions: z.string().nullable(),\n        records: z\n          .string()\n          .transform((str) => {\n            try {\n              return JSON.parse(str) as Record<string, unknown>;\n            } catch (error) {\n              console.error(error);\n              return {};\n            }\n          })\n          .pipe(z.record(z.string(), z.array(z.string()))),\n      }),\n      // REMINDER: cache the result for accessing the data for a check as it won't change\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get dnsListBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_list_14d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.int().optional(),\n        toDate: z.int().optional(),\n      }),\n      data: z.object({\n        type: z.literal(\"dns\").prefault(\"dns\"),\n        id: z.coerce.string().nullable(),\n        uri: z.string(),\n        latency: z.int(),\n        monitorId: z.coerce.string(),\n        requestStatus: z.enum([\"error\", \"success\", \"degraded\"]).nullable(),\n        region: z.enum(monitorRegions).or(z.string()),\n        cronTimestamp: z.int(),\n        trigger: z.enum(triggers).nullable().prefault(\"cron\"),\n        timestamp: z.number(),\n        records: z\n          .string()\n          .transform((str) => {\n            try {\n              return JSON.parse(str) as Record<string, unknown>;\n            } catch (error) {\n              console.error(error);\n              return {};\n            }\n          })\n          .pipe(z.record(z.string(), z.array(z.string()))),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get dnsMetricsDaily() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_metrics_1d__v0\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int().prefault(0),\n        success: z.int().prefault(0),\n        degraded: z.int().prefault(0),\n        error: z.int().prefault(0),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get dnsMetricsWeekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_metrics_7d__v0\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int().prefault(0),\n        success: z.int().prefault(0),\n        degraded: z.int().prefault(0),\n        error: z.int().prefault(0),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get dnsMetricsBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_metrics_14d__v0\",\n      parameters: z.object({\n        interval: z.int().optional(),\n        regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),\n        monitorId: z.string(),\n      }),\n      data: z.object({\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n        count: z.int().prefault(0),\n        success: z.int().prefault(0),\n        degraded: z.int().prefault(0),\n        error: z.int().prefault(0),\n        lastTimestamp: z.int().nullable(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get dnsUptime30d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_uptime_30d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n        regions: z.enum(monitorRegions).or(z.string()).array().optional(),\n        interval: z.int().optional(),\n      }),\n      data: z.object({\n        interval: z.coerce.date(),\n        success: z.int(),\n        degraded: z.int(),\n        error: z.int(),\n      }),\n    });\n  }\n\n  public get dnsMetricsLatency7d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_metrics_latency_7d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n      data: z.object({\n        timestamp: z.int(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n      }),\n    });\n  }\n\n  public get dnsMetricsRegionsBiweekly() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_metrics_regions_14d__v0\",\n      parameters: z.object({\n        monitorId: z.string(),\n        interval: z.int().optional(),\n        // Comma-separated list of regions, e.g. \"ams,fra\". Keeping string to pass directly.\n        regions: z.string().array().optional(),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n      data: z.object({\n        region: z.enum(monitorRegions).or(z.string()),\n        timestamp: z.int(),\n        p50Latency: z.number().nullable().prefault(0),\n        p75Latency: z.number().nullable().prefault(0),\n        p90Latency: z.number().nullable().prefault(0),\n        p95Latency: z.number().nullable().prefault(0),\n        p99Latency: z.number().nullable().prefault(0),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get dnsStatus45d() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_status_45d__v0\",\n      parameters: z.object({\n        monitorIds: z.string().array(),\n      }),\n      data: z.object({\n        day: z.string().transform((val) => {\n          // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00)\n          return new Date(`${val} GMT`).toISOString();\n        }),\n        count: z.number().prefault(0),\n        ok: z.number().prefault(0),\n        degraded: z.number().prefault(0),\n        error: z.number().prefault(0),\n        monitorId: z.coerce.string(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n\n  public get dnsMetricsLatency1dMulti() {\n    return this.tb.buildPipe({\n      pipe: \"endpoint__dns_metrics_latency_1d_multi__v0\",\n      parameters: z.object({\n        monitorIds: z.string().array().min(1),\n        fromDate: z.string().optional(),\n        toDate: z.string().optional(),\n      }),\n      data: z.object({\n        timestamp: z.int(),\n        monitorId: z.coerce.string(),\n        p50Latency: z.int(),\n        p75Latency: z.int(),\n        p90Latency: z.int(),\n        p95Latency: z.int(),\n        p99Latency: z.int(),\n      }),\n      opts: { next: { revalidate: REVALIDATE } },\n    });\n  }\n}\n"
  },
  {
    "path": "packages/tinybird/src/index.ts",
    "content": "export * from \"./client\";\nexport * from \"./audit-log\";\nexport * from \"@chronark/zod-bird\";\n"
  },
  {
    "path": "packages/tinybird/src/schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const jobTypes = [\"http\", \"tcp\", \"imcp\", \"udp\", \"dns\", \"ssl\"] as const;\nexport const jobTypeEnum = z.enum(jobTypes);\nexport type JobType = z.infer<typeof jobTypeEnum>;\n\nexport const periods = [\"1h\", \"1d\", \"3d\", \"7d\", \"14d\", \"45d\"] as const;\nexport const periodEnum = z.enum(periods);\nexport type Period = z.infer<typeof periodEnum>;\n\nexport const triggers = [\"cron\", \"api\"] as const;\nexport const triggerEnum = z.enum(triggers);\nexport type Trigger = z.infer<typeof triggerEnum>;\n\nexport const headersSchema = z\n  .string()\n  .nullable()\n  .optional()\n  .transform((val) => {\n    if (!val) return null;\n    const value = z.record(z.string(), z.string()).safeParse(JSON.parse(val));\n    if (value.success) return value.data;\n    return null;\n  });\n\nexport const httpTimingSchema = z.object({\n  dnsStart: z.number(),\n  dnsDone: z.number(),\n  connectStart: z.number(),\n  connectDone: z.number(),\n  tlsHandshakeStart: z.number(),\n  tlsHandshakeDone: z.number(),\n  firstByteStart: z.number(),\n  firstByteDone: z.number(),\n  transferStart: z.number(),\n  transferDone: z.number(),\n});\n\nexport function transformTiming(val: string) {\n  if (!val) return null;\n  const value = httpTimingSchema.safeParse(JSON.parse(val));\n  if (value.success) return value.data;\n  return null;\n}\n\nexport function calculateTiming(obj: z.infer<typeof httpTimingSchema>) {\n  if (!obj) return null;\n\n  return {\n    dns: obj.dnsDone - obj.dnsStart,\n    connect: obj.connectDone - obj.connectStart,\n    tls: obj.tlsHandshakeDone - obj.tlsHandshakeStart,\n    ttfb: obj.firstByteDone - obj.firstByteStart,\n    transfer: obj.transferDone - obj.transferStart,\n  };\n}\n\nexport const timingSchema = z\n  .string()\n  .nullable()\n  .optional()\n  .transform((val) => {\n    if (!val) return null;\n    const value = httpTimingSchema.safeParse(JSON.parse(val));\n    if (value.success) return value.data;\n    return null;\n  });\n\nexport const timingPhasesSchema = z\n  .string()\n  .nullable()\n  .optional()\n  .transform((val) => {\n    if (!val) return null;\n    const value = httpTimingSchema.safeParse(JSON.parse(val));\n    if (value.success) return calculateTiming(value.data);\n    return null;\n  });\n"
  },
  {
    "path": "packages/tinybird/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"moduleResolution\": \"node\",\n    \"preserveWatchOutput\": true,\n    \"skipLibCheck\": true,\n    \"noEmit\": true,\n    \"strict\": true\n  }\n}\n"
  },
  {
    "path": "packages/tracker/README.md",
    "content": "TODO: Update the different component/files to use the package as source of\ntruth!\n\n- [x] public/status\n- [ ] package/react dev deps\n- [x] `status-check` on status page\n- [x] tracker `bar` on status page\n- [x] og image api `status-check`\n- [x] monitor (overview) dasboard\n"
  },
  {
    "path": "packages/tracker/package.json",
    "content": "{\n  \"name\": \"@openstatus/tracker\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {},\n  \"dependencies\": {\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/tinybird\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/tracker/src/blacklist.ts",
    "content": "import { isSameDay } from \"./utils\";\n\n/**\n * Blacklist dates where we had issues with data collection\n */\nexport const blacklistDates: Record<string, string> = {\n  \"Fri Aug 25 2023\":\n    \"OpenStatus faced issues between 24.08. and 27.08., preventing data collection.\",\n  \"Sat Aug 26 2023\":\n    \"OpenStatus faced issues between 24.08. and 27.08., preventing data collection.\",\n  \"Wed Oct 18 2023\":\n    \"OpenStatus migrated from Vercel to Fly to improve the performance of the checker.\",\n};\n\nexport function isInBlacklist(day: Date) {\n  const el = Object.keys(blacklistDates).find((date) =>\n    isSameDay(new Date(date), day),\n  );\n  return el ? blacklistDates[el] : undefined;\n}\n"
  },
  {
    "path": "packages/tracker/src/config.ts",
    "content": "import type { StatusDetails, StatusVariant } from \"./types\";\nimport { Status } from \"./types\";\n\nexport const statusDetails: Record<Status, StatusDetails> = {\n  [Status.Operational]: {\n    long: \"All Systems Operational\",\n    short: \"Operational\",\n    variant: \"up\",\n  },\n  [Status.DegradedPerformance]: {\n    long: \"Degraded Performance\",\n    short: \"Degraded\",\n    variant: \"degraded\",\n  },\n  [Status.PartialOutage]: {\n    long: \"Partial Outage\",\n    short: \"Outage\",\n    variant: \"down\",\n  },\n  [Status.MajorOutage]: {\n    long: \"Major Outage\",\n    short: \"Outage\",\n    variant: \"down\",\n  },\n  [Status.UnderMaintenance]: {\n    long: \"Under Maintenance\",\n    short: \"Maintenance\",\n    variant: \"maintenance\",\n  },\n  [Status.Unknown]: {\n    long: \"Unknown\",\n    short: \"Unknown\",\n    variant: \"empty\",\n  },\n  [Status.Incident]: {\n    long: \"Downtime\",\n    short: \"Downtime\",\n    variant: \"incident\",\n  },\n};\n\n// TODO: include more variants especially for the '< 10 min' incidents e.g.\n// REMINDER: add `@openstatus/tracker/src/**/*.ts into tailwindcss content prop */\nexport const classNames: Record<StatusVariant, string> = {\n  up: \"bg-status-operational/90 data-[state=open]:bg-status-operational border-status-operational\",\n  degraded:\n    \"bg-status-degraded/90 data-[state=open]:bg-status-degraded border-status-degraded\",\n  down: \"bg-status-down/90 data-[state=open]:bg-status-down border-status-down\",\n  empty: \"bg-muted-foreground/20 data-[state=open]:bg-muted-foreground/30\",\n  incident:\n    \"bg-status-down/90 data-[state=open]:bg-status-down border-status-down\",\n  maintenance:\n    \"bg-status-monitoring/90 data-[state=open]:bg-status-monitoring border-status-monitoring\",\n};\n"
  },
  {
    "path": "packages/tracker/src/index.ts",
    "content": "export * from \"./tracker\";\nexport * from \"./types\";\nexport * from \"./config\";\nexport * from \"./utils\";\n"
  },
  {
    "path": "packages/tracker/src/mock.ts",
    "content": "import type { Monitor } from \"@openstatus/tinybird\";\n\nimport { Tracker } from \"./tracker\";\n\nexport const mockMonitor: Monitor[] = [\n  { day: \"2024-02-21 00:00:00\", count: 762, ok: 762 },\n  { day: \"2024-02-20 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-19 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-18 00:00:00\", count: 834, ok: 834 },\n  { day: \"2024-02-17 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-16 00:00:00\", count: 863, ok: 863 },\n  { day: \"2024-02-15 00:00:00\", count: 862, ok: 862 },\n  { day: \"2024-02-14 00:00:00\", count: 876, ok: 876 },\n  { day: \"2024-02-13 00:00:00\", count: 876, ok: 876 },\n  { day: \"2024-02-12 00:00:00\", count: 882, ok: 882 },\n  { day: \"2024-02-11 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-10 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-09 00:00:00\", count: 846, ok: 846 },\n  { day: \"2024-02-08 00:00:00\", count: 870, ok: 870 },\n  { day: \"2024-02-07 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-06 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-05 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-04 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-03 00:00:00\", count: 858, ok: 858 },\n  { day: \"2024-02-02 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-02-01 00:00:00\", count: 870, ok: 870 },\n  { day: \"2024-01-31 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-30 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-29 00:00:00\", count: 859, ok: 859 },\n  { day: \"2024-01-28 00:00:00\", count: 860, ok: 860 },\n  { day: \"2024-01-27 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-26 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-25 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-24 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-23 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-22 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-21 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-20 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-19 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-18 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-17 00:00:00\", count: 863, ok: 862 },\n  { day: \"2024-01-16 00:00:00\", count: 795, ok: 795 },\n  { day: \"2024-01-15 00:00:00\", count: 846, ok: 846 },\n  { day: \"2024-01-14 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-13 00:00:00\", count: 852, ok: 852 },\n  { day: \"2024-01-12 00:00:00\", count: 864, ok: 857 },\n  { day: \"2024-01-11 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-10 00:00:00\", count: 865, ok: 864 },\n  { day: \"2024-01-09 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-08 00:00:00\", count: 864, ok: 864 },\n  { day: \"2024-01-07 00:00:00\", count: 671, ok: 671 },\n];\n\nconst tracker = new Tracker({ data: mockMonitor });\nconsole.log(tracker.totalUptime);\n"
  },
  {
    "path": "packages/tracker/src/tracker.ts",
    "content": "import type {\n  Incident,\n  Maintenance,\n  StatusReport,\n  StatusReportUpdate,\n} from \"@openstatus/db/src/schema\";\n\nimport { isInBlacklist } from \"./blacklist\";\nimport { classNames, statusDetails } from \"./config\";\nimport type { StatusDetails, StatusVariant } from \"./types\";\nimport { Status } from \"./types\";\nimport { endOfDay, isSameDay, startOfDay } from \"./utils\";\n\ntype Monitor = {\n  count: number;\n  ok: number;\n  day: string;\n};\ntype StatusReports = (StatusReport & {\n  statusReportUpdates?: StatusReportUpdate[];\n})[];\ntype Incidents = Incident[];\ntype Maintenances = Maintenance[];\n\n/**\n * Tracker Class is supposed to handle the data and calculate from a single monitor.\n * But we use it to handle the StatusCheck as well (with no data for a single monitor).\n * We can create Inheritence to handle the StatusCheck and Monitor separately and even\n * StatusPage with multiple Monitors.\n */\nexport class Tracker {\n  private data: Monitor[] = [];\n  private statusReports: StatusReports = [];\n  private incidents: Incidents = [];\n  private maintenances: Maintenances = [];\n\n  constructor(arg: {\n    data?: Monitor[];\n    statusReports?: StatusReports;\n    incidents?: Incidents;\n    maintenances?: Maintenance[];\n  }) {\n    this.data = arg.data || []; // TODO: use another Class to handle a single Day\n    this.statusReports = arg.statusReports || [];\n    this.incidents = arg.incidents || [];\n    this.maintenances = arg.maintenances || [];\n  }\n\n  private calculateUptime(data: { ok: number; count: number }[]) {\n    const { count, ok } = this.aggregatedData(data);\n    if (count === 0) return 100; // starting with 100% uptime\n    return Math.round((ok / count) * 10_000) / 100; // round to 2 decimal places\n  }\n\n  private aggregatedData(data: { ok: number; count: number }[]) {\n    return data.reduce(\n      (prev, curr) => {\n        prev.ok += curr.ok;\n        prev.count += curr.count;\n        return prev;\n      },\n      { count: 0, ok: 0 },\n    );\n  }\n\n  get isDataMissing() {\n    const { count } = this.aggregatedData(this.data);\n    return count === 0;\n  }\n\n  private calculateUptimeStatus(data: { ok: number; count: number }[]): Status {\n    const uptime = this.calculateUptime(data);\n    if (uptime >= 98) return Status.Operational;\n    if (uptime >= 60) return Status.DegradedPerformance;\n    if (uptime > 30) return Status.PartialOutage;\n    return Status.MajorOutage;\n  }\n\n  private isOngoingIncident() {\n    return this.incidents.some((incident) => !incident.resolvedAt);\n  }\n\n  private isOngoingReport() {\n    const resolved: StatusReport[\"status\"][] = [\"monitoring\", \"resolved\"];\n    return this.statusReports.some(\n      (report) => !resolved.includes(report.status),\n    );\n  }\n\n  private isOngoingMaintenance() {\n    return this.maintenances.some((maintenance) => {\n      const now = new Date();\n      return (\n        new Date(maintenance.from).getTime() <= now.getTime() &&\n        new Date(maintenance.to).getTime() >= now.getTime()\n      );\n    });\n  }\n\n  get totalUptime(): number {\n    return this.calculateUptime(this.data);\n  }\n\n  get currentStatus(): Status {\n    if (this.isOngoingMaintenance()) return Status.UnderMaintenance;\n    if (this.isOngoingReport()) return Status.DegradedPerformance;\n    if (this.isOngoingIncident()) return Status.Incident;\n    return this.calculateUptimeStatus(this.data);\n  }\n\n  get currentVariant(): StatusVariant {\n    return statusDetails[this.currentStatus].variant;\n  }\n\n  get currentDetails(): StatusDetails {\n    return statusDetails[this.currentStatus];\n  }\n\n  get currentClassName(): string {\n    return classNames[this.currentVariant];\n  }\n\n  // HACK: this is a temporary solution to get the incidents\n  private getIncidentsByDay(day: Date): Incidents {\n    const incidents = this.incidents?.filter((incident) => {\n      const { startedAt, resolvedAt } = incident;\n      const eod = endOfDay(day);\n      const sod = startOfDay(day);\n\n      if (!startedAt) return false; // not started\n\n      const hasStartedAfterEndOfDay = startedAt.getTime() >= eod.getTime();\n\n      if (hasStartedAfterEndOfDay) return false;\n\n      if (!resolvedAt) return true; // still ongoing\n\n      const hasResolvedBeforeStartOfDay = resolvedAt.getTime() <= sod.getTime();\n\n      if (hasResolvedBeforeStartOfDay) return false;\n\n      const hasStartedBeforeEndOfDay = startedAt.getTime() <= eod.getTime();\n\n      const hasResolvedBeforeEndOfDay = resolvedAt.getTime() <= eod.getTime();\n\n      if (hasStartedBeforeEndOfDay || hasResolvedBeforeEndOfDay) return true;\n\n      return false;\n    });\n\n    return incidents;\n  }\n\n  // HACK: this is a temporary solution to get the status reports\n  private getStatusReportsByDay(props: Monitor): StatusReports {\n    const statusReports = this.statusReports?.filter((report) => {\n      const firstStatusReportUpdate = report?.statusReportUpdates?.sort(\n        (a, b) => a.date.getTime() - b.date.getTime(),\n      )?.[0];\n\n      if (!firstStatusReportUpdate) return false;\n\n      const day = new Date(props.day);\n      return isSameDay(firstStatusReportUpdate.date, day);\n    });\n    return statusReports;\n  }\n\n  private getMaintenancesByDay(day: Date): Maintenances {\n    const maintenances = this.maintenances.filter((maintenance) => {\n      const eod = endOfDay(day);\n      const sod = startOfDay(day);\n      return (\n        maintenance.from.getTime() <= eod.getTime() &&\n        maintenance.to.getTime() >= sod.getTime()\n      );\n    });\n    return maintenances;\n  }\n\n  // TODO: it would be great to create a class to handle a single day\n  // FIXME: will be always generated on each tracker.days call - needs to be in the constructor?\n  get days() {\n    const data = this.data.map((props) => {\n      const day = new Date(props.day);\n      const blacklist = isInBlacklist(day);\n      const incidents = this.getIncidentsByDay(day);\n      const statusReports = this.getStatusReportsByDay(props);\n      const maintenances = this.getMaintenancesByDay(day);\n\n      const isMissingData = props.count === 0;\n\n      /**\n       * 1. Maintenance\n       * 2. Status Reports (Degraded Performance)\n       * 3. Incidents\n       * 4. Uptime Status (Operational, Degraded Performance, Partial Outage, Major Outage)\n       */\n      const status = maintenances.length\n        ? Status.UnderMaintenance\n        : statusReports.length\n          ? Status.DegradedPerformance\n          : incidents.length\n            ? Status.Incident\n            : isMissingData\n              ? Status.Unknown\n              : this.calculateUptimeStatus([props]);\n\n      const variant = statusDetails[status].variant;\n      const label = statusDetails[status].short;\n\n      return {\n        ...props,\n        blacklist,\n        incidents,\n        statusReports,\n        maintenances,\n        status,\n        variant,\n        label: isMissingData ? \"Missing\" : label,\n      };\n    });\n    return data;\n  }\n\n  get toString() {\n    return statusDetails[this.currentStatus].short;\n  }\n}\n"
  },
  {
    "path": "packages/tracker/src/types.ts",
    "content": "import type { Incident, StatusReport } from \"@openstatus/db/src/schema\";\n\n// DO NOT CHANGE!\nexport enum Status {\n  Operational = \"operational\",\n  DegradedPerformance = \"degraded_performance\",\n  PartialOutage = \"partial_outage\",\n  MajorOutage = \"major_outage\",\n  UnderMaintenance = \"under_maintenance\",\n  Unknown = \"unknown\",\n  Incident = \"incident\",\n}\n\n// TODO: duplicate to `Status` enum above\nexport type StatusVariant =\n  | \"up\"\n  | \"degraded\"\n  | \"down\"\n  | \"empty\"\n  | \"incident\"\n  | \"maintenance\";\n\nexport type StatusDetails = {\n  long: string;\n  short: string;\n  variant: StatusVariant;\n};\n\n/**\n * Data used for the `Bar` component within the `Tracker` component.\n */\nexport type TrackerData = {\n  ok: number;\n  count: number;\n  date: Date;\n  incidents: Incident[];\n  statusReports: StatusReport[];\n  status: Status;\n  variant: StatusVariant;\n};\n"
  },
  {
    "path": "packages/tracker/src/utils.ts",
    "content": "export function endOfDay(date: Date): Date {\n  // Create a new Date object to avoid mutating the original date\n  // const newDate = new Date(date);\n  const newDate = new Date(\n    Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),\n  );\n\n  // Set hours, minutes, seconds, and milliseconds to end of day\n  newDate.setUTCHours(23, 59, 59, 999);\n\n  return newDate;\n}\n\nexport function startOfDay(date: Date): Date {\n  // Create a new Date object to avoid mutating the original date\n  // const newDate = new Date(date);\n  const newDate = new Date(\n    Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),\n  );\n\n  // Set hours, minutes, seconds, and milliseconds to start of day\n  newDate.setUTCHours(0, 0, 0, 0);\n\n  return newDate;\n}\n\nexport function isSameDay(date1: Date, date2: Date) {\n  const newDate1 = startOfDay(date1);\n  const newDate2 = startOfDay(date2);\n\n  return newDate1.toUTCString() === newDate2.toUTCString();\n}\n"
  },
  {
    "path": "packages/tracker/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2021\"\n  }\n}\n"
  },
  {
    "path": "packages/tsconfig/base.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Default\",\n  \"compilerOptions\": {\n    \"composite\": false,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"inlineSources\": false,\n    \"isolatedModules\": true,\n    \"moduleResolution\": \"node\",\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"preserveWatchOutput\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"strictNullChecks\": true\n  },\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/tsconfig/nextjs.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Next.js\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"allowJs\": true,\n    \"declaration\": false,\n    \"declarationMap\": false,\n    \"incremental\": true,\n    \"jsx\": \"preserve\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"noEmit\": true,\n    \"resolveJsonModule\": true,\n    \"strict\": true,\n    \"target\": \"ES2021\"\n  },\n  \"include\": [\"src\", \"next-env.d.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/tsconfig/package.json",
    "content": "{\n  \"name\": \"@openstatus/tsconfig\",\n  \"version\": \"0.0.1\"\n}\n"
  },
  {
    "path": "packages/tsconfig/react-library.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"React Library\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\"ES2015\", \"DOM\"],\n    \"module\": \"ESNext\",\n    \"target\": \"es6\"\n  }\n}\n"
  },
  {
    "path": "packages/ui/REGISTRY.md",
    "content": "# OpenStatus UI Registry\n\nThis package provides a shadcn/ui registry that can be used to install OpenStatus UI components.\n\n## Building the Registry\n\nThe registry is automatically built when building the web app. To manually build:\n\n```bash\npnpm registry:build\n```\n\nThis will:\n1. Transform all `@openstatus/ui/*` imports to `@/*`\n2. Build the shadcn registry\n3. Copy the registry files to `apps/web/public/r/`\n\n## Using the Registry\n\nOnce deployed, the registry will be available at:\n\n```\nhttps://openstatus.dev/r/registry.json\n```\n\nUsers can install components from this registry using:\n\n```bash\nnpx shadcn@latest add https://openstatus.dev/r/example\n```\n\nOr configure it as their registry in `components.json`:\n\n```json\n{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"registry\": \"https://openstatus.dev/r\"\n}\n```\n\n## Adding Components to the Registry\n\nTo add a new component to the registry, update `packages/ui/registry.json`:\n\n```json\n{\n  \"items\": [\n    {\n      \"name\": \"your-component\",\n      \"type\": \"registry:block\",\n      \"title\": \"Your Component\",\n      \"description\": \"Description of your component\",\n      \"registryDependencies\": [\"button\", \"card\"],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/your-component.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/your-component.tsx\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nThen run `pnpm registry:build` to regenerate the registry.\n\n## Build Process\n\nThe build process is integrated with Turborepo:\n\n- The web app build depends on `@openstatus/ui#registry:build`\n- This ensures the registry is always up-to-date when deploying the web app\n- Registry files are copied to `apps/web/public/r/` and served statically\n\n## Development\n\nThe registry source files are in `packages/ui/src/`:\n- `src/components/ui/*` - UI components\n- `src/components/blocks/*` - Component blocks\n- `src/lib/*` - Utility functions\n\nAll imports use `@openstatus/ui/*` internally, which are transformed to `@/*` during the registry build.\n"
  },
  {
    "path": "packages/ui/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"src/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@openstatus/ui/components\",\n    \"utils\": \"@openstatus/ui/lib/utils\",\n    \"ui\": \"@openstatus/ui/components/ui\",\n    \"lib\": \"@openstatus/ui/lib\",\n    \"hooks\": \"@openstatus/ui/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}\n"
  },
  {
    "path": "packages/ui/package.json",
    "content": "{\n  \"name\": \"@openstatus/ui\",\n  \"version\": \"0.0.0\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"./components/ui/*\": {\n      \"types\": \"./src/components/ui/*.tsx\",\n      \"default\": \"./src/components/ui/*.tsx\"\n    },\n    \"./components/blocks/status.types\": {\n      \"types\": \"./src/components/blocks/status.types.ts\",\n      \"default\": \"./src/components/blocks/status.types.ts\"\n    },\n    \"./components/blocks/status.utils\": {\n      \"types\": \"./src/components/blocks/status.utils.ts\",\n      \"default\": \"./src/components/blocks/status.utils.ts\"\n    },\n    \"./components/blocks/*\": {\n      \"types\": \"./src/components/blocks/*.tsx\",\n      \"default\": \"./src/components/blocks/*.tsx\"\n    },\n    \"./lib/*\": {\n      \"types\": \"./src/lib/*.ts\",\n      \"default\": \"./src/lib/*.ts\"\n    },\n    \"./hooks/*\": {\n      \"types\": \"./src/hooks/*.ts\",\n      \"default\": \"./src/hooks/*.ts\"\n    },\n    \"./globals\": \"./src/globals.css\"\n  },\n  \"scripts\": {\n    \"build\": \"echo '@openstatus/ui has no build step (registry builds separately)'\",\n    \"registry:build\": \"node scripts/transform-imports.mjs && cd dist && shadcn build && cd .. && node scripts/copy-to-web.mjs\",\n    \"lint\": \"biome lint \\\"**/*.ts*\\\"\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"1.9.4\",\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/luxon\": \"3.4.2\",\n    \"@types/node\": \"22.10.2\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"glob\": \"11.0.0\",\n    \"shadcn\": \"3.8.4\",\n    \"tailwindcss\": \"4.1.8\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"dependencies\": {\n    \"@date-fns/utc\": \"2.1.0\",\n    \"@dnd-kit/core\": \"6.3.1\",\n    \"@dnd-kit/modifiers\": \"9.0.0\",\n    \"@dnd-kit/sortable\": \"10.0.0\",\n    \"@dnd-kit/utilities\": \"3.2.2\",\n    \"@hookform/resolvers\": \"5.1.0\",\n    \"@radix-ui/react-accordion\": \"1.2.2\",\n    \"@radix-ui/react-alert-dialog\": \"1.1.4\",\n    \"@radix-ui/react-avatar\": \"1.1.10\",\n    \"@radix-ui/react-checkbox\": \"1.3.2\",\n    \"@radix-ui/react-collapsible\": \"1.1.11\",\n    \"@radix-ui/react-context-menu\": \"2.2.4\",\n    \"@radix-ui/react-dialog\": \"1.1.14\",\n    \"@radix-ui/react-dropdown-menu\": \"2.1.15\",\n    \"@radix-ui/react-hover-card\": \"1.1.14\",\n    \"@radix-ui/react-label\": \"2.1.7\",\n    \"@radix-ui/react-navigation-menu\": \"1.2.3\",\n    \"@radix-ui/react-popover\": \"1.1.14\",\n    \"@radix-ui/react-progress\": \"1.1.7\",\n    \"@radix-ui/react-radio-group\": \"1.3.7\",\n    \"@radix-ui/react-select\": \"2.2.5\",\n    \"@radix-ui/react-separator\": \"1.1.7\",\n    \"@radix-ui/react-slider\": \"1.3.5\",\n    \"@radix-ui/react-slot\": \"1.2.3\",\n    \"@radix-ui/react-switch\": \"1.2.5\",\n    \"@radix-ui/react-tabs\": \"1.1.12\",\n    \"@radix-ui/react-toggle\": \"1.1.10\",\n    \"@radix-ui/react-tooltip\": \"1.2.7\",\n    \"class-variance-authority\": \"0.7.1\",\n    \"clsx\": \"2.1.1\",\n    \"cmdk\": \"1.1.1\",\n    \"date-fns\": \"3.6.0\",\n    \"lucide-react\": \"0.525.0\",\n    \"luxon\": \"3.5.0\",\n    \"next\": \"16.1.6\",\n    \"qr-code-styling\": \"1.9.2\",\n    \"radix-ui\": \"1.4.3\",\n    \"react\": \"19.2.3\",\n    \"react-day-picker\": \"8.10.1\",\n    \"react-hook-form\": \"7.68.0\",\n    \"recharts\": \"2.15.4\",\n    \"tailwind-merge\": \"2.5.5\",\n    \"tailwindcss-animate\": \"1.0.7\",\n    \"zod\": \"4.1.13\"\n  },\n  \"peerDependencies\": {\n    \"next-themes\": \"0.4.6\",\n    \"sonner\": \"2.0.5\"\n  }\n}\n"
  },
  {
    "path": "packages/ui/registry.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"openstatus\",\n  \"homepage\": \"https://openstatus.dev\",\n  \"version\": \"0.1.0\",\n  \"style\": \"default\",\n  \"tailwind\": {\n    \"config\": {\n      \"theme\": {\n        \"extend\": {\n          \"colors\": {\n            \"success\": \"hsl(var(--success))\",\n            \"success-foreground\": \"hsl(var(--success-foreground))\",\n            \"warning\": \"hsl(var(--warning))\",\n            \"warning-foreground\": \"hsl(var(--warning-foreground))\",\n            \"destructive\": \"hsl(var(--destructive))\",\n            \"destructive-foreground\": \"hsl(var(--destructive-foreground))\",\n            \"info\": \"hsl(var(--info))\",\n            \"info-foreground\": \"hsl(var(--info-foreground))\"\n          }\n        }\n      }\n    }\n  },\n  \"cssVars\": {\n    \"light\": {\n      \"success\": \"142.1 76.2% 36.3%\",\n      \"success-foreground\": \"355.7 100% 97.3%\",\n      \"warning\": \"38 92% 50%\",\n      \"warning-foreground\": \"48 96% 89%\",\n      \"destructive\": \"0 84.2% 60.2%\",\n      \"destructive-foreground\": \"0 0% 98%\",\n      \"info\": \"221.2 83.2% 53.3%\",\n      \"info-foreground\": \"210 40% 98%\"\n    },\n    \"dark\": {\n      \"success\": \"142.1 70.6% 45.3%\",\n      \"success-foreground\": \"144.9 80.4% 10%\",\n      \"warning\": \"38 92% 50%\",\n      \"warning-foreground\": \"48 96% 89%\",\n      \"destructive\": \"0 72.2% 50.6%\",\n      \"destructive-foreground\": \"0 85.7% 97.3%\",\n      \"info\": \"217.2 91.2% 59.8%\",\n      \"info-foreground\": \"222.2 47.4% 11.2%\"\n    }\n  },\n  \"items\": [\n    {\n      \"name\": \"use-media-query\",\n      \"type\": \"registry:hook\",\n      \"title\": \"Use Media Query\",\n      \"description\": \"React hook for responsive media query detection\",\n      \"files\": [\n        {\n          \"path\": \"src/hooks/use-media-query.ts\",\n          \"type\": \"registry:hook\",\n          \"target\": \"hooks/use-media-query.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"use-copy-to-clipboard\",\n      \"type\": \"registry:hook\",\n      \"title\": \"Use Copy to Clipboard\",\n      \"description\": \"React hook for copying text to clipboard with toast notifications\",\n      \"registryDependencies\": [\"sonner\"],\n      \"files\": [\n        {\n          \"path\": \"src/hooks/use-copy-to-clipboard.ts\",\n          \"type\": \"registry:hook\",\n          \"target\": \"hooks/use-copy-to-clipboard.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-types\",\n      \"type\": \"registry:lib\",\n      \"title\": \"Status Types\",\n      \"description\": \"TypeScript type definitions for status components\",\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status.types.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/blocks/status.types.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-utils\",\n      \"type\": \"registry:lib\",\n      \"title\": \"Status Utilities\",\n      \"description\": \"Utility functions for status formatting and display\",\n      \"dependencies\": [\"date-fns\"],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status.utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/blocks/status.utils.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-icon\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Icon\",\n      \"description\": \"Unified status icon component with variants for different contexts\",\n      \"registryDependencies\": [\"status-types\"],\n      \"dependencies\": [\"lucide-react\"],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-icon.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-icon.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-layout\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Layout\",\n      \"description\": \"Layout primitives for composing status displays (Status, StatusHeader, StatusTitle, etc.)\",\n      \"registryDependencies\": [\"status-icon\"],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-layout.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-layout.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-blank\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Blank States\",\n      \"description\": \"Empty and skeleton state components for loading and no-data scenarios\",\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-blank.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-blank.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-timestamp\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Timestamp\",\n      \"description\": \"Interactive timestamp display with tooltip (simple) or hover-card (rich) variants showing multiple timezone formats\",\n      \"registryDependencies\": [\n        \"hover-card\",\n        \"tooltip\",\n        \"use-copy-to-clipboard\",\n        \"use-media-query\"\n      ],\n      \"dependencies\": [\"date-fns\", \"@date-fns/utc@2.1.0\", \"lucide-react\"],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-timestamp.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-timestamp.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-banner\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Banner\",\n      \"description\": \"Alert-style status banner with tabs support and timestamp display\",\n      \"registryDependencies\": [\n        \"status-icon\",\n        \"status-types\",\n        \"status-utils\",\n        \"status-timestamp\",\n        \"tabs\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-banner.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-banner.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-component\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Component\",\n      \"description\": \"Monitor component primitives for building status displays\",\n      \"registryDependencies\": [\n        \"status-icon\",\n        \"status-types\",\n        \"skeleton\",\n        \"tooltip\",\n        \"use-media-query\"\n      ],\n      \"dependencies\": [\"date-fns\", \"lucide-react\"],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-component.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-component.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-component-group\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Component Group\",\n      \"description\": \"Collapsible group wrapper for status components\",\n      \"registryDependencies\": [\n        \"status-component\",\n        \"status-types\",\n        \"collapsible\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-component-group.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-component-group.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-events\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Events\",\n      \"description\": \"Event timeline components for displaying status reports and maintenance updates\",\n      \"registryDependencies\": [\n        \"status-types\",\n        \"status-utils\",\n        \"status-timestamp\",\n        \"badge\",\n        \"separator\",\n        \"tooltip\"\n      ],\n      \"dependencies\": [\"date-fns\", \"lucide-react\"],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-events.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-events.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-bar\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Bar\",\n      \"description\": \"Interactive status timeline with hover, keyboard navigation, and event display\",\n      \"registryDependencies\": [\n        \"status-types\",\n        \"status-utils\",\n        \"hover-card\",\n        \"separator\",\n        \"skeleton\",\n        \"use-media-query\"\n      ],\n      \"dependencies\": [\"date-fns\"],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-bar.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-bar.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-feed\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Feed\",\n      \"description\": \"Unified feed component combining status reports and maintenance events\",\n      \"registryDependencies\": [\"status-blank\", \"status-events\", \"status-types\"],\n      \"files\": [\n        {\n          \"path\": \"src/components/blocks/status-feed.tsx\",\n          \"type\": \"registry:ui\",\n          \"target\": \"components/blocks/status-feed.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"status-complete\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Page (Complete)\",\n      \"description\": \"Complete status page components including all primitives, displays, and interactive elements\",\n      \"registryDependencies\": [\n        \"status-types\",\n        \"status-utils\",\n        \"status-icon\",\n        \"status-layout\",\n        \"status-blank\",\n        \"status-banner\",\n        \"status-component\",\n        \"status-component-group\",\n        \"status-events\",\n        \"status-bar\",\n        \"status-feed\"\n      ]\n    },\n    {\n      \"name\": \"status-essentials\",\n      \"type\": \"registry:block\",\n      \"title\": \"Status Page (Essentials)\",\n      \"description\": \"Essential status page components for basic status displays\",\n      \"registryDependencies\": [\n        \"status-types\",\n        \"status-utils\",\n        \"status-icon\",\n        \"status-layout\",\n        \"status-banner\",\n        \"status-blank\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/ui/scripts/copy-to-web.mjs",
    "content": "#!/usr/bin/env node\n\nimport { cpSync, existsSync, mkdirSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\nconst ROOT_DIR = join(__dirname, \"..\");\nconst DIST_PUBLIC_DIR = join(ROOT_DIR, \"dist/public\");\nconst PUBLIC_DIR = join(ROOT_DIR, \"public\");\nconst WEB_APP_PUBLIC_DIR = join(ROOT_DIR, \"../../apps/web/public\");\n\nconsole.log(\"📦 Copying registry to web app...\");\n\n// Check if the registry was built in dist/public/r or public/r\nlet registryDir = join(DIST_PUBLIC_DIR, \"r\");\nif (!existsSync(registryDir)) {\n  registryDir = join(PUBLIC_DIR, \"r\");\n  if (!existsSync(registryDir)) {\n    console.error(\"❌ Registry not found at dist/public/r/ or public/r/\");\n    console.error(\"   Run 'pnpm registry:build' first\");\n    process.exit(1);\n  }\n}\n\n// Ensure web app public directory exists\nif (!existsSync(WEB_APP_PUBLIC_DIR)) {\n  mkdirSync(WEB_APP_PUBLIC_DIR, { recursive: true });\n}\n\n// Copy registry to web app\nconst targetDir = join(WEB_APP_PUBLIC_DIR, \"r\");\ncpSync(registryDir, targetDir, { recursive: true, force: true });\n\nconsole.log(`✅ Registry copied to ${targetDir}`);\nconsole.log(\"🌐 The registry is now available in the web app!\");\n"
  },
  {
    "path": "packages/ui/scripts/transform-imports.mjs",
    "content": "#!/usr/bin/env node\n\nimport {\n  cpSync,\n  existsSync,\n  mkdirSync,\n  readFileSync,\n  rmSync,\n  writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { globSync } from \"glob\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\nconst ROOT_DIR = join(__dirname, \"..\");\nconst SRC_DIR = join(ROOT_DIR, \"src\");\nconst DIST_DIR = join(ROOT_DIR, \"dist\");\n\nconsole.log(\"🔄 Starting import transformation...\");\n\n// Clean up any existing dist directory\nif (existsSync(DIST_DIR)) {\n  rmSync(DIST_DIR, { recursive: true, force: true });\n}\n\n// Create dist directory\nmkdirSync(DIST_DIR, { recursive: true });\n\n// Copy source files to dist directory\nconsole.log(\"📁 Copying source files to dist directory...\");\ncpSync(SRC_DIR, join(DIST_DIR, \"src\"), { recursive: true });\n\n// Transform imports in all TypeScript/TSX files\nconsole.log(\"✨ Transforming imports from @openstatus/ui to @...\");\nconst files = globSync(\"**/*.{ts,tsx}\", {\n  cwd: join(DIST_DIR, \"src\"),\n  absolute: true,\n});\n\nlet transformCount = 0;\nfor (const file of files) {\n  let content = readFileSync(file, \"utf-8\");\n  const originalContent = content;\n\n  // Replace all @openstatus/ui imports with @\n  content = content.replace(\n    /@openstatus\\/ui\\/(components|lib|hooks|types)/g,\n    \"@/$1\",\n  );\n\n  if (content !== originalContent) {\n    writeFileSync(file, content, \"utf-8\");\n    transformCount++;\n  }\n}\n\nconsole.log(`✅ Transformed ${transformCount} files`);\n\n// Create a temporary tsconfig.json with updated paths\nconsole.log(\"📝 Creating temporary tsconfig.json...\");\nconst originalTsConfig = JSON.parse(\n  readFileSync(join(ROOT_DIR, \"tsconfig.json\"), \"utf-8\"),\n);\nconst tmpTsConfig = {\n  ...originalTsConfig,\n  compilerOptions: {\n    ...originalTsConfig.compilerOptions,\n    paths: {\n      \"@/*\": [\"./src/*\"],\n    },\n  },\n};\nwriteFileSync(\n  join(DIST_DIR, \"tsconfig.json\"),\n  JSON.stringify(tmpTsConfig, null, 2),\n  \"utf-8\",\n);\n\n// Transform and copy registry.json with full URLs for custom dependencies\nconsole.log(\"📋 Transforming registry.json...\");\nconst registryJsonPath = join(ROOT_DIR, \"registry.json\");\nif (existsSync(registryJsonPath)) {\n  const registryJson = JSON.parse(readFileSync(registryJsonPath, \"utf-8\"));\n\n  // Determine the base URL for the registry\n  const baseUrl =\n    process.env.VERCEL_ENV === \"production\"\n      ? \"https://openstatus.dev/r\"\n      : process.env.VERCEL_URL\n        ? `https://${process.env.VERCEL_URL}/r`\n        : \"https://openstatus.dev/r\";\n\n  console.log(`🔗 Using registry base URL: ${baseUrl}`);\n\n  // Transform registryDependencies for all items\n  if (registryJson.items) {\n    for (const item of registryJson.items) {\n      if (\n        item.registryDependencies &&\n        Array.isArray(item.registryDependencies)\n      ) {\n        item.registryDependencies = item.registryDependencies.map((dep) => {\n          // Prefix custom OpenStatus components (status-* and use-*) with full URL\n          if (dep.startsWith(\"status-\") || dep.startsWith(\"use-\")) {\n            return `${baseUrl}/${dep}.json`;\n          }\n          // Leave shadcn components unprefixed\n          return dep;\n        });\n      }\n    }\n  }\n\n  writeFileSync(\n    join(DIST_DIR, \"registry.json\"),\n    JSON.stringify(registryJson, null, 2),\n    \"utf-8\",\n  );\n  console.log(\n    \"✅ Transformed registry.json with full URLs for custom dependencies\",\n  );\n}\n\n// Transform and copy components.json\nconsole.log(\"📝 Transforming components.json...\");\nconst componentsJsonPath = join(ROOT_DIR, \"components.json\");\nif (existsSync(componentsJsonPath)) {\n  const componentsJson = JSON.parse(readFileSync(componentsJsonPath, \"utf-8\"));\n\n  // Transform aliases from @openstatus/ui to @\n  if (componentsJson.aliases) {\n    const transformedAliases = {};\n    for (const [key, value] of Object.entries(componentsJson.aliases)) {\n      if (typeof value === \"string\") {\n        transformedAliases[key] = value.replace(/^@openstatus\\/ui\\//, \"@/\");\n      } else {\n        transformedAliases[key] = value;\n      }\n    }\n    componentsJson.aliases = transformedAliases;\n  }\n\n  writeFileSync(\n    join(DIST_DIR, \"components.json\"),\n    JSON.stringify(componentsJson, null, 2),\n    \"utf-8\",\n  );\n  console.log(\"✅ Transformed components.json aliases\");\n}\n\nconsole.log(\"✅ Transformation complete! Transformed files ready in dist/\");\nconsole.log(\"🏗️  Next step: Run shadcn build in the dist directory\");\n"
  },
  {
    "path": "packages/ui/src/components/blocks/README.md",
    "content": "# Status Blocks Components\n\nA sophisticated composition-based architecture for displaying system status information. This collection provides flexible, accessible, and beautifully designed components for building status pages, incident reports, and uptime dashboards.\n\n## Overview\n\nThe blocks components are built on a **composition pattern** where simple primitives can be combined to create complex status displays. Rather than monolithic components with dozens of props, you compose smaller focused components together to achieve your desired layout and behavior.\n\n### Key Features\n\n- **Composition-First**: Build complex UIs from simple, focused primitives\n- **Type-Safe**: Full TypeScript support with discriminated unions\n- **Accessible**: ARIA roles, keyboard navigation, screen reader support\n- **Themeable**: CSS variables for colors, dark mode support\n- **Responsive**: Mobile-first design with adaptive layouts\n- **Interactive**: Keyboard navigation, hover cards, collapsible groups\n\n## Architecture\n\n### Design Principles\n\n1. **Separation of Concerns**: Each component has a single, clear purpose\n2. **Data Attributes**: Components use `data-*` attributes for status-based styling\n3. **CSS Group Patterns**: Parent components establish context via `group` classes\n4. **Headless Hooks**: Complex interactions separated from presentation (e.g., `useStatusBar`)\n5. **Composition over Configuration**: Combine components rather than configure via props\n\n### Component Layers\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Layout Components (Status, StatusComponent, StatusEvent)│\n│  └─ Establish context via data-variant/data-status      │\n├─────────────────────────────────────────────────────────┤\n│ Display Components (StatusIcon, StatusMessage, etc.)    │\n│  └─ Respond to parent context via CSS selectors         │\n├─────────────────────────────────────────────────────────┤\n│ Interactive Components (StatusBar, StatusComponentGroup)│\n│  └─ Manage state and interactions via hooks             │\n├─────────────────────────────────────────────────────────┤\n│ Utility Components (StatusTimestamp, StatusBlank, etc.) │\n│  └─ Provide specialized functionality                   │\n└─────────────────────────────────────────────────────────┘\n```\n\n## Core Concepts\n\n### Status Types\n\nAll components work with a unified `StatusType`:\n\n- **success**: All systems operational (green)\n- **degraded**: Partial performance issues (yellow)\n- **error**: Outage or major issue (red)\n- **info**: Maintenance or informational (blue)\n- **empty**: No data available (muted gray)\n\n### Data Attributes\n\nComponents use `data-variant` or `data-status` attributes to establish status context:\n\n```tsx\n<StatusComponent variant=\"degraded\">\n  {/* Child components automatically style for degraded status */}\n</StatusComponent>\n```\n\nChild components use CSS selectors to respond:\n\n```css\n.group-data-[variant=degraded]/component:text-warning\n```\n\n### Composition Pattern\n\nRather than:\n```tsx\n<Monitor\n  name=\"API\"\n  status=\"success\"\n  uptime=\"99.9%\"\n  showIcon={true}\n  showStatus={true}\n/>\n```\n\nWe compose:\n```tsx\n<StatusComponent variant=\"success\">\n  <StatusComponentHeader>\n    <StatusComponentHeaderLeft>\n      <StatusComponentIcon />\n      <StatusComponentTitle>API</StatusComponentTitle>\n    </StatusComponentHeaderLeft>\n    <StatusComponentHeaderRight>\n      <StatusComponentUptime>99.9%</StatusComponentUptime>\n      <StatusComponentStatus />\n    </StatusComponentHeaderRight>\n  </StatusComponentHeader>\n</StatusComponent>\n```\n\n## Component Categories\n\n### 1. Layout Components\n\nPrimary containers that establish page structure and status context.\n\n#### Status Components\n- **Status**: Root container for status pages\n- **StatusHeader**: Header with brand and title\n- **StatusTitle**: Page title\n- **StatusDescription**: Subtitle text\n- **StatusContent**: Main content area\n- **StatusBrand**: Logo/brand image\n- **StatusIcon**: Status indicator icon\n\n#### Monitor Components\n- **StatusComponent**: Individual monitor/service container\n- **StatusComponentHeader**: Monitor header layout\n- **StatusComponentHeaderLeft**: Left-aligned header content\n- **StatusComponentHeaderRight**: Right-aligned header content\n- **StatusComponentBody**: Monitor content area\n\n### 2. Status Display Components\n\nComponents for showing monitor status, uptime, and metrics.\n\n- **StatusComponentIcon**: Status indicator for monitors\n- **StatusComponentTitle**: Monitor/service name\n- **StatusComponentDescription**: Info tooltip for monitors\n- **StatusComponentUptime**: Uptime percentage display\n- **StatusComponentStatus**: Automatic status label\n- **StatusComponentFooter**: Date range footer\n\n### 3. Status Banner Components\n\nProminent banner for displaying system-wide status.\n\n- **StatusBanner**: Complete banner with icon/message/timestamp\n- **StatusBannerContainer**: Base container for custom banners\n- **StatusBannerMessage**: Automatic status message\n- **StatusBannerTitle**: Colored title bar\n- **StatusBannerContent**: Main content area\n- **StatusBannerIcon**: Banner status icon\n- **StatusBannerTabs**: Tab container for multi-section banners\n- **StatusBannerTabsList**: Tab navigation\n- **StatusBannerTabsTrigger**: Individual tab button\n- **StatusBannerTabsContent**: Tab panel content\n\n### 4. Status Bar Components\n\nInteractive uptime timeline with hover cards.\n\n- **StatusBar**: Main timeline component\n- **useStatusBar**: Headless hook for custom implementations\n- **StatusBarEvent**: Event badge in hover cards\n- **StatusBarSkeleton**: Loading skeleton\n\n### 5. Event Components\n\nComponents for displaying incident reports and maintenance.\n\n#### Container Components\n- **StatusEventGroup**: Feed container\n- **StatusEvent**: Individual event container\n- **StatusEventContent**: Event content wrapper\n\n#### Content Components\n- **StatusEventTitle**: Event title\n- **StatusEventTitleCheck**: Resolved indicator\n- **StatusEventAffected**: Affected services container\n- **StatusEventAffectedBadge**: Single service badge\n\n#### Date/Time Components\n- **StatusEventDate**: Date with relative time\n- **StatusEventAside**: Sidebar date (desktop)\n\n#### Timeline Components\n- **StatusEventTimelineReport**: Incident updates timeline\n- **StatusEventTimelineReportUpdate**: Single update entry\n- **StatusEventTimelineMaintenance**: Maintenance entry\n- **StatusEventTimelineTitle**: Timeline entry title\n- **StatusEventTimelineMessage**: Timeline entry message\n- **StatusEventTimelineDot**: Colored status dot\n- **StatusEventTimelineSeparator**: Connecting line\n\n### 6. Empty State Components\n\nVisualizations for empty states.\n\n- **StatusBlankEvents**: Empty state for incidents\n- **StatusBlankMonitors**: Empty state for monitors\n- **StatusBlankContainer**: Generic empty state container\n- **StatusBlankTitle**: Empty state title\n- **StatusBlankDescription**: Empty state description\n\n### 7. Utility Components\n\nSpecialized functionality components.\n\n- **StatusTimestamp**: Timezone-aware timestamp with hover details\n- **StatusFeed**: Unified feed of reports and maintenance\n- **StatusComponentGroup**: Collapsible monitor grouping\n\n## Composition Patterns\n\n### Pattern 1: Basic Monitor Display\n\n```tsx\n<StatusComponent variant=\"success\">\n  <StatusComponentHeader>\n    <StatusComponentHeaderLeft>\n      <StatusComponentIcon />\n      <StatusComponentTitle>Production API</StatusComponentTitle>\n      <StatusComponentDescription>\n        Handles all production traffic\n      </StatusComponentDescription>\n    </StatusComponentHeaderLeft>\n    <StatusComponentHeaderRight>\n      <StatusComponentUptime>99.95%</StatusComponentUptime>\n      <StatusComponentStatus />\n    </StatusComponentHeaderRight>\n  </StatusComponentHeader>\n  <StatusComponentBody>\n    <StatusBar data={uptimeData} />\n    <StatusComponentFooter data={uptimeData} />\n  </StatusComponentBody>\n</StatusComponent>\n```\n\n### Pattern 2: Status Banner with Tabs\n\n```tsx\n<StatusBannerTabs status=\"degraded\" defaultValue=\"impact\">\n  <StatusBannerTabsList>\n    <StatusBannerTabsTrigger value=\"impact\" status=\"degraded\">\n      Impact\n    </StatusBannerTabsTrigger>\n    <StatusBannerTabsTrigger value=\"updates\" status=\"degraded\">\n      Updates\n    </StatusBannerTabsTrigger>\n  </StatusBannerTabsList>\n\n  <StatusBannerTabsContent value=\"impact\">\n    <StatusBannerTitle>Degraded Performance</StatusBannerTitle>\n    <StatusBannerContent>\n      <p>We are experiencing elevated latency across all regions.</p>\n      <StatusEventAffected>\n        <StatusEventAffectedBadge>API</StatusEventAffectedBadge>\n        <StatusEventAffectedBadge>Database</StatusEventAffectedBadge>\n      </StatusEventAffected>\n    </StatusBannerContent>\n  </StatusBannerTabsContent>\n\n  <StatusBannerTabsContent value=\"updates\">\n    <StatusBannerContent>\n      <StatusEventTimelineReport updates={incidentUpdates} />\n    </StatusBannerContent>\n  </StatusBannerTabsContent>\n</StatusBannerTabs>\n```\n\n### Pattern 3: Incident Timeline\n\n```tsx\n<StatusEvent>\n  <StatusEventAside>\n    <StatusEventDate date={incidentDate} />\n  </StatusEventAside>\n\n  <StatusEventContent>\n    <div className=\"flex items-center gap-2\">\n      <StatusEventTitle>API Gateway Outage</StatusEventTitle>\n      <StatusEventTitleCheck />\n    </div>\n\n    <StatusEventAffected>\n      <StatusEventAffectedBadge>API Gateway</StatusEventAffectedBadge>\n      <StatusEventAffectedBadge>Authentication</StatusEventAffectedBadge>\n    </StatusEventAffected>\n\n    <StatusEventTimelineReport\n      updates={[\n        {\n          status: \"resolved\",\n          message: \"All services have been restored\",\n          date: new Date(\"2024-01-15T12:00:00Z\")\n        },\n        {\n          status: \"monitoring\",\n          message: \"Fix deployed, monitoring recovery\",\n          date: new Date(\"2024-01-15T11:45:00Z\")\n        },\n        {\n          status: \"identified\",\n          message: \"Root cause identified in load balancer\",\n          date: new Date(\"2024-01-15T11:15:00Z\")\n        },\n        {\n          status: \"investigating\",\n          message: \"Investigating API timeouts\",\n          date: new Date(\"2024-01-15T11:00:00Z\")\n        }\n      ]}\n    />\n  </StatusEventContent>\n</StatusEvent>\n```\n\n### Pattern 4: Unified Feed\n\n```tsx\n<StatusFeed\n  statusReports={[\n    {\n      id: 1,\n      title: \"API Outage\",\n      affected: [\"API\", \"Database\"],\n      updates: [\n        {\n          status: \"resolved\",\n          message: \"Issue resolved\",\n          date: new Date()\n        }\n      ]\n    }\n  ]}\n  maintenances={[\n    {\n      id: 2,\n      title: \"Database Upgrade\",\n      message: \"Upgrading to PostgreSQL 15\",\n      affected: [\"Database\"],\n      from: new Date(\"2024-01-20T02:00:00Z\"),\n      to: new Date(\"2024-01-20T04:00:00Z\")\n    }\n  ]}\n/>\n```\n\n### Pattern 5: Grouped Monitors\n\n```tsx\n<StatusContent>\n  <StatusComponentGroup\n    title=\"Core Services\"\n    status=\"success\"\n    defaultOpen={true}\n  >\n    <StatusComponent variant=\"success\">\n      <StatusComponentHeader>\n        <StatusComponentHeaderLeft>\n          <StatusComponentIcon />\n          <StatusComponentTitle>API Server</StatusComponentTitle>\n        </StatusComponentHeaderLeft>\n      </StatusComponentHeader>\n    </StatusComponent>\n\n    <StatusComponent variant=\"success\">\n      <StatusComponentHeader>\n        <StatusComponentHeaderLeft>\n          <StatusComponentIcon />\n          <StatusComponentTitle>Database</StatusComponentTitle>\n        </StatusComponentHeaderLeft>\n      </StatusComponentHeader>\n    </StatusComponent>\n  </StatusComponentGroup>\n\n  <StatusComponentGroup\n    title=\"Third-Party Services\"\n    status=\"degraded\"\n    defaultOpen={false}\n  >\n    <StatusComponent variant=\"degraded\">\n      <StatusComponentHeader>\n        <StatusComponentHeaderLeft>\n          <StatusComponentIcon />\n          <StatusComponentTitle>Payment Gateway</StatusComponentTitle>\n        </StatusComponentHeaderLeft>\n      </StatusComponentHeader>\n    </StatusComponent>\n  </StatusComponentGroup>\n</StatusContent>\n```\n\n## Theming & Styling\n\n### CSS Variables\n\nComponents use CSS variables for status colors:\n\n```css\n--success: /* Green */\n--warning: /* Yellow */\n--destructive: /* Red */\n--info: /* Blue */\n--muted: /* Gray */\n```\n\n### Dark Mode\n\nAll components support dark mode via CSS variables. Dark mode colors are automatically applied based on the `dark` class on a parent element.\n\n### Customization\n\nOverride component styles using className:\n\n```tsx\n<StatusComponent variant=\"success\" className=\"custom-styles\">\n  {/* Component content */}\n</StatusComponent>\n```\n\n## Accessibility\n\n### Keyboard Navigation\n\n- **StatusBar**: Full arrow key navigation (←/→ between bars, ↑/↓ between monitors)\n- **StatusComponentGroup**: Space/Enter to toggle, standard collapsible keyboard support\n- **StatusBannerTabs**: Tab key navigation, arrow keys to switch tabs\n\n### ARIA Support\n\n- `role=\"toolbar\"` on StatusBar for keyboard navigation context\n- `role=\"feed\"` on StatusEventGroup for event feed semantics\n- `aria-label`, `aria-pressed`, `aria-expanded` where appropriate\n- Tooltip triggers have `aria-label` for screen readers\n\n### Screen Readers\n\n- StatusTimestamp includes timezone information in accessible format\n- StatusIcon variations are distinguished by accessible labels\n- Empty states include descriptive text for context\n\n## API Reference\n\nFor detailed API documentation, see the JSDoc comments in each component file:\n\n- [status-layout.tsx](./status-layout.tsx) - Page layout components\n- [status-component.tsx](./status-component.tsx) - Monitor display components\n- [status-banner.tsx](./status-banner.tsx) - Status banner components\n- [status-bar.tsx](./status-bar.tsx) - Uptime timeline components\n- [status-events.tsx](./status-events.tsx) - Event and incident components\n- [status-feed.tsx](./status-feed.tsx) - Unified feed component\n- [status-timestamp.tsx](./status-timestamp.tsx) - Timestamp component\n- [status-blank.tsx](./status-blank.tsx) - Empty state components\n- [status-component-group.tsx](./status-component-group.tsx) - Grouping component\n- [status-icon.tsx](./status-icon.tsx) - Icon component\n- [status.utils.ts](./status.utils.ts) - Utility functions\n\n---\n\n**Note**: These components follow the headless UI pattern where applicable, separating state management from presentation. This allows for maximum flexibility while maintaining type safety and accessibility.\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-banner.tsx",
    "content": "import {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@openstatus/ui/components/ui/tabs\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { systemStatusLabels } from \"@openstatus/ui/components/blocks/status.utils\";\nimport { StatusIcon as UnifiedStatusIcon } from \"@openstatus/ui/components/blocks/status-icon\";\nimport type { StatusType } from \"@openstatus/ui/components/blocks/status.types\";\nimport { StatusTimestamp } from \"@openstatus/ui/components/blocks/status-timestamp\";\n\n/**\n * StatusBanner - Complete banner component with integrated icon, message, and timestamp\n *\n * A fully composed banner component that displays a prominent status message with:\n * - Status-colored icon on the left\n * - Automatic status message (e.g., \"All Systems Operational\")\n * - Current timestamp on the right\n *\n * The banner automatically styles itself based on the status type with colored\n * backgrounds and borders. This is the simplest way to display a status banner\n * when you don't need custom content.\n *\n * For custom banner content, use StatusBannerContainer with manual composition.\n *\n * @param status - The status type that determines appearance and message\n *\n * @example\n * ```tsx\n * <StatusBanner status=\"success\" />\n * // Displays: [✓ icon] All Systems Operational | Jan 15, 2024 10:30 (UTC)\n * ```\n *\n * @example\n * ```tsx\n * <StatusBanner status=\"degraded\" />\n * // Displays: [⚠ icon] Degraded Performance | Jan 15, 2024 10:30 (UTC)\n * ```\n *\n * @see StatusBannerContainer - For custom banner composition\n * @see StatusBannerMessage - For the automatic status message\n * @see StatusTimestamp - For the timestamp display\n */\nexport function StatusBanner({\n  className,\n  status,\n}: React.ComponentProps<\"div\"> & {\n  status?: Exclude<StatusType, \"empty\">;\n}) {\n  return (\n    <StatusBannerContainer\n      status={status}\n      className={cn(\n        \"flex items-center gap-3 px-3 py-2 sm:px-4 sm:py-3\",\n        \"data-[status=success]:bg-success/20\",\n        \"data-[status=degraded]:bg-warning/20\",\n        \"data-[status=error]:bg-destructive/20\",\n        \"data-[status=info]:bg-info/20\",\n        className,\n      )}\n    >\n      <StatusBannerIcon className=\"flex-shrink-0\" />\n      <div className=\"flex flex-1 flex-wrap items-center justify-between gap-2\">\n        <StatusBannerMessage className=\"font-semibold text-xl\" />\n        <StatusTimestamp date={new Date()} className=\"text-xs\" />\n      </div>\n    </StatusBannerContainer>\n  );\n}\nStatusBanner.displayName = \"StatusBanner\";\n\n/**\n * StatusBannerContainer - Base container for status banner composition\n *\n * Provides a rounded, bordered container with status-based styling via data\n * attributes. The container acts as a CSS group (/status-banner) enabling child\n * components to style themselves based on the status type.\n *\n * Background colors automatically adjust for light/dark mode:\n * - success: Green tint (bg-success/5 light, bg-success/10 dark)\n * - degraded: Yellow tint (bg-warning/5 light, bg-warning/10 dark)\n * - error: Red tint (bg-destructive/5 light, bg-destructive/10 dark)\n * - info: Blue tint (bg-info/5 light, bg-info/10 dark)\n *\n * @param status - The status type for styling\n *\n * @example\n * ```tsx\n * <StatusBannerContainer status=\"error\">\n *   <StatusBannerTitle>Active Incident</StatusBannerTitle>\n *   <StatusBannerContent>\n *     <p>We are experiencing issues with the API.</p>\n *   </StatusBannerContent>\n * </StatusBannerContainer>\n * ```\n *\n * @see StatusBanner - For a pre-composed banner with icon/message/timestamp\n * @see StatusBannerTitle - For colored title bar\n * @see StatusBannerContent - For main content area\n */\nexport function StatusBannerContainer({\n  className,\n  children,\n  status,\n}: React.ComponentProps<\"div\"> & {\n  status?: Exclude<StatusType, \"empty\">;\n}) {\n  return (\n    <div\n      data-slot=\"status-banner\"\n      data-status={status}\n      className={cn(\n        \"group/status-banner overflow-hidden rounded-lg border\",\n        \"data-[status=success]:border-success data-[status=success]:bg-success/5 dark:data-[status=success]:bg-success/10\",\n        \"data-[status=degraded]:border-warning data-[status=degraded]:bg-warning/5 dark:data-[status=degraded]:bg-warning/10\",\n        \"data-[status=error]:border-destructive data-[status=error]:bg-destructive/5 dark:data-[status=error]:bg-destructive/10\",\n        \"data-[status=info]:border-info data-[status=info]:bg-info/5 dark:data-[status=info]:bg-info/10\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\nStatusBannerContainer.displayName = \"StatusBannerContainer\";\n\n/**\n * StatusBannerMessage - Automatic status message display\n *\n * Displays a status message that automatically shows the appropriate text based\n * on the parent StatusBannerContainer's status. Uses CSS data attribute selectors\n * to show only the relevant message:\n * - success: \"All Systems Operational\"\n * - degraded: \"Degraded Performance\"\n * - error: \"Partial Outage\"\n * - info: \"Maintenance\"\n *\n * The messages are sourced from systemStatusLabels.long for consistent messaging.\n *\n * @example\n * ```tsx\n * <StatusBannerContainer status=\"success\">\n *   <StatusBannerMessage className=\"text-xl font-semibold\" />\n *   // Displays \"All Systems Operational\"\n * </StatusBannerContainer>\n * ```\n *\n * @see StatusBannerContainer - For setting the status context\n * @see systemStatusLabels - For the message text definitions\n */\nexport function StatusBannerMessage({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(className)} {...props}>\n      <span className=\"hidden group-data-[status=success]/status-banner:block\">\n        {systemStatusLabels.success.long}\n      </span>\n      <span className=\"hidden group-data-[status=degraded]/status-banner:block\">\n        {systemStatusLabels.degraded.long}\n      </span>\n      <span className=\"hidden group-data-[status=error]/status-banner:block\">\n        {systemStatusLabels.error.long}\n      </span>\n      <span className=\"hidden group-data-[status=info]/status-banner:block\">\n        {systemStatusLabels.info.long}\n      </span>\n    </div>\n  );\n}\n\n/**\n * StatusBannerTitle - Colored title bar for banner sections\n *\n * Displays a title with a solid status-colored background and light text color.\n * The background color automatically adjusts based on the parent StatusBannerContainer's\n * status type:\n * - success: Green background\n * - degraded: Yellow background\n * - error: Red background\n * - info: Blue background\n *\n * Typically used to create distinct sections within a banner or to highlight\n * important information.\n *\n * @example\n * ```tsx\n * <StatusBannerContainer status=\"error\">\n *   <StatusBannerTitle>Ongoing Incident</StatusBannerTitle>\n *   <StatusBannerContent>\n *     <p>Details about the incident...</p>\n *   </StatusBannerContent>\n * </StatusBannerContainer>\n * ```\n *\n * @see StatusBannerContainer - For setting the status context\n * @see StatusBannerContent - For main content below the title\n */\nexport function StatusBannerTitle({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"px-3 py-2 font-medium text-background\",\n        \"group-data-[status=success]/status-banner:bg-success\",\n        \"group-data-[status=degraded]/status-banner:bg-warning\",\n        \"group-data-[status=error]/status-banner:bg-destructive\",\n        \"group-data-[status=info]/status-banner:bg-info\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusBannerTitle.displayName = \"StatusBannerTitle\";\n\n/**\n * StatusBannerContent - Main content area for banner messages\n *\n * Provides consistent padding and spacing (gap-2) for banner content. Used to\n * display detailed messages, updates, or other information within a status banner.\n *\n * The padding is responsive: px-3 py-2 on mobile, px-4 py-3 on larger screens.\n *\n * @example\n * ```tsx\n * <StatusBannerContainer status=\"info\">\n *   <StatusBannerTitle>Scheduled Maintenance</StatusBannerTitle>\n *   <StatusBannerContent>\n *     <p>We will be performing maintenance on Jan 20 from 2-4 AM UTC.</p>\n *     <p>Some services may be temporarily unavailable.</p>\n *   </StatusBannerContent>\n * </StatusBannerContainer>\n * ```\n *\n * @see StatusBannerContainer - For the container\n * @see StatusBannerTitle - For optional title bar above content\n */\nexport function StatusBannerContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"flex flex-col gap-2 px-3 py-2 sm:px-4 sm:py-3\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusBannerContent.displayName = \"StatusBannerContent\";\n\n/**\n * StatusBannerIcon - Status indicator icon for banner context\n *\n * This component wraps the unified StatusIcon with variant=\"banner\", configuring\n * it to respond to the parent StatusBannerContainer's data-status attribute.\n * The displayed icon automatically changes based on the status type:\n * - success: CheckIcon\n * - degraded: TriangleAlertIcon\n * - error: AlertCircleIcon\n * - info: WrenchIcon\n *\n * @example\n * ```tsx\n * <StatusBannerContainer status=\"degraded\">\n *   <div className=\"flex items-center gap-3\">\n *     <StatusBannerIcon />\n *     <StatusBannerMessage />\n *   </div>\n * </StatusBannerContainer>\n * ```\n *\n * @see StatusBannerContainer - For setting the status context\n * @see StatusIcon from status-icon.tsx - For the underlying unified icon implementation\n */\nexport function StatusBannerIcon({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <UnifiedStatusIcon variant=\"banner\" className={className} {...props} />\n  );\n}\nStatusBannerIcon.displayName = \"StatusBannerIcon\";\n\n// ============================================================================\n// Tab Components\n// ============================================================================\n\n/**\n * StatusBannerTabs - Tab container for multi-section status banners\n *\n * Provides a tabbed interface for status banners, allowing multiple views or\n * sections within a single banner. The tabs container automatically applies\n * status-based background colors matching the banner theme.\n *\n * Built on Radix UI Tabs with status-aware styling.\n *\n * @param status - The status type for background color theming\n *\n * @example\n * ```tsx\n * <StatusBannerTabs status=\"degraded\" defaultValue=\"impact\">\n *   <StatusBannerTabsList>\n *     <StatusBannerTabsTrigger value=\"impact\" status=\"degraded\">\n *       Impact\n *     </StatusBannerTabsTrigger>\n *     <StatusBannerTabsTrigger value=\"updates\" status=\"degraded\">\n *       Updates\n *     </StatusBannerTabsTrigger>\n *   </StatusBannerTabsList>\n *   <StatusBannerTabsContent value=\"impact\">\n *     <StatusBannerContent>\n *       <p>Services affected by this incident...</p>\n *     </StatusBannerContent>\n *   </StatusBannerTabsContent>\n *   <StatusBannerTabsContent value=\"updates\">\n *     <StatusBannerContent>\n *       <p>Latest updates on resolution...</p>\n *     </StatusBannerContent>\n *   </StatusBannerTabsContent>\n * </StatusBannerTabs>\n * ```\n *\n * @see StatusBannerTabsList - For the tab navigation\n * @see StatusBannerTabsTrigger - For individual tab buttons\n * @see StatusBannerTabsContent - For tab panel content\n */\nexport function StatusBannerTabs({\n  className,\n  children,\n  status,\n  ...props\n}: React.ComponentProps<typeof Tabs> & {\n  status?: Exclude<StatusType, \"empty\">;\n}) {\n  return (\n    <Tabs\n      data-slot=\"status-banner-tabs\"\n      data-status={status}\n      className={cn(\n        \"gap-0\",\n        \"data-[status=success]:bg-success/20\",\n        \"data-[status=degraded]:bg-warning/20\",\n        \"data-[status=error]:bg-destructive/20\",\n        \"data-[status=info]:bg-info/20\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </Tabs>\n  );\n}\n\n/**\n * StatusBannerTabsList - Tab navigation container\n *\n * Wraps the tab triggers in a scrollable container with rounded top corners.\n * The container automatically handles overflow with horizontal scrolling on\n * smaller screens.\n *\n * @example\n * ```tsx\n * <StatusBannerTabsList>\n *   <StatusBannerTabsTrigger value=\"overview\" status=\"success\">\n *     Overview\n *   </StatusBannerTabsTrigger>\n *   <StatusBannerTabsTrigger value=\"details\" status=\"success\">\n *     Details\n *   </StatusBannerTabsTrigger>\n * </StatusBannerTabsList>\n * ```\n *\n * @see StatusBannerTabsTrigger - For individual tab buttons\n */\nexport function StatusBannerTabsList({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof TabsList>) {\n  return (\n    <div className={cn(\"rounded-t-lg\", \"w-full overflow-x-auto\")}>\n      <TabsList\n        className={cn(\n          \"rounded-none rounded-t-lg p-0\",\n          \"border-none\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </TabsList>\n    </div>\n  );\n}\n\n/**\n * StatusBannerTabsTrigger - Individual tab button\n *\n * Displays a single tab trigger button with status-based coloring. The button\n * shows:\n * - Inactive state: 50% opacity status color background\n * - Active state: Full status color background with light text\n *\n * The status parameter should match the parent StatusBannerTabs status for\n * consistent theming.\n *\n * @param status - The status type for color theming\n * @param value - The tab value (for Radix UI Tabs)\n *\n * @example\n * ```tsx\n * <StatusBannerTabsList>\n *   <StatusBannerTabsTrigger value=\"affected\" status=\"error\">\n *     Affected Services\n *   </StatusBannerTabsTrigger>\n *   <StatusBannerTabsTrigger value=\"timeline\" status=\"error\">\n *     Timeline\n *   </StatusBannerTabsTrigger>\n * </StatusBannerTabsList>\n * ```\n *\n * @see StatusBannerTabsList - For the container\n * @see StatusBannerTabsContent - For the corresponding content panels\n */\nexport function StatusBannerTabsTrigger({\n  className,\n  children,\n  status,\n  ...props\n}: React.ComponentProps<typeof TabsTrigger> & {\n  status?: Exclude<StatusType, \"empty\">;\n}) {\n  return (\n    <TabsTrigger\n      data-slot=\"status-banner-tabs-trigger\"\n      data-status={status}\n      className={cn(\n        \"font-mono\",\n        \"rounded-none border-none focus-visible:ring-inset\",\n        \"h-full text-foreground data-[state=active]:text-background dark:text-foreground dark:data-[state=active]:text-background\",\n        \"data-[state=active]:data-[status=success]:bg-success data-[status=success]:bg-success/50 dark:data-[state=active]:data-[status=success]:bg-success dark:data-[status=success]:bg-success/50\",\n        \"data-[state=active]:data-[status=degraded]:bg-warning data-[status=degraded]:bg-warning/50 dark:data-[state=active]:data-[status=degraded]:bg-warning dark:data-[status=degraded]:bg-warning/50\",\n        \"data-[state=active]:data-[status=error]:bg-destructive data-[status=error]:bg-destructive/50 dark:data-[state=active]:data-[status=error]:bg-destructive dark:data-[status=error]:bg-destructive/50\",\n        \"data-[state=active]:data-[status=info]:bg-info data-[status=info]:bg-info/50 dark:data-[state=active]:data-[status=info]:bg-info dark:data-[status=info]:bg-info/50\",\n        \"data-[state=active]:shadow-none\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </TabsTrigger>\n  );\n}\n\n/**\n * StatusBannerTabsContent - Tab panel content container\n *\n * Wraps the content for a single tab panel. The component includes negative\n * horizontal margin (-mx-3) to align edge-to-edge with the banner container\n * when used with StatusBannerContent.\n *\n * @param value - The tab value (must match a StatusBannerTabsTrigger value)\n *\n * @example\n * ```tsx\n * <StatusBannerTabsContent value=\"updates\">\n *   <StatusBannerContent>\n *     <h3>Latest Update</h3>\n *     <p>We have identified the issue and are working on a fix...</p>\n *   </StatusBannerContent>\n * </StatusBannerTabsContent>\n * ```\n *\n * @see StatusBannerTabsTrigger - For the tab button that activates this content\n * @see StatusBannerContent - Typically used as a child for proper padding\n */\nexport function StatusBannerTabsContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof TabsContent>) {\n  return (\n    <TabsContent className={cn(\"-mx-3\", className)} {...props}>\n      {children}\n    </TabsContent>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-bar.tsx",
    "content": "\"use client\";\n\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@openstatus/ui/components/ui/hover-card\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport { useMediaQuery } from \"@openstatus/ui/hooks/use-media-query\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { formatDistanceStrict } from \"date-fns\";\nimport { useCallback, useEffect, useRef, useState, forwardRef } from \"react\";\nimport {\n  statusColors,\n  formatDateRange,\n  requestStatusLabels,\n} from \"@openstatus/ui/components/blocks/status.utils\";\nimport type {\n  StatusBarData,\n  StatusEventType,\n  StatusType,\n} from \"@openstatus/ui/components/blocks/status.types\";\n\ninterface StatusBarProps {\n  data: StatusBarData[];\n  renderCard?: (\n    data: StatusBarData[\"card\"][number],\n    index: number,\n  ) => React.ReactNode;\n  renderBar?: (\n    data: StatusBarData[\"bar\"][number],\n    index: number,\n  ) => React.ReactNode;\n  renderEvent?: (\n    data: StatusBarData[\"events\"][number],\n    index: number,\n  ) => React.ReactNode;\n}\n\ninterface UseStatusBarProps {\n  dataLength: number;\n  isTouch: boolean;\n}\n\ntype InteractionType = \"pin\" | \"hover\" | \"focus\" | null;\n\n/**\n * useStatusBar - Headless hook for managing status bar interactions and keyboard navigation\n *\n * This hook provides the core logic for StatusBar interactions, implementing a\n * headless UI pattern that separates state management from presentation. It handles:\n *\n * **Interaction Modes**:\n * - **hover**: Desktop hover state (shows card on mouse hover, auto-hides on leave)\n * - **pin**: Pinned state (click to pin card open, click again or Esc to close)\n * - **focus**: Keyboard focus state (shows card while focused, hides on blur)\n *\n * **Keyboard Navigation**:\n * - **Arrow Left/Right**: Navigate between bars in the same timeline\n * - **Arrow Up/Down**: Navigate between timelines (different monitors)\n * - **Enter/Space**: Pin/unpin the active bar\n * - **Escape**: Close pinned card and remove focus\n *\n * **Touch Device Support**:\n * - Disables hover state on touch devices (detected via `(hover: none)` media query)\n * - Click interaction works on both touch and non-touch devices\n *\n * **Outside Click Handling**:\n * - Automatically closes pinned cards when clicking outside the component\n *\n * @param dataLength - Total number of bars in the timeline\n * @param isTouch - Whether the device supports touch (no hover capability)\n *\n * @returns Hook state and handlers:\n * - `activeIndex`: Currently active bar index (null if none)\n * - `isOpen`: Whether a card is currently displayed\n * - `interactionType`: Current interaction mode (\"pin\" | \"hover\" | \"focus\" | null)\n * - `containerRef`: Ref for the status bar container (for outside click detection)\n * - `handlers`: Event handler functions for mouse/keyboard/focus interactions\n * - `setButtonRef`: Function to register bar element refs (for keyboard navigation)\n *\n * @example\n * ```tsx\n * function CustomStatusBar({ data }) {\n *   const isTouch = useMediaQuery(\"(hover: none)\");\n *   const { activeIndex, handlers, setButtonRef, containerRef } = useStatusBar({\n *     dataLength: data.length,\n *     isTouch,\n *   });\n *\n *   return (\n *     <div ref={containerRef} role=\"toolbar\">\n *       {data.map((item, index) => (\n *         <button\n *           key={index}\n *           ref={(el) => setButtonRef(index, el)}\n *           onClick={() => handlers.onClick(index)}\n *           onFocus={() => handlers.onFocus(index)}\n *           onKeyDown={(e) => handlers.onKeyDown(e, index)}\n *         >\n *           {// Custom bar rendering}\n *         </button>\n *       ))}\n *     </div>\n *   );\n * }\n * ```\n *\n * @see StatusBar - For the complete implementation using this hook\n */\nfunction useStatusBar({ dataLength, isTouch }: UseStatusBarProps) {\n  const [activeIndex, setActiveIndex] = useState<number | null>(null);\n  const [interactionType, setInteractionType] = useState<InteractionType>(null);\n  const buttonRefs = useRef<(HTMLElement | null)[]>([]);\n  const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  // Clear hover timeout on unmount\n  useEffect(() => {\n    return () => {\n      if (hoverTimeoutRef.current) {\n        clearTimeout(hoverTimeoutRef.current);\n      }\n    };\n  }, []);\n\n  // Handle clicks outside to close pinned card\n  useEffect(() => {\n    if (interactionType !== \"pin\" || activeIndex === null) return;\n\n    const handleOutsideClick = (e: MouseEvent) => {\n      if (\n        containerRef.current &&\n        !containerRef.current.contains(e.target as Node)\n      ) {\n        setActiveIndex(null);\n        setInteractionType(null);\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleOutsideClick);\n    return () => document.removeEventListener(\"mousedown\", handleOutsideClick);\n  }, [interactionType, activeIndex]);\n\n  const clearHoverTimeout = useCallback(() => {\n    if (hoverTimeoutRef.current) {\n      clearTimeout(hoverTimeoutRef.current);\n      hoverTimeoutRef.current = null;\n    }\n  }, []);\n\n  const handleClick = useCallback(\n    (index: number) => {\n      clearHoverTimeout();\n      setActiveIndex((prev) => {\n        if (prev === index) {\n          setInteractionType(null);\n          return null;\n        }\n        setInteractionType(\"pin\");\n        return index;\n      });\n    },\n    [clearHoverTimeout],\n  );\n\n  const handleHoverStart = useCallback(\n    (index: number) => {\n      // On touch devices, don't show hover state\n      if (isTouch) return;\n\n      clearHoverTimeout();\n      setActiveIndex(index);\n      setInteractionType(\"hover\");\n    },\n    [isTouch, clearHoverTimeout],\n  );\n\n  const handleHoverEnd = useCallback(() => {\n    // Only clear hover state, not pinned or focused\n    if (interactionType !== \"hover\") return;\n\n    hoverTimeoutRef.current = setTimeout(() => {\n      setActiveIndex(null);\n      setInteractionType(null);\n    }, 100);\n  }, [interactionType]);\n\n  const handleFocus = useCallback((index: number) => {\n    setActiveIndex(index);\n    setInteractionType(\"focus\");\n  }, []);\n\n  const handleBlur = useCallback((e: React.FocusEvent) => {\n    // Only clear if not moving to another bar\n    const relatedTarget = e.relatedTarget as HTMLElement;\n    const isMovingToAnotherBar =\n      relatedTarget &&\n      relatedTarget.closest('[role=\"toolbar\"]') === containerRef.current &&\n      relatedTarget.getAttribute(\"role\") === \"button\";\n\n    if (!isMovingToAnotherBar) {\n      setActiveIndex(null);\n      setInteractionType(null);\n    }\n  }, []);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent, currentIndex: number) => {\n      switch (e.key) {\n        case \"Escape\":\n          e.preventDefault();\n          setActiveIndex(null);\n          setInteractionType(null);\n          clearHoverTimeout();\n          buttonRefs.current[currentIndex]?.blur();\n          break;\n\n        case \"ArrowLeft\":\n          e.preventDefault();\n          {\n            const newIndex =\n              currentIndex > 0 ? currentIndex - 1 : dataLength - 1;\n            buttonRefs.current[newIndex]?.focus();\n          }\n          break;\n\n        case \"ArrowRight\":\n          e.preventDefault();\n          {\n            const newIndex =\n              currentIndex < dataLength - 1 ? currentIndex + 1 : 0;\n            buttonRefs.current[newIndex]?.focus();\n          }\n          break;\n\n        case \"ArrowUp\":\n          e.preventDefault();\n          {\n            // Navigate to previous monitor's status bar\n            const prevMonitor = containerRef.current?.closest(\n              '[data-slot=\"status-component\"]',\n            )?.previousElementSibling;\n            if (prevMonitor) {\n              const prevBar = prevMonitor.querySelector('[role=\"toolbar\"]');\n              if (prevBar) {\n                const prevButtons = prevBar.querySelectorAll('[role=\"button\"]');\n                const targetButton = prevButtons[currentIndex] as HTMLElement;\n                targetButton?.focus();\n              }\n            }\n          }\n          break;\n\n        case \"ArrowDown\":\n          e.preventDefault();\n          {\n            // Navigate to next monitor's status bar\n            const nextMonitor = containerRef.current?.closest(\n              '[data-slot=\"status-component\"]',\n            )?.nextElementSibling;\n            if (nextMonitor) {\n              const nextBar = nextMonitor.querySelector('[role=\"toolbar\"]');\n              if (nextBar) {\n                const nextButtons = nextBar.querySelectorAll('[role=\"button\"]');\n                const targetButton = nextButtons[currentIndex] as HTMLElement;\n                targetButton?.focus();\n              }\n            }\n          }\n          break;\n\n        case \"Enter\":\n        case \" \":\n          e.preventDefault();\n          handleClick(currentIndex);\n          break;\n      }\n    },\n    [dataLength, clearHoverTimeout, handleClick],\n  );\n\n  const setButtonRef = useCallback((index: number, el: HTMLElement | null) => {\n    buttonRefs.current[index] = el;\n  }, []);\n\n  return {\n    activeIndex,\n    isOpen: activeIndex !== null,\n    interactionType,\n    containerRef,\n    handlers: {\n      onClick: handleClick,\n      onHoverStart: handleHoverStart,\n      onHoverEnd: handleHoverEnd,\n      onHoverCardEnter: clearHoverTimeout,\n      onHoverCardLeave: () => {\n        setActiveIndex(null);\n        setInteractionType(null);\n      },\n      onFocus: handleFocus,\n      onBlur: handleBlur,\n      onKeyDown: handleKeyDown,\n    },\n    setButtonRef,\n  };\n}\n\n/**\n * StatusBar - Interactive uptime timeline with keyboard navigation and hover cards\n *\n * Displays a horizontal timeline of status bars, where each bar represents a day's\n * status with color-coded segments. Interactive hover cards show detailed status\n * information, events, and incidents for each day.\n *\n * **Key Features**:\n * - **Visual Timeline**: Vertical bars with color-coded segments showing status over time\n * - **Interactive Cards**: Hover/click to see detailed breakdowns and events\n * - **Keyboard Navigation**: Full keyboard support with arrow keys, Enter, and Escape\n * - **Touch Support**: Optimized interactions for touch devices\n * - **Customizable Rendering**: Override default renderers for bars, cards, and events\n * - **Accessibility**: ARIA roles, labels, and keyboard navigation\n *\n * **Interaction Modes**:\n * - Desktop: Hover to preview, click to pin, Esc to close\n * - Touch: Tap to toggle card open/closed\n * - Keyboard: Arrow keys to navigate, Enter/Space to pin, Esc to close\n *\n * **Keyboard Navigation**:\n * - **Left/Right Arrows**: Move between bars in the timeline\n * - **Up/Down Arrows**: Navigate between different monitor timelines\n * - **Enter/Space**: Pin or unpin the active card\n * - **Escape**: Close card and remove focus\n *\n * **Data Structure**:\n * Each data item represents a day and contains:\n * - `day`: Date string\n * - `bar`: Array of segments with status and height percentage\n * - `card`: Array of status breakdowns to display in hover card\n * - `events`: Array of incidents/maintenance events for that day\n *\n * @param data - Array of status bar data items (one per day)\n * @param renderCard - Optional custom renderer for card content items\n * @param renderBar - Optional custom renderer for bar segments\n * @param renderEvent - Optional custom renderer for event badges\n *\n * @example\n * // Basic usage with default rendering\n * ```tsx\n * const uptimeData = [\n *   {\n *     day: \"2024-01-15\",\n *     bar: [{ status: \"success\", height: 100 }],\n *     card: [{ status: \"success\", value: \"100%\" }],\n *     events: []\n *   },\n *   {\n *     day: \"2024-01-16\",\n *     bar: [\n *       { status: \"success\", height: 80 },\n *       { status: \"error\", height: 20 }\n *     ],\n *     card: [\n *       { status: \"success\", value: \"80%\" },\n *       { status: \"error\", value: \"20%\" }\n *     ],\n *     events: [\n *       {\n *         id: \"inc-1\",\n *         type: \"incident\",\n *         name: \"API Downtime\",\n *         from: new Date(\"2024-01-16T10:00:00Z\"),\n *         to: new Date(\"2024-01-16T10:30:00Z\")\n *       }\n *     ]\n *   }\n * ];\n *\n * <StatusBar data={uptimeData} />\n * ```\n *\n * @example\n * // With custom bar renderer\n * ```tsx\n * <StatusBar\n *   data={uptimeData}\n *   renderBar={(segment, index) => (\n *     <div\n *       key={index}\n *       className=\"w-full transition-all\"\n *       style={{\n *         height: `${segment.height}%`,\n *         background: `linear-gradient(to top, ${statusColors[segment.status]}, transparent)`\n *       }}\n *     />\n *   )}\n * />\n * ```\n *\n * @example\n * // With custom event renderer\n * ```tsx\n * <StatusBar\n *   data={uptimeData}\n *   renderEvent={(event, index) => (\n *     <Link key={index} href={`/incidents/${event.id}`}>\n *       <div className=\"text-sm hover:underline\">\n *         {event.name}\n *       </div>\n *     </Link>\n *   )}\n * />\n * ```\n *\n * @see useStatusBar - For the headless hook powering the interactions\n * @see StatusBarSkeleton - For loading state\n * @see StatusBarEvent - For event badge rendering\n */\nexport function StatusBar({\n  data,\n  renderCard,\n  renderBar,\n  renderEvent,\n}: StatusBarProps) {\n  const isTouch = useMediaQuery(\"(hover: none)\");\n  const { activeIndex, interactionType, containerRef, handlers, setButtonRef } =\n    useStatusBar({\n      dataLength: data.length,\n      isTouch,\n    });\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"flex h-[50px] w-full items-end gap-px\"\n      data-slot=\"status-bar\"\n      role=\"toolbar\"\n      aria-label=\"Status tracker\"\n    >\n      {data.map((item, index) => {\n        const isActive = activeIndex === index;\n        const isPinned = isActive && interactionType === \"pin\";\n\n        return (\n          <StatusBarItem\n            key={item.day}\n            ref={(el) => setButtonRef(index, el)}\n            index={index}\n            item={item}\n            isActive={isActive}\n            isPinned={isPinned}\n            isTouch={isTouch}\n            isLastItem={index === data.length - 1}\n            handlers={handlers}\n            renderCard={renderCard}\n            renderBar={renderBar}\n            renderEvent={renderEvent}\n          />\n        );\n      })}\n    </div>\n  );\n}\nStatusBar.displayName = \"StatusBar\";\n\ninterface StatusBarItemProps {\n  index: number;\n  item: StatusBarData;\n  isActive: boolean;\n  isPinned: boolean;\n  isTouch: boolean;\n  isLastItem: boolean;\n  handlers: ReturnType<typeof useStatusBar>[\"handlers\"];\n  renderCard?: StatusBarProps[\"renderCard\"];\n  renderBar?: StatusBarProps[\"renderBar\"];\n  renderEvent?: StatusBarProps[\"renderEvent\"];\n}\n\nconst StatusBarItem = forwardRef<HTMLDivElement, StatusBarItemProps>(\n  (\n    {\n      index,\n      item,\n      isActive,\n      isPinned,\n      isTouch,\n      isLastItem,\n      handlers,\n      renderCard,\n      renderBar,\n      renderEvent,\n    },\n    ref,\n  ) => {\n    return (\n      <HoverCard openDelay={0} closeDelay={0} open={isActive}>\n        <HoverCardTrigger asChild>\n          <div\n            ref={ref}\n            className=\"group relative flex h-full flex-1 cursor-pointer flex-col outline-none hover:opacity-80 focus-visible:opacity-80 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[aria-pressed=true]:opacity-80 rounded-full\"\n            onClick={() => handlers.onClick(index)}\n            onFocus={() => handlers.onFocus(index)}\n            onBlur={handlers.onBlur}\n            onMouseEnter={() => handlers.onHoverStart(index)}\n            onMouseLeave={handlers.onHoverEnd}\n            onKeyDown={(e) => handlers.onKeyDown(e, index)}\n            tabIndex={isLastItem && !isActive ? 0 : isActive ? 0 : -1}\n            role=\"button\"\n            aria-label={`Day ${index + 1} status`}\n            aria-pressed={isPinned}\n            aria-expanded={isActive}\n            data-slot=\"status-bar-item\"\n          >\n            <div className=\"flex h-full w-full flex-col overflow-hidden rounded-full\">\n              {/* Render bar segments */}\n              {item.bar.map((segment, segmentIndex) => {\n                if (renderBar) {\n                  return renderBar(segment, segmentIndex);\n                }\n                return (\n                  <div\n                    key={`${item.day}-${segment.status}-${segmentIndex}`}\n                    className={cn(\"w-full transition-all\", {\n                      \"rounded-t-full\": segmentIndex === 0,\n                      \"rounded-b-full\": segmentIndex === item.bar.length - 1,\n                    })}\n                    style={{\n                      height: `${segment.height}%`,\n                      backgroundColor: statusColors[segment.status],\n                    }}\n                  />\n                );\n              })}\n            </div>\n          </div>\n        </HoverCardTrigger>\n        <HoverCardContent\n          side=\"top\"\n          align=\"center\"\n          className=\"w-auto min-w-40 p-0\"\n          onMouseEnter={handlers.onHoverCardEnter}\n          onMouseLeave={handlers.onHoverCardLeave}\n          onPointerDownOutside={(e) => {\n            // Prevent closing on touch devices when clicking the card\n            if (isTouch) {\n              e.preventDefault();\n            }\n          }}\n        >\n          <StatusBarCard\n            item={item}\n            isPinned={isPinned}\n            isTouch={isTouch}\n            renderCard={renderCard}\n            renderEvent={renderEvent}\n          />\n        </HoverCardContent>\n      </HoverCard>\n    );\n  },\n);\nStatusBarItem.displayName = \"StatusBarItem\";\n\ninterface StatusBarCardProps {\n  item: StatusBarData;\n  isPinned: boolean;\n  isTouch: boolean;\n  renderCard?: StatusBarProps[\"renderCard\"];\n  renderEvent?: StatusBarProps[\"renderEvent\"];\n}\n\n/**\n * StatusBarCard - Internal hover card content component\n *\n * Displays detailed status information for a single day in a hover card, including:\n * - Date header\n * - Status breakdown percentages\n * - Events/incidents for that day\n * - Pin/unpin instructions (when pinned on desktop)\n *\n * The card automatically formats the date and renders status items and events\n * using either custom renderers or default implementations.\n *\n * @param item - The status bar data for this day\n * @param isPinned - Whether the card is currently pinned open\n * @param isTouch - Whether the device supports touch\n * @param renderCard - Optional custom renderer for status items\n * @param renderEvent - Optional custom renderer for events\n */\nfunction StatusBarCard({\n  item,\n  isPinned,\n  isTouch,\n  renderCard,\n  renderEvent,\n}: StatusBarCardProps) {\n  return (\n    <div data-slot=\"status-bar-card\">\n      <div className=\"p-2 text-xs\">\n        {new Date(item.day).toLocaleDateString(\"default\", {\n          day: \"numeric\",\n          month: \"short\",\n          year: \"numeric\",\n        })}\n      </div>\n      <Separator />\n      <div className=\"space-y-1 p-2 text-sm\">\n        {item.card.map((cardItem, cardIndex) => {\n          if (renderCard) {\n            return renderCard(cardItem, cardIndex);\n          }\n          return (\n            <StatusBarContent\n              key={`${item.day}-card-${cardIndex}`}\n              status={cardItem.status}\n              value={cardItem.value}\n            />\n          );\n        })}\n      </div>\n      {item.events.length > 0 && (\n        <>\n          <Separator />\n          <div className=\"p-2\">\n            {item.events.map((event, eventIndex) => {\n              if (renderEvent) {\n                return renderEvent(event, eventIndex);\n              }\n              return (\n                <StatusBarEvent\n                  key={`${event.id}-${event.type}`}\n                  type={event.type}\n                  name={event.name}\n                  from={event.from}\n                  to={event.to}\n                />\n              );\n            })}\n          </div>\n        </>\n      )}\n      {isPinned && !isTouch && (\n        <>\n          <Separator />\n          <div className=\"flex cursor-pointer items-center p-2 text-muted-foreground text-xs\">\n            <span>Click again to unpin</span>\n            <kbd className=\"ml-auto inline-flex h-5 max-h-5 min-w-5 items-center justify-center rounded border border-input bg-background px-1.5 font-mono text-[10px] font-medium text-muted-foreground\">\n              Esc\n            </kbd>\n          </div>\n        </>\n      )}\n    </div>\n  );\n}\nStatusBarCard.displayName = \"StatusBarCard\";\n\n/**\n * StatusBarSkeleton - Loading skeleton for StatusBar\n *\n * Displays a skeleton loader matching the height and width of a StatusBar,\n * used while status data is being fetched.\n *\n * @example\n * ```tsx\n * {isLoading ? (\n *   <StatusBarSkeleton />\n * ) : (\n *   <StatusBar data={uptimeData} />\n * )}\n * ```\n *\n * @see StatusBar - For the actual status bar component\n */\nexport function StatusBarSkeleton({\n  className,\n  ...props\n}: React.ComponentProps<typeof Skeleton>) {\n  return (\n    <Skeleton\n      className={cn(\"h-[50px] w-full rounded-none bg-muted\", className)}\n      {...props}\n    />\n  );\n}\nStatusBarSkeleton.displayName = \"StatusBarSkeleton\";\n\n/**\n * StatusBarContent - Internal component for status breakdown rows\n *\n * Displays a single status item in the hover card with a colored indicator,\n * status label, and percentage value. Used by StatusBarCard to show the\n * breakdown of statuses for a day.\n *\n * @param status - The status type (success, degraded, error, info, empty)\n * @param value - The percentage or count value to display\n *\n * @example\n * ```tsx\n * <StatusBarContent status=\"success\" value=\"95.2%\" />\n * <StatusBarContent status=\"error\" value=\"4.8%\" />\n * ```\n */\nfunction StatusBarContent({\n  status,\n  value,\n}: {\n  status: StatusType;\n  value: string;\n}) {\n  return (\n    <div className=\"flex items-baseline gap-4\" data-slot=\"status-bar-content\">\n      <div className=\"flex items-center gap-2\">\n        <div\n          className=\"h-2.5 w-2.5 rounded-sm\"\n          style={{\n            backgroundColor: statusColors[status],\n          }}\n        />\n        <div className=\"text-sm\">{requestStatusLabels[status]}</div>\n      </div>\n      <div className=\"ml-auto font-mono text-muted-foreground text-xs tracking-tight\">\n        {value}\n      </div>\n    </div>\n  );\n}\nStatusBarContent.displayName = \"StatusBarContent\";\n\n/**\n * StatusBarEvent - Event badge for incidents and maintenance\n *\n * Displays an event within a status bar hover card, showing:\n * - Color-coded indicator (red for incidents, yellow for reports, blue for maintenance)\n * - Event name with truncation for long names\n * - Date range (formatted as \"Since\", \"Until\", or \"Jan 15 - Jan 16\")\n * - Duration (formatted as \"2 hours\", \"ongoing\", or \"across 3 days\" for multiple incidents)\n *\n * The component automatically determines the status color based on the event type:\n * - incident → error (red)\n * - report → degraded (yellow)\n * - maintenance → info (blue)\n *\n * Returns null if no start date is provided.\n *\n * @param name - Event name or title\n * @param from - Event start date\n * @param to - Event end date (null for ongoing events)\n * @param type - Event type (\"incident\", \"report\", or \"maintenance\")\n *\n * @example\n * ```tsx\n * <StatusBarEvent\n *   type=\"incident\"\n *   name=\"API Downtime\"\n *   from={new Date(\"2024-01-15T10:00:00Z\")}\n *   to={new Date(\"2024-01-15T10:30:00Z\")}\n * />\n * // Displays: [●] API Downtime\n * //           Jan 15, 10:00 AM - 10:30 AM  30 minutes\n * ```\n *\n * @example\n * ```tsx\n * // Ongoing incident\n * <StatusBarEvent\n *   type=\"incident\"\n *   name=\"Database connectivity issues\"\n *   from={new Date()}\n *   to={null}\n * />\n * // Displays: [●] Database connectivity issues\n * //           Since Jan 15, 2:30 PM  ongoing\n * ```\n *\n * @see StatusBarCard - For the card that contains event badges\n * @see formatDateRange - For date range formatting\n */\nexport function StatusBarEvent({\n  name,\n  from,\n  to,\n  type,\n}: {\n  name: string;\n  from?: Date | null;\n  to?: Date | null;\n  type: StatusEventType;\n}) {\n  if (!from) return null;\n\n  const status =\n    type === \"incident\" ? \"error\" : type === \"report\" ? \"degraded\" : \"info\";\n\n  return (\n    <div className=\"group relative text-sm\" data-slot=\"status-bar-event\">\n      {/* NOTE: this is to make the text truncate based on the width of the sibling element */}\n      {/* REMINDER: height needs to be equal the text height */}\n      <div className=\"h-4 w-full\" />\n      <div className=\"absolute inset-0 text-muted-foreground hover:text-foreground\">\n        <div className=\"flex items-center gap-2\">\n          <div\n            className=\"h-2.5 w-2.5 shrink-0 rounded-sm\"\n            style={{\n              backgroundColor: statusColors[status],\n            }}\n          />\n          <div className=\"truncate\">{name}</div>\n        </div>\n      </div>\n      <div className=\"mt-1 text-muted-foreground text-xs\">\n        {formatDateRange(from, to ?? undefined)}{\" \"}\n        <span className=\"ml-1.5 font-mono text-muted-foreground/70\">\n          {formatDuration({ from, to, name, type })}\n        </span>\n      </div>\n    </div>\n  );\n}\nStatusBarEvent.displayName = \"StatusBarEvent\";\n\n/**\n * formatDuration - Internal helper for formatting event durations\n *\n * Formats the duration of an event based on start/end dates and event name:\n * - No start date: returns null\n * - No end date: returns \"ongoing\"\n * - Multiple incidents (detected by \"Downtime (\" in name): returns \"across {duration}\"\n * - Zero seconds duration: returns null (hides duration)\n * - Otherwise: returns formatted duration (e.g., \"2 hours\", \"3 days\")\n *\n * @param from - Event start date\n * @param to - Event end date\n * @param name - Event name (used to detect multiple incident aggregations)\n * @returns Formatted duration string or null\n */\nconst formatDuration = ({\n  from,\n  to,\n  name,\n}: React.ComponentProps<typeof StatusBarEvent>) => {\n  if (!from) return null;\n  if (!to) return \"ongoing\";\n  const duration = formatDistanceStrict(from, to);\n  const isMultipleIncidents = name.includes(\"Downtime (\");\n  if (isMultipleIncidents) return `across ${duration}`;\n  if (duration === \"0 seconds\") return null;\n  return duration;\n};\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-blank.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\";\n\n// ============================================================================\n// Container Components\n// ============================================================================\n\n/**\n * StatusBlankContainer - Main container for empty state displays\n *\n * Provides a centered, bordered container with muted background for displaying\n * empty state content. The container is responsive with adaptive padding\n * (sm:px-8 sm:py-6) and uses flexbox for vertical centering of content.\n *\n * @example\n * ```tsx\n * <StatusBlankContainer>\n *   <StatusBlankTitle>No Data Available</StatusBlankTitle>\n *   <StatusBlankDescription>Add monitors to see data here</StatusBlankDescription>\n * </StatusBlankContainer>\n * ```\n *\n * @see StatusBlankTitle - For heading text\n * @see StatusBlankDescription - For descriptive text\n */\nexport function StatusBlankContainer({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col items-center justify-center gap-2.5 rounded-lg border bg-muted/30 px-3 py-2 text-center sm:px-8 sm:py-6\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n// ============================================================================\n// Text Components\n// ============================================================================\n\n/**\n * StatusBlankTitle - Title text for empty state displays\n *\n * Displays medium-weight title text within empty state containers.\n * Typically used to communicate the empty state condition.\n *\n * @example\n * ```tsx\n * <StatusBlankTitle>No monitors found</StatusBlankTitle>\n * ```\n *\n * @see StatusBlankDescription - For additional descriptive text\n */\nexport function StatusBlankTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"font-medium\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\n/**\n * StatusBlankDescription - Descriptive text for empty state displays\n *\n * Displays muted, monospaced text providing additional context or guidance\n * for the empty state. The monospace font helps distinguish it as secondary\n * instructional text.\n *\n * @example\n * ```tsx\n * <StatusBlankDescription>\n *   Add monitors to this page to see their status here\n * </StatusBlankDescription>\n * ```\n *\n * @see StatusBlankTitle - For primary heading text\n */\nexport function StatusBlankDescription({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"font-mono text-muted-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * StatusBlankContent - Content wrapper for empty state text\n *\n * Provides vertical spacing (space-y-1) for stacking title and description\n * components within an empty state display.\n *\n * @example\n * ```tsx\n * <StatusBlankContent>\n *   <StatusBlankTitle>No Events</StatusBlankTitle>\n *   <StatusBlankDescription>No events to display</StatusBlankDescription>\n * </StatusBlankContent>\n * ```\n */\nexport function StatusBlankContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"space-y-1\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\n// ============================================================================\n// Visualization Components\n// ============================================================================\n\n/**\n * StatusBlankReport - Visual representation of an empty incident report\n *\n * Displays a stylized preview of an incident report page with placeholder\n * header, report updates, and an overlay gradient. Used to visualize what\n * incident reports will look like when created.\n *\n * This component is typically rendered in multiple layers with different\n * scales and opacities to create a stacked card effect.\n *\n * @example\n * ```tsx\n * <div className=\"relative\">\n *   <StatusBlankReport className=\"absolute scale-80 opacity-80\" />\n *   <StatusBlankReport />\n * </div>\n * ```\n *\n * @see StatusBlankEvents - For composed empty state with stacked reports\n * @see StatusBlankPageHeader - For the header visualization\n * @see StatusBlankReportUpdate - For the update line visualizations\n */\nexport function StatusBlankReport({ ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <StatusBlankPage {...props}>\n      <StatusBlankPageHeader />\n      <div className=\"flex w-full flex-col\">\n        <StatusBlankReportUpdate />\n        <StatusBlankReportUpdate />\n      </div>\n      <StatusBlankOverlay />\n    </StatusBlankPage>\n  );\n}\n\n/**\n * StatusBlankMonitor - Visual representation of an empty monitor display\n *\n * Displays a stylized preview of a monitor page with placeholder header and\n * uptime visualization bars. Used to show what monitor displays will look like\n * when monitors are added.\n *\n * This component is typically rendered in multiple layers with different\n * scales and opacities to create a stacked card effect.\n *\n * @example\n * ```tsx\n * <div className=\"relative\">\n *   <StatusBlankMonitor className=\"absolute scale-80 opacity-80\" />\n *   <StatusBlankMonitor />\n * </div>\n * ```\n *\n * @see StatusBlankMonitors - For composed empty state with stacked monitors\n * @see StatusBlankPageHeader - For the header visualization\n * @see StatusBlankMonitorUptime - For the uptime bar visualization\n */\nexport function StatusBlankMonitor({ ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <StatusBlankPage {...props}>\n      <StatusBlankPageHeader />\n      <StatusBlankMonitorUptime />\n      <StatusBlankOverlay />\n    </StatusBlankPage>\n  );\n}\n\n// ============================================================================\n// Animation Components (for visualization internals)\n// ============================================================================\n\n/**\n * StatusBlankPage - Base container for blank page visualizations\n *\n * Provides a card-like container with border and background for page previews.\n * This component is used as the foundation for StatusBlankReport and\n * StatusBlankMonitor visualizations, providing consistent sizing and styling.\n *\n * The container uses relative positioning to enable absolutely positioned\n * overlay gradients within the visualization.\n *\n * @example\n * ```tsx\n * <StatusBlankPage>\n *   <StatusBlankPageHeader />\n *   <div>// Content here</div>\n *   <StatusBlankOverlay />\n * </StatusBlankPage>\n * ```\n *\n * @see StatusBlankReport - For report visualization using this container\n * @see StatusBlankMonitor - For monitor visualization using this container\n */\nexport function StatusBlankPage({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"relative flex w-full max-w-xs flex-1 flex-col items-center justify-center gap-4 overflow-hidden rounded-lg border border-border/70 bg-background px-3 py-2 text-center\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * StatusBlankPageHeader - Placeholder header for page visualizations\n *\n * Displays a stylized representation of a page header with placeholder elements\n * for logo, navigation items, and actions. Uses accent-colored rounded rectangles\n * to create a recognizable header pattern.\n *\n * The header shows:\n * - Left: Single square (logo placeholder)\n * - Center: Three rectangles (navigation items)\n * - Right: Single rectangle (action button)\n *\n * @example\n * ```tsx\n * <StatusBlankPage>\n *   <StatusBlankPageHeader />\n *   // Other visualization content\n * </StatusBlankPage>\n * ```\n */\nexport function StatusBlankPageHeader({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex w-full items-center justify-between gap-4\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"size-3 rounded-sm bg-accent/60\" />\n      <div className=\"flex flex-row gap-1\">\n        <div className=\"h-3 w-8 rounded-sm bg-accent/60\" />\n        <div className=\"h-3 w-8 rounded-sm bg-accent/60\" />\n        <div className=\"h-3 w-8 rounded-sm bg-accent/60\" />\n      </div>\n      <div className=\"h-3 w-8 rounded-sm bg-accent/60\" />\n    </div>\n  );\n}\n\n/**\n * StatusBlankMonitorUptime - Uptime visualization for monitor previews\n *\n * Displays a stylized uptime chart with placeholder labels and 30 vertical bars\n * representing uptime data. Some bars have varying heights to create a realistic\n * visualization pattern showing occasional incidents.\n *\n * The visualization includes:\n * - Top: Three placeholder labels for time periods\n * - Bottom: 30 vertical bars with most at full height and some shorter to indicate incidents\n *\n * @example\n * ```tsx\n * <StatusBlankMonitor>\n *   <StatusBlankPageHeader />\n *   <StatusBlankMonitorUptime />\n * </StatusBlankMonitor>\n * ```\n */\nexport function StatusBlankMonitorUptime({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex w-full flex-col items-center justify-between gap-1\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex w-full flex-row gap-1\">\n        <div className=\"h-3 w-8 rounded-sm bg-accent\" />\n        <div className=\"h-3 w-12 rounded-sm bg-accent\" />\n        <div className=\"h-3 w-10 rounded-sm bg-accent\" />\n      </div>\n      <div className=\"flex w-full flex-row items-end gap-0.5\">\n        {Array.from({ length: 30 }).map((_, index) => (\n          <div\n            key={index}\n            className={cn(\n              \"h-12 flex-1 rounded-sm bg-accent\",\n              [10, 20].includes(index) && \"h-8\",\n              [25].includes(index) && \"h-10\",\n            )}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/**\n * StatusBlankReportUpdate - Single update entry for report visualizations\n *\n * Displays a stylized representation of an incident report update with placeholder\n * elements for status indicator, timestamp, and message lines. Creates a timeline-like\n * appearance when stacked vertically.\n *\n * The update shows:\n * - Left: Square status indicator\n * - Right: Two label placeholders (status + timestamp) and two message lines\n *\n * @example\n * ```tsx\n * <StatusBlankPage>\n *   <StatusBlankPageHeader />\n *   <div className=\"flex flex-col\">\n *     <StatusBlankReportUpdate />\n *     <StatusBlankReportUpdate />\n *   </div>\n * </StatusBlankPage>\n * ```\n */\nexport function StatusBlankReportUpdate({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"flex w-full items-start gap-2\", className)} {...props}>\n      <div className=\"flex h-full flex-col items-center gap-0.5\">\n        <div className=\"size-3 rounded-sm bg-accent\" />\n      </div>\n      <div className=\"flex flex-1 flex-col gap-1 pb-2\">\n        <div className=\"flex items-center gap-1\">\n          <div className=\"h-3 w-12 rounded-sm bg-accent\" />\n          <div className=\"h-3 w-16 rounded-sm bg-accent\" />\n        </div>\n        <div className=\"h-3 w-full rounded-sm bg-accent\" />\n        <div className=\"h-3 w-full rounded-sm bg-accent\" />\n      </div>\n    </div>\n  );\n}\n\n/**\n * StatusBlankOverlay - Gradient overlay for page visualizations\n *\n * Provides a bottom-to-top gradient overlay that fades the visualization content\n * from transparent (top 40%) to background color (bottom), creating a subtle\n * depth effect. The overlay is absolutely positioned to cover the entire parent.\n *\n * This overlay helps create visual separation between the preview visualization\n * and any content that might be displayed on top of it (like call-to-action text).\n *\n * @example\n * ```tsx\n * <StatusBlankPage>\n *   <StatusBlankPageHeader />\n *   <StatusBlankMonitorUptime />\n *   <StatusBlankOverlay />\n * </StatusBlankPage>\n * ```\n */\nexport function StatusBlankOverlay({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"absolute inset-0 flex flex-col items-center justify-center gap-2 bg-gradient-to-b from-40% from-transparent to-background p-2\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n// ============================================================================\n// Composed Empty States\n// ============================================================================\n\n/**\n * StatusBlankEvents - Complete empty state for incident reports\n *\n * Displays a fully composed empty state with stacked StatusBlankReport\n * visualizations (three layers with varying scales and opacities) and\n * customizable title/description text. This creates an engaging empty state\n * that shows users what incident reports will look like.\n *\n * The visualization uses absolute positioning to create a layered card stack\n * effect with the front card at full opacity and back cards progressively\n * faded and scaled down.\n *\n * @param title - The empty state heading (default: \"No reports found\")\n * @param description - The empty state description (default: \"No reports found for this status page.\")\n *\n * @example\n * ```tsx\n * <StatusBlankEvents\n *   title=\"No incidents yet\"\n *   description=\"Incident reports will appear here when created\"\n * />\n * ```\n *\n * @see StatusBlankReport - For the underlying report visualization\n */\nexport function StatusBlankEvents({\n  title = \"No reports found\",\n  description = \"No reports found for this status page.\",\n  ...props\n}: React.ComponentProps<typeof StatusBlankContainer> & {\n  title?: string;\n  description?: string;\n}) {\n  return (\n    <StatusBlankContainer {...props}>\n      <div className=\"relative mt-8 flex w-full flex-col items-center justify-center\">\n        <StatusBlankReport className=\"-top-16 absolute scale-60 opacity-50\" />\n        <StatusBlankReport className=\"-top-8 absolute scale-80 opacity-80\" />\n        <StatusBlankReport />\n      </div>\n      <StatusBlankContent>\n        <StatusBlankTitle>{title}</StatusBlankTitle>\n        <StatusBlankDescription>{description}</StatusBlankDescription>\n      </StatusBlankContent>\n    </StatusBlankContainer>\n  );\n}\n\n/**\n * StatusBlankMonitors - Complete empty state for monitors\n *\n * Displays a fully composed empty state with stacked StatusBlankMonitor\n * visualizations (three layers with varying scales and opacities) and\n * customizable title/description text. This creates an engaging empty state\n * that shows users what monitor displays will look like.\n *\n * The visualization uses absolute positioning to create a layered card stack\n * effect with the front card at full opacity and back cards progressively\n * faded and scaled down.\n *\n * @param title - The empty state heading (default: \"No public monitors\")\n * @param description - The empty state description (default: \"No public monitors have been added to this page.\")\n *\n * @example\n * ```tsx\n * <StatusBlankMonitors\n *   title=\"No monitors configured\"\n *   description=\"Add monitors to start tracking uptime\"\n * />\n * ```\n *\n * @see StatusBlankMonitor - For the underlying monitor visualization\n */\nexport function StatusBlankMonitors({\n  title = \"No public monitors\",\n  description = \"No public monitors have been added to this page.\",\n  ...props\n}: React.ComponentProps<typeof StatusBlankContainer> & {\n  title?: string;\n  description?: string;\n}) {\n  return (\n    <StatusBlankContainer {...props}>\n      <div className=\"relative mt-8 flex w-full flex-col items-center justify-center\">\n        <StatusBlankMonitor className=\"-top-16 absolute scale-60 opacity-50\" />\n        <StatusBlankMonitor className=\"-top-8 absolute scale-80 opacity-80\" />\n        <StatusBlankMonitor />\n      </div>\n      <StatusBlankContent>\n        <StatusBlankTitle>{title}</StatusBlankTitle>\n        <StatusBlankDescription>{description}</StatusBlankDescription>\n      </StatusBlankContent>\n    </StatusBlankContainer>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-component-group.tsx",
    "content": "\"use client\";\n\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@openstatus/ui/components/ui/collapsible\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { useEffect, useState } from \"react\";\nimport {\n  StatusComponentIcon,\n  StatusComponentStatus,\n} from \"@openstatus/ui/components/blocks/status-component\";\nimport type { StatusType } from \"@openstatus/ui/components/blocks/status.types\";\n\n/**\n * StatusComponentGroup - Collapsible group for organizing related monitors\n *\n * A collapsible container component for grouping related StatusComponent items,\n * displaying an aggregate status for the group. The component shows:\n * - Group title\n * - Aggregate status label (e.g., \"Operational\", \"Degraded\")\n * - Status icon with appropriate color\n * - Expandable/collapsible content area for grouped monitors\n *\n * **Key Features**:\n * - **Collapsible**: Click to expand/collapse grouped monitors\n * - **Hydration-Aware Animations**: Prevents layout shift on initial render by delaying\n *   animations until after mount (avoids animation flash when defaultOpen=true)\n * - **Aggregate Status**: Shows overall status for the entire group\n * - **Keyboard Accessible**: Full keyboard support via Radix UI Collapsible\n * - **Hover Effects**: Subtle border and background changes on hover/open\n *\n * The component uses a hydration-aware animation pattern: animations are disabled\n * on the server and first client render, then enabled after mount. This prevents\n * layout shifts when `defaultOpen={true}` is used.\n *\n * @param title - The group name/title\n * @param status - The aggregate status type for the group (controls icon color and status label)\n * @param defaultOpen - Whether the group starts expanded (default: false)\n * @param children - StatusComponent items or other content to display when expanded\n *\n * @example\n * // Basic collapsible group with multiple monitors\n * ```tsx\n * <StatusComponentGroup\n *   title=\"API Services\"\n *   status=\"success\"\n *   defaultOpen={false}\n * >\n *   <StatusComponent variant=\"success\">\n *     <StatusComponentHeader>\n *       <StatusComponentHeaderLeft>\n *         <StatusComponentIcon />\n *         <StatusComponentTitle>REST API</StatusComponentTitle>\n *       </StatusComponentHeaderLeft>\n *     </StatusComponentHeader>\n *   </StatusComponent>\n *   <StatusComponent variant=\"success\">\n *     <StatusComponentHeader>\n *       <StatusComponentHeaderLeft>\n *         <StatusComponentIcon />\n *         <StatusComponentTitle>GraphQL API</StatusComponentTitle>\n *       </StatusComponentHeaderLeft>\n *     </StatusComponentHeader>\n *   </StatusComponent>\n * </StatusComponentGroup>\n * ```\n *\n * @example\n * // Degraded group with mixed statuses\n * ```tsx\n * <StatusComponentGroup\n *   title=\"Database Cluster\"\n *   status=\"degraded\"\n *   defaultOpen={true}\n * >\n *   <StatusComponent variant=\"success\">\n *     <StatusComponentHeader>\n *       <StatusComponentHeaderLeft>\n *         <StatusComponentIcon />\n *         <StatusComponentTitle>Primary DB</StatusComponentTitle>\n *       </StatusComponentHeaderLeft>\n *     </StatusComponentHeader>\n *   </StatusComponent>\n *   <StatusComponent variant=\"degraded\">\n *     <StatusComponentHeader>\n *       <StatusComponentHeaderLeft>\n *         <StatusComponentIcon />\n *         <StatusComponentTitle>Replica DB</StatusComponentTitle>\n *       </StatusComponentHeaderLeft>\n *     </StatusComponentHeader>\n *   </StatusComponent>\n * </StatusComponentGroup>\n * ```\n *\n * @example\n * // Multiple groups for different service categories\n * ```tsx\n * <StatusContent>\n *   <StatusComponentGroup title=\"Core Services\" status=\"success\">\n *     // Core service monitors...\n *   </StatusComponentGroup>\n *   <StatusComponentGroup title=\"Third-Party APIs\" status=\"degraded\" defaultOpen>\n *     // Third-party monitors...\n *   </StatusComponentGroup>\n *   <StatusComponentGroup title=\"Internal Tools\" status=\"success\">\n *     // Internal tool monitors...\n *   </StatusComponentGroup>\n * </StatusContent>\n * ```\n *\n * @see StatusComponent - For individual monitor displays within the group\n * @see StatusComponentIcon - For the status icon displayed in the header\n * @see StatusComponentStatus - For the status label displayed in the header\n */\nexport function StatusComponentGroup({\n  children,\n  title,\n  status,\n  className,\n  defaultOpen = false,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  title: string;\n  status?: Exclude<StatusType, \"empty\">;\n  children?: React.ReactNode;\n  defaultOpen?: boolean;\n}) {\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  return (\n    <div {...props}>\n      <Collapsible\n        defaultOpen={defaultOpen}\n        data-slot=\"status-component-group\"\n        className={cn(\n          \"-mx-3\",\n          \"rounded-lg border border-transparent bg-muted/50 hover:border-border/50 data-[state=open]:border-border/50 data-[state=open]:bg-muted/50\",\n          className,\n        )}\n      >\n        <CollapsibleTrigger\n          data-slot=\"status-component-group-trigger\"\n          className={cn(\n            \"group/component flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 font-medium font-mono\",\n            \"cursor-pointer\",\n            className,\n          )}\n          data-variant={status}\n          aria-label={`Toggle ${title} status details`}\n        >\n          {title}\n          <div className=\"flex items-center gap-2\">\n            <StatusComponentStatus className=\"text-sm\" />\n            <StatusComponentIcon />\n          </div>\n        </CollapsibleTrigger>\n        <CollapsibleContent\n          data-slot=\"status-component-group-content\"\n          data-animate={mounted}\n          className={cn(\n            \"flex flex-col gap-3 border-border/50 border-t px-3 py-2\",\n            \"overflow-hidden\",\n            // REMINDER: otherwise, if defaultOpen is true, the animation will be triggered and we have a layout shift\n            \"data-[animate=true]:data-[state=closed]:animate-collapsible-up data-[animate=true]:data-[state=open]:animate-collapsible-down\",\n          )}\n        >\n          {children}\n        </CollapsibleContent>\n      </Collapsible>\n    </div>\n  );\n}\nStatusComponentGroup.displayName = \"StatusComponentGroup\";\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-component.tsx",
    "content": "\"use client\";\n\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useMediaQuery } from \"@openstatus/ui/hooks/use-media-query\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { formatDistanceToNowStrict } from \"date-fns\";\nimport { InfoIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { StatusIcon as UnifiedStatusIcon } from \"@openstatus/ui/components/blocks/status-icon\";\nimport type {\n  StatusBarData,\n  StatusType,\n} from \"@openstatus/ui/components/blocks/status.types\";\nimport { systemStatusLabels } from \"@openstatus/ui/components/blocks/status.utils\";\n\n// ============================================================================\n// Layout Components\n// ============================================================================\n\ninterface StatusComponentProps extends React.ComponentProps<\"div\"> {\n  variant: Exclude<StatusType, \"empty\">;\n}\n\n/**\n * StatusComponent - Root container for individual monitor/service status displays\n *\n * This component serves as the main container for displaying a single monitor or\n * service status. It establishes the status type context via data-variant attribute,\n * which child components (like StatusComponentIcon and StatusComponentStatus) use\n * to display the appropriate colors and icons.\n *\n * The component acts as a CSS group (/component) for advanced selector patterns,\n * enabling child components to style themselves based on the parent's variant.\n *\n * @param variant - The status type (success, degraded, error, or info)\n *\n * @example\n * ```tsx\n * <StatusComponent variant=\"success\">\n *   <StatusComponentHeader>\n *     <StatusComponentHeaderLeft>\n *       <StatusComponentIcon />\n *       <StatusComponentTitle>API Server</StatusComponentTitle>\n *       <StatusComponentDescription>Main API endpoint</StatusComponentDescription>\n *     </StatusComponentHeaderLeft>\n *     <StatusComponentHeaderRight>\n *       <StatusComponentUptime>99.9%</StatusComponentUptime>\n *       <StatusComponentStatus />\n *     </StatusComponentHeaderRight>\n *   </StatusComponentHeader>\n *   <StatusComponentBody>\n *     <StatusBar data={uptimeData} />\n *   </StatusComponentBody>\n * </StatusComponent>\n * ```\n *\n * @see StatusComponentHeader - For header layout\n * @see StatusComponentBody - For content area\n * @see StatusComponentIcon - For status indicator icon\n * @see StatusComponentStatus - For status label\n */\nexport function StatusComponent({\n  variant,\n  className,\n  children,\n  ...props\n}: StatusComponentProps) {\n  return (\n    <div\n      data-slot=\"status-component\"\n      data-variant={variant}\n      className={cn(\"group/component space-y-2\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusComponent.displayName = \"StatusComponent\";\n\n// ============================================================================\n// Header Components\n// ============================================================================\n\n/**\n * StatusComponentHeader - Header container for monitor status information\n *\n * Provides a flex container with space-between alignment for the monitor header,\n * typically containing title/description on the left and status/uptime on the right.\n *\n * @example\n * ```tsx\n * <StatusComponentHeader>\n *   <StatusComponentHeaderLeft>\n *     <StatusComponentIcon />\n *     <StatusComponentTitle>Database</StatusComponentTitle>\n *   </StatusComponentHeaderLeft>\n *   <StatusComponentHeaderRight>\n *     <StatusComponentUptime>100%</StatusComponentUptime>\n *   </StatusComponentHeaderRight>\n * </StatusComponentHeader>\n * ```\n *\n * @see StatusComponentHeaderLeft - For left-aligned content\n * @see StatusComponentHeaderRight - For right-aligned content\n */\nexport function StatusComponentHeader({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-component-header\"\n      className={cn(\"flex items-center justify-between\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusComponentHeader.displayName = \"StatusComponentHeader\";\n\n/**\n * StatusComponentHeaderLeft - Left-aligned header content container\n *\n * Provides a flex container with gap-2 spacing for left-aligned header elements,\n * typically containing the status icon, title, and optional description icon.\n *\n * @example\n * ```tsx\n * <StatusComponentHeaderLeft>\n *   <StatusComponentIcon />\n *   <StatusComponentTitle>API Gateway</StatusComponentTitle>\n *   <StatusComponentDescription>\n *     Handles all incoming requests\n *   </StatusComponentDescription>\n * </StatusComponentHeaderLeft>\n * ```\n *\n * @see StatusComponentIcon - For status indicator\n * @see StatusComponentTitle - For service name\n * @see StatusComponentDescription - For info tooltip\n */\nexport function StatusComponentHeaderLeft({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-component-header-left\"\n      className={cn(\"flex items-center gap-2\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusComponentHeaderLeft.displayName = \"StatusComponentHeaderLeft\";\n\n/**\n * StatusComponentHeaderRight - Right-aligned header content container\n *\n * Provides a flex container with gap-3 spacing for right-aligned header elements,\n * typically containing uptime percentage and status label.\n *\n * @example\n * ```tsx\n * <StatusComponentHeaderRight>\n *   <StatusComponentUptime>99.95%</StatusComponentUptime>\n *   <StatusComponentStatus />\n * </StatusComponentHeaderRight>\n * ```\n *\n * @see StatusComponentUptime - For uptime percentage display\n * @see StatusComponentStatus - For status label\n */\nexport function StatusComponentHeaderRight({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-component-header-right\"\n      className={cn(\"flex items-center gap-3\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusComponentHeaderRight.displayName = \"StatusComponentHeaderRight\";\n\n// ============================================================================\n// Content Components\n// ============================================================================\n\n/**\n * StatusComponentBody - Main content area for monitor visualizations\n *\n * Provides vertical spacing (space-y-2) for stacking content like status bars,\n * charts, or other status visualizations within the component.\n *\n * @example\n * ```tsx\n * <StatusComponentBody>\n *   <StatusBar data={uptimeData} />\n *   <StatusComponentFooter data={uptimeData} />\n * </StatusComponentBody>\n * ```\n *\n * @see StatusBar - For uptime visualization bars\n * @see StatusComponentFooter - For footer with date range\n */\nexport function StatusComponentBody({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-component-body\"\n      className={cn(\"space-y-2\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusComponentBody.displayName = \"StatusComponentBody\";\n\n// ============================================================================\n// Display Components\n// ============================================================================\n\n/**\n * StatusComponentTitle - Monitor or service name display\n *\n * Displays the monitor/service name in monospace font with truncation for long names.\n * The text is medium weight and uses a base font size.\n *\n * @example\n * ```tsx\n * <StatusComponentTitle>Production API</StatusComponentTitle>\n * ```\n *\n * @example\n * ```tsx\n * <StatusComponentTitle>\n *   {monitor.name}\n * </StatusComponentTitle>\n * ```\n */\nexport function StatusComponentTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-component-title\"\n      className={cn(\n        \"truncate font-medium font-mono text-base text-foreground leading-5\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusComponentTitle.displayName = \"StatusComponentTitle\";\n\n/**\n * StatusComponentDescription - Info icon with tooltip for additional details\n *\n * Displays an info icon that shows a tooltip on hover (or tap on touch devices)\n * with additional description text. Returns null if no children are provided,\n * allowing for conditional rendering.\n *\n * Touch device support is built-in, toggling the tooltip on tap instead of\n * requiring hover.\n *\n * @param children - The description text to show in the tooltip\n *\n * @example\n * ```tsx\n * <StatusComponentDescription>\n *   This service handles user authentication and authorization\n * </StatusComponentDescription>\n * ```\n *\n * @example\n * ```tsx\n * // Conditionally rendered - returns null if no description\n * <StatusComponentDescription>\n *   {monitor.description}\n * </StatusComponentDescription>\n * ```\n */\nexport function StatusComponentDescription({\n  onClick,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipTrigger>) {\n  const isTouch = useMediaQuery(\"(hover: none)\");\n  const [open, setOpen] = useState(false);\n\n  if (!children) return null;\n\n  return (\n    <TooltipProvider delayDuration={0}>\n      <Tooltip open={open} onOpenChange={setOpen}>\n        <TooltipTrigger\n          onClick={(e) => {\n            if (isTouch) setOpen((prev) => !prev);\n            onClick?.(e);\n          }}\n          className=\"rounded-full\"\n          {...props}\n        >\n          <InfoIcon className=\"size-4 text-muted-foreground\" />\n        </TooltipTrigger>\n        <TooltipContent>\n          <p>{children}</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\nStatusComponentDescription.displayName = \"StatusComponentDescription\";\n\n/**\n * StatusComponentIcon - Status indicator icon for component context\n *\n * This component wraps the unified StatusIcon with variant=\"component\", configuring\n * it to respond to the parent StatusComponent's data-variant attribute.\n * The displayed icon and color automatically change based on the status type:\n * - success: Green check icon\n * - degraded: Yellow warning triangle\n * - error: Red alert circle\n * - info: Blue wrench icon\n *\n * The icon is smaller (size-[12.5px]) than other variants, optimized for inline\n * display next to monitor titles.\n *\n * @example\n * ```tsx\n * <StatusComponent variant=\"degraded\">\n *   <StatusComponentHeaderLeft>\n *     <StatusComponentIcon />\n *     <StatusComponentTitle>CDN</StatusComponentTitle>\n *   </StatusComponentHeaderLeft>\n * </StatusComponent>\n * ```\n *\n * @see StatusComponent - For setting the variant context\n * @see StatusIcon from status-icon.tsx - For the underlying unified icon implementation\n */\nexport function StatusComponentIcon({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <UnifiedStatusIcon variant=\"component\" className={className} {...props} />\n  );\n}\nStatusComponentIcon.displayName = \"StatusComponentIcon\";\n\n/**\n * StatusComponentFooter - Date range footer for status visualizations\n *\n * Displays a date range footer showing the time span of the displayed data,\n * with the start date on the left (formatted as relative time like \"45 days ago\")\n * and \"today\" on the right. Shows a skeleton loader when data is loading.\n *\n * If no data is available, displays a dash (-) on the left side.\n *\n * @param data - Array of status bar data points (uses first item's date for start)\n * @param isLoading - Whether the data is currently loading\n *\n * @example\n * ```tsx\n * <StatusComponentBody>\n *   <StatusBar data={uptimeData} />\n *   <StatusComponentFooter data={uptimeData} isLoading={false} />\n * </StatusComponentBody>\n * ```\n *\n * @example\n * ```tsx\n * // With loading state\n * <StatusComponentFooter data={[]} isLoading={true} />\n * ```\n *\n * @see StatusBar - For the visualization that this footer describes\n */\nexport function StatusComponentFooter({\n  data,\n  isLoading,\n}: {\n  data: StatusBarData[];\n  isLoading?: boolean;\n}) {\n  return (\n    <div\n      data-slot=\"status-component-footer\"\n      className=\"flex flex-row items-center justify-between font-mono text-muted-foreground text-xs leading-none\"\n    >\n      <div>\n        {isLoading ? (\n          <Skeleton className=\"h-3 w-18\" />\n        ) : data.length > 0 ? (\n          formatDistanceToNowStrict(new Date(data[0].day), {\n            unit: \"day\",\n            addSuffix: true,\n          })\n        ) : (\n          \"-\"\n        )}\n      </div>\n      <div>today</div>\n    </div>\n  );\n}\nStatusComponentFooter.displayName = \"StatusComponentFooter\";\n\n/**\n * StatusComponentUptime - Uptime percentage display\n *\n * Displays the uptime percentage in monospace font with slightly muted foreground\n * color. Typically shows values like \"99.9%\" or \"100%\".\n *\n * @example\n * ```tsx\n * <StatusComponentUptime>99.95%</StatusComponentUptime>\n * ```\n *\n * @example\n * ```tsx\n * <StatusComponentUptime>\n *   {calculateUptime(data)}%\n * </StatusComponentUptime>\n * ```\n *\n * @see StatusComponentUptimeSkeleton - For loading state\n */\nexport function StatusComponentUptime({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-component-uptime\"\n      className={cn(\n        \"font-mono text-foreground/80 text-sm leading-none\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusComponentUptime.displayName = \"StatusComponentUptime\";\n\n/**\n * StatusComponentUptimeSkeleton - Loading skeleton for uptime percentage\n *\n * Displays a skeleton loader matching the size of the uptime percentage display\n * (h-4 w-16), used while uptime data is being fetched.\n *\n * @example\n * ```tsx\n * <StatusComponentHeaderRight>\n *   {isLoading ? (\n *     <StatusComponentUptimeSkeleton />\n *   ) : (\n *     <StatusComponentUptime>{uptime}%</StatusComponentUptime>\n *   )}\n * </StatusComponentHeaderRight>\n * ```\n *\n * @see StatusComponentUptime - For the actual uptime display\n */\nexport function StatusComponentUptimeSkeleton({\n  className,\n  ...props\n}: React.ComponentProps<typeof Skeleton>) {\n  return <Skeleton className={cn(\"h-4 w-16\", className)} {...props} />;\n}\n\n/**\n * StatusComponentStatus - Automatic status label display\n *\n * Displays a status label that automatically shows the appropriate text and color\n * based on the parent StatusComponent's variant. The component uses CSS data\n * attribute selectors to show only the relevant status label:\n * - success: \"Operational\" (green)\n * - degraded: \"Degraded\" (yellow)\n * - error: \"Outage\" (red)\n * - info: \"Maintenance\" (blue)\n *\n * The labels are sourced from systemStatusLabels.short for consistent messaging\n * across the application.\n *\n * @example\n * ```tsx\n * <StatusComponent variant=\"success\">\n *   <StatusComponentHeaderRight>\n *     <StatusComponentStatus />\n *     // Displays \"Operational\" in green\n *   </StatusComponentHeaderRight>\n * </StatusComponent>\n * ```\n *\n * @example\n * ```tsx\n * <StatusComponent variant=\"degraded\">\n *   <StatusComponentHeaderRight>\n *     <StatusComponentStatus />\n *     // Displays \"Degraded\" in yellow\n *   </StatusComponentHeaderRight>\n * </StatusComponent>\n * ```\n *\n * @see StatusComponent - For setting the variant that controls the displayed status\n * @see systemStatusLabels - For the status label text definitions\n */\nexport function StatusComponentStatus({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-component-status\"\n      className={cn(\n        \"font-mono text-sm leading-none\",\n        \"group-data-[variant=success]/component:text-success\",\n        \"group-data-[variant=degraded]/component:text-warning\",\n        \"group-data-[variant=error]/component:text-destructive\",\n        \"group-data-[variant=info]/component:text-info\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"hidden group-data-[variant=success]/component:block\">\n        {systemStatusLabels.success.short}\n      </span>\n      <span className=\"hidden group-data-[variant=degraded]/component:block\">\n        {systemStatusLabels.degraded.short}\n      </span>\n      <span className=\"hidden group-data-[variant=error]/component:block\">\n        {systemStatusLabels.error.short}\n      </span>\n      <span className=\"hidden group-data-[variant=info]/component:block\">\n        {systemStatusLabels.info.short}\n      </span>\n    </div>\n  );\n}\nStatusComponentStatus.displayName = \"StatusComponentStatus\";\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-events.tsx",
    "content": "import { Badge } from \"@openstatus/ui/components/ui/badge\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\nimport { formatDistanceStrict } from \"date-fns\";\nimport { Check } from \"lucide-react\";\nimport {\n  formatDateRange,\n  incidentStatusLabels,\n  formatDate,\n  formatDateTime,\n} from \"@openstatus/ui/components/blocks/status.utils\";\nimport type { StatusReportUpdateType } from \"@openstatus/ui/components/blocks/status.types\";\nimport { StatusTimestamp } from \"@openstatus/ui/components/blocks/status-timestamp\";\n\n// ============================================================================\n// Container Components\n// ============================================================================\n\n/**\n * StatusEventGroup - Root container for status events and incident reports\n *\n * Provides a vertical flex container with consistent spacing (gap-4) for\n * displaying a feed of status events, incident reports, and maintenance notices.\n * The component includes ARIA role=\"feed\" for accessibility.\n *\n * @example\n * ```tsx\n * <StatusEventGroup>\n *   <StatusEvent>\n *     // First incident...\n *   </StatusEvent>\n *   <StatusEvent>\n *     // Second incident...\n *   </StatusEvent>\n * </StatusEventGroup>\n * ```\n *\n * @see StatusEvent - For individual event items\n */\nexport function StatusEventGroup({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-event-group\"\n      className={cn(\"flex flex-col gap-4\", className)}\n      role=\"feed\"\n      aria-label=\"Status events and updates\"\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * StatusEvent - Individual event container within StatusEventGroup\n *\n * Container for a single event (incident, report, or maintenance) with relative\n * positioning to support absolutely positioned date aside elements.\n *\n * @example\n * ```tsx\n * <StatusEvent>\n *   <StatusEventAside>\n *     <StatusEventDate date={new Date()} />\n *   </StatusEventAside>\n *   <StatusEventContent>\n *     <StatusEventTitle>API Outage</StatusEventTitle>\n *     // Event details...\n *   </StatusEventContent>\n * </StatusEvent>\n * ```\n */\nexport function StatusEvent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-event\"\n      className={cn(\"relative flex flex-col gap-2\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n// ============================================================================\n// Content Components\n// ============================================================================\n\n/**\n * StatusEventContent - Main content container for event details\n *\n * Provides a hoverable container with rounded borders and muted background on hover.\n * The hoverable behavior can be disabled for non-interactive events.\n *\n * @param hoverable - Whether to show hover effects (default: true)\n *\n * @example\n * ```tsx\n * <StatusEventContent>\n *   <StatusEventTitle>Database Maintenance</StatusEventTitle>\n *   <p>Scheduled maintenance from 2-4 AM UTC</p>\n * </StatusEventContent>\n * ```\n */\nexport function StatusEventContent({\n  className,\n  hoverable = true,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  hoverable?: boolean;\n}) {\n  return (\n    <div\n      data-slot=\"status-event-content\"\n      data-hoverable={hoverable}\n      className={cn(\n        \"group -mx-3 -my-2 flex flex-col gap-2 rounded-lg border border-transparent px-3 py-2\",\n        \"data-[hoverable=true]:hover:cursor-pointer data-[hoverable=true]:hover:border-border/50 data-[hoverable=true]:hover:bg-muted/50\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * StatusEventTitle - Title for status events\n *\n * Displays the event title in medium-weight font, typically used for incident\n * names or maintenance titles.\n *\n * @example\n * ```tsx\n * <StatusEventTitle>API Gateway Outage</StatusEventTitle>\n * ```\n */\nexport function StatusEventTitle({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-event-title\"\n      className={cn(\"font-medium\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * StatusEventTitleCheck - Resolved status indicator with tooltip\n *\n * Displays a green check icon in a circular badge with a tooltip explaining\n * that the report has been resolved. Typically displayed next to event titles.\n *\n * @example\n * ```tsx\n * <div className=\"flex items-center gap-2\">\n *   <StatusEventTitle>API Outage</StatusEventTitle>\n *   <StatusEventTitleCheck />\n * </div>\n * ```\n */\nexport function StatusEventTitleCheck({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-event-title-check\"\n      className={cn(\"flex items-center pl-1\", className)}\n      {...props}\n    >\n      <TooltipProvider>\n        <Tooltip>\n          <TooltipTrigger aria-label=\"Report resolved\">\n            <div className=\"rounded-full border border-success/20 bg-success/10 p-0.5 text-success\">\n              <Check className=\"size-3 shrink-0\" aria-hidden=\"true\" />\n            </div>\n          </TooltipTrigger>\n          <TooltipContent>\n            <p>Report resolved</p>\n          </TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n    </div>\n  );\n}\n\n// ============================================================================\n// Affected Services Components\n// ============================================================================\n\n/**\n * StatusEventAffected - Container for affected service badges\n *\n * Displays a wrapping flex container for StatusEventAffectedBadge components,\n * showing which services were impacted by an incident.\n *\n * @example\n * ```tsx\n * <StatusEventAffected>\n *   <StatusEventAffectedBadge>API</StatusEventAffectedBadge>\n *   <StatusEventAffectedBadge>Database</StatusEventAffectedBadge>\n *   <StatusEventAffectedBadge>CDN</StatusEventAffectedBadge>\n * </StatusEventAffected>\n * ```\n */\nexport function StatusEventAffected({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-event-affected\"\n      className={cn(\"flex flex-wrap gap-1\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * StatusEventAffectedBadge - Badge for individual affected service\n *\n * Displays a small secondary-style badge representing a single affected service.\n * Uses a smaller font size (text-[10px]) for compact display.\n *\n * @example\n * ```tsx\n * <StatusEventAffectedBadge>REST API</StatusEventAffectedBadge>\n * ```\n */\nexport function StatusEventAffectedBadge({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <Badge\n      data-slot=\"status-event-affected-badge\"\n      variant=\"secondary\"\n      className={cn(\"text-[10px]\", className)}\n      {...props}\n    >\n      {children}\n    </Badge>\n  );\n}\n\n// ============================================================================\n// Date/Time Components\n// ============================================================================\n\n/**\n * StatusEventDate - Date display with relative time badge\n *\n * Displays a formatted date with a relative time badge (e.g., \"2 days ago\").\n * For future dates, the badge is highlighted in info color. The layout is\n * responsive: horizontal on mobile (gap-2), vertical on desktop (flex-col).\n *\n * @param date - The event date to display\n *\n * @example\n * ```tsx\n * <StatusEventDate date={new Date(\"2024-01-15\")} />\n * // Displays: \"Jan 15, 2024\" with \"2 days ago\" badge\n * ```\n */\nexport function StatusEventDate({\n  className,\n  date,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  date: Date;\n}) {\n  const isFuture = date > new Date();\n  const distance = formatDistanceStrict(date, new Date(), { addSuffix: true });\n  return (\n    <div\n      data-slot=\"status-event-date\"\n      className={cn(\"flex gap-2 lg:flex-col\", className)}\n      {...props}\n    >\n      <div className=\"font-medium text-foreground\">\n        {formatDate(date, { month: \"short\" })}\n      </div>{\" \"}\n      <Badge\n        data-slot=\"status-event-date-badge\"\n        variant=\"secondary\"\n        className={cn(\n          \"text-[10px]\",\n          isFuture ? \"bg-info text-background dark:text-foreground\" : \"\",\n        )}\n      >\n        {distance}\n      </Badge>\n    </div>\n  );\n}\n\n/**\n * StatusEventAside - Sidebar date container (desktop only)\n *\n * Positions the date to the left of event content on desktop screens (lg breakpoint).\n * On mobile, it appears inline. Uses sticky positioning on desktop to keep dates\n * visible while scrolling through long events.\n *\n * @example\n * ```tsx\n * <StatusEvent>\n *   <StatusEventAside>\n *     <StatusEventDate date={incidentDate} />\n *   </StatusEventAside>\n *   <StatusEventContent>\n *     // Event content...\n *   </StatusEventContent>\n * </StatusEvent>\n * ```\n */\nexport function StatusEventAside({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-event-aside\"\n      className=\"lg:-left-32 border border-transparent lg:absolute lg:top-0 lg:h-full\"\n    >\n      <div className={cn(\"lg:sticky lg:top-0 lg:left-0\", className)} {...props}>\n        {children}\n      </div>\n    </div>\n  );\n}\n\ninterface StatusReportUpdate {\n  date: Date;\n  message: string;\n  status: StatusReportUpdateType;\n}\n\n// ============================================================================\n// Timeline Components\n// ============================================================================\n\n/**\n * StatusEventTimelineReport - Timeline of incident report updates\n *\n * Displays a chronological timeline of incident updates, sorted from newest to\n * oldest. Each update shows the status (investigating → identified → monitoring → resolved),\n * timestamp, message, and time elapsed between updates.\n *\n * **Automatic Duration Calculation**:\n * - First update (most recent): Shows total time from start to resolution (if resolved)\n * - Other updates: Shows time elapsed since the previous update\n *\n * @param updates - Array of report updates to display\n * @param withDot - Whether to show colored status dots (default: true)\n * @param maxUpdates - Maximum number of updates to display (optional, shows all if not specified)\n *\n * @example\n * ```tsx\n * <StatusEventTimelineReport\n *   updates={[\n *     {\n *       status: \"resolved\",\n *       message: \"All services restored\",\n *       date: new Date(\"2024-01-15T12:00:00Z\")\n *     },\n *     {\n *       status: \"monitoring\",\n *       message: \"Fix deployed, monitoring recovery\",\n *       date: new Date(\"2024-01-15T11:45:00Z\")\n *     },\n *     {\n *       status: \"identified\",\n *       message: \"Root cause identified in database\",\n *       date: new Date(\"2024-01-15T11:15:00Z\")\n *     },\n *     {\n *       status: \"investigating\",\n *       message: \"Investigating API timeouts\",\n *       date: new Date(\"2024-01-15T11:00:00Z\")\n *     }\n *   ]}\n * />\n * // Displays timeline with: \"Resolved (in 1 hour)\" → \"Monitoring (15 minutes earlier)\" → etc.\n * ```\n *\n * @see StatusEventTimelineReportUpdate - For individual update rendering\n */\nexport function StatusEventTimelineReport({\n  className,\n  updates,\n  withDot = true,\n  maxUpdates,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  updates: StatusReportUpdate[];\n  withDot?: boolean;\n  maxUpdates?: number;\n}) {\n  const sortedUpdates = [...updates].sort(\n    (a, b) => b.date.getTime() - a.date.getTime(),\n  );\n  const displayedUpdates = maxUpdates\n    ? sortedUpdates.slice(0, maxUpdates)\n    : sortedUpdates;\n\n  return (\n    <div\n      data-slot=\"status-event-timeline-report\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    >\n      {/* NOTE: make sure they are sorted by date */}\n      {displayedUpdates.map((update, index) => {\n        const updateDate = new Date(update.date);\n        let durationText: string | undefined;\n\n        if (index === 0) {\n          const startedAt = new Date(\n            sortedUpdates[sortedUpdates.length - 1].date,\n          );\n          const duration = formatDistanceStrict(startedAt, updateDate);\n\n          if (duration !== \"0 seconds\" && update.status === \"resolved\") {\n            durationText = `(in ${duration})`;\n          }\n        } else {\n          const lastUpdateDate = new Date(displayedUpdates[index - 1].date);\n          const timeFromLast = formatDistanceStrict(updateDate, lastUpdateDate);\n          durationText = `(${timeFromLast} earlier)`;\n        }\n\n        return (\n          <StatusEventTimelineReportUpdate\n            key={index}\n            report={update}\n            duration={durationText}\n            withSeparator={index !== displayedUpdates.length - 1}\n            withDot={withDot}\n            isLast={index === displayedUpdates.length - 1}\n          />\n        );\n      })}\n    </div>\n  );\n}\n\n/**\n * StatusEventTimelineReportUpdate - Single update entry in incident timeline\n *\n * Displays one update in the incident timeline with:\n * - Colored dot indicator (red=investigating, yellow=identified, blue=monitoring, green=resolved)\n * - Status label and timestamp (with StatusTimestamp for rich hover details)\n * - Duration text (e.g., \"in 1 hour\" or \"15 minutes earlier\")\n * - Update message\n * - Optional vertical separator line connecting to next update\n *\n * @param report - The update data (status, message, date)\n * @param duration - Optional duration text to display\n * @param withSeparator - Whether to show separator line to next update (default: true)\n * @param withDot - Whether to show colored status dot (default: true)\n * @param isLast - Whether this is the last update (affects bottom margin)\n *\n * @example\n * ```tsx\n * <StatusEventTimelineReportUpdate\n *   report={{\n *     status: \"resolved\",\n *     message: \"All systems operational\",\n *     date: new Date()\n *   }}\n *   duration=\"(in 45 minutes)\"\n *   withSeparator={false}\n *   isLast={true}\n * />\n * ```\n *\n * @see StatusEventTimelineDot - For the colored dot indicator\n * @see StatusEventTimelineSeparator - For the connecting line\n */\nexport function StatusEventTimelineReportUpdate({\n  report,\n  duration,\n  withSeparator = true,\n  withDot = true,\n  isLast = false,\n}: {\n  report: StatusReportUpdate;\n  withSeparator?: boolean;\n  duration?: string;\n  withDot?: boolean;\n  isLast?: boolean;\n}) {\n  return (\n    <div\n      data-slot=\"status-event-timeline-report-update\"\n      data-variant={report.status}\n      className=\"group\"\n    >\n      <div className=\"flex flex-row items-center justify-between gap-2\">\n        <div className=\"flex flex-row gap-4\">\n          {withDot ? (\n            <div className=\"flex flex-col\">\n              <div className=\"flex h-5 flex-col items-center justify-center\">\n                <StatusEventTimelineDot />\n              </div>\n              {withSeparator ? <StatusEventTimelineSeparator /> : null}\n            </div>\n          ) : null}\n          <div className={cn(isLast ? \"mb-0\" : \"mb-2\")}>\n            <StatusEventTimelineTitle>\n              <span>{incidentStatusLabels[report.status]}</span>{\" \"}\n              <span className=\"text-muted-foreground/70\">·</span>{\" \"}\n              <span className=\"font-mono text-muted-foreground text-xs\">\n                <StatusTimestamp date={report.date} variant=\"rich\" asChild>\n                  <span>{formatDateTime(report.date)}</span>\n                </StatusTimestamp>\n              </span>{\" \"}\n              {duration ? (\n                <span className=\"font-mono text-muted-foreground/70 text-xs\">\n                  {duration}\n                </span>\n              ) : null}\n            </StatusEventTimelineTitle>\n            <StatusEventTimelineMessage>\n              {report.message.trim() === \"\" ? (\n                <span className=\"text-muted-foreground/70\">-</span>\n              ) : (\n                // NOTE: App should wrap this with ProcessMessage if needed\n                <span>{report.message}</span>\n              )}\n            </StatusEventTimelineMessage>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\ninterface StatusMaintenanceUpdate {\n  title: string;\n  message: string;\n  from: Date;\n  to: Date;\n}\n\n/**\n * StatusEventTimelineMaintenance - Timeline entry for maintenance windows\n *\n * Displays a maintenance window with title, date range, duration, and message.\n * Uses a blue dot indicator to distinguish from incident updates.\n *\n * The date range is formatted and split to allow individual StatusTimestamp\n * components for each date, providing rich hover details.\n *\n * @param maintenance - The maintenance data (title, message, from, to dates)\n * @param withDot - Whether to show the blue maintenance dot (default: true)\n *\n * @example\n * ```tsx\n * <StatusEventTimelineMaintenance\n *   maintenance={{\n *     title: \"Database Upgrade\",\n *     message: \"Upgrading to PostgreSQL 15\",\n *     from: new Date(\"2024-01-20T02:00:00Z\"),\n *     to: new Date(\"2024-01-20T04:00:00Z\")\n *   }}\n * />\n * // Displays: [●] Database Upgrade · Jan 20, 2:00 AM - 4:00 AM (for 2 hours)\n * //           Upgrading to PostgreSQL 15\n * ```\n *\n * @see StatusEventTimelineDot - For the colored indicator\n * @see StatusTimestamp - For rich timestamp hover cards\n */\nexport function StatusEventTimelineMaintenance({\n  maintenance,\n  withDot = true,\n}: {\n  maintenance: StatusMaintenanceUpdate;\n  withDot?: boolean;\n}) {\n  const duration = formatDistanceStrict(maintenance.from, maintenance.to);\n  const range = formatDateRange(maintenance.from, maintenance.to);\n  // NOTE: because formatDateRange is sure to return a range, we can split it into two dates\n  const [from, to] = range.split(\" - \");\n  return (\n    <div\n      data-slot=\"status-event-timeline-maintenance\"\n      data-variant=\"maintenance\"\n      className=\"group\"\n    >\n      <div className=\"flex flex-row items-center justify-between gap-2\">\n        <div className=\"flex flex-row gap-4\">\n          {withDot ? (\n            <div className=\"flex flex-col\">\n              <div className=\"flex h-5 flex-col items-center justify-center\">\n                <StatusEventTimelineDot />\n              </div>\n            </div>\n          ) : null}\n          {/* NOTE: is always last, no need for className=\"mb-2\" */}\n          <div>\n            <StatusEventTimelineTitle>\n              <span>{maintenance.title}</span>{\" \"}\n              <span className=\"text-muted-foreground/70\">·</span>{\" \"}\n              <span className=\"font-mono text-muted-foreground text-xs\">\n                <StatusTimestamp date={maintenance.from} variant=\"rich\" asChild>\n                  <span>{from}</span>\n                </StatusTimestamp>\n                {\" - \"}\n                <StatusTimestamp date={maintenance.to} variant=\"rich\" asChild>\n                  <span>{to}</span>\n                </StatusTimestamp>\n              </span>{\" \"}\n              {duration ? (\n                <span className=\"font-mono text-muted-foreground/70 text-xs\">\n                  (for {duration})\n                </span>\n              ) : null}\n            </StatusEventTimelineTitle>\n            <StatusEventTimelineMessage>\n              {maintenance.message.trim() === \"\" ? (\n                <span className=\"text-muted-foreground/70\">-</span>\n              ) : (\n                maintenance.message\n              )}\n            </StatusEventTimelineMessage>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/**\n * StatusEventTimelineTitle - Title line for timeline entries\n *\n * Displays the title line of timeline entries with medium font weight,\n * typically containing status label, timestamp, and duration.\n *\n * @example\n * ```tsx\n * <StatusEventTimelineTitle>\n *   <span>Resolved</span> · <span>Jan 15, 10:30 AM</span>\n * </StatusEventTimelineTitle>\n * ```\n */\nexport function StatusEventTimelineTitle({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-event-timeline-title\"\n      className={cn(\"font-medium text-foreground text-sm\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * StatusEventTimelineMessage - Message content for timeline entries\n *\n * Displays the update message in monospace font with muted color and\n * consistent padding.\n *\n * @example\n * ```tsx\n * <StatusEventTimelineMessage>\n *   We have identified the root cause and deployed a fix\n * </StatusEventTimelineMessage>\n * ```\n */\nexport function StatusEventTimelineMessage({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-event-timeline-message\"\n      className={cn(\n        \"py-1.5 font-mono text-muted-foreground text-sm\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * StatusEventTimelineDot - Colored status indicator dot\n *\n * Displays a small circular dot with color based on the parent's data-variant:\n * - investigating: Red (destructive)\n * - identified: Yellow (warning)\n * - monitoring: Blue (info)\n * - resolved: Green (success)\n * - maintenance: Blue (info)\n *\n * @example\n * ```tsx\n * <div data-variant=\"resolved\">\n *   <StatusEventTimelineDot />\n *   // Displays green dot\n * </div>\n * ```\n */\nexport function StatusEventTimelineDot({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-event-timeline-dot\"\n      className={cn(\n        \"size-2.5 shrink-0 rounded-full bg-muted\",\n        \"group-data-[variant=resolved]:bg-success\",\n        \"group-data-[variant=monitoring]:bg-info\",\n        \"group-data-[variant=identified]:bg-warning\",\n        \"group-data-[variant=investigating]:bg-destructive\",\n        \"group-data-[variant=maintenance]:bg-info\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/**\n * StatusEventTimelineSeparator - Vertical line connecting timeline entries\n *\n * Displays a vertical separator line between timeline updates, colored to match\n * the status of the update it's connected to. Uses the same color scheme as\n * StatusEventTimelineDot.\n *\n * @example\n * ```tsx\n * <div data-variant=\"monitoring\">\n *   <StatusEventTimelineDot />\n *   <StatusEventTimelineSeparator />\n *   // Displays blue connecting line\n * </div>\n * ```\n */\nexport function StatusEventTimelineSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      data-slot=\"status-event-timeline-separator\"\n      orientation=\"vertical\"\n      className={cn(\n        \"mx-auto flex-1\",\n        \"group-data-[variant=resolved]:bg-success\",\n        \"group-data-[variant=monitoring]:bg-info\",\n        \"group-data-[variant=identified]:bg-warning\",\n        \"group-data-[variant=investigating]:bg-destructive\",\n        \"group-data-[variant=maintenance]:bg-info\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-feed.tsx",
    "content": "\"use client\";\nimport { useMemo } from \"react\";\nimport {\n  StatusBlankContainer,\n  StatusBlankContent,\n  StatusBlankDescription,\n  StatusBlankReport,\n  StatusBlankTitle,\n} from \"@openstatus/ui/components/blocks/status-blank\";\nimport {\n  StatusEvent,\n  StatusEventAffected,\n  StatusEventAffectedBadge,\n  StatusEventAside,\n  StatusEventContent,\n  StatusEventDate,\n  StatusEventGroup,\n  StatusEventTimelineMaintenance,\n  StatusEventTimelineReport,\n  StatusEventTitle,\n} from \"@openstatus/ui/components/blocks/status-events\";\nimport type {\n  StatusReport,\n  Maintenance,\n} from \"@openstatus/ui/components/blocks/status.types\";\n\ntype UnifiedEvent = {\n  id: number;\n  title: string;\n  startDate: Date;\n} & (\n  | { type: \"report\"; data: StatusReport }\n  | { type: \"maintenance\"; data: Maintenance }\n);\n\n/**\n * isStatusReport - Type guard for discriminating status reports\n *\n * Type guard function that narrows a UnifiedEvent to a status report event.\n * Used internally by StatusFeed to distinguish between reports and maintenance.\n *\n * @param event - The unified event to check\n * @returns True if the event is a status report\n *\n * @example\n * ```tsx\n * if (isStatusReport(event)) {\n *   // TypeScript knows event.data is StatusReport\n *   console.log(event.data.updates);\n * }\n * ```\n */\nfunction isStatusReport(\n  event: UnifiedEvent,\n): event is UnifiedEvent & { type: \"report\"; data: StatusReport } {\n  return event.type === \"report\";\n}\n\n/**\n * isMaintenance - Type guard for discriminating maintenance events\n *\n * Type guard function that narrows a UnifiedEvent to a maintenance event.\n * Used internally by StatusFeed to distinguish between reports and maintenance.\n *\n * @param event - The unified event to check\n * @returns True if the event is a maintenance\n *\n * @example\n * ```tsx\n * if (isMaintenance(event)) {\n *   // TypeScript knows event.data is Maintenance\n *   console.log(event.data.from, event.data.to);\n * }\n * ```\n */\nfunction isMaintenance(\n  event: UnifiedEvent,\n): event is UnifiedEvent & { type: \"maintenance\"; data: Maintenance } {\n  return event.type === \"maintenance\";\n}\n\n/**\n * StatusFeed - Unified feed of incident reports and maintenance events\n *\n * Displays a chronological feed combining both status reports (incidents) and\n * scheduled maintenance, sorted by date from newest to oldest. The component\n * intelligently merges the two event types and renders them using a discriminated\n * union pattern for type safety.\n *\n * **Key Features**:\n * - **Unified Timeline**: Combines reports and maintenance in chronological order\n * - **Discriminated Union**: Type-safe rendering using TypeScript discriminated unions\n * - **Memoization**: Uses `useMemo` to prevent flicker on re-renders\n * - **Empty State**: Shows a styled empty state when no events are present\n * - **Automatic Rendering**: Each event type is rendered with appropriate components\n *\n * **Data Structure**:\n * - **StatusReport**: Incident reports with updates timeline\n *   - id, title, affected services\n *   - updates array with status, message, date\n * - **Maintenance**: Scheduled maintenance windows\n *   - id, title, message, affected services\n *   - from/to date range\n *\n * The component creates a unified event array internally, using stable keys\n * (`${type}-${id}`) to prevent React flicker when data updates.\n *\n * @param statusReports - Array of incident reports to display\n * @param maintenances - Array of maintenance events to display\n *\n * @example\n * // Feed with both reports and maintenance\n * ```tsx\n * <StatusFeed\n *   statusReports={[\n *     {\n *       id: 1,\n *       title: \"API Outage\",\n *       affected: [\"API\", \"Database\"],\n *       updates: [\n *         {\n *           status: \"resolved\",\n *           message: \"All systems operational\",\n *           date: new Date(\"2024-01-15T12:00:00Z\")\n *         },\n *         {\n *           status: \"investigating\",\n *           message: \"Investigating API timeouts\",\n *           date: new Date(\"2024-01-15T11:00:00Z\")\n *         }\n *       ]\n *     }\n *   ]}\n *   maintenances={[\n *     {\n *       id: 2,\n *       title: \"Database Upgrade\",\n *       message: \"Upgrading to PostgreSQL 15\",\n *       affected: [\"Database\"],\n *       from: new Date(\"2024-01-20T02:00:00Z\"),\n *       to: new Date(\"2024-01-20T04:00:00Z\")\n *     }\n *   ]}\n * />\n * ```\n *\n * @example\n * // Empty state (no events)\n * ```tsx\n * <StatusFeed statusReports={[]} maintenances={[]} />\n * // Displays: \"No recent notifications\" empty state\n * ```\n *\n * @example\n * // Only incident reports\n * ```tsx\n * <StatusFeed\n *   statusReports={incidentReports}\n *   maintenances={[]}\n * />\n * ```\n *\n * @see StatusEventGroup - For the feed container\n * @see StatusEventTimelineReport - For incident rendering\n * @see StatusEventTimelineMaintenance - For maintenance rendering\n * @see isStatusReport - For type guard discrimination\n * @see isMaintenance - For type guard discrimination\n */\nexport function StatusFeed({\n  statusReports = [],\n  maintenances = [],\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  statusReports?: StatusReport[];\n  maintenances?: Maintenance[];\n}) {\n  // Memoize the unified events array to prevent flicker on re-renders\n  const unifiedEvents = useMemo<UnifiedEvent[]>(() => {\n    return [\n      ...statusReports.map(\n        (report): UnifiedEvent => ({\n          id: report.id,\n          title: report.title,\n          type: \"report\",\n          startDate:\n            report.updates[report.updates.length - 1]?.date || new Date(),\n          data: report,\n        }),\n      ),\n      ...maintenances.map(\n        (maintenance): UnifiedEvent => ({\n          id: maintenance.id,\n          title: maintenance.title,\n          type: \"maintenance\",\n          startDate: maintenance.from,\n          data: maintenance,\n        }),\n      ),\n    ].sort((a, b) => b.startDate.getTime() - a.startDate.getTime());\n  }, [statusReports, maintenances]);\n\n  if (unifiedEvents.length === 0) {\n    return (\n      <StatusBlankContainer>\n        <div className=\"relative mt-8 flex w-full flex-col items-center justify-center\">\n          <StatusBlankReport className=\"-top-16 absolute scale-60 opacity-50\" />\n          <StatusBlankReport className=\"-top-8 absolute scale-80 opacity-80\" />\n          <StatusBlankReport />\n        </div>\n        <StatusBlankContent>\n          <StatusBlankTitle>No recent notifications</StatusBlankTitle>\n          <StatusBlankDescription>\n            There have been no reports within the last 7 days.\n          </StatusBlankDescription>\n        </StatusBlankContent>\n      </StatusBlankContainer>\n    );\n  }\n\n  return (\n    <StatusEventGroup data-slot=\"status-feed\" {...props}>\n      {unifiedEvents.map((event) => {\n        // Use stable key to prevent flicker\n        const key = `${event.type}-${event.id}`;\n\n        if (isStatusReport(event)) {\n          const report = event.data;\n          return (\n            <StatusEvent key={key}>\n              <StatusEventAside>\n                <StatusEventDate date={event.startDate} />\n              </StatusEventAside>\n              <StatusEventContent>\n                <StatusEventTitle>{report.title}</StatusEventTitle>\n                {report.affected.length > 0 && (\n                  <StatusEventAffected>\n                    {report.affected.map((affected, index) => (\n                      <StatusEventAffectedBadge key={index}>\n                        {affected}\n                      </StatusEventAffectedBadge>\n                    ))}\n                  </StatusEventAffected>\n                )}\n                <StatusEventTimelineReport updates={report.updates} />\n              </StatusEventContent>\n            </StatusEvent>\n          );\n        }\n\n        if (isMaintenance(event)) {\n          const maintenance = event.data;\n          return (\n            <StatusEvent key={key}>\n              <StatusEventAside>\n                <StatusEventDate date={event.startDate} />\n              </StatusEventAside>\n              <StatusEventContent>\n                <StatusEventTitle>{maintenance.title}</StatusEventTitle>\n                {maintenance.affected.length > 0 && (\n                  <StatusEventAffected>\n                    {maintenance.affected.map((affected, index) => (\n                      <StatusEventAffectedBadge key={index}>\n                        {affected}\n                      </StatusEventAffectedBadge>\n                    ))}\n                  </StatusEventAffected>\n                )}\n                <StatusEventTimelineMaintenance\n                  maintenance={{\n                    title: maintenance.title,\n                    message: maintenance.message,\n                    from: maintenance.from,\n                    to: maintenance.to,\n                  }}\n                />\n              </StatusEventContent>\n            </StatusEvent>\n          );\n        }\n        return null;\n      })}\n    </StatusEventGroup>\n  );\n}\nStatusFeed.displayName = \"StatusFeed\";\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-icon.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\";\nimport {\n  AlertCircleIcon,\n  CheckIcon,\n  TriangleAlertIcon,\n  WrenchIcon,\n} from \"lucide-react\";\nimport type { StatusType } from \"@openstatus/ui/components/blocks/status.types\";\n\ninterface StatusIconProps extends React.ComponentProps<\"div\"> {\n  /**\n   * The status type to display\n   */\n  status?: StatusType;\n  /**\n   * The variant determines the CSS selector pattern used for status-based styling\n   * - \"default\": Uses group-data-[variant=...] (for Status component)\n   * - \"banner\": Uses group-data-[status=...]/status-banner (for StatusBanner component)\n   * - \"component\": Uses group-data-[variant=...]/component (for StatusComponent)\n   */\n  variant?: \"default\" | \"banner\" | \"component\";\n}\n\n/**\n * StatusIcon - A unified icon component for displaying status indicators\n *\n * This component renders the appropriate icon based on the status type:\n * - success: CheckIcon\n * - degraded: TriangleAlertIcon\n * - error: AlertCircleIcon\n * - info: WrenchIcon\n * - empty: No icon (muted background)\n *\n * @example\n * // In Status component context\n * <StatusIcon variant=\"default\" />\n *\n * @example\n * // In StatusBanner context\n * <StatusIcon variant=\"banner\" />\n *\n * @example\n * // In StatusComponent context\n * <StatusIcon variant=\"component\" />\n */\nexport function StatusIcon({\n  className,\n  variant = \"default\",\n  status,\n  ...props\n}: StatusIconProps) {\n  // Define size classes based on variant\n  const sizeClasses =\n    variant === \"component\"\n      ? \"size-[12.5px] [&>svg]:size-[9px]\"\n      : \"size-7 [&>svg]:size-4\";\n\n  // Define status-based classes based on variant\n  const statusClasses = (() => {\n    switch (variant) {\n      case \"banner\":\n        return [\n          \"group-data-[status=success]/status-banner:bg-success\",\n          \"group-data-[status=degraded]/status-banner:bg-warning\",\n          \"group-data-[status=error]/status-banner:bg-destructive\",\n          \"group-data-[status=info]/status-banner:bg-info\",\n        ];\n      case \"component\":\n        return [\n          \"group-data-[variant=success]/component:bg-success\",\n          \"group-data-[variant=degraded]/component:bg-warning\",\n          \"group-data-[variant=error]/component:bg-destructive\",\n          \"group-data-[variant=info]/component:bg-info\",\n        ];\n      case \"default\":\n      default:\n        return [\n          \"group-data-[variant=success]:bg-success\",\n          \"group-data-[variant=degraded]:bg-warning\",\n          \"group-data-[variant=error]:bg-destructive\",\n          \"group-data-[variant=info]:bg-info\",\n        ];\n    }\n  })();\n\n  // Define icon visibility classes based on variant\n  const iconVisibilityClasses = (() => {\n    switch (variant) {\n      case \"banner\":\n        return {\n          success: \"group-data-[status=success]/status-banner:block\",\n          degraded: \"group-data-[status=degraded]/status-banner:block\",\n          error: \"group-data-[status=error]/status-banner:block\",\n          info: \"group-data-[status=info]/status-banner:block\",\n        };\n      case \"component\":\n        return {\n          success: \"group-data-[variant=success]/component:block\",\n          degraded: \"group-data-[variant=degraded]/component:block\",\n          error: \"group-data-[variant=error]/component:block\",\n          info: \"group-data-[variant=info]/component:block\",\n        };\n      case \"default\":\n      default:\n        return {\n          success: \"group-data-[variant=success]:block\",\n          degraded: \"group-data-[variant=degraded]:block\",\n          error: \"group-data-[variant=error]:block\",\n          info: \"group-data-[variant=info]:block\",\n        };\n    }\n  })();\n\n  return (\n    <div\n      data-slot=\"status-icon\"\n      className={cn(\n        \"flex items-center justify-center rounded-full bg-muted text-background\",\n        sizeClasses,\n        ...statusClasses,\n        className,\n      )}\n      {...props}\n    >\n      <CheckIcon className={cn(\"hidden\", iconVisibilityClasses.success)} />\n      <TriangleAlertIcon\n        className={cn(\"hidden\", iconVisibilityClasses.degraded)}\n      />\n      <AlertCircleIcon className={cn(\"hidden\", iconVisibilityClasses.error)} />\n      <WrenchIcon className={cn(\"hidden\", iconVisibilityClasses.info)} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-layout.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\";\nimport { StatusIcon as UnifiedStatusIcon } from \"@openstatus/ui/components/blocks/status-icon\";\n\n/**\n * Status - Root container component for status page layouts\n *\n * This component serves as the primary container for status page content, providing\n * consistent spacing and establishing the status type context via data attributes.\n * The status type controls the visual appearance of child StatusIcon components\n * through CSS data attribute selectors (group-data-[variant=...]).\n *\n * The component acts as both a CSS group and peer for advanced selector patterns,\n * enabling child components to style themselves based on the parent's status.\n *\n * @param variant - The status type that determines the overall status indicator appearance\n *\n * @example\n * ```tsx\n * <Status variant=\"success\">\n *   <StatusHeader>\n *     <StatusBrand src=\"/logo.png\" alt=\"Company\" />\n *     <StatusTitle>System Status</StatusTitle>\n *   </StatusHeader>\n *   <StatusContent>\n *     {// Status components here}\n *   </StatusContent>\n * </Status>\n * ```\n *\n * @see StatusHeader - For header content with brand and title\n * @see StatusContent - For main status content area\n * @see StatusIcon - For status indicator icons that respond to variant\n */\nexport function Status({\n  children,\n  className,\n  variant = \"success\",\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  variant?: \"success\" | \"degraded\" | \"error\" | \"info\";\n}) {\n  return (\n    <div\n      data-variant={variant}\n      data-slot=\"status\"\n      className={cn(\"group peer flex flex-col gap-8\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatus.displayName = \"Status\";\n\n/**\n * StatusBrand - Brand logo/image component for status page headers\n *\n * Displays a brand image (typically a company logo) with consistent sizing.\n * The default size is 32x32px (size-8), suitable for header placement.\n *\n * @param src - Image source URL\n * @param alt - Alternative text for the image (required for accessibility)\n *\n * @example\n * ```tsx\n * <StatusHeader>\n *   <StatusBrand src=\"/logo.png\" alt=\"Company Name\" />\n *   <StatusTitle>System Status</StatusTitle>\n * </StatusHeader>\n * ```\n *\n * @see StatusHeader - For positioning brand within header layout\n */\nexport function StatusBrand({\n  src,\n  alt,\n  className,\n  ...props\n}: React.ComponentProps<\"img\">) {\n  return (\n    // biome-ignore lint/a11y/useAltText: <explanation>\n    <img src={src} alt={alt} className={cn(\"size-8\", className)} {...props} />\n  );\n}\nStatusBrand.displayName = \"StatusBrand\";\n\n/**\n * StatusHeader - Header container for status page branding and title\n *\n * Provides a container-query context (@container/status-header) for responsive\n * header layouts. Typically contains StatusBrand, StatusTitle, and StatusDescription\n * components arranged in a flexible layout.\n *\n * The container query context enables child components to respond to the header's\n * width rather than the viewport width, allowing for more modular layouts.\n *\n * @example\n * ```tsx\n * <StatusHeader>\n *   <div className=\"flex items-center gap-4\">\n *     <StatusBrand src=\"/logo.png\" alt=\"Company\" />\n *     <div>\n *       <StatusTitle>System Status</StatusTitle>\n *       <StatusDescription>Current system health</StatusDescription>\n *     </div>\n *   </div>\n * </StatusHeader>\n * ```\n *\n * @see StatusBrand - For brand logo placement\n * @see StatusTitle - For page title\n * @see StatusDescription - For subtitle text\n */\nexport function StatusHeader({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-header\"\n      className={cn(\"@container/status-header\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusHeader.displayName = \"StatusHeader\";\n\n/**\n * StatusTitle - Primary heading for status page\n *\n * Displays the main title with consistent typography (semibold, large text, tight leading).\n * Typically used within StatusHeader to show the page or organization name.\n *\n * The component uses semantic HTML div rather than h1 to allow flexible heading\n * levels based on page context. Apply appropriate heading tags as needed.\n *\n * @example\n * ```tsx\n * <StatusHeader>\n *   <StatusTitle>Acme Inc. Status</StatusTitle>\n *   <StatusDescription>Live system status and uptime</StatusDescription>\n * </StatusHeader>\n * ```\n *\n * @see StatusDescription - For subtitle text below the title\n */\nexport function StatusTitle({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-title\"\n      className={cn(\n        \"font-semibold text-foreground text-lg leading-none\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\nStatusTitle.displayName = \"StatusTitle\";\n\n/**\n * StatusDescription - Descriptive subtitle or secondary text\n *\n * Displays muted secondary text, typically used for subtitles, additional context,\n * or descriptions. The text color is set to muted foreground for visual hierarchy.\n *\n * @example\n * ```tsx\n * <StatusHeader>\n *   <StatusTitle>System Status</StatusTitle>\n *   <StatusDescription>\n *     Real-time monitoring of all services\n *   </StatusDescription>\n * </StatusHeader>\n * ```\n *\n * @see StatusTitle - For primary heading text\n */\nexport function StatusDescription({\n  children,\n  className,\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div data-slot=\"status-description\" className={cn(\"text-muted-foreground\", className)}>\n      {children}\n    </div>\n  );\n}\nStatusDescription.displayName = \"StatusDescription\";\n\n/**\n * StatusContent - Main content area for status page components\n *\n * Provides a vertical flex container with consistent spacing (gap-3) for status\n * page content. This is typically used to stack StatusComponent, StatusBanner,\n * or other status-related components.\n *\n * @example\n * ```tsx\n * <Status variant=\"success\">\n *   <StatusHeader>\n *     <StatusTitle>System Status</StatusTitle>\n *   </StatusHeader>\n *   <StatusContent>\n *     <StatusComponent variant=\"success\">API</StatusComponent>\n *     <StatusComponent variant=\"success\">Database</StatusComponent>\n *     <StatusComponent variant=\"degraded\">CDN</StatusComponent>\n *   </StatusContent>\n * </Status>\n * ```\n *\n * @see Status - For root container\n * @see StatusComponent - For individual service status displays\n */\nexport function StatusContent({\n  children,\n  className,\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div data-slot=\"status-content\" className={cn(\"flex flex-col gap-3\", className)}>\n      {children}\n    </div>\n  );\n}\nStatusContent.displayName = \"StatusContent\";\n\n/**\n * StatusIcon - Status indicator icon wrapper for Status component context\n *\n * This component wraps the unified StatusIcon with variant=\"default\", configuring\n * it to respond to the parent Status component's data-variant attribute.\n * The displayed icon automatically changes based on the status type:\n * - success: CheckIcon\n * - degraded: TriangleAlertIcon\n * - error: AlertCircleIcon\n * - info: WrenchIcon\n *\n * @example\n * ```tsx\n * <Status variant=\"success\">\n *   <StatusHeader>\n *     <StatusIcon />\n *     <StatusTitle>All Systems Operational</StatusTitle>\n *   </StatusHeader>\n * </Status>\n * ```\n *\n * @see Status - For setting the variant context\n * @see StatusIcon from status-icon.tsx - For the underlying unified icon implementation\n */\nexport function StatusIcon({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <UnifiedStatusIcon variant=\"default\" className={className} {...props} />\n  );\n}\nStatusIcon.displayName = \"StatusIcon\";\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status-timestamp.tsx",
    "content": "\"use client\";\n\nimport { UTCDate } from \"@date-fns/utc\";\nimport { format, formatDistanceToNowStrict } from \"date-fns\";\nimport { Check, Copy } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\n\nimport type { HoverCardContentProps } from \"@radix-ui/react-hover-card\";\n\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@openstatus/ui/components/ui/hover-card\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useCopyToClipboard } from \"@openstatus/ui/hooks/use-copy-to-clipboard\";\nimport { useMediaQuery } from \"@openstatus/ui/hooks/use-media-query\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\ntype BaseProps = {\n  date: Date;\n  variant?: \"simple\" | \"rich\";\n  className?: string;\n};\n\ntype SimpleVariantProps = BaseProps &\n  React.ComponentProps<typeof TooltipTrigger> & {\n    variant?: \"simple\";\n  };\n\ntype RichVariantProps = BaseProps &\n  React.ComponentProps<typeof HoverCardTrigger> & {\n    variant: \"rich\";\n    side?: HoverCardContentProps[\"side\"];\n    align?: HoverCardContentProps[\"align\"];\n    alignOffset?: HoverCardContentProps[\"alignOffset\"];\n    sideOffset?: HoverCardContentProps[\"sideOffset\"];\n  };\n\ntype StatusTimestampProps = SimpleVariantProps | RichVariantProps;\n\n/**\n * StatusTimestamp - Polymorphic timestamp display component with timezone support\n *\n * A flexible timestamp component that can display dates in two variants:\n * - **simple**: Shows a tooltip on hover with the formatted date (default)\n * - **rich**: Shows a hover card with local timezone, UTC, and relative time, plus copy-to-clipboard\n *\n * The component automatically detects the user's timezone using `Intl.DateTimeFormat()`\n * and displays the date in both the local timezone and UTC. The rich variant includes\n * a live-updating relative time display (\"2 minutes ago\") that refreshes every second\n * while the hover card is open.\n *\n * Touch device support is built-in for the rich variant, toggling the hover card on tap\n * instead of requiring hover.\n *\n * @param date - The date to display\n * @param variant - Display style: \"simple\" for tooltip, \"rich\" for hover card with details\n * @param side - (rich variant only) Placement of the hover card: \"top\" | \"right\" | \"bottom\" | \"left\"\n * @param align - (rich variant only) Alignment of the hover card: \"start\" | \"center\" | \"end\"\n * @param alignOffset - (rich variant only) Pixel offset for alignment (default: -4)\n * @param sideOffset - (rich variant only) Pixel offset from the trigger\n *\n * @example\n * // Simple variant with tooltip\n * ```tsx\n * <StatusTimestamp date={new Date()} />\n * ```\n *\n * @example\n * // Simple variant with custom children\n * ```tsx\n * <StatusTimestamp date={createdAt}>\n *   Created at\n * </StatusTimestamp>\n * ```\n *\n * @example\n * // Rich variant with hover card showing timezone details\n * ```tsx\n * <StatusTimestamp\n *   date={new Date()}\n *   variant=\"rich\"\n *   side=\"right\"\n * >\n *   2 hours ago\n * </StatusTimestamp>\n * ```\n *\n * @example\n * // Rich variant with custom positioning\n * ```tsx\n * <StatusTimestamp\n *   date={incidentDate}\n *   variant=\"rich\"\n *   side=\"bottom\"\n *   align=\"start\"\n *   alignOffset={0}\n * >\n *   {format(incidentDate, \"MMM d, HH:mm\")}\n * </StatusTimestamp>\n * ```\n */\nexport function StatusTimestamp(props: StatusTimestampProps) {\n  const { date, variant = \"simple\", className, ...rest } = props;\n\n  if (variant === \"rich\") {\n    const {\n      side = \"right\",\n      align = \"start\",\n      alignOffset = -4,\n      sideOffset,\n      children,\n      onClick,\n      ...triggerProps\n    } = rest as Omit<RichVariantProps, \"date\" | \"variant\" | \"className\">;\n\n    return (\n      <RichTimestamp\n        data-slot=\"status-timestamp\"\n        date={date}\n        side={side}\n        align={align}\n        alignOffset={alignOffset}\n        sideOffset={sideOffset}\n        className={className}\n        onClick={onClick}\n        {...triggerProps}\n      >\n        {children}\n      </RichTimestamp>\n    );\n  }\n\n  const { children, ...triggerProps } = rest as Omit<\n    SimpleVariantProps,\n    \"date\" | \"variant\" | \"className\"\n  >;\n\n  return (\n    <SimpleTimestamp\n      data-slot=\"status-timestamp\"\n      date={date}\n      className={className}\n      {...triggerProps}\n    >\n      {children}\n    </SimpleTimestamp>\n  );\n}\nStatusTimestamp.displayName = \"StatusTimestamp\";\n\n/**\n * SimpleTimestamp - Internal tooltip-based timestamp display\n *\n * Displays a formatted timestamp with an underlined, dashed decoration and shows\n * the full formatted date in a tooltip on hover. The timestamp is shown in monospace\n * font with muted foreground color.\n *\n * If no children are provided, displays the date in UTC format. If children are\n * provided, they are used as the trigger text while the tooltip still shows the\n * formatted date in the user's local timezone.\n *\n * @param date - The date to display\n * @param children - Optional custom text to display (falls back to formatted UTC date)\n *\n * @example\n * ```tsx\n * <SimpleTimestamp date={new Date()}>\n *   2 hours ago\n * </SimpleTimestamp>\n * ```\n */\nfunction SimpleTimestamp({\n  date,\n  className,\n  children,\n  ...props\n}: Omit<SimpleVariantProps, \"variant\">) {\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger\n          className={cn(\n            \"font-mono text-muted-foreground underline decoration-muted-foreground/30 decoration-dashed underline-offset-4\",\n            className,\n          )}\n          {...props}\n        >\n          {children || format(new UTCDate(date), \"LLL dd, y HH:mm (z)\")}\n        </TooltipTrigger>\n        <TooltipContent data-slot=\"status-timestamp-content\">\n          <p className=\"font-mono\">{format(date, \"LLL dd, y HH:mm (z)\")}</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\nSimpleTimestamp.displayName = \"SimpleTimestamp\";\n\n/**\n * RichTimestamp - Internal hover card timestamp display with timezone details\n *\n * Displays a hover card with comprehensive timestamp information:\n * - Local timezone timestamp with timezone abbreviation (e.g., \"PST\", \"EST\")\n * - UTC timestamp\n * - Relative time (\"2 hours ago\") that updates every second while open\n *\n * Each row in the hover card is clickable to copy the value to clipboard, with\n * a copy icon that appears on hover and changes to a check mark after copying.\n *\n * Touch device support: On touch devices (detected via `(hover: none)` media query),\n * tapping toggles the hover card open/closed instead of requiring hover.\n *\n * The relative time automatically updates every second while the hover card is\n * open, providing live feedback for recent timestamps.\n *\n * @param date - The date to display\n * @param side - Placement of the hover card (default: \"right\")\n * @param align - Alignment of the hover card (default: \"start\")\n * @param alignOffset - Pixel offset for alignment (default: -4)\n * @param sideOffset - Pixel offset from the trigger\n * @param children - Custom trigger content\n *\n * @example\n * ```tsx\n * <RichTimestamp\n *   date={new Date()}\n *   side=\"bottom\"\n *   align=\"center\"\n * >\n *   Click for details\n * </RichTimestamp>\n * ```\n */\nfunction RichTimestamp({\n  date,\n  side = \"right\",\n  align = \"start\",\n  alignOffset = -4,\n  sideOffset,\n  className,\n  children,\n  onClick,\n  ...props\n}: Omit<RichVariantProps, \"variant\">) {\n  const [open, setOpen] = useState(false);\n  const isTouch = useMediaQuery(\"(hover: none)\");\n  const [_, setRerender] = useState(0);\n\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const relative = formatDistanceToNowStrict(date, { addSuffix: true });\n  const formatted = format(date, \"LLL dd, y HH:mm:ss\");\n  const utc = format(new UTCDate(date), \"LLL dd, y HH:mm:ss\");\n\n  useEffect(() => {\n    // only setInterval if open\n    if (!open) return;\n\n    const interval = setInterval(() => {\n      setRerender((prev) => prev + 1);\n    }, 1000);\n\n    return () => clearInterval(interval);\n  }, [open]);\n\n  return (\n    <HoverCard openDelay={0} closeDelay={0} open={open} onOpenChange={setOpen}>\n      <HoverCardTrigger\n        className={className}\n        onClick={(e) => {\n          // NOTE: support touch devices\n          if (isTouch) setOpen((prev) => !prev);\n          onClick?.(e);\n        }}\n        {...props}\n      >\n        {children}\n      </HoverCardTrigger>\n      <HoverCardContent\n        data-slot=\"status-timestamp-content\"\n        className=\"z-10 w-auto p-2\"\n        {...{ side, align, alignOffset, sideOffset }}\n      >\n        <dl className=\"flex flex-col gap-1\">\n          <StatusTimestampRow value={formatted} label={timezone} />\n          <StatusTimestampRow value={utc} label=\"UTC\" />\n          <StatusTimestampRow value={relative} label=\"Relative\" />\n        </dl>\n      </HoverCardContent>\n    </HoverCard>\n  );\n}\nRichTimestamp.displayName = \"RichTimestamp\";\n\n/**\n * StatusTimestampRow - Internal component for hover card timestamp rows\n *\n * Displays a single row in the rich timestamp hover card with a label (e.g., \"UTC\")\n * and value (e.g., \"Jan 15, 2024 10:30:45\"). The entire row is clickable to copy\n * the value to clipboard.\n *\n * The copy icon appears on hover and changes to a check mark after successful copy.\n * A toast notification is shown when the value is copied.\n *\n * @param value - The timestamp string to display and copy\n * @param label - The label for this timestamp (e.g., \"UTC\", \"PST\", \"Relative\")\n */\nfunction StatusTimestampRow({\n  value,\n  label,\n}: {\n  value: string;\n  label: string;\n}) {\n  const { copy, isCopied } = useCopyToClipboard();\n\n  return (\n    <div\n      data-slot=\"status-timestamp-row\"\n      className=\"group flex items-center justify-between gap-4 text-sm\"\n      onClick={(e) => {\n        e.stopPropagation();\n        copy(value, { withToast: true });\n      }}\n    >\n      <dt className=\"text-muted-foreground\">{label}</dt>\n      <dd className=\"flex items-center gap-1 truncate font-mono\">\n        <span className=\"invisible group-hover:visible\">\n          {!isCopied ? (\n            <Copy className=\"h-3 w-3\" />\n          ) : (\n            <Check className=\"h-3 w-3\" />\n          )}\n        </span>\n        {value}\n      </dd>\n    </div>\n  );\n}\nStatusTimestampRow.displayName = \"StatusTimestampRow\";\n"
  },
  {
    "path": "packages/ui/src/components/blocks/status.types.ts",
    "content": "export type StatusType = \"success\" | \"degraded\" | \"error\" | \"info\" | \"empty\";\nexport type StatusEventType = \"incident\" | \"report\" | \"maintenance\";\nexport type StatusReportUpdateType = \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\"\n\nexport interface StatusReportUpdate {\n  date: Date;\n  message: string;\n  status: StatusReportUpdateType;\n}\n\nexport interface StatusReport {\n  id: number;\n  title: string;\n  affected: string[];\n  updates: StatusReportUpdate[];\n}\n\nexport interface Maintenance {\n  id: number;\n  title: string;\n  affected: string[];\n  message: string;\n  from: Date;\n  to: Date;\n}\n\n// Discriminated union for status events\nexport type StatusEventData =\n  | { type: \"report\"; data: StatusReport }\n  | { type: \"maintenance\"; data: Maintenance };\n\nexport type StatusBarData = {\n    day: string;\n    bar: {\n        status: StatusType;\n        // NOTE: is in percentage! should sum up to 100%\n        height: number;\n    }[];\n    card: {\n        status: StatusType;\n        value: string;\n    }[];\n    events: {\n        id: number;\n        name: string;\n        type: StatusEventType;\n        from: Date | null;\n        to: Date | null;\n    }[];\n}"
  },
  {
    "path": "packages/ui/src/components/blocks/status.utils.ts",
    "content": "import { endOfDay, isSameDay, startOfDay } from \"date-fns\";\n\n/**\n * Formats a date range in a human-readable format.\n *\n * NOTE: While Intl.DateTimeFormat.formatRange() is available in modern browsers,\n * we use a custom implementation to have fine-grained control over the output format\n * and to handle edge cases like \"Since\", \"Until\", and \"All time\" consistently.\n * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatRange\n *\n * @param from - Start date of the range (optional)\n * @param to - End date of the range (optional)\n * @param locale - Locale string for formatting (default: \"en-US\")\n * @returns A formatted date range string\n *\n * @example\n * formatDateRange(new Date('2024-01-01'), new Date('2024-01-05'))\n * // => \"January 1, 2024 - January 5, 2024\"\n *\n * @example\n * formatDateRange(new Date('2024-01-01 10:00'), new Date('2024-01-01 15:00'))\n * // => \"January 1, 10:00 AM - 3:00 PM\"\n */\nexport function formatDateRange(from?: Date, to?: Date) {\n\tconst sameDay = from && to && isSameDay(from, to);\n\tconst isFromStartDay = from && startOfDay(from).getTime() === from.getTime();\n\tconst isToEndDay = to && endOfDay(to).getTime() === to.getTime();\n\n\tif (sameDay) {\n\t\tif (from && to && from.getTime() === to.getTime()) {\n\t\t\treturn formatDateTime(from);\n\t\t}\n\t\tif (from && to) {\n\t\t\treturn `${formatDateTime(from)} - ${formatTime(to)}`;\n\t\t}\n\t}\n\n\tif (from && to) {\n\t\tif (isFromStartDay && isToEndDay) {\n\t\t\treturn `${formatDate(from)} - ${formatDate(to)}`;\n\t\t}\n\t\treturn `${formatDateTime(from)} - ${formatDateTime(to)}`;\n\t}\n\n\tif (to) {\n\t\treturn `Until ${formatDateTime(to)}`;\n\t}\n\n\tif (from) {\n\t\treturn `Since ${formatDateTime(from)}`;\n\t}\n\n\treturn \"All time\";\n}\n\n/**\n * Formats a date with locale support.\n *\n * @param date - The date to format\n * @param options - Intl.DateTimeFormatOptions to customize the output\n * @param locale - Locale string for formatting (default: \"en-US\")\n * @returns A formatted date string\n */\nexport function formatDate(\n\tdate: Date,\n\toptions?: Intl.DateTimeFormatOptions,\n\tlocale = \"en-US\",\n) {\n\treturn date.toLocaleDateString(locale, {\n\t\tyear: \"numeric\",\n\t\tmonth: \"long\",\n\t\tday: \"numeric\",\n\t\t...options,\n\t});\n}\n\n/**\n * Formats a date with time, with locale support.\n *\n * @param date - The date to format\n * @param locale - Locale string for formatting (default: \"en-US\")\n * @returns A formatted date and time string\n */\nexport function formatDateTime(date: Date, locale = \"en-US\") {\n\treturn date.toLocaleDateString(locale, {\n\t\tmonth: \"long\",\n\t\tday: \"numeric\",\n\t\thour: \"numeric\",\n\t\tminute: \"numeric\",\n\t});\n}\n\n/**\n * Formats a time with locale support.\n *\n * @param date - The date to format\n * @param locale - Locale string for formatting (default: \"en-US\")\n * @returns A formatted time string\n */\nexport function formatTime(date: Date, locale = \"en-US\") {\n\treturn date.toLocaleTimeString(locale, {\n\t\thour: \"numeric\",\n\t\tminute: \"numeric\",\n\t});\n}\n\nimport type {\n\tStatusReportUpdateType,\n\tStatusType,\n} from \"@openstatus/ui/components/blocks/status.types\";\n\n/**\n * System status display messages\n * Used for displaying status banner and component statuses\n */\nexport const systemStatusLabels: Record<\n\tStatusType,\n\t{ long: string; short: string }\n> = {\n\tsuccess: {\n\t\tlong: \"All Systems Operational\",\n\t\tshort: \"Operational\",\n\t},\n\tdegraded: {\n\t\tlong: \"Degraded Performance\",\n\t\tshort: \"Degraded\",\n\t},\n\terror: {\n\t\tlong: \"Partial Outage\",\n\t\tshort: \"Outage\",\n\t},\n\tinfo: {\n\t\tlong: \"Maintenance\",\n\t\tshort: \"Maintenance\",\n\t},\n\tempty: {\n\t\tlong: \"No Data\",\n\t\tshort: \"No Data\",\n\t},\n} as const;\n\n/**\n * Legacy messages object for backwards compatibility\n * @deprecated Use systemStatusLabels instead\n */\nexport const messages = {\n\tlong: {\n\t\tsuccess: systemStatusLabels.success.long,\n\t\tdegraded: systemStatusLabels.degraded.long,\n\t\terror: systemStatusLabels.error.long,\n\t\tinfo: systemStatusLabels.info.long,\n\t\tempty: systemStatusLabels.empty.long,\n\t},\n\tshort: {\n\t\tsuccess: systemStatusLabels.success.short,\n\t\tdegraded: systemStatusLabels.degraded.short,\n\t\terror: systemStatusLabels.error.short,\n\t\tinfo: systemStatusLabels.info.short,\n\t\tempty: systemStatusLabels.empty.short,\n\t},\n} as const;\n\n/**\n * Request status labels\n * Used for displaying individual request statuses\n */\nexport const requestStatusLabels: Record<StatusType, string> = {\n\tsuccess: \"Normal\",\n\tdegraded: \"Degraded\",\n\terror: \"Error\",\n\tinfo: \"Maintenance\",\n\tempty: \"No Data\",\n} as const;\n\n/**\n * Legacy requests object for backwards compatibility\n * @deprecated Use requestStatusLabels instead\n */\nexport const requests = requestStatusLabels;\n\n/**\n * Incident status labels\n * Used for displaying incident report update statuses\n */\nexport const incidentStatusLabels: Record<StatusReportUpdateType, string> = {\n\tresolved: \"Resolved\",\n\tmonitoring: \"Monitoring\",\n\tidentified: \"Identified\",\n\tinvestigating: \"Investigating\",\n} as const;\n\n/**\n * Legacy status object for backwards compatibility\n * @deprecated Use incidentStatusLabels instead\n */\nexport const status = incidentStatusLabels;\n\n/**\n * CSS variable mappings for status colors\n * Maps StatusType to corresponding CSS custom properties\n */\nexport const statusColors: Record<StatusType, string> = {\n\tsuccess: \"var(--success)\",\n\tdegraded: \"var(--warning)\",\n\terror: \"var(--destructive)\",\n\tinfo: \"var(--info)\",\n\tempty: \"var(--muted)\",\n} as const;\n\n/**\n * Legacy colors object for backwards compatibility\n * @deprecated Use statusColors instead\n */\nexport const colors = statusColors;\n"
  },
  {
    "path": "packages/ui/src/components/ui/alert-dialog.tsx",
    "content": "\"use client\";\n\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\nimport type * as React from \"react\";\n\nimport { buttonVariants } from \"@openstatus/ui/components/ui/button\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction AlertDialog({\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {\n  return <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" {...props} />;\n}\n\nfunction AlertDialogTrigger({\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {\n  return (\n    <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />\n  );\n}\n\nfunction AlertDialogPortal({\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {\n  return (\n    <AlertDialogPrimitive.Portal data-slot=\"alert-dialog-portal\" {...props} />\n  );\n}\n\nfunction AlertDialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {\n  return (\n    <AlertDialogPrimitive.Overlay\n      data-slot=\"alert-dialog-overlay\"\n      className={cn(\n        \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDialogContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {\n  return (\n    <AlertDialogPortal>\n      <AlertDialogOverlay />\n      <AlertDialogPrimitive.Content\n        data-slot=\"alert-dialog-content\"\n        className={cn(\n          \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg\",\n          className,\n        )}\n        {...props}\n      />\n    </AlertDialogPortal>\n  );\n}\n\nfunction AlertDialogHeader({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-dialog-header\"\n      className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDialogFooter({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {\n  return (\n    <AlertDialogPrimitive.Title\n      data-slot=\"alert-dialog-title\"\n      className={cn(\"font-semibold text-lg\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {\n  return (\n    <AlertDialogPrimitive.Description\n      data-slot=\"alert-dialog-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDialogAction({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {\n  return (\n    <AlertDialogPrimitive.Action\n      className={cn(buttonVariants(), className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDialogCancel({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {\n  return (\n    <AlertDialogPrimitive.Cancel\n      className={cn(buttonVariants({ variant: \"outline\" }), className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/alert.tsx",
    "content": "import { type VariantProps, cva } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-card text-card-foreground\",\n        destructive:\n          \"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Alert({\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n  return (\n    <div\n      data-slot=\"alert\"\n      role=\"alert\"\n      className={cn(alertVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-title\"\n      className={cn(\n        \"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-description\"\n      className={cn(\n        \"col-start-2 grid justify-items-start gap-1 text-muted-foreground text-sm [&_p]:leading-relaxed\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "packages/ui/src/components/ui/avatar.tsx",
    "content": "\"use client\";\n\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Avatar({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      className={cn(\n        \"relative flex size-8 shrink-0 overflow-hidden rounded-full\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarImage({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      className={cn(\"aspect-square size-full\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarFallback({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      className={cn(\n        \"flex size-full items-center justify-center rounded-full bg-muted\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
  },
  {
    "path": "packages/ui/src/components/ui/badge.tsx",
    "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive:\n          \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport type BadgeProps = React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean };\n\nfunction Badge({ className, variant, asChild = false, ...props }: BadgeProps) {\n  const Comp = asChild ? Slot : \"span\";\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "packages/ui/src/components/ui/breadcrumb.tsx",
    "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Breadcrumb({ ...props }: React.ComponentProps<\"nav\">) {\n  return <nav aria-label=\"breadcrumb\" data-slot=\"breadcrumb\" {...props} />;\n}\n\nfunction BreadcrumbList({ className, ...props }: React.ComponentProps<\"ol\">) {\n  return (\n    <ol\n      data-slot=\"breadcrumb-list\"\n      className={cn(\n        \"flex flex-wrap items-center gap-1.5 break-words text-muted-foreground text-sm sm:gap-2.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction BreadcrumbItem({ className, ...props }: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"breadcrumb-item\"\n      className={cn(\"inline-flex items-center gap-1.5\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction BreadcrumbLink({\n  asChild,\n  className,\n  ...props\n}: React.ComponentProps<\"a\"> & {\n  asChild?: boolean;\n}) {\n  const Comp = asChild ? Slot : \"a\";\n\n  return (\n    <Comp\n      data-slot=\"breadcrumb-link\"\n      className={cn(\"transition-colors hover:text-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction BreadcrumbPage({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"breadcrumb-page\"\n      role=\"link\"\n      aria-disabled=\"true\"\n      aria-current=\"page\"\n      className={cn(\"font-normal text-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction BreadcrumbSeparator({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"breadcrumb-separator\"\n      role=\"presentation\"\n      aria-hidden=\"true\"\n      className={cn(\"[&>svg]:size-3.5\", className)}\n      {...props}\n    >\n      {children ?? <ChevronRight />}\n    </li>\n  );\n}\n\nfunction BreadcrumbEllipsis({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"breadcrumb-ellipsis\"\n      role=\"presentation\"\n      aria-hidden=\"true\"\n      className={cn(\"flex size-9 items-center justify-center\", className)}\n      {...props}\n    >\n      <MoreHorizontal className=\"size-4\" />\n      <span className=\"sr-only\">More</span>\n    </span>\n  );\n}\n\nexport {\n  Breadcrumb,\n  BreadcrumbList,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n  BreadcrumbEllipsis,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/button-group.tsx",
    "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\n\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nconst buttonGroupVariants = cva(\n  \"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2\",\n  {\n    variants: {\n      orientation: {\n        horizontal:\n          \"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none\",\n        vertical:\n          \"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none\",\n      },\n    },\n    defaultVariants: {\n      orientation: \"horizontal\",\n    },\n  },\n);\n\nfunction ButtonGroup({\n  className,\n  orientation,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof buttonGroupVariants>) {\n  return (\n    <div\n      role=\"group\"\n      data-slot=\"button-group\"\n      data-orientation={orientation}\n      className={cn(buttonGroupVariants({ orientation }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction ButtonGroupText({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  asChild?: boolean;\n}) {\n  const Comp = asChild ? Slot : \"div\";\n\n  return (\n    <Comp\n      className={cn(\n        \"flex items-center gap-2 rounded-md border bg-muted px-4 font-medium text-sm shadow-xs [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ButtonGroupSeparator({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      data-slot=\"button-group-separator\"\n      orientation={orientation}\n      className={cn(\n        \"!m-0 relative self-stretch bg-input data-[orientation=vertical]:h-auto\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  ButtonGroup,\n  ButtonGroupSeparator,\n  ButtonGroupText,\n  buttonGroupVariants,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/button.tsx",
    "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "packages/ui/src/components/ui/calendar.tsx",
    "content": "\"use client\";\n\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { DayPicker } from \"react-day-picker\";\n\nimport { buttonVariants } from \"@openstatus/ui/components/ui/button\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Calendar({\n  className,\n  classNames,\n  showOutsideDays = true,\n  ...props\n}: React.ComponentProps<typeof DayPicker>) {\n  return (\n    <DayPicker\n      showOutsideDays={showOutsideDays}\n      className={cn(\"p-3\", className)}\n      classNames={{\n        months: \"flex flex-col sm:flex-row gap-2\",\n        month: \"flex flex-col gap-4\",\n        caption: \"flex justify-center pt-1 relative items-center w-full\",\n        caption_label: \"text-sm font-medium\",\n        nav: \"flex items-center gap-1\",\n        nav_button: cn(\n          buttonVariants({ variant: \"outline\" }),\n          \"size-7 bg-transparent p-0 opacity-50 hover:opacity-100\",\n        ),\n        nav_button_previous: \"absolute left-1\",\n        nav_button_next: \"absolute right-1\",\n        table: \"w-full border-collapse space-x-1\",\n        head_row: \"flex\",\n        head_cell:\n          \"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]\",\n        row: \"flex w-full mt-2\",\n        cell: cn(\n          \"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md\",\n          props.mode === \"range\"\n            ? \"[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md\"\n            : \"[&:has([aria-selected])]:rounded-md\",\n        ),\n        day: cn(\n          buttonVariants({ variant: \"ghost\" }),\n          \"size-8 p-0 font-normal aria-selected:opacity-100\",\n        ),\n        day_range_start:\n          \"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground\",\n        day_range_end:\n          \"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground\",\n        day_selected:\n          \"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground\",\n        day_today: \"bg-accent text-accent-foreground\",\n        day_outside:\n          \"day-outside text-muted-foreground aria-selected:text-muted-foreground\",\n        day_disabled: \"text-muted-foreground opacity-50\",\n        day_range_middle:\n          \"aria-selected:bg-accent aria-selected:text-accent-foreground\",\n        day_hidden: \"invisible\",\n        ...classNames,\n      }}\n      components={{\n        IconLeft: ({ className, ...props }) => (\n          <ChevronLeft className={cn(\"size-4\", className)} {...props} />\n        ),\n        IconRight: ({ className, ...props }) => (\n          <ChevronRight className={cn(\"size-4\", className)} {...props} />\n        ),\n      }}\n      {...props}\n    />\n  );\n}\n\nexport { Calendar };\n"
  },
  {
    "path": "packages/ui/src/components/ui/card.tsx",
    "content": "import type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"font-semibold leading-none\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/chart.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const;\n\nexport type ChartConfig = {\n  [k in string]: {\n    label?: React.ReactNode;\n    icon?: React.ComponentType;\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, string> }\n  );\n};\n\ntype ChartContextProps = {\n  config: ChartConfig;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nexport function useChart() {\n  const context = React.useContext(ChartContext);\n\n  if (!context) {\n    throw new Error(\"useChart must be used within a <ChartContainer />\");\n  }\n\n  return context;\n}\n\nfunction ChartContainer({\n  id,\n  className,\n  children,\n  config,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  config: ChartConfig;\n  children: React.ComponentProps<\n    typeof RechartsPrimitive.ResponsiveContainer\n  >[\"children\"];\n}) {\n  const uniqueId = React.useId();\n  const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn(\n          \"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden\",\n          className,\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer>\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  );\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(\n    ([, config]) => config.theme || config.color,\n  );\n\n  if (!colorConfig.length) {\n    return null;\n  }\n\n  return (\n    <style\n      // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n      dangerouslySetInnerHTML={{\n        __html: Object.entries(THEMES)\n          .map(\n            ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n  .map(([key, itemConfig]) => {\n    const color =\n      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n      itemConfig.color;\n    return color ? `  --color-${key}: ${color};` : null;\n  })\n  .join(\"\\n\")}\n}\n`,\n          )\n          .join(\"\\n\"),\n      }}\n    />\n  );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\nfunction ChartTooltipContent({\n  active,\n  payload,\n  className,\n  indicator = \"dot\",\n  hideLabel = false,\n  hideIndicator = false,\n  label,\n  labelFormatter,\n  labelClassName,\n  formatter,\n  color,\n  nameKey,\n  labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n  React.ComponentProps<\"div\"> & {\n    hideLabel?: boolean;\n    hideIndicator?: boolean;\n    indicator?: \"line\" | \"dot\" | \"dashed\";\n    nameKey?: string;\n    labelKey?: string;\n  }) {\n  const { config } = useChart();\n\n  const tooltipLabel = React.useMemo(() => {\n    if (hideLabel || !payload?.length) {\n      return null;\n    }\n\n    const [item] = payload;\n    const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n    const itemConfig = getPayloadConfigFromPayload(config, item, key);\n    const value =\n      !labelKey && typeof label === \"string\"\n        ? config[label as keyof typeof config]?.label || label\n        : itemConfig?.label;\n\n    if (labelFormatter) {\n      return (\n        <div className={cn(\"font-medium\", labelClassName)}>\n          {labelFormatter(value, payload)}\n        </div>\n      );\n    }\n\n    if (!value) {\n      return null;\n    }\n\n    return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>;\n  }, [\n    label,\n    labelFormatter,\n    payload,\n    hideLabel,\n    labelClassName,\n    config,\n    labelKey,\n  ]);\n\n  if (!active || !payload?.length) {\n    return null;\n  }\n\n  const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n  return (\n    <div\n      className={cn(\n        \"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl\",\n        className,\n      )}\n    >\n      {!nestLabel ? tooltipLabel : null}\n      <div className=\"grid gap-1.5\">\n        {payload.map((item, index) => {\n          const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n          const itemConfig = getPayloadConfigFromPayload(config, item, key);\n          const indicatorColor = color || item.payload.fill || item.color;\n\n          return (\n            <div\n              key={item.dataKey}\n              className={cn(\n                \"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\",\n                indicator === \"dot\" && \"items-center\",\n              )}\n            >\n              {formatter && item?.value !== undefined && item.name ? (\n                formatter(item.value, item.name, item, index, item.payload)\n              ) : (\n                <>\n                  {itemConfig?.icon ? (\n                    <itemConfig.icon />\n                  ) : (\n                    !hideIndicator && (\n                      <div\n                        className={cn(\n                          \"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)\",\n                          {\n                            \"h-2.5 w-2.5\": indicator === \"dot\",\n                            \"w-1\": indicator === \"line\",\n                            \"w-0 border-[1.5px] border-dashed bg-transparent\":\n                              indicator === \"dashed\",\n                            \"my-0.5\": nestLabel && indicator === \"dashed\",\n                          },\n                        )}\n                        style={\n                          {\n                            \"--color-bg\": indicatorColor,\n                            \"--color-border\": indicatorColor,\n                          } as React.CSSProperties\n                        }\n                      />\n                    )\n                  )}\n                  <div\n                    className={cn(\n                      \"flex flex-1 justify-between leading-none\",\n                      nestLabel ? \"items-end\" : \"items-center\",\n                    )}\n                  >\n                    <div className=\"grid gap-1.5\">\n                      {nestLabel ? tooltipLabel : null}\n                      <span className=\"text-muted-foreground\">\n                        {itemConfig?.label || item.name}\n                      </span>\n                    </div>\n                    {item.value && (\n                      <span className=\"font-medium font-mono text-foreground tabular-nums\">\n                        {item.value.toLocaleString()}\n                      </span>\n                    )}\n                  </div>\n                </>\n              )}\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = \"bottom\",\n  nameKey,\n}: React.ComponentProps<\"div\"> &\n  Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n    hideIcon?: boolean;\n    nameKey?: string;\n  }) {\n  const { config } = useChart();\n\n  if (!payload?.length) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center justify-center gap-4\",\n        verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n        className,\n      )}\n    >\n      {payload.map((item) => {\n        const key = `${nameKey || item.dataKey || \"value\"}`;\n        const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n        return (\n          <div\n            key={item.value}\n            className={cn(\n              \"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground\",\n            )}\n          >\n            {itemConfig?.icon && !hideIcon ? (\n              <itemConfig.icon />\n            ) : (\n              <div\n                className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n                style={{\n                  backgroundColor: item.color,\n                }}\n              />\n            )}\n            {itemConfig?.label}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string,\n) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined;\n  }\n\n  const payloadPayload =\n    \"payload\" in payload &&\n    typeof payload.payload === \"object\" &&\n    payload.payload !== null\n      ? payload.payload\n      : undefined;\n\n  let configLabelKey: string = key;\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === \"string\"\n  ) {\n    configLabelKey = payload[key as keyof typeof payload] as string;\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[\n      key as keyof typeof payloadPayload\n    ] as string;\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config];\n}\n\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/checkbox.tsx",
    "content": "\"use client\";\n\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { CheckIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Checkbox({\n  className,\n  ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n  return (\n    <CheckboxPrimitive.Root\n      data-slot=\"checkbox\"\n      className={cn(\n        \"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:data-[state=checked]:bg-primary dark:aria-invalid:ring-destructive/40\",\n        className,\n      )}\n      {...props}\n    >\n      <CheckboxPrimitive.Indicator\n        data-slot=\"checkbox-indicator\"\n        className=\"flex items-center justify-center text-current transition-none\"\n      >\n        <CheckIcon className=\"size-3.5\" />\n      </CheckboxPrimitive.Indicator>\n    </CheckboxPrimitive.Root>\n  );\n}\n\nexport { Checkbox };\n"
  },
  {
    "path": "packages/ui/src/components/ui/collapsible.tsx",
    "content": "\"use client\";\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\";\n\nfunction Collapsible({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {\n  return <CollapsiblePrimitive.Root data-slot=\"collapsible\" {...props} />;\n}\n\nfunction CollapsibleTrigger({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleTrigger\n      data-slot=\"collapsible-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction CollapsibleContent({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleContent\n      data-slot=\"collapsible-content\"\n      {...props}\n    />\n  );\n}\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n"
  },
  {
    "path": "packages/ui/src/components/ui/command.tsx",
    "content": "\"use client\";\n\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { SearchIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@openstatus/ui/components/ui/dialog\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Command({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive>) {\n  return (\n    <CommandPrimitive\n      data-slot=\"command\"\n      className={cn(\n        \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CommandDialog({\n  title = \"Command Palette\",\n  description = \"Search for a command to run...\",\n  children,\n  ...props\n}: React.ComponentProps<typeof Dialog> & {\n  title?: string;\n  description?: string;\n}) {\n  return (\n    <Dialog {...props}>\n      <DialogHeader className=\"sr-only\">\n        <DialogTitle>{title}</DialogTitle>\n        <DialogDescription>{description}</DialogDescription>\n      </DialogHeader>\n      <DialogContent className=\"overflow-hidden p-0\">\n        <Command className=\"**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nfunction CommandInput({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input>) {\n  return (\n    <div\n      data-slot=\"command-input-wrapper\"\n      className=\"flex h-9 items-center gap-2 border-b px-3\"\n    >\n      <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n      <CommandPrimitive.Input\n        data-slot=\"command-input\"\n        className={cn(\n          \"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction CommandList({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.List>) {\n  return (\n    <CommandPrimitive.List\n      data-slot=\"command-list\"\n      className={cn(\n        \"max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CommandEmpty({\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n  return (\n    <CommandPrimitive.Empty\n      data-slot=\"command-empty\"\n      className=\"py-6 text-center text-sm\"\n      {...props}\n    />\n  );\n}\n\nfunction CommandGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Group>) {\n  return (\n    <CommandPrimitive.Group\n      data-slot=\"command-group\"\n      className={cn(\n        \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:text-xs\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CommandSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n  return (\n    <CommandPrimitive.Separator\n      data-slot=\"command-separator\"\n      className={cn(\"-mx-1 h-px bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CommandItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Item>) {\n  return (\n    <CommandPrimitive.Item\n      data-slot=\"command-item\"\n      className={cn(\n        \"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CommandShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"command-shortcut\"\n      className={cn(\n        \"ml-auto text-muted-foreground text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\nCommandShortcut.displayName = \"CommandShortcut\";\n\nfunction CommandLoading({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Loading>) {\n  return (\n    <CommandPrimitive.Loading\n      data-slot=\"command-loading\"\n      className={cn(\"px-2 py-1.5 text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandLoading,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/context-menu.tsx",
    "content": "\"use client\";\n\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\";\nimport { Check, ChevronRight, Circle } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nconst ContextMenu = ContextMenuPrimitive.Root;\n\nconst ContextMenuTrigger = ContextMenuPrimitive.Trigger;\n\nconst ContextMenuGroup = ContextMenuPrimitive.Group;\n\nconst ContextMenuPortal = ContextMenuPrimitive.Portal;\n\nconst ContextMenuSub = ContextMenuPrimitive.Sub;\n\nconst ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;\n\nconst ContextMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <ContextMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </ContextMenuPrimitive.SubTrigger>\n));\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;\n\nconst ContextMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;\n\nconst ContextMenuContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Portal>\n    <ContextMenuPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"bg-popover text-popover-foreground animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md\",\n        className,\n      )}\n      {...props}\n    />\n  </ContextMenuPrimitive.Portal>\n));\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;\n\nconst ContextMenuItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <ContextMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50\",\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.CheckboxItem>\n));\nContextMenuCheckboxItem.displayName =\n  ContextMenuPrimitive.CheckboxItem.displayName;\n\nconst ContextMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <ContextMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.RadioItem>\n));\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;\n\nconst ContextMenuLabel = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"text-foreground px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;\n\nconst ContextMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n    {...props}\n  />\n));\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;\n\nconst ContextMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n};\nContextMenuShortcut.displayName = \"ContextMenuShortcut\";\n\nexport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioItem,\n  ContextMenuLabel,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuGroup,\n  ContextMenuPortal,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuRadioGroup,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Dialog({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n  return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n  return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(\n        \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content>) {\n  return (\n    <DialogPortal data-slot=\"dialog-portal\">\n      <DialogOverlay />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <DialogPrimitive.Close className=\"absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\">\n          <XIcon />\n          <span className=\"sr-only\">Close</span>\n        </DialogPrimitive.Close>\n      </DialogPrimitive.Content>\n    </DialogPortal>\n  );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn(\"font-semibold text-lg leading-none\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"dialog-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  );\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in\",\n          className,\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"data-[variant=destructive]:*:[svg]:!text-destructive relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[disabled]:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 font-medium text-sm data-[inset]:pl-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"ml-auto text-muted-foreground text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[inset]:pl-8 data-[state=open]:text-accent-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/form.tsx",
    "content": "\"use client\";\n\nimport type * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport {\n  Controller,\n  type ControllerProps,\n  type FieldPath,\n  type FieldValues,\n  FormProvider,\n  useFormContext,\n  useFormState,\n} from \"react-hook-form\";\n\nimport { Label } from \"@openstatus/ui/components/ui/label\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nconst Form = FormProvider;\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue,\n);\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState } = useFormContext();\n  const formState = useFormState({ name: fieldContext.name });\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error(\"useFormField should be used within <FormField>\");\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n  {} as FormItemContextValue,\n);\n\nfunction FormItem({ className, ...props }: React.ComponentProps<\"div\">) {\n  const id = React.useId();\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div\n        data-slot=\"form-item\"\n        className={cn(\"grid gap-2\", className)}\n        {...props}\n      />\n    </FormItemContext.Provider>\n  );\n}\n\nfunction FormLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  const { error, formItemId } = useFormField();\n\n  return (\n    <Label\n      data-slot=\"form-label\"\n      data-error={!!error}\n      className={cn(\"data-[error=true]:text-destructive\", className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  );\n}\n\nfunction FormControl({ ...props }: React.ComponentProps<typeof Slot>) {\n  const { error, formItemId, formDescriptionId, formMessageId } =\n    useFormField();\n\n  return (\n    <Slot\n      data-slot=\"form-control\"\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  );\n}\n\nfunction FormDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  const { formDescriptionId } = useFormField();\n\n  return (\n    <p\n      data-slot=\"form-description\"\n      id={formDescriptionId}\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction FormMessage({ className, ...props }: React.ComponentProps<\"p\">) {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error?.message ?? \"\") : props.children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <p\n      data-slot=\"form-message\"\n      id={formMessageId}\n      className={cn(\"text-destructive text-sm\", className)}\n      {...props}\n    >\n      {body}\n    </p>\n  );\n}\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/hover-card.tsx",
    "content": "\"use client\";\n\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction HoverCard({\n  ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {\n  return <HoverCardPrimitive.Root data-slot=\"hover-card\" {...props} />;\n}\n\nfunction HoverCardTrigger({\n  ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {\n  return (\n    <HoverCardPrimitive.Trigger data-slot=\"hover-card-trigger\" {...props} />\n  );\n}\n\nfunction HoverCardContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {\n  return (\n    <HoverCardPrimitive.Portal data-slot=\"hover-card-portal\">\n      <HoverCardPrimitive.Content\n        data-slot=\"hover-card-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in\",\n          className,\n        )}\n        {...props}\n      />\n    </HoverCardPrimitive.Portal>\n  );\n}\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent };\n"
  },
  {
    "path": "packages/ui/src/components/ui/input-group.tsx",
    "content": "\"use client\";\n\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Textarea } from \"@openstatus/ui/components/ui/textarea\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction InputGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"input-group\"\n      role=\"group\"\n      className={cn(\n        \"group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30\",\n        \"h-9 min-w-0 has-[>textarea]:h-auto\",\n\n        // Variants based on alignment.\n        \"has-[>[data-align=inline-start]]:[&>input]:pl-2\",\n        \"has-[>[data-align=inline-end]]:[&>input]:pr-2\",\n        \"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3\",\n        \"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3\",\n\n        // Focus state.\n        \"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50\",\n\n        // Error state.\n        \"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40\",\n\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst inputGroupAddonVariants = cva(\n  \"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50\",\n  {\n    variants: {\n      align: {\n        \"inline-start\":\n          \"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]\",\n        \"inline-end\":\n          \"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]\",\n        \"block-start\":\n          \"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5\",\n        \"block-end\":\n          \"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5\",\n      },\n    },\n    defaultVariants: {\n      align: \"inline-start\",\n    },\n  },\n);\n\nfunction InputGroupAddon({\n  className,\n  align = \"inline-start\",\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof inputGroupAddonVariants>) {\n  return (\n    <div\n      role=\"group\"\n      data-slot=\"input-group-addon\"\n      data-align={align}\n      className={cn(inputGroupAddonVariants({ align }), className)}\n      onClick={(e) => {\n        if ((e.target as HTMLElement).closest(\"button\")) {\n          return;\n        }\n        e.currentTarget.parentElement?.querySelector(\"input\")?.focus();\n      }}\n      {...props}\n    />\n  );\n}\n\nconst inputGroupButtonVariants = cva(\n  \"text-sm shadow-none flex gap-2 items-center\",\n  {\n    variants: {\n      size: {\n        xs: \"h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2\",\n        sm: \"h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5\",\n        \"icon-xs\":\n          \"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0\",\n        \"icon-sm\": \"size-8 p-0 has-[>svg]:p-0\",\n      },\n    },\n    defaultVariants: {\n      size: \"xs\",\n    },\n  },\n);\n\nfunction InputGroupButton({\n  className,\n  type = \"button\",\n  variant = \"ghost\",\n  size = \"xs\",\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, \"size\"> &\n  VariantProps<typeof inputGroupButtonVariants>) {\n  return (\n    <Button\n      type={type}\n      data-size={size}\n      variant={variant}\n      className={cn(inputGroupButtonVariants({ size }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction InputGroupText({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      className={cn(\n        \"flex items-center gap-2 text-muted-foreground text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction InputGroupInput({\n  className,\n  ...props\n}: React.ComponentProps<\"input\">) {\n  return (\n    <Input\n      data-slot=\"input-group-control\"\n      className={cn(\n        \"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction InputGroupTextarea({\n  className,\n  ...props\n}: React.ComponentProps<\"textarea\">) {\n  return (\n    <Textarea\n      data-slot=\"input-group-control\"\n      className={cn(\n        \"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupText,\n  InputGroupInput,\n  InputGroupTextarea,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/input.tsx",
    "content": "import type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n        \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n        \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Input };\n"
  },
  {
    "path": "packages/ui/src/components/ui/kbd.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\"\n\nfunction Kbd({ className, ...props }: React.ComponentProps<\"kbd\">) {\n  return (\n    <kbd\n      data-slot=\"kbd\"\n      className={cn(\n        \"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none\",\n        \"[&_svg:not([class*='size-'])]:size-3\",\n        \"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction KbdGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <kbd\n      data-slot=\"kbd-group\"\n      className={cn(\"inline-flex items-center gap-1\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Kbd, KbdGroup }\n"
  },
  {
    "path": "packages/ui/src/components/ui/label.tsx",
    "content": "\"use client\";\n\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Label({\n  className,\n  ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"label\"\n      className={cn(\n        \"flex select-none items-center gap-2 font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Label };\n"
  },
  {
    "path": "packages/ui/src/components/ui/popover.tsx",
    "content": "\"use client\";\n\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Popover({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n  return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />;\n}\n\nfunction PopoverTrigger({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n  return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />;\n}\n\nfunction PopoverContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        data-slot=\"popover-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in\",\n          className,\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  );\n}\n\nfunction PopoverAnchor({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n  return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />;\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };\n"
  },
  {
    "path": "packages/ui/src/components/ui/progress.tsx",
    "content": "\"use client\";\n\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Progress({\n  className,\n  value,\n  ...props\n}: React.ComponentProps<typeof ProgressPrimitive.Root>) {\n  return (\n    <ProgressPrimitive.Root\n      data-slot=\"progress\"\n      className={cn(\n        \"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\",\n        className,\n      )}\n      {...props}\n    >\n      <ProgressPrimitive.Indicator\n        data-slot=\"progress-indicator\"\n        className=\"h-full w-full flex-1 bg-primary transition-all\"\n        style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n      />\n    </ProgressPrimitive.Root>\n  );\n}\n\nexport { Progress };\n"
  },
  {
    "path": "packages/ui/src/components/ui/qr-code.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport QRCodeStyling, {\n  type Options as QRCodeOptions,\n} from \"qr-code-styling\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nexport interface QRCodeProps {\n  data: string;\n  image?: string;\n  size?: number;\n  className?: string;\n  options?: Partial<QRCodeOptions>;\n}\n\nexport const QRCode = ({\n  data,\n  image,\n  size = 200,\n  className,\n  options,\n}: QRCodeProps) => {\n  const [qrCode] = useState<QRCodeStyling>(\n    new QRCodeStyling({\n      width: size,\n      height: size,\n      type: \"svg\",\n      data,\n      image,\n      dotsOptions: {\n        color: \"#000000\",\n        type: \"rounded\",\n      },\n      backgroundOptions: {\n        color: \"#ffffff\",\n      },\n      imageOptions: {\n        crossOrigin: \"anonymous\",\n        margin: 20,\n      },\n      ...options,\n    }),\n  );\n  const ref = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (ref.current) {\n      qrCode.append(ref.current);\n    }\n  }, [qrCode, ref]);\n\n  useEffect(() => {\n    if (!qrCode) return;\n    qrCode.update({\n      data,\n      image,\n      width: size,\n      height: size,\n      ...options,\n    });\n  }, [qrCode, data, image, size, options]);\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\"flex items-center justify-center\", className)}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/radio-group.tsx",
    "content": "\"use client\";\n\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { CircleIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction RadioGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n  return (\n    <RadioGroupPrimitive.Root\n      data-slot=\"radio-group\"\n      className={cn(\"grid gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction RadioGroupItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n  return (\n    <RadioGroupPrimitive.Item\n      data-slot=\"radio-group-item\"\n      className={cn(\n        \"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator\n        data-slot=\"radio-group-indicator\"\n        className=\"relative flex items-center justify-center\"\n      >\n        <CircleIcon className=\"-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 size-2 fill-primary\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "packages/ui/src/components/ui/select.tsx",
    "content": "\"use client\";\n\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Select({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n  return <SelectPrimitive.Root data-slot=\"select\" {...props} />;\n}\n\nfunction SelectGroup({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n  return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />;\n}\n\nfunction SelectValue({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n  return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />;\n}\n\nfunction SelectTrigger({\n  className,\n  size = \"default\",\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n  size?: \"sm\" | \"default\";\n}) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      className={cn(\n        \"flex w-fit items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[size=default]:h-9 data-[size=sm]:h-8 data-[placeholder]:text-muted-foreground *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <ChevronDownIcon className=\"size-4 opacity-50\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nfunction SelectContent({\n  className,\n  children,\n  position = \"popper\",\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Content\n        data-slot=\"select-content\"\n        className={cn(\n          \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in\",\n          position === \"popper\" &&\n            \"data-[side=left]:-translate-x-1 data-[side=top]:-translate-y-1 data-[side=right]:translate-x-1 data-[side=bottom]:translate-y-1\",\n          className,\n        )}\n        position={position}\n        {...props}\n      >\n        <SelectScrollUpButton />\n        <SelectPrimitive.Viewport\n          className={cn(\n            \"p-1\",\n            position === \"popper\" &&\n              \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\",\n          )}\n        >\n          {children}\n        </SelectPrimitive.Viewport>\n        <SelectScrollDownButton />\n      </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n  );\n}\n\nfunction SelectLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n  return (\n    <SelectPrimitive.Label\n      data-slot=\"select-label\"\n      className={cn(\"px-2 py-1.5 text-muted-foreground text-xs\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      className={cn(\n        \"relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n        <SelectPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n  );\n}\n\nfunction SelectSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"-mx-1 pointer-events-none my-1 h-px bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectScrollUpButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n  return (\n    <SelectPrimitive.ScrollUpButton\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronUpIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollUpButton>\n  );\n}\n\nfunction SelectScrollDownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n  return (\n    <SelectPrimitive.ScrollDownButton\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollDownButton>\n  );\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "packages/ui/src/components/ui/sheet.tsx",
    "content": "\"use client\";\n\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n  return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n  return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n  return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nfunction SheetPortal({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n  return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />;\n}\n\nfunction SheetOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n  return (\n    <SheetPrimitive.Overlay\n      data-slot=\"sheet-overlay\"\n      className={cn(\n        \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SheetContent({\n  className,\n  children,\n  side = \"right\",\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n}) {\n  return (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content\n        data-slot=\"sheet-content\"\n        className={cn(\n          \"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n          side === \"right\" &&\n            \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n          side === \"left\" &&\n            \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n          side === \"top\" &&\n            \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n          side === \"bottom\" &&\n            \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <SheetPrimitive.Close className=\"absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary\">\n          <XIcon className=\"size-4\" />\n          <span className=\"sr-only\">Close</span>\n        </SheetPrimitive.Close>\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  );\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-header\"\n      className={cn(\"flex flex-col gap-1.5 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Title>) {\n  return (\n    <SheetPrimitive.Title\n      data-slot=\"sheet-title\"\n      className={cn(\"font-semibold text-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n  return (\n    <SheetPrimitive.Description\n      data-slot=\"sheet-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Sheet,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/sidebar.tsx",
    "content": "\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport { PanelLeftIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Button } from \"@openstatus/ui/components/ui/button\";\nimport { Input } from \"@openstatus/ui/components/ui/input\";\nimport { Separator } from \"@openstatus/ui/components/ui/separator\";\nimport {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetHeader,\n  SheetTitle,\n} from \"@openstatus/ui/components/ui/sheet\";\nimport { Skeleton } from \"@openstatus/ui/components/ui/skeleton\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@openstatus/ui/components/ui/tooltip\";\nimport { useIsMobile } from \"@openstatus/ui/hooks/use-mobile\";\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\";\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;\nconst SIDEBAR_WIDTH = \"16rem\";\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\";\nconst SIDEBAR_WIDTH_ICON = \"3rem\";\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\n\ntype SidebarContextProps = {\n  state: \"expanded\" | \"collapsed\";\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  openMobile: boolean;\n  setOpenMobile: (open: boolean) => void;\n  isMobile: boolean;\n  toggleSidebar: () => void;\n};\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null);\n\nfunction useSidebar() {\n  const context = React.useContext(SidebarContext);\n  if (!context) {\n    throw new Error(\"useSidebar must be used within a SidebarProvider.\");\n  }\n\n  return context;\n}\n\nfunction SidebarProvider({\n  defaultOpen = true,\n  open: openProp,\n  onOpenChange: setOpenProp,\n  className,\n  style,\n  children,\n  cookieName,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  defaultOpen?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  // NOTE: change from default shadcn sidebar\n  cookieName?: string;\n}) {\n  const isMobile = useIsMobile();\n  const [openMobile, setOpenMobile] = React.useState(false);\n\n  // This is the internal state of the sidebar.\n  // We use openProp and setOpenProp for control from outside the component.\n  const [_open, _setOpen] = React.useState(defaultOpen);\n  const open = openProp ?? _open;\n  const setOpen = React.useCallback(\n    (value: boolean | ((value: boolean) => boolean)) => {\n      const openState = typeof value === \"function\" ? value(open) : value;\n      if (setOpenProp) {\n        setOpenProp(openState);\n      } else {\n        _setOpen(openState);\n      }\n\n      // This sets the cookie to keep the sidebar state.\n      document.cookie = `${cookieName ?? SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;\n    },\n    [setOpenProp, open, cookieName],\n  );\n\n  // Helper to toggle the sidebar.\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  const toggleSidebar = React.useCallback(() => {\n    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);\n  }, [isMobile, setOpen, setOpenMobile]);\n\n  // Adds a keyboard shortcut to toggle the sidebar.\n  React.useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (\n        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n        (event.metaKey || event.ctrlKey)\n      ) {\n        event.preventDefault();\n        toggleSidebar();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [toggleSidebar]);\n\n  // We add a state so that we can do data-state=\"expanded\" or \"collapsed\".\n  // This makes it easier to style the sidebar with Tailwind classes.\n  const state = open ? \"expanded\" : \"collapsed\";\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  const contextValue = React.useMemo<SidebarContextProps>(\n    () => ({\n      state,\n      open,\n      setOpen,\n      isMobile,\n      openMobile,\n      setOpenMobile,\n      toggleSidebar,\n    }),\n    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],\n  );\n\n  return (\n    <SidebarContext.Provider value={contextValue}>\n      <TooltipProvider delayDuration={0}>\n        <div\n          data-slot=\"sidebar-wrapper\"\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH,\n              \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n              ...style,\n            } as React.CSSProperties\n          }\n          className={cn(\n            \"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </div>\n      </TooltipProvider>\n    </SidebarContext.Provider>\n  );\n}\n\nfunction Sidebar({\n  side = \"left\",\n  variant = \"sidebar\",\n  collapsible = \"offcanvas\",\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  side?: \"left\" | \"right\";\n  variant?: \"sidebar\" | \"floating\" | \"inset\";\n  collapsible?: \"offcanvas\" | \"icon\" | \"none\";\n}) {\n  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();\n\n  if (collapsible === \"none\") {\n    return (\n      <div\n        data-slot=\"sidebar\"\n        className={cn(\n          \"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    );\n  }\n\n  if (isMobile) {\n    return (\n      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>\n        <SheetContent\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar\"\n          data-mobile=\"true\"\n          className=\"w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden\"\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH_MOBILE,\n            } as React.CSSProperties\n          }\n          side={side}\n        >\n          <SheetHeader className=\"sr-only\">\n            <SheetTitle>Sidebar</SheetTitle>\n            <SheetDescription>Displays the mobile sidebar.</SheetDescription>\n          </SheetHeader>\n          <div className=\"flex h-full w-full flex-col\">{children}</div>\n        </SheetContent>\n      </Sheet>\n    );\n  }\n\n  return (\n    <div\n      className=\"group peer hidden text-sidebar-foreground md:block\"\n      data-state={state}\n      data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n      data-variant={variant}\n      data-side={side}\n      data-slot=\"sidebar\"\n    >\n      {/* This is what handles the sidebar gap on desktop */}\n      <div\n        data-slot=\"sidebar-gap\"\n        className={cn(\n          \"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear\",\n          \"group-data-[collapsible=offcanvas]:w-0\",\n          \"group-data-[side=right]:rotate-180\",\n          variant === \"floating\" || variant === \"inset\"\n            ? \"group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]\"\n            : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon)\",\n        )}\n      />\n      <div\n        data-slot=\"sidebar-container\"\n        className={cn(\n          \"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex\",\n          side === \"left\"\n            ? \"left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]\"\n            : \"right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]\",\n          // Adjust the padding for floating and inset variants.\n          variant === \"floating\" || variant === \"inset\"\n            ? \"p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]\"\n            : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l\",\n          className,\n        )}\n        {...props}\n      >\n        <div\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar-inner\"\n          className=\"flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm\"\n        >\n          {children}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction SidebarTrigger({\n  className,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { toggleSidebar } = useSidebar();\n\n  return (\n    <Button\n      data-sidebar=\"trigger\"\n      data-slot=\"sidebar-trigger\"\n      variant=\"ghost\"\n      size=\"icon\"\n      className={cn(\"size-7\", className)}\n      onClick={(event) => {\n        onClick?.(event);\n        toggleSidebar();\n      }}\n      {...props}\n    >\n      <PanelLeftIcon />\n      <span className=\"sr-only\">Toggle Sidebar</span>\n    </Button>\n  );\n}\n\nfunction SidebarRail({ className, ...props }: React.ComponentProps<\"button\">) {\n  const { toggleSidebar } = useSidebar();\n\n  return (\n    <button\n      data-sidebar=\"rail\"\n      data-slot=\"sidebar-rail\"\n      aria-label=\"Toggle Sidebar\"\n      tabIndex={-1}\n      onClick={toggleSidebar}\n      title=\"Toggle Sidebar\"\n      className={cn(\n        \"-translate-x-1/2 group-data-[side=left]:-right-4 absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=right]:left-0 sm:flex\",\n        \"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize\",\n        \"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize\",\n        \"group-data-[collapsible=offcanvas]:translate-x-0 hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full\",\n        \"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2\",\n        \"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarInset({ className, ...props }: React.ComponentProps<\"main\">) {\n  return (\n    <main\n      data-slot=\"sidebar-inset\"\n      className={cn(\n        \"relative flex w-full flex-1 flex-col bg-background\",\n        \"md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarInput({\n  className,\n  ...props\n}: React.ComponentProps<typeof Input>) {\n  return (\n    <Input\n      data-slot=\"sidebar-input\"\n      data-sidebar=\"input\"\n      className={cn(\"h-8 w-full bg-background shadow-none\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-header\"\n      data-sidebar=\"header\"\n      className={cn(\"flex flex-col gap-2 p-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-footer\"\n      data-sidebar=\"footer\"\n      className={cn(\"flex flex-col gap-2 p-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      data-slot=\"sidebar-separator\"\n      data-sidebar=\"separator\"\n      className={cn(\"mx-2 w-auto bg-sidebar-border\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-content\"\n      data-sidebar=\"content\"\n      className={cn(\n        \"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-group\"\n      data-sidebar=\"group\"\n      className={cn(\"relative flex w-full min-w-0 flex-col p-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarGroupLabel({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"div\"> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"div\";\n\n  return (\n    <Comp\n      data-slot=\"sidebar-group-label\"\n      data-sidebar=\"group-label\"\n      className={cn(\n        \"flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        \"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarGroupAction({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"sidebar-group-action\"\n      data-sidebar=\"group-action\"\n      className={cn(\n        \"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        // Increases the hit area of the button on mobile.\n        \"after:-inset-2 after:absolute md:after:hidden\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarGroupContent({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-group-content\"\n      data-sidebar=\"group-content\"\n      className={cn(\"w-full text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenu({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"sidebar-menu\"\n      data-sidebar=\"menu\"\n      className={cn(\"flex w-full min-w-0 flex-col gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuItem({ className, ...props }: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"sidebar-menu-item\"\n      data-sidebar=\"menu-item\"\n      className={cn(\"group/menu-item relative\", className)}\n      {...props}\n    />\n  );\n}\n\nconst sidebarMenuButtonVariants = cva(\n  \"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n        outline:\n          \"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]\",\n      },\n      size: {\n        default: \"h-8 text-sm\",\n        sm: \"h-7 text-xs\",\n        lg: \"h-12 text-sm group-data-[collapsible=icon]:p-0!\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction SidebarMenuButton({\n  asChild = false,\n  isActive = false,\n  variant = \"default\",\n  size = \"default\",\n  tooltip,\n  className,\n  ...props\n}: React.ComponentProps<\"button\"> & {\n  asChild?: boolean;\n  isActive?: boolean;\n  tooltip?: string | React.ComponentProps<typeof TooltipContent>;\n} & VariantProps<typeof sidebarMenuButtonVariants>) {\n  const Comp = asChild ? Slot : \"button\";\n  const { isMobile, state } = useSidebar();\n\n  const button = (\n    <Comp\n      data-slot=\"sidebar-menu-button\"\n      data-sidebar=\"menu-button\"\n      data-size={size}\n      data-active={isActive}\n      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n      {...props}\n    />\n  );\n\n  if (!tooltip) {\n    return button;\n  }\n\n  if (typeof tooltip === \"string\") {\n    tooltip = {\n      children: tooltip,\n    };\n  }\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{button}</TooltipTrigger>\n      <TooltipContent\n        side=\"right\"\n        align=\"center\"\n        hidden={state !== \"collapsed\" || isMobile}\n        {...tooltip}\n      />\n    </Tooltip>\n  );\n}\n\nfunction SidebarMenuAction({\n  className,\n  asChild = false,\n  showOnHover = false,\n  ...props\n}: React.ComponentProps<\"button\"> & {\n  asChild?: boolean;\n  showOnHover?: boolean;\n}) {\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"sidebar-menu-action\"\n      data-sidebar=\"menu-action\"\n      className={cn(\n        \"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0\",\n        // Increases the hit area of the button on mobile.\n        \"after:-inset-2 after:absolute md:after:hidden\",\n        \"peer-data-[size=sm]/menu-button:top-1\",\n        \"peer-data-[size=default]/menu-button:top-1.5\",\n        \"peer-data-[size=lg]/menu-button:top-2.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        showOnHover &&\n          \"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuBadge({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-menu-badge\"\n      data-sidebar=\"menu-badge\"\n      className={cn(\n        \"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums\",\n        \"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground\",\n        \"peer-data-[size=sm]/menu-button:top-1\",\n        \"peer-data-[size=default]/menu-button:top-1.5\",\n        \"peer-data-[size=lg]/menu-button:top-2.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuSkeleton({\n  className,\n  showIcon = false,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showIcon?: boolean;\n}) {\n  // Random width between 50 to 90%.\n  const width = React.useMemo(() => {\n    return `${Math.floor(Math.random() * 40) + 50}%`;\n  }, []);\n\n  return (\n    <div\n      data-slot=\"sidebar-menu-skeleton\"\n      data-sidebar=\"menu-skeleton\"\n      className={cn(\"flex h-8 items-center gap-2 rounded-md px-2\", className)}\n      {...props}\n    >\n      {showIcon && (\n        <Skeleton\n          className=\"size-4 rounded-md\"\n          data-sidebar=\"menu-skeleton-icon\"\n        />\n      )}\n      <Skeleton\n        className=\"h-4 max-w-(--skeleton-width) flex-1\"\n        data-sidebar=\"menu-skeleton-text\"\n        style={\n          {\n            \"--skeleton-width\": width,\n          } as React.CSSProperties\n        }\n      />\n    </div>\n  );\n}\n\nfunction SidebarMenuSub({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"sidebar-menu-sub\"\n      data-sidebar=\"menu-sub\"\n      className={cn(\n        \"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border border-l px-2.5 py-0.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuSubItem({\n  className,\n  ...props\n}: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"sidebar-menu-sub-item\"\n      data-sidebar=\"menu-sub-item\"\n      className={cn(\"group/menu-sub-item relative\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuSubButton({\n  asChild = false,\n  size = \"md\",\n  isActive = false,\n  className,\n  ...props\n}: React.ComponentProps<\"a\"> & {\n  asChild?: boolean;\n  size?: \"sm\" | \"md\";\n  isActive?: boolean;\n}) {\n  const Comp = asChild ? Slot : \"a\";\n\n  return (\n    <Comp\n      data-slot=\"sidebar-menu-sub-button\"\n      data-sidebar=\"menu-sub-button\"\n      data-size={size}\n      data-active={isActive}\n      className={cn(\n        \"-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground\",\n        \"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground\",\n        size === \"sm\" && \"text-xs\",\n        size === \"md\" && \"text-sm\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupAction,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarInput,\n  SidebarInset,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuBadge,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarMenuSkeleton,\n  SidebarMenuSub,\n  SidebarMenuSubButton,\n  SidebarMenuSubItem,\n  SidebarProvider,\n  SidebarRail,\n  SidebarSeparator,\n  SidebarTrigger,\n  useSidebar,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"skeleton\"\n      className={cn(\"animate-pulse rounded-md bg-accent\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "packages/ui/src/components/ui/slider.tsx",
    "content": "\"use client\";\n\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\nimport * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Slider({\n  className,\n  defaultValue,\n  value,\n  min = 0,\n  max = 100,\n  ...props\n}: React.ComponentProps<typeof SliderPrimitive.Root>) {\n  const _values = React.useMemo(\n    () =>\n      Array.isArray(value)\n        ? value\n        : Array.isArray(defaultValue)\n          ? defaultValue\n          : [min, max],\n    [value, defaultValue, min, max],\n  );\n\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"slider\"\n      defaultValue={defaultValue}\n      value={value}\n      min={min}\n      max={max}\n      className={cn(\n        \"relative flex w-full touch-none select-none items-center data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[disabled]:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <SliderPrimitive.Track\n        data-slot=\"slider-track\"\n        className={cn(\n          \"relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1.5\",\n        )}\n      >\n        <SliderPrimitive.Range\n          data-slot=\"slider-range\"\n          className={cn(\n            \"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\",\n          )}\n        />\n      </SliderPrimitive.Track>\n      {Array.from({ length: _values.length }, (_, index) => (\n        <SliderPrimitive.Thumb\n          data-slot=\"slider-thumb\"\n          key={index}\n          className=\"block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:outline-hidden focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50\"\n        />\n      ))}\n    </SliderPrimitive.Root>\n  );\n}\n\nexport { Slider };\n"
  },
  {
    "path": "packages/ui/src/components/ui/sonner.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { Toaster as Sonner, type ToasterProps } from \"sonner\";\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme();\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group\"\n      style={\n        {\n          \"--normal-bg\": \"var(--popover)\",\n          \"--normal-text\": \"var(--popover-foreground)\",\n          \"--normal-border\": \"var(--border)\",\n        } as React.CSSProperties\n      }\n      {...props}\n    />\n  );\n};\n\nexport { Toaster };\n"
  },
  {
    "path": "packages/ui/src/components/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Switch({\n  className,\n  ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      className={cn(\n        \"peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80\",\n        className,\n      )}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        data-slot=\"switch-thumb\"\n        className={cn(\n          \"pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground\",\n        )}\n      />\n    </SwitchPrimitive.Root>\n  );\n}\n\nexport { Switch };\n"
  },
  {
    "path": "packages/ui/src/components/ui/table.tsx",
    "content": "\"use client\";\n\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n  return (\n    <div\n      data-slot=\"table-container\"\n      className=\"relative w-full overflow-x-auto\"\n    >\n      <table\n        data-slot=\"table\"\n        className={cn(\"w-full caption-bottom text-sm\", className)}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\"[&_tr]:border-b\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n  return (\n    <tbody\n      data-slot=\"table-body\"\n      className={cn(\"[&_tr:last-child]:border-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n  return (\n    <tfoot\n      data-slot=\"table-footer\"\n      className={cn(\n        \"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      className={cn(\n        \"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n  return (\n    <th\n      data-slot=\"table-head\"\n      className={cn(\n        \"h-10 whitespace-nowrap px-2 text-left align-middle font-medium text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        \"whitespace-nowrap p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCaption({\n  className,\n  ...props\n}: React.ComponentProps<\"caption\">) {\n  return (\n    <caption\n      data-slot=\"table-caption\"\n      className={cn(\"mt-4 text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "packages/ui/src/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Tabs({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      className={cn(\"flex flex-col gap-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TabsList({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      className={cn(\n        \"inline-flex h-9 w-fit items-center justify-center rounded-lg bg-muted p-[3px] text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 font-medium text-foreground text-sm transition-[color,box-shadow] focus-visible:border-ring focus-visible:outline-1 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:shadow-sm dark:text-muted-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "packages/ui/src/components/ui/textarea.tsx",
    "content": "import type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"field-sizing-content flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Textarea };\n"
  },
  {
    "path": "packages/ui/src/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@openstatus/ui/lib/utils\";\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  );\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return (\n    <TooltipProvider>\n      <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n    </TooltipProvider>\n  );\n}\n\nfunction TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />;\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in text-balance rounded-md bg-primary px-3 py-1.5 text-primary-foreground text-xs data-[state=closed]:animate-out\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary\" />\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "packages/ui/src/globals.css",
    "content": "@import \"tailwindcss\";\n\n@source \"../**/*.{ts,tsx}\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n  --color-sidebar: var(--sidebar);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-ring: var(--sidebar-ring);\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n\n  --color-success: var(--success);\n  --color-warning: var(--warning);\n  --color-info: var(--info);\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.145 0 0);\n  --sidebar-primary: oklch(0.205 0 0);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.97 0 0);\n  --sidebar-accent-foreground: oklch(0.205 0 0);\n  --sidebar-border: oklch(0.922 0 0);\n  --sidebar-ring: oklch(0.708 0 0);\n\n  --success: oklch(0.72 0.19 150);\n  --warning: oklch(0.77 0.16 70);\n  --info: oklch(0.62 0.19 260);\n\n  --rainbow-1: oklch(0.64 0.21 25);\n  --rainbow-2: oklch(0.70 0.19 48);\n  --rainbow-3: oklch(0.77 0.16 70);\n  --rainbow-4: oklch(0.80 0.16 86);\n  --rainbow-5: oklch(0.77 0.20 131);\n  --rainbow-6: oklch(0.72 0.19 150);\n  --rainbow-7: oklch(0.70 0.15 162);\n  --rainbow-8: oklch(0.70 0.12 183);\n  --rainbow-9: oklch(0.71 0.13 215);\n  --rainbow-10: oklch(0.68 0.15 237);\n  --rainbow-11: oklch(0.62 0.19 260);\n  --rainbow-12: oklch(0.59 0.20 277);\n  --rainbow-13: oklch(0.61 0.22 293);\n  --rainbow-14: oklch(0.63 0.23 304);\n  --rainbow-15: oklch(0.67 0.26 322);\n  --rainbow-16: oklch(0.66 0.21 354);\n  --rainbow-17: oklch(0.65 0.22 16);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: oklch(0.205 0 0);\n  --sidebar-foreground: oklch(0.985 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.269 0 0);\n  --sidebar-accent-foreground: oklch(0.985 0 0);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.556 0 0);\n\n  --success: oklch(0.72 0.19 150);\n  --warning: oklch(0.77 0.16 70);\n  --info: oklch(0.62 0.19 260);\n\n  --rainbow-1: oklch(0.64 0.21 25);\n  --rainbow-2: oklch(0.70 0.19 48);\n  --rainbow-3: oklch(0.77 0.16 70);\n  --rainbow-4: oklch(0.80 0.16 86);\n  --rainbow-5: oklch(0.77 0.20 131);\n  --rainbow-6: oklch(0.72 0.19 150);\n  --rainbow-7: oklch(0.70 0.15 162);\n  --rainbow-8: oklch(0.70 0.12 183);\n  --rainbow-9: oklch(0.71 0.13 215);\n  --rainbow-10: oklch(0.68 0.15 237);\n  --rainbow-11: oklch(0.62 0.19 260);\n  --rainbow-12: oklch(0.59 0.20 277);\n  --rainbow-13: oklch(0.61 0.22 293);\n  --rainbow-14: oklch(0.63 0.23 304);\n  --rainbow-15: oklch(0.67 0.26 322);\n  --rainbow-16: oklch(0.66 0.21 354);\n  --rainbow-17: oklch(0.65 0.22 16);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "packages/ui/src/hooks/use-cookie-state.ts",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\n\nexport function useCookieState<T extends string>(\n  name: string,\n  defaultValue?: T,\n  config?: { expires?: number },\n) {\n  const [state, setState] = useState<T>();\n\n  const handleChange = useCallback(\n    (value: T) => {\n      if (document) {\n        const date = new Date();\n        date.setTime(date.getTime() + 365 * 24 * 60 * 60 * 1000); // in one year\n        document.cookie = `${name}=${value}; path=/; expires=${\n          config?.expires ?? date.toUTCString()\n        }`;\n        setState(value);\n      }\n    },\n    [name, config],\n  );\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  useEffect(() => {\n    if (document) {\n      const cookie = document.cookie\n        .split(\"; \")\n        .find((row) => row.startsWith(name));\n      if (!cookie) {\n        setState(defaultValue);\n        return;\n      }\n      const value = cookie.split(\"=\")[1] as T;\n      setState(value);\n    }\n  }, []);\n\n  return [state, handleChange] as const;\n}\n"
  },
  {
    "path": "packages/ui/src/hooks/use-copy-to-clipboard.ts",
    "content": "\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nexport function useCopyToClipboard() {\n  const [text, setText] = useState<string | null>(null);\n\n  const copy = useCallback(\n    async (\n      text: string,\n      {\n        timeout = 3000,\n        withToast = false,\n        successMessage = \"Copied to clipboard\",\n      }: {\n        timeout?: number;\n        withToast?: boolean | string;\n        successMessage?: string;\n      },\n    ) => {\n      if (!navigator?.clipboard) {\n        console.warn(\"Clipboard not supported\");\n        return false;\n      }\n\n      try {\n        await navigator.clipboard.writeText(text);\n        setText(text);\n\n        if (timeout) {\n          setTimeout(() => {\n            setText(null);\n          }, timeout);\n        }\n\n        if (withToast) {\n          if (typeof withToast === \"string\") {\n            toast.success(withToast);\n          } else {\n            toast.success(successMessage);\n          }\n        }\n\n        return true;\n      } catch (error) {\n        console.warn(\"Copy failed\", error);\n        setText(null);\n        return false;\n      }\n    },\n    [],\n  );\n\n  return { text, copy, isCopied: text !== null };\n}\n"
  },
  {
    "path": "packages/ui/src/hooks/use-debounce-callback.ts",
    "content": "import { useCallback, useEffect, useRef } from \"react\";\n\nexport function useDebounceCallback<Args extends unknown[]>(\n  callback: (...args: Args) => void,\n  delay = 500,\n) {\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const debounced = useCallback(\n    (...args: Args) => {\n      if (timerRef.current) {\n        clearTimeout(timerRef.current);\n      }\n\n      timerRef.current = setTimeout(() => {\n        callback(...args);\n      }, delay);\n    },\n    [callback, delay],\n  );\n\n  useEffect(() => {\n    return () => {\n      if (timerRef.current) clearTimeout(timerRef.current);\n    };\n  }, []);\n\n  return debounced;\n}\n"
  },
  {
    "path": "packages/ui/src/hooks/use-debounce.ts",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\n// consider using https://github.com/xnimorz/use-debounce\nexport function useDebounce<T>(value: T, delay?: number): T {\n  const [debouncedValue, setDebouncedValue] = React.useState<T>(value);\n\n  React.useEffect(() => {\n    const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500);\n\n    return () => {\n      clearTimeout(timer);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}\n"
  },
  {
    "path": "packages/ui/src/hooks/use-media-query.ts",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\ntype MediaQuery = string | number;\n\nexport function useMediaQuery(query: MediaQuery): boolean {\n  const [matches, setMatches] = useState<boolean>(false);\n\n  useEffect(() => {\n    const mediaQueryList = window.matchMedia(String(query));\n\n    const handleChange = (event: MediaQueryListEvent) => {\n      setMatches(event.matches);\n    };\n\n    // Initial check\n    setMatches(mediaQueryList.matches);\n\n    // Add listener for changes\n    mediaQueryList.addEventListener(\"change\", handleChange);\n\n    // Clean up listener on unmount\n    return () => {\n      mediaQueryList.removeEventListener(\"change\", handleChange);\n    };\n  }, [query]);\n\n  return matches;\n}\n"
  },
  {
    "path": "packages/ui/src/hooks/use-mobile.ts",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile() {\n  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(\n    undefined,\n  );\n\n  React.useEffect(() => {\n    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n    const onChange = () => {\n      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n    };\n    mql.addEventListener(\"change\", onChange);\n    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n    return () => mql.removeEventListener(\"change\", onChange);\n  }, []);\n\n  return !!isMobile;\n}\n"
  },
  {
    "path": "packages/ui/src/lib/compose-refs.ts",
    "content": "// @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/composeRefs.tsx\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    ref(value);\n  } else if (ref !== null && ref !== undefined) {\n    (ref as React.MutableRefObject<T>).current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]) {\n  // biome-ignore lint/complexity/noForEach: <explanation>\n  return (node: T) => refs.forEach((ref) => setRef(ref, node));\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]) {\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n"
  },
  {
    "path": "packages/ui/src/lib/utils.ts",
    "content": "import type { ClassValue } from \"clsx\";\nimport { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "packages/ui/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/react-library.json\",\n  \"include\": [\".\"],\n  \"exclude\": [\"dist\", \"build\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"paths\": {\n      \"@openstatus/ui/*\": [\"./src/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "packages/upstash/package.json",
    "content": "{\n  \"name\": \"@openstatus/upstash\",\n  \"version\": \"0.0.0\",\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.ts\",\n  \"dependencies\": {\n    \"@upstash/qstash\": \"2.6.2\",\n    \"@upstash/redis\": \"1.22.1\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"@types/node\": \"22.10.2\",\n    \"tsup\": \"7.2.0\",\n    \"typescript\": \"5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/upstash/src/index.ts",
    "content": "export * from \"./redis/client\";\nexport * from \"@upstash/redis\";\n"
  },
  {
    "path": "packages/upstash/src/redis/client.ts",
    "content": "import { Redis } from \"@upstash/redis\";\n\n// TO BE TESTED\nexport const redis = Redis.fromEnv();\n"
  },
  {
    "path": "packages/upstash/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"moduleResolution\": \"node\",\n    \"preserveWatchOutput\": true,\n    \"skipLibCheck\": true,\n    \"noEmit\": true,\n    \"strict\": true\n  }\n}\n"
  },
  {
    "path": "packages/utils/index.ts",
    "content": "import { base } from \"@openstatus/assertions\";\nimport { monitorMethods, monitorStatus } from \"@openstatus/db/src/schema\";\n\nimport { z } from \"zod\";\n\nexport const httpPayloadSchema = z.object({\n  workspaceId: z.string(),\n  monitorId: z.string(),\n  method: z.enum(monitorMethods),\n  body: z.string().optional(),\n  headers: z.array(z.object({ key: z.string(), value: z.string() })).optional(),\n  url: z.string(),\n  cronTimestamp: z.number(),\n  status: z.enum(monitorStatus),\n  assertions: z.array(base).nullable(),\n  timeout: z.number().prefault(45000),\n  degradedAfter: z.number().nullable(),\n  trigger: z.enum([\"cron\", \"api\"]).optional().nullable().prefault(\"cron\"),\n  otelConfig: z\n    .object({\n      endpoint: z.string(),\n      headers: z.record(z.string(), z.string()),\n    })\n    .optional(),\n  retry: z.number().prefault(3),\n  followRedirects: z.boolean().prefault(true),\n});\n\nexport type HttpPayload = z.infer<typeof httpPayloadSchema>;\n\nexport const tpcPayloadSchema = z.object({\n  status: z.enum(monitorStatus),\n  workspaceId: z.string(),\n  uri: z.string(),\n  monitorId: z.string(),\n  assertions: z.array(base).nullable(),\n  cronTimestamp: z.number(),\n  timeout: z.number().prefault(45000),\n  degradedAfter: z.number().nullable(),\n  trigger: z.enum([\"cron\", \"api\"]).optional().nullable().prefault(\"cron\"),\n  otelConfig: z\n    .object({\n      endpoint: z.string(),\n      headers: z.record(z.string(), z.string()),\n    })\n    .optional(),\n  retry: z.number().prefault(3),\n});\n\nexport type TcpPayload = z.infer<typeof tpcPayloadSchema>;\n\nexport const DNSPayloadSchema = z.object({\n  status: z.enum(monitorStatus),\n  workspaceId: z.string(),\n  uri: z.string(),\n  monitorId: z.string(),\n  assertions: z.array(base).nullable(),\n  cronTimestamp: z.number(),\n  timeout: z.number().prefault(45000),\n  degradedAfter: z.number().nullable(),\n  trigger: z.enum([\"cron\", \"api\"]).optional().nullable().prefault(\"cron\"),\n  otelConfig: z\n    .object({\n      endpoint: z.string(),\n      headers: z.record(z.string(), z.string()),\n    })\n    .optional(),\n  retry: z.number().prefault(3),\n});\n\nexport type DNSPayload = z.infer<typeof DNSPayloadSchema>;\n\nexport function transformHeaders(headers: { key: string; value: string }[]) {\n  return headers.length > 0\n    ? headers.reduce(\n        (acc, curr) => {\n          acc[curr.key] = curr.value;\n          return acc;\n        },\n        {} as Record<string, string>,\n      )\n    : {};\n}\n"
  },
  {
    "path": "packages/utils/package.json",
    "content": "{\n  \"name\": \"@openstatus/utils\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.ts\",\n  \"scripts\": {},\n  \"dependencies\": {\n    \"@openstatus/assertions\": \"workspace:*\",\n    \"@openstatus/db\": \"workspace:*\",\n    \"@openstatus/regions\": \"workspace:*\",\n    \"zod\": \"4.1.13\"\n  },\n  \"devDependencies\": {\n    \"@openstatus/tsconfig\": \"workspace:*\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/utils/tsconfig.json",
    "content": "{\n  \"extends\": \"@openstatus/tsconfig/base.json\",\n  \"include\": [\"src\", \"*.ts\"]\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"apps/*\"\n  - \"packages/**/*\"\n  - \"packages/config/*\"\n"
  },
  {
    "path": "process-compose.yaml",
    "content": "version: \"0.5\"\nis_strict: true\nprocesses:\n  init_dependencies:\n    namespace: init\n    command: |\n      pnpm install\n    is_tty: true\n  init_turso:\n    namespace: init\n    command: |\n      turso dev --db-file openstatus-dev.db\n    is_tty: true\n    availability:\n      restart: \"no\"\n  init_dx:\n    namespace: init\n    command: |\n      pnpm dx\n      kill $(pgrep -f \"turso dev\")\n    is_tty: true\n    depends_on:\n      init_dependencies:\n        condition: process_completed_successfully\n      init_turso:\n        condition: process_started\n  dev:\n    namespace: dev\n    command: |\n      pnpm dev:web\n    is_tty: true\n    depends_on:\n      init_dx:\n        condition: process_completed_successfully\n"
  },
  {
    "path": "ralph/.gitignore",
    "content": "# Ignore PRD and progress files as they are specific to local development\nprd.json\nprogress.txt\n\n"
  },
  {
    "path": "ralph/README.md",
    "content": "Ralph is a technique for running AI coding agents in a loop. Our approach is taken from Matt Pocock's [Getting Started with Ralph](https://www.aihero.dev/getting-started-with-ralph) writeup. Make sure to have everything installed.\n\nThe `prd.json` file is an array of object with the following format:\n\n```\n{\n  \"category\": \"functional\",\n  \"description\": \"When a user is on wrong dashboard /status-pages/[id] redirect him to /status-pages\",\n  \"steps\": [\n    \"Redirect user no access for page id\",\n    \"Avoid throwing an error\",\n  ],\n  \"passes\": false\n}\n```\n\n- category: \"functional\" | \"ui\" or other categories\n- description: define what you are building\n- steps: break the task down into multiple smaller steps\n- passes: determines whether or not all defined steps and tests have succeed or not and makes it easier to track progress\n\nThe `progress.txt` file simply keeps track of the changes and implementation decisions.\n\nYou can run Ralph with a human-in-the-loop by running:\n\n```\n./ralph-once.sh\n```\n\nOr in AFK mode within the sandbox environment by specifying the iteration number with:\n\n```\n./afk-raph.sh 10\n```"
  },
  {
    "path": "ralph/afk-ralph.sh",
    "content": "#!/bin/bash\nset -e\n\nif [ -z \"$1\" ]; then\n  echo \"Usage: $0 <iterations>\"\n  exit 1\nfi\n\nfor ((i=1; i<=$1; i++)); do\n  result=$(docker sandbox run claude --permission-mode acceptEdits -p \"@prd.json @progress.txt \\\n  1. Find the highest-priority task and implement it. \\\n  2. Run your tests and type checks. \\\n  3. Update the PRD with what was done. \\\n  4. Append your progress to progress.txt. \\\n  5. Commit your changes. \\\n  ONLY WORK ON A SINGLE TASK. \\\n  If the PRD is complete, output <promise>COMPLETE</promise>.\")\n\n  echo \"$result\"\n\n  if [[ \"$result\" == *\"<promise>COMPLETE</promise>\"* ]]; then\n    echo \"PRD complete after $i iterations.\"\n    exit 0\n  fi\ndone"
  },
  {
    "path": "ralph/ralph-once.sh",
    "content": "#!/bin/bash\n\nclaude --permission-mode acceptEdits \"@prd.json @progress.txt \\\n1. Read the PRD and progress file. \\\n2. Find the next incomplete task and implement it. \\\n3. Commit your changes. \\\n4. Update progress.txt with what you did. \\\nONLY DO ONE TASK AT A TIME.\""
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"globalDependencies\": [\"**/.env.*local\"],\n  \"tasks\": {\n    \"registry:build\": {\n      \"outputs\": [\"public/r/**\"]\n    },\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"env\": [\n        \"\\\\*\",\n        \"RESEND_API_KEY\",\n        \"\\\\!NEXT_PUBLIC_GIT_\\\\*\",\n        \"DATABASE_URL\",\n        \"DATABASE_AUTH_TOKEN\",\n        \"TINY_BIRD_API_KEY\",\n        \"NODE_ENV\",\n        \"BLOB_READ_WRITE_TOKEN\",\n        \"TEAM_ID_VERCEL\",\n        \"PROJECT_ID_VERCEL\",\n        \"STRIPE_SECRET_KEY\",\n        \"UNKEY_TOKEN\",\n        \"UNKEY_API_ID\",\n        \"UPSTASH_REDIS_REST_URL\",\n        \"UPSTASH_REDIS_REST_TOKEN\",\n        \"OPENPANEL_CLIENT_SECRET\",\n        \"NEXT_PUBLIC_OPENPANEL_CLIENT_ID\",\n        \"AUTH_SECRET\",\n        \"CRON_SECRET\",\n        \"SELF_HOST\"\n      ],\n      \"outputs\": [\".next/**\", \"!.next/cache/**\", \"dist/**\"]\n    },\n    \"lint\": {},\n    \"dev\": {\n      \"cache\": false\n    },\n    \"@openstatus/web#dev\": {\n      \"cache\": false,\n      \"dependsOn\": [\"@openstatus/react#build\"]\n    },\n    \"@openstatus/status-page#dev\": {\n      \"cache\": false\n    },\n    \"@openstatus/dashboard#dev\": {\n      \"cache\": false\n    },\n    \"test\": {\n      \"cache\": false\n    },\n    \"dx\": {\n      \"cache\": false,\n      \"dependsOn\": [\"seed\"]\n    },\n    \"seed\": {\n      \"cache\": false,\n      \"dependsOn\": [\"migrate\"]\n    },\n    \"migrate\": {\n      \"cache\": false,\n      \"dependsOn\": [\"env\"]\n    },\n    \"env\": {\n      \"cache\": true\n    }\n  }\n}\n"
  },
  {
    "path": "utils/api-bruno/Monitor Summary.bru",
    "content": "meta {\n  name: Monitor Summary\n  type: http\n  seq: 3\n}\n\nget {\n  url: {{url}}/v1/monitor/1/summary\n  body: none\n  auth: none\n}\n\nheaders {\n  x-openstatus-key: 1\n}\n"
  },
  {
    "path": "utils/api-bruno/OpenApi.bru",
    "content": "meta {\n  name: OpenApi\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{url}}/v1/openapi\n  body: none\n  auth: none\n}\n"
  },
  {
    "path": "utils/api-bruno/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"OpenStatus\",\n  \"type\": \"collection\"\n}\n"
  },
  {
    "path": "utils/api-bruno/checker.bru",
    "content": "meta {\n  name: checker\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{url}}/checker\n  body: text\n  auth: none\n}\n\nheaders {\n  Authorization: Basic test\n  X-CloudTasks-TaskRetryCount: 1\n  Content-Type: application/json\n}\n\nbody:json {\n  {\n    \"workspaceId\": 1,\n    \"monitorId\": 1,\n    \"method\": \"GET\",\n    \"url\":\"https://openstat.us/404\",\n    \"status\": \"active\",\n    \"cronTimestamp\":1699088595307\n    \"pageId\":1\n  }\n}\n\nbody:text {\n  {\n    \"workspaceId\": \"1\",\n    \"monitorId\": \"1\",\n    \"method\": \"GET\",\n    \"url\":\"https://openstat.us/404\",\n    \"status\": \"active\",\n    \"cronTimestamp\":1699088595307,\n    \"pageId\":1\n  }\n}\n"
  },
  {
    "path": "utils/api-bruno/environments/local.bru",
    "content": "vars {\n  url: http://localhost:3000\n}\n"
  },
  {
    "path": "utils/api-bruno/environments/prod.bru",
    "content": "vars {\n  url: https://api.openstatus.dev\n}\n"
  },
  {
    "path": "utils/api-bruno/incident_update/Get Status Report Update.bru",
    "content": "meta {\n  name: Get Status Report Update\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{url}}/v1/status_report_update/{{updateId}}\n  body: none\n  auth: none\n}\n\nheaders {\n  x-openstatus-key: api-key\n}\n\nvars:pre-request {\n  updateId: 32\n}\n"
  },
  {
    "path": "utils/api-bruno/incidents/All Status Reports.bru",
    "content": "meta {\n  name: All  Status Reports\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{url}}/v1/status_report\n  body: none\n  auth: none\n}\n\nheaders {\n  x-openstatus-key: api-key\n}\n"
  },
  {
    "path": "utils/api-bruno/incidents/Get Status Report.bru",
    "content": "meta {\n  name: Get on status report\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{url}}/v1/status_report/{{id}}\n  body: none\n  auth: none\n}\n\nheaders {\n  x-openstatus-key: api-key\n}\n\nvars:pre-request {\n  id: 31\n}\n"
  }
]